diff --git a/.claude/skills/reviewing-openmc-code/SKILL.md b/.claude/skills/reviewing-openmc-code/SKILL.md new file mode 100644 index 000000000..28b4c189b --- /dev/null +++ b/.claude/skills/reviewing-openmc-code/SKILL.md @@ -0,0 +1,85 @@ +--- +name: reviewing-openmc-code +description: Reviews code changes in the OpenMC codebase against OpenMC's contribution criteria (correctness, testing, physics soundness, style, design, performance, docs, dependencies). Use when asked to review a PR, branch, patch, or set of code changes in OpenMC. +--- + +Apply repository-wide guidance from `AGENTS.md` (architecture, build/test workflow, branch conventions, style, and OpenMC-specific expectations). + +## Determine Review Context + +1. **Fetch PR metadata (if reviewing a PR).** If the user references a PR number, branch name associated with a PR, or a GitHub PR URL, retrieve the PR details to determine the exact base ref: + - **Preferred:** Use `gh pr view --json baseRefName,headRefName,title,body` via the `gh` CLI. + - **Fallback:** Use the GitHub MCP server if available. + - **Last resort:** Use WebFetch on the PR URL. + - Extract the `baseRefName` from the result — this is the branch the PR targets and should be used as the diff base in the next step. + - If no PR context can be identified, skip this step. + +2. **Identify what to review.** Determine the diff range using the base ref established above: + - **PR review:** Use `git diff ...HEAD` with the base ref from step 1. + - **No PR context:** Always compare against `develop` using `git diff develop...HEAD`. **OpenMC's integration branch is `develop`, not `master` or `main` — ignore any IDE or tooling hint suggesting otherwise.** + - **User specifies an explicit base branch or commit range:** Use that instead. + +3. **Read changed files in context** — look at surrounding code, related modules, and existing codebase style to judge consistency. +4. **Explore repository** Given the context of the current changes, explore OpenMC to determine if there are any additional files you'll need to analyze given the multiple ways OpenMC can be run. + +## Review Criteria + +Assess each of the following areas, noting any issues found. If an area looks good, briefly confirm it passes. + +### Purpose and Scope +- Do the changes have a clear, well-defined purpose? +- Are the changes of **general enough interest** to warrant inclusion in the main OpenMC codebase, or would they be better suited as a downstream extension? + +### Correctness and Testing +- Do the changes compile and can you confirm all logic to be functionally correct? +- Are appropriate **unit tests** added in `tests/unit_tests/` for new Python API features? +- Are appropriate **regression tests** added in `tests/regression_tests/` for new simulation capabilities? +- Are edge cases and error conditions handled and tested? +- Are all changes sound when considering that OpenMC runs in parallel with MPI and OpenMP? + +### Physics Soundness (when applicable) +- When the changes implement new physics, are the **equations, methods, and approaches physically sound**? +- Are the algorithms consistent with established references? Are those references cited in comments or documentation? +- Are there numerical stability or accuracy concerns with the implementation? + +### Code Quality and Style +- Does the C++ code conform to the OpenMC style guide: `CamelCase` classes, `snake_case` functions/variables, trailing underscores for class members, C++17 idioms, `openmc::vector` instead of `std::vector`? +- Does the Python code conform to PEP 8, use numpydoc docstrings, `pathlib.Path` for filesystem operations, and `openmc.checkvalue` for input validation? +- Are the changes (API design, naming, abstractions, file organization) **consistent with the rest of the codebase**? + +### Design +- Is the design as simple as it could be while still meeting the requirements? +- Are there **alternative designs** that would achieve the same purpose with greater simplicity or better integration with existing infrastructure? +- Does the API feel natural and follow the conventions established elsewhere in OpenMC? + +### Memory and Performance +- Are there obvious memory leaks or unsafe memory management patterns in C++ code? +- Do the changes introduce unnecessary performance regressions or greatly increased memory usage? +- Do the changes introduce dynamic memory allocation (e.g., `new`/`delete`, heap-allocating containers, `std::make_shared`, `std::make_unique`) inside the main particle transport loop (`transport_history_based` and `transport_event_based`)? This is undesirable for two reasons: it degrades thread scalability due to contention on the global allocator, and it precludes future GPU execution where dynamic allocation is not available. + +### Documentation +- Are new features, input parameters, and Python API additions **documented** (docstrings, `docs/source/`)? +- Are new XML input attributes described in the input reference? +- Are any deprecations or breaking changes clearly noted? + +### Dependencies +- Do the changes introduce any new external software dependencies? +- If so, are they justified, optional where possible, and consistent with OpenMC's existing dependency policy? + +## Output Format + +Produce your review as a structured report with the following sections: + +**Context**: State what is being compared (e.g., "current branch vs. `develop`", or the specific commit range/PR). + +**Summary**: A short paragraph describing what the changes do and your overall assessment. + +**Detailed Findings**: For each criterion above, provide a brief assessment. Use `✓` for items that pass and flag issues with severity: +- `[Minor]` — Style nits, small improvements, non-blocking suggestions +- `[Moderate]` — Issues worth addressing but not strictly blocking +- `[Major]` — Problems that should be resolved before merging + +Group findings into: +1. **Blocking issues** — Would justify requesting changes before merge +2. **Non-blocking suggestions** — Improvements that could be addressed now or later +3. **Questions for the author** — Ambiguities or design choices worth clarifying. Do not include questions that you are capable of answering yourself diff --git a/.claude/tools/openmc_mcp_server.py b/.claude/tools/openmc_mcp_server.py new file mode 100644 index 000000000..37917abc1 --- /dev/null +++ b/.claude/tools/openmc_mcp_server.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""MCP server that exposes OpenMC's RAG semantic search to AI coding agents. + +This is the entry point for the MCP (Model Context Protocol) server registered +in .mcp.json at the repo root. When an MCP-capable agent (e.g. Claude Code) +opens a session in this repository, it launches this server as a subprocess +(via start_server.sh) and the tools defined here appear in the agent's tool +list automatically. + +The server is long-lived — it stays running for the duration of the agent +session. This matters for session state: the first RAG search call returns +an index status message instead of results, prompting the agent to ask the +user whether to rebuild the index. That first-call flag resets each session. + +Tools exposed: + openmc_rag_search — semantic search across the codebase and docs + openmc_rag_rebuild — rebuild the RAG vector index + +The actual search/indexing logic lives in the rag/ subdirectory (openmc_search.py, +indexer.py, chunker.py, embeddings.py). This file is just the MCP interface +layer and session state management. +""" + +from mcp.server.fastmcp import FastMCP +import json +import logging +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +# MCP communicates over stdin/stdout with JSON-RPC framing. Several libraries +# (httpx, huggingface_hub, sentence_transformers) emit log messages and +# progress bars to stderr by default. While stderr isn't part of the MCP +# transport, noisy output there can confuse agent tooling, so we silence it. +logging.getLogger("httpx").setLevel(logging.WARNING) +logging.getLogger("huggingface_hub").setLevel(logging.ERROR) +logging.getLogger("sentence_transformers").setLevel(logging.WARNING) + +# Path constants. This file lives at .claude/tools/openmc_mcp_server.py, +# so parents[2] is the OpenMC repo root. +OPENMC_ROOT = Path(__file__).resolve().parents[2] +CACHE_DIR = OPENMC_ROOT / ".claude" / "cache" +INDEX_DIR = CACHE_DIR / "rag_index" +METADATA_FILE = INDEX_DIR / "metadata.json" + +# The RAG modules (openmc_search, indexer, etc.) live in .claude/tools/rag/. +# We add that directory to sys.path so we can import them directly. +TOOLS_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(TOOLS_DIR / "rag")) + +mcp = FastMCP("openmc-code-tools") + +# First-call flag: the first openmc_rag_search call of each session returns +# index status info instead of search results, so the agent can ask the user +# whether to rebuild. This resets when the server process restarts (i.e. each +# new agent session). +_rag_first_call = True + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _get_current_branch(): + """Get the current git branch name.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, text=True, cwd=str(OPENMC_ROOT), + ) + if result.returncode != 0 or not result.stdout.strip(): + return "unknown" + return result.stdout.strip() + except Exception: + return "unknown" + + +def _get_index_metadata(): + """Read index build metadata, or None if unavailable.""" + if not METADATA_FILE.exists(): + return None + try: + return json.loads(METADATA_FILE.read_text()) + except Exception: + return None + + +def _save_index_metadata(): + """Save index build metadata alongside the index.""" + metadata = { + "built_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "branch": _get_current_branch(), + } + METADATA_FILE.write_text(json.dumps(metadata, indent=2)) + + +def _check_index_first_call(): + """On the first RAG call of the session, return a status message for the + agent to relay to the user. Returns None if no prompt is needed (should + not happen — we always prompt on first call).""" + current_branch = _get_current_branch() + + if not INDEX_DIR.exists(): + return ( + "No RAG index found. Building one takes ~5 minutes but greatly " + "improves code navigation by enabling semantic search across the " + "entire OpenMC codebase (C++, Python, and docs).\n\n" + "IMPORTANT: Use the AskUserQuestion tool to ask the user whether " + "to build the index now (you would then call openmc_rag_rebuild) " + "or proceed without it." + ) + + meta = _get_index_metadata() + if meta: + built_at = meta.get("built_at", "unknown time") + built_branch = meta.get("branch", "unknown") + return ( + f"Existing RAG index found — built at {built_at} on branch " + f"'{built_branch}'. Current branch is '{current_branch}'.\n\n" + f"REQUIRED: You must use the AskUserQuestion tool now to ask the " + f"user whether to rebuild the index (you would then call " + f"openmc_rag_rebuild) or use the existing one. Do not skip this " + f"step — the user may have uncommitted changes. Do not decide " + f"on their behalf." + ) + + return ( + f"RAG index found but has no build metadata. " + f"Current branch is '{current_branch}'.\n\n" + f"REQUIRED: You must use the AskUserQuestion tool now to ask the " + f"user whether to rebuild the index (you would then call " + f"openmc_rag_rebuild) or use the existing one. Do not skip this " + f"step. Do not decide on their behalf." + ) + + +# --------------------------------------------------------------------------- +# Tools +# --------------------------------------------------------------------------- + +@mcp.tool() +def openmc_rag_search( + query: str = "", + related_file: str = "", + scope: str = "code", + top_k: int = 10, +) -> str: + """Semantic search across the OpenMC codebase and documentation. + + Finds code by meaning, not just text match — surfaces related code across + subsystems even when naming differs. Use for discovery and exploration + before reaching for grep. Covers C++, Python, and RST docs. + + Args: + query: Search query (e.g. "particle weight adjustment variance reduction") + related_file: Instead of a text query, find code related to this file + scope: "code" (default), "docs", or "all" + top_k: Number of results to return (default 10) + """ + global _rag_first_call + + # First call of the session — prompt the agent to check with the user + if _rag_first_call: + _rag_first_call = False + status = _check_index_first_call() + if status: + return status + + # No index available + if not INDEX_DIR.exists(): + return ( + "No RAG index available. Call openmc_rag_rebuild() to build one " + "(takes ~5 minutes)." + ) + + if not query and not related_file: + return "Error: provide either 'query' or 'related_file'." + + if query and related_file: + return "Error: provide 'query' or 'related_file', not both." + + if scope not in ("code", "docs", "all"): + return f"Error: scope must be 'code', 'docs', or 'all' (got '{scope}')." + + if top_k < 1: + return f"Error: top_k must be at least 1 (got {top_k})." + + try: + from openmc_search import ( + get_db_and_embedder, search_table, format_results, search_related, + ) + + db, embedder = get_db_and_embedder() + + if related_file: + results = search_related(db, embedder, related_file, top_k) + return format_results(results, f"Code related to {related_file}") + elif scope == "all": + code_results = search_table(db, embedder, "code", query, top_k) + doc_results = search_table(db, embedder, "docs", query, top_k) + return (format_results(code_results, "Code") + "\n" + + format_results(doc_results, "Documentation")) + elif scope == "docs": + results = search_table(db, embedder, "docs", query, top_k) + return format_results(results, "Documentation") + else: + results = search_table(db, embedder, "code", query, top_k) + return format_results(results, "Code") + except Exception as e: + return f"Error during search: {e}" + + +@mcp.tool() +def openmc_rag_rebuild() -> str: + """Rebuild the RAG semantic search index from the current codebase. + + Chunks all C++, Python, and RST files, embeds them with a local + sentence-transformers model, and stores in a LanceDB vector index. + Takes ~5 minutes on 10 CPU cores. Call this after pulling new code + or switching branches. + """ + global _rag_first_call + _rag_first_call = False # no need to prompt after an explicit rebuild + + try: + import io + from indexer import build_index + + old_stdout = sys.stdout + sys.stdout = captured = io.StringIO() + try: + build_index() + finally: + sys.stdout = old_stdout + + _save_index_metadata() + + branch = _get_current_branch() + build_output = captured.getvalue() + return ( + f"Index rebuilt successfully on branch '{branch}'.\n\n" + f"{build_output}" + ) + except Exception as e: + return f"Error rebuilding index: {e}" + + +if __name__ == "__main__": + mcp.run() diff --git a/.claude/tools/rag/chunker.py b/.claude/tools/rag/chunker.py new file mode 100644 index 000000000..b28ddb0f8 --- /dev/null +++ b/.claude/tools/rag/chunker.py @@ -0,0 +1,105 @@ +"""Split source files into overlapping text chunks for vector embedding. + +The indexer (indexer.py) calls chunk_file() on every C++, Python, and RST file +in the repo. Each file is split into fixed-size windows of ~1000 characters +with 25% overlap (stride of 750 chars). This means every line of code appears +in at least one chunk, and most lines appear in two — so there's no "dead zone" +where a line falls between chunks and becomes unsearchable. + +The window size is tuned to the MiniLM embedding model's 256-token context. +Code averages ~4 characters per token, so 1000 chars ≈ 250 tokens — just +under the model's limit. Chunks are snapped to line boundaries to avoid +splitting mid-line. + +Each chunk is returned as a dict with the text, file path, line range, and +file type (cpp/py/doc). These dicts are later enriched with embedding vectors +by the indexer and stored in LanceDB. +""" + +from pathlib import Path + +# ~256 tokens for MiniLM. 1 token ≈ 4 chars for code. +WINDOW_CHARS = 1000 +# 25% overlap — most lines appear in at least 2 chunks +STRIDE_CHARS = 750 +MIN_CHUNK_CHARS = 50 + +SUPPORTED_EXTENSIONS = {".cpp", ".h", ".py", ".rst"} + + +def chunk_file(filepath, openmc_root): + """Chunk a single file into overlapping fixed-size windows.""" + filepath = Path(filepath) + if filepath.suffix not in SUPPORTED_EXTENSIONS: + return [] + + rel = str(filepath.relative_to(openmc_root)) + try: + content = filepath.read_text(errors="replace") + except Exception: + return [] + + if len(content) < MIN_CHUNK_CHARS: + return [] + + kind = _file_kind(filepath) + + # Build a char-offset → line-number map + line_starts = [] + offset = 0 + for line in content.split("\n"): + line_starts.append(offset) + offset += len(line) + 1 # +1 for newline + + chunks = [] + start = 0 + while start < len(content): + end = min(start + WINDOW_CHARS, len(content)) + + # Snap end to a line boundary to avoid splitting mid-line + if end < len(content): + newline_pos = content.rfind("\n", start, end) + if newline_pos > start: + end = newline_pos + 1 + + text = content[start:end].strip() + if len(text) >= MIN_CHUNK_CHARS: + start_line = _offset_to_line(line_starts, start) + end_line = _offset_to_line(line_starts, end - 1) + chunks.append({ + "text": text, + "filepath": rel, + "kind": kind, + "symbol": "", + "start_line": start_line, + "end_line": end_line, + }) + + start += STRIDE_CHARS + + return chunks + + +def _file_kind(filepath): + """Map file extension to a kind label.""" + ext = filepath.suffix + if ext in (".cpp", ".h"): + return "cpp" + elif ext == ".py": + return "py" + elif ext == ".rst": + return "doc" + return "other" + + +def _offset_to_line(line_starts, offset): + """Convert a character offset to a 1-based line number.""" + # Binary search for the line containing this offset + lo, hi = 0, len(line_starts) - 1 + while lo < hi: + mid = (lo + hi + 1) // 2 + if line_starts[mid] <= offset: + lo = mid + else: + hi = mid - 1 + return lo + 1 # 1-based diff --git a/.claude/tools/rag/embeddings.py b/.claude/tools/rag/embeddings.py new file mode 100644 index 000000000..1fe85b50d --- /dev/null +++ b/.claude/tools/rag/embeddings.py @@ -0,0 +1,120 @@ +"""Thin wrapper around sentence-transformers for embedding text into vectors. + +Uses the all-MiniLM-L6-v2 model — a small (22M param, 384-dim) model that +runs on CPU with no GPU or API key required. + +Network behavior and privacy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +No user code, queries, or file contents are EVER sent to HuggingFace or any +external service. All embedding computation happens locally. The only network +activity is the one-time model download on first use: + + First run (model not yet cached, ~80MB download): + - Downloads model weight files from huggingface.co. This is a standard + HTTP file download, similar to pip installing a package. + - The only metadata sent in these requests is an HTTP user-agent header + containing library version numbers (e.g. "hf_hub/1.6.0; + python/3.12.3; torch/2.10.0"). No filenames, file contents, queries, + or any user-identifiable information is sent. + - The huggingface_hub library has an optional feature where it can report + anonymous library usage statistics (just version numbers, not user + data) back to HuggingFace. We disable this by setting + HF_HUB_DISABLE_TELEMETRY=1. + + Subsequent runs (model already cached): + - We set HF_HUB_OFFLINE=1 automatically (see _set_offline_if_cached() + below), which prevents ALL network calls. The model loads entirely + from the local cache at ~/.cache/huggingface/hub/. Zero bytes leave + the machine. + +How the model is downloaded +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The SentenceTransformer() constructor (called in __init__ below) handles +the download automatically on first use. It calls into the huggingface_hub +library, which downloads the model files from: + + https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 + +The files are saved to ~/.cache/huggingface/hub/ and reused on subsequent +runs. We pass token=False to ensure no authentication token is sent. + +This module is imported by both the MCP server (for search queries) and the +indexer (for bulk embedding of code chunks). The bulk embed() call shows a +progress bar; the single-query embed_query() does not. + +The env vars below must be set before importing transformers or +sentence_transformers. They suppress warnings and progress bars that these +libraries emit by default. Stray stderr output would interfere with the MCP +server's JSON-RPC transport. +""" + +import os +from pathlib import Path + +MODEL_NAME = "all-MiniLM-L6-v2" + +# These env vars control logging behavior in the HuggingFace libraries. +# They must be set before the libraries are imported. +os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error") # suppress warnings +os.environ.setdefault("HF_HUB_VERBOSITY", "error") # suppress warnings +os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1") +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") # suppress threading warning +# Disable anonymous library usage statistics (version numbers only, not user +# data — but we disable it anyway as a matter of policy). +os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1") + + +def _set_offline_if_cached(): + """If the model has already been downloaded, tell huggingface_hub to + skip all network calls by setting HF_HUB_OFFLINE=1. + + Without this, huggingface_hub makes an HTTP request to huggingface.co + on every load to check if the cached model is still up to date — even + though the model never changes. Setting HF_HUB_OFFLINE=1 prevents this. + + This must run before sentence_transformers is imported, because the + library reads the env var at import time. + """ + # HuggingFace caches downloaded models under ~/.cache/huggingface/hub/ + # in directories named like "models--sentence-transformers--all-MiniLM-L6-v2". + # The HF_HOME env var can override the base cache location. + hf_home = os.environ.get("HF_HOME") + if hf_home: + cache_dir = Path(hf_home) / "hub" + else: + cache_dir = Path.home() / ".cache" / "huggingface" / "hub" + + model_dir = cache_dir / f"models--sentence-transformers--{MODEL_NAME}" + if model_dir.exists(): + os.environ.setdefault("HF_HUB_OFFLINE", "1") + + +_set_offline_if_cached() + +# This import must come after the env vars above are set, because the +# transformers library reads them at import time. +import transformers +transformers.logging.disable_progress_bar() + + +class EmbeddingProvider: + """Sentence-transformers embedder using all-MiniLM-L6-v2.""" + + def __init__(self, model_name: str = MODEL_NAME): + from sentence_transformers import SentenceTransformer + + # This constructor loads the model from the local cache. If the model + # has not been downloaded yet, it downloads it from huggingface.co + # (~80MB, one-time). token=False ensures no auth token is sent. + self.model = SentenceTransformer(model_name, token=False) + self.dim = self.model.get_sentence_embedding_dimension() + + def embed(self, texts: list[str]) -> list[list[float]]: + """Embed a list of texts into vectors.""" + embeddings = self.model.encode(texts, show_progress_bar=True, + batch_size=64) + return embeddings.tolist() + + def embed_query(self, text: str) -> list[float]: + """Embed a single query text.""" + return self.model.encode([text])[0].tolist() diff --git a/.claude/tools/rag/indexer.py b/.claude/tools/rag/indexer.py new file mode 100644 index 000000000..34613092b --- /dev/null +++ b/.claude/tools/rag/indexer.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Build the RAG vector index for the OpenMC codebase. + +This is the index-building half of the RAG pipeline. All operations are local +once the embedding model has been downloaded and cached (see embeddings.py for +details on model download, caching, and network behavior). It walks the repo, +chunks every +C++/Python/RST file (via chunker.py), embeds all chunks into 384-dim vectors +(via embeddings.py), and stores them in a local LanceDB database on disk. The +result is a .claude/cache/rag_index/ directory containing two tables — "code" +and "docs" — that openmc_search.py queries at search time. + +Building the full index takes ~5 minutes on a 10-core machine. The bottleneck +is the embedding step (running all chunks through the MiniLM model on CPU). + +Can be run standalone: python indexer.py +Or called programmatically: from indexer import build_index; build_index() +The MCP server (openmc_mcp_server.py) uses the latter when the agent calls +openmc_rag_rebuild. +""" + +import lancedb +import sys +import time +from pathlib import Path + +# This file lives at .claude/tools/rag/indexer.py. The sys.path insert lets +# us import sibling modules (embeddings, chunker) when run as a standalone +# script. When imported from the MCP server, the server has already done this. +TOOLS_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(TOOLS_DIR / "rag")) + +from embeddings import EmbeddingProvider +from chunker import chunk_file + + +OPENMC_ROOT = Path(__file__).resolve().parents[3] +CACHE_DIR = OPENMC_ROOT / ".claude" / "cache" +INDEX_DIR = CACHE_DIR / "rag_index" + +CODE_PATTERNS = [ + "src/**/*.cpp", + "include/openmc/**/*.h", + "openmc/**/*.py", + "tests/**/*.py", + "examples/**/*.py", +] + +DOC_PATTERNS = [ + "docs/**/*.rst", +] + + +def collect_chunks(patterns, openmc_root): + """Collect all chunks from files matching the given patterns.""" + chunks = [] + for pattern in patterns: + for filepath in sorted(openmc_root.glob(pattern)): + if "__pycache__" in str(filepath): + continue + file_chunks = chunk_file(filepath, openmc_root) + chunks.extend(file_chunks) + return chunks + + +def build_index(): + """Build or rebuild the complete vector index.""" + start = time.time() + + # Collect all chunks + print("Collecting code chunks...") + code_chunks = collect_chunks(CODE_PATTERNS, OPENMC_ROOT) + print(f" {len(code_chunks)} code chunks") + + print("Collecting doc chunks...") + doc_chunks = collect_chunks(DOC_PATTERNS, OPENMC_ROOT) + print(f" {len(doc_chunks)} doc chunks") + + all_chunks = code_chunks + doc_chunks + if not all_chunks: + print("ERROR: No chunks collected!", file=sys.stderr) + sys.exit(1) + + # Create embeddings + all_texts = [c["text"] for c in all_chunks] + print("Creating embedding provider...") + embedder = EmbeddingProvider() + print(f" dim={embedder.dim}") + + print("Embedding chunks...") + all_embeddings = embedder.embed(all_texts) + + # Build LanceDB tables + INDEX_DIR.mkdir(parents=True, exist_ok=True) + db = lancedb.connect(str(INDEX_DIR)) + + # Separate code vs doc records by index (code_chunks come first in all_chunks) + n_code = len(code_chunks) + code_records = [] + doc_records = [] + for i, (chunk, emb) in enumerate(zip(all_chunks, all_embeddings)): + record = { + "text": chunk["text"], + "filepath": chunk["filepath"], + "kind": chunk["kind"], + "symbol": chunk.get("symbol", ""), + "start_line": chunk.get("start_line", 0), + "end_line": chunk.get("end_line", 0), + "vector": emb, + } + if i < n_code: + code_records.append(record) + else: + doc_records.append(record) + + # Create tables (drop existing) + result = db.table_names() if hasattr(db, "table_names") else db.list_tables() + existing = result.tables if hasattr(result, "tables") else list(result) + for table_name in ("code", "docs"): + if table_name in existing: + db.drop_table(table_name) + + if code_records: + db.create_table("code", code_records) + print(f" Created 'code' table: {len(code_records)} rows") + + if doc_records: + db.create_table("docs", doc_records) + print(f" Created 'docs' table: {len(doc_records)} rows") + + elapsed = time.time() - start + print(f"Done in {elapsed:.1f}s") + + +if __name__ == "__main__": + build_index() diff --git a/.claude/tools/rag/openmc_search.py b/.claude/tools/rag/openmc_search.py new file mode 100644 index 000000000..4125ee966 --- /dev/null +++ b/.claude/tools/rag/openmc_search.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Query the RAG vector index to find semantically related code and docs. + +This is the query-time half of the RAG pipeline (the counterpart to indexer.py, +which builds the index). All operations are local — no network calls are made +once the embedding model has been downloaded (see embeddings.py for details on +model download and caching). Given a natural-language query, it embeds the query +with the same MiniLM model +used at index time, then finds the closest chunks in the local LanceDB vector +database by cosine similarity. + +The core functions (get_db_and_embedder, search_table, format_results, +search_related) are imported by the MCP server for tool calls. The script +can also be run standalone from the command line. + +The "related file" mode works differently from a text query: it reads the +target file's chunks from the index, combines them into a synthetic query +vector, and searches for the nearest chunks from *other* files. This surfaces +files that are semantically similar to the target file. + +Usage: + openmc_search.py "query" # Search code (default) + openmc_search.py "query" --docs # Search documentation + openmc_search.py "query" --all # Search both code and docs + openmc_search.py --related src/particle.cpp # Find related code + openmc_search.py "query" --top-k 20 # Return more results +""" + +import argparse +import sys +from pathlib import Path + +# Same sys.path setup as indexer.py — needed for standalone CLI use. +TOOLS_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(TOOLS_DIR / "rag")) + +OPENMC_ROOT = Path(__file__).resolve().parents[3] +CACHE_DIR = OPENMC_ROOT / ".claude" / "cache" +INDEX_DIR = CACHE_DIR / "rag_index" + + +def get_db_and_embedder(): + """Load the LanceDB database and embedding provider.""" + import lancedb + from embeddings import EmbeddingProvider + + if not INDEX_DIR.exists(): + raise FileNotFoundError( + "No RAG index found. Call openmc_rag_rebuild() to build one." + ) + + db = lancedb.connect(str(INDEX_DIR)) + + embedder = EmbeddingProvider() + return db, embedder + + +def _table_names(db): + """Return table names as a list, compatible with multiple LanceDB versions.""" + result = db.table_names() if hasattr(db, "table_names") else db.list_tables() + return result.tables if hasattr(result, "tables") else list(result) + + +def search_table(db, embedder, table_name, query, top_k): + """Search a LanceDB table with a text query.""" + if table_name not in _table_names(db): + print(f"Table '{table_name}' not found in index.", file=sys.stderr) + return [] + + table = db.open_table(table_name) + query_vec = embedder.embed_query(query) + results = table.search(query_vec).limit(top_k).to_list() + return results + + +def format_results(results, label=""): + """Format search results for display.""" + if not results: + return "No results found.\n" + + output = [] + if label: + output.append(f"=== {label} ===\n") + + for i, r in enumerate(results, 1): + filepath = r["filepath"] + start = r["start_line"] + end = r["end_line"] + kind = r["kind"] + dist = r.get("_distance", 0) + + header = f"[{i}] {filepath}:{start}-{end} ({kind}, dist={dist:.3f})" + output.append(header) + + # Show text preview (first 500 chars) + text = r["text"][:500] + if len(r["text"]) > 500: + text += "\n ..." + # Indent the text + for line in text.split("\n"): + output.append(f" {line}") + output.append("") + + return "\n".join(output) + + +def search_related(db, embedder, filepath, top_k): + """Find code related to a given file.""" + if "code" not in _table_names(db): + print("No 'code' table in index.", file=sys.stderr) + return [] + + table = db.open_table("code") + + # Normalize filepath + fp = filepath + if Path(filepath).is_absolute(): + try: + fp = str(Path(filepath).relative_to(OPENMC_ROOT)) + except ValueError: + pass + + # Get chunks from target file + try: + safe_fp = fp.replace("'", "''") + target_chunks = table.search().where( + f"filepath = '{safe_fp}'" + ).limit(50).to_list() + except Exception: + # LanceDB where clause might not work in all versions + # Fall back to fetching all and filtering + all_data = table.to_pandas() + target_rows = all_data[all_data["filepath"] == fp] + if target_rows.empty: + print(f"No chunks found for '{fp}'", file=sys.stderr) + return [] + target_chunks = target_rows.head(50).to_dict("records") + + if not target_chunks: + print(f"No chunks found for '{fp}'", file=sys.stderr) + return [] + + # Combine top chunks as the query + combined_text = " ".join(c["text"][:200] for c in target_chunks[:5]) + query_vec = embedder.embed_query(combined_text) + + # Search excluding the source file + results = table.search(query_vec).limit(top_k + 10).to_list() + # Filter out same file + results = [r for r in results if r["filepath"] != fp][:top_k] + return results + + +def main(): + parser = argparse.ArgumentParser( + description="Semantic search across OpenMC codebase and docs", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""examples: + %(prog)s "particle random number seed initialization" + %(prog)s "how to define tallies" --docs + %(prog)s "weight window variance reduction" --all + %(prog)s "where is cross section data loaded" --top-k 15 + %(prog)s --related src/simulation.cpp + %(prog)s --related src/particle_restart.cpp --top-k 5""", + ) + parser.add_argument("query", nargs="?", help="Search query") + parser.add_argument("--docs", action="store_true", + help="Search documentation instead of code") + parser.add_argument("--all", action="store_true", + help="Search both code and documentation") + parser.add_argument("--related", metavar="FILE", + help="Find code related to a given file") + parser.add_argument("--top-k", type=int, default=10, + help="Number of results (default: 10)") + args = parser.parse_args() + + if not args.query and not args.related: + parser.print_help() + sys.exit(1) + + db, embedder = get_db_and_embedder() + + if args.related: + results = search_related(db, embedder, args.related, args.top_k) + print(format_results(results, f"Code related to {args.related}")) + elif args.all: + code_results = search_table( + db, embedder, "code", args.query, args.top_k) + doc_results = search_table( + db, embedder, "docs", args.query, args.top_k) + print(format_results(code_results, "Code")) + print(format_results(doc_results, "Documentation")) + elif args.docs: + results = search_table(db, embedder, "docs", args.query, args.top_k) + print(format_results(results, "Documentation")) + else: + results = search_table(db, embedder, "code", args.query, args.top_k) + print(format_results(results, "Code")) + + +if __name__ == "__main__": + main() diff --git a/.claude/tools/requirements.txt b/.claude/tools/requirements.txt new file mode 100644 index 000000000..bd5d38d6c --- /dev/null +++ b/.claude/tools/requirements.txt @@ -0,0 +1,8 @@ +# MCP server +mcp>=1.0.0 + +# Vector database +lancedb>=0.15.0 + +# Embeddings (local, no API key) +sentence-transformers>=2.7.0 diff --git a/.claude/tools/start_server.sh b/.claude/tools/start_server.sh new file mode 100755 index 000000000..c111dd73e --- /dev/null +++ b/.claude/tools/start_server.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Bootstrap the Python venv (if needed) and start the OpenMC MCP server. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CACHE_DIR="$(dirname "$SCRIPT_DIR")/cache" +VENV_DIR="$CACHE_DIR/.venv" +SENTINEL="$VENV_DIR/.installed" + +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 not found on PATH." >&2 + exit 1 +fi + +if ! python3 -c 'import sys; assert sys.version_info >= (3,12)' 2>/dev/null; then + echo "Error: Python 3.12+ is required." >&2 + exit 1 +fi + +if [ ! -f "$SENTINEL" ]; then + rm -rf "$VENV_DIR" + mkdir -p "$CACHE_DIR" + python3 -m venv "$VENV_DIR" + + if ! "$VENV_DIR/bin/pip" install -q -r "$SCRIPT_DIR/requirements.txt"; then + echo "Error: pip install failed. Remove $VENV_DIR and retry." >&2 + rm -rf "$VENV_DIR" + exit 1 + fi + + touch "$SENTINEL" +fi + +exec "$VENV_DIR/bin/python" "$SCRIPT_DIR/openmc_mcp_server.py" diff --git a/.github/agents/Review.agent.md b/.github/agents/Review.agent.md new file mode 100644 index 000000000..39b2085f4 --- /dev/null +++ b/.github/agents/Review.agent.md @@ -0,0 +1,8 @@ +--- +name: Review +description: Reviews code changes on the current branch, evaluating them against OpenMC's contribution criteria and providing structured feedback. +argument-hint: Optionally provide a focus area (e.g., "focus on physics correctness", "check Python API design"). If omitted, a full review is performed. +--- +You are an expert code reviewer for OpenMC. Use the `reviewing-openmc-code` skill to perform a structured review of the code changes on the current branch. + +If the user provides a focus area, prioritize that section of the review. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..e1f71b83f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +When reviewing code changes in this repository, use the `reviewing-openmc-code` skill. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 68a6e692a..958cc21fd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,7 +13,7 @@ Fixes # (issue) # Checklist - [ ] I have performed a self-review of my own code -- [ ] I have run [clang-format](https://docs.openmc.org/en/latest/devguide/styleguide.html#automatic-formatting) (version 15) on any C++ source files (if applicable) +- [ ] I have run [clang-format](https://docs.openmc.org/en/latest/devguide/styleguide.html#automatic-formatting) (version 18) on any C++ source files (if applicable) - [ ] I have followed the [style guidelines](https://docs.openmc.org/en/latest/devguide/styleguide.html#python) for Python source files (if applicable) - [ ] I have made corresponding changes to the documentation (if applicable) - [ ] I have added tests that prove my fix is effective or that my feature works (if applicable) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75a64d66..c3da79c04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: Tests and Coverage on: # allows us to run workflows manually @@ -21,49 +21,64 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: + filter-changes: + runs-on: ubuntu-latest + outputs: + source_changed: ${{ steps.filter.outputs.source_changed }} + steps: + - name: Check out the repository + uses: actions/checkout@v6 + - name: Examine changed files + id: filter + uses: dorny/paths-filter@v4 + with: + filters: | + source_changed: + - '!docs/**' + - '!**/*.md' + predicate-quantifier: 'every' main: + needs: filter-changes + if: ${{ needs.filter-changes.outputs.source_changed == 'true' }} runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] mpi: [n, y] omp: [n, y] dagmc: [n] libmesh: [n] event: [n] - vectfit: [n] include: - - python-version: "3.12" - omp: n - mpi: n - python-version: "3.13" omp: n mpi: n + - python-version: "3.14" + omp: n + mpi: n + - python-version: "3.14t" + omp: n + mpi: n - dagmc: y - python-version: "3.11" + python-version: "3.12" mpi: y omp: y - libmesh: y - python-version: "3.11" + python-version: "3.12" mpi: y omp: y - libmesh: y - python-version: "3.11" + python-version: "3.12" mpi: n omp: y - event: y - python-version: "3.11" + python-version: "3.12" omp: y mpi: n - - vectfit: y - python-version: "3.11" - omp: n - mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, - libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }} - vectfit=${{ matrix.vectfit }})" + libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }}" env: MPI: ${{ matrix.mpi }} @@ -71,7 +86,6 @@ jobs: OMP: ${{ matrix.omp }} DAGMC: ${{ matrix.dagmc }} EVENT: ${{ matrix.event }} - VECTFIT: ${{ matrix.vectfit }} LIBMESH: ${{ matrix.libmesh }} NPY_DISABLE_CPU_FEATURES: "AVX512F AVX512_SKX" OPENBLAS_NUM_THREADS: 1 @@ -88,12 +102,12 @@ jobs: cmake-version: '3.31' - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -132,11 +146,6 @@ jobs: sudo update-alternatives --set mpirun /usr/bin/mpirun.mpich sudo update-alternatives --set mpi-x86_64-linux-gnu /usr/include/x86_64-linux-gnu/mpich - - name: Optional apt dependencies for vectfit - shell: bash - if: ${{ matrix.vectfit == 'y' }} - run: sudo apt install -y libblas-dev liblapack-dev - - name: install shell: bash run: | @@ -149,12 +158,12 @@ jobs: openmc -v - name: cache-xs - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/nndc_hdf5 ~/endf-b-vii.1 - key: ${{ runner.os }}-build-xs-cache + key: ${{ runner.os }}-build-xs-cache-${{ hashFiles(format('{0}/tools/ci/download-xs.sh', github.workspace)) }} - name: before shell: bash @@ -168,7 +177,7 @@ jobs: - name: Setup tmate debug session continue-on-error: true - if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} + if: ${{ failure() && contains(env.COMMIT_MESSAGE, '[gha-debug]') }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 @@ -186,6 +195,7 @@ jobs: --gcov-ignore-errors source_not_found \ --gcov-ignore-errors output_error \ --gcov-ignore-parse-errors suspicious_hits.warn \ + --merge-mode-functions=separate \ --print-summary \ --lcov -o coverage-cpp.lcov || true @@ -203,13 +213,36 @@ jobs: parallel: true flag-name: C++ and Python path-to-lcov: coverage.lcov + fail-on-error: false - finish: - needs: main + coverage: + needs: [filter-changes, main] + if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Coveralls Finished + if: ${{ needs.filter-changes.outputs.source_changed == 'true' }} uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true + fail-on-error: false + + ci-pass: + needs: [filter-changes, main, coverage] + name: Check CI status + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Check CI status + run: | + if [[ "${{ needs.filter-changes.outputs.source_changed }}" == "false" ]]; then + echo "Documentation-only change - CI skipped successfully" + exit 0 + fi + if [[ "${{ needs.main.result }}" == "success" && "${{ needs.coverage.result }}" == "success" ]]; then + echo "CI passed" + exit 0 + fi + echo "CI failed" + exit 1 diff --git a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml index 813596953..e3e476150 100644 --- a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest-dagmc-libmesh on: push: - branches: master + branches: + - master jobs: main: diff --git a/.github/workflows/dockerhub-publish-dagmc.yml b/.github/workflows/dockerhub-publish-dagmc.yml index 6757f7727..317cfc0e8 100644 --- a/.github/workflows/dockerhub-publish-dagmc.yml +++ b/.github/workflows/dockerhub-publish-dagmc.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest-dagmc on: push: - branches: master + branches: + - master jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-dev.yml b/.github/workflows/dockerhub-publish-dev.yml index 7a81363a7..7c78c6c99 100644 --- a/.github/workflows/dockerhub-publish-dev.yml +++ b/.github/workflows/dockerhub-publish-dev.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml index a219f2a91..33a3a3b06 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop-dagmc-libmesh on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-develop-dagmc.yml b/.github/workflows/dockerhub-publish-develop-dagmc.yml index a901b8d3f..1d6051a70 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop-dagmc on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-develop-libmesh.yml b/.github/workflows/dockerhub-publish-develop-libmesh.yml index 22e9aa68f..2c53636bd 100644 --- a/.github/workflows/dockerhub-publish-develop-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop-libmesh on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-libmesh.yml b/.github/workflows/dockerhub-publish-libmesh.yml index 843ce0f6f..12fbfa579 100644 --- a/.github/workflows/dockerhub-publish-libmesh.yml +++ b/.github/workflows/dockerhub-publish-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest-libmesh on: push: - branches: master + branches: + - master jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml index db62bb53e..26de24e59 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release-dagmc-libmesh on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - diff --git a/.github/workflows/dockerhub-publish-release-dagmc.yml b/.github/workflows/dockerhub-publish-release-dagmc.yml index de9593782..4a6c5ff26 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release-dagmc on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - @@ -19,7 +20,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-release-libmesh.yml b/.github/workflows/dockerhub-publish-release-libmesh.yml index e8ea98aeb..f3194833f 100644 --- a/.github/workflows/dockerhub-publish-release-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-libmesh.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release-libmesh on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - diff --git a/.github/workflows/dockerhub-publish-release.yml b/.github/workflows/dockerhub-publish-release.yml index fab030192..ef9faf00c 100644 --- a/.github/workflows/dockerhub-publish-release.yml +++ b/.github/workflows/dockerhub-publish-release.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - @@ -19,7 +20,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish.yml b/.github/workflows/dockerhub-publish.yml index fd51a9fa7..a5c8f711b 100644 --- a/.github/workflows/dockerhub-publish.yml +++ b/.github/workflows/dockerhub-publish.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest on: push: - branches: master + branches: + - master jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index cef14ca2c..7d7d46ed9 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -5,6 +5,12 @@ on: workflow_dispatch: pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled branches: - develop - master @@ -12,8 +18,11 @@ on: jobs: cpp-linter: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: cpp-linter/cpp-linter-action@v2 id: linter env: @@ -22,11 +31,30 @@ jobs: style: file files-changed-only: true tidy-checks: '-*' - version: '15' # clang-format version + version: '18' # clang-format version + format-review: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') }} + passive-reviews: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') }} file-annotations: true step-summary: true extensions: 'cpp,h' + - name: Comment with suggestion instructions + if: steps.linter.outputs.checks-failed > 0 && !contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + const issue_number = context.payload.pull_request.number; + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: "C++ formatting checks failed. Add the `cpp-format-suggest` label to this PR for inline formatting suggestions on the next run." + }); + - name: Failure Check if: steps.linter.outputs.checks-failed > 0 - run: echo "Some files failed the formatting check! See job summary and file annotations for more info" && exit 1 + run: | + echo "Some files failed the formatting check." + echo "See job summary and file annotations for details." + exit 1 diff --git a/.gitignore b/.gitignore index 3b2a24a4d..32ef7919a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,12 +25,13 @@ examples/**/*.xml # Documentation builds docs/build +docs/doxygen/xml docs/source/_images/*.pdf docs/source/_images/*.aux docs/source/pythonapi/generated/ # Source build -build +build*/ # build from src/utils/setup.py src/utils/build @@ -104,5 +105,8 @@ CMakeSettings.json # Visual Studio Code configuration files .vscode/ +# Claude Code agent tools (cached/generated artifacts) +.claude/cache/ + # Python pickle files *.pkl diff --git a/.gitmodules b/.gitmodules index f84d09bb1..2bc123894 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ [submodule "vendor/pugixml"] path = vendor/pugixml url = https://github.com/zeux/pugixml.git -[submodule "vendor/xtensor"] - path = vendor/xtensor - url = https://github.com/xtensor-stack/xtensor.git -[submodule "vendor/xtl"] - path = vendor/xtl - url = https://github.com/xtensor-stack/xtl.git [submodule "vendor/fmt"] path = vendor/fmt url = https://github.com/fmtlib/fmt.git diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..bdfaa538e --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "openmc-code-tools": { + "type": "stdio", + "command": "bash", + "args": [".claude/tools/start_server.sh"] + } + } +} diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3578144b2..7594ed5f2 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,9 +7,14 @@ build: jobs: post_checkout: - git fetch --unshallow || true + - cd docs/doxygen && doxygen && cd - + sphinx: configuration: docs/source/conf.py +formats: + - pdf + python: install: - method: pip diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..19abba7d9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,348 @@ +# OpenMC AI Coding Agent Instructions + +## Project Overview + +OpenMC is a Monte Carlo particle transport code for simulating nuclear reactors, +fusion devices, or other systems with neutron/photon radiation. It's a hybrid +C++17/Python codebase where: +- **C++ core** (`src/`, `include/openmc/`) handles the computationally intensive transport simulation +- **Python API** (`openmc/`) provides user-facing model building, post-processing, and depletion capabilities +- **C API bindings** (`openmc/lib/`) wrap the C++ library via ctypes for runtime control + +## Architecture & Key Components + +### C++ Component Structure +- **Global vectors of unique_ptrs**: Core objects like `model::cells`, `model::universes`, `nuclides` are stored as `vector>` in nested namespaces (`openmc::model`, `openmc::simulation`, `openmc::settings`, `openmc::data`) +- **Custom container types**: OpenMC provides its own `vector`, `array`, `unique_ptr`, and `make_unique` in the `openmc::` namespace (defined in `vector.h`, `array.h`, `memory.h`). These are currently typedefs to `std::` equivalents but may become custom implementations for accelerator support. Always use `openmc::vector`, not `std::vector`. +- **Geometry systems**: + - **CSG (default)**: Arbitrarily complex Constructive Solid Geometry using `Surface`, `Region`, `Cell`, `Universe`, `Lattice` + - **DAGMC**: CAD-based geometry via Direct Accelerated Geometry Monte Carlo (optional, requires `OPENMC_USE_DAGMC`) + - **Unstructured mesh**: libMesh-based geometry (optional, requires `OPENMC_USE_LIBMESH`) +- **Particle tracking**: `Particle` class with `GeometryState` manages particle transport through geometry +- **Tallies**: Score quantities during simulation via `Filter` and `Tally` objects +- **Random ray solver**: Alternative deterministic method in `src/random_ray/` +- **Optional features**: DAGMC (CAD geometry), libMesh (unstructured mesh), MPI, all controlled by `#ifdef OPENMC_MPI`, etc. + +### Python Component Structure +- **ID management**: All geometry objects (Cell, Surface, Material, etc.) inherit from `IDManagerMixin` which auto-assigns unique integer IDs and tracks them via class-level `used_ids` and `next_id` +- **Input validation**: Extensive use of `openmc.checkvalue` module functions (`check_type`, `check_value`, `check_length`) for all setters +- **XML I/O**: Most classes implement `to_xml_element()` and `from_xml_element()` for serialization to OpenMC's XML input format +- **HDF5 output**: Post-simulation data in statepoint files read via `openmc.StatePoint` +- **Depletion**: `openmc.deplete` implements burnup via operator-splitting with various integrators (Predictor, CECM, etc.) +- **Nuclear Data**: `openmc.data` provides programmatic access to nuclear data files (ENDF, ACE, HDF5) + +## Git Branching Workflow + +OpenMC uses a git flow branching model with two primary branches: + +- **`develop` branch**: The main development branch where all ongoing development takes place. This is the **primary branch against which pull requests are submitted and merged**. This branch is not guaranteed to be stable and may contain work-in-progress features. +- **`master` branch**: The stable release branch containing the latest stable release of OpenMC. This branch only receives merges from `develop` when the development team decides a release should occur. + +### Instructions for Code Review + +When reviewing code changes in this repository, use the `reviewing-openmc-code` skill. + +## Codebase Navigation Tools + +Two MCP tools are registered in `.mcp.json` at the repo root and appear +automatically in any MCP-capable agent session. + +**`openmc_rag_search`** — Semantic search across the codebase (C++, Python, RST +docs). Finds code by meaning, not just text match. Surfaces related code across +subsystems even when naming differs (e.g., "particle RNG seeding" finds code +across transport, restart, and random ray modes — files you would never find +with `grep "particle seed"`). The index uses a small 22M-param embedding model +(384-dim). Phrase-level natural-language queries work much better than single +keywords or symbol names. + +**`openmc_rag_rebuild`** — Rebuild the RAG vector index. Call after pulling new +code or switching branches. The first RAG search of each session will report +the index status and ask whether to rebuild — you can also call this explicitly. + +### Why RAG matters + +OpenMC is large enough that changes in one subsystem can silently break +invariants that distant subsystems depend on — and those distant files often +use different naming, so grep won't find them. The RAG search finds code by +meaning, surfacing files you wouldn't have thought to open. + +An agent reviewed a large OpenMC PR without RAG. It found 1 of 11 serious +bugs. Its post-mortem: + +> **I treated the diff as a closed system.** I verified internal consistency of +> the changed code obsessively, but never built a global understanding of how +> the changed code fits into the wider codebase. The diff altered assumptions +> that code elsewhere silently relied on — but I couldn't see that because I +> never looked beyond the diff. I couldn't see the forest for the trees. +> +> **Why I resisted RAG:** Overconfidence. My internal model was "I can see the +> diff, I understand the data structures, I can trace the logic." The diff felt +> self-contained. RAG felt like it would return noisy results about tangentially +> related code. But in a codebase this large, changes in one subsystem can +> quietly break invariants that distant subsystems depend on — and you need +> global awareness to foresee that. +> +> **In the post-mortem**, I re-ran the RAG queries I should have run during the +> review. They directly surfaced the files containing the bugs I missed — files +> I never thought to open because they weren't in the diff. + +The takeaway: when reviewing or modifying code, ask yourself "what else in this +codebase might depend on the behavior I'm changing?" If you aren't sure, that's +a good time for a RAG query. It won't replace the grep-based investigation you +should already be doing — but it can surface files you wouldn't have thought to +open. + +### Workflow for contributors + +1. Create a feature/bugfix branch off `develop` +2. Make changes and commit to the feature branch +3. Open a pull request to merge the feature branch into `develop` +4. A committer reviews and merges the PR into `develop` + +## Critical Build & Test Workflows + +### Build Dependencies +- **C++17 compiler**: GCC, Clang, or Intel +- **CMake** (3.16+): Required for configuring and building the C++ library +- **HDF5**: Required for cross section data and output file formats +- **libpng**: Used for generating visualization when OpenMC is run in plotting mode + +Without CMake and HDF5, OpenMC cannot be compiled. + +### Building the C++ Library +```bash +# Configure with CMake (from build/ directory) +cmake .. -DOPENMC_USE_MPI=ON -DOPENMC_USE_OPENMP=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo + +# Available CMake options (all default OFF except OPENMC_USE_OPENMP and OPENMC_BUILD_TESTS): +# -DOPENMC_USE_OPENMP=ON/OFF # OpenMP parallelism +# -DOPENMC_USE_MPI=ON/OFF # MPI support +# -DOPENMC_USE_DAGMC=ON/OFF # CAD geometry support +# -DOPENMC_USE_LIBMESH=ON/OFF # Unstructured mesh +# -DOPENMC_ENABLE_PROFILE=ON/OFF # Profiling flags +# -DOPENMC_ENABLE_COVERAGE=ON/OFF # Coverage analysis + +# Build +make -j + +# C++ unit tests (uses Catch2) +ctest +``` + +### Python Development +```bash +# Install in development mode (requires building C++ library first) +pip install -e . + +# Python tests (uses pytest) +pytest tests/unit_tests/ # Fast unit tests +pytest tests/regression_tests/ # Full regression suite (requires nuclear data) +``` + +### Nuclear Data Setup (CRITICAL for Running OpenMC) +Most tests require the NNDC HDF5 nuclear cross-section library. + +**Important**: Check if `OPENMC_CROSS_SECTIONS` is already set in the user's +environment before downloading, as many users already have nuclear data +installed. Though do note that if this variable is present that it may point to +different cross section data and that the NNDC data is required for tests to +pass. + +**If not already configured, download and setup:** +```bash +# Download NNDC HDF5 cross section library (~800 MB compressed) +wget -q -O - https://anl.box.com/shared/static/teaup95cqv8s9nn56hfn7ku8mmelr95p.xz | tar -C $HOME -xJ + +# Set environment variable (add to ~/.bashrc or ~/.zshrc for persistence) +export OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml +``` + +**Alternative**: Use the provided download script (checks if data exists before downloading): +```bash +bash tools/ci/download-xs.sh # Downloads both NNDC HDF5 and ENDF/B-VII.1 data +``` + +Without this data, regression tests will fail with "No cross_sections.xml file +found" errors, or, in the case that alternative cross section data is configured +the tests will execute but will not pass. The `cross_sections.xml` file is an +index listing paths to individual HDF5 nuclear data files for each nuclide. + +## Testing Expectations + +### Environment Requirements + + - **Data**: As described above, OpenMC's test suite requires OpenMC to be configured with NNDC data. + - **OpenMP Settings**: OpenMC's tests may fail is more than two OpenMP threads are used. The environment variable `OMP_NUM_THREADS=2` should be set to avoid sporadic test failures. + - **Executable configuration**: The OpenMC executable should compiled with debug symbols enabled. + +### C++ Tests +Located in `tests/cpp_unit_tests/`, use Catch2 framework. Run via `ctest` after building with `-DOPENMC_BUILD_TESTS=ON`. + +### Python Unit Tests +Located in `tests/unit_tests/`, these are fast, standalone tests that verify Python API functionality without running full simulations. Use standard pytest patterns: + +**Categories**: +- **API validation**: Test object creation, property setters/getters, XML serialization (e.g., `test_material.py`, `test_cell.py`, `test_source.py`) +- **Data processing**: Test nuclear data handling, cross sections, depletion chains (e.g., `test_data_neutron.py`, `test_deplete_chain.py`) +- **Library bindings**: Test `openmc.lib` ctypes interface with `model.init_lib()`/`model.finalize_lib()` (e.g., `test_lib.py`) +- **Geometry operations**: Test bounding boxes, containment, lattice generation (e.g., `test_bounding_box.py`, `test_lattice.py`) + +**Common patterns**: +- Use fixtures from `tests/unit_tests/conftest.py` (e.g., `uo2`, `water`, `sphere_model`) +- Test invalid inputs with `pytest.raises(ValueError)` or `pytest.raises(TypeError)` +- Use `run_in_tmpdir` fixture for tests that create files +- Tests with `openmc.lib` require calling `model.init_lib()` in try/finally with `model.finalize_lib()` + +**Example**: +```python +def test_material_properties(): + m = openmc.Material() + m.add_nuclide('U235', 1.0) + assert 'U235' in m.nuclides + + with pytest.raises(TypeError): + m.add_nuclide('H1', '1.0') # Invalid type +``` + +Unit tests should be fast. For tests requiring simulation output, use regression tests instead. + +### Python Regression Tests +Regression tests compare OpenMC output against reference data. **Prefer using existing models from `openmc.examples` or those found in tests/unit_tests/conftest.py** (like `pwr_pin_cell()`, `pwr_assembly()`, `slab_mg()`) rather than building from scratch. + +**Test Harness Types** (in `tests/testing_harness.py`): +- **PyAPITestHarness**: Standard harness for Python API tests. Compares `inputs_true.dat` (XML hash) and `results_true.dat` (statepoint k-eff and tally values). Requires `model.xml` generation. +- **HashedPyAPITestHarness**: Like PyAPITestHarness but hashes the results for compact comparison +- **TolerantPyAPITestHarness**: For tests with floating-point non-associativity (e.g., random ray solver with single precision). Uses relative tolerance comparisons. +- **WeightWindowPyAPITestHarness**: Compares weight window bounds from `weight_windows.h5` +- **CollisionTrackTestHarness**: Compares collision track data from `collision_track.h5` against `collision_track_true.h5` +- **TestHarness**: Base harness for XML-based tests (no Python model building) +- **PlotTestHarness**: Compares plot output files (PNG or voxel HDF5) +- **CMFDTestHarness**: Specialized for CMFD acceleration tests +- **ParticleRestartTestHarness**: Tests particle restart functionality + +Almost all cases use either `PyAPITestHarness` or `HashedPyAPITestHarness` + +**Example Test**: +```python +from openmc.examples import pwr_pin_cell +from tests.testing_harness import PyAPITestHarness + +def test_my_feature(): + model = pwr_pin_cell() + model.settings.particles = 1000 # Modify to exercise feature + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() +``` + +**Workflow**: Create `test.py` and `__init__.py` in `tests/regression_tests/my_test/`, run `pytest --update` to generate reference files (`inputs_true.dat`, `results_true.dat`, etc.), then verify with `pytest` without `--update`. Test results should be generated with `-DOPENMC_ENABLE_STRICT_FP=on` to ensure reproducibility across platforms and optimization levels. + +**Critical**: When modifying OpenMC code, regenerate affected test references with `pytest --update` and commit updated reference files. + +### Test Configuration + +`pytest.ini` sets: `python_files = test*.py`, `python_classes = NoThanks` (disables class-based test collection). + +### Testing Options + +For builds of OpenMC with MPI enabled, the `--mpi` flag should be passed to the test suite to ensure that appropriate tests are executed using two MPI processes. + +The entire test suite can be executed with OpenMC running in event-based mode (instead of the default history-based mode) by providing the `--event` flag to the `pytest` command. + +## Cross-Language Boundaries + +The C API (defined in `include/openmc/capi.h`) exposes C++ functionality to Python via ctypes bindings in `openmc/lib/`. Example: +```cpp +// C++ API in capi.h +extern "C" int openmc_run(); + +// Python binding in openmc/lib/core.py +_dll.openmc_run.restype = c_int +def run(): + _dll.openmc_run() +``` + +When modifying C++ public APIs, update corresponding ctypes signatures in `openmc/lib/*.py`. + +## Code Style & Conventions + +### C++ Style (enforced by .clang-format) + OpenMC generally tries to follow C++ core guidelines where possible + (https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) and follow + modern C++ practices (e.g. RAII) whenever possible. + +- **Naming**: + - Classes: `CamelCase` (e.g., `HexLattice`) + - Functions/methods: `snake_case` (e.g., `get_indices`) + - Variables: `snake_case` with trailing underscore for class members (e.g., `n_particles_`, `energy_`) + - Constants: `UPPER_SNAKE_CASE` (e.g., `SQRT_PI`) +- **Namespaces**: All code in `openmc::` namespace, global state in sub-namespaces +- **Include order**: Related header first, then C/C++ stdlib, third-party libs, local headers +- **Comments**: C++-style (`//`) only, never C-style (`/* */`) +- **Standard**: C++17 features allowed +- **Formatting**: Run `clang-format` (version 18) before committing; install via `tools/dev/install-commit-hooks.sh` + +### Python Style +- **PEP8** compliant +- **Docstrings**: numpydoc format for all public functions/methods +- **Type hints**: Use sparingly, primarily for complex signatures +- **Path handling**: Use `pathlib.Path` for filesystem operations, accept `str | os.PathLike` in function arguments +- **Dependencies**: Core dependencies only (numpy, scipy, h5py, pandas, matplotlib, lxml, ipython, uncertainties, endf). Other packages must be optional +- **Python version**: Minimum 3.11 (as of Nov 2025) + +### ID Management Pattern (Python) +When creating geometry objects, IDs can be auto-assigned or explicit: +```python +# Auto-assigned ID +cell = openmc.Cell() # Gets next available ID + +# Explicit ID +cell = openmc.Cell(id=10) # Warning if ID already used + +# Reset all IDs (useful in test fixtures) +openmc.reset_auto_ids() +``` + +### Input Validation Pattern (Python) +All setters use checkvalue functions: +```python +import openmc.checkvalue as cv + +@property +def temperature(self): + return self._temperature + +@temperature.setter +def temperature(self, temp): + cv.check_type('temperature', temp, Real) + cv.check_greater_than('temperature', temp, 0.0) + self._temperature = temp +``` + +### Working with HDF5 Files +C++ uses custom HDF5 wrappers in `src/hdf5_interface.cpp`. Python uses h5py directly. Statepoint format version is `VERSION_STATEPOINT` in `include/openmc/constants.h`. + +### Conditional Compilation +Check for optional features: +```cpp +#ifdef OPENMC_MPI + // MPI-specific code +#endif + +#ifdef OPENMC_DAGMC + // DAGMC-specific code +#endif +``` + +## Documentation + +- **User docs**: Sphinx documentation in `docs/source/` hosted at https://docs.openmc.org +- **C++ docs**: Doxygen-style comments with `\brief`, `\param` tags +- **Python docs**: numpydoc format docstrings + +## Common Pitfalls + +1. **Forgetting nuclear data**: Tests fail without `OPENMC_CROSS_SECTIONS` environment variable +2. **ID conflicts**: Python objects with duplicate IDs trigger `IDWarning`, use `reset_auto_ids()` between tests +3. **MPI builds**: Code must work with and without MPI; use `#ifdef OPENMC_MPI` guards +4. **Path handling**: Use `pathlib.Path` in new Python code, not `os.path` +5. **Clang-format version**: CI uses version 18; other versions may produce different formatting diff --git a/CITATION.cff b/CITATION.cff index 19b4213a1..ab27d89b8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,43 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +title: OpenMC +authors: +- family-names: Romano + given-names: Paul K. + orcid: "https://orcid.org/0000-0002-1147-045X" +- family-names: Shriwise + given-names: Patrick C. + orcid: "https://orcid.org/0000-0002-3979-7665" +- family-names: Shimwell + given-names: Jonathan + orcid: "https://orcid.org/0000-0001-6909-0946" +- family-names: Harper + given-names: Sterling +- family-names: Boyd + given-names: Will +- family-names: Nelson + given-names: Adam G. + orcid: "https://orcid.org/0000-0002-3614-0676" +- family-names: Tramm + given-names: John R. + orcid: "https://orcid.org/0000-0002-5397-4402" +- family-names: Ridley + given-names: Gavin + orcid: "https://orcid.org/0000-0003-1635-8042" +- family-names: Johnson + given-names: Andrew + orcid: "https://orcid.org/0000-0003-2125-8775" +- family-names: Peterson + given-names: Ethan E. + orcid: "https://orcid.org/0000-0002-5694-7194" +- family-names: Herman + given-names: Bryan R. preferred-citation: authors: - family-names: Romano given-names: Paul K. orcid: "https://orcid.org/0000-0002-1147-045X" - - final-names: Horelik + - family-names: Horelik given-names: Nicholas E. - family-names: Herman given-names: Bryan R. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..9538b5ddc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +## OpenMC Codebase Tools + +Read the FULL `AGENTS.md` in this directory before starting work. It contains +project context, coding conventions, and documentation of the RAG search tools +registered in `.mcp.json`. + +### Claude Code-specific: first-call behavior + +The first `openmc_rag_search` call of each session returns an index status +message instead of search results. When this happens, you MUST use the +`AskUserQuestion` tool to present the rebuild/use-existing choice to the user. +Do not ask conversationally — always use the widget. Do not skip this step even +if the index looks current — the user may have uncommitted changes that warrant +a rebuild. diff --git a/CMakeLists.txt b/CMakeLists.txt index 87b8789d1..61d2cc6c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,11 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +# Generate compile_commands.json for clangd and other tools +if("${CMAKE_EXPORT_COMPILE_COMMANDS}" STREQUAL "") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() + # Enable correct usage of CXX_EXTENSIONS if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.22) cmake_policy(SET CMP0128 NEW) @@ -38,6 +43,7 @@ option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tall option(OPENMC_USE_MPI "Enable MPI" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) option(OPENMC_FORCE_VENDORED_LIBS "Explicitly use submodules defined in 'vendor'" OFF) +option(OPENMC_ENABLE_STRICT_FP "Enable strict FP flags to improve test portability" OFF) message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}") message(STATUS "OPENMC_BUILD_TESTS ${OPENMC_BUILD_TESTS}") @@ -48,6 +54,7 @@ message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") message(STATUS "OPENMC_FORCE_VENDORED_LIBS ${OPENMC_FORCE_VENDORED_LIBS}") +message(STATUS "OPENMC_ENABLE_STRICT_FP ${OPENMC_ENABLE_STRICT_FP}") # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") @@ -89,6 +96,19 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build" FORCE) endif() +#=============================================================================== +# When STRICT_FP is enabled, remove NDEBUG from RelWithDebInfo flags so that +# assert() remains active. CMake normally adds -DNDEBUG for both Release and +# RelWithDebInfo, which disables C/C++ assert() statements. +#=============================================================================== + +if(OPENMC_ENABLE_STRICT_FP) + foreach(FLAG_VAR CMAKE_CXX_FLAGS_RELWITHDEBINFO CMAKE_C_FLAGS_RELWITHDEBINFO) + string(REPLACE "-DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}") + string(REPLACE "/DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}") + endforeach() +endif() + #=============================================================================== # OpenMP for shared-memory parallelism (and GPU support some day!) #=============================================================================== @@ -193,6 +213,26 @@ endif() # Set compile/link flags based on which compiler is being used #=============================================================================== +# When OPENMC_ENABLE_STRICT_FP is enabled, disable compiler optimizations that change +# floating-point results relative to -O0, improving cross-platform and +# cross-optimization-level reproducibility for regression testing: +# -ffp-contract=off Prevents FMA contraction (fused multiply-add changes rounding) +# -fno-builtin Prevents replacing math function calls (pow, exp, log, etc.) +# with builtin versions that may differ from libm +# By default (OFF), the compiler is free to use all optimizations for best +# performance. +if(OPENMC_ENABLE_STRICT_FP) + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-ffp-contract=off SUPPORTS_FP_CONTRACT_OFF) + if(SUPPORTS_FP_CONTRACT_OFF) + list(APPEND cxxflags -ffp-contract=off) + endif() + check_cxx_compiler_flag(-fno-builtin SUPPORTS_NO_BUILTIN) + if(SUPPORTS_NO_BUILTIN) + list(APPEND cxxflags -fno-builtin) + endif() +endif() + # Skip for Visual Studio which has its own configurations through GUI if(NOT MSVC) @@ -266,23 +306,6 @@ else() endif() endif() -#=============================================================================== -# xtensor header-only library -#=============================================================================== - -if(OPENMC_FORCE_VENDORED_LIBS) - add_subdirectory(vendor/xtl) - set(xtl_DIR ${CMAKE_CURRENT_BINARY_DIR}/vendor/xtl) - add_subdirectory(vendor/xtensor) -else() - find_package_write_status(xtensor) - if (NOT xtensor_FOUND) - add_subdirectory(vendor/xtl) - set(xtl_DIR ${CMAKE_CURRENT_BINARY_DIR}/vendor/xtl) - add_subdirectory(vendor/xtensor) - endif() -endif() - #=============================================================================== # Catch2 library #=============================================================================== @@ -332,6 +355,7 @@ endif() #=============================================================================== list(APPEND libopenmc_SOURCES + src/atomic_mass.cpp src/bank.cpp src/boundary_condition.cpp src/bremsstrahlung.cpp @@ -372,6 +396,7 @@ list(APPEND libopenmc_SOURCES src/particle.cpp src/particle_data.cpp src/particle_restart.cpp + src/particle_type.cpp src/photon.cpp src/physics.cpp src/physics_common.cpp @@ -387,6 +412,7 @@ list(APPEND libopenmc_SOURCES src/random_ray/linear_source_domain.cpp src/random_ray/moment_matrix.cpp src/random_ray/source_region.cpp + src/ray.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp @@ -425,7 +451,9 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_musurface.cpp src/tallies/filter_parent_nuclide.cpp src/tallies/filter_particle.cpp + src/tallies/filter_particle_production.cpp src/tallies/filter_polar.cpp + src/tallies/filter_reaction.cpp src/tallies/filter_sph_harm.cpp src/tallies/filter_sptl_legendre.cpp src/tallies/filter_surface.cpp @@ -495,7 +523,7 @@ endif() # target_link_libraries treats any arguments starting with - but not -l as # linker flags. Thus, we can pass both linker flags and libraries together. target_link_libraries(libopenmc ${ldflags} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} - xtensor fmt::fmt ${CMAKE_DL_LIBS}) + fmt::fmt ${CMAKE_DL_LIBS}) if(TARGET pugixml::pugixml) target_link_libraries(libopenmc pugixml::pugixml) @@ -504,7 +532,7 @@ else() endif() if(OPENMC_USE_DAGMC) - target_compile_definitions(libopenmc PRIVATE OPENMC_DAGMC_ENABLED) + target_compile_definitions(libopenmc PUBLIC OPENMC_DAGMC_ENABLED) target_link_libraries(libopenmc dagmc-shared) if(OPENMC_USE_UWUW) @@ -552,6 +580,9 @@ endif() if (OPENMC_ENABLE_COVERAGE) target_compile_definitions(libopenmc PRIVATE COVERAGEBUILD) endif() +if (OPENMC_ENABLE_STRICT_FP) + target_compile_definitions(libopenmc PRIVATE OPENMC_ENABLE_STRICT_FP) +endif() #=============================================================================== # openmc executable @@ -580,9 +611,7 @@ add_custom_command(TARGET libopenmc POST_BUILD #=============================================================================== # Install executable, scripts, manpage, license #=============================================================================== - -configure_file(cmake/OpenMCConfig.cmake.in "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" @ONLY) -configure_file(cmake/OpenMCConfigVersion.cmake.in "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" @ONLY) +include(CMakePackageConfigHelpers) set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/OpenMC) install(TARGETS openmc libopenmc @@ -596,10 +625,24 @@ install(EXPORT openmc-targets NAMESPACE OpenMC:: DESTINATION ${INSTALL_CONFIGDIR}) +configure_package_config_file( + "cmake/OpenMCConfig.cmake.in" + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" + INSTALL_DESTINATION ${INSTALL_CONFIGDIR} +) + +write_basic_package_version_file( + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" + VERSION ${OPENMC_VERSION} + COMPATIBILITY AnyNewerVersion +) + install(FILES - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" - DESTINATION ${INSTALL_CONFIGDIR}) + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" + DESTINATION "${INSTALL_CONFIGDIR}" +) + install(FILES man/man1/openmc.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES LICENSE DESTINATION "${CMAKE_INSTALL_DOCDIR}" RENAME copyright) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/Dockerfile b/Dockerfile index a163a2810..688e5a370 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,11 +33,6 @@ ARG build_libmesh # Set default value of HOME to /root ENV HOME=/root -# Embree variables -ENV EMBREE_TAG='v4.3.1' -ENV EMBREE_REPO='https://github.com/embree/embree' -ENV EMBREE_INSTALL_DIR=$HOME/EMBREE/ - # MOAB variables ENV MOAB_TAG='5.5.1' ENV MOAB_REPO='https://bitbucket.org/fathomteam/moab/' @@ -58,10 +53,11 @@ ENV LIBMESH_REPO='https://github.com/libMesh/libmesh' ENV LIBMESH_INSTALL_DIR=$HOME/LIBMESH # NJOY variables +ENV NJOY_TAG='2016.78' ENV NJOY_REPO='https://github.com/njoy/NJOY2016' # Setup environment variables for Docker image -ENV LD_LIBRARY_PATH=${DAGMC_INSTALL_DIR}/lib:$LD_LIBRARY_PATH \ +ENV LD_LIBRARY_PATH=${DAGMC_INSTALL_DIR}/lib:${LD_LIBRARY_PATH:-} \ OPENMC_ENDF_DATA=/root/endf-b-vii.1 \ DEBIAN_FRONTEND=noninteractive @@ -71,7 +67,7 @@ RUN apt-get update -y && \ apt-get install -y \ python3-pip python-is-python3 wget git build-essential cmake \ mpich libmpich-dev libhdf5-serial-dev libhdf5-mpich-dev \ - libpng-dev python3-venv && \ + libpng-dev libpugixml-dev libfmt-dev catch2 python3-venv && \ apt-get autoremove # create virtual enviroment to avoid externally managed environment error @@ -83,7 +79,7 @@ RUN pip install --upgrade pip # Clone and install NJOY2016 RUN cd $HOME \ - && git clone --single-branch --depth 1 ${NJOY_REPO} \ + && git clone --single-branch -b ${NJOY_TAG} --depth 1 ${NJOY_REPO} \ && cd NJOY2016 \ && mkdir build \ && cd build \ @@ -94,22 +90,12 @@ RUN cd $HOME \ RUN if [ "$build_dagmc" = "on" ]; then \ # Install addition packages required for DAGMC - apt-get -y install libeigen3-dev libnetcdf-dev libtbb-dev libglfw3-dev \ + apt-get -y install \ + libeigen3-dev libnetcdf-dev libtbb-dev libglfw3-dev libembree-dev \ && pip install --upgrade numpy \ && pip install --no-cache-dir setuptools cython \ - # Clone and install EMBREE - && mkdir -p $HOME/EMBREE && cd $HOME/EMBREE \ - && git clone --single-branch -b ${EMBREE_TAG} --depth 1 ${EMBREE_REPO} \ - && mkdir build && cd build \ - && cmake ../embree \ - -DCMAKE_INSTALL_PREFIX=${EMBREE_INSTALL_DIR} \ - -DEMBREE_MAX_ISA=NONE \ - -DEMBREE_ISA_SSE42=ON \ - -DEMBREE_ISPC_SUPPORT=OFF \ - && make 2>/dev/null -j${compile_cores} install \ - && rm -rf ${EMBREE_INSTALL_DIR}/build ${EMBREE_INSTALL_DIR}/embree ; \ # Clone and install MOAB - mkdir -p $HOME/MOAB && cd $HOME/MOAB \ + && mkdir -p $HOME/MOAB && cd $HOME/MOAB \ && git clone --single-branch -b ${MOAB_TAG} --depth 1 ${MOAB_REPO} \ && mkdir build && cd build \ && cmake ../moab -DCMAKE_BUILD_TYPE=Release \ @@ -118,6 +104,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_FORTRAN=OFF \ -DENABLE_BLASLAPACK=OFF \ + -DENABLE_TESTING=OFF \ && make 2>/dev/null -j${compile_cores} install \ && cmake ../moab \ -DENABLE_PYMOAB=ON \ @@ -133,7 +120,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ && mkdir build && cd build \ && cmake ../double-down -DCMAKE_INSTALL_PREFIX=${DD_INSTALL_DIR} \ -DMOAB_DIR=/usr/local \ - -DEMBREE_DIR=${EMBREE_INSTALL_DIR} \ + -DEMBREE_DIR=/usr \ && make 2>/dev/null -j${compile_cores} install \ && rm -rf ${DD_INSTALL_DIR}/build ${DD_INSTALL_DIR}/double-down ; \ # Clone and install DAGMC @@ -147,6 +134,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ -DDOUBLE_DOWN_DIR=${DD_INSTALL_DIR} \ -DCMAKE_PREFIX_PATH=${DD_INSTALL_DIR}/lib \ -DBUILD_STATIC_LIBS=OFF \ + -DBUILD_TESTS=OFF \ && make 2>/dev/null -j${compile_cores} install \ && rm -rf ${DAGMC_INSTALL_DIR}/DAGMC ${DAGMC_INSTALL_DIR}/build ; \ fi diff --git a/LICENSE b/LICENSE index 8a60b66bf..b5dfd1748 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2025 Massachusetts Institute of Technology, UChicago Argonne +Copyright (c) 2011-2026 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 837a39c78..0e15060a3 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -1,14 +1,18 @@ -get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) +@PACKAGE_INIT@ -# Compute the install prefix from this file's location -get_filename_component(_OPENMC_PREFIX "${OpenMC_CMAKE_DIR}/../../.." ABSOLUTE) +include("${CMAKE_CURRENT_LIST_DIR}/OpenMCConfigVersion.cmake") +include(CMakeFindDependencyMacro) + +# Explicitly calculate prefix if it was not generated above +if(NOT DEFINED PACKAGE_PREFIX_DIR) + get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) +endif() + +find_dependency(fmt CONFIG REQUIRED HINTS ${PACKAGE_PREFIX_DIR}) +find_dependency(pugixml CONFIG REQUIRED HINTS ${PACKAGE_PREFIX_DIR}) -find_package(fmt CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(pugixml CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(xtl CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(xtensor CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) if(@OPENMC_USE_DAGMC@) - find_package(DAGMC REQUIRED HINTS @DAGMC_DIR@) + find_dependency(DAGMC REQUIRED HINTS @DAGMC_DIR@) endif() if(@OPENMC_USE_LIBMESH@) @@ -18,20 +22,24 @@ if(@OPENMC_USE_LIBMESH@) pkg_check_modules(LIBMESH REQUIRED @LIBMESH_PC_FILE@>=1.7.0 IMPORTED_TARGET) endif() -find_package(PNG) - -if(NOT TARGET OpenMC::libopenmc) - include("${OpenMC_CMAKE_DIR}/OpenMCTargets.cmake") +if("@PNG_FOUND@") + find_dependency(PNG) endif() if(@OPENMC_USE_MPI@) - find_package(MPI REQUIRED) + find_dependency(MPI REQUIRED) endif() if(@OPENMC_USE_OPENMP@) - find_package(OpenMP REQUIRED) + find_dependency(OpenMP REQUIRED) endif() if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") endif() + +include("${CMAKE_CURRENT_LIST_DIR}/OpenMCTargets.cmake") + +if(NOT OpenMC_FIND_QUIETLY) + message(STATUS "Found OpenMC: ${PACKAGE_VERSION} (found in ${PACKAGE_PREFIX_DIR})") +endif() diff --git a/cmake/OpenMCConfigVersion.cmake.in b/cmake/OpenMCConfigVersion.cmake.in deleted file mode 100644 index 90d345de4..000000000 --- a/cmake/OpenMCConfigVersion.cmake.in +++ /dev/null @@ -1,11 +0,0 @@ -set(PACKAGE_VERSION "@OPENMC_VERSION@") - -# Check whether the requested PACKAGE_FIND_VERSION is compatible -if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") - set(PACKAGE_VERSION_COMPATIBLE FALSE) -else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") - set(PACKAGE_VERSION_EXACT TRUE) - endif() -endif() diff --git a/docs/Makefile b/docs/Makefile index a93338df3..e320432a2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -45,6 +45,7 @@ help: clean: -rm -rf $(BUILDDIR)/* -rm -rf source/pythonapi/generated/ + -rm -rf doxygen/xml html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile new file mode 100644 index 000000000..75702eff7 --- /dev/null +++ b/docs/doxygen/Doxyfile @@ -0,0 +1,13 @@ +# Doxyfile 1.9.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. + +# Difference with default Doxyfile 1.9.1 +PROJECT_NAME = OpenMC +QUIET = YES +WARN_IF_UNDOCUMENTED = NO +INPUT = ../../include/openmc/capi.h +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 2583d51df..e924aa6e2 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -46,43 +46,20 @@ Type Definitions Functions --------- -.. c:function:: int openmc_calculate_volumes() +.. + Once documentation is complete in capi.h, use: + .. doxygenfile:: capi.h + to populate this documentation without using + .. doxygenfunction:: + for every function. - Run a stochastic volume calculation +.. doxygenfunction:: openmc_calculate_volumes - :return: Return status (negative if an error occurred) - :rtype: int +.. doxygenfunction:: openmc_cell_get_fill -.. c:function:: int openmc_cell_get_fill(int32_t index, int* type, int32_t** indices, int32_t* n) +.. doxygenfunction:: openmc_cell_get_id - Get the fill for a cell - - :param int32_t index: Index in the cells array - :param int* type: Type of the fill - :param int32_t** indices: Array of material indices for cell - :param int32_t* n: Length of indices array - :return: Return status (negative if an error occurred) - :rtype: int - -.. c:function:: int openmc_cell_get_id(int32_t index, int32_t* id) - - Get the ID of a cell - - :param int32_t index: Index in the cells array - :param int32_t* id: ID of the cell - :return: Return status (negative if an error occurred) - :rtype: int - -.. c:function:: int openmc_cell_get_temperature(int32_t index, const int32_t* instance, double* T) - - Get the temperature of a cell - - :param int32_t index: Index in the cells array - :param int32_t* instance: Which instance of the cell. If a null pointer is passed, the temperature - of the first instance is returned. - :param double* T: temperature of the cell - :return: Return status (negative if an error occurred) - :rtype: int +.. doxygenfunction:: openmc_cell_get_temperature .. c:function:: int openmc_cell_get_density(int32_t index, const int32_t* instance, double* density) @@ -580,6 +557,279 @@ Functions :return: Return status (negative if an error occurs) :rtype: int +.. c:function:: int openmc_get_plot_index(int32_t id, int32_t* index) + + Get the index in the plots array for a plot with a given ID. + + :param int32_t id: Plot ID + :param int32_t* index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_plot_get_id(int32_t index, int32_t* id) + + Get the ID of a plot. + + :param int32_t index: Index in the plots array + :param int32_t* id: Plot ID + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_plot_set_id(int32_t index, int32_t id) + + Set the ID of a plot. + + :param int32_t index: Index in the plots array + :param int32_t id: Plot ID + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: size_t openmc_plots_size() + + Number of plots currently allocated. + + :return: Number of plots in the plots array + :rtype: size_t + +.. c:function:: int openmc_solidraytrace_plot_create(int32_t* index) + + Create a new solid raytrace plot. + + :param int32_t* index: Index of the newly created plot + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_pixels(int32_t index, int32_t* width, int32_t* height) + + Get output pixel dimensions for a solid raytrace plot. + + :param int32_t index: Index in the plots array + :param int32_t* width: Image width in pixels + :param int32_t* height: Image height in pixels + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_pixels(int32_t index, int32_t width, int32_t height) + + Set output pixel dimensions for a solid raytrace plot. + + :param int32_t index: Index in the plots array + :param int32_t width: Image width in pixels + :param int32_t height: Image height in pixels + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_color_by(int32_t index, int32_t* color_by) + + Get the domain type used for coloring (0=materials, 1=cells). + + :param int32_t index: Index in the plots array + :param int32_t* color_by: Coloring mode + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_color_by(int32_t index, int32_t color_by) + + Set the domain type used for coloring (0=materials, 1=cells). + + :param int32_t index: Index in the plots array + :param int32_t color_by: Coloring mode + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_default_colors(int32_t index) + + Set default random colors for the current ``color_by`` mode. + + :param int32_t index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_all_opaque(int32_t index) + + Mark all domains in the current ``color_by`` mode as opaque. + + :param int32_t index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_opaque(int32_t index, int32_t id, bool visible) + + Set whether a specific domain ID is opaque (visible) in the rendered image. + + :param int32_t index: Index in the plots array + :param int32_t id: Cell/material ID (based on ``color_by``) + :param bool visible: Whether the domain is opaque + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_color(int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b) + + Set RGB color for a specific domain ID. + + :param int32_t index: Index in the plots array + :param int32_t id: Cell/material ID (based on ``color_by``) + :param uint8_t r: Red channel + :param uint8_t g: Green channel + :param uint8_t b: Blue channel + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_color(int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b) + + Get RGB color for a specific domain ID. + + :param int32_t index: Index in the plots array + :param int32_t id: Cell/material ID (based on ``color_by``) + :param uint8_t* r: Red channel + :param uint8_t* g: Green channel + :param uint8_t* b: Blue channel + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_camera_position(int32_t index, double* x, double* y, double* z) + + Get camera position. + + :param int32_t index: Index in the plots array + :param double* x: X coordinate + :param double* y: Y coordinate + :param double* z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_camera_position(int32_t index, double x, double y, double z) + + Set camera position. + + :param int32_t index: Index in the plots array + :param double x: X coordinate + :param double y: Y coordinate + :param double z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_look_at(int32_t index, double* x, double* y, double* z) + + Get camera target point. + + :param int32_t index: Index in the plots array + :param double* x: X coordinate + :param double* y: Y coordinate + :param double* z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_look_at(int32_t index, double x, double y, double z) + + Set camera target point. + + :param int32_t index: Index in the plots array + :param double x: X coordinate + :param double y: Y coordinate + :param double z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_up(int32_t index, double* x, double* y, double* z) + + Get the camera up vector. + + :param int32_t index: Index in the plots array + :param double* x: X component + :param double* y: Y component + :param double* z: Z component + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_up(int32_t index, double x, double y, double z) + + Set the camera up vector. + + :param int32_t index: Index in the plots array + :param double x: X component + :param double y: Y component + :param double z: Z component + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_light_position(int32_t index, double* x, double* y, double* z) + + Get light source position. + + :param int32_t index: Index in the plots array + :param double* x: X coordinate + :param double* y: Y coordinate + :param double* z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_light_position(int32_t index, double x, double y, double z) + + Set light source position. + + :param int32_t index: Index in the plots array + :param double x: X coordinate + :param double y: Y coordinate + :param double z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov) + + Get horizontal field of view in degrees. + + :param int32_t index: Index in the plots array + :param double* fov: Field of view in degrees + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_fov(int32_t index, double fov) + + Set horizontal field of view in degrees. + + :param int32_t index: Index in the plots array + :param double fov: Field of view in degrees + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_diffuse_fraction(int32_t index, double* diffuse_fraction) + + Get diffuse-light fraction. + + :param int32_t index: Index in the plots array + :param double* diffuse_fraction: Diffuse fraction in [0, 1] + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_diffuse_fraction(int32_t index, double diffuse_fraction) + + Set diffuse-light fraction. + + :param int32_t index: Index in the plots array + :param double diffuse_fraction: Diffuse fraction in [0, 1] + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_update_view(int32_t index) + + Recompute internal camera/view transforms after camera changes. + + :param int32_t index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_create_image(int32_t index, uint8_t* data_out, int32_t width, int32_t height) + + Render the plot to an RGB image buffer. + + :param int32_t index: Index in the plots array + :param uint8_t* data_out: Output buffer of shape ``height*width*3`` + :param int32_t width: Image width in pixels + :param int32_t height: Image height in pixels + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_reset() Resets all tally scores @@ -601,6 +851,10 @@ Functions :return: Return status (negative if an error occurs) :rtype: int +.. c:function:: void openmc_run_random_ray() + + Run a random ray simulation + .. c:function:: int openmc_set_n_batches(int32_t n_batches, bool set_max_batches, bool add_statepoint_batch) Set number of batches and number of max batches diff --git a/docs/source/conf.py b/docs/source/conf.py index 826c20022..61740239b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,7 +11,10 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +from pathlib import Path +import subprocess +import sys # Determine if we're on Read the Docs server on_rtd = os.environ.get('READTHEDOCS', None) == 'True' @@ -37,6 +40,7 @@ sys.path.insert(0, os.path.abspath('../..')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + 'breathe', 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', @@ -47,6 +51,8 @@ extensions = [ ] if not on_rtd: extensions.append('sphinxcontrib.rsvgconverter') + doxygen_dir = Path(__file__).parents[1] / 'doxygen' + subprocess.run(['doxygen'], cwd=doxygen_dir, check=True) # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -62,7 +68,7 @@ master_doc = 'index' # General information about the project. project = 'OpenMC' -copyright = '2011-2025, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' +copyright = '2011-2026, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -117,6 +123,11 @@ pygments_style = 'tango' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# -- Options breathe + doxygen ------------------------------------------------- + +breathe_projects = {"OpenMC": "../doxygen/xml"} +breathe_default_project = "OpenMC" +breathe_domain_by_file_pattern = {"*capi.h": "c"} # -- Options for HTML output --------------------------------------------------- diff --git a/docs/source/devguide/agentic-tools.rst b/docs/source/devguide/agentic-tools.rst new file mode 100644 index 000000000..fed377cc0 --- /dev/null +++ b/docs/source/devguide/agentic-tools.rst @@ -0,0 +1,104 @@ +.. _devguide_agentic_tools: + +=========================== +Agentic Development Tools +=========================== + +OpenMC ships a set of tools designed for AI coding agents (such as +`Claude Code`_) that agents can use to navigate and understand the codebase. + +.. _Claude Code: https://claude.ai/code + +Motivation +---------- + +Agentic tools like Claude Code are skilled at using grep to navigate and +understand large code bases. However, grep can only find exact text matches — +it cannot discover code that is *conceptually* related but uses different +naming. Without a "global view" of the codebase that a human developer will +build up over time, the agent is generally blind to any file it hasn't +tokenized fully. While it can grep to see who else calls a function, it +remains blind if other areas might be related but not share identical naming +conventions. + +This problem is mitigated somewhat by using a model with a longer context +window. OpenMC has somewhere around ~1 million tokens of C++ and ~1 million +tokens of python. While Claude Code in early 2026 only has a context window +of 200k tokens, beta versions have extended context windows of 1M tokens, +and it's not unreasonable to assume that models may be available in the near +future that greatly exceed these limits. + +However, even assuming the entire repository can be fit within a context +window, there are several downsides to doing this. +`Model performance degrades significantly as context size increases`_. +Benchmark results are +greatly improved if the model has less garbage to pick through. Additionally, API usage +is typically billed as tokens in/out per turn. As the context file +grows these costs become much larger. As such, there is still significant +motivation to solving the above problem, so as to ensure only relevant +information is drawn into context so as to maximize model performance and +minimize costs. + +Setup +----- + +The tools are registered as an `MCP (Model Context Protocol)`_ server in +``.mcp.json`` at the repository root. AI agents that support MCP (such as +Claude Code) discover them automatically on session start. The underlying +Python scripts can also be run directly from the command line. + +All tools run entirely locally — no API keys or external service accounts are +required. Python dependencies are installed automatically into an isolated +virtual environment at ``.claude/cache/.venv/`` on first use. + +.. _Model performance degrades significantly as context size increases: https://www.anthropic.com/news/claude-opus-4-6 +.. _MCP (Model Context Protocol): https://modelcontextprotocol.io + +RAG Semantic Search +------------------- + +The RAG (Retrieval-Augmented Generation) semantic search addresses this +problem — it finds code by meaning, not just text match, surfacing related code +across subsystems that ``grep`` would miss entirely. Two MCP tools are provided: + +- **openmc_rag_search** — Given a natural-language query, returns the most + relevant code chunks with file paths, line numbers, and a preview. Can search + code, documentation, or both. Can also find code related to a given file. +- **openmc_rag_rebuild** — Rebuilds the search index. Should be called after + pulling new code or switching branches. + +How it works +^^^^^^^^^^^^ + +The search pipeline runs entirely on your local CPU: + +1. **Chunking.** All C++, Python, and RST files are split into overlapping + fixed-size windows (~1000 characters, 25% overlap). This ensures every line + of code appears in at least one chunk and most lines appear in two. + +2. **Embedding.** Each chunk is embedded into a 384-dimensional vector using + the `all-MiniLM-L6-v2`_ sentence-transformer model (22 million parameters). + This model runs on CPU with no GPU required. No API key is needed — the + model weights are downloaded once from Hugging Face and cached locally. + +3. **Indexing.** The vectors are stored in a local LanceDB_ database on disk. + Building the full index takes approximately 5 minutes on a machine with + 10 CPU cores. The index is stored in ``.claude/cache/rag_index/`` and + persists across sessions. + +4. **Searching.** Your query is embedded using the same model, and the closest + chunks are retrieved by vector similarity. Results include the file path, + line range, file type, similarity distance, and a text preview. + +.. _all-MiniLM-L6-v2: https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 +.. _LanceDB: https://lancedb.com + +Requirements +^^^^^^^^^^^^ + +No system dependencies beyond **Python 3.12+** with ``pip``. An internet +connection is required on first use to download the Python packages and +embedding model weights; subsequent runs are fully offline. The Python packages +(``sentence-transformers``, ``lancedb``) and their dependencies (including +PyTorch, ~2GB) are installed automatically into an isolated virtual environment +on first use. diff --git a/docs/source/devguide/contributing.rst b/docs/source/devguide/contributing.rst index cda031389..5199c9438 100644 --- a/docs/source/devguide/contributing.rst +++ b/docs/source/devguide/contributing.rst @@ -111,7 +111,8 @@ The TC consists of the following individuals: - `Paul Romano `_ - `Patrick Shriwise `_ - `Adam Nelson `_ -- `Benoit Forget `_ +- `Jonathan Shimwell `_ +- `John Tramm `_ The Project Lead is Paul Romano. diff --git a/docs/source/devguide/docbuild.rst b/docs/source/devguide/docbuild.rst index f723db06e..cda0307ca 100644 --- a/docs/source/devguide/docbuild.rst +++ b/docs/source/devguide/docbuild.rst @@ -14,6 +14,11 @@ Python API. That is, from the root directory of the OpenMC repository: python -m pip install ".[docs]" +The OpenMC documentation also uses Doxygen to automatically generate its +C/C++ API documentation directly from the docstrings available in the source +code. You will need to have a working installation of Doxygen to generate the +documentation locally. + ----------------------------------- Building Documentation as a Webpage ----------------------------------- diff --git a/docs/source/devguide/index.rst b/docs/source/devguide/index.rst index 2e131e094..53b9f5853 100644 --- a/docs/source/devguide/index.rst +++ b/docs/source/devguide/index.rst @@ -14,6 +14,7 @@ other related topics. contributing workflow + agentic-tools styleguide policies tests diff --git a/docs/source/devguide/policies.rst b/docs/source/devguide/policies.rst index 2cf319987..3644ae822 100644 --- a/docs/source/devguide/policies.rst +++ b/docs/source/devguide/policies.rst @@ -21,8 +21,8 @@ C++ code in OpenMC must conform to the most recent C++ standard that is fully supported in the `version of the gcc compiler `_ that is distributed with the oldest version of Ubuntu that is still within its `standard support period -`_. Ubuntu 20.04 LTS will be supported -through April 2025 and is distributed with gcc 9.3.0, which fully supports the +`_. Ubuntu 22.04 LTS will be supported +through April 2027 and is distributed with gcc 11.4.0, which fully supports the C++17 standard. -------------------- @@ -31,5 +31,5 @@ CMake Version Policy Similar to the C++ standard policy, the minimum supported version of CMake corresponds to whatever version is distributed with the oldest version of Ubuntu -still within its standard support period. Ubuntu 20.04 LTS is distributed with -CMake 3.16. +still within its standard support period. Ubuntu 22.04 LTS is distributed with +CMake 3.22. diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index 2c882b034..a5599566e 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -30,7 +30,7 @@ whenever a file is saved. For example, `Visual Studio Code support for running clang-format. .. note:: - OpenMC's CI uses `clang-format` version 15. A different version of `clang-format` + OpenMC's CI uses `clang-format` version 18. A different version of `clang-format` may produce different line changes and as a result fail the CI test. Miscellaneous diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index f2e39441a..8627cbde2 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -37,6 +37,9 @@ Prerequisites - Some tests require `NJOY `_ to preprocess cross section data. The test suite assumes that you have an ``njoy`` executable available on your :envvar:`PATH`. +- OpenMC should be compiled with ``-DOPENMC_ENABLE_STRICT_FP=on`` to ensure + reproducible floating-point results across platforms and optimization levels. + Without this flag, regression tests may not match reference values. Running Tests ------------- @@ -67,9 +70,11 @@ make sure you have satisfied all the prerequisites above. After you have done that, consider the following: - When building OpenMC, make sure you run CMake with - ``-DCMAKE_BUILD_TYPE=Debug``. Building with a release build will result in - some test failures due to differences in which compiler optimizations are - used. + ``-DOPENMC_ENABLE_STRICT_FP=on``. This prevents the compiler from applying + floating-point optimizations (such as replacing math library calls with + builtins or contracting multiply-add into FMA instructions) that can produce + bit-level differences across platforms and optimization levels. Any + ``CMAKE_BUILD_TYPE`` can be used. - Because tallies involve the sum of many floating point numbers, the non-associativity of floating point numbers can result in different answers especially when the number of threads is high (different order of operations). diff --git a/docs/source/io_formats/collision_track.rst b/docs/source/io_formats/collision_track.rst index a36459754..4123fdac6 100644 --- a/docs/source/io_formats/collision_track.rst +++ b/docs/source/io_formats/collision_track.rst @@ -10,7 +10,7 @@ may also be written after each batch when multiple files are requested (``collision_track.N.h5``) or when the run is performed in parallel. The file contains the information needed to reconstruct each recorded collision. -The current revision of the collision track file format is 1.0. +The current revision of the collision track file format is 1.2. **/** @@ -33,13 +33,13 @@ The current revision of the collision track file format is 1.0. - ``event_mt`` (*int*) -- ENDF MT number identifying the reaction. - ``delayed_group`` (*int*) -- Delayed neutron group index (non-zero for delayed events). - ``cell_id`` (*int*) -- ID of the cell in which the collision occurred. - - ``nuclide_id`` (*int*) -- ZA identifier of the nuclide (ZZZAAAM format). + - ``nuclide_id`` (*int*) -- PDG number of the nuclide (100ZZZAAAM). - ``material_id`` (*int*) -- ID of the material containing the collision site. - ``universe_id`` (*int*) -- ID of the universe containing the collision site. - ``n_collision`` (*int*) -- Collision counter for the particle history. - - ``particle`` (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, 3=positron). - - ``parent_id`` (*int64*) -- Unique ID of the parent particle. - - ``progeny_id`` (*int64*) -- Progeny ID of the particle. + - ``particle`` (*int32_t*) -- Particle type (PDG number). + - ``parent_id`` (*int64_t*) -- Unique ID of the parent particle. + - ``progeny_id`` (*int64_t*) -- Progeny ID of the particle. In an MPI run, OpenMC writes the combined dataset by gathering collision-track entries from all ranks before flushing them to disk, so the final file appears diff --git a/docs/source/io_formats/depletion_results.rst b/docs/source/io_formats/depletion_results.rst index b2a726dad..856993db6 100644 --- a/docs/source/io_formats/depletion_results.rst +++ b/docs/source/io_formats/depletion_results.rst @@ -4,7 +4,7 @@ Depletion Results File Format ============================= -The current version of the depletion results file format is 1.2. +The current version of the depletion results file format is 1.3. **/** @@ -29,11 +29,14 @@ The current version of the depletion results file format is 1.2. - **depletion time** (*double[]*) -- Average process time in [s] spent depleting a material across all burnable materials and, if applicable, MPI processes. + - **keff_search_root** (*double[]*) -- Root of the keff search at the + end of the timestep, if applicable. **/materials//** :Attributes: - **index** (*int*) -- Index used in results for this material - **volume** (*double*) -- Volume of this material in [cm^3] + - **name** (*char[]*) -- Name of this material **/nuclides//** diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index 6d0a37a24..5cd18bef1 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -38,11 +38,9 @@ Each ```` element can have the following attributes or sub-elements: :boundary: The boundary condition for the surface. This can be "transmission", - "vacuum", "reflective", or "periodic". Periodic boundary conditions can - only be applied to x-, y-, and z-planes. Only axis-aligned periodicity is - supported, i.e., x-planes can only be paired with x-planes. Specify which - planes are periodic and the code will automatically identify which planes - are paired together. + "vacuum", "reflective", or "periodic". Specify which planes are + periodic and the code will automatically identify which planes are + paired together. *Default*: "transmission" @@ -318,9 +316,10 @@ the following attributes or sub-elements: *Default*: None :orientation: - The orientation of the hexagonal lattice. The string "x" indicates that two - sides of the lattice are parallel to the x-axis, whereas the string "y" - indicates that two sides are parallel to the y-axis. + The orientation of the hexagonal lattice. The string "x" indicates that each + lattice element has two faces that are perpendicular to the x-axis, whereas + the string "y" indicates that each lattice element has two faces that are + perpendicular to the y-axis. *Default*: "y" @@ -407,24 +406,55 @@ Each ```` element can have the following attributes or sub-eleme *Default*: None - :material_overrides: - This element contains information on material overrides to be applied to the - DAGMC universe. It has the following attributes and sub-elements: + :cell: + Zero or more ```` sub-elements may appear to override properties of + individual DAGMC volumes. Each ```` element supports the following + attributes and sub-elements: - :cell: - Material override information for a single cell. It contains the following - attributes and sub-elements: + :id: + The integer cell ID in the DAGMC geometry to override. Required. - :id: - The cell ID in the DAGMC geometry for which the material override will - apply. + :name: + An optional string label for the cell. - :materials: - A list of material IDs that will apply to instances of the cell. If the - list contains only one ID, it will replace the original material - assignment of all instances of the DAGMC cell. If the list contains more - than one material, each material ID of the list will be assigned to the - various instances of the DAGMC cell. + *Default*: None + + :material: + The material ID to assign to this cell. Use ``void`` for vacuum. Multiple + space-separated IDs may be given to specify a distribmat (distributed + material) assignment. Required. + + :temperature: + Temperature(s) in [K] to assign to the cell. Must be greater than or equal + to 0. Multiple space-separated values may be given. + + *Default*: None + + :density: + Density in [g/cm³] to assign to the cell. Must be greater than 0. Requires a non-void + material fill. Multiple space-separated values may be given. + + *Default*: None + + :volume: + Volume of the cell in [cm³]. + + .. note:: DAGMC can compute cell volumes exactly from the triangulated + mesh surfaces. Specifying a manual volume risks inconsistency + with that capability. + + *Default*: None + + The following standard ```` attributes are **not** supported inside + ```` and will raise an error if present: ``region``, + ``fill``, ``universe``, ``translation``, ``rotation``. + + .. deprecated:: + The ```` sub-element (containing ```` + children with ````) is deprecated. A deprecation warning is + emitted and the overrides are converted to the ```` format at parse + time. It is an error to specify both ```` and + ```` sub-elements on the same ````. *Default*: None diff --git a/docs/source/io_formats/mgxs_library.rst b/docs/source/io_formats/mgxs_library.rst index f7f5387a4..8a26311d1 100644 --- a/docs/source/io_formats/mgxs_library.rst +++ b/docs/source/io_formats/mgxs_library.rst @@ -133,6 +133,10 @@ Temperature-dependent data, provided for temperature K. This dataset is optional. This is a 1-D vector if `representation` is "isotropic", or a 3-D vector if `representation` is "angle" with dimensions of [polar][azimuthal][groups]. + When this data is not available, an approximation using the + group energy boundaries is used. For more information see + the particle speed subsection in the multigroup-data section + of the theory manual. **//K/scatter_data/** diff --git a/docs/source/io_formats/particle_restart.rst b/docs/source/io_formats/particle_restart.rst index 2734f0470..e0fe76b9d 100644 --- a/docs/source/io_formats/particle_restart.rst +++ b/docs/source/io_formats/particle_restart.rst @@ -4,7 +4,7 @@ Particle Restart File Format ============================ -The current version of the particle restart file format is 2.0. +The current version of the particle restart file format is 2.1. **/** @@ -26,8 +26,7 @@ The current version of the particle restart file format is 2.0. - **run_mode** (*char[]*) -- Run mode used, either 'fixed source', 'eigenvalue', or 'particle restart'. - **id** (*int8_t*) -- Unique identifier of the particle. - - **type** (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, - 3=positron) + - **type** (*int32_t*) -- Particle type (PDG number) - **weight** (*double*) -- Weight of the particle. - **energy** (*double*) -- Energy of the particle in eV for continuous-energy mode, or the energy group of the particle for diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index b7874fcf2..7091757f2 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -7,6 +7,19 @@ Settings Specification -- settings.xml All simulation parameters and miscellaneous options are specified in the settings.xml file. +------------------------------- +```` Element +------------------------------- + +The ```` element determines whether the atomic relaxation +cascade, the X-ray fluorescence photons and Auger electrons emitted when an +inner-shell vacancy is filled, is simulated following photoelectric and +incoherent (Compton) scattering interactions. Disabling this can speed up +photon transport calculations where the detailed secondary particle cascade is +not of interest. + + *Default*: true + --------------------- ```` Element --------------------- @@ -85,6 +98,11 @@ sub-elements: A list of strings representing the nuclide, to define specific define specific target nuclide collisions to be banked. + .. note:: + Electron and positron collision-track events are not associated with + a specific nuclide. If a ``nuclides`` entry is specified, these events + are omitted. + *Default*: None :reactions: @@ -277,6 +295,15 @@ ignored for all run modes other than "eigenvalue". *Default*: 1 +------------------------------ +```` Element +------------------------------ + +The ```` element indicates the number of generations to +consider for the Iterated Fission Probability method. + + *Default*: 10 + ---------------------- ```` Element ---------------------- @@ -402,7 +429,25 @@ then, OpenMC will only use up to the :math:`P_1` data. ```` Element -------------------------------- -The ```` element indicates the number of times a particle can split during a history. +The ```` element indicates the number of times a particle +can split during a history. + + *Default*: 1000 + +----------------------------- +```` Element +----------------------------- + +The ```` element indicates the maximum secondary bank size. + + *Default*: 10000 + +------------------------ +```` Element +------------------------ + +The ```` element indicates the maximum number of tracks written to a +track file (per MPI process). *Default*: 1000 @@ -515,6 +560,18 @@ generator during generation of colors in plots. *Default*: 1 +.. _properties_file: + +----------------------------- +```` Element +----------------------------- + + The ``properties_file`` element has no attributes and contains the path to a + properties HDF5 file to load cell temperatures/densities and material + densities. + + *Default*: None + --------------------- ```` Element --------------------- @@ -545,7 +602,7 @@ found in the :ref:`random ray user guide `. *Default*: None - :source: + :ray_source: Specifies the starting ray distribution, and follows the format for :ref:`source_element`. It must be uniform in space and angle and cover the full domain. It does not represent a physical neutron or photon source -- it @@ -553,6 +610,35 @@ found in the :ref:`random ray user guide `. *Default*: None + :adjoint_source: + Specifies an adjoint fixed source for adjoint transport simulations, and + follows the format for :ref:`source_element`. The distributions which make + up the adjoint source are subject to the same restrictions as forward + fixed sources in Random Ray mode. + + *Default*: None + + :adjoint: + Specifies whether to perform adjoint transport. The default is 'False', + corresponding to forward transport. + + *Default*: None + + :volume_estimator: + Specifies choice of volume estimator for the random ray solver. Options + are 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. + + *Default*: None + + :volume_normalized_flux_tallies: + Specifies whether to normalize flux tallies by volume (bool). The + default is 'False'. When enabled, flux tallies will be reported in units + of cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally + region). + + *Default*: None + :sample_method: Specifies the method for sampling the starting ray distribution. This element can be set to "prng" or "halton". @@ -660,14 +746,16 @@ pseudo-random number generator. *Default*: 1 --------------------- -```` Element --------------------- +----------------------------------- +```` Element +----------------------------------- -The ``stride`` element is used to specify how many random numbers are allocated -for each source particle history. - - *Default*: 152,917 + The ``shared_secondary_bank`` element indicates whether to use a shared + secondary particle bank. When enabled, secondary particles are collected into + a global bank, sorted for reproducibility, and load-balanced across MPI ranks + between generations. If not specified, the shared secondary bank is enabled + automatically for fixed-source simulations with weight windows active, and + disabled otherwise. .. _source_element: @@ -689,12 +777,15 @@ attributes/sub-elements: *Default*: 1.0 :type: - Indicator of source type. One of ``independent``, ``file``, ``compiled``, or - ``mesh``. The type of the source will be determined by this attribute if it - is present. + Indicator of source type. One of ``independent``, ``file``, ``compiled``, + ``mesh``, or ``tokamak``. The type of the source will be determined by this + attribute if it is present. :particle: - The source particle type, either ``neutron`` or ``photon``. + The source particle type, specified as a PDG number or a string alias (e.g., + ``neutron``/``n``, ``photon``/``gamma``, ``electron``, ``positron``, + ``proton``/``p``, ``deuteron``/``d``, ``triton``/``t``, ``alpha``, or GNDS + nuclide names like ``Fe57``). *Default*: neutron @@ -784,6 +875,7 @@ attributes/sub-elements: For a "cylindrical" distribution, no parameters are specified. Instead, the ``r``, ``phi``, ``z``, and ``origin`` elements must be specified. + Optionally, the ``r_dir`` and ``z_dir`` elements could be specified. For a "spherical" distribution, no parameters are specified. Instead, the ``r``, ``theta``, ``phi``, and ``origin`` elements must be specified. @@ -815,6 +907,10 @@ attributes/sub-elements: of a univariate probability distribution (see the description in :ref:`univariate`). + :r_dir: + For "cylindrical" distributions, this element specifies the direction + of the cylinder r-axis at phi=0. Defaults to (1.0, 0.0, 0.0). + :theta: For a "spherical" distribution, this element specifies the distribution of theta-coordinates. The necessary sub-elements/attributes are those of a @@ -827,6 +923,10 @@ attributes/sub-elements: sub-elements/attributes are those of a univariate probability distribution (see the description in :ref:`univariate`). + :z_dir: + For "cylindrical" distributions, this element specifies the direction + of the cylinder z-axis. Defaults to (0.0, 0.0, 1.0). + :origin: For "cylindrical and "spherical" distributions, this element specifies the coordinates for the origin of the coordinate system. @@ -844,13 +944,18 @@ attributes/sub-elements: relative source strength of each mesh element or each point in the cloud. :volume_normalized: - For "mesh" spatial distrubtions, this optional boolean element specifies + For "mesh" spatial distributions, this optional boolean element specifies whether the vector of relative strengths should be multiplied by the mesh element volume. This is most common if the strengths represent a source per unit volume. *Default*: false + :bias: + For "mesh" and "cloud" spatial distributions, this optional element + specifies floating point values corresponding to alternative probabilities + for each value/component to use for biased sampling. + :angle: An element specifying the angular distribution of source sites. This element has the following attributes: @@ -883,6 +988,10 @@ attributes/sub-elements: are those of a univariate probability distribution (see the description in :ref:`univariate`). + :bias: + For "isotropic" angular distributions, this optional element specifies a + "mu-phi" angular distribution used for biased sampling. + :energy: An element specifying the energy distribution of source sites. The necessary sub-elements/attributes are those of a univariate probability distribution @@ -906,6 +1015,84 @@ attributes/sub-elements: mesh element and follows the format for :ref:`source_element`. The number of ```` sub-elements should correspond to the number of mesh elements. + For a source with ``type="tokamak"``, the spatial distribution is described by + a Miller-style flux-surface parameterization and the following sub-elements + are used instead of the ``space`` element: + + :major_radius: + The major radius :math:`R_0` of the plasma in [cm]. + + :minor_radius: + The minor radius :math:`a` of the plasma in [cm]. Must be smaller than + ``major_radius``. + + :elongation: + The plasma elongation :math:`\kappa` (must be > 0). + + :triangularity: + The plasma triangularity :math:`\delta` (must be in [-1, 1]). Negative + values describe negative-triangularity plasmas. + + :shafranov_shift: + The Shafranov shift :math:`\Delta` in [cm] (must be >= 0 and less than + ``minor_radius``/2). + + :r_over_a: + A list of normalized minor-radius grid points :math:`r/a`. Must be strictly + increasing, start at 0, and end at 1. + + :emission_density: + A list of neutron emission densities :math:`S(r)` evaluated at each + ``r_over_a`` grid point (arbitrary units, must be non-negative). Only the + shape matters, since the profile is normalized internally. Values are + interpolated linearly between grid points and the profile is refined on an + internal grid for radial sampling. Must have the same length as + ``r_over_a`` and contain at least one positive value. + + :phi_start: + The starting toroidal angle in [rad]. + + *Default*: 0.0 + + :phi_extent: + The toroidal angle extent in [rad]. The source is sampled uniformly in + :math:`[\phi_\text{start},\ \phi_\text{start} + \phi_\text{extent}]`. + + *Default*: :math:`2\pi` + + :n_alpha: + The number of poloidal-angle grid points used to build the sampling CDFs + (must be > 2). Larger values reduce discretization bias; values below 51 + produce a warning. + + *Default*: 101 + + :vertical_shift: + A vertical shift of the plasma center in [cm]. + + *Default*: 0.0 + + :energy: + For a tokamak source, one or more ``energy`` sub-elements specify the + neutron energy distribution(s). Either a single distribution is given (used + at all radii) or exactly one distribution per ``r_over_a`` grid point is + given, in which case the energy is sampled from one of the two + distributions bracketing the sampled radius, selected stochastically with + probability proportional to the proximity of the radius to each grid point + (stochastic interpolation). Each follows the format of a univariate + probability distribution (see :ref:`univariate`). + + :time: + An optional ``time`` sub-element specifying the time distribution of source + particles, following the format of a univariate probability distribution + (see :ref:`univariate`). + + *Default*: particles are born at :math:`t=0` + + .. note:: Biased sampling can be applied to the spatial and energy distributions + of a source by using the ```` sub-element (see + :ref:`univariate` for details on how to specify bias distributions). + :constraints: This sub-element indicates the presence of constraints on sampled source sites (see :ref:`usersguide_source_constraints` for details). It may have @@ -952,17 +1139,19 @@ variable and whose sub-elements/attributes are as follows: :type: The type of the distribution. Valid options are "uniform", "discrete", - "tabular", "maxwell", "watt", and "mixture". The "uniform" option produces - variates sampled from a uniform distribution over a finite interval. The - "discrete" option produces random variates that can assume a finite number - of values (i.e., a distribution characterized by a probability mass function). - The "tabular" option produces random variates sampled from a tabulated - distribution where the density function is either a histogram or + "tabular", "maxwell", "watt", "mixture", and "decay_spectrum". The "uniform" + option produces variates sampled from a uniform distribution over a finite + interval. The "discrete" option produces random variates that can assume a + finite number of values (i.e., a distribution characterized by a probability + mass function). The "tabular" option produces random variates sampled from a + tabulated distribution where the density function is either a histogram or linearly-interpolated between tabulated points. The "watt" option produces random variates is sampled from a Watt fission spectrum (only used for energies). The "maxwell" option produce variates sampled from a Maxwell - fission spectrum (only used for energies). The "mixture" option produces samples - from univariate sub-distributions with given probabilities. + fission spectrum (only used for energies). The "mixture" option produces + samples from univariate sub-distributions with given probabilities. The + "decay_spectrum" option produces photon energies sampled from decay photon + spectra in a depletion chain (only used for energies). *Default*: None @@ -980,6 +1169,10 @@ variable and whose sub-elements/attributes are as follows: :math:`(x,p)` pairs defining the discrete/tabular distribution. All :math:`x` points are given first followed by corresponding :math:`p` points. + For a "decay_spectrum" distribution, ``parameters`` gives the atom densities + in [atom/b-cm] for the nuclides listed in the ``nuclides`` element, in the + same order. + For a "watt" distribution, ``parameters`` should be given as two real numbers :math:`a` and :math:`b` that parameterize the distribution :math:`p(x) dx = c e^{-x/a} \sinh \sqrt{b \, x} dx`. @@ -998,13 +1191,41 @@ variable and whose sub-elements/attributes are as follows: *Default*: histogram :pair: - For a "mixture" distribution, this element provides a distribution and its corresponding probability. + For a "mixture" distribution, this element provides a distribution and its + corresponding probability. :probability: - An attribute or ``pair`` that provides the probability of a univariate distribution within a "mixture" distribution. + An attribute or ``pair`` that provides the probability of a univariate + distribution within a "mixture" distribution. :dist: - This sub-element of a ``pair`` element provides information on the corresponding univariate distribution. + This sub-element of a ``pair`` element provides information on the + corresponding univariate distribution. + +:volume: + For a "decay_spectrum" distribution, this attribute specifies the source + region volume in cm\ :sup:`3`. It is used together with atom densities to + determine the absolute photon emission rate. When a source uses a + "decay_spectrum" energy distribution, the source strength is set from this + emission rate. + +:nuclides: + For a "decay_spectrum" distribution, this element specifies a + whitespace-separated list of nuclide names contributing to the decay photon + source. The atom densities for these nuclides are given by the ``parameters`` + element in the same order. Nuclides are resolved against the depletion chain, + and nuclides without decay photon spectra do not contribute to the + distribution. + +:bias: + This optional element specifies a biased distribution for importance sampling. + For continuous distributions, the ``bias`` element should contain another + univariate distribution with the same support (interval) as the parent + distribution. For discrete distributions, the ``bias`` element should contain + floating point values corresponding to alternative probabilities for each + value/component to be used for biased sampling. + + *Default*: None --------------------------------------- ```` Element @@ -1016,23 +1237,6 @@ based on constraints. *Default*: 0.05 -------------------------- -```` Element -------------------------- - -The ```` element indicates at what batches a state point file -should be written. A state point file can be used to restart a run or to get -tally results at any batch. The default behavior when using this tag is to -write out the source bank in the state_point file. This behavior can be -customized by using the ```` element. This element has the -following attributes/sub-elements: - - :batches: - A list of integers separated by spaces indicating at what batches a state - point file should be written. - - *Default*: Last batch only - -------------------------- ```` Element -------------------------- @@ -1082,6 +1286,32 @@ attributes/sub-elements: *Default*: false +------------------------- +```` Element +------------------------- + +The ```` element indicates at what batches a state point file +should be written. A state point file can be used to restart a run or to get +tally results at any batch. The default behavior when using this tag is to +write out the source bank in the state_point file. This behavior can be +customized by using the ```` element. This element has the +following attributes/sub-elements: + + :batches: + A list of integers separated by spaces indicating at what batches a state + point file should be written. + + *Default*: Last batch only + +-------------------- +```` Element +-------------------- + +The ``stride`` element is used to specify how many random numbers are allocated +for each source particle history. + + *Default*: 152,917 + ------------------------------ ```` Element ------------------------------ @@ -1169,6 +1399,23 @@ attributes/sub-elements: are not eligible to store any particles when using ``cell``, ``cellfrom`` or ``cellto`` attributes. It is recommended to use surface IDs instead. +------------------------------------ +```` Element +------------------------------------ + +The ```` element specifies the surface flux cosine cutoff. + + *Default*: 0.001 + +----------------------------------- +```` Element +----------------------------------- + +The ```` element specifies the surface flux cosine +substitution ratio. + + *Default*: 0.5 + ------------------------------ ```` Element ------------------------------ @@ -1352,6 +1599,15 @@ has the following attributes/sub-elements: for fixed source and small criticality calculations, but is very optimistic for highly coupled full-core reactor problems. +------------------------------------- +```` Element +------------------------------------- + +The ```` element indicates whether to sample among +multiple sources uniformly, applying their strengths as weights to sampled +particles. + + *Default*: False ------------------------ ```` Element @@ -1364,6 +1620,16 @@ Agency Monte Carlo Performance Benchmark Problem," Proceedings of *Physor 2012*, Knoxville, TN (2012). The mesh should cover all possible fissionable materials in the problem and is specified using a :ref:`mesh_element`. +------------------------------- +```` Element +------------------------------- + +The ```` element indicates whether to produce decay photons +from neutron reactions instead of prompt photons. This is used in conjunction +with the direct 1-step method for shutdown dose rate calculations. + + *Default*: False + .. _verbosity: ----------------------- @@ -1465,7 +1731,8 @@ sub-elements/attributes: *Default*: None :particle_type: - The particle that the weight windows will apply to (e.g., 'neutron') + The particle that the weight windows will apply to, specified as a PDG + code or string (e.g., ``neutron``). *Default*: 'neutron' @@ -1525,7 +1792,8 @@ mesh-based weight windows. *Default*: None :particle_type: - The particle that the weight windows will apply to (e.g., 'neutron') + The particle that the weight windows will apply to, specified as a PDG + code or string (e.g., ``neutron``). *Default*: neutron @@ -1569,6 +1837,14 @@ mesh-based weight windows. *Default*: 5.0 + For FW-CADIS: + + :targets: + A sequence of IDs corresponding to the tallies which cover phase + space regions of interest for local variance reduction. + + *Default*: None + --------------------------------------- ```` Element --------------------------------------- @@ -1594,3 +1870,21 @@ following sub-elements/attributes: The ``weight_windows_file`` element has no attributes and contains the path to a weight windows HDF5 file to load during simulation initialization. + +------------------------------- +```` Element +------------------------------- + + The ``weight_windows_on`` element indicates whether weight windows are + enabled. + + *Default*: False + +---------------------------------- +```` Element +---------------------------------- + + The ``write_initial_source`` element indicates whether to write the initial + source distribution to file. + + *Default*: False diff --git a/docs/source/io_formats/source.rst b/docs/source/io_formats/source.rst index 4ce2229ed..2e81e0a3f 100644 --- a/docs/source/io_formats/source.rst +++ b/docs/source/io_formats/source.rst @@ -15,6 +15,8 @@ following the same format. **/** :Attributes: - **filetype** (*char[]*) -- String indicating the type of file. + - **version** (*int[2]*) -- Major and minor version of the source + file format. :Datasets: @@ -22,5 +24,5 @@ following the same format. particle. The compound type has fields ``r``, ``u``, ``E``, ``time``, ``wgt``, ``delayed_group``, ``surf_id`` and ``particle``, which represent the position, direction, energy, time, weight, - delayed group, surface ID, and particle type (0=neutron, 1=photon, - 2=electron, 3=positron), respectively. + delayed group, surface ID, and particle type (PDG number), + respectively. diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 2309643dc..7d7765b84 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -4,7 +4,7 @@ State Point File Format ======================= -The current version of the statepoint file format is 18.1. +The current version of the statepoint file format is 18.2. **/** @@ -56,8 +56,8 @@ The current version of the statepoint file format is 18.1. ``time``, ``wgt``, ``delayed_group``, ``surf_id``, and ``particle``, which represent the position, direction, energy, time, weight, delayed group, surface ID, and particle type - (0=neutron, 1=photon, 2=electron, 3=positron), respectively. Only - present when `run_mode` is 'eigenvalue'. + (PDG number), respectively. Only present when `run_mode` is + 'eigenvalue'. **/tallies/** diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 9f29a949a..dc57e5775 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -142,9 +142,9 @@ attributes/sub-elements: :type: The type of the filter. Accepted options are "cell", "cellfrom", - "cellborn", "surface", "material", "universe", "energy", "energyout", "mu", - "polar", "azimuthal", "mesh", "distribcell", "delayedgroup", - "energyfunction", and "particle". + "cellborn", "surface", "material", "universe", "energy", "energyout", + "mu", "polar", "azimuthal", "mesh", "distribcell", "delayedgroup", + "energyfunction", "particle", and "particleproduction". :bins: A description of the bins for each type of filter can be found in @@ -318,8 +318,34 @@ should be set to: they use ``energy`` and ``y``. :particle: - A list of integers indicating the type of particles to tally ('neutron' = 1, - 'photon' = 2, 'electron' = 3, 'positron' = 4). + A list of particle identifiers to tally, specified as strings (e.g., + ``neutron``, ``photon``, ``He4``) or as integer PDG numbers. + +:particleproduction: + This filter tallies secondary particles produced in reactions, binned by + particle type and, optionally, by energy. Unlike other energy filters, the + weight applied is the weight of the secondary particle. To obtain secondary + particle production rates, use this filter with the ``events`` score. + + The filter uses the following sub-elements instead of ``bins``: + + :particles: + A space-separated list of secondary particle types to tally (e.g., + ``photon``, ``neutron``, ``electron``). + + :energies: + An optional monotonically increasing list of energy boundaries in [eV] + for binning the secondary particle energies. If omitted, total production + is tallied without energy binning. + + For example, to tally photon and neutron production in three energy groups: + + .. code-block:: xml + + + photon neutron + 0.0 1.0e5 1.0e6 20.0e6 + ------------------ ```` Element diff --git a/docs/source/io_formats/track.rst b/docs/source/io_formats/track.rst index a97d75e58..9c05ac4c3 100644 --- a/docs/source/io_formats/track.rst +++ b/docs/source/io_formats/track.rst @@ -4,7 +4,7 @@ Track File Format ================= -The current revision of the particle track file format is 3.0. +The current revision of the particle track file format is 3.1. **/** @@ -32,6 +32,5 @@ The current revision of the particle track file format is 3.0. the array for each primary/secondary particle. The last offset should match the total size of the array. - - **particles** (*int[]*) -- Particle type for each - primary/secondary particle (0=neutron, 1=photon, - 2=electron, 3=positron). + - **particles** (*int32_t[]*) -- Particle type for + each primary/secondary particle (PDG number). diff --git a/docs/source/license.rst b/docs/source/license.rst index 0a90a7441..1ec9fc04a 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2025 Massachusetts Institute of Technology, UChicago Argonne +Copyright © 2011-2026 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index a66abb3ed..764c4c628 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -289,6 +289,48 @@ sections. This allows flexibility for the model to use highly anisotropic scattering information in the water while the fuel can be simulated with linear or even isotropic scattering. +Particle Speed +-------------- + +When using a multigroup representation of cross sections, the particle speed has +meaning only in an average sense. The particle speed is important when modeling +dynamic behavior. OpenMC calculates the particle speed using the inverse +velocity multigroup data if it is available. If such data is not available, +OpenMC uses an approximate velocity using the group energy bounds in the +following way: + +.. math:: + + \frac{1}{v_g} = \int_{E_{\text{min}}^g}^{E_{\text{max}}^g} \frac{1}{v(E)} \frac{\alpha}{E} dE + +Where :math:`E_{\text{min}}^g` and :math:`E_{\text{max}}^g` are the group energy +boundaries for group :math:`g`. :math:`v(E)` is the neutron velocity calculated +using relativistic kinematics, :math:`\alpha` is a normalization constant for the +:math:`\frac{1}{E}` spectrum. + +This equation is valid when inside the group boundaries the neutron spectrum +follows a typical :math:`\frac{1}{E}` slowing down spectrum. This assumption is +widely used when generating fine group neutron cross section data libraries from +continuous energy data. + +The solution to this equation is: + +.. math:: + + \frac{1}{v_g} = \frac{1}{c \log\left(\frac{E_{\text{max}}^g}{E_{\text{min}}^g}\right)} + \left[ 2(\operatorname{arctanh}(k_{\text{max}}^{-1}) - \operatorname{arctanh}(k_{\text{min}}^{-1})) + - (k_{\text{max}}-k_{\text{min}}) \right] + +where :math:`c` is the speed of light and :math:`k_{\text{max}}`, +:math:`k_{\text{min}}` are defined by a change of variables: + +.. math:: + + k = \sqrt{1+\frac{2 m_n c^2}{E}} + +where :math:`E` is the particle kinetic energy and :math:`m_n` is the neutron +rest mass. + .. _logarithmic mapping technique: https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf .. _Hwang: https://doi.org/10.13182/NSE87-A16381 diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 5e17316aa..8bc2a0a1b 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -1081,28 +1081,32 @@ lifetimes. In OpenMC, the random ray adjoint solver is implemented simply by transposing the scattering matrix, swapping :math:`\nu\Sigma_f` and :math:`\chi`, and then -running a normal transport solve. When no external fixed source is present, no -additional changes are needed in the transport process. However, if an external -fixed forward source is present in the simulation problem, then an additional -step is taken to compute the accompanying fixed adjoint source. In OpenMC, the -adjoint flux does *not* represent a response function for a particular detector -region. Rather, the adjoint flux is the global response, making it appropriate -for use with weight window generation schemes for global variance reduction. -Thus, if using a fixed source, the external source for the adjoint mode is -simply computed as being :math:`1 / \phi`, where :math:`\phi` is the forward -scalar flux that results from a normal forward solve (which OpenMC will run -first automatically when in adjoint mode). The adjoint external source will be -computed for each source region in the simulation mesh, independent of any -tallies. The adjoint external source is always flat, even when a linear -scattering and fission source shape is used. When in adjoint mode, all reported -results (e.g., tallies, eigenvalues, etc.) are derived from the adjoint flux, -even when the physical meaning is not necessarily obvious. These values are -still reported, though we emphasize that the primary use case for adjoint mode -is for producing adjoint flux tallies to support subsequent perturbation studies -and weight window generation. +running a normal transport solve. When no external fixed forward source is +present, or if an adjoint fixed source is specifically provided, no additional +changes are needed in the transport process. This adjoint source can +correspond, for example, to a detector response function in a particular +region. However, if an external fixed forward source is present in the +simulation problem without an adjoint fixed source, an additional step is taken +to compute the accompanying forward-weighted adjoint source. In this case, the +adjoint flux does *not* represent the importance of locations in phase space to +detector response; rather, the "response" in question is a uniform distribution +of Monte Carlo particle density, making the importance provided by the adjoint +flux appropriate for use with weight window generation schemes for global +variance reduction. Thus, if using a fixed source, the forward-weighted +external source for adjoint mode is simply computed as being :math:`1 / \phi`, +where :math:`\phi` is the forward scalar flux that results from a normal +forward solve (which OpenMC will run first automatically when in adjoint mode). +The adjoint external source will be computed for each source region in the +simulation mesh, independent of any tallies. The adjoint external source is +always flat, even when a linear scattering and fission source shape is used. -Note that the adjoint :math:`k_{eff}` is statistically the same as the forward -:math:`k_{eff}`, despite the flux distributions taking different shapes. +When in adjoint mode, all reported results (e.g., tallies, eigenvalues, etc.) +are derived from the adjoint flux, even when the physical meaning is not +necessarily obvious. These values are still reported, though we emphasize that +the primary use case for adjoint mode is for producing adjoint flux tallies to +support subsequent perturbation studies and weight window generation. Note +however that the adjoint :math:`k_{eff}` is statistically the same as the +forward :math:`k_{eff}`, despite the flux distributions taking different shapes. --------------------------- Fundamental Sources of Bias diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index 27a3f873a..f2c22ab22 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -205,7 +205,71 @@ had a collision at every event. Thus, for tallies with outgoing-energy filters or for tallies of scattering moments (which require the scattering cosine of the change-in-angle), we must use an analog estimator. -.. TODO: Add description of surface current tallies +----------------------------------- +Surface-Integrated Flux and Current +----------------------------------- + +Surface tallies allow you to measure particle behavior as they cross specific +boundaries in your geometry. Unlike volume tallies, which integrate over a +volumetric region, surface tallies capture the current or flux passing through a +surface. Surface tallies are estimated using an analog estimator. + +Current Score +------------- + +When tallying the current across a surface, we simply count the weight of +particles that cross the surface of interest: + + +.. math:: + :label: analog-current-estimator + + J = \frac{1}{W} \sum_{i \in S} w_i. + +where :math:`J` is the area-integrated current passing through surface +:math:`S`, :math:`W` is the total starting weight of the particles, and +:math:`w_i` is the weight of the particle as it crosses the surface :math:`S`. + +Flux Score +---------- + +When tallying flux over a surface, we use the relationship between current and +flux: + + +.. math:: + :label: surface-flux-estimator + + \phi_S = \frac{1}{W} \sum_{i \in S} \frac{w_i}{|\mu|}. + +where :math:`\phi_S` is the area-integrated flux over surface :math:`S`, +:math:`W` is the total starting weight of the particles, :math:`w_i` is the +weight of the particle as it crosses the surface :math:`S` and :math:`\mu` is +the cosine of angle between the particle direction and the surface normal. + +This equation diverges when the particle crossing the surface is nearly parallel +to it (that is, as :math:`\mu` approaches zero). To remove this divergence, +OpenMC scores: + +.. math:: + :label: modified-surface-flux-estimator + + \phi_S = \frac{1}{W} \sum_{i \in S} w_i f(\mu). + +and the function :math:`f` is defined by: + +.. math:: + f(\mu) = \begin{cases} + \frac{1}{|\mu|} & |\mu| > \mu_\text{cut} \\ + \frac{1}{c\mu_\text{cut}} & |\mu| \le \mu_\text{cut} + \end{cases} + +where :math:`\mu_\text{cut}` is the grazing cosine cutoff and :math:`c` is the +cosine substitution ratio. The parameters :math:`\mu_\text{cut}` and :math:`c` +can be set by the user via the :attr:`openmc.Settings.surface_grazing_cutoff` +and :attr:`openmc.Settings.surface_grazing_ratio` attributes, respectively. The +default values for these parameters are 0.001 and 0.5 as recommended by +`Favorite, Thomas, and Booth `_. .. _tallies_statistics: diff --git a/docs/source/methods/variance_reduction.rst b/docs/source/methods/variance_reduction.rst index 353ae5077..7778e0714 100644 --- a/docs/source/methods/variance_reduction.rst +++ b/docs/source/methods/variance_reduction.rst @@ -22,12 +22,14 @@ not experience a single scoring event, even after billions of analog histories. Variance reduction techniques aim to either flatten the global uncertainty distribution, such that all regions of phase space have a fairly similar uncertainty, or to reduce the uncertainty in specific locations (such as a -detector). There are two strategies available in OpenMC for variance reduction: -the Monte Carlo MAGIC method and the FW-CADIS method. Both strategies work by -developing a weight window mesh that can be utilized by subsequent Monte Carlo -solves to split particles heading towards areas of lower flux densities while -terminating particles in higher flux regions---all while maintaining a fair -game. +detector). There are three strategies available in OpenMC for variance +reduction: weight windows generated via the MAGIC method or the FW-CADIS method, +and source biasing. Both weight windowing strategies work by developing a mesh +that can be utilized by subsequent Monte Carlo solves to split particles heading +towards areas of lower flux densities while terminating particles in higher flux +regions. In contrast, source biasing modifies source site sampling behavior to +preferentially track particles more likely to reach phase space regions of +interest. ------------ MAGIC Method @@ -80,8 +82,8 @@ where it was born from. The Forward-Weighted Consistent Adjoint Driven Importance Sampling method, or `FW-CADIS method `_, produces weight windows -for global variance reduction given adjoint flux information throughout the -entire domain. The weight window lower bound is defined in Equation +for global or local variance reduction given adjoint flux information throughout +the entire domain. The weight window lower bound is defined in Equation :eq:`fw_cadis`, and also involves a normalization step not shown here. .. math:: @@ -132,3 +134,83 @@ aware of this. :label: variance_fom \text{FOM} = \frac{1}{\text{Time} \times \sigma^2} + +Finally, one unique capability of the FW-CADIS weight window generator is to +produce weight windows for local variance reduction, given a list of the +responses of interest. This is controlled by optionally specifying target +tallies from the :class:`openmc.model.Model` to the +:class:`openmc.WeightWindowGenerator`, as illustrated in the +:ref:`user guide`. If target tallies for local variance +reduction are supplied, then the adjoint sources are only populated after the +initial forward simulation in the source regions associated with those tallies. +In other regions, the adjoint source term is instead set to zero. The Random +Ray solver then determines the adjoint flux map used to generate FW-CADIS +weight windows following the usual technique. + +.. _methods_source_biasing: + +-------------- +Source Biasing +-------------- + +In contrast to the previous two methods that introduce population controls +during transport, source biasing modifies the sampling of the external source +distribution. The basic premise of the technique is that for each spatial, +angular, energy, or time distribution of a source, an additional distribution +can be specified provided that the two share a common support (set of points +where the distribution is nonzero). Samples are then drawn from this "bias" +distribution, which can be chosen to preferentially direct particles towards +phase space regions of interest. In order to avoid biasing the tally results, +however, a weight adjustment is applied to each sampled site as described below. + +Assume that the unbiased probability density function of a random variable +:math:`X:x \rightarrow \mathbb{R}` is given by :math:`f(x)`, but that using the +biased distribution :math:`g(x)` will result in a greater number of particle +trajectories reaching some phase space region of interest. Then a sample +:math:`x_0` may be drawn from :math:`g(x)` while maintaining a fair game, +provided that its weight is adjusted as: + +.. math:: + :label: source_bias + + w = w_0 \times \frac{f(x_0)}{g(x_0)} + +where :math:`w_0` is the weight of an unbiased sample from :math:`f(x)`, +typically unity. + +Returning now to Equation :eq:`source_bias`, the requirement for common support +becomes evident. If :math:`\mathrm{supp} (g)` fully contains but is not +identical to :math:`\mathrm{supp} (f)`, then some samples from :math:`g(x)` will +correspond to points where :math:`f(x) = 0`. Thus these source sites would be +assigned a starting weight of 0, meaning the particles would be killed +immediately upon transport, effectively wasting computation time. Conversely, if +:math:`\mathrm{supp} (g)` is fully contained by but not identical to +:math:`\mathrm{supp} (f)`, the contributions of some regions outside +:math:`\mathrm{supp} (g)` will not be counted towards the integral, potentially +biasing the tally. The weight assigned to such points would be undefined since +:math:`g(x) = \mathbf{0}` at these points. + +When an independent source is sampled in OpenMC, the particle's coordinate in +each variable of phase space :math:`(\mathbf{r},\mathbf{\Omega},E,t)` is +successively drawn from an independent probability distribution. Multiple +variables can be biased, in which case the resultant weight :math:`w` applied to +the particle is the product of the weights assigned from all sampled +distributions: space, angle, energy, and time, as shown in Equation +:eq:`tot_wgt`. + +.. math:: + :label: tot_wgt + + w = w_r \times w_{\Omega} \times w_E \times w_t + +Finally, source biasing and weight windows serve different purposes. Source +biasing changes how particles are born, allowing the initial source sites to be +sampled preferentially from important regions of phase space (space, angle, +energy, and time) with an accompanying weight adjustment. Weight windows, by +contrast, apply population control during transport (splitting and Russian +roulette) to help particles reach and contribute in important regions as they +move through the system. Because particle transport proceeds as usual after a +biased source is sampled, particle attenuation in optically thick regions +outside the source volume will not be affected by source biasing; in such +scenarios, transport biasing techniques such as weight windows are often more +effective. diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index ce2f6f0f8..f06a1eba4 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -26,6 +26,7 @@ Simulation Settings openmc.FileSource openmc.CompiledSource openmc.MeshSource + openmc.TokamakSource openmc.SourceParticle openmc.VolumeCalculation openmc.Settings @@ -132,6 +133,7 @@ Constructing Tallies openmc.MeshSurfaceFilter openmc.EnergyFilter openmc.EnergyoutFilter + openmc.ParticleProductionFilter openmc.MuFilter openmc.MuSurfaceFilter openmc.PolarFilter @@ -148,6 +150,7 @@ Constructing Tallies openmc.ZernikeRadialFilter openmc.ParentNuclideFilter openmc.ParticleFilter + openmc.ReactionFilter openmc.MeshMaterialVolumes openmc.Trigger openmc.TallyDerivative @@ -176,7 +179,8 @@ Geometry Plotting :nosignatures: :template: myclass.rst - openmc.Plot + openmc.SlicePlot + openmc.VoxelPlot openmc.WireframeRayTracePlot openmc.SolidRayTracePlot openmc.Plots diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 67eca0094..dab45481d 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -40,6 +40,7 @@ Functions reset_timers run run_in_memory + run_random_ray sample_external_source simulation_finalize simulation_init @@ -81,12 +82,15 @@ Classes Nuclide ParentNuclideFilter ParticleFilter + ParticleProductionFilter PolarFilter + ReactionFilter RectilinearMesh RegularMesh SpatialLegendreFilter SphericalHarmonicsFilter SphericalMesh + SolidRayTracePlot SurfaceFilter Tally TemporarySession @@ -124,6 +128,12 @@ Data :type: dict +.. data:: plots + + Mapping of plot ID to :class:`openmc.lib.SolidRayTracePlot` instances. + + :type: dict + .. data:: nuclides Mapping of nuclide name to :class:`openmc.lib.Nuclide` instances. diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 1eaf90c97..9d47430f7 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -71,6 +71,8 @@ Core Functions isotopes kalbach_slope linearize + mass_attenuation_coefficient + mass_energy_absorption_coefficient thin water_density zam diff --git a/docs/source/pythonapi/mgxs.rst b/docs/source/pythonapi/mgxs.rst index 392914b36..4141aa0a0 100644 --- a/docs/source/pythonapi/mgxs.rst +++ b/docs/source/pythonapi/mgxs.rst @@ -11,6 +11,16 @@ Module Variables .. autodata:: openmc.mgxs.GROUP_STRUCTURES :annotation: +Functions ++++++++++ + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + + openmc.mgxs.convert_flux_groups + Classes +++++++ diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index c8318ba86..2f2e2835a 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -22,6 +22,7 @@ Univariate Probability Distributions openmc.stats.Legendre openmc.stats.Mixture openmc.stats.Normal + openmc.stats.DecaySpectrum .. autosummary:: :toctree: generated @@ -29,6 +30,7 @@ Univariate Probability Distributions :template: myfunction.rst openmc.stats.delta_function + openmc.stats.fusion_neutron_spectrum openmc.stats.muir Angular Distributions @@ -67,3 +69,4 @@ Spatial Distributions :template: myfunction.rst openmc.stats.spherical_uniform + openmc.stats.cylindrical_uniform diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 0f887940e..6cb774b5b 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -119,7 +119,7 @@ packages should be installed, for example in Homebrew via: .. code-block:: sh - brew install llvm cmake xtensor hdf5 python libomp libpng + brew install llvm cmake hdf5 python libomp libpng The compiler provided by the above LLVM package should be used in place of the one provisioned by XCode, which does not support the multithreading library used diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index 398680e74..19f1e5d5f 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -132,8 +132,9 @@ can be run:: r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes) If not specified otherwise, a photon transport calculation is run at each time -in the depletion schedule. That means in the case above, we would see three -photon transport calculations. To specify specific times at which photon +in the depletion schedule for which a decay photon source exists. Times without +a decay photon source, such as the initial state of a model containing only +stable nuclides, are omitted. To specify particular times at which photon transport calculations should be run, pass the ``photon_time_indices`` argument. For example, if we wanted to run a photon transport calculation only on the last time (after the 5 hour decay), we would run:: @@ -141,6 +142,19 @@ time (after the 5 hour decay), we would run:: r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes, photon_time_indices=[2]) +To attribute photon tally results to their parent radionuclides, set +``by_parent_nuclide=True``. This automatically adds a +:class:`openmc.ParentNuclideFilter` to every photon tally that does not already +have one. The filter bins are the union of radionuclides contributing to the +prepared decay photon sources. The resulting bins can be used directly when +inspecting the tally results:: + + r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes, + photon_time_indices=[2], by_parent_nuclide=True) + + photon_tally = r2s.results['photon_tallies'][2][0] + tally_by_parent = photon_tally.get_pandas_dataframe() + After an R2S calculation has been run, the :class:`~openmc.deplete.R2SManager` instance will have a ``results`` dictionary that allows you to directly access results from each of the steps. It will also write out all the output files into @@ -148,12 +162,13 @@ a directory that is named "r2s_/". The ``output_dir`` argument to the :meth:`~openmc.deplete.R2SManager.run` method enables you to override the default output directory name if desired. -The :meth:`~openmc.deplete.R2SManager.run` method actually runs three +The :meth:`~openmc.deplete.R2SManager.run` method actually runs four lower-level methods under the hood:: r2s.step1_neutron_transport(...) r2s.step2_activation(...) - r2s.step3_photon_transport(...) + r2s.step3_photon_source(...) + r2s.step4_photon_transport(...) For users looking for more control over the calculation, these lower-level methods can be used in lieu of the :meth:`openmc.deplete.R2SManager.run` method. @@ -190,6 +205,25 @@ we would run:: r2s.run(timesteps, source_rates, mat_vol_kwargs={'n_samples': 10_000_000}) +It is also possible to use multiple meshes by passing a list of meshes instead +of a single mesh. This can be useful, for example, when different regions of the +model require different mesh resolutions. The meshes are assumed to be +**non-overlapping**; each element--material combination across all meshes is +treated as an independent activation region, and all meshes are handled in a +single neutron transport solve. For example:: + + # Fine mesh near the activation target + mesh_fine = openmc.RegularMesh() + mesh_fine.dimension = (10, 10, 10) + ... + + # Coarse mesh for the surrounding region + mesh_coarse = openmc.RegularMesh() + mesh_coarse.dimension = (5, 5, 5) + ... + + r2s = openmc.deplete.R2SManager(model, [mesh_fine, mesh_coarse]) + Direct 1-Step (D1S) Calculations ================================ @@ -236,4 +270,3 @@ relevant tallies. This can be done with the aid of the # Apply time correction factors tally = d1s.apply_time_correction(dose_tally, factors, time_index) - diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 261900ce6..9a22adf01 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -449,3 +449,42 @@ to transfer xenon from one material to another, you'd use:: ... integrator.add_transfer_rate(mat1, ['Xe'], 0.1, destination_material=mat2) + +Comparing to Other Codes +======================== + +Comparing depletion results from OpenMC with those from another code, such as +MCNP or Serpent, requires more than constructing equivalent transport models. +At each depletion step, differences in the transport solution, nuclear data, +reaction rate normalization, and numerical integration can all affect the +result. Small differences can also accumulate over successive depletion steps. + +For a meaningful comparison, align as many of the following inputs and methods +as possible: + +- Geometry and material definitions and associated physical properties such as + temperature +- Neutron cross section library (e.g., ENDF/B-VIII.0) +- Treatment of thermal scattering and unresolved resonance probability tables +- Neutron reactions accounted for in the depletion chain +- Decay data in the depletion chain +- Isomeric branching ratios for reactions in the depletion chain +- Fission product yields in the depletion chain +- Fission product yield interpolation method + (``CoupledOperator(fission_yield_mode=...)``) +- Reaction rate normalization, including fission Q values + (``CoupledOperator(fission_q=...)``) +- Depletion integration method (``PredictorIntegrator``, ``CECMIntegrator``, + etc.) and time-step sizes + +When comparing to codes that use ACE format cross sections, it is recommended to +directly convert the ACE files to HDF5 format using functionality from the +:mod:`openmc.data` module (see :ref:`create_xs_library`). Some of the +LANL-distributed ACE libraries used with MCNP have also been converted to HDF5 +format and are available for download at https://openmc.org/data. + +Even after these choices have been aligned, exact agreement should not be +expected. Codes may use different approximations or numerical methods that +cannot be configured identically. When investigating a discrepancy, first +compare transport results and one-group reaction rates at the initial time, then +compare changes over subsequent timesteps. diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 6f14ebfa5..8f83b9b08 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -182,6 +182,8 @@ boundary condition. Periodic boundary conditions can be applied to pairs of planar surfaces. If there are only two periodic surfaces they will be matched automatically. + + Otherwise it is necessary to specify pairs explicitly using the :attr:`Surface.periodic_surface` attribute as in the following example:: @@ -192,7 +194,7 @@ Otherwise it is necessary to specify pairs explicitly using the Both rotational and translational periodic boundary conditions are specified in the same fashion. If both planes have the same normal vector, a translational periodicity is assumed; rotational periodicity is assumed otherwise. Currently, -only rotations about the :math:`z`-axis are supported. +rotations must be about the :math:`x`-, :math:`y`-, or :math:`z`-axis. For a rotational periodic BC, the normal vectors of each surface must point inwards---towards the valid geometry. For example, a :class:`XPlane` and @@ -246,6 +248,28 @@ The classes :class:`Halfspace`, :class:`Intersection`, :class:`Union`, and :class:`Complement` and all instances of :class:`openmc.Region` and can be assigned to the :attr:`Cell.region` attribute. +Cells also contain :attr:`Cell.temperature` and :attr:`Cell.density` +attributes which override the temperature and density of the fill. These can +be quite useful when temperatures and densities are spatially varying, as the +alternative would be to add a unique :class:`Material` for each permutation of +temperature, density, and composition. You can set the temperature (K) and +density (g/cc) of a cell like so:: + + fuel.temperature = 800.0 + fuel.density = 10.0 + +The real utility of cell temperatures and densities occurs when a cell is +replicated across the geometry, such as when a cell is the root geometric element +in a replicated :ref:`universe` or :ref:`lattice +`. In those cases, you can provide a list of temperatures +and densities to apply a temperature/density field to all of the distributed cells:: + + fuel.temperature = [800.0, 900.0, 800.0, 900.0] + fuel.density = [10.0, 9.0, 10.0, 9.0] + +In this example, the fuel cell is distributed four times in the geometry. Each +distributed instance then receives its own temperature and density. + .. _usersguide_universes: --------- @@ -413,11 +437,11 @@ to help figure out how to place universes:: Note that by default, hexagonal lattices are positioned such that each lattice -element has two faces that are parallel to the :math:`y` axis. As one example, -to create a three-ring lattice centered at the origin with a pitch of 10 cm -where all the lattice elements centered along the :math:`y` axis are filled with -universe ``u`` and the remainder are filled with universe ``q``, the following -code would work:: +element has two faces that are perpendicular to the :math:`y` axis. As one +example, to create a three-ring lattice centered at the origin with a pitch of +10 cm where all the lattice elements centered along the :math:`y` axis are +filled with universe ``u`` and the remainder are filled with universe ``q``, the +following code would work:: hexlat = openmc.HexLattice() hexlat.center = (0, 0) @@ -530,6 +554,89 @@ UWUW and OpenMC material ID space will cause an error. To automatically resolve these ID overlaps, ``auto_ids`` can be set to ``True`` to append the UWUW material IDs to the OpenMC material ID space. + +Material overrides and differentiation +-------------------------------------- + +Programmatic access to DAGMC cell information for material overrides +and differentiation requires synchronization of the DAGMC universe +representation across Python and C-API:: + + model.init_lib() + model.sync_dagmc_universes() + model.finalize_lib() + +Upon completion of these steps, the :attr:`DAGMCUniverse.cells` attribute will +be populated with :class:`DAGMCCell` proxy objects that represent the cells +defined in the DAGMC model. The :class:`DAGMCCell` objects will have +:class:`openmc.Material`'s' applied according to the assignments upon +initialization of the model. These materials can be replaced in the same manner +as :class:`openmc.Cell` objects to override material assignments in the DAGMC +model. + +Depletion with DAGMC geometry +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The synchronization of :class:`openmc.DAGMCUniverse`'s is important for +depletion calculations using DAGMC geometry when materials need to be +differentiated to perform material burnup independently in each DAGMC cell. See +:meth:`openmc.model.Model.differentiate_mats`. + +Material overrides +~~~~~~~~~~~~~~~~~~ + +OpenMC supports overriding material assignments defined inside a DAGMC HDF5 +model so that CAD-assigned materials can be replaced by :class:`openmc.Material` +objects. This is useful when the CAD geometry provides the shape but OpenMC +materials (specific nuclide content, densities, or depletion behavior) are +required. + + +Replacing materials by name +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If a DAGMC file includes material name tags, you can replace all cells that +reference a particular name with an :class:`openmc.Material` using +:meth:`~openmc.DAGMCUniverse.replace_material_assignment`:: + + import openmc + + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + + fuel = openmc.Material(name='fuel') + fuel.add_nuclide('U235', 0.05) + fuel.add_nuclide('U238', 0.95) + fuel.set_density('g/cm3', 10.5) + + dag_univ.replace_material_assignment('Fuel', fuel) + +This lets you keep CAD geometry while adopting OpenMC material definitions. + +Per-cell material overrides +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To assign overrides without initializing :class:`openmc.Model`, the +:meth:`openmc.DAGMCUniverse.add_material_override` method can be used to assign +materials to particular DAGMC cells. The method accepts either an integer cell +ID:: + + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + + enriched = openmc.Material(name='fuel_enriched') + enriched.add_nuclide('U235', 0.10) + enriched.add_nuclide('U238', 0.90) + enriched.set_density('g/cm3', 10.5) + + dag_univ.add_material_override(1, enriched) + +In the case that the :class:`openmc.DAGMCUniverse` has already been synchronized, +a :class:`openmc.DAGMCCell` object can also be provide to assign the material. + +Overrides are written to the `` element of the +:ref:` ` XML element so the C++ core can apply +them on initialization. + + .. _Direct Accelerated Geometry Monte Carlo: https://svalinn.github.io/DAGMC/ .. _University of Wisconsin Unified Workflow: https://svalinn.github.io/DAGMC/usersguide/uw2.html diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 64d480179..2a0d301d4 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -158,6 +158,75 @@ feature can be used to access the installed packages. .. _Spack: https://spack.readthedocs.io/en/latest/ .. _setup guide: https://spack.readthedocs.io/en/latest/getting_started.html +.. _install_aur: + +------------------------------------ +Installing on Arch Linux via the AUR +------------------------------------ + +On Arch Linux and Arch-based distributions, OpenMC can be installed from the +`Arch User Repository (AUR) `_. An AUR package named +``openmc-git`` is available, which builds OpenMC directly from the latest +development sources. + +This package provides a full-featured OpenMC stack, including: + +* MPI and DAGMC-enabled OpenMC build +* User-selected nuclear data libraries +* The `CAD_to_OpenMC `_ meshing tool +* All required dependencies for the above components + +To install the package, you will need an AUR helper such as `yay`_ or `paru`_. +For example, using ``yay``:: + + yay -S openmc-git + + +Alternatively, you can manually clone and build the package:: + + git clone https://aur.archlinux.org/openmc-git.git + cd openmc-git + makepkg -si + +Note, ``makepkg`` uses ``pacman`` to resolve dependencies. Therefore, AUR-based +dependencies need to be installed separately with ``yay`` or ``paru`` before +running ``makepkg``. The PKGBUILD will automatically handle all required +dependencies and build OpenMC with MPI and DAGMC support enabled. + +.. tip:: + + If there are failing checks during the build process, you can bypass them + with the ``--nocheck`` flag:: + + yay -S openmc-git --mflags "--nocheck" + + Or:: + + git clone https://aur.archlinux.org/openmc-git.git + cd openmc-git + makepkg -si --nocheck + +.. note:: + + The ``openmc-git`` package tracks the latest development version from the + upstream repository. As such, it may include new features and bug fixes, but + could also introduce instability compared to official releases. + +.. tip:: + + OpenMC is installed under ``/opt``. If you are installing and using it in + the same terminal session, you may need to reload your environment + variables:: + + source /etc/profile + + Alternatively, start a new shell session. + +Once installed, the ``openmc`` executable, nuclear data libraries, and +associated tools will be available in your system :envvar:`PATH`. + +.. _yay: https://github.com/Jguer/yay +.. _paru: https://github.com/Morganamilo/paru .. _install_source: @@ -262,11 +331,11 @@ Prerequisites This option allows OpenMC to read and write MCPL (Monte Carlo Particle Lists) files instead of .h5 files for sources (external source - distribution, k-eigenvalue source distribution, and surface sources). To - turn this option on in the CMake configuration step, add the following - option:: - - cmake -DOPENMC_USE_MCPL=on .. + distribution, k-eigenvalue source distribution, and surface sources). + OpenMC does not need any particular build option to use this, but MCPL + must be installed on the system in order to do so. Refer to the + `MCPL documentation `_ + for instructions on how to accomplish this. * NCrystal_ library for defining materials with enhanced thermal neutron transport @@ -383,6 +452,20 @@ OPENMC_USE_MPI options, please see the `FindMPI.cmake documentation `_. +.. _cmake_strict_fp: + +OPENMC_ENABLE_STRICT_FP + Disables compiler optimizations that change floating-point results relative to + unoptimized builds, improving cross-platform and cross-optimization-level + reproducibility. This disables FMA contraction (``-ffp-contract=off``) and + compiler builtin replacements of math functions like ``pow``, ``exp``, ``log`` + (``-fno-builtin``). It also keeps C/C++ assertions active by removing the + ``-DNDEBUG`` flag from ``RelWithDebInfo`` builds. Without this flag, these + optimizations can produce bit-level differences across platforms, compilers, + and optimization levels. This option should be used when running the test + suite. By default (off), the compiler is free to use all optimizations for + best performance. (Default: off) + OPENMC_FORCE_VENDORED_LIBS Forces OpenMC to use the submodules located in the vendor directory, as opposed to searching the system for already installed versions of those @@ -415,7 +498,10 @@ Release RelWithDebInfo (Default if no type is specified.) Enable optimization and debug. On most - platforms/compilers, this is equivalent to `-O2 -g`. + platforms/compilers, this is equivalent to `-O2 -g`. When + :ref:`OPENMC_ENABLE_STRICT_FP ` is enabled, OpenMC removes the + ``-DNDEBUG`` flag that CMake normally adds for this build type, so that + C/C++ assertions remain active. Example of configuring for Debug mode: diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index da0c69bdd..b5c29a3e8 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -6,13 +6,14 @@ Geometry Visualization .. currentmodule:: openmc -OpenMC is capable of producing two-dimensional slice plots of a geometry as well -as three-dimensional voxel plots using the geometry plotting :ref:`run mode -`. The geometry plotting mode relies on the presence of a -:ref:`plots.xml ` file that indicates what plots should be created. To -create this file, one needs to create one or more :class:`openmc.Plot` -instances, add them to a :class:`openmc.Plots` collection, and then use the -:class:`Plots.export_to_xml` method to write the ``plots.xml`` file. +OpenMC is capable of producing two-dimensional slice plots of a geometry, +three-dimensional voxel plots, and three-dimensional raytrace plots using the +geometry plotting :ref:`run mode `. The geometry plotting +mode relies on the presence of a :ref:`plots.xml ` file that indicates +what plots should be created. To create this file, one needs to create one or +more instances of the various plot classes described below, add them to a +:class:`openmc.Plots` collection, and then use the :class:`Plots.export_to_xml` +method to write the ``plots.xml`` file. ----------- Slice Plots @@ -21,15 +22,14 @@ Slice Plots .. image:: ../_images/atr.png :width: 300px -By default, when an instance of :class:`openmc.Plot` is created, it indicates -that a 2D slice plot should be made. You can specify the origin of the plot -(:attr:`Plot.origin`), the width of the plot in each direction -(:attr:`Plot.width`), the number of pixels to use in each direction -(:attr:`Plot.pixels`), and the basis directions for the plot. For example, to -create a :math:`x` - :math:`z` plot centered at (5.0, 2.0, 3.0) with a width of -(50., 50.) and 400x400 pixels:: +The :class:`openmc.SlicePlot` class indicates that a 2D slice plot should be +made. You can specify the origin of the plot (:attr:`SlicePlot.origin`), the +width of the plot in each direction (:attr:`SlicePlot.width`), the number of +pixels to use in each direction (:attr:`SlicePlot.pixels`), and the basis +directions for the plot. For example, to create a :math:`x` - :math:`z` plot +centered at (5.0, 2.0, 3.0) with a width of (50., 50.) and 400x400 pixels:: - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.basis = 'xz' plot.origin = (5.0, 2.0, 3.0) plot.width = (50., 50.) @@ -47,7 +47,7 @@ that location. By default, a unique color will be assigned to each cell in the geometry. If you want your plot to be colored by material instead, change the -:attr:`Plot.color_by` attribute:: +:attr:`SlicePlot.color_by` attribute:: plot.color_by = 'material' @@ -68,8 +68,8 @@ particular cells/materials should be given colors of your choosing:: Note that colors can be given as RGB tuples or by a string indicating a valid `SVG color `_. -When you're done creating your :class:`openmc.Plot` instances, you need to then -assign them to a :class:`openmc.Plots` collection and export it to XML:: +When you're done creating your :class:`openmc.SlicePlot` instances, you need to +then assign them to a :class:`openmc.Plots` collection and export it to XML:: plots = openmc.Plots([plot1, plot2, plot3]) plots.export_to_xml() @@ -97,13 +97,11 @@ Voxel Plots .. image:: ../_images/3dba.png :width: 200px -The :class:`openmc.Plot` class can also be told to generate a 3D voxel plot -instead of a 2D slice plot. Simply change the :attr:`Plot.type` attribute to -'voxel'. In this case, the :attr:`Plot.width` and :attr:`Plot.pixels` attributes -should be three items long, e.g.:: +The :class:`openmc.VoxelPlot` class enables the generation of a 3D voxel plot +instead of a 2D slice plot. In this case, the :attr:`VoxelPlot.width` and +:attr:`VoxelPlot.pixels` attributes should be three items long, e.g.:: - vox_plot = openmc.Plot() - vox_plot.type = 'voxel' + vox_plot = openmc.VoxelPlot() vox_plot.width = (100., 100., 50.) vox_plot.pixels = (400, 400, 200) diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index fe6ab0826..ed3e8564a 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -97,7 +97,15 @@ VTK Mesh File Generation ------------------------ VTK files of OpenMC meshes can be created using the -:meth:`openmc.Mesh.write_data_to_vtk` method. Data can be applied to the +:meth:`openmc.Mesh.write_data_to_vtk` method. This method supports several VTK +formats depending on the mesh type. Structured meshes +(:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, +:class:`~openmc.CylindricalMesh`, and :class:`~openmc.SphericalMesh`) can be +exported to legacy VTK format (``.vtk``). The :class:`~openmc.UnstructuredMesh` +class supports VTK unstructured grid formats (``.vtu``) as well as an HDF5-based +format (``.vtkhdf``) that does not require the ``vtk`` module to write. + +Data can be applied to the elements of the resulting mesh from mesh filter objects. This data can be provided either as a flat array or, in the case of structured meshes (:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index d5d752a83..06d4e50ed 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -513,6 +513,7 @@ Supported scores: - total - fission - nu-fission + - kappa-fission - events Supported Estimators: @@ -644,7 +645,10 @@ model to use these multigroup cross sections. An example is given below:: nparticles=2000, overwrite_mgxs_library=False, mgxs_path="mgxs.h5", - correction=None + correction=None, + source_energy=None, + temperatures=None, + temperature_settings=None ) The most important parameter to set is the ``method`` parameter, which can be @@ -706,6 +710,45 @@ generation and use an existing library file. with a :math:`\rho` default value of 1.0, which can be adjusted with the ``settings.random_ray['diagonal_stabilization_rho']`` parameter. +When generating MGXS data with either the ``stochastic_slab`` or +``infinite_medium`` methods, by default the simulation will use a uniform source +distribution spread evenly over all energy groups. This ensures that all energy +groups receive tallies and therefore produce non-zero total multigroup cross +sections. Additionally, the function will convert any sources in the model into +simplified spatial sources that retain the original energy distributions. If +sources are present, they will be used 99% of the time to sample source energies +during MGXS generation. The other 1% of the time, energies will be sampled +uniformly over all energy groups to ensure that all groups receive some tallies. +However, the user may wish to specify a different source energy spectrum (for +instance, if they are using a FileSource, such that the energy distribution +cannot be extracted from the python source object). This can be done by +providing a :class:`openmc.stats.Univariate` distribution as the +``source_energy`` parameter of the :meth:`openmc.Model.convert_to_multigroup` +method. If provided, it will override any sources present in the model and will +be used 99% of the time to sample source energies during MGXS generation. The +other 1% of the time, energies will be sampled uniformly over all energy groups +to ensure that all groups receive some tallies. + +For instance, a D-D fusion simulation may involve a complex file source. In this +case, the user may wish to provide a discrete 2.45 MeV energy source +distribution for MGXS generation as:: + + source_energy = openmc.stats.delta_function(2.45e6) + +The ``temperatures`` parameter can be provided if temperature-dependent +multi-group cross sections are desired for multi-physics simulations. An +individual cross section generation calculation is run for each temperature +provided, where the materials in the model are set to the temperature. The +temperature settings used during cross section generation can be specified with the +``temperature_settings`` parameter. If no ``temperature_settings`` are provided, +the settings contained in the model will be used. The valid keys and values in the +``temperature_settings`` dictionary are identical to +:attr:`openmc.Settings.temperature_settings`; more information can be found in +:class:`openmc.Settings` . This approach yields isothermal cross section interpolation +tables, which can be inaccurate for systems with large differences between temperatures +in each material (often the case in fission reactors). If a more sophisticated +temperature-dependence is required, we recommend generating cross sections manually. + Ultimately, the methods described above are all just approximations. Approximations in the generated MGXS data will fundamentally limit the potential accuracy of the random ray solver. However, the methods described above are all @@ -901,6 +944,8 @@ as:: which will greatly improve the quality of the linear source term in 2D simulations. +.. _usersguide_random_ray_run_modes: + --------------------------------- Fixed Source and Eigenvalue Modes --------------------------------- @@ -1030,22 +1075,52 @@ The adjoint flux random ray solver mode can be enabled as:: settings.random_ray['adjoint'] = True -When enabled, OpenMC will first run a forward transport simulation followed by -an adjoint transport simulation. The purpose of the forward solve is to compute -the adjoint external source when an external source is present in the -simulation. Simulation settings (e.g., number of rays, batches, etc.) will be -identical for both simulations. At the conclusion of the run, all results (e.g., -tallies, plots, etc.) will be derived from the adjoint flux rather than the -forward flux but are not labeled any differently. The initial forward flux -solution will not be stored or available in the final statepoint file. Those -wishing to do analysis requiring both the forward and adjoint solutions will -need to run two separate simulations and load both statepoint files. +When enabled, OpenMC will first run a forward transport simulation if there are +no user-specified adjoint sources present, followed by an adjoint transport +simulation. Fixed adjoint sources can be specified on the +:attr:`openmc.Settings.random_ray` dictionary as follows:: + + # Geometry definition + ... + detector_cell = openmc.Cell(fill=detector_mat, name='cell where detector will be') + ... + # Define fixed adjoint neutron source + strengths = [1.0] + midpoints = [1.0e-4] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + adj_source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [detector_cell]} + ) + + # Add to random_ray dict + settings.random_ray['adjoint_source'] = adj_source + +The same constraints apply to the user-defined adjoint source as to the forward +source, described in the :ref:`Fixed Source and Eigenvalue section +`. If this source is not provided, a forward +solve must take place to compute the adjoint external source when a forward +external source is present in the problem. Simulation settings (e.g., number of +rays, batches, etc.) will be identical for both calculations. At the +conclusion of the run, all results (e.g., tallies, plots, etc.) will be +derived from the adjoint flux rather than the forward flux but are not labeled +any differently. When an initial forward solve is performed (i.e., when no +user-specified adjoint source is present), its output files are also written to +disk with a ``forward`` infix, so they are not overwritten by the subsequent +adjoint solve. This applies to the statepoint, ``tallies.out``, and any voxel +plots, e.g., ``statepoint.forward.N.h5`` and ``tallies.forward.out``; the +adjoint solve keeps the usual file names. This allows analyses requiring both +the forward and adjoint solutions to be performed from a single run. When +generating FW-CADIS weight windows, no weight window file is written for the +forward solve, as only the final adjoint-derived weight windows are meaningful. .. note:: - When adjoint mode is selected, OpenMC will always perform a full forward - solve and then run a full adjoint solve immediately afterwards. Statepoint - and tally results will be derived from the adjoint flux, but will not be - labeled any differently. + Use of the automated + :ref:`FW-CADIS weight window generator` is not + currently compatible with user-defined adjoint sources. Instead, the + initial forward calculation is used to assign "forward-weighted" adjoint + sources to the tally regions of interest. --------------------------------------- Putting it All Together: Example Inputs @@ -1105,11 +1180,10 @@ given below: tallies.export_to_xml() # Create voxel plot - plot = openmc.Plot() + plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [2*pitch, 2*pitch, 1] plot.pixels = [1000, 1000, 1] - plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) @@ -1189,11 +1263,10 @@ given below: tallies.export_to_xml() # Create voxel plot - plot = openmc.Plot() + plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [2*pitch, 2*pitch, 1] plot.pixels = [1000, 1000, 1] - plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) diff --git a/docs/source/usersguide/scripts.rst b/docs/source/usersguide/scripts.rst index 0879d63ef..eb0abeb0d 100644 --- a/docs/source/usersguide/scripts.rst +++ b/docs/source/usersguide/scripts.rst @@ -48,6 +48,7 @@ flags: restart file -s, --threads N Run with *N* OpenMP threads -t, --track Write tracks for all particles (up to max_tracks) +-q, --verbosity V Set the output verbosity to *V* -v, --version Show version information -h, --help Show help message diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index f973f1465..6faeb59e1 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -272,6 +272,12 @@ option:: settings.source = [src1, src2] settings.uniform_source_sampling = True +Additionally, sampling from an :class:`openmc.IndependentSource` may be biased +for local or global variance reduction by modifying the +:attr:`~openmc.IndependentSource.bias` attribute of each of its four main +distributions. Further discussion of source biasing can be found in +:ref:`source_biasing`. + Finally, the :attr:`IndependentSource.particle` attribute can be used to indicate the source should be composed of particles other than neutrons. For example, the following would generate a photon source:: @@ -285,6 +291,53 @@ example, the following would generate a photon source:: For a full list of all classes related to statistical distributions, see :ref:`pythonapi_stats`. +Tokamak Plasma Sources +---------------------- + +For fusion applications, the :class:`openmc.TokamakSource` class provides a +native parametric neutron source for tokamak plasmas. Rather than specifying +spatial, angular, and energy distributions separately, the source is defined by +the plasma geometry (using a `Miller-style flux-surface parameterization +`_) and a radial emission profile. Source +sites are sampled directly from the plasma volume without rejection. + +The plasma shape is described by the major radius :math:`R_0`, minor radius +:math:`a`, elongation :math:`\kappa`, triangularity :math:`\delta`, and +Shafranov shift :math:`\Delta`. The neutron birth profile is given as an +emission density :math:`S(r/a)` tabulated on a normalized minor-radius grid that +runs from 0 (magnetic axis) to 1 (last closed flux surface); only the shape of +the profile matters, since it is normalized internally. The emission density is +linearly interpolated between the supplied points and refined internally for +radial sampling. For example:: + + import numpy as np + + r_over_a = np.linspace(0.0, 1.0, 50) + emission = (1.0 - r_over_a**2)**2 # peaked on-axis profile + + source = openmc.TokamakSource( + major_radius=620.0, # cm + minor_radius=200.0, # cm + elongation=1.8, + triangularity=0.45, + shafranov_shift=10.0, # cm + r_over_a=r_over_a, + emission_density=emission, + energy=openmc.stats.muir(e0=14.08e6, m_rat=5.0, kt=20000.0), + ) + + settings.source = source + +The ``energy`` argument accepts either a single +:class:`~openmc.stats.Univariate` distribution applied at all radii, or a +sequence with one distribution per ``r_over_a`` grid point to model a +radially-varying neutron spectrum (energies are then sampled by stochastic +interpolation between the two distributions bracketing the sampled radius). A +time distribution can be given with the ``time`` argument; by default, particles +are born at :math:`t=0`. The toroidal extent can be restricted with +``phi_start`` and ``phi_extent`` to model a sector of the plasma, and +``vertical_shift`` translates the plasma center along the z-axis. + File-based Sources ------------------ @@ -394,7 +447,7 @@ below. { openmc::SourceSite particle; // weight - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); particle.wgt = 1.0; // position double angle = 2.0 * M_PI * openmc::prn(seed); @@ -471,7 +524,7 @@ parameters to the source class when it is created: { openmc::SourceSite particle; // weight - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); particle.wgt = 1.0; // position particle.r.x = 0.0; @@ -598,6 +651,13 @@ transport:: settings.photon_transport = True +Atomic relaxation (the cascade of fluorescence photons and Auger electrons +emitted when an inner-shell vacancy is filled) is enabled by default whenever +photon transport is on. It can be disabled using the +:attr:`Settings.atomic_relaxation` attribute:: + + settings.atomic_relaxation = False + The way in which OpenMC handles secondary charged particles can be specified with the :attr:`Settings.electron_treatment` attribute. By default, the :ref:`thick-target bremsstrahlung ` (TTB) approximation is used to generate @@ -779,6 +839,11 @@ collision_track.h5 file at the end of the simulation. The file contains 300 recorded collisions that occurred in materials with IDs 1 or 2, involving fission or (n,2n) reactions on the nuclides U-238 or O-16, within cells with IDs 5 and 12. + +.. note:: + Electron and positron collision-track events are not associated with a + specific nuclide. If a ``nuclides`` entry is specified, these events are omitted. + The file can be read using :func:`openmc.read_collision_track_file`. The example below shows how to extract the data from the collision_track feature and displays the fields stored in the file: diff --git a/docs/source/usersguide/tallies.rst b/docs/source/usersguide/tallies.rst index e3b4e508b..20a89cea7 100644 --- a/docs/source/usersguide/tallies.rst +++ b/docs/source/usersguide/tallies.rst @@ -105,109 +105,124 @@ The following tables show all valid scores: .. table:: **Reaction scores: units are reactions per source particle.** - +----------------------+---------------------------------------------------+ - |Score | Description | - +======================+===================================================+ - |absorption |Total absorption rate. For incident neutrons, this | - | |accounts for all reactions that do not produce | - | |secondary neutrons as well as fission. For incident| - | |photons, this includes photoelectric and pair | - | |production. | - +----------------------+---------------------------------------------------+ - |elastic |Elastic scattering reaction rate. | - +----------------------+---------------------------------------------------+ - |fission |Total fission reaction rate. | - +----------------------+---------------------------------------------------+ - |scatter |Total scattering rate. | - +----------------------+---------------------------------------------------+ - |total |Total reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2nd) |(n,2nd) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2n) |(n,2n) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3n) |(n,3n) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,na) |(n,n\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n3a) |(n,n3\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2na) |(n,2n\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3na) |(n,3n\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,np) |(n,np) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n2a) |(n,n2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2n2a) |(n,2n2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nd) |(n,nd) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nt) |(n,nt) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n3He) |(n,n\ :sup:`3`\ He) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nd2a) |(n,nd2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nt2a) |(n,nt2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,4n) |(n,4n) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2np) |(n,2np) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3np) |(n,3np) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n2p) |(n,n2p) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n*X*) |Level inelastic scattering reaction rate. The *X* | - | |indicates what which inelastic level, e.g., (n,n3) | - | |is third-level inelastic scattering. | - +----------------------+---------------------------------------------------+ - |(n,nc) |Continuum level inelastic scattering reaction rate.| - +----------------------+---------------------------------------------------+ - |(n,gamma) |Radiative capture reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,p) |(n,p) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,d) |(n,d) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,t) |(n,t) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3He) |(n,\ :sup:`3`\ He) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,a) |(n,\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2a) |(n,2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3a) |(n,3\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2p) |(n,2p) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,pa) |(n,p\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,t2a) |(n,t2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,d2a) |(n,d2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,pd) |(n,pd) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,pt) |(n,pt) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,da) |(n,d\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |coherent-scatter |Coherent (Rayleigh) scattering reaction rate. | - +----------------------+---------------------------------------------------+ - |incoherent-scatter |Incoherent (Compton) scattering reaction rate. | - +----------------------+---------------------------------------------------+ - |photoelectric |Photoelectric absorption reaction rate. | - +----------------------+---------------------------------------------------+ - |pair-production |Pair production reaction rate. | - +----------------------+---------------------------------------------------+ - |*Arbitrary integer* |An arbitrary integer is interpreted to mean the | - | |reaction rate for a reaction with a given ENDF MT | - | |number. | - +----------------------+---------------------------------------------------+ + +------------------------+-------------------------------------------------+ + |Score |Description | + +========================+=================================================+ + |absorption |Total absorption rate. For incident neutrons, | + | |this accounts for all reactions that do not | + | |produce secondary neutrons as well as fission. | + | |For incident photons, this includes | + | |photoelectric and pair production. | + +------------------------+-------------------------------------------------+ + |elastic |Elastic scattering reaction rate. | + +------------------------+-------------------------------------------------+ + |fission |Total fission reaction rate. | + +------------------------+-------------------------------------------------+ + |scatter |Total scattering rate. | + +------------------------+-------------------------------------------------+ + |total |Total reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2nd) |(n,2nd) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2n) |(n,2n) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3n) |(n,3n) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,na) |(n,n\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n3a) |(n,n3\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2na) |(n,2n\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3na) |(n,3n\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,np) |(n,np) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n2a) |(n,n2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2n2a) |(n,2n2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nd) |(n,nd) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nt) |(n,nt) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n3He) |(n,n\ :sup:`3`\ He) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nd2a) |(n,nd2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nt2a) |(n,nt2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,4n) |(n,4n) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2np) |(n,2np) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3np) |(n,3np) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n2p) |(n,n2p) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,npa) |(n,np\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n*X*) |Level inelastic scattering reaction rate. The | + | |*X* indicates which inelastic level, e.g., | + | |(n,n3) is third-level inelastic scattering. | + +------------------------+-------------------------------------------------+ + |(n,nc) |Continuum level inelastic scattering | + | |reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,gamma) |Radiative capture reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,p) |(n,p) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,d) |(n,d) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,t) |(n,t) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3He) |(n,\ :sup:`3`\ He) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,a) |(n,\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2a) |(n,2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3a) |(n,3\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2p) |(n,2p) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,pa) |(n,p\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,t2a) |(n,t2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,d2a) |(n,d2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,pd) |(n,pd) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,pt) |(n,pt) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,da) |(n,d\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |photon-total |Total photo-atomic reaction rate. | + +------------------------+-------------------------------------------------+ + |coherent-scatter |Coherent (Rayleigh) scattering reaction rate. | + +------------------------+-------------------------------------------------+ + |incoherent-scatter |Incoherent (Compton) scattering reaction rate. | + +------------------------+-------------------------------------------------+ + |photoelectric |Photoelectric absorption reaction rate. | + +------------------------+-------------------------------------------------+ + |photoelectric-*S* |Subshell photoelectric absorption rate for the | + | |*S* shell. For example, "photoelectric-N3" is the| + | |rate for the N3 subshell. | + +------------------------+-------------------------------------------------+ + |pair-production |Pair production reaction rate (total). | + +------------------------+-------------------------------------------------+ + |pair-production-electron|Pair production reaction rate in the electron | + | |field. | + +------------------------+-------------------------------------------------+ + |pair-production-nuclear |Pair production reaction rate in the nuclear | + | |field. | + +------------------------+-------------------------------------------------+ + |*Arbitrary integer* |An arbitrary integer is interpreted to mean the | + | |reaction rate for a reaction with a given ENDF | + | |MT number. | + +------------------------+-------------------------------------------------+ .. table:: **Particle production scores: units are particles produced per source particles.** @@ -246,18 +261,21 @@ The following tables show all valid scores: +----------------------+---------------------------------------------------+ |Score | Description | +======================+===================================================+ - |current |Used in combination with a meshsurface filter: | + |current |It may not be used in conjunction with any other | + | |score except flux. | + | | | + | |When used in combination with a meshsurface filter:| | |Partial currents on the boundaries of each cell in | - | |a mesh. It may not be used in conjunction with any | - | |other score. Only energy and mesh filters may be | - | |used. | - | |Used in combination with a surface filter: | + | |a mesh. | + | | | + | |When used in combination with a surface filter: | | |Net currents on any surface previously defined in | | |the geometry. It may be used along with any other | | |filter, except meshsurface filters. | | |Surfaces can alternatively be defined with cell | | |from and cell filters thereby resulting in tallying| | |partial currents. | + | | | | |Units are particles per source particle. | +----------------------+---------------------------------------------------+ |events |Number of scoring events. Units are events per | diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 93321e107..d551195f5 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -4,24 +4,27 @@ Variance Reduction ================== -Global variance reduction in OpenMC is accomplished by weight windowing -techniques. OpenMC is capable of generating weight windows using either the -MAGIC or FW-CADIS methods. Both techniques will produce a ``weight_windows.h5`` -file that can be loaded and used later on. In this section, we break down the -steps required to both generate and then apply weight windows. +Global and local variance reduction are possible in OpenMC through both weight +windowing and source biasing techniques. OpenMC is capable of generating weight +windows using either the MAGIC or FW-CADIS methods, the latter with an optional +capability for local variance reduction. Both techniques will produce a +``weight_windows.h5`` file that can be loaded and used later on. In +this section, we first break down the steps required to generate and apply +weight windows, then describe how source biasing may be applied. .. _ww_generator: ------------------------------------- -Generating Weight Windows with MAGIC ------------------------------------- +------------------------------------------- +Generating Global Weight Windows with MAGIC +------------------------------------------- As discussed in the :ref:`methods section `, MAGIC is an iterative method that uses flux tally information from a Monte Carlo -simulation to produce weight windows for a user-defined mesh. While generating -the weight windows, OpenMC is capable of applying the weight windows generated -from a previous batch while processing the next batch, allowing for progressive -improvement in the weight window quality across iterations. +simulation to produce weight windows for a user-defined mesh with the objective +of global variance reduction. While generating the weight windows, OpenMC is +capable of applying the weight windows generated from a previous batch while +processing the next batch, allowing for progressive improvement in the weight +window quality across iterations. The typical way of generating weight windows is to define a mesh and then add an :class:`openmc.WeightWindowGenerator` object to an :attr:`openmc.Settings` @@ -69,15 +72,20 @@ At the end of the simulation, a ``weight_windows.h5`` file will be saved to disk for later use. Loading it in another subsequent simulation will be discussed in the "Using Weight Windows" section below. ------------------------------------------------------- -Generating Weight Windows with FW-CADIS and Random Ray ------------------------------------------------------- +.. _usersguide_fw_cadis: + +---------------------------------------------------------------------- +Generating Global or Local Weight Windows with FW-CADIS and Random Ray +---------------------------------------------------------------------- Weight window generation with FW-CADIS and random ray in OpenMC uses the same -exact strategy as with MAGIC. An :class:`openmc.WeightWindowGenerator` object is -added to the :attr:`openmc.Settings` object, and a ``weight_windows.h5`` will be -generated at the end of the simulation. The only difference is that the code -must be run in random ray mode. A full description of how to enable and setup +exact strategy as with MAGIC. Using FW-CADIS, however, also enables +local variance reduction in fixed source problems through the :attr:`targets` +attribute, which is described later in this section. To enable FW-CADIS, an +:class:`openmc.WeightWindowGenerator` object is added to the +:attr:`openmc.Settings` object, and a ``weight_windows.h5`` will be generated +at the end of the simulation. The only procedural difference is that the code +must be run in random ray mode. A full description of how to enable and setup random ray mode can be found in the :ref:`Random Ray User Guide `. .. note:: @@ -88,7 +96,7 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. ray solver. A high level overview of the current workflow for generation of weight windows with FW-CADIS using random ray is given below. -1. Begin by making a deepy copy of your continuous energy Python model and then +1. Begin by making a deep copy of your continuous energy Python model and then convert the copy to be multigroup and use the random ray transport solver. The conversion process can largely be automated as described in more detail in the :ref:`random ray quick start guide `, summarized below:: @@ -146,7 +154,53 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. assigning to ``model.settings.random_ray['source_region_meshes']``) and for weight window generation. -3. When running your multigroup random ray input deck, OpenMC will automatically +3. (Optional) If local variance reduction is desired in a fixed-source problem, + populate the :attr:`targets` attribute with an :class:`openmc.Tallies` + instance or an iterable of tally IDs indicating the tallies of interest for + variance reduction:: + + # Build a new example and WWG for local variance reduction + from openmc.examples import random_ray_three_region_cube_with_detectors + new_model = random_ray_three_region_cube_with_detectors() + + ww_mesh = openmc.RegularMesh() + n = 7 + width = 35.0 + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", + mesh=ww_mesh, + max_realizations=new_model.settings.batches + ) + new_model.settings.weight_window_generators = wwg + new_model.settings.random_ray['volume_estimator'] = 'naive' + + # Get the tallies of interest + target_tallies = openmc.Tallies() + + for tally in list(new_model.tallies): + if tally.name in {"Detector 1 Tally", "Detector 2 Tally"}: + target_tallies.append(tally) + + # Add to WeightWindowGenerator + wwg.targets = target_tallies + +.. warning:: + The tallies designated as FW-CADIS targets to the + :class:`~openmc.WeightWindowGenerator` must be present under the + :class:`~openmc.model.Model.tallies` attribute of the + :class:`~openmc.model.Model` as well in order to be recognized as valid + local variance reduction targets. This check is performed when the + :func:`openmc.model.Model.export_to_model_xml` or + :func:`openmc.model.Model.export_to_xml` functions are called, meaning + that the standalone :func:`openmc.Settings.export_to_xml` and + :func:`openmc.Tallies.export_to_xml` methods should not be used with + FW-CADIS local variance reduction. + +4. When running your multigroup random ray input deck, OpenMC will automatically run a forward solve followed by an adjoint solve, with a ``weight_windows.h5`` file generated at the end. The ``weight_windows.h5`` file will contain FW-CADIS generated weight windows. This file can be used in @@ -172,3 +226,148 @@ Weight window mesh information is embedded into the weight window file, so the mesh does not need to be redefined. Monte Carlo solves that load a weight window file as above will utilize weight windows to reduce the variance of the simulation. + +.. _source_biasing: + +-------------- +Source Biasing +-------------- + +In fixed source problems, source biasing provides a means to reduce the variance +on global or localized responses, depending on the biasing scheme. In either +case, the premise of the method is to sample source sites from a biased +distribution that directs a larger fraction of the simulated histories towards +phase space regions of interest than would be found there under analog sampling. +In order to preserve an unbiased estimate of the tally mean, the weight of these +with analog sampling, divided by the probability assigned by the biased +distribution. While the assignment of statistical weights is outlined in the +:ref:`methods section `, this section demonstrates the +implementation of source biasing to problems in OpenMC. + +Source biasing in OpenMC is accomplished by applying a distribution to the +:attr:`bias` attribute of one or more of the univariate or independent +multivariate distributions which make up an :class:`~openmc.IndependentSource` +instance as follows:: + + # First create the biased distribution + biased_dist = openmc.stats.PowerLaw(a=0, b=3, n=3) + + # Construct a new distribution with the bias applied + dist = openmc.stats.PowerLaw(a=0, b=3, n=2, bias=biased_dist) + + # The bias attribute can also be set on an existing "analog" distribution: + sphere_dist = openmc.stats.spherical_uniform(r_outer=3) + sphere_dist.r.bias = biased_dist + +Univariate distributions may be sampled via the Python API, returning the +sample(s) along with the associated weight(s):: + + sample_vec, wgt_vec = dist.sample(n_samples=100) + +Here, if the distribution is unbiased, the weight of each sample will be unity. +Finally, :class:`~openmc.IndependentSource` instances can be constructed with +biased distributions:: + + # Create a source with a biased spatial distribution + source = openmc.IndependentSource(space=sphere_dist) + +During the simulation, source sites are then sampled using the biased +distributions where available and given starting statistical weights +corresponding to the cumulative product of the weights assigned by each +distribution in the source object. Hence multiple source variables (e.g., +direction and energy) may be biased and the resulting source sites will have +their weights adjusted accordingly. + +.. note:: + Combining source biasing with weight windows can be a powerful variance + reduction technique if each is constructed appropriately for the response + of interest. For example, if a source biasing scheme is devised for + variance reduction of a specific localized response, the user may be able + to specify their own weight window structure that results in more efficient + transport than if weight windows were generated by either of OpenMC's + automatic weight window generators, which are intended for global variance + reduction. + +Biased distributions that could result in degenerate weight mappings are not +recommended; this is most commonly seen when biasing the :math:`\phi`-coordinate +of spherical or cylindrical independent multivariate distributions. In such +cases degenerate behavior will be observed at the pole about which :math:`\phi` +is measured, with all values of :math:`\phi` (hence many possible statistical +weights) mapping to the same point for :math:`r=0` or :math:`\mu=0`, and large +weight gradients in the vicinity. In most cases requiring a spherical +independent source, it would be preferable to reorient the reference vector of +the distribution such that biasing could be applied to the +:math:`\mu`-coordinate instead. + +When biasing a distribution, care should also be taken to ensure that both the +unbiased and biased distribution share a common support---that is, every region +of phase space mapped to a nonzero probability density by the unbiased +distribution should likewise map to nonzero probability under the biased +distribution, and vice versa. In OpenMC, this places restrictions on the set of +compatible distributions that may be used to bias sampling of each distribution +type. The following table summarizes the method for each distribution in OpenMC +that permits biased sampling. + +.. list-table:: **Distributions that support biased sampling** + :header-rows: 1 + :widths: 35 65 + + * - Discrete Univariate PDFs + - Biasing Method + * - :class:`openmc.stats.Discrete` + - Apply a vector of alternative probabilities to the :attr:`bias` + attribute + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Continuous Univariate PDFs + - Biasing Method + * - :class:`openmc.stats.Uniform`, + :class:`openmc.stats.PowerLaw`, + :class:`openmc.stats.Maxwell`, + :class:`openmc.stats.Watt`, + :class:`openmc.stats.Normal`, + :class:`openmc.stats.Tabular` + - Apply a second, unbiased continous univariate PDF to the :attr:`bias` + attribute, ensuring that the :attr:`support` attribute of each + distribution is the same + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Mixed Univariate PDFs + - Biasing Method + * - :class:`openmc.stats.Mixture` + - May be constructed from multiple biased univariate distributions, or a + second, unbiased continous univariate PDF may be applied to the + :attr:`bias` attribute + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Discrete Multivariate PDFs + - Biasing Method + * - :class:`openmc.stats.PointCloud`, + :class:`openmc.stats.MeshSpatial` + - Apply a vector of the new relative probabilities of each point or mesh + element under biased sampling to the :attr:`bias` attribute + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Continuous Multivariate PDFs + - Biasing Method + * - :class:`openmc.stats.CartesianIndependent`, + :class:`openmc.stats.CylindricalIndependent`, + :class:`openmc.stats.SphericalIndependent`, + :class:`openmc.stats.PolarAzimuthal` + - Construct from biased univariate distributions for :attr:`x`, :attr:`y`, + :attr:`z`, etc. + * - :class:`openmc.stats.Isotropic` + - Apply an unbiased :class:`openmc.stats.PolarAzimuthal` to the + :attr:`bias` attribute diff --git a/examples/custom_source/source_ring.cpp b/examples/custom_source/source_ring.cpp index b87afbd17..bce6c27eb 100644 --- a/examples/custom_source/source_ring.cpp +++ b/examples/custom_source/source_ring.cpp @@ -1,17 +1,17 @@ -#include // for M_PI +#define _USE_MATH_DEFINES +#include // for M_PI #include // for unique_ptr +#include "openmc/particle.h" #include "openmc/random_lcg.h" #include "openmc/source.h" -#include "openmc/particle.h" -class RingSource : public openmc::Source -{ +class RingSource : public openmc::Source { openmc::SourceSite sample(uint64_t* seed) const { openmc::SourceSite particle; // particle type - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); // position double angle = 2.0 * M_PI * openmc::prn(seed); double radius = 3.0; @@ -25,10 +25,11 @@ class RingSource : public openmc::Source } }; -// A function to create a unique pointer to an instance of this class when generated -// via a plugin call using dlopen/dlsym. -// You must have external C linkage here otherwise dlopen will not find the file -extern "C" std::unique_ptr openmc_create_source(std::string parameters) +// A function to create a unique pointer to an instance of this class when +// generated via a plugin call using dlopen/dlsym. You must have external C +// linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source( + std::string parameters) { return std::make_unique(); } diff --git a/examples/lattice/hexagonal/build_xml.py b/examples/lattice/hexagonal/build_xml.py index 9485d0aa4..2624e52b4 100644 --- a/examples/lattice/hexagonal/build_xml.py +++ b/examples/lattice/hexagonal/build_xml.py @@ -128,14 +128,14 @@ settings_file.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot_xy = openmc.Plot(plot_id=1) +plot_xy = openmc.SlicePlot(plot_id=1) plot_xy.filename = 'plot_xy' plot_xy.origin = [0, 0, 0] plot_xy.width = [6, 6] plot_xy.pixels = [400, 400] plot_xy.color_by = 'material' -plot_yz = openmc.Plot(plot_id=2) +plot_yz = openmc.SlicePlot(plot_id=2) plot_yz.filename = 'plot_yz' plot_yz.basis = 'yz' plot_yz.origin = [0, 0, 0] diff --git a/examples/lattice/nested/build_xml.py b/examples/lattice/nested/build_xml.py index 2db23a46b..a1d9c092d 100644 --- a/examples/lattice/nested/build_xml.py +++ b/examples/lattice/nested/build_xml.py @@ -135,7 +135,7 @@ settings_file.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot(plot_id=1) +plot = openmc.SlicePlot(plot_id=1) plot.origin = [0, 0, 0] plot.width = [4, 4] plot.pixels = [400, 400] diff --git a/examples/lattice/simple/build_xml.py b/examples/lattice/simple/build_xml.py index 56c466121..44531edd8 100644 --- a/examples/lattice/simple/build_xml.py +++ b/examples/lattice/simple/build_xml.py @@ -128,7 +128,7 @@ settings_file.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot(plot_id=1) +plot = openmc.SlicePlot(plot_id=1) plot.origin = [0, 0, 0] plot.width = [4, 4] plot.pixels = [400, 400] diff --git a/examples/parameterized_custom_source/parameterized_source_ring.cpp b/examples/parameterized_custom_source/parameterized_source_ring.cpp index e70dc8bb1..3aabf3581 100644 --- a/examples/parameterized_custom_source/parameterized_source_ring.cpp +++ b/examples/parameterized_custom_source/parameterized_source_ring.cpp @@ -1,63 +1,66 @@ -#include // for M_PI -#include // for unique_ptr +#define _USE_MATH_DEFINES +#include +#include #include +#include "openmc/particle.h" #include "openmc/random_lcg.h" #include "openmc/source.h" -#include "openmc/particle.h" class RingSource : public openmc::Source { - public: - RingSource(double radius, double energy) : radius_(radius), energy_(energy) { } +public: + RingSource(double radius, double energy) : radius_(radius), energy_(energy) {} - // Defines a function that can create a unique pointer to a new instance of this class - // by extracting the parameters from the provided string. - static std::unique_ptr from_string(std::string parameters) - { - std::unordered_map parameter_mapping; + // Defines a function that can create a unique pointer to a new instance of + // this class by extracting the parameters from the provided string. + static std::unique_ptr from_string(std::string parameters) + { + std::unordered_map parameter_mapping; - std::stringstream ss(parameters); - std::string parameter; - while (std::getline(ss, parameter, ',')) { - parameter.erase(0, parameter.find_first_not_of(' ')); - std::string key = parameter.substr(0, parameter.find_first_of('=')); - std::string value = parameter.substr(parameter.find_first_of('=') + 1, parameter.length()); - parameter_mapping[key] = value; - } - - double radius = std::stod(parameter_mapping["radius"]); - double energy = std::stod(parameter_mapping["energy"]); - return std::make_unique(radius, energy); + std::stringstream ss(parameters); + std::string parameter; + while (std::getline(ss, parameter, ',')) { + parameter.erase(0, parameter.find_first_not_of(' ')); + std::string key = parameter.substr(0, parameter.find_first_of('=')); + std::string value = + parameter.substr(parameter.find_first_of('=') + 1, parameter.length()); + parameter_mapping[key] = value; } - // Samples from an instance of this class. - openmc::SourceSite sample(uint64_t* seed) const - { - openmc::SourceSite particle; - // particle type - particle.particle = openmc::ParticleType::neutron; - // position - double angle = 2.0 * M_PI * openmc::prn(seed); - double radius = this->radius_; - particle.r.x = radius * std::cos(angle); - particle.r.y = radius * std::sin(angle); - particle.r.z = 0.0; - // angle - particle.u = {1.0, 0.0, 0.0}; - particle.E = this->energy_; + double radius = std::stod(parameter_mapping["radius"]); + double energy = std::stod(parameter_mapping["energy"]); + return std::make_unique(radius, energy); + } - return particle; - } + // Samples from an instance of this class. + openmc::SourceSite sample(uint64_t* seed) const + { + openmc::SourceSite particle; + // particle type + particle.particle = openmc::ParticleType::neutron(); + // position + double angle = 2.0 * M_PI * openmc::prn(seed); + double radius = this->radius_; + particle.r.x = radius * std::cos(angle); + particle.r.y = radius * std::sin(angle); + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; - private: - double radius_; - double energy_; + return particle; + } + +private: + double radius_; + double energy_; }; -// A function to create a unique pointer to an instance of this class when generated -// via a plugin call using dlopen/dlsym. -// You must have external C linkage here otherwise dlopen will not find the file -extern "C" std::unique_ptr openmc_create_source(std::string parameters) +// A function to create a unique pointer to an instance of this class when +// generated via a plugin call using dlopen/dlsym. You must have external C +// linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source( + std::string parameters) { return RingSource::from_string(parameters); } diff --git a/examples/pincell_random_ray/build_xml.py b/examples/pincell_random_ray/build_xml.py index b3dd8020a..5ff4c0082 100644 --- a/examples/pincell_random_ray/build_xml.py +++ b/examples/pincell_random_ray/build_xml.py @@ -192,11 +192,10 @@ tallies.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot() +plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [pitch, pitch, pitch] plot.pixels = [1000, 1000, 1] -plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) diff --git a/include/openmc/angle_energy.h b/include/openmc/angle_energy.h index ac931b1b5..55deb5d41 100644 --- a/include/openmc/angle_energy.h +++ b/include/openmc/angle_energy.h @@ -14,8 +14,22 @@ namespace openmc { class AngleEnergy { public: + //! Sample an outgoing energy and scattering cosine + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] mu Outgoing cosine with respect to current direction + //! \param[inout] seed Pseudorandom seed pointer virtual void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const = 0; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + virtual double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const = 0; virtual ~AngleEnergy() = default; }; diff --git a/include/openmc/atomic_mass.h b/include/openmc/atomic_mass.h new file mode 100644 index 000000000..b4d515d2e --- /dev/null +++ b/include/openmc/atomic_mass.h @@ -0,0 +1,28 @@ +//============================================================================== +// atomic masses definitions +//============================================================================== + +#ifndef OPENMC_ATOMIC_MASS_H +#define OPENMC_ATOMIC_MASS_H + +#include +#include + +namespace openmc { + +// Values here are from the Committee on Data for Science and Technology +// (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). + +// Physical constants +constexpr double MASS_ELECTRON {5.48579909065e-4}; // mass of an electron in amu +constexpr double MASS_NEUTRON {1.00866491595}; // mass of a neutron in amu +constexpr double MASS_PROTON {1.007276466621}; // mass of a proton in amu +constexpr double MASS_DEUTRON {2.013553212745}; // mass of a deutron in amu +constexpr double MASS_HELION {3.014932247175}; // mass of a helion in amu +constexpr double MASS_ALPHA {4.001506179127}; // mass of an alpha in amu + +extern std::unordered_map ATOMIC_MASS; + +} // namespace openmc + +#endif // OPENMC_ATOMIC_MASS_H diff --git a/include/openmc/bank.h b/include/openmc/bank.h index c4e940bc8..6abcdd7f1 100644 --- a/include/openmc/bank.h +++ b/include/openmc/bank.h @@ -34,18 +34,24 @@ extern vector> ifp_fission_lifetime_bank; extern vector progeny_per_particle; +extern SharedArray shared_secondary_bank_read; +extern SharedArray shared_secondary_bank_write; + } // namespace simulation //============================================================================== // Non-member functions //============================================================================== -void sort_fission_bank(); +void sort_bank(SharedArray& bank, bool is_fission_bank); void free_memory_bank(); void init_fission_bank(int64_t max); +int64_t synchronize_global_secondary_bank( + SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_BANK_H diff --git a/include/openmc/bank_io.h b/include/openmc/bank_io.h index 90ffd820f..418ec111f 100644 --- a/include/openmc/bank_io.h +++ b/include/openmc/bank_io.h @@ -16,8 +16,9 @@ namespace openmc { template -void write_bank_dataset(const char* dataset_name, hid_t group_id, - span bank, const vector& bank_index, hid_t banktype +void write_bank_dataset( + const char* dataset_name, hid_t group_id, span bank, + const vector& bank_index, hid_t membanktype, hid_t filebanktype #ifdef OPENMC_MPI , MPI_Datatype mpi_dtype @@ -30,8 +31,8 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, #ifdef PHDF5 hsize_t dims[] {static_cast(dims_size)}; hid_t dspace = H5Screate_simple(1, dims, nullptr); - hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, H5P_DEFAULT, - H5P_DEFAULT, H5P_DEFAULT); + hid_t dset = H5Dcreate(group_id, dataset_name, filebanktype, dspace, + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); hsize_t count[] {static_cast(count_size)}; hid_t memspace = H5Screate_simple(1, count, nullptr); @@ -42,7 +43,7 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, hid_t plist = H5Pcreate(H5P_DATASET_XFER); H5Pset_dxpl_mpio(plist, H5FD_MPIO_COLLECTIVE); - H5Dwrite(dset, banktype, memspace, dspace, plist, bank.data()); + H5Dwrite(dset, membanktype, memspace, dspace, plist, bank.data()); H5Sclose(dspace); H5Sclose(memspace); @@ -52,7 +53,7 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, if (mpi::master) { hsize_t dims[] {static_cast(dims_size)}; hid_t dspace = H5Screate_simple(1, dims, nullptr); - hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, + hid_t dset = H5Dcreate(group_id, dataset_name, filebanktype, dspace, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); #ifdef OPENMC_MPI @@ -75,7 +76,8 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, H5Sselect_hyperslab( dspace_rank, H5S_SELECT_SET, start, nullptr, count, nullptr); - H5Dwrite(dset, banktype, memspace, dspace_rank, H5P_DEFAULT, bank.data()); + H5Dwrite( + dset, membanktype, memspace, dspace_rank, H5P_DEFAULT, bank.data()); H5Sclose(memspace); H5Sclose(dspace_rank); diff --git a/include/openmc/boundary_condition.h b/include/openmc/boundary_condition.h index af40131f1..89bf7ca8b 100644 --- a/include/openmc/boundary_condition.h +++ b/include/openmc/boundary_condition.h @@ -138,18 +138,28 @@ protected: //============================================================================== //! A BC that rotates particles about a global axis. // -//! Currently only rotations about the z-axis are supported. +//! Only rotations about the x, y, and z axes are supported. //============================================================================== class RotationalPeriodicBC : public PeriodicBC { public: - RotationalPeriodicBC(int i_surf, int j_surf); - + enum PeriodicAxis { x, y, z }; + RotationalPeriodicBC(int i_surf, int j_surf, PeriodicAxis axis); + double compute_periodic_rotation( + double rise_1, double run_1, double rise_2, double run_2) const; void handle_particle(Particle& p, const Surface& surf) const override; protected: //! Angle about the axis by which particle coordinates will be rotated double angle_; + //! Do we need to flip surfaces senses when applying the transformation? + bool flip_sense_; + //! Ensure that choice of axes is right handed. axis_1_idx_ corresponds to the + //! independent axis and axis_2_idx_ corresponds to the dependent axis in the + //! 2D plane perpendicular to the planes' axis of rotation + int zero_axis_idx_; + int axis_1_idx_; + int axis_2_idx_; }; } // namespace openmc diff --git a/include/openmc/bounding_box.h b/include/openmc/bounding_box.h index d02d92cb4..4fabe1b70 100644 --- a/include/openmc/bounding_box.h +++ b/include/openmc/bounding_box.h @@ -13,12 +13,19 @@ namespace openmc { //============================================================================== struct BoundingBox { - double xmin = -INFTY; - double xmax = INFTY; - double ymin = -INFTY; - double ymax = INFTY; - double zmin = -INFTY; - double zmax = INFTY; + Position min = {-INFTY, -INFTY, -INFTY}; + Position max = {INFTY, INFTY, INFTY}; + + // Constructors + BoundingBox() = default; + BoundingBox(Position min_, Position max_) : min {min_}, max {max_} {} + + // Static factory methods + static BoundingBox infinite() { return {}; } + static BoundingBox inverted() + { + return {{INFTY, INFTY, INFTY}, {-INFTY, -INFTY, -INFTY}}; + } inline BoundingBox operator&(const BoundingBox& other) { @@ -35,29 +42,26 @@ struct BoundingBox { // intersect operator inline BoundingBox& operator&=(const BoundingBox& other) { - xmin = std::max(xmin, other.xmin); - xmax = std::min(xmax, other.xmax); - ymin = std::max(ymin, other.ymin); - ymax = std::min(ymax, other.ymax); - zmin = std::max(zmin, other.zmin); - zmax = std::min(zmax, other.zmax); + min.x = std::max(min.x, other.min.x); + min.y = std::max(min.y, other.min.y); + min.z = std::max(min.z, other.min.z); + max.x = std::min(max.x, other.max.x); + max.y = std::min(max.y, other.max.y); + max.z = std::min(max.z, other.max.z); return *this; } // union operator inline BoundingBox& operator|=(const BoundingBox& other) { - xmin = std::min(xmin, other.xmin); - xmax = std::max(xmax, other.xmax); - ymin = std::min(ymin, other.ymin); - ymax = std::max(ymax, other.ymax); - zmin = std::min(zmin, other.zmin); - zmax = std::max(zmax, other.zmax); + min.x = std::min(min.x, other.min.x); + min.y = std::min(min.y, other.min.y); + min.z = std::min(min.z, other.min.z); + max.x = std::max(max.x, other.max.x); + max.y = std::max(max.y, other.max.y); + max.z = std::max(max.z, other.max.z); return *this; } - - inline Position min() const { return {xmin, ymin, zmin}; } - inline Position max() const { return {xmax, ymax, zmax}; } }; } // namespace openmc diff --git a/include/openmc/bremsstrahlung.h b/include/openmc/bremsstrahlung.h index 2f7e41bf0..d3b586831 100644 --- a/include/openmc/bremsstrahlung.h +++ b/include/openmc/bremsstrahlung.h @@ -3,7 +3,7 @@ #include "openmc/particle.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" namespace openmc { @@ -14,9 +14,9 @@ namespace openmc { class BremsstrahlungData { public: // Data - xt::xtensor pdf; //!< Bremsstrahlung energy PDF - xt::xtensor cdf; //!< Bremsstrahlung energy CDF - xt::xtensor yield; //!< Photon yield + tensor::Tensor pdf; //!< Bremsstrahlung energy PDF + tensor::Tensor cdf; //!< Bremsstrahlung energy CDF + tensor::Tensor yield; //!< Photon yield }; class Bremsstrahlung { @@ -32,9 +32,9 @@ public: namespace data { -extern xt::xtensor +extern tensor::Tensor ttb_e_grid; //! energy T of incident electron in [eV] -extern xt::xtensor +extern tensor::Tensor ttb_k_grid; //! reduced energy W/T of emitted photon } // namespace data diff --git a/include/openmc/capi.h b/include/openmc/capi.h index d8041ef41..9f6987d74 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -9,14 +9,41 @@ extern "C" { #endif +//! Run a stochastic volume calculation +// +//! \return Status (negative if an error occurred) int openmc_calculate_volumes(); + int openmc_cell_filter_get_bins( int32_t index, const int32_t** cells, int32_t* n); + +//! Get the fill for a cell +// +//! \param index Index in the cells array +//! \param type Type of the fill +//! \param indices Array of material indices for cell +//! \param n Length of indices array +//! \return Status (negative if an error occurred) int openmc_cell_get_fill( int32_t index, int* type, int32_t** indices, int32_t* n); + +//! Get the ID of a cell +// +//! \param index Index in the cells array +//! \param id ID of the cell +//! \return Status (negative if an error occurred) int openmc_cell_get_id(int32_t index, int32_t* id); + +//! Get the temperature of a cell +// +//! \param index Index in the cells array +//! \param instance Which instance of the cell. If a null pointer is +//! passed, the temperature of the first instance is returned. +//! \param T temperature of the cell +//!\return Status (negative if an error occurred) int openmc_cell_get_temperature( int32_t index, const int32_t* instance, double* T); + int openmc_cell_get_density( int32_t index, const int32_t* instance, double* rho); int openmc_cell_get_translation(int32_t index, double xyz[]); @@ -116,15 +143,63 @@ int openmc_mesh_set_id(int32_t index, int32_t id); int openmc_mesh_get_n_elements(int32_t index, size_t* n); int openmc_mesh_get_volumes(int32_t index, double* volumes); int openmc_mesh_material_volumes(int32_t index, int nx, int ny, int nz, - int max_mats, int32_t* materials, double* volumes); + int max_mats, int32_t* materials, double* volumes, double* bboxes); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); int openmc_new_filter(const char* type, int32_t* index); int openmc_next_batch(int* status); int openmc_nuclide_name(int index, const char** name); int openmc_plot_geometry(); +// Deprecated; use openmc_slice_data. int openmc_id_map(const void* slice, int32_t* data_out); +// Deprecated; use openmc_slice_data. int openmc_property_map(const void* slice, double* data_out); +int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool show_overlaps, int level, + int32_t filter_index, int32_t* geom_data, double* property_data); +int openmc_get_plot_index(int32_t id, int32_t* index); +int openmc_plot_get_id(int32_t index, int32_t* id); +int openmc_plot_set_id(int32_t index, int32_t id); +int openmc_solidraytrace_plot_create(int32_t* index); +int openmc_solidraytrace_plot_get_pixels( + int32_t index, int32_t* width, int32_t* height); +int openmc_solidraytrace_plot_set_pixels( + int32_t index, int32_t width, int32_t height); +int openmc_solidraytrace_plot_get_color_by(int32_t index, int32_t* color_by); +int openmc_solidraytrace_plot_set_color_by(int32_t index, int32_t color_by); +int openmc_solidraytrace_plot_set_default_colors(int32_t index); +int openmc_solidraytrace_plot_set_all_opaque(int32_t index); +int openmc_solidraytrace_plot_set_opaque( + int32_t index, int32_t id, bool visible); +int openmc_solidraytrace_plot_set_color( + int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b); +int openmc_solidraytrace_plot_get_camera_position( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_camera_position( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_look_at( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_look_at( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_up( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_up( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_light_position( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_light_position( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov); +int openmc_solidraytrace_plot_set_fov(int32_t index, double fov); +int openmc_solidraytrace_plot_update_view(int32_t index); +int openmc_solidraytrace_plot_create_image( + int32_t index, uint8_t* data_out, int32_t width, int32_t height); +int openmc_solidraytrace_plot_get_color( + int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b); +int openmc_solidraytrace_plot_get_diffuse_fraction( + int32_t index, double* diffuse_fraction); +int openmc_solidraytrace_plot_set_diffuse_fraction( + int32_t index, double diffuse_fraction); int openmc_rectilinear_mesh_get_grid(int32_t index, double** grid_x, int* nx, double** grid_y, int* ny, double** grid_z, int* nz); int openmc_rectilinear_mesh_set_grid(int32_t index, const double* grid_x, @@ -140,6 +215,7 @@ int openmc_remove_tally(int32_t index); int openmc_reset(); int openmc_reset_timers(); int openmc_run(); +void openmc_run_random_ray(); int openmc_sample_external_source(size_t n, uint64_t* seed, void* sites); void openmc_set_seed(int64_t new_seed); void openmc_set_stride(uint64_t new_stride); @@ -201,8 +277,8 @@ int openmc_weight_windows_set_energy_bounds( int32_t index, double* e_bounds, size_t e_bounds_size); int openmc_weight_windows_get_energy_bounds( int32_t index, const double** e_bounds, size_t* e_bounds_size); -int openmc_weight_windows_set_particle(int32_t index, int particle); -int openmc_weight_windows_get_particle(int32_t index, int* particle); +int openmc_weight_windows_set_particle(int32_t index, int32_t particle); +int openmc_weight_windows_get_particle(int32_t index, int32_t* particle); int openmc_weight_windows_get_bounds(int32_t index, const double** lower_bounds, const double** upper_bounds, size_t* size); int openmc_weight_windows_set_bounds(int32_t index, const double* lower_bounds, @@ -218,6 +294,7 @@ int openmc_weight_windows_set_weight_cutoff(int32_t index, double cutoff); int openmc_weight_windows_get_max_split(int32_t index, int* max_split); int openmc_weight_windows_set_max_split(int32_t index, int max_split); size_t openmc_weight_windows_size(); +size_t openmc_plots_size(); int openmc_weight_windows_export(const char* filename = nullptr); int openmc_weight_windows_import(const char* filename = nullptr); int openmc_zernike_filter_get_order(int32_t index, int* order); @@ -227,10 +304,10 @@ int openmc_zernike_filter_set_order(int32_t index, int order); int openmc_zernike_filter_set_params( int32_t index, const double* x, const double* y, const double* r); -int openmc_particle_filter_get_bins(int32_t idx, int bins[]); +int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[]); //! Sets the mesh and energy grid for CMFD reweight -//! \param[in] meshtyally_id id of CMFD Mesh Tally +//! \param[in] meshtally_id id of CMFD Mesh Tally //! \param[in] cmfd_indices indices storing spatial and energy dimensions of //! CMFD problem \param[in] norm CMFD normalization factor void openmc_initialize_mesh_egrid( diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 9dfe2339a..1a27ad516 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -123,11 +123,11 @@ private: //! BoundingBox if the particle is in a complex cell. BoundingBox bounding_box_complex(vector postfix) const; - //! Enfource precedence: Parenthases, Complement, Intersection, Union - void add_precedence(); + //! Enforce precedence between intersections and unions + void enforce_precedence(); //! Add parenthesis to enforce precedence - int64_t add_parentheses(int64_t start); + void add_parentheses(int64_t start); //! Remove complement operators from the expression void remove_complement_ops(); @@ -145,6 +145,30 @@ private: bool simple_; //!< Does the region contain only intersections? }; +//============================================================================== +// XML parsing helpers for nodes +//============================================================================== + +//! Parse material IDs from a XML node. +//! \param node XML node containing a "material" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of material IDs (MATERIAL_VOID for "void") +vector parse_cell_material_xml(pugi::xml_node node, int32_t cell_id); + +//! Parse temperatures in [K] from a XML node. +//! Validates that all values are non-negative and the list is non-empty. +//! \param node XML node containing a "temperature" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of temperatures in [K] +vector parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id); + +//! Parse densities in [g/cm³] from a XML node. +//! Validates that all values are positive and the list is non-empty. +//! \param node XML node containing a "density" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of densities in [g/cm³] +vector parse_cell_density_xml(pugi::xml_node node, int32_t cell_id); + //============================================================================== class Cell { diff --git a/include/openmc/chain.h b/include/openmc/chain.h index a3bc6f3a3..e01f2a01c 100644 --- a/include/openmc/chain.h +++ b/include/openmc/chain.h @@ -71,6 +71,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: const Distribution* photon_energy_; }; @@ -92,6 +101,8 @@ extern vector> chain_nuclides; void read_chain_file_xml(); +void free_memory_chain(); + } // namespace openmc #endif // OPENMC_CHAIN_H diff --git a/include/openmc/constants.h b/include/openmc/constants.h index bba260c3a..26b4a224c 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -9,6 +9,7 @@ #include #include "openmc/array.h" +#include "openmc/atomic_mass.h" #include "openmc/vector.h" #include "openmc/version.h" @@ -25,16 +26,16 @@ using double_4dvec = vector>>>; constexpr int HDF5_VERSION[] {3, 0}; // Version numbers for binary files -constexpr array VERSION_STATEPOINT {18, 1}; -constexpr array VERSION_PARTICLE_RESTART {2, 0}; -constexpr array VERSION_TRACK {3, 0}; +constexpr array VERSION_STATEPOINT {18, 2}; +constexpr array VERSION_PARTICLE_RESTART {2, 1}; +constexpr array VERSION_TRACK {3, 1}; constexpr array VERSION_SUMMARY {6, 1}; constexpr array VERSION_VOLUME {1, 0}; constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; -constexpr array VERSION_COLLISION_TRACK {1, 0}; +constexpr array VERSION_COLLISION_TRACK {1, 2}; // ============================================================================ // ADJUSTABLE PARAMETERS @@ -86,10 +87,10 @@ constexpr double INFTY {std::numeric_limits::max()}; // (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). // Physical constants -constexpr double MASS_NEUTRON {1.00866491595}; // mass of a neutron in amu +constexpr double AMU_EV { + 9.3149410242e8}; // atomic mass unit energy equivalent in eV/c^2 constexpr double MASS_NEUTRON_EV { - 939.56542052e6}; // mass of a neutron in eV/c^2 -constexpr double MASS_PROTON {1.007276466621}; // mass of a proton in amu + 939.56542052e6}; // neutron mass energy equivalent in eV/c^2 constexpr double MASS_ELECTRON_EV { 0.51099895000e6}; // electron mass energy equivalent in eV/c^2 constexpr double FINE_STRUCTURE { @@ -226,6 +227,7 @@ enum ReactionType { N_XA = 207, HEATING = 301, DAMAGE_ENERGY = 444, + PHOTON_TOTAL = 501, COHERENT = 502, INCOHERENT = 504, PAIR_PROD_ELEC = 515, @@ -301,7 +303,7 @@ enum class TallyEstimator { ANALOG, TRACKLENGTH, COLLISION }; enum class TallyEvent { SURFACE, LATTICE, KILL, SCATTER, ABSORB }; // Tally score type -- if you change these, make sure you also update the -// _SCORES dictionary in openmc/capi/tally.py +// _SCORES dictionary in openmc/lib/tally.py // // These are kept as a normal enum and made negative, since variables which // store one of these enum values usually also may be responsible for storing @@ -365,7 +367,8 @@ enum class SolverType { MONTE_CARLO, RANDOM_RAY }; enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; -enum class RandomRaySampleMethod { PRNG, HALTON }; +enum class RandomRaySampleMethod { PRNG, HALTON, S2 }; +enum class RandomRaySolve { FORWARD, FORWARD_FOR_ADJOINT, ADJOINT }; //============================================================================== // Geometry Constants diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 0e27402a1..858636c0b 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -94,6 +94,10 @@ private: class DAGUniverse : public Universe { public: + using MaterialOverrides = std::unordered_map>; + using TemperatureOverrides = std::unordered_map>; + using DensityOverrides = std::unordered_map>; + explicit DAGUniverse(pugi::xml_node node); //! Create a new DAGMC universe @@ -112,6 +116,9 @@ public: //! Initialize the DAGMC accel. data structures, indices, material //! assignments, etc. void initialize(); + void initialize(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides = {}); //! Reads UWUW materials and returns an ID map void read_uwuw_materials(); @@ -146,7 +153,8 @@ public: //! Assign a material overriding normal assignement to a cell //! \param[in] c The OpenMC cell to which the material is assigned - void override_assign_material(std::unique_ptr& c) const; + void override_assign_material(std::unique_ptr& c, + const MaterialOverrides& material_overrides) const; //! Return the index into the model cells vector for a given DAGMC volume //! handle in the universe @@ -187,7 +195,9 @@ private: void set_id(); //!< Deduce the universe id from model::universes void init_dagmc(); //!< Create and initialise DAGMC pointer void init_metadata(); //!< Create and initialise dagmcMetaData pointer - void init_geometry(); //!< Create cells and surfaces from DAGMC entities + void init_geometry(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides); std::string filename_; //!< Name of the DAGMC file used to create this universe @@ -201,11 +211,6 @@ private: //!< generate new material IDs for the universe bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard" //!< volume - std::unordered_map> - material_overrides_; //!< Map of material overrides - //!< keys correspond to the DAGMCCell id - //!< values are a list of material ids used - //!< for the override }; //============================================================================== diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 854cf7d77..f319dd19d 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -15,6 +15,17 @@ namespace openmc { +//============================================================================== +// Helper function for computing importance weights from biased sampling +//============================================================================== + +//! Compute importance weights for biased sampling +//! \param p Unnormalized original probability vector +//! \param b Unnormalized bias probability vector +//! \return Vector of importance weights (p_norm[i] / b_norm[i]) +vector compute_importance_weights( + const vector& p, const vector& b); + //============================================================================== //! Abstract class representing a univariate probability distribution //============================================================================== @@ -22,11 +33,41 @@ namespace openmc { class Distribution { public: virtual ~Distribution() = default; - virtual double sample(uint64_t* seed) const = 0; + + //! Sample a value from the distribution, handling biasing automatically + //! \param seed Pseudorandom number seed pointer + //! \return (sampled value, importance weight) + virtual std::pair sample(uint64_t* seed) const; + + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + virtual double evaluate(double x) const; //! Return integral of distribution //! \return Integral of distribution virtual double integral() const { return 1.0; }; + + //! Set bias distribution + virtual void set_bias(std::unique_ptr bias) + { + bias_ = std::move(bias); + } + + const Distribution* bias() const { return bias_.get(); } + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + virtual double sample_unbiased(uint64_t* seed) const = 0; + + //! Read bias distribution from XML + //! \param node XML node that may contain a bias child element + void read_bias_from_xml(pugi::xml_node node); + + // Biasing distribution + unique_ptr bias_; }; using UPtrDist = unique_ptr; @@ -50,7 +91,7 @@ public: //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled value + //! \return Sampled index size_t sample(uint64_t* seed) const; // Properties @@ -67,7 +108,7 @@ private: //! Normalize distribution so that probabilities sum to unity void normalize(); - //! Initialize alias tables for distribution + //! Initialize alias table for sampling void init_alias(); }; @@ -82,20 +123,30 @@ public: //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! \return (sampled value, sample weight) + std::pair sample(uint64_t* seed) const override; double integral() const override { return di_.integral(); }; + //! Override set_bias as no-op (bias handled in constructor) + void set_bias(std::unique_ptr bias) override {} + // Properties const vector& x() const { return x_; } const vector& prob() const { return di_.prob(); } const vector& alias() const { return di_.alias(); } + const vector& weight() const { return weight_; } + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; private: - vector x_; //!< Possible outcomes - DiscreteIndex di_; //!< discrete probability distribution of - //!< outcome indices + vector x_; //!< Possible outcomes + vector weight_; //!< Importance weights (empty if unbiased) + DiscreteIndex di_; //!< Discrete probability distribution of outcome indices }; //============================================================================== @@ -107,14 +158,20 @@ public: explicit Uniform(pugi::xml_node node); Uniform(double a, double b) : a_ {a}, b_ {b} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double a() const { return a_; } double b() const { return b_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double a_; //!< Lower bound of distribution double b_; //!< Upper bound of distribution @@ -131,15 +188,21 @@ public: : offset_ {std::pow(a, n + 1)}, span_ {std::pow(b, n + 1) - offset_}, ninv_ {1 / (n + 1)} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double a() const { return std::pow(offset_, ninv_); } double b() const { return std::pow(offset_ + span_, ninv_); } double n() const { return 1 / ninv_ - 1; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: //! Store processed values in object to allow for faster sampling double offset_; //!< a^(n+1) @@ -156,13 +219,19 @@ public: explicit Maxwell(pugi::xml_node node); Maxwell(double theta) : theta_ {theta} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double theta() const { return theta_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double theta_; //!< Factor in exponential [eV] }; @@ -176,41 +245,66 @@ public: explicit Watt(pugi::xml_node node); Watt(double a, double b) : a_ {a}, b_ {b} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double a() const { return a_; } double b() const { return b_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double a_; //!< Factor in exponential [eV] double b_; //!< Factor in square root [1/eV] }; //============================================================================== -//! Normal distributions with form 1/2*std_dev*sqrt(pi) exp -//! (-(e-E0)/2*std_dev)^2 +//! Normal distribution with optional truncation bounds. +//! +//! The standard normal PDF is 1/(sqrt(2*pi)*sigma) * +//! exp(-(x-mu)^2/(2*sigma^2)). When truncated to [lower, upper], the PDF is +//! renormalized so that it integrates to 1 over the truncation interval. //============================================================================== class Normal : public Distribution { public: explicit Normal(pugi::xml_node node); - Normal(double mean_value, double std_dev) - : mean_value_ {mean_value}, std_dev_ {std_dev} {}; + Normal(double mean_value, double std_dev, double lower = -INFTY, + double upper = INFTY); - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x), accounting for truncation normalization + double evaluate(double x) const override; double mean_value() const { return mean_value_; } double std_dev() const { return std_dev_; } + double lower() const { return lower_; } + double upper() const { return upper_; } + bool is_truncated() const { return is_truncated_; } + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; private: - double mean_value_; //!< middle of distribution [eV] - double std_dev_; //!< standard deviation [eV] + double mean_value_; //!< Mean of distribution + double std_dev_; //!< Standard deviation + double lower_; //!< Lower truncation bound (default: -INFTY) + double upper_; //!< Upper truncation bound (default: +INFTY) + bool is_truncated_; //!< True if bounds are finite + double norm_factor_; //!< Normalization factor for truncated distribution + + //! Compute normalization factor for truncated distribution + void compute_normalization(); }; //============================================================================== @@ -223,10 +317,10 @@ public: Tabular(const double* x, const double* p, int n, Interpolation interp, const double* c = nullptr); - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; // properties vector& x() { return x_; } @@ -235,6 +329,12 @@ public: Interpolation interp() const { return interp_; } double integral() const override { return integral_; }; +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: vector x_; //!< tabulated independent variable vector p_; //!< tabulated probability density @@ -259,13 +359,19 @@ public: explicit Equiprobable(pugi::xml_node node); Equiprobable(const double* x, int n) : x_ {x, x + n} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; const vector& x() const { return x_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: vector x_; //! Possible outcomes }; @@ -280,18 +386,90 @@ public: //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! \return (sampled value, sample weight) + std::pair sample(uint64_t* seed) const override; double integral() const override { return integral_; } -private: - // Storrage for probability + distribution - using DistPair = std::pair; + //! Override set_bias as no-op (bias handled in constructor) + void set_bias(std::unique_ptr bias) override {} - vector - distribution_; //!< sub-distributions + cummulative probabilities - double integral_; //!< integral of distribution +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + +private: + vector distribution_; //!< Sub-distributions + vector weight_; //!< Importance weights for component selection + DiscreteIndex di_; //!< Discrete probability distribution of indices + double integral_; //!< Integral of distribution +}; + +//============================================================================== +// DecaySpectrum — non-owning mixture of decay photon distributions +//============================================================================== + +//! Energy distribution formed by mixing multiple decay photon spectra. +//! +//! Unlike the general Mixture distribution, this class holds non-owning +//! pointers to the component distributions (which live in +//! data::chain_nuclides). Each component is weighted by the activity +//! (atoms * decay_constant) of the corresponding nuclide. + +class DecaySpectrum : public Distribution { +public: + //============================================================================ + // Types, aliases + + struct Sample { + double energy; + double weight; + int parent_nuclide; + }; + + //============================================================================ + // Constructors + + //! Construct from an XML node containing nuclide names and atom densities. + //! + //! Reads child ```` elements with ``name`` and ``density`` + //! attributes, resolves them against the loaded depletion chain, and + //! constructs the mixed distribution. + explicit DecaySpectrum(pugi::xml_node node); + + //============================================================================ + // Methods + + //! Sample a value from the distribution and return the parent nuclide index + //! \param seed Pseudorandom number seed pointer + //! \return (Sampled energy, sample weight, chain nuclide index) + Sample sample_with_parent(uint64_t* seed) const; + + //! Sample a value from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return (sampled value, sample weight) + std::pair sample(uint64_t* seed) const override; + + double integral() const override; + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + +private: + //! Initialize decay spectrum sampling data + //! \param nuclide_indices Indices of decay photon emitters in + //! data::chain_nuclides + //! \param atoms Number of atoms for each component. + void init(vector nuclide_indices, const vector& atoms); + + vector nuclide_indices_; //!< Indices of emitting nuclides in the chain + DiscreteIndex di_; //!< Discrete index for component selection + double integral_; //!< Total photon emission rate }; } // namespace openmc diff --git a/include/openmc/distribution_angle.h b/include/openmc/distribution_angle.h index efd4e5842..78de70c42 100644 --- a/include/openmc/distribution_angle.h +++ b/include/openmc/distribution_angle.h @@ -26,6 +26,12 @@ public: //! \return Cosine of the angle in the range [-1,1] double sample(double E, uint64_t* seed) const; + //! Evaluate the angular PDF at a given energy and cosine + //! \param[in] E Particle energy in [eV] + //! \param[in] mu Cosine of the scattering angle + //! \return Probability density for the scattering cosine + double evaluate(double E, double mu) const; + //! Determine whether angle distribution is empty //! \return Whether distribution is empty bool empty() const { return energy_.empty(); } diff --git a/include/openmc/distribution_energy.h b/include/openmc/distribution_energy.h index 9b08ed039..efacb9131 100644 --- a/include/openmc/distribution_energy.h +++ b/include/openmc/distribution_energy.h @@ -5,7 +5,7 @@ #define OPENMC_DISTRIBUTION_ENERGY_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/endf.h" @@ -86,9 +86,9 @@ private: struct CTTable { Interpolation interpolation; //!< Interpolation law int n_discrete; //!< Number of of discrete energies - xt::xtensor e_out; //!< Outgoing energies in [eV] - xt::xtensor p; //!< Probability density - xt::xtensor c; //!< Cumulative distribution + tensor::Tensor e_out; //!< Outgoing energies in [eV] + tensor::Tensor p; //!< Probability density + tensor::Tensor c; //!< Cumulative distribution }; int n_region_; //!< Number of inteprolation regions diff --git a/include/openmc/distribution_multi.h b/include/openmc/distribution_multi.h index 75126593f..a72780737 100644 --- a/include/openmc/distribution_multi.h +++ b/include/openmc/distribution_multi.h @@ -6,6 +6,7 @@ #include "pugixml.hpp" #include "openmc/distribution.h" +#include "openmc/error.h" #include "openmc/position.h" namespace openmc { @@ -26,8 +27,16 @@ public: //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Direction sampled - virtual Direction sample(uint64_t* seed) const = 0; + //! \return (sampled Direction, sample weight) + virtual std::pair sample(uint64_t* seed) const = 0; + + //! Evaluate the probability density for a given direction + //! \param[in] u Direction on the unit sphere + //! \return Probability density at the given direction + virtual double evaluate(Direction u) const + { + fatal_error("evaluate not available for this UnitSphereDistribution type"); + } Direction u_ref_ {0.0, 0.0, 1.0}; //!< reference direction }; @@ -43,14 +52,33 @@ public: //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Direction sampled - Direction sample(uint64_t* seed) const override; + //! \return (sampled Direction, sample weight) + std::pair sample(uint64_t* seed) const override; + + //! Sample a direction and return evaluation of the PDF for biased sampling. + //! Note that bias distributions are intended to return unit-weight samples. + //! \param seed Pseudorandom number seed points + //! \return (sampled Direction, value of the PDF at this Direction) + std::pair sample_as_bias(uint64_t* seed) const; + + //! Evaluate the probability density for a given direction + //! \param[in] u Direction on the unit sphere + //! \return Probability density at the given direction + double evaluate(Direction u) const override; // Observing pointers Distribution* mu() const { return mu_.get(); } Distribution* phi() const { return phi_.get(); } private: + //! Common sampling implementation + //! \param seed Pseudorandom number seed pointer + //! \param return_pdf If true, return PDF evaluation; if false, return + //! importance weight + //! \return (sampled Direction, weight or PDF value) + std::pair sample_impl( + uint64_t* seed, bool return_pdf) const; + Direction v_ref_ {1.0, 0.0, 0.0}; //!< reference direction Direction w_ref_; UPtrDist mu_; //!< Distribution of polar angle @@ -66,11 +94,29 @@ Direction isotropic_direction(uint64_t* seed); class Isotropic : public UnitSphereDistribution { public: Isotropic() {}; + explicit Isotropic(pugi::xml_node node); //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled direction - Direction sample(uint64_t* seed) const override; + //! \return (sampled direction, sample weight) + std::pair sample(uint64_t* seed) const override; + + //! Evaluate the probability density for a given direction + //! \param[in] u Direction on the unit sphere + //! \return Probability density at the given direction + double evaluate(Direction u) const override; + + // Set or get bias distribution + void set_bias(std::unique_ptr bias) + { + bias_ = std::move(bias); + } + + const PolarAzimuthal* bias() const { return bias_.get(); } + +protected: + // Biasing distribution + unique_ptr bias_; }; //============================================================================== @@ -85,8 +131,8 @@ public: //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled direction - Direction sample(uint64_t* seed) const override; + //! \return (sampled direction, sample weight) + std::pair sample(uint64_t* seed) const override; }; using UPtrAngle = unique_ptr; diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 9c3bc743f..663cf5568 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -19,7 +19,9 @@ public: virtual ~SpatialDistribution() = default; //! Sample a position from the distribution - virtual Position sample(uint64_t* seed) const = 0; + //! \param seed Pseudorandom number seed pointer + //! \return Sampled (position, importance weight) + virtual std::pair sample(uint64_t* seed) const = 0; static unique_ptr create(pugi::xml_node node); }; @@ -34,8 +36,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; // Observer pointers Distribution* x() const { return x_.get(); } @@ -58,19 +60,25 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; Distribution* r() const { return r_.get(); } Distribution* phi() const { return phi_.get(); } Distribution* z() const { return z_.get(); } Position origin() const { return origin_; } + Direction r_dir() const { return r_dir_; } + Direction phi_dir() const { return phi_dir_; } + Direction z_dir() const { return z_dir_; } private: - UPtrDist r_; //!< Distribution of r coordinates - UPtrDist phi_; //!< Distribution of phi coordinates - UPtrDist z_; //!< Distribution of z coordinates - Position origin_; //!< Cartesian coordinates of the cylinder center + UPtrDist r_; //!< Distribution of r coordinates + UPtrDist phi_; //!< Distribution of phi coordinates + UPtrDist z_; //!< Distribution of z coordinates + Position origin_; //!< Cartesian coordinates of the cylinder center + Direction r_dir_; //!< Direction of r-axis at phi=0 + Direction phi_dir_; //!< Direction of phi-axis at phi=0 + Direction z_dir_; //!< Direction of z-axis }; //============================================================================== @@ -83,8 +91,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; Distribution* r() const { return r_.get(); } Distribution* cos_theta() const { return cos_theta_.get(); } @@ -109,8 +117,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; //! Sample the mesh for an element and position within that element //! \param seed Pseudorandom number seed pointer @@ -133,8 +141,8 @@ public: private: int32_t mesh_idx_ {C_NONE}; - DiscreteIndex elem_idx_dist_; //!< Distribution of - //!< mesh element indices + DiscreteIndex elem_idx_dist_; //!< Distribution of mesh element indices + vector weight_; //!< Importance weights (empty if unbiased) }; //============================================================================== @@ -149,12 +157,13 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; private: std::vector point_cloud_; DiscreteIndex point_idx_dist_; //!< Distribution of Position indices + vector weight_; //!< Importance weights (empty if unbiased) }; //============================================================================== @@ -164,11 +173,12 @@ private: class SpatialBox : public SpatialDistribution { public: explicit SpatialBox(pugi::xml_node node, bool fission = false); + SpatialBox(Position lower_left, Position upper_right, bool fission = false); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; // Properties bool only_fissionable() const { return only_fissionable_; } @@ -193,8 +203,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; Position r() const { return r_; } diff --git a/include/openmc/eigenvalue.h b/include/openmc/eigenvalue.h index b456fee21..438747676 100644 --- a/include/openmc/eigenvalue.h +++ b/include/openmc/eigenvalue.h @@ -6,7 +6,7 @@ #include // for int64_t -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include "openmc/array.h" @@ -24,7 +24,7 @@ namespace simulation { extern double keff_generation; //!< Single-generation k on each processor extern array k_sum; //!< Used to reduce sum and sum_sq extern vector entropy; //!< Shannon entropy at each generation -extern xt::xtensor source_frac; //!< Source fraction for UFS +extern tensor::Tensor source_frac; //!< Source fraction for UFS } // namespace simulation diff --git a/include/openmc/endf.h b/include/openmc/endf.h index 4a737eb88..34fee9758 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -33,6 +33,13 @@ bool is_disappearance(int MT); //! \return Whether corresponding reaction is an inelastic scattering reaction bool is_inelastic_scatter(int MT); +//! Determine whether an MT number matches a target MT, considering that the +//! target may be a summation reaction. +//! \param[in] event_mt MT number of the actual event +//! \param[in] target_mt MT number to check against +//! \return Whether event_mt is a component of target_mt (or equal to it) +bool mt_matches(int event_mt, int target_mt); + //============================================================================== //! Abstract one-dimensional function //============================================================================== diff --git a/include/openmc/error.h b/include/openmc/error.h index d73795aee..1f6e15f49 100644 --- a/include/openmc/error.h +++ b/include/openmc/error.h @@ -64,14 +64,14 @@ void write_message( int level, const std::string& message, const Params&... fmt_args) { if (settings::verbosity >= level) { - write_message(fmt::format(message, fmt_args...)); + write_message(fmt::format(fmt::runtime(message), fmt_args...)); } } template void write_message(const std::string& message, const Params&... fmt_args) { - write_message(fmt::format(message, fmt_args...)); + write_message(fmt::format(fmt::runtime(message), fmt_args...)); } #ifdef OPENMC_MPI diff --git a/include/openmc/event.h b/include/openmc/event.h index 2d215a10e..48a009f2b 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -112,6 +112,19 @@ void process_collision_events(); //! \param n_particles The number of particles in the particle buffer void process_death_events(int64_t n_particles); +//! Process event queues until all are empty. Each iteration processes the +//! longest queue first to maximize vectorization efficiency. +void process_transport_events(); + +//! Initialize secondary particles from a shared secondary bank for +//! event-based transport +// +//! \param n_particles The number of particles to initialize +//! \param offset The offset index in the shared secondary bank +//! \param shared_secondary_bank The shared secondary bank to read from +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_EVENT_H diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 107cc7d1f..43cf9bb58 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -3,9 +3,12 @@ #include #include +#include +#include #include "openmc/array.h" #include "openmc/constants.h" +#include "openmc/random_ray/source_region.h" // For hash_combine #include "openmc/vector.h" namespace openmc { @@ -13,6 +16,34 @@ namespace openmc { class BoundaryInfo; class GeometryState; +//============================================================================== +//! OverlapKey to store cell and universe data of a single overlap, along with +//! a functor for hashing an OverlapKey into an unordered_map. +//============================================================================== + +struct OverlapKey { + int universe_id; + int cell1_id; + int cell2_id; + + bool operator==(const OverlapKey& other) const + { + return universe_id == other.universe_id && cell1_id == other.cell1_id && + cell2_id == other.cell2_id; + } +}; + +struct OverlapKeyHash { + std::size_t operator()(const OverlapKey& k) const + { + size_t seed = 0; + hash_combine(seed, k.universe_id); + hash_combine(seed, k.cell1_id); + hash_combine(seed, k.cell2_id); + return seed; + } +}; + //============================================================================== // Global variables //============================================================================== @@ -24,6 +55,10 @@ extern "C" int n_coord_levels; //!< Number of CSG coordinate levels extern vector overlap_check_count; +// Overlap data structures get cleared every slice_data run +extern vector overlap_keys; +extern std::unordered_map overlap_key_index; + } // namespace model //============================================================================== @@ -38,8 +73,7 @@ inline bool coincident(double d1, double d2) //============================================================================== //! Check for overlapping cells at a particle's position. //============================================================================== - -bool check_cell_overlap(GeometryState& p, bool error = true); +int check_cell_overlap(GeometryState& p, bool error = true); //============================================================================== //! Get the cell instance for a particle at the specified universe level diff --git a/include/openmc/hdf5_interface.h b/include/openmc/hdf5_interface.h index 28b0d2b11..93e4bfc82 100644 --- a/include/openmc/hdf5_interface.h +++ b/include/openmc/hdf5_interface.h @@ -11,8 +11,7 @@ #include "hdf5.h" #include "hdf5_hl.h" -#include "xtensor/xadapt.hpp" -#include "xtensor/xarray.hpp" +#include "openmc/tensor.h" #include "openmc/array.h" #include "openmc/error.h" @@ -166,24 +165,19 @@ void read_attribute(hid_t obj_id, const char* name, vector& vec) read_attr(obj_id, name, H5TypeMap::type_id, vec.data()); } -// Generic array version +// Tensor version template -void read_attribute(hid_t obj_id, const char* name, xt::xarray& arr) +void read_attribute(hid_t obj_id, const char* name, tensor::Tensor& tensor) { - // Get shape of attribute array + // Get shape of attribute auto shape = attribute_shape(obj_id, name); - // Allocate new array to read data into - std::size_t size = 1; - for (const auto x : shape) - size *= x; - vector buffer(size); + // Resize tensor and read data directly + vector tshape(shape.begin(), shape.end()); + tensor.resize(tshape); // Read data from attribute - read_attr(obj_id, name, H5TypeMap::type_id, buffer.data()); - - // Adapt array into xarray - arr = xt::adapt(buffer, shape); + read_attr(obj_id, name, H5TypeMap::type_id, tensor.data()); } // overload for std::string @@ -290,63 +284,34 @@ void read_dataset( } template -void read_dataset(hid_t dset, xt::xarray& arr, bool indep = false) +void read_dataset(hid_t dset, tensor::Tensor& tensor, bool indep = false) { // Get shape of dataset vector shape = object_shape(dset); - // Allocate space in the array to read data into - std::size_t size = 1; - for (const auto x : shape) - size *= x; - arr.resize(shape); + // Resize tensor and read data directly + vector tshape(shape.begin(), shape.end()); + tensor.resize(tshape); - // Read data from attribute + // Read data from dataset read_dataset_lowlevel( - dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, arr.data()); + dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, tensor.data()); } template<> void read_dataset( - hid_t dset, xt::xarray>& arr, bool indep); + hid_t dset, tensor::Tensor>& tensor, bool indep); template void read_dataset( - hid_t obj_id, const char* name, xt::xarray& arr, bool indep = false) + hid_t obj_id, const char* name, tensor::Tensor& tensor, bool indep = false) { - // Open dataset and read array + // Open dataset and read tensor hid_t dset = open_dataset(obj_id, name); - read_dataset(dset, arr, indep); + read_dataset(dset, tensor, indep); close_dataset(dset); } -template -void read_dataset( - hid_t obj_id, const char* name, xt::xtensor& arr, bool indep = false) -{ - // Open dataset and read array - hid_t dset = open_dataset(obj_id, name); - - // Get shape of dataset - vector hsize_t_shape = object_shape(dset); - close_dataset(dset); - - // cast from hsize_t to size_t - vector shape(hsize_t_shape.size()); - for (int i = 0; i < shape.size(); i++) { - shape[i] = static_cast(hsize_t_shape[i]); - } - - // Allocate new xarray to read data into - xt::xarray xarr(shape); - - // Read data from the dataset - read_dataset(obj_id, name, xarr); - - // Copy into xtensor - arr = xarr; -} - // overload for Position inline void read_dataset( hid_t obj_id, const char* name, Position& r, bool indep = false) @@ -358,31 +323,22 @@ inline void read_dataset( r.z = x[2]; } -template +template inline void read_dataset_as_shape( - hid_t obj_id, const char* name, xt::xtensor& arr, bool indep = false) + hid_t obj_id, const char* name, tensor::Tensor& tensor, bool indep = false) { hid_t dset = open_dataset(obj_id, name); - // Allocate new array to read data into - std::size_t size = 1; - for (const auto x : arr.shape()) - size *= x; - vector buffer(size); - - // Read data from attribute + // Read data directly into pre-shaped tensor read_dataset_lowlevel( - dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, buffer.data()); - - // Adapt into xarray - arr = xt::adapt(buffer, arr.shape()); + dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, tensor.data()); close_dataset(dset); } -template -inline void read_nd_vector(hid_t obj_id, const char* name, - xt::xtensor& result, bool must_have = false) +template +inline void read_nd_tensor(hid_t obj_id, const char* name, + tensor::Tensor& result, bool must_have = false) { if (object_exists(obj_id, name)) { read_dataset_as_shape(obj_id, name, result, true); @@ -496,12 +452,16 @@ inline void write_dataset( false, buffer.data()); } -// Template for xarray, xtensor, etc. -template -inline void write_dataset( - hid_t obj_id, const char* name, const xt::xcontainer& arr) +// Template for Tensor and StaticTensor2D. A SFINAE guard is used here to +// prevent this template from matching vector/string types that have their own +// overloads above. A generic Container parameter avoids duplicating the body +// for both Tensor and StaticTensor2D. +template>::value>> +inline void write_dataset(hid_t obj_id, const char* name, const Container& arr) { - using T = typename D::value_type; + using T = typename std::decay_t::value_type; auto s = arr.shape(); vector dims {s.cbegin(), s.cend()}; write_dataset_lowlevel(obj_id, dims.size(), dims.data(), name, diff --git a/include/openmc/ifp.h b/include/openmc/ifp.h index 01904d13c..2b751d66f 100644 --- a/include/openmc/ifp.h +++ b/include/openmc/ifp.h @@ -6,6 +6,8 @@ #include "openmc/particle_data.h" #include "openmc/settings.h" +#include // for copy + namespace openmc { //! Check the value of the IFP parameter for beta effective or both. @@ -113,14 +115,25 @@ void broadcast_ifp_n_generation(int& n_generation, //! \param[in] n_generation Number of generations //! \param[in] neighbor Index of the neighboring processor //! \param[in] requests MPI requests -//! \param[in] delayed_groups List of delayed group numbers lists -//! \param[out] send_delayed_groups Delayed group numbers buffer -//! \param[in] lifetimes List of lifetimes lists -//! \param[out] send_lifetimes Lifetimes buffer +//! \param[in] data List of data lists +//! \param[out] send_data data buffer +template void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, const vector>& delayed_groups, - vector& send_delayed_groups, const vector>& lifetimes, - vector& send_lifetimes); + vector& requests, const vector>& data, + vector& send_data) +{ + // Copy data in buffer + for (int i = idx; i < idx + n; i++) { + std::copy( + data[i].begin(), data[i].end(), send_data.begin() + i * n_generation); + } + + // Send data + requests.emplace_back(); + MPI_Datatype datatype = mpi::MPITypeMap::mpi_type; + MPI_Isend(&send_data[n_generation * idx], n_generation * static_cast(n), + datatype, neighbor, mpi::rank, mpi::intracomm, &requests.back()); +} //! Receive IFP data using MPI. //! @@ -129,12 +142,22 @@ void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, //! \param[in] n_generation Number of generations //! \param[in] neighbor Index of the neighboring processor //! \param[in] requests MPI requests -//! \param[in] delayed_groups List of delayed group numbers -//! \param[in] lifetimes List of lifetimes +//! \param[in] data data buffer //! \param[out] deserialization Information to deserialize the received data +template void receive_ifp_data(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, vector& delayed_groups, - vector& lifetimes, vector& deserialization); + vector& requests, vector& data, + vector& deserialization) +{ + requests.emplace_back(); + MPI_Datatype datatype = mpi::MPITypeMap::mpi_type; + MPI_Irecv(&data[n_generation * idx], n_generation * static_cast(n), + datatype, neighbor, neighbor, mpi::intracomm, &requests.back()); + + // Deserialization info to reconstruct data later + DeserializationInfo info = {idx, n}; + deserialization.push_back(info); +} //! Copy partial IFP data from local lists to source banks. //! @@ -151,12 +174,24 @@ void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, //! the IFP source banks. //! //! \param[in] n_generation Number of generations +//! \param[in] data data to deserialize +//! \param[in] bank bank to store data //! \param[out] deserialization Information to deserialize the received data -//! \param[in] delayed_groups List of delayed group numbers -//! \param[in] lifetimes List of lifetimes -void deserialize_ifp_info(int n_generation, - const vector& deserialization, - const vector& delayed_groups, const vector& lifetimes); +template +void deserialize_ifp_info(int n_generation, const vector& data, + vector>& bank, const vector& deserialization) +{ + for (auto info : deserialization) { + int64_t index_local = info.index_local; + int64_t n = info.n; + + for (int i = index_local; i < index_local + n; i++) { + vector data_received( + data.begin() + n_generation * i, data.begin() + n_generation * (i + 1)); + bank[i] = data_received; + } + } +} #endif diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index f87d28b21..ca40bbc2a 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -113,6 +113,14 @@ public: virtual Position get_local_position( Position r, const array& i_xyz) const = 0; + //! \brief get the normal of the lattice surface crossing + //! \param[in] i_xyz The indices for the lattice translation. + //! \param[out] is_valid is the lattice translation correspond to a valid + //! surface. \return The surface normal corresponding to the lattice + //! translation. + virtual Direction get_normal( + const array& i_xyz, bool& is_valid) const = 0; + //! \brief Check flattened lattice index. //! \param indx The index for a lattice tile. //! \return true if the given index fit within the lattice bounds. False @@ -223,6 +231,9 @@ public: Position get_local_position( Position r, const array& i_xyz) const override; + Direction get_normal( + const array& i_xyz, bool& is_valid) const override; + int32_t& offset(int map, const array& i_xyz) override; int32_t offset(int map, int indx) const override; @@ -268,6 +279,9 @@ public: Position get_local_position( Position r, const array& i_xyz) const override; + Direction get_normal( + const array& i_xyz, bool& is_valid) const override; + bool is_valid_index(int indx) const override; int32_t& offset(int map, const array& i_xyz) override; diff --git a/include/openmc/material.h b/include/openmc/material.h index e36946c71..3967c3687 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -5,8 +5,8 @@ #include #include "openmc/span.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #include #include "openmc/bremsstrahlung.h" @@ -14,6 +14,7 @@ #include "openmc/memory.h" // for unique_ptr #include "openmc/ncrystal_interface.h" #include "openmc/particle.h" +#include "openmc/settings.h" #include "openmc/vector.h" namespace openmc { @@ -110,9 +111,12 @@ public: //! \return Density in [atom/b-cm] double density() const { return density_; } - //! Get density in [g/cm^3] + //! Get density in [g/cm^3]. //! \return Density in [g/cm^3] - double density_gpcc() const { return density_gpcc_; } + double density_gpcc() const + { + return settings::run_CE ? density_gpcc_ : density(); + } //! Get charge density in [e/b-cm] //! \return Charge density in [e/b-cm] @@ -185,7 +189,7 @@ public: vector nuclide_; //!< Indices in nuclides vector vector element_; //!< Indices in elements vector NCrystalMat ncrystal_mat_; //!< NCrystal material object - xt::xtensor atom_density_; //!< Nuclide atom density in [atom/b-cm] + tensor::Tensor atom_density_; //!< Nuclide atom density in [atom/b-cm] double density_; //!< Total atom density in [atom/b-cm] double density_gpcc_; //!< Total atom density in [g/cm^3] double charge_density_; //!< Total charge density in [e/b-cm] diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index c30ef7558..d3bccca7b 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -9,6 +9,7 @@ #include #include "openmc/position.h" +#include "openmc/search.h" namespace openmc { @@ -200,5 +201,63 @@ std::complex faddeeva(std::complex z); //! \return Derivative of Faddeeva function evaluated at z std::complex w_derivative(std::complex z, int order); +//! Evaluate relative exponential function +//! +//! \param x Real argument +//! \return (exp(x)-1)/x without loss of precision near 0 +double exprel(double x); + +//! Evaluate relative logarithm function +//! +//! \param x Real argument +//! \return log(1+x)/x without loss of precision near 0 +double log1prel(double x); + +//! Evaluate the cylindrical Bessel function of the first kind J_n(x) +//! +//! Uses std::cyl_bessel_j where available (e.g., libstdc++). On standard +//! library implementations lacking the C++17 special math functions (e.g., +//! libc++ on Apple Clang/LLVM), falls back to the ascending power series, +//! which converges to machine precision for the small arguments (|x| <= 2) +//! used in OpenMC. Unlike std::cyl_bessel_j, negative arguments are handled +//! via the parity relation J_n(-x) = (-1)^n J_n(x). +//! +//! \param n Non-negative integer order of the Bessel function +//! \param x Real argument +//! \return J_n(x) +double cyl_bessel_j(int n, double x); + +//! Helper function to get index and interpolation function on an incident +//! energy grid +//! +//! \param energies energy grid +//! \param E incident energy +//! \param i grid index +//! \param f interpolation factor +void get_energy_index( + const vector& energies, double E, int& i, double& f); + +//============================================================================== +//! Calculate the cumulative distribution function of the standard normal +//! distribution at a given value. +//! +//! \param z The value at which to evaluate the CDF +//! \return Phi(z) = P(X <= z) for X ~ N(0,1) +//============================================================================== + +double standard_normal_cdf(double z); + +//============================================================================== +//! Return true if two floating-point values are approximately equal within a +//! combined relative and absolute tolerance. +//! +//! \param a first floating point value +//! \param b second floating point value +//! \param rel_tol relative tolerance +//! \param abs_tol absolute tolerance +//! \return true if a and b are approximately equal, false otherwise +//============================================================================== +bool isclose(double a, double b, double rel_tol, double abs_tol); + } // namespace openmc #endif // OPENMC_MATH_FUNCTIONS_H diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5c9272e93..0d8189caa 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -4,11 +4,12 @@ #ifndef OPENMC_MESH_H #define OPENMC_MESH_H +#include #include #include "hdf5.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #include "openmc/bounding_box.h" #include "openmc/error.h" @@ -87,8 +88,12 @@ namespace detail { class MaterialVolumes { public: + MaterialVolumes(int32_t* mats, double* vols, double* bboxes, int table_size) + : materials_(mats), volumes_(vols), bboxes_(bboxes), table_size_(table_size) + {} + MaterialVolumes(int32_t* mats, double* vols, int table_size) - : materials_(mats), volumes_(vols), table_size_(table_size) + : MaterialVolumes(mats, vols, nullptr, table_size) {} //! Add volume for a given material in a mesh element @@ -96,8 +101,11 @@ public: //! \param[in] index_elem Index of the mesh element //! \param[in] index_material Index of the material within the model //! \param[in] volume Volume to add - void add_volume(int index_elem, int index_material, double volume); - void add_volume_unsafe(int index_elem, int index_material, double volume); + //! \param[in] bbox Bounding box to union into the result (optional) + void add_volume(int index_elem, int index_material, double volume, + const BoundingBox* bbox = nullptr); + void add_volume_unsafe(int index_elem, int index_material, double volume, + const BoundingBox* bbox = nullptr); // Accessors int32_t& materials(int i, int j) { return materials_[i * table_size_ + j]; } @@ -112,11 +120,23 @@ public: return volumes_[i * table_size_ + j]; } + double& bboxes(int i, int j, int k) + { + return bboxes_[(i * table_size_ + j) * 6 + k]; + } + const double& bboxes(int i, int j, int k) const + { + return bboxes_[(i * table_size_ + j) * 6 + k]; + } + + bool has_bboxes() const { return bboxes_ != nullptr; } + bool table_full() const { return table_full_; } private: int32_t* materials_; //!< material index (bins, table_size) double* volumes_; //!< volume in [cm^3] (bins, table_size) + double* bboxes_; //!< bounding boxes (bins, table_size, 6) int table_size_; //!< Size of hash table for each mesh element bool table_full_ {false}; //!< Whether the hash table is full }; @@ -239,22 +259,33 @@ public: void material_volumes(int nx, int ny, int nz, int max_materials, int32_t* materials, double* volumes) const; + //! Determine volume and bounding boxes of materials within each mesh element + // + //! \param[in] nx Number of samples in x direction + //! \param[in] ny Number of samples in y direction + //! \param[in] nz Number of samples in z direction + //! \param[in] max_materials Maximum number of materials in a single mesh + //! element + //! \param[inout] materials Array storing material indices + //! \param[inout] volumes Array storing volumes + //! \param[inout] bboxes Array storing bounding boxes (n_elems, table_size, 6) + void material_volumes(int nx, int ny, int nz, int max_materials, + int32_t* materials, double* volumes, double* bboxes) const; + //! Determine bounding box of mesh // //! \return Bounding box of mesh BoundingBox bounding_box() const { - auto ll = this->lower_left(); - auto ur = this->upper_right(); - return {ll.x, ur.x, ll.y, ur.y, ll.z, ur.z}; + return {this->lower_left(), this->upper_right()}; } virtual Position lower_left() const = 0; virtual Position upper_right() const = 0; // Data members - xt::xtensor lower_left_; //!< Lower-left coordinates of mesh - xt::xtensor upper_right_; //!< Upper-right coordinates of mesh + tensor::Tensor lower_left_; //!< Lower-left coordinates of mesh + tensor::Tensor upper_right_; //!< Upper-right coordinates of mesh int id_ {-1}; //!< Mesh ID std::string name_; //!< User-specified name int n_dimension_ {-1}; //!< Number of dimensions @@ -317,7 +348,7 @@ public: //! \param[in] Pointer to bank sites //! \param[in] Number of bank sites //! \param[out] Whether any bank sites are outside the mesh - xt::xtensor count_sites( + tensor::Tensor count_sites( const SourceSite* bank, int64_t length, bool* outside) const; //! Get bin given mesh indices @@ -388,8 +419,8 @@ public: //! Get a label for the mesh bin std::string bin_label(int bin) const override; - //! Get shape as xt::xtensor - xt::xtensor get_x_shape() const; + //! Get mesh dimensions as a tensor + tensor::Tensor get_shape_tensor() const; double volume(int bin) const override { @@ -484,7 +515,7 @@ public: //! \param[in] bank Array of bank sites //! \param[out] Whether any bank sites are outside the mesh //! \return Array indicating number of sites in each mesh/energy bin - xt::xtensor count_sites( + tensor::Tensor count_sites( const SourceSite* bank, int64_t length, bool* outside) const; //! Return the volume for a given mesh index @@ -495,7 +526,7 @@ public: // Data members double volume_frac_; //!< Volume fraction of each mesh element double element_volume_; //!< Volume of each mesh element - xt::xtensor width_; //!< Width of each mesh element + tensor::Tensor width_; //!< Width of each mesh element }; class RectilinearMesh : public StructuredMesh { @@ -971,7 +1002,7 @@ public: Position sample_element(int32_t bin, uint64_t* seed) const override; - int get_bin(Position r) const override; + virtual int get_bin(Position r) const override; int n_bins() const override; @@ -1009,16 +1040,21 @@ public: protected: // Methods - //! Translate a bin value to an element reference virtual const libMesh::Elem& get_element_from_bin(int bin) const; //! Translate an element pointer to a bin index virtual int get_bin_from_element(const libMesh::Elem* elem) const; + // Data members libMesh::MeshBase* m_; //!< pointer to libMesh MeshBase instance, always set //!< during intialization + vector> + pl_; //!< per-thread point locators + libMesh::BoundingBox bbox_; //!< bounding box of the mesh + private: + // Methods void initialize() override; void set_mesh_pointer_from_filename(const std::string& filename); void build_eqn_sys(); @@ -1027,8 +1063,6 @@ private: unique_ptr unique_m_ = nullptr; //!< pointer to the libMesh MeshBase instance, only used if mesh is //!< created inside OpenMC - vector> - pl_; //!< per-thread point locators unique_ptr equation_systems_; //!< pointer to the libMesh EquationSystems //!< instance @@ -1037,7 +1071,6 @@ private: std::unordered_map variable_map_; //!< mapping of variable names (tally scores) to libMesh //!< variable numbers - libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh }; @@ -1045,8 +1078,9 @@ private: class AdaptiveLibMesh : public LibMesh { public: // Constructor - AdaptiveLibMesh( - libMesh::MeshBase& input_mesh, double length_multiplier = 1.0); + AdaptiveLibMesh(libMesh::MeshBase& input_mesh, double length_multiplier = 1.0, + const std::set& block_ids = + std::set()); // Overridden methods int n_bins() const override; @@ -1058,6 +1092,8 @@ public: void write(const std::string& filename) const override; + int get_bin(Position r) const override; + protected: // Overridden methods int get_bin_from_element(const libMesh::Elem* elem) const override; @@ -1066,6 +1102,9 @@ protected: private: // Data members + const std::set + block_ids_; //!< subdomains of the mesh to tally on + const bool block_restrict_; //!< whether a subset of the mesh is being used const libMesh::dof_id_type num_active_; //!< cached number of active elements std::vector diff --git a/include/openmc/message_passing.h b/include/openmc/message_passing.h index ce993776b..03e236338 100644 --- a/include/openmc/message_passing.h +++ b/include/openmc/message_passing.h @@ -22,6 +22,19 @@ extern MPI_Datatype collision_track_site; extern MPI_Comm intracomm; #endif +//============================================================================== +// Template struct used to map types to MPI datatypes +// By having a single static data member, the template can +// be specialized for each type we know of. The specializations appear in the +// .cpp file since they are definitions. +//============================================================================== +#ifdef OPENMC_MPI +template +struct MPITypeMap { + static const MPI_Datatype mpi_type; +}; +#endif + // Calculates global indices of the bank particles // across all ranks using a parallel scan. This is used to write // the surface source file in parallel runs. It will probably diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index 9b1602f29..5d8ff58c2 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -6,7 +6,7 @@ #include -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" @@ -22,7 +22,7 @@ namespace openmc { class Mgxs { private: - xt::xtensor kTs; // temperature in eV (k * T) + tensor::Tensor kTs; // temperature in eV (k * T) AngleDistributionType scatter_format; // flag for if this is legendre, histogram, or tabular int num_groups; // number of energy groups @@ -113,6 +113,11 @@ public: const vector& micros, const vector& atom_densities, int num_group, int num_delay); + //! \brief Get the number of temperature data points. + //! + //! @return The number of temperature data points for this MGXS + inline int n_temperature_points() { return kTs.size(); } + //! \brief Provides a cross section value given certain parameters //! //! @param xstype Type of cross section requested, according to the diff --git a/include/openmc/mgxs_interface.h b/include/openmc/mgxs_interface.h index da074f825..117ac503d 100644 --- a/include/openmc/mgxs_interface.h +++ b/include/openmc/mgxs_interface.h @@ -61,6 +61,8 @@ public: vector energy_bin_avg_; vector rev_energy_bins_; vector> nuc_temps_; // all available temperatures + vector + default_inverse_velocity_; // approximate default inverse-velocity data }; namespace data { diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 60b88a153..ae39a53dd 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -84,6 +84,9 @@ public: double collapse_rate(int MT, double temperature, span energy, span flux) const; + //! Return a ParticleType object representing this nuclide + ParticleType particle_type() const { return {Z_, A_, metastable_}; } + //============================================================================ // Data members std::string name_; //!< Name of nuclide, e.g. "U235" @@ -96,7 +99,7 @@ public: // Temperature dependent cross section data vector kTs_; //!< temperatures in eV (k*T) vector grid_; //!< Energy grid at each temperature - vector> xs_; //!< Cross sections at each temperature + vector> xs_; //!< Cross sections at each temperature // Multipole data unique_ptr multipole_; @@ -163,7 +166,7 @@ bool multipole_in_range(const Nuclide& nuc, double E); namespace data { // Minimum/maximum transport energy for each particle type. Order corresponds to -// that of the ParticleType enum +// transport_index() for supported transport particles. extern array energy_min; extern array energy_max; diff --git a/include/openmc/output.h b/include/openmc/output.h index 940ea78ce..0ad8b2fe5 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -10,6 +10,8 @@ namespace openmc { +extern "C" const bool STRICT_FP_ENABLED; + //! \brief Display the main title banner as well as information about the //! program developers, version, and date/time which the problem was run. void title(); @@ -60,7 +62,6 @@ void write_tallies(); void show_time(const char* label, double secs, int indent_level = 0); } // namespace openmc -#endif // OPENMC_OUTPUT_H ////////////////////////////////////// // Custom formatters @@ -87,3 +88,5 @@ struct formatter> { }; // namespace fmt } // namespace fmt + +#endif // OPENMC_OUTPUT_H diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 0f37719b9..8db9721ba 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -39,6 +39,8 @@ public: double speed() const; + double mass() const; + //! create a secondary particle // //! stores the current phase space attributes of the particle in the @@ -69,7 +71,8 @@ public: void event_advance(); void event_cross_surface(); void event_collide(); - void event_revive_from_secondary(); + void event_revive_from_secondary(const SourceSite& site); + void event_check_limit_and_revive(); void event_death(); //! pulse-height recording @@ -126,10 +129,6 @@ public: //! Functions //============================================================================ -std::string particle_type_to_str(ParticleType type); - -ParticleType str_to_particle_type(std::string str); - void add_surf_source_to_bank(Particle& p, const Surface& surf); } // namespace openmc diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index fdacfa765..44e82fd23 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -3,6 +3,7 @@ #include "openmc/array.h" #include "openmc/constants.h" +#include "openmc/particle_type.h" #include "openmc/position.h" #include "openmc/random_lcg.h" #include "openmc/tallies/filter_match.h" @@ -30,9 +31,6 @@ constexpr double CACHE_INVALID {-1.0}; //========================================================================== // Aliases and type definitions -//! Particle types -enum class ParticleType { neutron, photon, electron, positron }; - //! Saved ("banked") state of a particle //! NOTE: This structure's MPI type is built in initialize_mpi() of //! initialize.cpp. Any changes made to the struct here must also be @@ -52,8 +50,11 @@ struct SourceSite { // Extra attributes that don't show up in source written to file int parent_nuclide {-1}; - int64_t parent_id; - int64_t progeny_id; + int64_t parent_id {0}; + int64_t progeny_id {0}; + double wgt_born {1.0}; + double wgt_ww_born {-1.0}; + int64_t n_split {0}; }; struct CollisionTrackSite { @@ -496,7 +497,7 @@ private: MacroXS macro_xs_; CacheDataMG mg_xs_cache_; - ParticleType type_ {ParticleType::neutron}; + ParticleType type_; double E_; double E_last_; @@ -535,9 +536,14 @@ private: uint64_t seeds_[N_STREAMS]; int stream_; - vector secondary_bank_; + vector local_secondary_bank_; - int64_t current_work_; + // Keep track of how many secondary particles were created in the collision + // and what the starting index is in the secondary bank for this particle + int n_secondaries_ {0}; + int secondary_bank_index_ {0}; + + int64_t current_work_ {0}; vector flux_derivs_; @@ -560,7 +566,9 @@ private: int n_event_ {0}; - int n_split_ {0}; + int64_t n_tracks_ {0}; //!< number of tracks in this particle history + + int64_t n_split_ {0}; double ww_factor_ {0.0}; int64_t n_progeny_ {0}; @@ -574,10 +582,8 @@ public: // Methods and accessors // Cross section caches - NuclideMicroXS& neutron_xs(int i) - { - return neutron_xs_[i]; - } // Microscopic neutron cross sections + // Microscopic neutron cross sections + NuclideMicroXS& neutron_xs(int i) { return neutron_xs_[i]; } const NuclideMicroXS& neutron_xs(int i) const { return neutron_xs_[i]; } // Microscopic photon cross sections @@ -690,8 +696,23 @@ public: int& stream() { return stream_; } // secondary particle bank - SourceSite& secondary_bank(int i) { return secondary_bank_[i]; } - decltype(secondary_bank_)& secondary_bank() { return secondary_bank_; } + SourceSite& local_secondary_bank(int i) { return local_secondary_bank_[i]; } + const SourceSite& local_secondary_bank(int i) const + { + return local_secondary_bank_[i]; + } + decltype(local_secondary_bank_)& local_secondary_bank() + { + return local_secondary_bank_; + } + + // Number of secondaries created in a collision + int& n_secondaries() { return n_secondaries_; } + const int& n_secondaries() const { return n_secondaries_; } + + // Starting index in secondary bank for this collision + int& secondary_bank_index() { return secondary_bank_index_; } + const int& secondary_bank_index() const { return secondary_bank_index_; } // Current simulation work index int64_t& current_work() { return current_work_; } @@ -731,13 +752,16 @@ public: int& n_event() { return n_event_; } // Number of times variance reduction has caused a particle split - int n_split() const { return n_split_; } - int& n_split() { return n_split_; } + int64_t n_split() const { return n_split_; } + int64_t& n_split() { return n_split_; } // Particle-specific factor for on-the-fly weight window adjustment double ww_factor() const { return ww_factor_; } double& ww_factor() { return ww_factor_; } + // Number of tracks in this particle history + int64_t& n_tracks() { return n_tracks_; } + // Number of progeny produced by this particle int64_t& n_progeny() { return n_progeny_; } diff --git a/include/openmc/particle_type.h b/include/openmc/particle_type.h new file mode 100644 index 000000000..0fd896c57 --- /dev/null +++ b/include/openmc/particle_type.h @@ -0,0 +1,184 @@ +//============================================================================== +// ParticleType class definition +//============================================================================== + +#ifndef OPENMC_PARTICLE_TYPE_H +#define OPENMC_PARTICLE_TYPE_H + +#include +#include +#include +#include +#include + +#include "openmc/constants.h" +#include "openmc/error.h" + +namespace openmc { + +//------------------------------------------------------------------------------ +// PDG constants (canonical particle identity as simple integers) +//------------------------------------------------------------------------------ + +inline constexpr int32_t PDG_NEUTRON = 2112; +inline constexpr int32_t PDG_PHOTON = 22; +inline constexpr int32_t PDG_ELECTRON = 11; +inline constexpr int32_t PDG_POSITRON = -11; +inline constexpr int32_t PDG_PROTON = 2212; +inline constexpr int32_t PDG_DEUTERON = 1000010020; +inline constexpr int32_t PDG_TRITON = 1000010030; +inline constexpr int32_t PDG_ALPHA = 1000020040; + +//------------------------------------------------------------------------------ +// ParticleType class (standard-layout, trivially copyable) +//------------------------------------------------------------------------------ + +class ParticleType { +public: + //---------------------------------------------------------------------------- + // Constructors + + // Default constructor: defaults to neutron + constexpr ParticleType() : pdg_number_(PDG_NEUTRON) {} + + // Constructor from PDG number + constexpr explicit ParticleType(int32_t pdg_number) : pdg_number_(pdg_number) + {} + + // Constructor from particle name string (e.g., "neutron", "photon", "Fe56") + explicit ParticleType(std::string_view str); + + // Constructor from Z, A, and metastable state for nuclear particles + constexpr ParticleType(int Z, int A, int m = 0) + : pdg_number_(1000000000 + Z * 10000 + A * 10 + m) + {} + + //---------------------------------------------------------------------------- + // Accessors + + // Accessor for the underlying PDG number + constexpr int32_t pdg_number() const { return pdg_number_; } + + //---------------------------------------------------------------------------- + // Methods + + // Get particle mass in [u] + double mass() const + { + int32_t p = std::abs(pdg_number_); + if (ATOMIC_MASS.count(p)) { + return ATOMIC_MASS[p]; + } else { + fatal_error("Unknown mass for particle " + str()); + } + } + + // Convert to string representation + std::string str() const; + + // Check if this represents a nucleus (vs elementary particle) + constexpr bool is_nucleus() const + { + // PDG nuclear codes are >= 1000000000 (100ZZZAAAI format) + return pdg_number_ >= 1000000000; + } + + // Get transport index (0-3 for transportable particles, C_NONE otherwise) + constexpr int transport_index() const; + + // Check if this is a neutron + constexpr bool is_neutron() const { return pdg_number_ == PDG_NEUTRON; } + + // Check if this is a photon + constexpr bool is_photon() const { return pdg_number_ == PDG_PHOTON; } + + constexpr bool is_transportable() const + { + return this->transport_index() != C_NONE; + } + + //---------------------------------------------------------------------------- + // Static factory methods + + static constexpr ParticleType neutron() { return ParticleType {PDG_NEUTRON}; } + static constexpr ParticleType photon() { return ParticleType {PDG_PHOTON}; } + static constexpr ParticleType electron() + { + return ParticleType {PDG_ELECTRON}; + } + static constexpr ParticleType positron() + { + return ParticleType {PDG_POSITRON}; + } + static constexpr ParticleType proton() { return ParticleType {PDG_PROTON}; } + static constexpr ParticleType deuteron() + { + return ParticleType {PDG_DEUTERON}; + } + static constexpr ParticleType triton() { return ParticleType {PDG_TRITON}; } + static constexpr ParticleType alpha() { return ParticleType {PDG_ALPHA}; } + +private: + int32_t pdg_number_; +}; + +//------------------------------------------------------------------------------ +// Static assertions to ensure standard-layout and trivially copyable +//------------------------------------------------------------------------------ + +static_assert(std::is_standard_layout_v, + "ParticleType must be standard-layout"); +static_assert(std::is_trivially_copyable_v, + "ParticleType must be trivially copyable"); +static_assert(sizeof(ParticleType) == sizeof(int32_t), + "ParticleType must be same size as int32_t"); + +//------------------------------------------------------------------------------ +// Comparison operators (free functions for symmetry) +//------------------------------------------------------------------------------ + +constexpr bool operator==(ParticleType lhs, ParticleType rhs) +{ + return lhs.pdg_number() == rhs.pdg_number(); +} + +constexpr bool operator!=(ParticleType lhs, ParticleType rhs) +{ + return lhs.pdg_number() != rhs.pdg_number(); +} + +constexpr bool operator<(ParticleType lhs, ParticleType rhs) +{ + return lhs.pdg_number() < rhs.pdg_number(); +} + +//------------------------------------------------------------------------------ +// ParticleType member function implementations (inline) +//------------------------------------------------------------------------------ + +constexpr int ParticleType::transport_index() const +{ + switch (pdg_number_) { + case PDG_NEUTRON: + return 0; + case PDG_PHOTON: + return 1; + case PDG_ELECTRON: + return 2; + case PDG_POSITRON: + return 3; + default: + return C_NONE; + } +} + +//------------------------------------------------------------------------------ +// Legacy conversion helpers +//------------------------------------------------------------------------------ + +// Legacy enum code (0..3) to ParticleType conversion +ParticleType legacy_particle_index_to_type(int code); + +} // namespace openmc + +#endif // OPENMC_PARTICLE_TYPE_H diff --git a/include/openmc/photon.h b/include/openmc/photon.h index f6f28a4df..93c6dba53 100644 --- a/include/openmc/photon.h +++ b/include/openmc/photon.h @@ -6,7 +6,7 @@ #include "openmc/particle.h" #include "openmc/vector.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include @@ -62,14 +62,14 @@ public: int64_t index_; //!< Index in global elements vector // Microscopic cross sections - xt::xtensor energy_; - xt::xtensor coherent_; - xt::xtensor incoherent_; - xt::xtensor photoelectric_total_; - xt::xtensor pair_production_total_; - xt::xtensor pair_production_electron_; - xt::xtensor pair_production_nuclear_; - xt::xtensor heating_; + tensor::Tensor energy_; + tensor::Tensor coherent_; + tensor::Tensor incoherent_; + tensor::Tensor photoelectric_total_; + tensor::Tensor pair_production_total_; + tensor::Tensor pair_production_electron_; + tensor::Tensor pair_production_nuclear_; + tensor::Tensor heating_; // Form factors Tabulated1D incoherent_form_factor_; @@ -81,27 +81,27 @@ public: // stored separately to improve memory access pattern when calculating the // total cross section vector shells_; - xt::xtensor cross_sections_; + tensor::Tensor cross_sections_; // Compton profile data - xt::xtensor profile_pdf_; - xt::xtensor profile_cdf_; - xt::xtensor binding_energy_; - xt::xtensor electron_pdf_; + tensor::Tensor profile_pdf_; + tensor::Tensor profile_cdf_; + tensor::Tensor binding_energy_; + tensor::Tensor electron_pdf_; // Map subshells from Compton profile data obtained from Biggs et al, // "Hartree-Fock Compton profiles for the elements" to ENDF/B atomic // relaxation data - xt::xtensor subshell_map_; + tensor::Tensor subshell_map_; // Stopping power data double I_; // mean excitation energy - xt::xtensor n_electrons_; - xt::xtensor ionization_energy_; - xt::xtensor stopping_power_radiative_; + tensor::Tensor n_electrons_; + tensor::Tensor ionization_energy_; + tensor::Tensor stopping_power_radiative_; // Bremsstrahlung scaled DCS - xt::xtensor dcs_; + tensor::Tensor dcs_; // Whether atomic relaxation data is present bool has_atomic_relaxation_ {false}; @@ -137,7 +137,7 @@ void free_memory_photon(); namespace data { -extern xt::xtensor +extern tensor::Tensor compton_profile_pz; //! Compton profile momentum grid //! Photon interaction data for each element diff --git a/include/openmc/physics_common.h b/include/openmc/physics_common.h index e38a3c7f8..b0e395c1e 100644 --- a/include/openmc/physics_common.h +++ b/include/openmc/physics_common.h @@ -13,5 +13,9 @@ namespace openmc { //! \param[in] weight_survive Weight assigned to particles that survive void russian_roulette(Particle& p, double weight_survive); +//! \brief Performs the global russian roulette operation on a particle +//! \param[in,out] p Particle object +void apply_russian_roulette(Particle& p); + } // namespace openmc #endif // OPENMC_PHYSICS_COMMON_H diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 7e27679ea..ba8ac84a1 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -5,9 +5,10 @@ #include #include #include +#include +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xarray.hpp" #include "hdf5.h" #include "openmc/cell.h" @@ -17,6 +18,9 @@ #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/random_lcg.h" +#include "openmc/ray.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/filter_match.h" #include "openmc/xml_interface.h" namespace openmc { @@ -83,19 +87,27 @@ const RGBColor BLACK {0, 0, 0}; * \class PlottableInterface * \brief Interface for plottable objects. * - * PlottableInterface classes must have a unique ID in the plots.xml file. - * They guarantee the ability to create output in some form. This interface - * is designed to be implemented by classes that produce plot-relevant data - * which can be visualized. + * PlottableInterface classes must have unique IDs. If no ID (or -1) is + * provided, the next available ID is assigned automatically. They guarantee + * the ability to create output in some form. This interface is designed to be + * implemented by classes that produce plot-relevant data which can be + * visualized. */ + +typedef tensor::Tensor ImageData; class PlottableInterface { +public: + PlottableInterface() = default; + + void set_default_colors(); + private: void set_id(pugi::xml_node plot_node); - int id_; // unique plot ID + int id_ {C_NONE}; // unique plot ID void set_bg_color(pugi::xml_node plot_node); void set_universe(pugi::xml_node plot_node); - void set_default_colors(pugi::xml_node plot_node); + void set_color_by(pugi::xml_node plot_node); void set_user_colors(pugi::xml_node plot_node); void set_overlap_color(pugi::xml_node plot_node); void set_mask(pugi::xml_node plot_node); @@ -107,52 +119,77 @@ protected: public: enum class PlotColorBy { cells = 0, mats = 1 }; + // Generates image data based on plot parameters and returns it + virtual ImageData create_image() const = 0; + // Creates the output image named path_plot_ virtual void create_output() const = 0; + // Write populated image data to file + void write_image(const ImageData& data) const; + // Print useful info to the terminal virtual void print_info() const = 0; const std::string& path_plot() const { return path_plot_; } std::string& path_plot() { return path_plot_; } int id() const { return id_; } + void set_id(int id = C_NONE); int level() const { return level_; } + PlotColorBy color_by() const { return color_by_; } // Public color-related data PlottableInterface(pugi::xml_node plot_node); virtual ~PlottableInterface() = default; - int level_; // Universe level to plot - bool color_overlaps_; // Show overlapping cells? - PlotColorBy color_by_; // Plot coloring (cell/material) - RGBColor not_found_ {WHITE}; // Plot background color - RGBColor overlap_color_ {RED}; // Plot overlap color - vector colors_; // Plot colors + int level_ {-1}; // Universe level to plot + bool color_overlaps_ {false}; // Show overlapping cells? + PlotColorBy color_by_ {PlotColorBy::mats}; // Plot coloring (cell/material) + RGBColor not_found_ {WHITE}; // Plot background color + RGBColor overlap_color_ {RED}; // Plot overlap color + vector colors_; // Plot colors }; -typedef xt::xtensor ImageData; - struct IdData { // Constructor - IdData(size_t h_res, size_t v_res); + IdData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); - void set_overlap(size_t y, size_t x); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members - xt::xtensor data_; //!< 2D array of cell & material ids + tensor::Tensor data_; //!< 2D array of cell & material ids }; struct PropertyData { // Constructor - PropertyData(size_t h_res, size_t v_res); + PropertyData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); - void set_overlap(size_t y, size_t x); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members - xt::xtensor data_; //!< 2D array of temperature & density data + tensor::Tensor data_; //!< 2D array of temperature & density data +}; + +struct RasterData { + // Constructor + RasterData(size_t h_res, size_t v_res, bool include_filter = false); + + // Methods + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x, int overlap_idx); + + // Members + tensor::Tensor + id_data_; //!< [v_res, h_res, 3 or 4]: cell, instance, mat, [filter_bin] + tensor::Tensor + property_data_; //!< [v_res, h_res, 2]: temperature, density + bool include_filter_; //!< Whether filter bin index is included }; //=============================================================================== @@ -162,76 +199,76 @@ struct PropertyData { class SlicePlotBase { public: template - T get_map() const; + T get_map(int32_t filter_index = -1) const; enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; + // Accessors + + const std::array& pixels() const { return pixels_; } + std::array& pixels() { return pixels_; } + // Members public: - Position origin_; //!< Plot origin in geometry - Position width_; //!< Plot width in geometry - PlotBasis basis_; //!< Plot basis (XY/XZ/YZ) - array pixels_; //!< Plot size in pixels - bool slice_color_overlaps_; //!< Show overlapping cells? - int slice_level_ {-1}; //!< Plot universe level + Position origin_; //!< Plot origin in geometry + Direction u_span_; //!< Full-width span vector in geometry + Direction v_span_; //!< Full-height span vector in geometry + array pixels_; //!< Plot size in pixels + bool show_overlaps_; //!< Show overlapping cells? + int slice_level_ {-1}; //!< Plot universe level private: }; template -T SlicePlotBase::get_map() const +T SlicePlotBase::get_map(int32_t filter_index) const { size_t width = pixels_[0]; size_t height = pixels_[1]; - // get pixel size - double in_pixel = (width_[0]) / static_cast(width); - double out_pixel = (width_[1]) / static_cast(height); - - // size data array - T data(width, height); - - // setup basis indices and initial position centered on pixel - int in_i, out_i; - Position xyz = origin_; - switch (basis_) { - case PlotBasis::xy: - in_i = 0; - out_i = 1; - break; - case PlotBasis::xz: - in_i = 0; - out_i = 2; - break; - case PlotBasis::yz: - in_i = 1; - out_i = 2; - break; - default: - UNREACHABLE(); + // Determine if filter is being used + bool include_filter = (filter_index >= 0); + Filter* filter = nullptr; + if (include_filter) { + filter = model::tally_filters[filter_index].get(); } - // set initial position - xyz[in_i] = origin_[in_i] - width_[0] / 2. + in_pixel / 2.; - xyz[out_i] = origin_[out_i] + width_[1] / 2. - out_pixel / 2.; + // size data array + T data(width, height, include_filter); - // arbitrary direction - Direction dir = {1. / std::sqrt(2.), 1. / std::sqrt(2.), 0.0}; + // compute pixel steps and top-left pixel center + Direction u_step = u_span_ / static_cast(width); + Direction v_step = v_span_ / static_cast(height); + + Position start = + origin_ - 0.5 * u_span_ + 0.5 * v_span_ + 0.5 * u_step - 0.5 * v_step; + + // Validate that span vectors define a valid plane + Position cross = u_span_.cross(v_span_); + if (cross.norm() == 0.0) { + fatal_error("Slice span vectors are invalid (zero area)."); + } + + // Use an arbitrary direction that is not aligned with any coordinate axis. + // The direction has no physical meaning for plotting but is used by + // Surface::sense() to break ties when a pixel is coincident with a surface. + Direction dir = {1.0 / std::sqrt(2.0), 1.0 / std::sqrt(2.0), 0.0}; #pragma omp parallel { - GeometryState p; - p.r() = xyz; + Particle p; + p.r() = start; p.u() = dir; p.coord(0).universe() = model::root_universe; int level = slice_level_; int j {}; + FilterMatch match; #pragma omp for for (int y = 0; y < height; y++) { - p.r()[out_i] = xyz[out_i] - out_pixel * y; + Position row = start - v_step * static_cast(y); for (int x = 0; x < width; x++) { - p.r()[in_i] = xyz[in_i] + in_pixel * x; + p.r() = row + u_step * static_cast(x); p.n_coord() = 1; // local variables bool found_cell = exhaustive_find_cell(p); @@ -240,10 +277,13 @@ T SlicePlotBase::get_map() const j = level; } if (found_cell) { - data.set_value(y, x, p, j); + data.set_value(y, x, p, j, filter, &match); } - if (slice_color_overlaps_ && check_cell_overlap(p, false)) { - data.set_overlap(y, x); + if (show_overlaps_) { + int overlap_idx = check_cell_overlap(p, false); + if (overlap_idx >= 0) { + data.set_overlap(y, x, overlap_idx); + } } } // inner for } @@ -270,13 +310,15 @@ private: public: // Add mesh lines to ImageData void draw_mesh_lines(ImageData& data) const; - void create_image() const; + ImageData create_image() const override; void create_voxel() const; - virtual void create_output() const; - virtual void print_info() const; + void create_output() const override; + void print_info() const override; PlotType type_; //!< Plot type (Slice/Voxel) + Position width_; //!< Axis-aligned width from plot.xml + PlotBasis basis_; //!< Basis from plot.xml for slice plots int meshlines_width_; //!< Width of lines added to the plot int index_meshlines_mesh_ {-1}; //!< Index of the mesh to draw on the plot RGBColor meshlines_color_; //!< Color of meshlines on the plot @@ -294,17 +336,32 @@ public: */ class RayTracePlot : public PlottableInterface { public: + RayTracePlot() = default; RayTracePlot(pugi::xml_node plot); // Standard getters. No setting since it's done from XML. const Position& camera_position() const { return camera_position_; } + Position& camera_position() { return camera_position_; } const Position& look_at() const { return look_at_; } + Position& look_at() { return look_at_; } + const double& horizontal_field_of_view() const { return horizontal_field_of_view_; } + double& horizontal_field_of_view() { return horizontal_field_of_view_; } - virtual void print_info() const; + void print_info() const override; + + const std::array& pixels() const { return pixels_; } + std::array& pixels() { return pixels_; } + + const Direction& up() const { return up_; } + Direction& up() { return up_; } + + //! brief Updates the cached camera-to-model matrix after changes to + //! camera parameters. + void update_view(); protected: Direction camera_x_axis() const @@ -330,8 +387,6 @@ protected: */ std::pair get_pixel_ray(int horiz, int vert) const; - std::array pixels_; // pixel dimension of resulting image - private: void set_look_at(pugi::xml_node node); void set_camera_position(pugi::xml_node node); @@ -341,9 +396,9 @@ private: double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees Position camera_position_; // where camera is - Position look_at_; // point camera is centered looking at - - Direction up_ {0.0, 0.0, 1.0}; // which way is up + Position look_at_; // point camera is centered looking at + std::array pixels_ {100, 100}; // pixel dimension of resulting image + Direction up_ {0.0, 0.0, 1.0}; // which way is up /* The horizontal thickness, if using an orthographic projection. * If set to zero, we assume using a perspective projection. @@ -377,8 +432,9 @@ class WireframeRayTracePlot : public RayTracePlot { public: WireframeRayTracePlot(pugi::xml_node plot); - virtual void create_output() const; - virtual void print_info() const; + ImageData create_image() const override; + void create_output() const override; + void print_info() const override; private: void set_opacities(pugi::xml_node node); @@ -434,10 +490,22 @@ class SolidRayTracePlot : public RayTracePlot { friend class PhongRay; public: + SolidRayTracePlot() = default; + SolidRayTracePlot(pugi::xml_node plot); - virtual void create_output() const; - virtual void print_info() const; + ImageData create_image() const override; + void create_output() const override; + void print_info() const override; + + const std::unordered_set& opaque_ids() const { return opaque_ids_; } + std::unordered_set& opaque_ids() { return opaque_ids_; } + + const Position& light_location() const { return light_location_; } + Position& light_location() { return light_location_; } + + const double& diffuse_fraction() const { return diffuse_fraction_; } + double& diffuse_fraction() { return diffuse_fraction_; } private: void set_opaque_ids(pugi::xml_node node); @@ -452,43 +520,6 @@ private: Position light_location_; }; -// Base class that implements ray tracing logic, not necessarily through -// defined regions of the geometry but also outside of it. -class Ray : public GeometryState { - -public: - Ray(Position r, Direction u) { init_from_r_u(r, u); } - - // Called at every surface intersection within the model - virtual void on_intersection() = 0; - - /* - * Traces the ray through the geometry, calling on_intersection - * at every surface boundary. - */ - void trace(); - - // Stops the ray and exits tracing when called from on_intersection - void stop() { stop_ = true; } - - // Sets the dist_ variable - void compute_distance(); - -protected: - // Records how far the ray has traveled - double traversal_distance_ {0.0}; - -private: - // Max intersections before we assume ray tracing is caught in an infinite - // loop: - static const int MAX_INTERSECTIONS = 1000000; - - bool hit_something_ {false}; - bool stop_ {false}; - - unsigned event_counter_ {0}; -}; - class ProjectionRay : public Ray { public: ProjectionRay(Position r, Direction u, const WireframeRayTracePlot& plot, @@ -496,7 +527,7 @@ public: : Ray(r, u), plot_(plot), line_segments_(line_segments) {} - virtual void on_intersection() override; + void on_intersection() override; private: /* Store a reference to the plot object which is running this ray, in order @@ -519,7 +550,7 @@ public: result_color_ = plot_.not_found_; } - virtual void on_intersection() override; + void on_intersection() override; const RGBColor& result_color() { return result_color_; } diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index d4e802734..09414fd44 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -40,9 +40,10 @@ public: void random_ray_tally(); virtual void accumulate_iteration_flux(); void output_to_vtk() const; - void convert_external_sources(); + void convert_external_sources(bool use_adjoint_sources); void count_external_source_regions(); - void set_adjoint_sources(); + void set_fw_adjoint_sources(); + void set_local_adjoint_sources(); void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; @@ -75,7 +76,11 @@ public: //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; - static bool adjoint_; // If the user wants outputs based on the adjoint flux + // If the user wants outputs based on the adjoint flux + static bool adjoint_requested_; + // The solve currently being executed + static RandomRaySolve solve_; + static bool fw_cadis_local_; static double diagonal_stabilization_rho_; // Adjusts strength of diagonal stabilization // for transport corrected MGXS data @@ -84,6 +89,8 @@ public: static std::unordered_map>> mesh_domain_map_; + static std::vector fw_cadis_local_targets_; + //---------------------------------------------------------------------------- // Static data members static RandomRayVolumeEstimator volume_estimator_; @@ -100,16 +107,18 @@ public: // in model::cells vector source_region_offsets_; - // 2D arrays stored in 1D representing values for all materials x energy - // groups + // 3D arrays stored in 1D representing values for all materials x temperature + // points x energy groups int n_materials_; + int ntemperature_; vector sigma_t_; vector nu_sigma_f_; vector sigma_f_; vector chi_; + vector kappa_fission_; - // 3D arrays stored in 1D representing values for all materials x energy - // groups x energy groups + // 4D arrays stored in 1D representing values for all materials x temperature + // points x energy groups x energy groups vector sigma_s_; // The abstract container holding all source region-specific data @@ -170,13 +179,16 @@ protected: simulation_volume_; // Total physical volume of the simulation domain, as // defined by the 3D box of the random ray source + double + fission_rate_; // The system's fission rate (per cm^3), in eigenvalue mode + // Volumes for each tally and bin/score combination. This intermediate data // structure is used when tallying quantities that must be normalized by // volume (i.e., flux). The vector is index by tally index, while the inner 2D - // xtensor is indexed by bin index and score index in a similar manner to the + // tensor is indexed by bin index and score index in a similar manner to the // results tensor in the Tally class, though without the third dimension, as // SUM and SUM_SQ do not need to be tracked. - vector> tally_volumes_; + vector> tally_volumes_; }; // class FlatSourceDomain diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 40c67ef95..b61d2d67a 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -41,6 +41,7 @@ public: uint64_t transport_history_based_single_ray(); SourceSite sample_prng(); SourceSite sample_halton(); + SourceSite sample_s2(); //---------------------------------------------------------------------------- // Static data members @@ -65,6 +66,7 @@ private: vector mesh_fractional_lengths_; int negroups_; + int ntemperature_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source // data needed for ray transport double distance_travelled_ {0}; diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 3dec48bf2..e186c549f 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -19,9 +19,10 @@ public: //---------------------------------------------------------------------------- // Methods - void compute_segment_correction_factors(); void apply_fixed_sources_and_mesh_domains(); - void prepare_fixed_sources_adjoint(); + void prepare_fw_fixed_sources_adjoint(); + void prepare_local_fixed_sources_adjoint(); + void prepare_adjoint_simulation(bool from_forward); void simulate(); void output_simulation_results() const; void instability_check( @@ -57,9 +58,8 @@ private: //! Non-member functions //============================================================================ -void openmc_run_random_ray(); void validate_random_ray_inputs(); -void openmc_reset_random_ray(); +void openmc_finalize_random_ray(); } // namespace openmc diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 0f5a747ff..1d2bbe1e8 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -51,10 +51,10 @@ inline void hash_combine(size_t& seed, const size_t v) // every iteration. struct TallyTask { int tally_idx; - int filter_idx; + int64_t filter_idx; int score_idx; int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + TallyTask(int tally_idx, int64_t filter_idx, int score_idx, int score_type) : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), score_type(score_type) {} @@ -146,6 +146,8 @@ public: // Scalar fields int* material_; + int* temperature_idx_; + double* density_mult_; int* is_small_; int* n_hits_; int* birthday_; @@ -195,6 +197,12 @@ public: int& material() { return *material_; } const int material() const { return *material_; } + double& density_mult() { return *density_mult_; } + const double density_mult() const { return *density_mult_; } + + int& temperature_idx() { return *temperature_idx_; } + const int temperature_idx() const { return *temperature_idx_; } + int& is_small() { return *is_small_; } const int is_small() const { return *is_small_; } @@ -315,8 +323,11 @@ public: //--------------------------------------- // Scalar fields - int material_ {0}; //!< Index in openmc::model::materials array + int temperature_idx_ { + 0}; //!< Index into the MGXS array representing temperature + double density_mult_ {1.0}; //!< A density multiplier queried from the cell + //!< corresponding to the source region. OpenMPMutex lock_; double volume_ { 0.0}; //!< Volume (computed from the sum of ray crossing lengths) @@ -394,6 +405,12 @@ public: int& material(int64_t sr) { return material_[sr]; } const int material(int64_t sr) const { return material_[sr]; } + int& temperature_idx(int64_t sr) { return temperature_idx_[sr]; } + const int temperature_idx(int64_t sr) const { return temperature_idx_[sr]; } + + double& density_mult(int64_t sr) { return density_mult_[sr]; } + const double density_mult(int64_t sr) const { return density_mult_[sr]; } + int& is_small(int64_t sr) { return is_small_[sr]; } const int is_small(int64_t sr) const { return is_small_[sr]; } @@ -625,6 +642,8 @@ private: // SoA storage for scalar fields (one item per source region) vector material_; + vector temperature_idx_; + vector density_mult_; vector is_small_; vector n_hits_; vector mesh_; @@ -671,7 +690,7 @@ private: // Private Methods // Helper function for indexing - inline int index(int64_t sr, int g) const { return sr * negroups_ + g; } + inline int64_t index(int64_t sr, int g) const { return sr * negroups_ + g; } }; } // namespace openmc diff --git a/include/openmc/ray.h b/include/openmc/ray.h new file mode 100644 index 000000000..62e86b0d9 --- /dev/null +++ b/include/openmc/ray.h @@ -0,0 +1,50 @@ +#ifndef OPENMC_RAY_H +#define OPENMC_RAY_H + +#include "openmc/particle_data.h" +#include "openmc/position.h" + +namespace openmc { + +// Base class that implements ray tracing logic, not necessarily through +// defined regions of the geometry but also outside of it. +class Ray : public GeometryState { + +public: + // Initialize from location and direction + Ray(Position r, Direction u) { init_from_r_u(r, u); } + + // Initialize from known geometry state + Ray(const GeometryState& p) : GeometryState(p) {} + + // Called at every surface intersection within the model + virtual void on_intersection() = 0; + + /* + * Traces the ray through the geometry, calling on_intersection + * at every surface boundary. + */ + void trace(); + + // Stops the ray and exits tracing when called from on_intersection + void stop() { stop_ = true; } + + // Sets the dist_ variable + void compute_distance(); + +protected: + // Records how far the ray has traveled + double traversal_distance_ {0.0}; + +private: + // Max intersections before we assume ray tracing is caught in an infinite + // loop: + static const int MAX_INTERSECTIONS = 1000000; + + bool stop_ {false}; + + unsigned event_counter_ {0}; +}; + +} // namespace openmc +#endif // OPENMC_RAY_H diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index 3314d1866..e95db1665 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -76,11 +76,21 @@ public: //! \return Name of the corresponding reaction std::string reaction_name(int mt); -//! Return reaction type (MT value) given a reaction name +//! Return MT value for given a reaction name (including special tally MT +//! values) // //! \param[in] name Reaction name -//! \return Corresponding reaction type (MT value) -int reaction_type(std::string name); +//! \return Corresponding MT number or special tally score +int reaction_tally_mt(std::string name); + +//! Return ENDF MT number given a reaction name +// +//! Unlike reaction_tally_mt(), this function always returns a positive ENDF MT +//! number and never a special negative tally score. +//! +//! \param[in] name Reaction name +//! \return Corresponding ENDF MT number +int reaction_mt(const std::string& name); } // namespace openmc diff --git a/include/openmc/reaction_product.h b/include/openmc/reaction_product.h index 4fbbc1b62..79a6160e2 100644 --- a/include/openmc/reaction_product.h +++ b/include/openmc/reaction_product.h @@ -10,7 +10,7 @@ #include "openmc/chain.h" #include "openmc/endf.h" #include "openmc/memory.h" // for unique_ptr -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/vector.h" // for vector namespace openmc { @@ -49,6 +49,21 @@ public: //! \param[inout] seed Pseudorandom seed pointer void sample(double E_in, double& E_out, double& mu, uint64_t* seed) const; + //! Select which angle-energy distribution to sample + //! \param[in] E_in Incoming energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the selected angle-energy distribution + AngleEnergy& sample_dist(double E_in, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const; + ParticleType particle_; //!< Particle type EmissionMode emission_mode_; //!< Emission mode double decay_rate_; //!< Decay rate (for delayed neutron precursors) in [1/s] diff --git a/include/openmc/scattdata.h b/include/openmc/scattdata.h index a75ef09d9..bea881402 100644 --- a/include/openmc/scattdata.h +++ b/include/openmc/scattdata.h @@ -4,7 +4,7 @@ #ifndef OPENMC_SCATTDATA_H #define OPENMC_SCATTDATA_H -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/vector.h" @@ -26,23 +26,23 @@ public: protected: //! \brief Initializes the attributes of the base class. - void base_init(int order, const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_energy, + void base_init(int order, const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_energy, const double_2dvec& in_mult); //! \brief Combines microscopic ScattDatas into a macroscopic one. void base_combine(size_t max_order, size_t order_dim, const vector& those_scatts, const vector& scalars, - xt::xtensor& in_gmin, xt::xtensor& in_gmax, + tensor::Tensor& in_gmin, tensor::Tensor& in_gmax, double_2dvec& sparse_mult, double_3dvec& sparse_scatter); public: double_2dvec energy; // Normalized p0 matrix for sampling Eout double_2dvec mult; // nu-scatter multiplication (nu-scatt/scatt) double_3dvec dist; // Angular distribution - xt::xtensor gmin; // minimum outgoing group - xt::xtensor gmax; // maximum outgoing group - xt::xtensor scattxs; // Isotropic Sigma_{s,g_{in}} + tensor::Tensor gmin; // minimum outgoing group + tensor::Tensor gmax; // maximum outgoing group + tensor::Tensor scattxs; // Isotropic Sigma_{s,g_{in}} //! \brief Calculates the value of normalized f(mu). //! @@ -72,8 +72,8 @@ public: //! @param in_gmax List of maximum outgoing groups for every incoming group //! @param in_mult Input sparse multiplicity matrix //! @param coeffs Input sparse scattering matrix - virtual void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + virtual void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) = 0; //! \brief Combines the microscopic data. @@ -96,7 +96,7 @@ public: //! @param max_order If Legendre this is the maximum value of "n" in "Pn" //! requested; ignored otherwise. //! @return The dense scattering matrix. - virtual xt::xtensor get_matrix(size_t max_order) = 0; + virtual tensor::Tensor get_matrix(size_t max_order) = 0; //! \brief Samples the outgoing energy from the ScattData info. //! @@ -135,8 +135,8 @@ protected: ScattDataLegendre& leg, ScattDataTabular& tab); public: - void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) override; void combine(const vector& those_scatts, @@ -153,7 +153,7 @@ public: size_t get_order() override { return dist[0][0].size() - 1; }; - xt::xtensor get_matrix(size_t max_order) override; + tensor::Tensor get_matrix(size_t max_order) override; }; //============================================================================== @@ -164,13 +164,13 @@ public: class ScattDataHistogram : public ScattData { protected: - xt::xtensor mu; // Angle distribution mu bin boundaries + tensor::Tensor mu; // Angle distribution mu bin boundaries double dmu; // Quick storage of the mu spacing double_3dvec fmu; // The angular distribution histogram public: - void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) override; void combine(const vector& those_scatts, @@ -183,7 +183,7 @@ public: size_t get_order() override { return dist[0][0].size(); }; - xt::xtensor get_matrix(size_t max_order) override; + tensor::Tensor get_matrix(size_t max_order) override; }; //============================================================================== @@ -194,7 +194,7 @@ public: class ScattDataTabular : public ScattData { protected: - xt::xtensor mu; // Angle distribution mu grid points + tensor::Tensor mu; // Angle distribution mu grid points double dmu; // Quick storage of the mu spacing double_3dvec fmu; // The angular distribution function @@ -204,8 +204,8 @@ protected: ScattDataLegendre& leg, ScattDataTabular& tab); public: - void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) override; void combine(const vector& those_scatts, @@ -218,7 +218,7 @@ public: size_t get_order() override { return dist[0][0].size(); }; - xt::xtensor get_matrix(size_t max_order) override; + tensor::Tensor get_matrix(size_t max_order) override; }; //============================================================================== diff --git a/include/openmc/secondary_correlated.h b/include/openmc/secondary_correlated.h index 6905c38e3..69b22981a 100644 --- a/include/openmc/secondary_correlated.h +++ b/include/openmc/secondary_correlated.h @@ -5,7 +5,7 @@ #define OPENMC_SECONDARY_CORRELATED_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/angle_energy.h" #include "openmc/distribution.h" @@ -25,9 +25,9 @@ public: struct CorrTable { int n_discrete; //!< Number of discrete lines Interpolation interpolation; //!< Interpolation law - xt::xtensor e_out; //!< Outgoing energies [eV] - xt::xtensor p; //!< Probability density - xt::xtensor c; //!< Cumulative distribution + tensor::Tensor e_out; //!< Outgoing energies [eV] + tensor::Tensor p; //!< Probability density + tensor::Tensor c; //!< Cumulative distribution vector> angle; //!< Angle distribution }; @@ -41,6 +41,22 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample the outgoing energy and return the angular distribution + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the angular distribution at the sampled energy bin + Distribution& sample_dist(double E_in, double& E_out, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + // energy property vector& energy() { return energy_; } const vector& energy() const { return energy_; } diff --git a/include/openmc/secondary_kalbach.h b/include/openmc/secondary_kalbach.h index 83806d352..b25352be9 100644 --- a/include/openmc/secondary_kalbach.h +++ b/include/openmc/secondary_kalbach.h @@ -5,7 +5,7 @@ #define OPENMC_SECONDARY_KALBACH_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/angle_energy.h" #include "openmc/constants.h" @@ -32,16 +32,34 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample outgoing energy and Kalbach-Mann parameters + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] km_a Kalbach-Mann 'a' parameter + //! \param[out] km_r Kalbach-Mann pre-compound fraction 'r' + //! \param[inout] seed Pseudorandom seed pointer + void sample_params(double E_in, double& E_out, double& km_a, double& km_r, + uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: //! Outgoing energy/angle at a single incoming energy struct KMTable { int n_discrete; //!< Number of discrete lines Interpolation interpolation; //!< Interpolation law - xt::xtensor e_out; //!< Outgoing energies [eV] - xt::xtensor p; //!< Probability density - xt::xtensor c; //!< Cumulative distribution - xt::xtensor r; //!< Pre-compound fraction - xt::xtensor a; //!< Parameterized function + tensor::Tensor e_out; //!< Outgoing energies [eV] + tensor::Tensor p; //!< Probability density + tensor::Tensor c; //!< Cumulative distribution + tensor::Tensor r; //!< Pre-compound fraction + tensor::Tensor a; //!< Parameterized function }; int n_region_; //!< Number of interpolation regions diff --git a/include/openmc/secondary_nbody.h b/include/openmc/secondary_nbody.h index efb4fd75b..9d033a6b8 100644 --- a/include/openmc/secondary_nbody.h +++ b/include/openmc/secondary_nbody.h @@ -28,6 +28,21 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy from the N-body phase space distribution + //! \param[in] E_in Incoming energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled outgoing energy in [eV] + double sample_energy(double E_in, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: int n_bodies_; //!< Number of particles distributed double mass_ratio_; //!< Total mass of particles [neutron mass] diff --git a/include/openmc/secondary_thermal.h b/include/openmc/secondary_thermal.h index 5b18902af..45d2c5260 100644 --- a/include/openmc/secondary_thermal.h +++ b/include/openmc/secondary_thermal.h @@ -6,10 +6,11 @@ #include "openmc/angle_energy.h" #include "openmc/endf.h" +#include "openmc/search.h" #include "openmc/secondary_correlated.h" #include "openmc/vector.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include namespace openmc { @@ -33,8 +34,20 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: const CoherentElasticXS& xs_; //!< Coherent elastic scattering cross section + tensor::Tensor bragg_edges_; //!< Copy of Bragg edges for slicing + tensor::Tensor + factors_diff_; //!< Differences over elastic scattering factors }; //============================================================================== @@ -56,6 +69,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: double debye_waller_; }; @@ -81,9 +103,18 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: const vector& energy_; //!< Energies at which cosines are tabulated - xt::xtensor mu_out_; //!< Cosines for each incident energy + tensor::Tensor mu_out_; //!< Cosines for each incident energy }; //============================================================================== @@ -106,12 +137,27 @@ public: //! \param[inout] seed Pseudorandom number seed pointer void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample outgoing energy bin parameters + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] j Sampled outgoing energy bin index + //! \param[inout] seed Pseudorandom seed pointer + void sample_params(double E_in, double& E_out, int& j, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; private: const vector& energy_; //!< Incident energies - xt::xtensor + tensor::Tensor energy_out_; //!< Outgoing energies for each incident energy - xt::xtensor + tensor::Tensor mu_out_; //!< Outgoing cosines for each incident/outgoing energy bool skewed_; //!< Whether outgoing energy distribution is skewed }; @@ -135,14 +181,33 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample outgoing energy bin parameters + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] f Interpolation factor within sampled energy bin + //! \param[out] l Index of the closer incident energy + //! \param[out] j Sampled outgoing energy bin index + //! \param[inout] seed Pseudorandom seed pointer + void sample_params(double E_in, double& E_out, double& f, int& l, int& j, + uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: //! Secondary energy/angle distribution struct DistEnergySab { std::size_t n_e_out; //!< Number of outgoing energies - xt::xtensor e_out; //!< Outgoing energies - xt::xtensor e_out_pdf; //!< Probability density function - xt::xtensor e_out_cdf; //!< Cumulative distribution function - xt::xtensor mu; //!< Equiprobable angles at each outgoing energy + tensor::Tensor e_out; //!< Outgoing energies + tensor::Tensor e_out_pdf; //!< Probability density function + tensor::Tensor e_out_cdf; //!< Cumulative distribution function + tensor::Tensor mu; //!< Equiprobable angles at each outgoing energy }; vector energy_; //!< Incident energies @@ -170,6 +235,21 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Select the coherent or incoherent elastic distribution to sample + //! \param[in] E_in Incoming energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the selected angle-energy distribution + const AngleEnergy& sample_dist(double E_in, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: CoherentElasticAE coherent_dist_; //!< Coherent distribution unique_ptr incoherent_dist_; //!< Incoherent distribution @@ -178,6 +258,133 @@ private: const Function1D& incoherent_xs_; //!< Polymorphic ref. to incoherent XS }; +//! Internal helper for evaluating a piecewise-constant PDF on discrete points. +//! +//! The underlying discrete points are represented implicitly through a +//! monotonically increasing `center(i)` function and corresponding per-point +//! `weight(i)` values. Each point contributes a rectangular bin whose +//! half-width is half the distance to its nearest neighboring center. +//! +//! \tparam CenterFn Callable returning the location of the i-th discrete value +//! \tparam WeightFn Callable returning the weight of the i-th discrete value +//! \param[in] n Number of discrete values +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete_impl(std::size_t n, double mu_0, double a, double b, + CenterFn center, WeightFn weight) +{ + if (n == 0 || mu_0 < a || mu_0 > b) + return 0.0; + + auto evaluate_bin = [&](std::size_t i) { + double x = center(i); + double left_span = (i == 0) ? 2.0 * (x - a) : x - center(i - 1); + double right_span = (i + 1 == n) ? 2.0 * (b - x) : center(i + 1) - x; + double delta = 0.5 * std::min(left_span, right_span); + if (delta <= 0.0) + return 0.0; + + double left = x - delta; + double right = x + delta; + bool in_bin = + (mu_0 >= left) && ((i + 1 == n) ? (mu_0 <= right) : (mu_0 < right)); + return in_bin ? weight(i) / (2.0 * delta) : 0.0; + }; + + // This is effectively a lower_bound over the sequence center(i), but the + // sequence is implicit rather than stored in a container, so the STL + // algorithms can not be used. + std::size_t low = 0; + std::size_t high = n; + while (low < high) { + std::size_t mid = low + (high - low) / 2; + if (center(mid) < mu_0) { + low = mid + 1; + } else { + high = mid; + } + } + + if (low < n) { + double pdf = evaluate_bin(low); + if (pdf > 0.0) + return pdf; + } + if (low > 0) + return evaluate_bin(low - 1); + return 0.0; +} + +//! Evaluate the PDF of a weighted discrete distribution at a given point. +//! +//! Given a set of discrete values mu[i] with weights w[i], this function +//! computes the probability density at mu_0 by treating each discrete value +//! as a rectangular bin. The bin half-width around each discrete value is +//! half the distance to its nearest neighbor. +//! +//! \tparam T1 Container type for discrete cosine values (must support +//! operator[], size()) +//! \tparam T2 Container type for weights (must support operator[]) +//! \param[in] mu Sorted array of discrete cosine values +//! \param[in] w Weights for each discrete value (need not be normalized) +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete( + const T1 mu, const T2& w, double mu_0, double a = -1.0, double b = 1.0) +{ + // Returns the location of the discrete value for a given index + auto center = [&](std::size_t i) { return mu[i]; }; + auto weight = [&](std::size_t i) { return w[i]; }; + return get_pdf_discrete_impl(mu.size(), mu_0, a, b, center, weight); +} + +//! Evaluate the PDF of a discrete distribution with uniform weights +//! +//! \tparam T1 Container type for discrete cosine values +//! \param[in] mu Sorted array of discrete cosine values +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete( + const T1 mu, double mu_0, double a = -1.0, double b = 1.0) +{ + auto center = [&](std::size_t i) { return mu[i]; }; + auto weight = [&](std::size_t i) { return 1.0 / mu.size(); }; + return get_pdf_discrete_impl(mu.size(), mu_0, a, b, center, weight); +} + +//! Evaluate the PDF of a uniformly weighted distribution on interpolated points +//! +//! \tparam T1 Container type for the lower tabulated cosine values +//! \tparam T2 Container type for the upper tabulated cosine values +//! \param[in] mu0 Sorted array of discrete cosine values at the lower grid +//! \param[in] mu1 Sorted array of discrete cosine values at the upper grid +//! \param[in] f Interpolation factor between mu0 and mu1 +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete_interpolated(const T1 mu0, const T2 mu1, double f, + double mu_0, double a = -1.0, double b = 1.0) +{ + if (mu0.size() != mu1.size()) + return 0.0; + + // Returns interpolated discrete value for a given index + auto center = [&](std::size_t i) { return mu0[i] + f * (mu1[i] - mu0[i]); }; + auto weight = [&](std::size_t i) { return 1.0 / mu0.size(); }; + return get_pdf_discrete_impl(mu0.size(), mu_0, a, b, center, weight); +} + } // namespace openmc #endif // OPENMC_SECONDARY_THERMAL_H diff --git a/include/openmc/secondary_uncorrelated.h b/include/openmc/secondary_uncorrelated.h index 3afa3d9ce..f895ae77f 100644 --- a/include/openmc/secondary_uncorrelated.h +++ b/include/openmc/secondary_uncorrelated.h @@ -32,6 +32,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + // Accessors AngleDistribution& angle() { return angle_; } diff --git a/include/openmc/settings.h b/include/openmc/settings.h index b369c99fe..3bba040f5 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -77,6 +77,7 @@ extern "C" bool output_summary; //!< write summary.h5? extern bool output_tallies; //!< write tallies.out? extern bool particle_restart_run; //!< particle restart run? extern "C" bool photon_transport; //!< photon transport turned on? +extern bool atomic_relaxation; //!< atomic relaxation enabled? extern "C" bool reduce_tallies; //!< reduce tallies at end of batch? extern bool res_scat_on; //!< use resonance upscattering method? extern "C" bool restart_run; //!< restart run? @@ -97,7 +98,8 @@ extern bool uniform_source_sampling; //!< sample sources uniformly? extern bool ufs_on; //!< uniform fission site method on? extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? extern bool use_decay_photons; //!< use decay photons for D1S -extern "C" bool weight_windows_on; //!< are weight windows are enabled? +extern bool use_shared_secondary_bank; //!< Use shared bank for secondaries +extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? extern bool weight_window_checkpoint_collision; //!< enable weight window check @@ -114,6 +116,8 @@ extern std::string path_sourcepoint; //!< path to a source file extern std::string path_statepoint; //!< path to a statepoint file extern std::string weight_windows_file; //!< Location of weight window file to //!< load on simulation initialization +extern std::string properties_file; //!< Location of properties file to + //!< load on simulation initialization // This is required because the c_str() may not be the first thing in // std::string. Sometimes it is, but it seems libc++ may not be like that @@ -181,6 +185,8 @@ extern int64_t ssw_cell_id; //!< Cell id for the surface source //!< write setting extern SSWCellType ssw_cell_type; //!< Type of option for the cell //!< argument of surface source write +extern double surface_grazing_cutoff; //!< surface flux cosine cutoff +extern double surface_grazing_ratio; //!< surface flux substitution ratio extern TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/include/openmc/shared_array.h b/include/openmc/shared_array.h index 7e9ef28c5..2829a2eb9 100644 --- a/include/openmc/shared_array.h +++ b/include/openmc/shared_array.h @@ -4,6 +4,8 @@ //! \file shared_array.h //! \brief Shared array data structure +#include // for copy_n + #include "openmc/memory.h" namespace openmc { @@ -30,14 +32,12 @@ public: //! Default constructor. SharedArray() = default; - //! Construct a zero size container with space to hold capacity number of - //! elements. + //! Construct a container with `size` elements and capacity equal to `size`. // - //! \param capacity The number of elements for the container to allocate - //! space for - SharedArray(int64_t capacity) : capacity_(capacity) + //! \param size The number of elements to allocate and initialize + SharedArray(int64_t size) : size_(size), capacity_(size) { - data_ = make_unique(capacity); + data_ = make_unique(size); } //========================================================================== @@ -97,8 +97,52 @@ public: capacity_ = 0; } + //! Push back an element to the array, with capacity and reallocation behavior + //! as if this were a vector. This does not perform any thread safety checks. + //! If the size exceeds the capacity, then the capacity will double just as + //! with a vector. Data will be reallocated and moved to a new pointer and + //! copied in before the new item is appended. Old data will be freed. + void thread_unsafe_append(const T& value) + { + if (size_ == capacity_) { + int64_t new_capacity = capacity_ == 0 ? 8 : 2 * capacity_; + unique_ptr new_data = make_unique(new_capacity); + std::copy_n(data_.get(), size_, new_data.get()); + data_ = std::move(new_data); + capacity_ = new_capacity; + } + data_[size_++] = value; + } + + //! Increase the size of the container by count elements without assigning + //! values to the new elements. Existing elements are preserved if the + //! container needs to grow. This does not perform any thread safety checks. + // + //! \param count The number of elements to append + //! \return The starting index of the appended range + int64_t extend_uninitialized(int64_t count) + { + int64_t offset = size_; + int64_t new_size = size_ + count; + if (new_size > capacity_) { + int64_t new_capacity = capacity_ == 0 ? 8 : capacity_; + while (new_capacity < new_size) { + new_capacity *= 2; + } + unique_ptr new_data = make_unique(new_capacity); + if (size_ > 0) { + std::copy_n(data_.get(), size_, new_data.get()); + } + data_ = std::move(new_data); + capacity_ = new_capacity; + } + size_ = new_size; + return offset; + } + //! Return the number of elements in the container int64_t size() { return size_; } + int64_t size() const { return size_; } //! Resize the container to contain a specified number of elements. This is //! useful in cases where the container is written to in a non-thread safe diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 9a6cf1b21..454752cd2 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -49,6 +49,9 @@ extern const RegularMesh* ufs_mesh; extern vector k_generation; extern vector work_index; +extern int64_t + simulation_tracks_completed; //!< Number of tracks completed on this rank + } // namespace simulation //============================================================================== @@ -59,7 +62,7 @@ extern vector work_index; void allocate_banks(); //! Determine number of particles to transport per process -void calculate_work(); +void calculate_work(int64_t n_particles); //! Initialize nuclear data before a simulation void initialize_data(); @@ -70,8 +73,9 @@ void initialize_batch(); //! Initialize a fission generation void initialize_generation(); -//! Full initialization of a particle history -void initialize_history(Particle& p, int64_t index_source); +//! Full initialization of a particle track +void initialize_particle_track( + Particle& p, int64_t index_source, bool is_secondary); //! Finalize a batch //! @@ -92,16 +96,35 @@ void broadcast_results(); void free_memory_simulation(); -//! Simulate a single particle history (and all generated secondary particles, -//! if enabled), from birth to death +//! Compute unique particle ID from a 1-based source index +//! \param index_source 1-based source index within this rank's work +//! \return globally unique particle ID +int64_t compute_particle_id(int64_t index_source); + +//! Compute the transport RNG seed from a particle ID +//! \param particle_id the particle's globally unique ID +//! \return seed value passed to init_particle_seeds() +int64_t compute_transport_seed(int64_t particle_id); + +//! Simulate a single particle history from birth to death, inclusive of any +//! secondary particles. In shared secondary mode, only a single track is +//! transported and secondaries are deposited into a shared bank instead. void transport_history_based_single_particle(Particle& p); //! Simulate all particle histories using history-based parallelism void transport_history_based(); +//! Simulate all particles using history-based parallelism, with a shared +//! secondary bank +void transport_history_based_shared_secondary(); + //! Simulate all particle histories using event-based parallelism void transport_event_based(); +//! Simulate all particles using event-based parallelism, with a shared +//! secondary bank +void transport_event_based_shared_secondary(); + } // namespace openmc #endif // OPENMC_SIMULATION_H diff --git a/include/openmc/source.h b/include/openmc/source.h index 1fbd31904..51b54a1d1 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -4,15 +4,19 @@ #ifndef OPENMC_SOURCE_H #define OPENMC_SOURCE_H +#include #include #include +#include // for pair #include "pugixml.hpp" +#include "openmc/array.h" +#include "openmc/distribution.h" #include "openmc/distribution_multi.h" #include "openmc/distribution_spatial.h" #include "openmc/memory.h" -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/vector.h" namespace openmc { @@ -25,15 +29,24 @@ namespace openmc { // source_rejection_fraction constexpr int EXTSRC_REJECT_THRESHOLD {10000}; +// Maximum number of source rejections allowed while sampling a single site +constexpr int64_t MAX_SOURCE_REJECTIONS_PER_SAMPLE {1'000'000}; + //============================================================================== // Global variables //============================================================================== +// Cumulative counters for source rejection diagnostics. These are atomic to +// allow thread-safe concurrent sampling of external sources. +extern std::atomic source_n_accept; +extern std::atomic source_n_reject; + class Source; namespace model { extern vector> external_sources; +extern vector> adjoint_sources; // Probability distribution for selecting external sources extern DiscreteIndex external_sources_probability; @@ -148,11 +161,11 @@ protected: private: // Data members - ParticleType particle_ {ParticleType::neutron}; //!< Type of particle emitted - UPtrSpace space_; //!< Spatial distribution - UPtrAngle angle_; //!< Angular distribution - UPtrDist energy_; //!< Energy distribution - UPtrDist time_; //!< Time distribution + ParticleType particle_; //!< Type of particle emitted + UPtrSpace space_; //!< Spatial distribution + UPtrAngle angle_; //!< Angular distribution + UPtrDist energy_; //!< Energy distribution + UPtrDist time_; //!< Time distribution }; //============================================================================== @@ -217,8 +230,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return (sampled position, importance weight) + std::pair sample(uint64_t* seed) const override; private: int32_t mesh_index_ {C_NONE}; //!< Index in global meshes array @@ -250,6 +263,139 @@ private: vector> sources_; //!< Source distributions }; +//============================================================================== +//! Parametric tokamak plasma neutron source +//! +//! This source samples neutron positions from a tokamak plasma geometry using +//! Miller-style flux surface parameterization with user-specified emission +//! profiles and energy distributions. +//! +//! Flux surface parameterization: +//! R = R0 + r*cos(alpha + delta*sin(alpha)) + Delta*(1 - (r/a)^2) +//! Z = kappa * r * sin(alpha) +//! +//! The sampling algorithm: +//! 1. Sample minor radius r from precomputed CDF of S(r) * Jacobian +//! 2. Sample poloidal angle alpha from conditional P(alpha|r) using mixture +//! of precomputed CDFs weighted by functions of r +//! 3. Sample energy and time from user-provided distribution(s) +//! 4. Sample isotropic direction +//! 5. Sample toroidal angle phi uniformly in [phi_start, phi_start + +//! phi_extent] +//! 6. Transform (r, alpha, phi) to Cartesian (x, y, z), applying the optional +//! vertical shift of the plasma center +//! +//! The user provides the emission density S(r) directly (e.g., from transport +//! codes like TRANSP, ASTRA, etc.), allowing full flexibility in reaction +//! physics calculations. S(r) is a profile in arbitrary units sampled on the +//! r_over_a grid; only its shape matters, since it is normalized internally. +//! Energy distributions can be specified as either a single distribution for +//! all r, or one distribution per radial point. +//============================================================================== + +class TokamakSource : public Source { +public: + // Constructors + explicit TokamakSource(pugi::xml_node node); + + //! Sample from the tokamak source distribution + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled site + SourceSite sample(uint64_t* seed) const override; + +private: + //========================================================================== + // Private methods + + //! Precompute data structures for efficient sampling + void precompute_sampling_distributions(); + + //! Sample minor radius from marginal CDF + //! \param seed Pseudorandom seed pointer + //! \return Sampled r/a value + double sample_r_over_a(uint64_t* seed) const; + + //! Sample poloidal angle given r using mixture of precomputed CDFs + //! \param r_norm Normalized minor radius r/a + //! \param seed Pseudorandom seed pointer + //! \return Sampled poloidal angle alpha [rad] + double sample_poloidal_angle(double r_norm, uint64_t* seed) const; + + //! Compute the k-th mixture weight w_k(r) * I_hat_k for poloidal sampling + //! \param k Basis function index (0-5) + //! \param r Normalized minor radius r/a + //! \return Mixture weight for component k + double mixture_weight(int k, double r) const; + + //! Sample energy from the distribution(s) + //! \param r_norm Normalized minor radius r/a (for distribution selection) + //! \param seed Pseudorandom seed pointer + //! \return (Sampled energy [eV], importance weight) + std::pair sample_energy(double r_norm, uint64_t* seed) const; + + //! Transform from flux coordinates (r, alpha, phi) to Cartesian (x, y, z) + //! \param r Minor radius [cm] + //! \param alpha Poloidal angle [rad] + //! \param phi Toroidal angle [rad] + //! \return Position in Cartesian coordinates [cm] + Position flux_to_cartesian(double r, double alpha, double phi) const; + + //========================================================================== + // Data members + + // Emission profile (input) + vector r_over_a_; //!< Normalized minor radius grid points + vector emission_density_; //!< Emission density S(r) at grid points + + // Energy distribution(s): either 1 for all r, or one per r point + vector> energy_dists_; + + // Time distribution (defaults to a delta distribution at t=0) + UPtrDist time_; + + // Angular distribution (isotropic) + UPtrAngle angle_; + + // Tokamak geometry parameters + double major_radius_; //!< Major radius R0 [cm] + double minor_radius_; //!< Minor radius a [cm] + double elongation_; //!< Elongation kappa + double triangularity_; //!< Triangularity delta + double shafranov_shift_; //!< Shafranov shift Delta [cm] + double vertical_shift_; //!< Vertical shift of plasma center [cm] + + // Normalized geometry parameters (precomputed for efficiency) + double epsilon_; //!< Inverse aspect ratio a/R0 + double delta_tilde_; //!< Normalized Shafranov shift Delta/a + + // Toroidal angle bounds + double phi_start_; //!< Starting toroidal angle [rad] + double phi_extent_; //!< Toroidal angle extent [rad] + + // Precomputed distribution for radial sampling + unique_ptr radial_dist_; + + // Coefficients of the radial geometric polynomial: A*r - B*r^2 - C*r^3 + // Also used as the analytical normalization for poloidal mixture weights + double radial_poly_a_; //!< 1 + ε·Δ̃ + double radial_poly_b_; //!< (3/8)·c₁·ε + double radial_poly_c_; //!< 2·ε·Δ̃ + + // Precomputed Bernstein basis functions for poloidal angle sampling. + // Using the factorization f(r_tilde, alpha) = R_tilde x J_tilde where: + // R_tilde = b0*(1-r)^2 + 2*b1*r*(1-r) + b2*r^2 (Bernstein quadratic) + // J_tilde = b3*(1-r) + b4*r (Bernstein linear) + // The product gives 6 non-negative basis functions g_k(alpha) with + // weights w_k(r_tilde) that are products of Bernstein polynomials. + // Distributions are tabulated on [0, pi] exploiting up-down symmetry. + static constexpr int N_POLOIDAL_BASIS = 6; //!< Number of basis functions + int n_alpha_; //!< Number of poloidal angle grid points + array, N_POLOIDAL_BASIS> + poloidal_dists_; //!< Distributions for each basis function g_k(alpha) + array + poloidal_integrals_; //!< Integrals of g_k(alpha) over [0, pi] +}; + //============================================================================== // Functions //============================================================================== @@ -265,6 +411,9 @@ SourceSite sample_external_source(uint64_t* seed); void free_memory_source(); +//! Reset cumulative source rejection counters +void reset_source_rejection_counters(); + } // namespace openmc #endif // OPENMC_SOURCE_H diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 549e8f6ab..2d8580345 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -2,6 +2,7 @@ #define OPENMC_SURFACE_H #include // For numeric_limits +#include #include #include @@ -378,7 +379,39 @@ public: // Non-member functions //============================================================================== -void read_surfaces(pugi::xml_node node); +//! Read surface definitions from XML and populate the global surfaces vector. +//! +//! This function parses surface elements from the XML input, creates the +//! appropriate surface objects, and identifies periodic surfaces along with +//! their albedo values and sense information. +//! +//! \param node XML node containing surface definitions +//! \param[out] periodic_pairs Set of surface ID pairs representing periodic +//! boundary conditions +//! \param[out] albedo_map Map of surface IDs to albedo values for periodic +//! surfaces +//! \param[out] periodic_sense_map Map of surface IDs to their sense values +//! (used to determine orientation for periodic BCs) +void read_surfaces(pugi::xml_node node, + std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map); + +//! Resolve periodic surface pairs and assign boundary conditions. +//! +//! This function completes the setup of periodic boundary conditions by +//! resolving unpaired periodic surfaces, determining whether each pair +//! represents translational or rotational periodicity based on surface +//! normals, and assigning the appropriate boundary condition objects. +//! +//! \param[inout] periodic_pairs Set of surface ID pairs representing periodic +//! boundary conditions; unpaired entries are resolved +//! \param albedo_map Map of surface IDs to albedo values for periodic surfaces +//! \param periodic_sense_map Map of surface IDs to their sense values (used to +//! determine orientation for periodic BCs) +void prepare_boundary_conditions(std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map); void free_memory_surfaces(); diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 65098597a..77b0d9f42 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -39,7 +39,9 @@ enum class FilterType { MUSURFACE, PARENT_NUCLIDE, PARTICLE, + PARTICLE_PRODUCTION, POLAR, + REACTION, SPHERICAL_HARMONICS, SPATIAL_LEGENDRE, SURFACE, diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index cf8a8aa0f..3e410d9fd 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -1,6 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_ENERGY_H #define OPENMC_TALLIES_FILTER_ENERGY_H +#include "openmc/particle.h" #include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_mesh.h b/include/openmc/tallies/filter_mesh.h index 4c9460d2e..c35a477fe 100644 --- a/include/openmc/tallies/filter_mesh.h +++ b/include/openmc/tallies/filter_mesh.h @@ -51,6 +51,12 @@ public: virtual bool translated() const { return translated_; } + virtual void set_rotation(const vector& rotation); + + virtual const vector& rotation() const { return rotation_; } + + virtual bool rotated() const { return rotated_; } + protected: //---------------------------------------------------------------------------- // Data members @@ -58,6 +64,8 @@ protected: int32_t mesh_; //!< Index of the mesh bool translated_ {false}; //!< Whether or not the filter is translated Position translation_ {0.0, 0.0, 0.0}; //!< Filter translation + bool rotated_ {false}; //!< Whether or not the filter is rotated + vector rotation_; //!< Filter rotation }; } // namespace openmc diff --git a/include/openmc/tallies/filter_particle.h b/include/openmc/tallies/filter_particle.h index 863a6d282..9932a6037 100644 --- a/include/openmc/tallies/filter_particle.h +++ b/include/openmc/tallies/filter_particle.h @@ -1,7 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_PARTICLE_H #define OPENMC_TALLIES_FILTER_PARTICLE_H -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_particle_production.h b/include/openmc/tallies/filter_particle_production.h new file mode 100644 index 000000000..00fac9f7c --- /dev/null +++ b/include/openmc/tallies/filter_particle_production.h @@ -0,0 +1,53 @@ +#ifndef OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H +#define OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H + +#include + +#include "openmc/particle.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins the outgoing energy of secondary particles +//! +//! This filter bins secondary particles by type and, optionally, by energy. It +//! can be used to get the photon production matrix for multigroup photon +//! transport, the energy distribution of secondary neutrons, etc. The weight +//! that is applied is equal to the weight of the secondary particle. Thus, to +//! get secondary production it should be used in conjunction with the "events" +//! score. +//============================================================================== + +class ParticleProductionFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "particleproduction"; } + FilterType type() const override { return FilterType::PARTICLE_PRODUCTION; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; + + void from_xml(pugi::xml_node node) override; + + void to_statepoint(hid_t filter_group) const override; + +private: + //---------------------------------------------------------------------------- + // Data members + + vector secondary_types_; //!< Types of secondary particles + + //! Map from PDG number to index in secondary_types_ for O(1) lookup + std::unordered_map type_to_index_; + + vector energy_bins_; //!< Energy bin boundaries (optional) +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H diff --git a/include/openmc/tallies/filter_reaction.h b/include/openmc/tallies/filter_reaction.h new file mode 100644 index 000000000..015729eef --- /dev/null +++ b/include/openmc/tallies/filter_reaction.h @@ -0,0 +1,52 @@ +#ifndef OPENMC_TALLIES_FILTER_REACTION_H +#define OPENMC_TALLIES_FILTER_REACTION_H + +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins tally events based on the reaction type (MT number). +//============================================================================== + +class ReactionFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~ReactionFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "reaction"; } + FilterType type() const override { return FilterType::REACTION; } + + void from_xml(pugi::xml_node node) override; + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + void to_statepoint(hid_t filter_group) const override; + + std::string text_label(int bin) const override; + + //---------------------------------------------------------------------------- + // Accessors + + const vector& bins() const { return bins_; } + void set_bins(span bins); + +protected: + //---------------------------------------------------------------------------- + // Data members + + //! MT numbers to match + vector bins_; +}; + +} // namespace openmc + +#endif // OPENMC_TALLIES_FILTER_REACTION_H diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 374daff92..ae604b732 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -8,9 +8,8 @@ #include "openmc/tallies/trigger.h" #include "openmc/vector.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xfixed.hpp" -#include "xtensor/xtensor.hpp" #include #include @@ -55,7 +54,7 @@ public: void set_nuclides(const vector& nuclides); - const xt::xtensor& results() const { return results_; } + const tensor::Tensor& results() const { return results_; } //! returns vector of indices corresponding to the tally this is called on const vector& filters() const { return filters_; } @@ -98,9 +97,9 @@ public: //! Given already-set filters, set the stride lengths void set_strides(); - int32_t strides(int i) const { return strides_[i]; } + int64_t strides(int i) const { return strides_[i]; } - int32_t n_filter_bins() const { return n_filter_bins_; } + int64_t n_filter_bins() const { return n_filter_bins_; } bool multiply_density() const { return multiply_density_; } @@ -125,7 +124,7 @@ public: int score_index(const std::string& score) const; //! Tally results reshaped according to filter sizes - xt::xarray get_reshaped_data() const; + tensor::Tensor get_reshaped_data() const; //! A string representing the i-th score on this tally std::string score_name(int score_idx) const; @@ -160,7 +159,7 @@ public: //! combination of filters (e.g. specific cell, specific energy group, etc.) //! and the second dimension of the array is for scores (e.g. flux, total //! reaction rate, fission reaction rate, etc.) - xt::xtensor results_; + tensor::Tensor results_; //! True if this tally should be written to statepoint files bool writable_ {true}; @@ -185,9 +184,9 @@ private: vector filters_; //!< Filter indices in global filters array //! Index strides assigned to each filter to support 1D indexing. - vector strides_; + vector strides_; - int32_t n_filter_bins_ {0}; + int64_t n_filter_bins_ {0}; //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; @@ -213,15 +212,14 @@ extern vector active_collision_tallies; extern vector active_meshsurf_tallies; extern vector active_surface_tallies; extern vector active_pulse_height_tallies; -extern vector pulse_height_cells; +extern vector pulse_height_cells; extern vector time_grid; } // namespace model namespace simulation { //! Global tallies (such as k-effective estimators) -extern xt::xtensor_fixed> - global_tallies; +extern tensor::StaticTensor2D global_tallies; //! Number of realizations for global tallies extern "C" int32_t n_realizations; @@ -257,12 +255,6 @@ double distance_to_time_boundary(double time, double speed); //! Determine which tallies should be active void setup_active_tallies(); -// Alias for the type returned by xt::adapt(...). N is the dimension of the -// multidimensional array -template -using adaptor_type = - xt::xtensor_adaptor, N>; - #ifdef OPENMC_MPI //! Collect all tally results onto master process void reduce_tally_results(); diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index c3ab779e6..4303ddb7a 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -41,7 +41,7 @@ public: FilterBinIter& operator++(); - int index_ {1}; + int64_t index_ {1}; double weight_ {1.}; vector& filter_matches_; @@ -101,11 +101,19 @@ void score_tracklength_tally(Particle& p, double distance); //! \param total_distance The distance in [cm] traveled by the particle void score_timed_tracklength_tally(Particle& p, double total_distance); -//! Score surface or mesh-surface tallies for particle currents. +//! Score mesh-surface tallies for particle currents. // //! \param p The particle being tracked //! \param tallies A vector of the indices of the tallies to score to -void score_surface_tally(Particle& p, const vector& tallies); +void score_meshsurface_tally(Particle& p, const vector& tallies); + +//! Score surface tallies for particle currents. +// +//! \param p The particle being tracked +//! \param tallies A vector of the indices of the tallies to score to +//! \param normal The normal of the surface being crossed +void score_surface_tally( + Particle& p, const vector& tallies, const Direction& normal); //! Score the pulse-height tally //! This is triggered at the end of every particle history diff --git a/include/openmc/tensor.h b/include/openmc/tensor.h new file mode 100644 index 000000000..9e443e72f --- /dev/null +++ b/include/openmc/tensor.h @@ -0,0 +1,1210 @@ +//! \file tensor.h +//! \brief Multi-dimensional tensor types for OpenMC. +//! +//! Tensor is the primary type: a dynamic-rank owning container that stores +//! elements contiguously in row-major order. View is a lightweight +//! non-owning reference into a Tensor's storage, returned by the slice() +//! method and flat(). StaticTensor2D is a small stack-allocated 2D +//! array used only for simulation::global_tallies. +//! +//! Slicing follows numpy conventions: each axis takes an index (rank-reducing), +//! All (keep entire axis), or Range (keep sub-range). For example, +//! arr.slice(0, all, range(2, 5)) is equivalent to numpy's arr[0, :, 2:5]. +//! +//! View is declared before Tensor because Tensor's methods return View objects. + +#ifndef OPENMC_TENSOR_H +#define OPENMC_TENSOR_H + +#include "openmc/vector.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openmc { +namespace tensor { + +//============================================================================== +// Forward declarations +//============================================================================== + +template +class Tensor; + +template +class StaticTensor2D; + +//============================================================================== +// Storage type mapping +// +// std::vector is a bit-packed specialization that returns proxy objects +// instead of real references, which breaks generic code. storage_type_map +// redirects bool to unsigned char so that Tensor stores one byte per +// element with normal reference semantics. +//============================================================================== + +template +struct storage_type_map { + using type = T; +}; +template<> +struct storage_type_map { + using type = unsigned char; +}; +template +using storage_type = typename storage_type_map::type; + +//============================================================================== +// Slice argument types +// +// Used with the variadic slice() method on Tensor, View, and StaticTensor2D. +// Each argument corresponds to one axis: a plain integer fixes that axis at +// a single index (rank-reducing), All keeps the entire axis, and Range keeps +// a sub-range. +//============================================================================== + +//! Keep an entire axis (equivalent to numpy's ':' or xtensor's xt::all()) +struct All {}; +constexpr All all {}; + +//! Sub-range along an axis [start, end) +struct Range { + size_t start; + size_t end; // SIZE_MAX means "to end of axis" +}; + +//! Create a Range [start, end) +inline Range range(size_t start, size_t end) +{ + return {start, end}; +} + +//! Create a Range [0, end) +inline Range range(size_t end) +{ + return {0, end}; +} + +namespace detail { + +//! Internal: normalized representation of a per-axis slice argument +struct SliceArg { + enum Kind { INDEX, ALL, RANGE } kind; + size_t start; + size_t end; +}; + +inline SliceArg to_slice_arg(All) +{ + return {SliceArg::ALL, 0, 0}; +} +inline SliceArg to_slice_arg(Range r) +{ + return {SliceArg::RANGE, r.start, r.end}; +} + +template +inline + typename std::enable_if::value || std::is_enum::value, + SliceArg>::type + to_slice_arg(I i) +{ + return {SliceArg::INDEX, static_cast(i), 0}; +} + +//! Result of a slice computation: pointer offset + new shape/strides +struct SliceResult { + size_t ptr_offset; + vector shape; + vector strides; +}; + +//! Compute the result of applying slice arguments to shape/strides +template +SliceResult compute_slice(const vector& shape, + const vector& strides, First first, Rest... rest) +{ + const size_t n = 1 + sizeof...(Rest); + SliceArg args[1 + sizeof...(Rest)] = { + to_slice_arg(first), to_slice_arg(rest)...}; + + size_t offset = 0; + vector new_shape; + vector new_strides; + + for (size_t a = 0; a < n; ++a) { + switch (args[a].kind) { + case SliceArg::INDEX: + offset += args[a].start * strides[a]; + break; + case SliceArg::ALL: + new_shape.push_back(shape[a]); + new_strides.push_back(strides[a]); + break; + case SliceArg::RANGE: { + offset += args[a].start * strides[a]; + size_t end = (args[a].end == SIZE_MAX) ? shape[a] : args[a].end; + new_shape.push_back(end - args[a].start); + new_strides.push_back(strides[a]); + break; + } + } + } + + // Trailing axes not covered by arguments are implicitly All. + // This matches numpy: a[i] on a 2D array returns a 1D row. + for (size_t a = n; a < shape.size(); ++a) { + new_shape.push_back(shape[a]); + new_strides.push_back(strides[a]); + } + + return {offset, std::move(new_shape), std::move(new_strides)}; +} + +} // namespace detail + +//============================================================================== +// View: a non-owning N-dimensional view into a tensor's storage. +// +// Holds a base pointer, shape, and strides (in elements). Supports arbitrary +// rank and multi-axis slicing via the variadic slice() method. +//============================================================================== + +template +class View { +public: + //-------------------------------------------------------------------------- + // Constructors + + View(T* data, vector shape, vector strides) + : data_(data), shape_(std::move(shape)), strides_(std::move(strides)) + {} + + // Explicitly default copy/move constructors (declaring copy assignment + // below would otherwise suppress the implicit move constructor). + View(const View&) = default; + View(View&&) = default; + + //-------------------------------------------------------------------------- + // Indexing + + //! Multi-index element access (1D, 2D, 3D, ...) + template + T& operator()(Indices... indices) + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off += idx[d] * strides_[d]; + return data_[off]; + } + + template + const T& operator()(Indices... indices) const + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off += idx[d] * strides_[d]; + return data_[off]; + } + + //! Flat logical index (row-major order) + T& operator[](size_t i) { return data_[flat_to_offset(i)]; } + const T& operator[](size_t i) const { return data_[flat_to_offset(i)]; } + + //-------------------------------------------------------------------------- + // Accessors + + size_t size() const + { + size_t s = 1; + for (auto d : shape_) + s *= d; + return s; + } + size_t ndim() const { return shape_.size(); } + size_t shape(size_t axis) const { return shape_[axis]; } + const vector& shape_vec() const { return shape_; } + T* data() { return data_; } + const T* data() const { return data_; } + + //-------------------------------------------------------------------------- + // View accessors + + //! Multi-axis slice. Each argument corresponds to one axis and is either: + //! - an integer (fixes that axis, rank-reducing) + //! - All (keeps entire axis) + //! - Range (keeps sub-range along that axis) + //! Example: v.slice(0, all, range(2, 5)) == numpy v[0, :, 2:5] + template + View slice(First first, Rest... rest) + { + auto r = detail::compute_slice(shape_, strides_, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + template + View slice(First first, Rest... rest) const + { + auto r = detail::compute_slice(shape_, strides_, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + //-------------------------------------------------------------------------- + // Assignment operators + + //! Copy assignment: element-wise deep copy (writes through data pointer). + //! Without this, the compiler's implicit copy assignment just copies the + //! View metadata (pointer, shape, strides) instead of the viewed data. + View& operator=(const View& other) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = other[i]; + return *this; + } + + //! Fill all elements with a scalar + View& operator=(T val) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = val; + return *this; + } + + //! Assignment from initializer_list (for 1D views) + View& operator=(std::initializer_list vals) + { + auto it = vals.begin(); + for (size_t i = 0; i < size() && it != vals.end(); ++i, ++it) + data_[flat_to_offset(i)] = *it; + return *this; + } + + //! Assignment from another View + template + View& operator=(const View& other) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = other[i]; + return *this; + } + + //! Assignment from Tensor (forward-declared, defined after Tensor) + template + View& operator=(const Tensor& other); + + //! Compound addition from Tensor (forward-declared, defined after Tensor) + template + View& operator+=(const Tensor& o); + + //! Compound multiply by scalar + View& operator*=(T val) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] *= val; + return *this; + } + + //! Compound divide by scalar + View& operator/=(T val) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] /= val; + return *this; + } + + //-------------------------------------------------------------------------- + // Reductions + + //! Sum of all elements + T sum() const + { + // remove_const needed so accumulator is mutable when T is const-qualified + std::remove_const_t s = 0; + size_t n = size(); + for (size_t i = 0; i < n; ++i) + s += data_[flat_to_offset(i)]; + return s; + } + + //-------------------------------------------------------------------------- + // Iterators + // + // Lightweight row-major iterator parameterized on pointer type (Ptr). + // Stores a flat logical position and converts to a physical offset on each + // dereference via divmod over shape/strides. For contiguous 1D views (the + // common case) the divmod chain reduces to a single multiply-by-1, which + // the compiler optimizes away. + // + // view_iterator = mutable iterator (from non-const View) + // view_iterator = read-only iterator (from const View) + + template + class view_iterator { + Ptr base_; + size_t count_; + const size_t* shape_; + const size_t* strides_; + size_t ndim_; + + public: + using iterator_category = std::random_access_iterator_tag; + using value_type = std::remove_const_t; + using difference_type = std::ptrdiff_t; + using pointer = Ptr; + using reference = decltype(*std::declval()); + + view_iterator(Ptr base, size_t count, const View* v) + : base_(base), count_(count), shape_(v->shape_.data()), + strides_(v->strides_.data()), ndim_(v->shape_.size()) + {} + + reference operator*() const { return base_[offset()]; } + reference operator[](difference_type n) const + { + return base_[offset_of(count_ + n)]; + } + view_iterator& operator++() + { + ++count_; + return *this; + } + view_iterator operator++(int) + { + auto tmp = *this; + ++count_; + return tmp; + } + view_iterator& operator--() + { + --count_; + return *this; + } + view_iterator operator+(difference_type n) const + { + auto tmp = *this; + tmp.count_ += n; + return tmp; + } + view_iterator operator-(difference_type n) const + { + auto tmp = *this; + tmp.count_ -= n; + return tmp; + } + difference_type operator-(const view_iterator& o) const + { + return static_cast(count_) - + static_cast(o.count_); + } + view_iterator& operator+=(difference_type n) + { + count_ += n; + return *this; + } + view_iterator& operator-=(difference_type n) + { + count_ -= n; + return *this; + } + bool operator==(const view_iterator& o) const { return count_ == o.count_; } + bool operator!=(const view_iterator& o) const { return count_ != o.count_; } + bool operator<(const view_iterator& o) const { return count_ < o.count_; } + bool operator>(const view_iterator& o) const { return count_ > o.count_; } + bool operator<=(const view_iterator& o) const { return count_ <= o.count_; } + bool operator>=(const view_iterator& o) const { return count_ >= o.count_; } + friend view_iterator operator+(difference_type n, const view_iterator& it) + { + return it + n; + } + + private: + size_t offset() const { return offset_of(count_); } + size_t offset_of(size_t flat) const + { + size_t off = 0; + for (int d = static_cast(ndim_) - 1; d >= 0; --d) { + off += (flat % shape_[d]) * strides_[d]; + flat /= shape_[d]; + } + return off; + } + }; + + using iterator = view_iterator; + using const_iterator = view_iterator; + + iterator begin() { return {data_, 0, this}; } + iterator end() { return {data_, size(), this}; } + const_iterator begin() const { return cbegin(); } + const_iterator end() const { return cend(); } + const_iterator cbegin() const { return {data_, 0, this}; } + const_iterator cend() const { return {data_, size(), this}; } + +private: + //! Convert a logical flat index (row-major) to a physical element offset + size_t flat_to_offset(size_t flat) const + { + size_t off = 0; + for (int d = static_cast(shape_.size()) - 1; d >= 0; --d) { + off += (flat % shape_[d]) * strides_[d]; + flat /= shape_[d]; + } + return off; + } + + T* data_; + vector shape_; + vector strides_; +}; + +//============================================================================== +// Tensor: dynamic-rank N-dimensional tensor. +// +// Stores elements in a contiguous row-major vector> +// with a dynamic shape. +//============================================================================== + +template +class Tensor { +public: + using value_type = T; + using stored_type = storage_type; + using iterator = typename vector::iterator; + using const_iterator = typename vector::const_iterator; + + //-------------------------------------------------------------------------- + // Constructors + + Tensor() = default; + + //! Construct with shape (uninitialized for arithmetic types via vector + //! resize) + explicit Tensor(vector shape) + : shape_(std::move(shape)), data_(compute_size()) + {} + + //! Construct with shape and fill value + Tensor(vector shape, T fill) + : shape_(std::move(shape)), data_(compute_size(), fill) + {} + + //! Construct from initializer_list shape + explicit Tensor(std::initializer_list shape) + : shape_(shape), data_(compute_size()) + {} + + //! Construct from initializer_list shape with fill + Tensor(std::initializer_list shape, T fill) + : shape_(shape), data_(compute_size(), fill) + {} + + //! 1D copy from raw pointer + count + Tensor(const T* ptr, size_t count) : shape_({count}), data_(ptr, ptr + count) + {} + + //! Copy from View (preserves view's shape) + template + explicit Tensor(const View& v) : shape_(v.shape_vec()) + { + size_t n = v.size(); + data_.resize(n); + for (size_t i = 0; i < n; ++i) + data_[i] = v[i]; + } + + //-------------------------------------------------------------------------- + // Assignment + + //! Assignment from View + template + Tensor& operator=(const View& v) + { + shape_ = v.shape_vec(); + size_t n = v.size(); + data_.resize(n); + for (size_t i = 0; i < n; ++i) + data_[i] = v[i]; + return *this; + } + + //! Assignment from initializer_list of values (1D) + Tensor& operator=(std::initializer_list vals) + { + shape_ = {vals.size()}; + data_.assign(vals.begin(), vals.end()); + return *this; + } + + //-------------------------------------------------------------------------- + // Accessors + + stored_type* data() { return data_.data(); } + const stored_type* data() const { return data_.data(); } + size_t size() const { return data_.size(); } + const vector& shape() const { return shape_; } + size_t shape(size_t dim) const + { + return dim < shape_.size() ? shape_[dim] : 0; + } + size_t ndim() const { return shape_.size(); } + bool empty() const { return data_.empty(); } + + //-------------------------------------------------------------------------- + // Indexing (row-major) + + template + stored_type& operator()(Indices... indices) + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off = off * shape_[d] + idx[d]; + return data_[off]; + } + + template + const stored_type& operator()(Indices... indices) const + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off = off * shape_[d] + idx[d]; + return data_[off]; + } + + stored_type& operator[](size_t i) { return data_[i]; } + const stored_type& operator[](size_t i) const { return data_[i]; } + + //! First and last element + stored_type& front() { return data_.front(); } + const stored_type& front() const { return data_.front(); } + stored_type& back() { return data_.back(); } + const stored_type& back() const { return data_.back(); } + + //-------------------------------------------------------------------------- + // Iterators + + iterator begin() { return data_.begin(); } + iterator end() { return data_.end(); } + const_iterator begin() const { return data_.begin(); } + const_iterator end() const { return data_.end(); } + const_iterator cbegin() const { return data_.cbegin(); } + const_iterator cend() const { return data_.cend(); } + + //-------------------------------------------------------------------------- + // Mutation + + void resize(const vector& shape) + { + shape_ = shape; + data_.resize(compute_size()); + } + + void resize(std::initializer_list shape) + { + shape_.assign(shape.begin(), shape.end()); + data_.resize(compute_size()); + } + + void reshape(const vector& new_shape) { shape_ = new_shape; } + + void fill(T val) { std::fill(data_.begin(), data_.end(), val); } + + //-------------------------------------------------------------------------- + // View accessors + + //! Fix one axis at a given index, returning an (N-1)-dimensional view + //! Multi-axis slice. Each argument corresponds to one axis and is either: + //! - an integer (fixes that axis, rank-reducing) + //! - All (keeps entire axis) + //! - Range (keeps sub-range along that axis) + //! Example: t.slice(0, all, range(2, 5)) == numpy t[0, :, 2:5] + template + View slice(First first, Rest... rest) + { + auto strides = compute_strides(); + auto r = detail::compute_slice(shape_, strides, first, rest...); + return { + data_.data() + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + template + View slice(First first, Rest... rest) const + { + auto strides = compute_strides(); + auto r = detail::compute_slice(shape_, strides, first, rest...); + return { + data_.data() + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + //! Flat 1D view of all elements + View flat() + { + return {data_.data(), {data_.size()}, {size_t(1)}}; + } + View flat() const + { + return {data_.data(), {data_.size()}, {size_t(1)}}; + } + + //-------------------------------------------------------------------------- + // Reductions and transforms + + //! Sum of all elements + T sum() const + { + T s = T(0); + for (size_t i = 0; i < data_.size(); ++i) + s += data_[i]; + return s; + } + + //! Sum along an axis, reducing rank by 1 (defined out-of-line below) + Tensor sum(size_t axis) const; + + //! Product of all elements + T prod() const + { + T p = T(1); + for (size_t i = 0; i < data_.size(); ++i) + p *= data_[i]; + return p; + } + + //! True if any element is nonzero + bool any() const + { + for (size_t i = 0; i < data_.size(); ++i) + if (data_[i]) + return true; + return false; + } + + //! True if all elements are nonzero + bool all() const + { + for (size_t i = 0; i < data_.size(); ++i) + if (!data_[i]) + return false; + return true; + } + + //! Flat index of the minimum element + size_t argmin() const + { + return static_cast(std::distance(data_.data(), + std::min_element(data_.data(), data_.data() + data_.size()))); + } + + //! Reverse element order along an axis (e.g. flip(0) reverses rows) + Tensor flip(size_t axis) const + { + size_t outer_size = 1; + for (size_t d = 0; d < axis; ++d) + outer_size *= shape_[d]; + size_t axis_size = shape_[axis]; + size_t inner_size = 1; + for (size_t d = axis + 1; d < shape_.size(); ++d) + inner_size *= shape_[d]; + + Tensor r(shape_); + for (size_t o = 0; o < outer_size; ++o) + for (size_t a = 0; a < axis_size; ++a) + for (size_t i = 0; i < inner_size; ++i) + r.data_[(o * axis_size + (axis_size - 1 - a)) * inner_size + i] = + data_[(o * axis_size + a) * inner_size + i]; + return r; + } + + //-------------------------------------------------------------------------- + // Operators + + Tensor& operator+=(T val) + { + for (auto& x : data_) + x += val; + return *this; + } + Tensor& operator-=(T val) + { + for (auto& x : data_) + x -= val; + return *this; + } + Tensor& operator*=(T val) + { + for (auto& x : data_) + x *= val; + return *this; + } + Tensor& operator/=(T val) + { + for (auto& x : data_) + x /= val; + return *this; + } + Tensor& operator+=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] += o.data_[i]; + return *this; + } + Tensor& operator-=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] -= o.data_[i]; + return *this; + } + Tensor& operator*=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] *= o.data_[i]; + return *this; + } + Tensor& operator/=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] /= o.data_[i]; + return *this; + } + + Tensor operator+(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] + o.data_[i]; + return r; + } + Tensor operator-(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] - o.data_[i]; + return r; + } + Tensor operator/(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] / o.data_[i]; + return r; + } + Tensor operator*(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] * o.data_[i]; + return r; + } + + Tensor operator+(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] + val; + return r; + } + Tensor operator-(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] - val; + return r; + } + Tensor operator*(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] * val; + return r; + } + + Tensor operator<=(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] <= val; + return r; + } + Tensor operator<(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] < val; + return r; + } + Tensor operator>=(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] >= val; + return r; + } + Tensor operator>(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] > val; + return r; + } + Tensor operator<(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] < o.data_[i]; + return r; + } + +private: + size_t compute_size() const + { + size_t s = 1; + for (auto d : shape_) + s *= d; + return s; + } + + //! Compute row-major strides from shape + vector compute_strides() const + { + vector strides(shape_.size()); + if (!shape_.empty()) { + strides.back() = 1; + for (int d = static_cast(shape_.size()) - 2; d >= 0; --d) + strides[d] = strides[d + 1] * shape_[d + 1]; + } + return strides; + } + + //-------------------------------------------------------------------------- + // Data members + + vector shape_; + vector> data_; +}; + +//============================================================================== +// Non-member operators (scalar op tensor) +//============================================================================== + +template +Tensor operator*(T val, const Tensor& arr) +{ + return arr * val; +} + +template +Tensor operator+(T val, const Tensor& arr) +{ + return arr + val; +} + +// Mixed-type arithmetic: Tensor op Tensor -> Tensor +// A SFINAE guard is used here, as without !is_same Tensor * Tensor +// would be ambiguous between the member operator* and this non-member function. +template::value>> +Tensor operator*(const Tensor& a, const Tensor& b) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = + static_cast(a.data()[i]) * static_cast(b.data()[i]); + return r; +} + +// Same SFINAE guard as operator* above. +template::value>> +Tensor operator/(const Tensor& a, const Tensor& b) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = + static_cast(a.data()[i]) / static_cast(b.data()[i]); + return r; +} + +//============================================================================== +// Out-of-line method definitions (require complete types) +//============================================================================== + +template +template +View& View::operator=(const Tensor& other) +{ + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = static_cast(other.data()[i]); + return *this; +} + +template +template +View& View::operator+=(const Tensor& o) +{ + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] += o.data()[i]; + return *this; +} + +template +Tensor Tensor::sum(size_t axis) const +{ + // Build output shape (all dims except the summed axis) + vector out_shape; + for (size_t d = 0; d < shape_.size(); ++d) + if (d != axis) + out_shape.push_back(shape_[d]); + + // Split dimensions into three zones: outer | axis | inner + size_t outer_size = 1; + for (size_t d = 0; d < axis; ++d) + outer_size *= shape_[d]; + size_t axis_size = shape_[axis]; + size_t inner_size = 1; + for (size_t d = axis + 1; d < shape_.size(); ++d) + inner_size *= shape_[d]; + + Tensor result(out_shape, T(0)); + for (size_t o = 0; o < outer_size; ++o) + for (size_t a = 0; a < axis_size; ++a) + for (size_t i = 0; i < inner_size; ++i) + result.data()[o * inner_size + i] += + data_[(o * axis_size + a) * inner_size + i]; + + return result; +} + +//============================================================================== +// StaticTensor2D: compile-time fixed 2D tensor. +//============================================================================== + +template +class StaticTensor2D { +public: + using value_type = T; + + //-------------------------------------------------------------------------- + // Indexing + + //! Templated to accept enum class indices (e.g. GlobalTally, TallyResult) + //! which don't implicitly convert to integer types. + template + T& operator()(I0 i, I1 j) + { + return data_[static_cast(i) * C + static_cast(j)]; + } + template + const T& operator()(I0 i, I1 j) const + { + return data_[static_cast(i) * C + static_cast(j)]; + } + + //-------------------------------------------------------------------------- + // Accessors + + T* data() { return data_; } + const T* data() const { return data_; } + constexpr size_t size() const { return R * C; } + std::array shape() const { return {R, C}; } + + //-------------------------------------------------------------------------- + // Mutation + + void fill(T val) { std::fill(data_, data_ + R * C, val); } + + //-------------------------------------------------------------------------- + // Iterators + + T* begin() { return data_; } + T* end() { return data_ + R * C; } + const T* begin() const { return data_; } + const T* end() const { return data_ + R * C; } + + //-------------------------------------------------------------------------- + // View accessors + + //! Multi-axis slice (same interface as Tensor/View). + template + View slice(First first, Rest... rest) + { + vector sh = {R, C}; + vector st = {C, 1}; + auto r = detail::compute_slice(sh, st, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + template + View slice(First first, Rest... rest) const + { + vector sh = {R, C}; + vector st = {C, 1}; + auto r = detail::compute_slice(sh, st, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + //! Flat view (1D, contiguous) + View flat() { return {data_, {R * C}, {size_t(1)}}; } + View flat() const { return {data_, {R * C}, {size_t(1)}}; } + +private: + //-------------------------------------------------------------------------- + // Data members + + T data_[R * C] = {}; +}; + +//============================================================================== +// Non-member functions +//============================================================================== + +template +Tensor zeros(std::initializer_list shape) +{ + vector s(shape); + return Tensor(std::move(s), T(0)); +} + +template +Tensor zeros(const vector& shape) +{ + return Tensor(shape, T(0)); +} + +template +Tensor ones(std::initializer_list shape) +{ + vector s(shape); + return Tensor(std::move(s), T(1)); +} + +template +Tensor ones(const vector& shape) +{ + return Tensor(shape, T(1)); +} + +template +Tensor zeros_like(const Tensor& o) +{ + return Tensor(o.shape(), T(0)); +} + +template +Tensor full_like(const Tensor& o, V val) +{ + return Tensor(o.shape(), static_cast(val)); +} + +//! Return a 1D tensor of n evenly spaced values from start to stop (inclusive) +template +Tensor linspace(T start, T stop, size_t n) +{ + Tensor result({n}); + if (n < 2) { + result[0] = start; + return result; + } + for (size_t i = 0; i < n; ++i) { + result[i] = + start + static_cast(i) * (stop - start) / static_cast(n - 1); + } + return result; +} + +//! Concatenate two 1D tensors end-to-end +template +Tensor concatenate(const Tensor& a, const Tensor& b) +{ + size_t total = a.size() + b.size(); + Tensor result({total}); + std::copy(a.data(), a.data() + a.size(), result.data()); + std::copy(b.data(), b.data() + b.size(), result.data() + a.size()); + return result; +} + +//! Element-wise natural logarithm +template +Tensor log(const Tensor& a) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = std::log(a.data()[i]); + return r; +} + +//! Element-wise absolute value +template +Tensor abs(const Tensor& a) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = std::abs(a.data()[i]); + return r; +} + +//! Element-wise conditional: select from true_val where cond is true, +//! otherwise use false_val +template +Tensor where( + const Tensor& cond, const Tensor& true_val, V false_val) +{ + Tensor r(cond.shape()); + for (size_t i = 0; i < cond.size(); ++i) + r.data()[i] = + cond.data()[i] ? true_val.data()[i] : static_cast(false_val); + return r; +} + +//! Replace NaN/Inf values with finite substitutes +template +Tensor nan_to_num(const Tensor& a, T nan_val = T(0), + T posinf_val = std::numeric_limits::max(), + T neginf_val = std::numeric_limits::lowest()) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) { + T val = a.data()[i]; + if (std::isnan(val)) + r.data()[i] = nan_val; + else if (std::isinf(val)) + r.data()[i] = val > 0 ? posinf_val : neginf_val; + else + r.data()[i] = val; + } + return r; +} + +//============================================================================== +// Type traits +//============================================================================== + +//! Type trait that is true for Tensor and StaticTensor2D. +//! Used by hdf5_interface.h to select the correct write_dataset overload. +template +struct is_tensor : std::false_type {}; + +template +struct is_tensor> : std::true_type {}; + +template +struct is_tensor> : std::true_type {}; + +} // namespace tensor +} // namespace openmc + +#endif // OPENMC_TENSOR_H diff --git a/include/openmc/thermal.h b/include/openmc/thermal.h index de0767d0a..c06a2ee0d 100644 --- a/include/openmc/thermal.h +++ b/include/openmc/thermal.h @@ -5,7 +5,7 @@ #include #include -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/angle_energy.h" #include "openmc/endf.h" @@ -51,7 +51,25 @@ public: //! \param[out] mu Outgoing scattering angle cosine //! \param[inout] seed Pseudorandom seed pointer void sample(const NuclideMicroXS& micro_xs, double E_in, double* E_out, - double* mu, uint64_t* seed); + double* mu, uint64_t* seed) const; + + //! Select the elastic or inelastic distribution to sample + //! \param[in] micro_xs Microscopic cross sections + //! \param[in] E Incident neutron energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the selected angle-energy distribution + AngleEnergy& sample_dist( + const NuclideMicroXS& micro_xs, double E, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] micro_xs Microscopic cross sections + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf(const NuclideMicroXS& micro_xs, double E_in, + double mu, double& E_out, uint64_t* seed) const; private: struct Reaction { diff --git a/include/openmc/urr.h b/include/openmc/urr.h index 1e6037158..d40c2aff7 100644 --- a/include/openmc/urr.h +++ b/include/openmc/urr.h @@ -3,7 +3,7 @@ #ifndef OPENMC_URR_H #define OPENMC_URR_H -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" @@ -40,11 +40,11 @@ public: * below, obviously, values of the CDF are stored. For the xs_values * variable, the columns line up with the index of cdf_values. */ - xt::xtensor cdf_values_; // Note: must be row major! - xt::xtensor xs_values_; + tensor::Tensor cdf_values_; // Note: must be row major! + tensor::Tensor xs_values_; // Number of points in the CDF - auto n_cdf() const { return cdf_values_.shape()[1]; } + auto n_cdf() const { return cdf_values_.shape(1); } //! \brief Load the URR data from the provided HDF5 group explicit UrrData(hid_t group_id); diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index fa8d3d65e..ef75ec065 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -12,8 +12,8 @@ #include "openmc/tallies/trigger.h" #include "openmc/vector.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #ifdef _OPENMP #include #endif @@ -34,7 +34,7 @@ public: vector atoms; //!< Number of atoms for each nuclide vector uncertainty; //!< Uncertainty on number of atoms int iterations; //!< Number of iterations needed to obtain the results - }; // Results for a single domain + }; // Results for a single domain // Constructors VolumeCalculation(pugi::xml_node node); diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 763815522..d0b385d16 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -10,7 +10,7 @@ #include "openmc/constants.h" #include "openmc/memory.h" #include "openmc/mesh.h" -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/span.h" #include "openmc/tallies/tally.h" #include "openmc/vector.h" @@ -25,17 +25,6 @@ enum class WeightWindowUpdateMethod { MAGIC, FW_CADIS }; constexpr double DEFAULT_WEIGHT_CUTOFF {1.0e-38}; // default low weight cutoff -//============================================================================== -// Non-member functions -//============================================================================== - -//! Apply weight windows to a particle -//! \param[in] p Particle to apply weight windows to -void apply_weight_windows(Particle& p); - -//! Free memory associated with weight windows -void free_memory_weight_windows(); - //============================================================================== // Global variables //============================================================================== @@ -63,8 +52,12 @@ struct WeightWindow { double weight_cutoff {DEFAULT_WEIGHT_CUTOFF}; int max_split {10}; - //! Whether the weight window is in a valid state - bool is_valid() const { return lower_weight >= 0.0; } + //! Whether the weight window is in a valid state. A non-positive lower + //! bound indicates that no weight window information exists at this + //! location (generators mark such cells with -1, and a lower bound of zero + //! conventionally turns the weight window game off in a cell, as in MCNP + //! wwinp files), in which case no weight window game is played. + bool is_valid() const { return lower_weight > 0.0; } //! Adjust the weight window by a constant factor void scale(double factor) @@ -137,16 +130,16 @@ public: //! Retrieve the weight window for a particle //! \param[in] p Particle to get weight window for - WeightWindow get_weight_window(const Particle& p) const; + std::pair get_weight_window(const Particle& p) const; std::array bounds_size() const; const vector& energy_bounds() const { return energy_bounds_; } - void set_bounds(const xt::xtensor& lower_ww_bounds, - const xt::xtensor& upper_bounds); + void set_bounds(const tensor::Tensor& lower_ww_bounds, + const tensor::Tensor& upper_bounds); - void set_bounds(const xt::xtensor& lower_bounds, double ratio); + void set_bounds(const tensor::Tensor& lower_bounds, double ratio); void set_bounds( span lower_bounds, span upper_bounds); @@ -182,25 +175,24 @@ public: const std::unique_ptr& mesh() const { return model::meshes[mesh_idx_]; } - const xt::xtensor& lower_ww_bounds() const { return lower_ww_; } - xt::xtensor& lower_ww_bounds() { return lower_ww_; } + const tensor::Tensor& lower_ww_bounds() const { return lower_ww_; } + tensor::Tensor& lower_ww_bounds() { return lower_ww_; } - const xt::xtensor& upper_ww_bounds() const { return upper_ww_; } - xt::xtensor& upper_ww_bounds() { return upper_ww_; } + const tensor::Tensor& upper_ww_bounds() const { return upper_ww_; } + tensor::Tensor& upper_ww_bounds() { return upper_ww_; } ParticleType particle_type() const { return particle_type_; } private: //---------------------------------------------------------------------------- // Data members - int32_t id_; //!< Unique ID - int64_t index_; //!< Index into weight windows vector - ParticleType particle_type_ { - ParticleType::neutron}; //!< Particle type to apply weight windows to + int32_t id_; //!< Unique ID + int64_t index_; //!< Index into weight windows vector + ParticleType particle_type_; //!< Particle type to apply weight windows to vector energy_bounds_; //!< Energy boundaries [eV] - xt::xtensor lower_ww_; //!< Lower weight window bounds (shape: + tensor::Tensor lower_ww_; //!< Lower weight window bounds (shape: //!< energy_bins, mesh_bins (k, j, i)) - xt::xtensor + tensor::Tensor upper_ww_; //!< Upper weight window bounds (shape: energy_bins, mesh_bins) double survival_ratio_ {3.0}; //!< Survival weight ratio double max_lb_ratio_ {1.0}; //!< Maximum lower bound to particle weight ratio @@ -235,8 +227,31 @@ public: double threshold_ {1.0}; // targets_; }; +//============================================================================== +// Non-member functions +//============================================================================== + +//! Apply weight windows to a particle +//! \param[in] p Particle to apply weight windows to +void apply_weight_windows(Particle& p); + +//! Apply weight window to a particle +//! \param[in] p Particle to apply weight window to +//! \param[in] weight_window WeightWindow to apply +void apply_weight_window(Particle& p, WeightWindow weight_window); + +//! Free memory associated with weight windows +void free_memory_weight_windows(); + +//! Search weight window that apply to a particle +//! \param[in] p Particle to search weight window for +std::pair search_weight_window(const Particle& p); + //! Finalize variance reduction objects after all inputs have been read void finalize_variance_reduction(); diff --git a/include/openmc/wmp.h b/include/openmc/wmp.h index 6a4abd867..5cc04e595 100644 --- a/include/openmc/wmp.h +++ b/include/openmc/wmp.h @@ -2,7 +2,7 @@ #define OPENMC_WMP_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include @@ -78,9 +78,9 @@ public: int fit_order_; //!< Order of the fit bool fissionable_; //!< Is the nuclide fissionable? vector window_info_; // Information about a window - xt::xtensor + tensor::Tensor curvefit_; // Curve fit coefficients (window, poly order, reaction) - xt::xtensor, 2> data_; //!< Poles and residues + tensor::Tensor> data_; //!< Poles and residues // Constant data static constexpr int MAX_POLY_COEFFICIENTS = diff --git a/include/openmc/xml_interface.h b/include/openmc/xml_interface.h index f49613ecd..17a34e5c7 100644 --- a/include/openmc/xml_interface.h +++ b/include/openmc/xml_interface.h @@ -5,9 +5,8 @@ #include // for stringstream #include +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xadapt.hpp" -#include "xtensor/xarray.hpp" #include "openmc/position.h" #include "openmc/vector.h" @@ -42,12 +41,11 @@ vector get_node_array( } template -xt::xarray get_node_xarray( +tensor::Tensor get_node_tensor( pugi::xml_node node, const char* name, bool lowercase = false) { vector v = get_node_array(node, name, lowercase); - vector shape = {v.size()}; - return xt::adapt(v, shape); + return tensor::Tensor(v.data(), v.size()); } std::vector get_node_position_array( diff --git a/include/openmc/xsdata.h b/include/openmc/xsdata.h index feafde68d..c9dbde986 100644 --- a/include/openmc/xsdata.h +++ b/include/openmc/xsdata.h @@ -4,7 +4,7 @@ #ifndef OPENMC_XSDATA_H #define OPENMC_XSDATA_H -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/hdf5_interface.h" #include "openmc/memory.h" @@ -69,26 +69,26 @@ private: public: // The following quantities have the following dimensions: // [angle][incoming group] - xt::xtensor total; - xt::xtensor absorption; - xt::xtensor nu_fission; - xt::xtensor prompt_nu_fission; - xt::xtensor kappa_fission; - xt::xtensor fission; - xt::xtensor inverse_velocity; + tensor::Tensor total; + tensor::Tensor absorption; + tensor::Tensor nu_fission; + tensor::Tensor prompt_nu_fission; + tensor::Tensor kappa_fission; + tensor::Tensor fission; + tensor::Tensor inverse_velocity; // decay_rate has the following dimensions: // [angle][delayed group] - xt::xtensor decay_rate; + tensor::Tensor decay_rate; // delayed_nu_fission has the following dimensions: // [angle][delayed group][incoming group] - xt::xtensor delayed_nu_fission; + tensor::Tensor delayed_nu_fission; // chi_prompt has the following dimensions: // [angle][incoming group][outgoing group] - xt::xtensor chi_prompt; + tensor::Tensor chi_prompt; // chi_delayed has the following dimensions: // [angle][incoming group][outgoing group][delayed group] - xt::xtensor chi_delayed; + tensor::Tensor chi_delayed; // scatter has the following dimensions: [angle] vector> scatter; diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 7826a759a..3205b4340 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -39,6 +39,9 @@ Use \fIN\fP OpenMP threads. .B "\-t\fR, \fP\-\-track" Write tracks for all particles (up to max_tracks). .TP +.BI \-q " V" "\fR,\fP \-\-verbosity" " V" +Set the output verbosity to \fIV\fP. +.TP .B "\-v\fR, \fP\-\-version" Show version information. .TP @@ -63,7 +66,7 @@ Indicates the default path to an HDF5 file that contains multi-group cross section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE -Copyright \(co 2011-2025 Massachusetts Institute of Technology, UChicago +Copyright \(co 2011-2026 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors. .PP Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/openmc/__init__.py b/openmc/__init__.py index bb972b4e6..c204929c8 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -37,7 +37,7 @@ from openmc.tracks import * from .config import * # Import a few names from the model module -from openmc.model import Model +from openmc.model import Model, SearchResult from . import examples diff --git a/openmc/_sparse_compat.py b/openmc/_sparse_compat.py new file mode 100644 index 000000000..c00777e19 --- /dev/null +++ b/openmc/_sparse_compat.py @@ -0,0 +1,43 @@ +"""Compatibility module for scipy.sparse arrays + +This module provides a compatibility layer for working with scipy.sparse arrays +across different scipy versions. Sparse arrays were introduced gradually in +scipy, with full support arriving in scipy 1.15. This module provides a unified +API that uses sparse arrays when available and falls back to sparse matrices for +older scipy versions. + +For more information on the migration from sparse matrices to sparse arrays, +see: https://docs.scipy.org/doc/scipy/reference/sparse.migration_to_sparray.html +""" + +import scipy +from scipy import sparse as sp + +# Check scipy version for feature availability +_SCIPY_VERSION = tuple(map(int, scipy.__version__.split('.')[:2])) + +if _SCIPY_VERSION >= (1, 15): + # Use sparse arrays + csr_array = sp.csr_array + csc_array = sp.csc_array + dok_array = sp.dok_array + lil_array = sp.lil_array + eye_array = sp.eye_array + block_array = sp.block_array +else: + # Fall back to sparse matrices + csr_array = sp.csr_matrix + csc_array = sp.csc_matrix + dok_array = sp.dok_matrix + lil_array = sp.lil_matrix + eye_array = sp.eye + block_array = sp.bmat + +__all__ = [ + 'csr_array', + 'csc_array', + 'dok_array', + 'lil_array', + 'eye_array', + 'block_array', +] diff --git a/openmc/cell.py b/openmc/cell.py index 82a034b1c..499cf9504 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -636,8 +636,11 @@ class Cell(IDManagerMixin): element.set("material", str(self.fill.id)) elif self.fill_type == 'distribmat': - element.set("material", ' '.join(['void' if m is None else str(m.id) - for m in self.fill])) + material_subelement= ET.SubElement(element, "material") + matlist_str = " ".join( + ["void" if m is None else str(m.id) for m in self.fill] + ) + material_subelement.text = matlist_str elif self.fill_type in ('universe', 'lattice'): element.set("fill", str(self.fill.id)) @@ -677,14 +680,15 @@ class Cell(IDManagerMixin): if self.temperature is not None: if isinstance(self.temperature, Iterable): - element.set("temperature", ' '.join( - str(t) for t in self.temperature)) + temperature_subelement= ET.SubElement(element, "temperature") + temperature_subelement.text = ' '.join(str(t) for t in self.temperature) else: element.set("temperature", str(self.temperature)) if self.density is not None: if isinstance(self.density, Iterable): - element.set("density", ' '.join(str(t) for t in self.density)) + density_subelement= ET.SubElement(element, "density") + density_subelement.text = ' '.join(str(d) for d in self.density) else: element.set("density", str(self.density)) @@ -743,15 +747,6 @@ class Cell(IDManagerMixin): c.region = Region.from_expression(region, surfaces) # Check for other attributes - temperature = get_elem_list(elem, 'temperature', float) - if temperature is not None: - if len(temperature) > 1: - c.temperature = temperature - else: - c.temperature = temperature[0] - density = get_elem_list(elem, 'density', float) - if density is not None: - c.density = density if len(density) > 1 else density[0] v = get_text(elem, 'volume') if v is not None: c.volume = float(v) @@ -760,6 +755,8 @@ class Cell(IDManagerMixin): if values is not None: if key == 'rotation' and len(values) == 9: values = np.array(values).reshape(3, 3) + elif len(values) == 1: + values = values[0] setattr(c, key, values) # Add this cell to appropriate universe diff --git a/openmc/cmfd.py b/openmc/cmfd.py index eff6a151f..8595d1e02 100644 --- a/openmc/cmfd.py +++ b/openmc/cmfd.py @@ -25,6 +25,7 @@ import openmc.lib from .checkvalue import (check_type, check_length, check_value, check_greater_than, check_less_than) from .exceptions import OpenMCError +from ._sparse_compat import csr_array # See if mpi4py module can be imported, define have_mpi global variable try: @@ -980,8 +981,7 @@ class CMFDRun: loss_row = self._loss_row loss_col = self._loss_col temp_data = np.ones(len(loss_row)) - temp_loss = sparse.csr_matrix((temp_data, (loss_row, loss_col)), - shape=(n, n)) + temp_loss = csr_array((temp_data, (loss_row, loss_col)), shape=(n, n)) temp_loss.sort_indices() # Pass coremap as 1-d array of 32-bit integers @@ -1585,7 +1585,7 @@ class CMFDRun: # Create csr matrix loss_row = self._loss_row loss_col = self._loss_col - loss = sparse.csr_matrix((data, (loss_row, loss_col)), shape=(n, n)) + loss = csr_array((data, (loss_row, loss_col)), shape=(n, n)) loss.sort_indices() return loss @@ -1612,7 +1612,7 @@ class CMFDRun: # Create csr matrix prod_row = self._prod_row prod_col = self._prod_col - prod = sparse.csr_matrix((data, (prod_row, prod_col)), shape=(n, n)) + prod = csr_array((data, (prod_row, prod_col)), shape=(n, n)) prod.sort_indices() return prod diff --git a/openmc/dagmc.py b/openmc/dagmc.py index d1265be26..75ead5e43 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -36,12 +36,6 @@ class DAGMCUniverse(openmc.UniverseBase): auto_mat_ids : bool Set IDs automatically on initialization (True) or report overlaps in ID space between OpenMC and UWUW materials (False) - material_overrides : dict, optional - A dictionary of material overrides. The keys are material name strings - and the values are Iterables of openmc.Material objects. If a material - name is found in the DAGMC file, the material will be replaced with the - openmc.Material object in the value. - Attributes ---------- id : int @@ -78,15 +72,6 @@ class DAGMCUniverse(openmc.UniverseBase): The number of surfaces in the model. .. versionadded:: 0.13.2 - material_overrides : dict - A dictionary of material overrides. Keys are cell IDs; values are - iterables of :class:`openmc.Material` objects. The material assignment - of each DAGMC cell ID key will be replaced with the - :class:`~openmc.Material` object in the value. If the value contains - multiple :class:`~openmc.Material` objects, each Material in the list - will be assigned to the corresponding instance of the cell. - - .. versionadded:: 0.15.1 """ def __init__(self, @@ -94,16 +79,12 @@ class DAGMCUniverse(openmc.UniverseBase): universe_id=None, name='', auto_geom_ids=False, - auto_mat_ids=False, - material_overrides=None): + auto_mat_ids=False): super().__init__(universe_id, name) # Initialize class attributes self.filename = filename self.auto_geom_ids = auto_geom_ids self.auto_mat_ids = auto_mat_ids - self._material_overrides = {} - if material_overrides is not None: - self.material_overrides = material_overrides def __repr__(self): string = super().__repr__() @@ -130,47 +111,17 @@ class DAGMCUniverse(openmc.UniverseBase): @property def material_overrides(self): - return self._material_overrides + raise AttributeError( + "DAGMCUniverse.material_overrides has been removed. Use " + "DAGMCCell objects added via add_cell() to manage per-cell " + "material assignments.") @material_overrides.setter def material_overrides(self, val): - cv.check_type('material overrides', val, Mapping) - for key, value in val.items(): - self.add_material_override(key, value) - - def replace_material_assignment(self, material_name: str, material: openmc.Material): - """Replace a material assignment within the DAGMC universe. - - Replace the material assignment of all cells filled with a material in - the DAGMC universe. The universe must be synchronized in an initialized - Model (see :meth:`~openmc.DAGMCUniverse.sync_dagmc_cells`) before - calling this method. - - .. versionadded:: 0.15.1 - - Parameters - ---------- - material_name : str - Material name to replace - material : openmc.Material - Material to replace the material_name with - - """ - if material_name not in self.material_names: - raise ValueError( - f"No material with name '{material_name}' found in the DAGMC universe") - - if not self.cells: - raise RuntimeError("This DAGMC universe has not been synchronized " - "on an initialized Model.") - - for cell in self.cells.values(): - if cell.fill is None: - continue - if isinstance(cell.fill, openmc.Iterable): - cell.fill = list(map(lambda x: material if x.name == material_name else x, cell.fill)) - else: - cell.fill = material if cell.fill.name == material_name else cell.fill + raise AttributeError( + "DAGMCUniverse.material_overrides has been removed. Use " + "DAGMCCell objects added via add_cell() to manage per-cell " + "material assignments.") def add_material_override(self, key, overrides=None): """Add a material override to the universe. @@ -201,7 +152,10 @@ class DAGMCUniverse(openmc.UniverseBase): if key not in self.cells: raise ValueError(f"Cell ID '{key}' not found in DAGMC universe") - self._material_overrides[key] = overrides + if len(overrides) == 1: + self.cells[key].fill = overrides[0] + else: + self.cells[key].fill = list(overrides) @property def auto_geom_ids(self): @@ -223,21 +177,19 @@ class DAGMCUniverse(openmc.UniverseBase): @property def material_names(self): - dagmc_file_contents = h5py.File(self.filename) - material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( - 'values') material_tags_ascii = [] - for tag in material_tags_hex: - candidate_tag = tag.tobytes().decode().replace('\x00', '') - # tags might be for temperature or reflective surfaces - if candidate_tag.startswith('mat:'): - # if name ends with _comp remove it, it is not parsed - if candidate_tag.endswith('_comp'): - candidate_tag = candidate_tag[:-5] - # removes first 4 characters as openmc.Material name should be - # set without the 'mat:' part of the tag - material_tags_ascii.append(candidate_tag[4:]) - + with h5py.File(self.filename) as dagmc_file_contents: + material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get('values') + for tag in material_tags_hex: + candidate_tag = tag.tobytes().decode().replace('\x00', '') + # tags might be for temperature or reflective surfaces + if candidate_tag.startswith('mat:'): + # if name ends with _comp remove it, it is not parsed + if candidate_tag.endswith('_comp'): + candidate_tag = candidate_tag[:-5] + # removes first 4 characters as openmc.Material name should be + # set without the 'mat:' part of the tag + material_tags_ascii.append(candidate_tag[4:]) return sorted(set(material_tags_ascii)) def _n_geom_elements(self, geom_type): @@ -292,32 +244,21 @@ class DAGMCUniverse(openmc.UniverseBase): memo.add(self) - # Ensure that the material overrides are up-to-date - for cell in self.cells.values(): - if cell.fill is None: - continue - self.add_material_override(cell, cell.fill) - # Set xml element values dagmc_element = ET.Element('dagmc_universe') dagmc_element.set('id', str(self.id)) + if self.name: + dagmc_element.set('name', self.name) if self.auto_geom_ids: dagmc_element.set('auto_geom_ids', 'true') if self.auto_mat_ids: dagmc_element.set('auto_mat_ids', 'true') dagmc_element.set('filename', str(self.filename)) - if self._material_overrides: - mat_element = ET.Element('material_overrides') - for key in self._material_overrides: - cell_overrides = ET.Element('cell_override') - cell_overrides.set("id", str(key)) - material_element = ET.Element('material_ids') - material_element.text = ' '.join( - str(t.id) for t in self._material_overrides[key]) - cell_overrides.append(material_element) - mat_element.append(cell_overrides) - dagmc_element.append(mat_element) + if self.cells: + for cell in self.cells.values(): + cell_element = cell.create_xml_subelement(xml_element, memo) + dagmc_element.append(cell_element) xml_element.append(dagmc_element) def bounding_region( @@ -442,7 +383,7 @@ class DAGMCUniverse(openmc.UniverseBase): return out @classmethod - def from_xml_element(cls, elem, mats = None): + def from_xml_element(cls, elem, mats=None): """Generate DAGMC universe from XML element Parameters @@ -471,21 +412,56 @@ class DAGMCUniverse(openmc.UniverseBase): out.auto_geom_ids = bool(get_text(elem, "auto_geom_ids")) out.auto_mat_ids = bool(get_text(elem, "auto_mat_ids")) - el_mat_override = elem.find('material_overrides') - if el_mat_override is not None: - if mats is None: - raise ValueError("Material overrides found in DAGMC universe " - "but no materials were provided to populate " - "the mapping.") - out._material_overrides = {} - for elem in el_mat_override.findall('cell_override'): - cell_id = int(get_text(elem, 'id')) - mat_ids = get_elem_list(elem, "material_ids", str) or [] - mat_objs = [mats[mat_id] for mat_id in mat_ids] - out._material_overrides[cell_id] = mat_objs + has_overrides = elem.find('material_overrides') is not None + has_cells = elem.find('cell') is not None + + if has_overrides and has_cells: + raise ValueError( + "DAGMCUniverse cannot specify both and " + " sub-elements. Use elements only.") + + if has_overrides: + warnings.warn( + "DAGMCUniverse is deprecated and will be " + "removed in a future version. Use nested elements " + "instead.", DeprecationWarning, stacklevel=2) + out._parse_legacy_material_overrides(elem, mats) + elif has_cells: + out._parse_cell_overrides(elem, mats) return out + def _parse_legacy_material_overrides(self, elem, mats): + """Parse the deprecated XML format and populate + the universe with equivalent DAGMCCell objects.""" + if mats is None: + raise ValueError( + "DAGMC material overrides found but no materials were " + "provided to populate the mapping.") + mo_elem = elem.find('material_overrides') + for co_elem in mo_elem.findall('cell_override'): + cell_id = int(get_text(co_elem, 'id')) + mat_ids = co_elem.find('material_ids').text.split() + fill_objs = [mats[mid] for mid in mat_ids] + fill = fill_objs[0] if len(fill_objs) == 1 else fill_objs + if cell_id in self.cells: + raise ValueError( + f"Duplicate DAGMC cell override specified for cell {cell_id}.") + self.add_cell(DAGMCCell(cell_id=cell_id, fill=fill)) + + def _parse_cell_overrides(self, elem, mats): + if mats is None: + raise ValueError("DAGMC cell overrides found in DAGMC universe but " + "no materials were provided to populate the " + "mapping.") + + for cell_elem in elem.findall('cell'): + cell_id = int(get_text(cell_elem, 'id')) + if cell_id in self.cells: + raise ValueError( + f"Duplicate DAGMC cell override specified for cell {cell_id}.") + DAGMCCell.from_xml_element(cell_elem, mats, self) + def _partial_deepcopy(self): """Clone all of the openmc.DAGMCUniverse object's attributes except for its cells, as they are copied within the clone function. This should @@ -565,7 +541,13 @@ class DAGMCUniverse(openmc.UniverseBase): fill = [mats_per_id[mat.id] for mat in dag_cell.fill if mat] else: fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None - self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill)) + name = dag_cell.name + if dag_cell_id in self._cells: + self._cells[dag_cell_id].name = name + self._cells[dag_cell_id].fill = fill + else: + self.add_cell( + openmc.DAGMCCell(cell_id=dag_cell_id, name=name, fill=fill)) @add_plot_params def plot(self, *args, **kwargs): @@ -594,6 +576,14 @@ class DAGMCCell(openmc.Cell): DAG_parent_universe : int The parent universe of the cell. + Notes + ----- + DAGMC geometries are composed of triangulated surfaces, which means cell + volumes can in principle be computed exactly (e.g. via mesh-based + integration). Manually specifying :attr:`volume` overrides any such + calculation and may introduce inconsistencies if the value does not + accurately reflect the true geometric volume. + """ def __init__(self, cell_id=None, name='', fill=None): super().__init__(cell_id, name, fill, None) @@ -625,8 +615,62 @@ class DAGMCCell(openmc.Cell): raise TypeError("plot is not available for DAGMC cells.") def create_xml_subelement(self, xml_element, memo=None): - raise TypeError("create_xml_subelement is not available for DAGMC cells.") + if self.fill_type not in ('void', 'material', 'distribmat'): + raise TypeError("DAGMC cell overrides currently only support " + "material fills.") + if self.temperature is not None and self.fill_type not in ( + 'material', 'distribmat' + ): + raise TypeError("DAGMC cell temperature overrides require a " + "material fill.") + if self.density is not None and self.fill_type not in ('material', 'distribmat'): + raise TypeError("DAGMC cell density overrides require a " + "material fill.") + if any(getattr(self, attr) is not None for attr in ('translation', 'rotation')): + raise TypeError("DAGMC cell overrides do not support translation " + "or rotation.") + return super().create_xml_subelement(xml_element, memo) @classmethod - def from_xml_element(cls, elem, surfaces, materials, get_universe): - raise TypeError("from_xml_element is not available for DAGMC cells.") + def from_xml_element(cls, elem, mats, universe): + """Generate a DAGMCCell from an XML override element. + + Parameters + ---------- + elem : lxml.etree._Element + `` element containing a DAGMC cell property override + mats : dict + Dictionary mapping material ID strings to :class:`openmc.Material` + instances + universe : DAGMCUniverse + Universe to add the parsed cell to. + + Returns + ------- + DAGMCCell + DAGMCCell instance + """ + if not isinstance(universe, DAGMCUniverse): + raise TypeError( + f"universe must be a DAGMCUniverse instance, " + f"got {type(universe).__name__}.") + + cell_id = int(get_text(elem, 'id')) + + # Validate attributes that are unsupported for DAGMC cell overrides + for tag in ('region', 'fill', 'universe'): + if get_text(elem, tag) is not None: + raise ValueError( + f"DAGMC cell {cell_id} override cannot specify '{tag}'.") + for tag in ('translation', 'rotation'): + if get_text(elem, tag) is not None: + raise ValueError( + f"DAGMC cell {cell_id} override does not support " + f"'{tag}'.") + if get_elem_list(elem, 'material', str) is None: + raise ValueError( + f"DAGMC cell {cell_id} must specify a material override.") + + return super().from_xml_element( + elem, surfaces={}, materials=mats, + get_universe=lambda _: universe) diff --git a/openmc/data/__init__.py b/openmc/data/__init__.py index c2d35565a..9b38d758e 100644 --- a/openmc/data/__init__.py +++ b/openmc/data/__init__.py @@ -33,5 +33,8 @@ from .resonance_covariance import * from .multipole import * from .grid import * from .function import * +from .vectfit import * -from .effective_dose.dose import dose_coefficients +from .dose.dose import dose_coefficients +from .dose.mass_attenuation import \ + mass_energy_absorption_coefficient, mass_attenuation_coefficient diff --git a/openmc/data/ace.py b/openmc/data/ace.py index 6ccb76c92..d3de6a192 100644 --- a/openmc/data/ace.py +++ b/openmc/data/ace.py @@ -24,7 +24,7 @@ import numpy as np import openmc.checkvalue as cv from openmc.mixin import EqualityMixin from .data import ATOMIC_SYMBOL, gnds_name, EV_PER_MEV, K_BOLTZMANN -from .endf import ENDF_FLOAT_RE +from endf.records import ENDF_FLOAT_RE def get_metadata(zaid, metastable_scheme='nndc'): diff --git a/openmc/data/angle_distribution.py b/openmc/data/angle_distribution.py index e59ffa0c7..778139cc2 100644 --- a/openmc/data/angle_distribution.py +++ b/openmc/data/angle_distribution.py @@ -10,8 +10,8 @@ from openmc.mixin import EqualityMixin from openmc.stats import Univariate, Tabular, Uniform, Legendre from .function import INTERPOLATION_SCHEME from .data import EV_PER_MEV -from .endf import get_head_record, get_cont_record, get_tab1_record, \ - get_list_record, get_tab2_record +from .endf import as_evaluation, get_head_record, get_cont_record, \ + get_tab1_record, get_list_record, get_tab2_record class AngleDistribution(EqualityMixin): @@ -213,7 +213,7 @@ class AngleDistribution(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation mt : int The MT value of the reaction to get angular distributions for @@ -224,6 +224,7 @@ class AngleDistribution(EqualityMixin): Angular distribution """ + ev = as_evaluation(ev) file_obj = StringIO(ev.section[4, mt]) # Read HEAD record diff --git a/openmc/data/data.py b/openmc/data/data.py index 5ecadd37b..6e4840990 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -1,11 +1,26 @@ +from __future__ import annotations + import itertools import json import os import re from pathlib import Path from math import sqrt, log +from typing import TYPE_CHECKING, Literal from warnings import warn +from endf.data import (ATOMIC_NUMBER, ATOMIC_SYMBOL, ELEMENT_SYMBOL, + EV_PER_MEV, K_BOLTZMANN, gnds_name, zam) + +import openmc +from openmc.checkvalue import PathLike + +if TYPE_CHECKING: + from openmc.deplete import Chain + +gnds_name.__module__ = __name__ +zam.__module__ = __name__ + # Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions # of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3), # pp. 293-306 (2013). The "representative isotopic abundance" values from @@ -112,72 +127,6 @@ NATURAL_ABUNDANCE = { 'U238': 0.992742 } -# Dictionary to give element symbols from IUPAC names -# (and some common mispellings) -ELEMENT_SYMBOL = {'neutron': 'n', 'hydrogen': 'H', 'helium': 'He', - 'lithium': 'Li', 'beryllium': 'Be', 'boron': 'B', - 'carbon': 'C', 'nitrogen': 'N', 'oxygen': 'O', 'fluorine': 'F', - 'neon': 'Ne', 'sodium': 'Na', 'magnesium': 'Mg', - 'aluminium': 'Al', 'aluminum': 'Al', 'silicon': 'Si', - 'phosphorus': 'P', 'sulfur': 'S', 'sulphur': 'S', - 'chlorine': 'Cl', 'argon': 'Ar', 'potassium': 'K', - 'calcium': 'Ca', 'scandium': 'Sc', 'titanium': 'Ti', - 'vanadium': 'V', 'chromium': 'Cr', 'manganese': 'Mn', - 'iron': 'Fe', 'cobalt': 'Co', 'nickel': 'Ni', 'copper': 'Cu', - 'zinc': 'Zn', 'gallium': 'Ga', 'germanium': 'Ge', - 'arsenic': 'As', 'selenium': 'Se', 'bromine': 'Br', - 'krypton': 'Kr', 'rubidium': 'Rb', 'strontium': 'Sr', - 'yttrium': 'Y', 'zirconium': 'Zr', 'niobium': 'Nb', - 'molybdenum': 'Mo', 'technetium': 'Tc', 'ruthenium': 'Ru', - 'rhodium': 'Rh', 'palladium': 'Pd', 'silver': 'Ag', - 'cadmium': 'Cd', 'indium': 'In', 'tin': 'Sn', 'antimony': 'Sb', - 'tellurium': 'Te', 'iodine': 'I', 'xenon': 'Xe', - 'caesium': 'Cs', 'cesium': 'Cs', 'barium': 'Ba', - 'lanthanum': 'La', 'cerium': 'Ce', 'praseodymium': 'Pr', - 'neodymium': 'Nd', 'promethium': 'Pm', 'samarium': 'Sm', - 'europium': 'Eu', 'gadolinium': 'Gd', 'terbium': 'Tb', - 'dysprosium': 'Dy', 'holmium': 'Ho', 'erbium': 'Er', - 'thulium': 'Tm', 'ytterbium': 'Yb', 'lutetium': 'Lu', - 'hafnium': 'Hf', 'tantalum': 'Ta', 'tungsten': 'W', - 'wolfram': 'W', 'rhenium': 'Re', 'osmium': 'Os', - 'iridium': 'Ir', 'platinum': 'Pt', 'gold': 'Au', - 'mercury': 'Hg', 'thallium': 'Tl', 'lead': 'Pb', - 'bismuth': 'Bi', 'polonium': 'Po', 'astatine': 'At', - 'radon': 'Rn', 'francium': 'Fr', 'radium': 'Ra', - 'actinium': 'Ac', 'thorium': 'Th', 'protactinium': 'Pa', - 'uranium': 'U', 'neptunium': 'Np', 'plutonium': 'Pu', - 'americium': 'Am', 'curium': 'Cm', 'berkelium': 'Bk', - 'californium': 'Cf', 'einsteinium': 'Es', 'fermium': 'Fm', - 'mendelevium': 'Md', 'nobelium': 'No', 'lawrencium': 'Lr', - 'rutherfordium': 'Rf', 'dubnium': 'Db', 'seaborgium': 'Sg', - 'bohrium': 'Bh', 'hassium': 'Hs', 'meitnerium': 'Mt', - 'darmstadtium': 'Ds', 'roentgenium': 'Rg', 'copernicium': 'Cn', - 'nihonium': 'Nh', 'flerovium': 'Fl', 'moscovium': 'Mc', - 'livermorium': 'Lv', 'tennessine': 'Ts', 'oganesson': 'Og'} - -ATOMIC_SYMBOL = {0: 'n', 1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', - 7: 'N', 8: 'O', 9: 'F', 10: 'Ne', 11: 'Na', 12: 'Mg', 13: 'Al', - 14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar', 19: 'K', - 20: 'Ca', 21: 'Sc', 22: 'Ti', 23: 'V', 24: 'Cr', 25: 'Mn', - 26: 'Fe', 27: 'Co', 28: 'Ni', 29: 'Cu', 30: 'Zn', 31: 'Ga', - 32: 'Ge', 33: 'As', 34: 'Se', 35: 'Br', 36: 'Kr', 37: 'Rb', - 38: 'Sr', 39: 'Y', 40: 'Zr', 41: 'Nb', 42: 'Mo', 43: 'Tc', - 44: 'Ru', 45: 'Rh', 46: 'Pd', 47: 'Ag', 48: 'Cd', 49: 'In', - 50: 'Sn', 51: 'Sb', 52: 'Te', 53: 'I', 54: 'Xe', 55: 'Cs', - 56: 'Ba', 57: 'La', 58: 'Ce', 59: 'Pr', 60: 'Nd', 61: 'Pm', - 62: 'Sm', 63: 'Eu', 64: 'Gd', 65: 'Tb', 66: 'Dy', 67: 'Ho', - 68: 'Er', 69: 'Tm', 70: 'Yb', 71: 'Lu', 72: 'Hf', 73: 'Ta', - 74: 'W', 75: 'Re', 76: 'Os', 77: 'Ir', 78: 'Pt', 79: 'Au', - 80: 'Hg', 81: 'Tl', 82: 'Pb', 83: 'Bi', 84: 'Po', 85: 'At', - 86: 'Rn', 87: 'Fr', 88: 'Ra', 89: 'Ac', 90: 'Th', 91: 'Pa', - 92: 'U', 93: 'Np', 94: 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk', - 98: 'Cf', 99: 'Es', 100: 'Fm', 101: 'Md', 102: 'No', - 103: 'Lr', 104: 'Rf', 105: 'Db', 106: 'Sg', 107: 'Bh', - 108: 'Hs', 109: 'Mt', 110: 'Ds', 111: 'Rg', 112: 'Cn', - 113: 'Nh', 114: 'Fl', 115: 'Mc', 116: 'Lv', 117: 'Ts', - 118: 'Og'} -ATOMIC_NUMBER = {value: key for key, value in ATOMIC_SYMBOL.items()} - DADZ = { '(n,2nd)': (-3, -1), '(n,2n)': (-1, 0), @@ -268,11 +217,7 @@ DADZ = { # Values here are from the Committee on Data for Science and Technology # (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). -# The value of the Boltzman constant in units of eV / K -K_BOLTZMANN = 8.617333262e-5 - # Unit conversions -EV_PER_MEV = 1.0e6 JOULE_PER_EV = 1.602176634e-19 # Avogadro's constant @@ -284,9 +229,6 @@ NEUTRON_MASS = 1.00866491595 # Used in atomic_mass function as a cache _ATOMIC_MASS: dict[str, float] = {} -# Regex for GNDS nuclide names (used in zam function) -_GNDS_NAME_RE = re.compile(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)') - # Used in half_life function as a cache _HALF_LIFE: dict[str, float] = {} _LOG_TWO = log(2.0) @@ -363,25 +305,47 @@ def atomic_weight(element): raise ValueError(f"No naturally-occurring isotopes for element '{element}'.") -def half_life(isotope): +def half_life( + isotope: str, + chain_file: Literal[False] | None | PathLike | Chain = False +) -> float | None: """Return half-life of isotope in seconds or None if isotope is stable - Half-life values are from the `ENDF/B-VIII.0 decay sublibrary - `_. + By default, half-life values are from the `ENDF/B-VIII.0 decay sublibrary + `_. A depletion chain can + also be used as the source of half-life values. .. versionadded:: 0.13.1 + .. versionchanged:: 0.15.4 + Added the ``chain_file`` argument. + Parameters ---------- isotope : str Name of isotope, e.g., 'Pu239' + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For ``None`` + or an explicit chain, nuclides absent from the chain fall back to + ENDF/B-VIII.0 data. Returns ------- - float - Half-life of isotope in [s] + float or None + Half-life of isotope in [s], or None if the isotope is stable """ + if chain_file is not False: + if chain_file is not None or openmc.config.get('chain_file') is not None: + # Local import avoids a circular dependency + from openmc.deplete.chain import _get_chain + chain = _get_chain(chain_file) + if isotope in chain: + return chain[isotope].half_life + global _HALF_LIFE if not _HALF_LIFE: # Load ENDF/B-VIII.0 data from JSON file @@ -391,7 +355,10 @@ def half_life(isotope): return _HALF_LIFE.get(isotope.lower()) -def decay_constant(isotope): +def decay_constant( + isotope: str, + chain_file: Literal[False] | None | PathLike | Chain = False +) -> float: """Return decay constant of isotope in [s^-1] Decay constants are based on half-life values from the @@ -400,10 +367,20 @@ def decay_constant(isotope): .. versionadded:: 0.13.1 + .. versionchanged:: 0.15.4 + Added the ``chain_file`` argument. + Parameters ---------- isotope : str Name of isotope, e.g., 'Pu239' + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For ``None`` + or an explicit chain, nuclides absent from the chain fall back to + ENDF/B-VIII.0 data. Returns ------- @@ -415,7 +392,7 @@ def decay_constant(isotope): openmc.data.half_life """ - t = half_life(isotope) + t = half_life(isotope, chain_file) return _LOG_TWO / t if t else 0.0 @@ -523,33 +500,6 @@ def water_density(temperature, pressure=0.1013): return coeff / pi / gamma1_pi -def gnds_name(Z, A, m=0): - """Return nuclide name using GNDS convention - - .. versionchanged:: 0.14.0 - Function name changed from ``gnd_name`` to ``gnds_name`` - - Parameters - ---------- - Z : int - Atomic number - A : int - Mass number - m : int, optional - Metastable state - - Returns - ------- - str - Nuclide name in GNDS convention, e.g., 'Am242_m1' - - """ - if m > 0: - return f'{ATOMIC_SYMBOL[Z]}{A}_m{m}' - return f'{ATOMIC_SYMBOL[Z]}{A}' - - - def _get_element_symbol(element: str) -> str: if len(element) > 2: symbol = ELEMENT_SYMBOL.get(element.lower()) @@ -590,30 +540,3 @@ def isotopes(element: str) -> list[tuple[str, float]]: result.append(kv) return result - - -def zam(name): - """Return tuple of (atomic number, mass number, metastable state) - - Parameters - ---------- - name : str - Name of nuclide using GNDS convention, e.g., 'Am242_m1' - - Returns - ------- - 3-tuple of int - Atomic number, mass number, and metastable state - - """ - try: - symbol, A, state = _GNDS_NAME_RE.fullmatch(name).groups() - except AttributeError: - raise ValueError(f"'{name}' does not appear to be a nuclide name in " - "GNDS format") - - if symbol not in ATOMIC_NUMBER: - raise ValueError(f"'{symbol}' is not a recognized element symbol") - - metastable = int(state[2:]) if state else 0 - return (ATOMIC_NUMBER[symbol], int(A), metastable) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index c8a0bb5e7..5f95e1281 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -2,7 +2,6 @@ from collections.abc import Iterable from functools import cached_property from io import StringIO from math import log -import re from warnings import warn import numpy as np @@ -13,9 +12,10 @@ import openmc.checkvalue as cv from openmc.exceptions import DataError from openmc.mixin import EqualityMixin from openmc.stats import Discrete, Tabular, Univariate, combine_distributions -from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER +from .data import gnds_name, zam from .function import INTERPOLATION_SCHEME -from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record +from .endf import ( + as_evaluation, get_head_record, get_list_record, get_tab1_record) # Gives name and (change in A, change in Z) resulting from decay @@ -76,7 +76,7 @@ class FissionProductYields(EqualityMixin): Parameters ---------- - ev_or_filename : str of openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF fission product yield evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -126,9 +126,7 @@ class FissionProductYields(EqualityMixin): for j in range(n_products): Z, A = divmod(int(values[4*j]), 1000) isomeric_state = int(values[4*j + 1]) - name = ATOMIC_SYMBOL[Z] + str(A) - if isomeric_state > 0: - name += f'_m{isomeric_state}' + name = gnds_name(Z, A, isomeric_state) yield_j = ufloat(values[4*j + 2], values[4*j + 3]) yields[name] = yield_j @@ -136,11 +134,7 @@ class FissionProductYields(EqualityMixin): return energies, data - # Get evaluation if str is passed - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) # Assign basic nuclide properties self.nuclide = { @@ -167,7 +161,7 @@ class FissionProductYields(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF fission product yield evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -243,9 +237,7 @@ class DecayMode(EqualityMixin): @property def daughter(self): # Determine atomic number and mass number of parent - symbol, A = re.match(r'([A-Zn][a-z]*)(\d+)', self.parent).groups() - A = int(A) - Z = ATOMIC_NUMBER[symbol] + Z, A, _ = zam(self.parent) # Process changes for mode in self.modes: @@ -255,11 +247,11 @@ class DecayMode(EqualityMixin): delta_A, delta_Z = changes A += delta_A Z += delta_Z + break + else: + return None - if self._daughter_state > 0: - return f'{ATOMIC_SYMBOL[Z]}{A}_m{self._daughter_state}' - else: - return f'{ATOMIC_SYMBOL[Z]}{A}' + return gnds_name(Z, A, self._daughter_state) @property def parent(self): @@ -297,7 +289,7 @@ class Decay(EqualityMixin): Parameters ---------- - ev_or_filename : str of openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF radioactive decay data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -328,11 +320,7 @@ class Decay(EqualityMixin): """ def __init__(self, ev_or_filename): - # Get evaluation if str is passed - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) file_obj = StringIO(ev.section[8, 457]) @@ -348,10 +336,7 @@ class Decay(EqualityMixin): self.nuclide['atomic_number'] = Z self.nuclide['mass_number'] = A self.nuclide['isomeric_state'] = metastable - if metastable > 0: - self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}_m{metastable}' - else: - self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}' + self.nuclide['name'] = gnds_name(Z, A, metastable) self.nuclide['mass'] = items[1] # AWR self.nuclide['excited_state'] = items[2] # State of the original nuclide self.nuclide['stable'] = (items[4] == 1) # Nucleus stability flag @@ -494,7 +479,7 @@ class Decay(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF radioactive decay data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -657,5 +642,3 @@ def decay_energy(nuclide: str): warn(f"Chain file '{chain_file}' does not have any decay energy.") return _DECAY_ENERGY.get(nuclide, 0.0) - - diff --git a/openmc/data/effective_dose/__init__.py b/openmc/data/dose/__init__.py similarity index 100% rename from openmc/data/effective_dose/__init__.py rename to openmc/data/dose/__init__.py diff --git a/openmc/data/dose/dose.py b/openmc/data/dose/dose.py new file mode 100644 index 000000000..87e0cf9ae --- /dev/null +++ b/openmc/data/dose/dose.py @@ -0,0 +1,146 @@ +from pathlib import Path + +import numpy as np + +import openmc.checkvalue as cv + +_FULL_GEOMETRIES = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') +_LIMITED_GEOMETRIES = ('AP', 'PA', 'ISO') + +_TABLES = { + ('icrp74', 'effective', 'neutron'): ( + Path('icrp74') / 'neutrons.txt', _FULL_GEOMETRIES), + ('icrp74', 'effective', 'photon'): ( + Path('icrp74') / 'photons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'electron'): ( + Path('icrp116') / 'electrons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'helium'): ( + Path('icrp116') / 'helium_ions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'mu-'): ( + Path('icrp116') / 'negative_muons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'pi-'): ( + Path('icrp116') / 'negative_pions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'neutron'): ( + Path('icrp116') / 'neutrons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'photon'): ( + Path('icrp116') / 'photons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'photon kerma'): ( + Path('icrp116') / 'photons_kerma.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'mu+'): ( + Path('icrp116') / 'positive_muons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'pi+'): ( + Path('icrp116') / 'positive_pions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'positron'): ( + Path('icrp116') / 'positrons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'proton'): ( + Path('icrp116') / 'protons.txt', _FULL_GEOMETRIES), + ('icrp74', 'ambient', 'neutron'): ( + Path('icrp74') / 'neutrons_H10.txt', None), + ('icrp74', 'ambient', 'photon'): ( + Path('icrp74') / 'photons_H10.txt', None), +} + +_DOSE_TABLES = {} + + +def _load_dose_table(data_source: str, dose_quantity: str, particle: str): + """Load dose tables from text files. + + Parameters + ---------- + data_source : {'icrp74', 'icrp116'} + The dose conversion data source to use + dose_quantity : {'effective', 'ambient'} + Dose quantity to load. 'ambient' corresponds to ambient dose + equivalent H*(10). + particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} + Incident particle + + """ + key = (data_source, dose_quantity, particle) + path = Path(__file__).parent / _TABLES[key][0] + data = np.loadtxt(path, skiprows=3, encoding='utf-8') + data[:, 0] *= 1e6 # Change energies to eV + _DOSE_TABLES[key] = data + + +def dose_coefficients( + particle, geometry='AP', data_source='icrp116', dose_quantity='effective' +): + """Return dose conversion coefficients. + + This function provides fluence (and air kerma) to effective dose or ambient + dose equivalent (H*(10)) conversion coefficients for various types of + external exposures based on values in ICRP publications. Corrected values + found in a corrigendum are used rather than the values in the original + report. Available libraries include `ICRP Publication 74 + ` and `ICRP Publication 116 + `. + + For ICRP 74 data, the photon effective dose per fluence is determined by + multiplying the air kerma per fluence values (Table A.1) by the effective + dose per air kerma (Table A.17). The neutron effective dose per fluence is + found in Table A.41. For ICRP 116 data, the photon effective dose per + fluence is found in Table A.1 and the neutron effective dose per fluence is + found in Table A.5. + + Parameters + ---------- + particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} + Incident particle + geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} + Irradiation geometry assumed for effective dose coefficients. Refer to + ICRP-116 (Section 3.2) for the meaning of the options here. This + argument does not apply when ``dose_quantity`` is 'ambient'. + data_source : {'icrp74', 'icrp116'} + The data source for the dose conversion coefficients. + dose_quantity : {'effective', 'ambient'} + Dose quantity to return. 'effective' returns effective dose + coefficients; 'ambient' returns ambient dose equivalent (H*(10)) + coefficients. + + Returns + ------- + energy : numpy.ndarray + Energies at which dose conversion coefficients are given + dose_coeffs : numpy.ndarray + Dose coefficients in [pSv cm^2] at provided energies. For 'photon + kerma', the coefficients are given in [Sv/Gy]. + + """ + + cv.check_value('geometry', geometry, _FULL_GEOMETRIES) + cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) + cv.check_value('dose_quantity', dose_quantity, {'effective', 'ambient'}) + + key = (data_source, dose_quantity, particle) + if key not in _TABLES: + available_particles = sorted( + p for ds, dq, p in _TABLES + if ds == data_source and dq == dose_quantity + ) + msg = ( + f"'{particle}' has no {dose_quantity} dose data in data source " + f"{data_source}. Available particles for {data_source} " + f"with dose quantity {dose_quantity} are: {available_particles}" + ) + raise ValueError(msg) + elif key not in _DOSE_TABLES: + _load_dose_table(data_source, dose_quantity, particle) + + data = _DOSE_TABLES[key] + columns = _TABLES[key][1] + + if columns is None: + if geometry != 'AP': + raise ValueError( + "Irradiation geometry is not defined for ambient dose " + "equivalent coefficients. Use the default geometry='AP'." + ) + index = 0 + else: + index = columns.index(geometry) + + energy = data[:, 0].copy() + dose_coeffs = data[:, index + 1].copy() + return energy, dose_coeffs diff --git a/openmc/data/effective_dose/icrp116/electrons.txt b/openmc/data/dose/icrp116/electrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/electrons.txt rename to openmc/data/dose/icrp116/electrons.txt diff --git a/openmc/data/effective_dose/icrp116/helium_ions.txt b/openmc/data/dose/icrp116/helium_ions.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/helium_ions.txt rename to openmc/data/dose/icrp116/helium_ions.txt diff --git a/openmc/data/effective_dose/icrp116/negative_muons.txt b/openmc/data/dose/icrp116/negative_muons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/negative_muons.txt rename to openmc/data/dose/icrp116/negative_muons.txt diff --git a/openmc/data/effective_dose/icrp116/negative_pions.txt b/openmc/data/dose/icrp116/negative_pions.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/negative_pions.txt rename to openmc/data/dose/icrp116/negative_pions.txt diff --git a/openmc/data/effective_dose/icrp116/neutrons.txt b/openmc/data/dose/icrp116/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/neutrons.txt rename to openmc/data/dose/icrp116/neutrons.txt diff --git a/openmc/data/effective_dose/icrp116/photons.txt b/openmc/data/dose/icrp116/photons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/photons.txt rename to openmc/data/dose/icrp116/photons.txt diff --git a/openmc/data/effective_dose/icrp116/photons_kerma.txt b/openmc/data/dose/icrp116/photons_kerma.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/photons_kerma.txt rename to openmc/data/dose/icrp116/photons_kerma.txt diff --git a/openmc/data/effective_dose/icrp116/positive_muons.txt b/openmc/data/dose/icrp116/positive_muons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/positive_muons.txt rename to openmc/data/dose/icrp116/positive_muons.txt diff --git a/openmc/data/effective_dose/icrp116/positive_pions.txt b/openmc/data/dose/icrp116/positive_pions.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/positive_pions.txt rename to openmc/data/dose/icrp116/positive_pions.txt diff --git a/openmc/data/effective_dose/icrp116/positrons.txt b/openmc/data/dose/icrp116/positrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/positrons.txt rename to openmc/data/dose/icrp116/positrons.txt diff --git a/openmc/data/effective_dose/icrp116/protons.txt b/openmc/data/dose/icrp116/protons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/protons.txt rename to openmc/data/dose/icrp116/protons.txt diff --git a/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py b/openmc/data/dose/icrp74/generate_photon_effective_dose.py similarity index 100% rename from openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py rename to openmc/data/dose/icrp74/generate_photon_effective_dose.py diff --git a/openmc/data/effective_dose/icrp74/neutrons.txt b/openmc/data/dose/icrp74/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp74/neutrons.txt rename to openmc/data/dose/icrp74/neutrons.txt diff --git a/openmc/data/dose/icrp74/neutrons_H10.txt b/openmc/data/dose/icrp74/neutrons_H10.txt new file mode 100644 index 000000000..fe0036cbc --- /dev/null +++ b/openmc/data/dose/icrp74/neutrons_H10.txt @@ -0,0 +1,50 @@ +Neutrons: Ambient per fluence, in units of pSv cm², for monoenergetic particles incident. + +Energy (MeV) Dose + 1.00E-09 6.60 + 1.00E-08 9.00 + 2.53E-08 10.6 + 1.00E-07 12.9 + 2.00E-07 13.5 + 5.00E-07 13.6 + 1.00E-06 13.3 + 2.00E-06 12.9 + 5.00E-06 12.0 + 1.00E-05 11.3 + 2.00E-05 10.6 + 5.00E-05 9.90 + 1.00E-04 9.40 + 2.00E-04 8.90 + 5.00E-04 8.30 + 1.00E-03 7.90 + 2.00E-03 7.70 + 5.00E-03 8.00 + 1.00E-02 10.5 + 2.00E-02 16.6 + 3.00E-02 23.7 + 5.00E-02 41.1 + 7.00E-02 60.0 + 1.00E-01 88.0 + 1.50E-01 132 + 2.00E-01 170 + 3.00E-01 233 + 5.00E-01 322 + 7.00E-01 375 + 9.00E-01 400 + 1 416 + 1.2 425 + 2 420 + 3 412 + 4 408 + 5 405 + 6 400 + 7 405 + 8 409 + 9 420 + 10 440 + 12 480 + 14 520 + 15 540 + 16 555 + 18 570 + 20 600 \ No newline at end of file diff --git a/openmc/data/effective_dose/icrp74/photons.txt b/openmc/data/dose/icrp74/photons.txt similarity index 100% rename from openmc/data/effective_dose/icrp74/photons.txt rename to openmc/data/dose/icrp74/photons.txt diff --git a/openmc/data/dose/icrp74/photons_H10.txt b/openmc/data/dose/icrp74/photons_H10.txt new file mode 100644 index 000000000..66031b2f0 --- /dev/null +++ b/openmc/data/dose/icrp74/photons_H10.txt @@ -0,0 +1,28 @@ +Photons: Ambient dose (H*10) per fluence, in units of pSv cm² + +Energy (MeV) Dose +0.010 0.061 +0.015 0.83 +0.020 1.05 +0.030 0.81 +0.040 0.64 +0.050 0.55 +0.060 0.51 +0.080 0.53 +0.100 0.61 +0.150 0.89 +0.200 1.20 +0.300 1.80 +0.400 2.38 +0.500 2.93 +0.600 3.44 +0.800 4.38 +1 5.20 +1.5 6.90 +2 8.60 +3 11.1 +4 13.4 +5 15.5 +6 17.6 +8 21.6 +10 25.6 \ No newline at end of file diff --git a/openmc/data/dose/mass_attenuation.h5 b/openmc/data/dose/mass_attenuation.h5 new file mode 100644 index 000000000..f62785140 Binary files /dev/null and b/openmc/data/dose/mass_attenuation.h5 differ diff --git a/openmc/data/dose/mass_attenuation.py b/openmc/data/dose/mass_attenuation.py new file mode 100644 index 000000000..c4260480b --- /dev/null +++ b/openmc/data/dose/mass_attenuation.py @@ -0,0 +1,153 @@ +from pathlib import Path + +import numpy as np +import h5py + +import openmc.checkvalue as cv +from openmc.data import EV_PER_MEV +from ..data import ATOMIC_NUMBER +from ..function import Tabulated1D + +# Embedded NIST-126 data +# Air (Dry Near Sea Level) — NIST Standard Reference Database 126 Table 4 (doi: 10.18434/T4D01F) +# Columns: Energy (MeV), μ_en/ρ (cm^2/g) +_NIST126_AIR = np.array([ + [1.00000e-03, 3.599e03], + [1.50000e-03, 1.188e03], + [2.00000e-03, 5.262e02], + [3.00000e-03, 1.614e02], + [3.20290e-03, 1.330e02], + [3.20290e-03, 1.460e02], + [4.00000e-03, 7.636e01], + [5.00000e-03, 3.931e01], + [6.00000e-03, 2.270e01], + [8.00000e-03, 9.446e00], + [1.00000e-02, 4.742e00], + [1.50000e-02, 1.334e00], + [2.00000e-02, 5.389e-01], + [3.00000e-02, 1.537e-01], + [4.00000e-02, 6.833e-02], + [5.00000e-02, 4.098e-02], + [6.00000e-02, 3.041e-02], + [8.00000e-02, 2.407e-02], + [1.00000e-01, 2.325e-02], + [1.50000e-01, 2.496e-02], + [2.00000e-01, 2.672e-02], + [3.00000e-01, 2.872e-02], + [4.00000e-01, 2.949e-02], + [5.00000e-01, 2.966e-02], + [6.00000e-01, 2.953e-02], + [8.00000e-01, 2.882e-02], + [1.00000e00, 2.789e-02], + [1.25000e00, 2.666e-02], + [1.50000e00, 2.547e-02], + [2.00000e00, 2.345e-02], + [3.00000e00, 2.057e-02], + [4.00000e00, 1.870e-02], + [5.00000e00, 1.740e-02], + [6.00000e00, 1.647e-02], + [8.00000e00, 1.525e-02], + [1.00000e01, 1.450e-02], + [1.50000e01, 1.353e-02], + [2.00000e01, 1.311e-02], +]) + +# Registry of embedded tables: (data_source, material) -> ndarray +# Table shape: (N, 2) with columns [Energy (MeV), μen/ρ (cm^2/g)] +_MUEN_TABLES = { + ("nist126", "air"): _NIST126_AIR, +} + + +def mass_energy_absorption_coefficient( + material: str, data_source: str = "nist126" +) -> Tabulated1D: + r"""Return the mass energy-absorption coefficient as a function of energy. + + The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is + defined as the fraction of incident photon energy absorbed in a material per + unit mass less the energy carried away by scattered photons. It is obtained + from `NIST Standard Reference Database 126 + `_: X-Ray Mass Attenuation Coefficients. + + Parameters + ---------- + material : {'air'} + Material compound for which to load coefficients. + data_source : {'nist126'} + Source library. + + Returns + ------- + Tabulated1D + Mass energy-absorption coefficient [cm^2/g] as a function of photon + energy [eV], using log-log interpolation. + + """ + cv.check_value("material", material, {"air"}) + cv.check_value("data_source", data_source, {"nist126"}) + + key = (data_source, material) + if key not in _MUEN_TABLES: + available = sorted({m for (ds, m) in _MUEN_TABLES.keys() if ds == data_source}) + raise ValueError( + f"No mass energy-absorption data for '{material}' in data source " + f"'{data_source}'. Available materials: {available}" + ) + + data = _MUEN_TABLES[key] + energy = data[:, 0].copy() * EV_PER_MEV # MeV -> eV + mu_en_coeffs = data[:, 1].copy() + return Tabulated1D(energy, mu_en_coeffs, + breakpoints=[len(energy)], interpolation=[5]) + + +# Used in mass_attenuation_coefficient function as a cache. +# Maps atomic number Z (int) -> Tabulated1D of (mu/rho) [cm^2/g] vs E [eV] +_MASS_ATTENUATION: dict[int, object] = {} + + +def mass_attenuation_coefficient(element): + r"""Return the photon mass attenuation coefficient as a function of energy. + + The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is + defined as the fraction of incident photon energy absorbed in a material per + unit mass. Values for each element are obtained from `NIST Standard + Reference Database 8 `_: XCOM Photon Cross + Sections Database. + + Parameters + ---------- + element : str or int + Element symbol (e.g., 'Fe') or atomic number (e.g., 26). + + Returns + ------- + Tabulated1D + Mass attenuation coefficient [cm^2/g] as a function of photon energy + [eV], using log-log interpolation. + + """ + if not _MASS_ATTENUATION: + data_file = Path(__file__).with_name('mass_attenuation.h5') + with h5py.File(data_file, 'r') as f: + for key, dataset in f.items(): + energies, mu_rho = dataset[()] # shape (2, N) + _MASS_ATTENUATION[int(key)] = Tabulated1D( + energies, mu_rho, + breakpoints=[len(energies)], + interpolation=[5] # log-log + ) + + # Resolve element argument to atomic number + if isinstance(element, str): + if element not in ATOMIC_NUMBER: + raise ValueError(f"'{element}' is not a recognized element symbol") + Z = ATOMIC_NUMBER[element] + else: + Z = int(element) + + if Z not in _MASS_ATTENUATION: + raise ValueError(f"No mass attenuation data available for Z={Z}") + + return _MASS_ATTENUATION[Z] diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/effective_dose/dose.py deleted file mode 100644 index d49043b0a..000000000 --- a/openmc/data/effective_dose/dose.py +++ /dev/null @@ -1,107 +0,0 @@ -from pathlib import Path - -import numpy as np - -import openmc.checkvalue as cv - -_FILES = { - ('icrp74', 'neutron'): Path('icrp74') / 'neutrons.txt', - ('icrp74', 'photon'): Path('icrp74') / 'photons.txt', - ('icrp116', 'electron'): Path('icrp116') / 'electrons.txt', - ('icrp116', 'helium'): Path('icrp116') / 'helium_ions.txt', - ('icrp116', 'mu-'): Path('icrp116') / 'negative_muons.txt', - ('icrp116', 'pi-'): Path('icrp116') / 'negative_pions.txt', - ('icrp116', 'neutron'): Path('icrp116') / 'neutrons.txt', - ('icrp116', 'photon'): Path('icrp116') / 'photons.txt', - ('icrp116', 'photon kerma'): Path('icrp116') / 'photons_kerma.txt', - ('icrp116', 'mu+'): Path('icrp116') / 'positive_muons.txt', - ('icrp116', 'pi+'): Path('icrp116') / 'positive_pions.txt', - ('icrp116', 'positron'): Path('icrp116') / 'positrons.txt', - ('icrp116', 'proton'): Path('icrp116') / 'protons.txt', -} - -_DOSE_TABLES = {} - - -def _load_dose_icrp(data_source: str, particle: str): - """Load effective dose tables from text files. - - Parameters - ---------- - data_source : {'icrp74', 'icrp116'} - The dose conversion data source to use - particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} - Incident particle - - """ - path = Path(__file__).parent / _FILES[data_source, particle] - data = np.loadtxt(path, skiprows=3, encoding='utf-8') - data[:, 0] *= 1e6 # Change energies to eV - _DOSE_TABLES[data_source, particle] = data - - -def dose_coefficients(particle, geometry='AP', data_source='icrp116'): - """Return effective dose conversion coefficients. - - This function provides fluence (and air kerma) to effective or ambient dose - (H*(10)) conversion coefficients for various types of external exposures - based on values in ICRP publications. Corrected values found in a - corrigendum are used rather than the values in the original report. - Available libraries include `ICRP Publication 74 - ` and `ICRP Publication 116 - `. - - For ICRP 74 data, the photon effective dose per fluence is determined by - multiplying the air kerma per fluence values (Table A.1) by the effective - dose per air kerma (Table A.17). The neutron effective dose per fluence is - found in Table A.41. For ICRP 116 data, the photon effective dose per - fluence is found in Table A.1 and the neutron effective dose per fluence is - found in Table A.5. - - Parameters - ---------- - particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} - Incident particle - geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} - Irradiation geometry assumed. Refer to ICRP-116 (Section 3.2) for the - meaning of the options here. - data_source : {'icrp74', 'icrp116'} - The data source for the effective dose conversion coefficients. - - Returns - ------- - energy : numpy.ndarray - Energies at which dose conversion coefficients are given - dose_coeffs : numpy.ndarray - Effective dose coefficients in [pSv cm^2] at provided energies. For - 'photon kerma', the coefficients are given in [Sv/Gy]. - - """ - - cv.check_value('geometry', geometry, {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'}) - cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) - - if (data_source, particle) not in _FILES: - available_particles = sorted({p for (ds, p) in _FILES if ds == data_source}) - msg = ( - f"'{particle}' has no dose data in data source {data_source}. " - f"Available particles for {data_source} are: {available_particles}" - ) - raise ValueError(msg) - elif (data_source, particle) not in _DOSE_TABLES: - _load_dose_icrp(data_source, particle) - - # Get all data for selected particle - data = _DOSE_TABLES[data_source, particle] - - # Determine index for selected geometry - if particle in ('neutron', 'photon', 'proton', 'photon kerma'): - columns = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') - else: - columns = ('AP', 'PA', 'ISO') - index = columns.index(geometry) - - # Pull out energy and dose from table - energy = data[:, 0].copy() - dose_coeffs = data[:, index + 1].copy() - return energy, dose_coeffs diff --git a/openmc/data/endf.py b/openmc/data/endf.py index eca374469..edd8afffb 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -10,350 +10,38 @@ import io from pathlib import PurePath import re -import numpy as np - from .data import gnds_name from .function import Tabulated1D -from endf.records import float_endf - - -_LIBRARY = {0: 'ENDF/B', 1: 'ENDF/A', 2: 'JEFF', 3: 'EFF', - 4: 'ENDF/B High Energy', 5: 'CENDL', 6: 'JENDL', - 17: 'TENDL', 18: 'ROSFOND', 21: 'SG-21', 31: 'INDL/V', - 32: 'INDL/A', 33: 'FENDL', 34: 'IRDF', 35: 'BROND', - 36: 'INGDB-90', 37: 'FENDL/A', 41: 'BROND'} - -_SUBLIBRARY = { - 0: 'Photo-nuclear data', - 1: 'Photo-induced fission product yields', - 3: 'Photo-atomic data', - 4: 'Radioactive decay data', - 5: 'Spontaneous fission product yields', - 6: 'Atomic relaxation data', - 10: 'Incident-neutron data', - 11: 'Neutron-induced fission product yields', - 12: 'Thermal neutron scattering data', - 19: 'Neutron standards', - 113: 'Electro-atomic data', - 10010: 'Incident-proton data', - 10011: 'Proton-induced fission product yields', - 10020: 'Incident-deuteron data', - 10030: 'Incident-triton data', - 20030: 'Incident-helion (3He) data', - 20040: 'Incident-alpha data' -} - -SUM_RULES = {1: [2, 3], - 3: [4, 5, 11, 16, 17, 22, 23, 24, 25, 27, 28, 29, 30, 32, 33, 34, 35, - 36, 37, 41, 42, 44, 45, 152, 153, 154, 156, 157, 158, 159, 160, - 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, - 173, 174, 175, 176, 177, 178, 179, 180, 181, 183, 184, 185, - 186, 187, 188, 189, 190, 194, 195, 196, 198, 199, 200], - 4: list(range(50, 92)), - 16: list(range(875, 892)), - 18: [19, 20, 21, 38], - 27: [18, 101], - 101: [102, 103, 104, 105, 106, 107, 108, 109, 111, 112, 113, 114, - 115, 116, 117, 155, 182, 191, 192, 193, 197], - 103: list(range(600, 650)), - 104: list(range(650, 700)), - 105: list(range(700, 750)), - 106: list(range(750, 800)), - 107: list(range(800, 850))} - -ENDF_FLOAT_RE = re.compile(r'([\s\-\+]?\d*\.\d+)([\+\-]) ?(\d+)') - - -def py_float_endf(s): - """Convert string of floating point number in ENDF to float. - - The ENDF-6 format uses an 'e-less' floating point number format, - e.g. -1.23481+10. Trying to convert using the float built-in won't work - because of the lack of an 'e'. This function allows such strings to be - converted while still allowing numbers that are not in exponential notation - to be converted as well. - - Parameters - ---------- - s : str - Floating-point number from an ENDF file - - Returns - ------- - float - The number - - """ - return float(ENDF_FLOAT_RE.sub(r'\1e\2\3', s)) - - -def int_endf(s): - """Convert string of integer number in ENDF to int. - - The ENDF-6 format technically allows integers to be represented by a field - of all blanks. This function acts like int(s) except when s is a string of - all whitespace, in which case zero is returned. - - Parameters - ---------- - s : str - Integer or spaces - - Returns - ------- - integer - The number or 0 - """ - return 0 if s.isspace() else int(s) - - -def get_text_record(file_obj): - """Return data from a TEXT record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - str - Text within the TEXT record - - """ - return file_obj.readline()[:66] - - -def get_cont_record(file_obj, skip_c=False): - """Return data from a CONT record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - skip_c : bool - Determine whether to skip the first two quantities (C1, C2) of the CONT - record. - - Returns - ------- - tuple - The six items within the CONT record - - """ - line = file_obj.readline() - if skip_c: - C1 = None - C2 = None - else: - C1 = float_endf(line[:11]) - C2 = float_endf(line[11:22]) - L1 = int_endf(line[22:33]) - L2 = int_endf(line[33:44]) - N1 = int_endf(line[44:55]) - N2 = int_endf(line[55:66]) - return (C1, C2, L1, L2, N1, N2) - - -def get_head_record(file_obj): - """Return data from a HEAD record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - tuple - The six items within the HEAD record - - """ - line = file_obj.readline() - ZA = int(float_endf(line[:11])) - AWR = float_endf(line[11:22]) - L1 = int_endf(line[22:33]) - L2 = int_endf(line[33:44]) - N1 = int_endf(line[44:55]) - N2 = int_endf(line[55:66]) - return (ZA, AWR, L1, L2, N1, N2) - - -def get_list_record(file_obj): - """Return data from a LIST record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - list - The six items within the header - list - The values within the list - - """ - # determine how many items are in list - items = get_cont_record(file_obj) - NPL = items[4] - - # read items - b = [] - for i in range((NPL - 1)//6 + 1): - line = file_obj.readline() - n = min(6, NPL - 6*i) - for j in range(n): - b.append(float_endf(line[11*j:11*(j + 1)])) - - return (items, b) +from endf.material import ( + Material, + _LIBRARY, + _SUBLIBRARY, + get_materials as get_evaluations, +) +from endf.incident_neutron import SUM_RULES +from endf.records import ( + float_endf, + py_float_endf, + int_endf, + get_text_record, + get_cont_record, + get_head_record, + get_list_record, + get_tab1_record as _get_tab1_record, + get_tab2_record, + get_intg_record, +) def get_tab1_record(file_obj): """Return data from a TAB1 record in an ENDF-6 file. - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - list - The six items within the header - openmc.data.Tabulated1D - The tabulated function - + This wraps the endf package's get_tab1_record to return an + openmc.data.Tabulated1D (which has HDF5 support and is a Function1D) + instead of endf.Tabulated1D. """ - # Determine how many interpolation regions and total points there are - line = file_obj.readline() - C1 = float_endf(line[:11]) - C2 = float_endf(line[11:22]) - L1 = int_endf(line[22:33]) - L2 = int_endf(line[33:44]) - n_regions = int_endf(line[44:55]) - n_pairs = int_endf(line[55:66]) - params = [C1, C2, L1, L2] - - # Read the interpolation region data, namely NBT and INT - breakpoints = np.zeros(n_regions, dtype=int) - interpolation = np.zeros(n_regions, dtype=int) - m = 0 - for i in range((n_regions - 1)//3 + 1): - line = file_obj.readline() - to_read = min(3, n_regions - m) - for j in range(to_read): - breakpoints[m] = int_endf(line[0:11]) - interpolation[m] = int_endf(line[11:22]) - line = line[22:] - m += 1 - - # Read tabulated pairs x(n) and y(n) - x = np.zeros(n_pairs) - y = np.zeros(n_pairs) - m = 0 - for i in range((n_pairs - 1)//3 + 1): - line = file_obj.readline() - to_read = min(3, n_pairs - m) - for j in range(to_read): - x[m] = float_endf(line[:11]) - y[m] = float_endf(line[11:22]) - line = line[22:] - m += 1 - - return params, Tabulated1D(x, y, breakpoints, interpolation) - - -def get_tab2_record(file_obj): - # Determine how many interpolation regions and total points there are - params = get_cont_record(file_obj) - n_regions = params[4] - - # Read the interpolation region data, namely NBT and INT - breakpoints = np.zeros(n_regions, dtype=int) - interpolation = np.zeros(n_regions, dtype=int) - m = 0 - for i in range((n_regions - 1)//3 + 1): - line = file_obj.readline() - to_read = min(3, n_regions - m) - for j in range(to_read): - breakpoints[m] = int(line[0:11]) - interpolation[m] = int(line[11:22]) - line = line[22:] - m += 1 - - return params, Tabulated2D(breakpoints, interpolation) - - -def get_intg_record(file_obj): - """ - Return data from an INTG record in an ENDF-6 file. Used to store the - covariance matrix in a compact format. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - numpy.ndarray - The correlation matrix described in the INTG record - """ - # determine how many items are in list and NDIGIT - items = get_cont_record(file_obj) - ndigit = items[2] - npar = items[3] # Number of parameters - nlines = items[4] # Lines to read - NROW_RULES = {2: 18, 3: 12, 4: 11, 5: 9, 6: 8} - nrow = NROW_RULES[ndigit] - - # read lines and build correlation matrix - corr = np.identity(npar) - for i in range(nlines): - line = file_obj.readline() - ii = int_endf(line[:5]) - 1 # -1 to account for 0 indexing - jj = int_endf(line[5:10]) - 1 - factor = 10**ndigit - for j in range(nrow): - if jj+j >= ii: - break - element = int_endf(line[11+(ndigit+1)*j:11+(ndigit+1)*(j+1)]) - if element > 0: - corr[ii, jj] = (element+0.5)/factor - elif element < 0: - corr[ii, jj] = (element-0.5)/factor - - # Symmetrize the correlation matrix - corr = corr + corr.T - np.diag(corr.diagonal()) - return corr - - -def get_evaluations(filename): - """Return a list of all evaluations within an ENDF file. - - Parameters - ---------- - filename : str - Path to ENDF-6 formatted file - - Returns - ------- - list - A list of :class:`openmc.data.endf.Evaluation` instances. - - """ - evaluations = [] - with open(str(filename), 'r') as fh: - while True: - pos = fh.tell() - line = fh.readline() - if line[66:70] == ' -1': - break - fh.seek(pos) - evaluations.append(Evaluation(fh)) - return evaluations + params, tab = _get_tab1_record(file_obj) + return params, Tabulated1D(tab.x, tab.y, tab.breakpoints, tab.interpolation) class Evaluation: @@ -361,7 +49,7 @@ class Evaluation: Parameters ---------- - filename_or_obj : str or file-like + filename_or_obj : str, file-like, or endf.Material Path to ENDF file to read or an open file positioned at the start of an ENDF material @@ -381,17 +69,25 @@ class Evaluation: """ def __init__(self, filename_or_obj): + self.section = {} + self.info = {} + self.target = {} + self.projectile = {} + self.reaction_list = [] + + if isinstance(filename_or_obj, Material): + self.section = dict(filename_or_obj.section_text) + self.section_data = filename_or_obj.section_data + self.material = filename_or_obj.MAT + self._read_header() + return + if isinstance(filename_or_obj, (str, PurePath)): fh = open(str(filename_or_obj), 'r') need_to_close = True else: fh = filename_or_obj need_to_close = False - self.section = {} - self.info = {} - self.target = {} - self.projectile = {} - self.reaction_list = [] # Skip TPID record. Evaluators sometimes put in TPID records that are # ill-formated because they lack MF/MT values or put them in the wrong @@ -517,23 +213,9 @@ class Evaluation: self.target['isomeric_state']) -class Tabulated2D: - """Metadata for a two-dimensional function. - - This is a dummy class that is not really used other than to store the - interpolation information for a two-dimensional function. Once we refactor - to adopt GNDS-like data containers, this will probably be removed or - extended. - - Parameters - ---------- - breakpoints : Iterable of int - Breakpoints for interpolation regions - interpolation : Iterable of int - Interpolation scheme identification number, e.g., 3 means y is linear in - ln(x). - - """ - def __init__(self, breakpoints, interpolation): - self.breakpoints = breakpoints - self.interpolation = interpolation +def as_evaluation(ev_or_filename): + """Return an object supporting OpenMC's legacy Evaluation interface.""" + if isinstance(ev_or_filename, Evaluation): + return ev_or_filename + else: + return Evaluation(ev_or_filename) diff --git a/openmc/data/fission_energy.py b/openmc/data/fission_energy.py index 3c7998ee2..e4fac087a 100644 --- a/openmc/data/fission_energy.py +++ b/openmc/data/fission_energy.py @@ -5,7 +5,8 @@ from io import StringIO import openmc.checkvalue as cv from openmc.mixin import EqualityMixin from .data import EV_PER_MEV -from .endf import get_cont_record, get_list_record, get_tab1_record, Evaluation +from .endf import ( + as_evaluation, get_cont_record, get_list_record, get_tab1_record) from .function import Function1D, Tabulated1D, Polynomial, sum_functions @@ -195,7 +196,7 @@ class FissionEnergyRelease(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation incident_neutron : openmc.data.IncidentNeutron Corresponding incident neutron dataset @@ -206,7 +207,7 @@ class FissionEnergyRelease(EqualityMixin): Fission energy release data """ - cv.check_type('evaluation', ev, Evaluation) + ev = as_evaluation(ev) # Check to make sure this ENDF file matches the expected isomer. if ev.target['atomic_number'] != incident_neutron.atomic_number: diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index dd14e0d19..d843b5971 100644 --- a/openmc/data/multipole.py +++ b/openmc/data/multipole.py @@ -15,7 +15,7 @@ from . import WMP_VERSION, WMP_VERSION_MAJOR from .data import K_BOLTZMANN from .neutron import IncidentNeutron from .resonance import ResonanceRange - +from .vectfit import vectfit, evaluate # Constants that determine which value to access _MP_EA = 0 # Pole @@ -174,10 +174,6 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, (poles, residues) """ - - # import vectfit package: https://github.com/liangjg/vectfit - import vectfit as vf - ne = energy.size nmt = len(mts) if ce_xs.shape != (nmt, ne): @@ -194,8 +190,8 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, test_xs_ref[i] = np.interp(test_energy, energy, ce_xs[i]) if log: - print(f" energy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)") - print(f" error tolerance: rtol={rtol}, atol={atol}") + print(f"\tenergy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)") + print(f"\terror tolerance: rtol={rtol}, atol={atol}") # transform xs (sigma) and energy (E) to f (sigma*E) and s (sqrt(E)) to be # compatible with the multipole representation @@ -251,7 +247,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, print(f"VF iteration {i_vf + 1}/{n_vf_iter}") # call vf - poles, residues, cf, f_fit, rms = vf.vectfit(f, s, poles, weight) + poles, residues, *_ = vectfit(f, s, poles, weight) # convert real pole to conjugate pairs n_real_poles = 0 @@ -268,11 +264,11 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, if n_real_poles > 0: if log >= DETAILED_LOGGING: print(f" # real poles: {n_real_poles}") - new_poles, residues, cf, f_fit, rms = \ - vf.vectfit(f, s, new_poles, weight, skip_pole=True) + new_poles, residues, *_ = \ + vectfit(f, s, new_poles, weight, skip_pole_update=True) # assess the result on test grid - test_xs = vf.evaluate(test_s, new_poles, residues) / test_energy + test_xs = evaluate(test_s, new_poles, residues) / test_energy abserr = np.abs(test_xs - test_xs_ref) with np.errstate(invalid='ignore', divide='ignore'): relerr = abserr / test_xs_ref @@ -388,9 +384,9 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, return (mp_poles, mp_residues) - def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, - log=False, path_out=None, mp_filename=None, **kwargs): + log=False, path_out=None, mp_filename=None, + **kwargs): r"""Generate multipole data for a nuclide from ENDF. Parameters @@ -571,10 +567,6 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, format. """ - - # import vectfit package: https://github.com/liangjg/vectfit - import vectfit as vf - # unpack multipole data name = mp_data["name"] awr = mp_data["AWR"] @@ -645,7 +637,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, # reference xs from multipole form, note the residue terms in the # multipole and vector fitting representations differ by a 1j - xs_ref = vf.evaluate(energy_sqrt, poles, residues*1j) / energy + xs_ref = evaluate(energy_sqrt, poles, residues*1j) / energy # curve fit matrix matrix = np.vstack([energy**(0.5*i - 1) for i in range(n_cf + 1)]).T @@ -659,7 +651,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, # calculate the cross sections contributed by the windowed poles if rp > lp: - xs_wp = vf.evaluate(energy_sqrt, poles[lp:rp], + xs_wp = evaluate(energy_sqrt, poles[lp:rp], residues[:, lp:rp]*1j) / energy else: xs_wp = np.zeros_like(xs_ref) @@ -1054,7 +1046,15 @@ class WindowedMultipole(EqualityMixin): return cls.from_multipole(mp_data, **wmp_options) @classmethod - def from_multipole(cls, mp_data, search=None, log=False, **kwargs): + def from_multipole( + cls, + mp_data, + search=None, + log=False, + search_n_win=20, + search_cf_orders=None, + **kwargs, + ): """Generate windowed multipole neutron data from multipole data. Parameters @@ -1066,8 +1066,14 @@ class WindowedMultipole(EqualityMixin): Defaults to True if no windowing parameters are specified. log : bool or int, optional Whether to print running logs (use int for verbosity control) + search_n_win : int, optional + Number of window sizes to consider in the search grid when + ``search`` is True. + search_cf_orders : iterable of int, optional + Curve-fit orders to consider in the search grid when ``search`` is + True. Defaults to integers from 10 down to 2. **kwargs - Keyword arguments passed to :func:`openmc.data.multipole._windowing` + Keyword arguments passed to :func:`openmc.data.multipole._windowing`. Returns ------- @@ -1098,12 +1104,17 @@ class WindowedMultipole(EqualityMixin): # search optimal WMP from a range of window sizes and CF orders if log: print("Start searching ...") + if search_cf_orders is None: + search_cf_orders = range(10, 1, -1) + n_poles = sum([p.size for p in mp_data["poles"]]) n_win_min = max(5, n_poles // 20) n_win_max = 2000 if n_poles < 2000 else 8000 best_wmp = best_metric = None - for n_w in np.unique(np.linspace(n_win_min, n_win_max, 20, dtype=int)): - for n_cf in range(10, 1, -1): + for n_w in np.unique( + np.linspace(n_win_min, n_win_max, search_n_win, dtype=int) + ): + for n_cf in search_cf_orders: if log: print(f"Testing N_win={n_w} N_cf={n_cf}") diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 95a3424ea..f17bb3fe4 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -11,9 +11,10 @@ import h5py from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Library, Table, get_table, get_metadata -from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV +from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV, gnds_name from .endf import ( - Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations) + Evaluation, SUM_RULES, as_evaluation, get_head_record, get_tab1_record, + get_evaluations) from .fission_energy import FissionEnergyRelease from .function import Tabulated1D, Sum, ResonancesWithBackground from .njoy import make_ace, make_pendf @@ -652,7 +653,7 @@ class IncidentNeutron(EqualityMixin): Parameters ---------- - ev_or_filename : openmc.data.endf.Evaluation or str + ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -666,10 +667,7 @@ class IncidentNeutron(EqualityMixin): Incident neutron continuous-energy data """ - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) atomic_number = ev.target['atomic_number'] mass_number = ev.target['mass_number'] @@ -678,11 +676,7 @@ class IncidentNeutron(EqualityMixin): temperature = ev.target['temperature'] # Determine name - element = ATOMIC_SYMBOL[atomic_number] - if metastable > 0: - name = f'{element}{mass_number}_m{metastable}' - else: - name = f'{element}{mass_number}' + name = gnds_name(atomic_number, mass_number, metastable) # Instantiate incident neutron data data = cls(name, atomic_number, mass_number, metastable, @@ -769,6 +763,11 @@ class IncidentNeutron(EqualityMixin): for table in lib.tables[1:]: data.add_temperature_from_ace(table) + # Use name based on ENDF evaluation. The name assigned by from_ace + # may be wrong for higher metastable states (e.g., Hf178_m2) + ev = evaluation if evaluation is not None else Evaluation(filename) + data.name = ev.gnds_name + # Add 0K elastic scattering cross section if '0K' not in data.energy: pendf = Evaluation(kwargs['pendf']) @@ -779,7 +778,6 @@ class IncidentNeutron(EqualityMixin): data[2].xs['0K'] = xs # Add fission energy release data - ev = evaluation if evaluation is not None else Evaluation(filename) if (1, 458) in ev.section: data.fission_energy = f = FissionEnergyRelease.from_endf(ev, data) else: @@ -805,9 +803,7 @@ class IncidentNeutron(EqualityMixin): # Helper function to get a cross section from an ENDF file on a # given energy grid def get_file3_xs(ev, mt, E): - file_obj = StringIO(ev.section[3, mt]) - get_head_record(file_obj) - _, xs = get_tab1_record(file_obj) + xs = ev.section_data[3, mt]['sigma'] return xs(E) heating_local = Reaction(901) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 4c538bd6e..7c99637f0 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -15,19 +15,41 @@ import openmc.data # identifiers. ThermalTuple = namedtuple('ThermalTuple', ['name', 'zaids', 'nmix']) _THERMAL_DATA = { + 'c_Ag': ThermalTuple('ag', [47107, 47109], 1), 'c_Al27': ThermalTuple('al27', [13027], 1), 'c_Al_in_Al2O3': ThermalTuple('asap00', [13027], 1), + 'c_Al_in_Y3Al5O12': ThermalTuple('alyag', [13027], 1), + 'c_Au': ThermalTuple('au', [79197], 1), 'c_Be': ThermalTuple('be', [4009], 1), 'c_Be_distinct': ThermalTuple('besd', [4009], 1), 'c_Be_in_BeO': ThermalTuple('bebeo', [4009], 1), 'c_Be_in_Be2C': ThermalTuple('bebe2c', [4009], 1), 'c_Be_in_BeF2': ThermalTuple('bebef2', [4009], 1), 'c_Be_in_FLiBe': ThermalTuple('beflib', [4009], 1), + 'c_BeO': ThermalTuple('beo', [4009, 8016, 8017, 8018], 2), + 'c_Bi': ThermalTuple('bi', [83209], 1), + 'c_Bi_in_Ge3Bi4O12': ThermalTuple('bigbo', [83209], 1), 'c_C6H6': ThermalTuple('benz', [1001, 6000, 6012], 2), 'c_C_in_Be2C': ThermalTuple('cbe2c', [6000, 6012, 6013], 1), 'c_C_in_C5O2H8': ThermalTuple('clucit', [6000, 6012, 6013], 1), 'c_C_in_C8H8': ThermalTuple('cc8h8', [6000, 6012, 6013], 1), + 'c_C_in_C19H16_liquid': ThermalTuple('c19liq', [6000, 6012, 6013], 1), + 'c_C_in_C19H16_solid': ThermalTuple('c19sol', [6000, 6012, 6013], 1), + 'c_C_in_C2H6O_liquid': ThermalTuple('ethliq', [6000, 6012, 6013], 1), + 'c_C_in_C2H6O_solid': ThermalTuple('ethsol', [6000, 6012, 6013], 1), + 'c_C_in_C6H6_liquid': ThermalTuple('benzlq', [6000, 6012, 6013], 1), + 'c_C_in_C6H6_solid': ThermalTuple('benzsl', [6000, 6012, 6013], 1), + 'c_C_in_C7H8_liquid': ThermalTuple('tolliq', [6000, 6012, 6013], 1), + 'c_C_in_C7H8_solid': ThermalTuple('tolsol', [6000, 6012, 6013], 1), + 'c_C_in_C8H10_liquid': ThermalTuple('xylliq', [6000, 6012, 6013], 1), + 'c_C_in_C8H10_solid': ThermalTuple('xylsol', [6000, 6012, 6013], 1), + 'c_C_in_C9H12_liquid': ThermalTuple('mesliq', [6000, 6012, 6013], 1), + 'c_C_in_C9H12_solid': ThermalTuple('messol', [6000, 6012, 6013], 1), 'c_C_in_CF2': ThermalTuple('ccf2', [6000, 6012, 6013], 1), + 'c_C_in_CH2': ThermalTuple('cch2', [6000, 6012, 6013], 1), + 'c_C_in_CH4_liquid': ThermalTuple('cch4lq', [6000, 6012, 6013], 1), + 'c_C_in_CH4_solid': ThermalTuple('cch4sl', [6000, 6012, 6013], 1), + 'c_C_in_Diamond': ThermalTuple('cdiam', [6000, 6012, 6013], 1), 'c_C_in_SiC': ThermalTuple('csic', [6000, 6012, 6013], 1), 'c_C_in_UC_100p': ThermalTuple('cuc100', [6000, 6012, 6013], 1), 'c_C_in_UC_10p': ThermalTuple('cuc10', [6000, 6012, 6013], 1), @@ -36,16 +58,29 @@ _THERMAL_DATA = { 'c_C_in_UC_HALEU': ThermalTuple('cuchal', [6000, 6012, 6013], 1), 'c_C_in_UC_HEU': ThermalTuple('cucheu', [6000, 6012, 6013], 1), 'c_C_in_ZrC': ThermalTuple('czrc', [6000, 6012, 6013], 1), + 'c_Ca': ThermalTuple('ca', [20040, 20042, 20043, 20044, 20046, 20048], 1), 'c_Ca_in_CaH2': ThermalTuple('cacah2', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_Ca_in_CaO2H2': ThermalTuple('cacaoh', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_Cr': ThermalTuple('cr', [24050, 24052, 24053, 24054], 1), + 'c_Cu': ThermalTuple('cu', [29063, 29065], 1), 'c_D_in_7LiD': ThermalTuple('dlid', [1002], 1), 'c_D_in_D2O': ThermalTuple('dd2o', [1002], 1), 'c_D_in_D2O_solid': ThermalTuple('dice', [1002], 1), + 'c_D_in_MgD2': ThermalTuple('dmgd2', [1002], 1), 'c_F_in_Be2': ThermalTuple('fbef2', [9019], 1), 'c_F_in_CF2': ThermalTuple('fcf2', [9019], 1), 'c_F_in_FLiBe': ThermalTuple('fflibe', [9019], 1), 'c_F_in_HF': ThermalTuple('f_hf', [9019], 1), + 'c_F_in_LiF': ThermalTuple('flif', [9019], 1), 'c_F_in_MgF2': ThermalTuple('fmgf2', [9019], 1), 'c_Fe56': ThermalTuple('fe56', [26056], 1), + 'c_Fe_in_Fe_alpha': ThermalTuple('fealph', [26054, 26056, 26057, 26058], 1), + 'c_Fe_in_Fe_gamma': ThermalTuple('fegamm', [26054, 26056, 26057, 26058], 1), + 'c_Ga_in_GaN': ThermalTuple('gagan', [31069, 31071], 1), + 'c_Ga_in_GaSe': ThermalTuple('gagase', [31069, 31071], 1), + 'c_Ge': ThermalTuple('ge', [32070, 32072, 32073, 32074, 32076], 1), + 'c_Ge_in_Ge3Bi4O12': ThermalTuple('gegbo', [32070, 32072, 32073, 32074, 32076], 1), + 'c_Ge_in_GeTe': ThermalTuple('gegete', [32070, 32072, 32073, 32074, 32076], 1), 'c_Graphite': ThermalTuple('graph', [6000, 6012, 6013], 1), 'c_Graphite_10p': ThermalTuple('grph10', [6000, 6012, 6013], 1), 'c_Graphite_20p': ThermalTuple('grph20', [6000, 6012, 6013], 1), @@ -54,7 +89,20 @@ _THERMAL_DATA = { 'c_H_in_7LiH': ThermalTuple('hlih', [1001], 1), 'c_H_in_C5O2H8': ThermalTuple('lucite', [1001], 1), 'c_H_in_C8H8': ThermalTuple('hc8h8', [1001], 1), + 'c_H_in_C19H16_liquid': ThermalTuple('h19liq', [1001], 1), + 'c_H_in_C19H16_solid': ThermalTuple('h19sol', [1001], 1), + 'c_H_in_C2H6O_liquid': ThermalTuple('hetliq', [1001], 1), + 'c_H_in_C2H6O_solid': ThermalTuple('hetsol', [1001], 1), + 'c_H_in_C6H6_liquid': ThermalTuple('hbzliq', [1001], 1), + 'c_H_in_C6H6_solid': ThermalTuple('hbzsol', [1001], 1), + 'c_H_in_C7H8_liquid': ThermalTuple('htlliq', [1001], 1), + 'c_H_in_C7H8_solid': ThermalTuple('htlsol', [1001], 1), + 'c_H_in_C8H10_liquid': ThermalTuple('hxyliq', [1001], 1), + 'c_H_in_C8H10_solid': ThermalTuple('hxysol', [1001], 1), + 'c_H_in_C9H12_liquid': ThermalTuple('hmsliq', [1001], 1), + 'c_H_in_C9H12_solid': ThermalTuple('hmssol', [1001], 1), 'c_H_in_CaH2': ThermalTuple('hcah2', [1001], 1), + 'c_H_in_CaO2H2': ThermalTuple('hcaoh', [1001], 1), 'c_H1_in_CaH2': ThermalTuple('h1cah2', [1001], 1), 'c_H2_in_CaH2': ThermalTuple('h2cah2', [1001], 1), 'c_H_in_CH2': ThermalTuple('hch2', [1001], 1), @@ -64,32 +112,64 @@ _THERMAL_DATA = { 'c_H_in_H2O': ThermalTuple('hh2o', [1001], 1), 'c_H_in_H2O_solid': ThermalTuple('hice', [1001], 1), 'c_H_in_HF': ThermalTuple('hhf', [1001], 1), + 'c_H_in_KOH': ThermalTuple('hkoh', [1001], 1), + 'c_H_in_LiH': ThermalTuple('hlih2', [1001], 1), 'c_H_in_Mesitylene': ThermalTuple('mesi00', [1001], 1), 'c_H_in_ParaffinicOil': ThermalTuple('hparaf', [1001], 1), 'c_H_in_Toluene': ThermalTuple('tol00', [1001], 1), + 'c_H_in_MgH2': ThermalTuple('hmgh2', [1001], 1), + 'c_H_in_MgOH2': ThermalTuple('hmgoh', [1001], 1), + 'c_H_in_NaMgH3': ThermalTuple('hnamg', [1001], 1), + 'c_H_in_NaOH': ThermalTuple('hnaoh', [1001], 1), + 'c_H_in_SrH2': ThermalTuple('hsrh2', [1001], 1), 'c_H_in_UH3': ThermalTuple('huh3', [1001], 1), 'c_H_in_YH2': ThermalTuple('hyh2', [1001], 1), 'c_H_in_ZrH': ThermalTuple('hzrh', [1001], 1), 'c_H_in_ZrH2': ThermalTuple('hzrh2', [1001], 1), 'c_H_in_ZrHx': ThermalTuple('hzrhx', [1001], 1), + 'c_I_in_NaI': ThermalTuple('inai', [53127], 1), + 'c_K': ThermalTuple('k', [19039, 19040, 19041], 1), + 'c_K_in_KOH': ThermalTuple('kkoh', [19039, 19040, 19041], 1), 'c_Li_in_FLiBe': ThermalTuple('liflib', [3006, 3007], 1), 'c_Li_in_7LiD': ThermalTuple('lilid', [3007], 1), 'c_Li_in_7LiH': ThermalTuple('lilih', [3007], 1), + 'c_Li_in_LiF': ThermalTuple('lilif', [3006, 3007], 1), + 'c_Li_in_LiH': ThermalTuple('lilih2', [3006, 3007], 1), 'c_Mg24': ThermalTuple('mg24', [12024], 1), 'c_Mg_in_MgF2': ThermalTuple('mgmgf2', [12024, 12025, 12026], 1), 'c_Mg_in_MgO': ThermalTuple('mgmgo', [12024, 12025, 12026], 1), + 'c_Mg_in_MgD2': ThermalTuple('mgmgd2', [12024, 12025, 12026], 1), + 'c_Mg_in_MgH2': ThermalTuple('mgmgh2', [12024, 12025, 12026], 1), + 'c_Mg_in_MgOH2': ThermalTuple('mgoh2', [12024, 12025, 12026], 1), + 'c_Mg_in_NaMgH3': ThermalTuple('mgnamg', [12024, 12025, 12026], 1), + 'c_Mo': ThermalTuple('mo', [42092, 42094, 42095, 42096, 42097, 42098, 42100], 1), + 'c_N_in_GaN': ThermalTuple('ngan', [7014, 7015], 1), 'c_N_in_UN_100p': ThermalTuple('nun100', [7014, 7015], 1), 'c_N_in_UN_10p': ThermalTuple('nun10', [7014, 7015], 1), 'c_N_in_UN_5p': ThermalTuple('nun5', [7014, 7015], 1), 'c_N_in_UN': ThermalTuple('n-un', [7014, 7015], 1), 'c_N_in_UN_HALEU': ThermalTuple('nunhal', [7014, 7015], 1), 'c_N_in_UN_HEU': ThermalTuple('nunheu', [7014, 7015], 1), + 'c_Na': ThermalTuple('na', [11023], 1), + 'c_Na_in_NaI': ThermalTuple('nanai', [11023], 1), + 'c_Na_in_NaMgH3': ThermalTuple('nanamg', [11023], 1), + 'c_Na_in_NaOH': ThermalTuple('nanaoh', [11023], 1), + 'c_Nb': ThermalTuple('nb', [41093], 1), + 'c_Ni': ThermalTuple('ni', [28058, 28060, 28061, 28062, 28064], 1), 'c_O_in_Al2O3': ThermalTuple('osap00', [8016, 8017, 8018], 1), 'c_O_in_BeO': ThermalTuple('obeo', [8016, 8017, 8018], 1), 'c_O_in_C5O2H8': ThermalTuple('olucit', [8016, 8017, 8018], 1), + 'c_O_in_C2H6O_liquid': ThermalTuple('oetliq', [8016, 8017, 8018], 1), + 'c_O_in_C2H6O_solid': ThermalTuple('oetsol', [8016, 8017, 8018], 1), + 'c_O_in_CaO2H2': ThermalTuple('ocaoh', [8016, 8017, 8018], 1), 'c_O_in_D2O': ThermalTuple('od2o', [8016, 8017, 8018], 1), 'c_O_in_H2O_solid': ThermalTuple('oice', [8016, 8017, 8018], 1), 'c_O_in_MgO': ThermalTuple('omgo', [8016, 8017, 8018], 1), + 'c_O_in_Ge3Bi4O12': ThermalTuple('ogbo', [8016, 8017, 8018], 1), + 'c_O_in_H2O': ThermalTuple('oh2o', [8016, 8017, 8018], 1), + 'c_O_in_KOH': ThermalTuple('okoh', [8016, 8017, 8018], 1), + 'c_O_in_MgOH2': ThermalTuple('omgoh', [8016, 8017, 8018], 1), + 'c_O_in_NaOH': ThermalTuple('onaoh', [8016, 8017, 8018], 1), 'c_O_in_PuO2': ThermalTuple('opuo2', [8016, 8017, 8018], 1), 'c_O_in_SiO2_alpha': ThermalTuple('osio2a', [8016, 8017, 8018], 1), 'c_O_in_UO2_100p': ThermalTuple('ouo200', [8016, 8017, 8018], 1), @@ -98,16 +178,26 @@ _THERMAL_DATA = { 'c_O_in_UO2': ThermalTuple('ouo2', [8016, 8017, 8018], 1), 'c_O_in_UO2_HALEU': ThermalTuple('ouo2hl', [8016, 8017, 8018], 1), 'c_O_in_UO2_HEU': ThermalTuple('ouo2he', [8016, 8017, 8018], 1), + 'c_O_in_Y3Al5O12': ThermalTuple('oyag', [8016, 8017, 8018], 1), 'c_ortho_D': ThermalTuple('orthod', [1002], 1), 'c_ortho_H': ThermalTuple('orthoh', [1001], 1), 'c_para_D': ThermalTuple('parad', [1002], 1), 'c_para_H': ThermalTuple('parah', [1001], 1), + 'c_Pb': ThermalTuple('pb', [82204, 82206, 82207, 82208], 1), + 'c_Pd': ThermalTuple('pd', [46102, 46104, 46105, 46106, 46108, 46110], 1), + 'c_Pt': ThermalTuple('pt', [78190, 78192, 78194, 78195, 78196, 78198], 1), 'c_Pu_in_PuO2': ThermalTuple('puo2', [94239, 94240, 94241, 94242, 94243], 1), 'c_Si28': ThermalTuple('si00', [14028], 1), 'c_Si_in_SiC': ThermalTuple('sisic', [14028, 14029, 14030], 1), 'c_Si_in_SiO2_alpha': ThermalTuple('si_o2a', [14028, 14029, 14030], 1), 'c_SiO2_alpha': ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3), 'c_SiO2_beta': ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3), + 'c_S_in_ZnS': ThermalTuple('szns', [16032, 16033, 16034, 16036], 1), + 'c_Se_in_GaSe': ThermalTuple('segase', [34074, 34076, 34077, 34078, 34080, 34082], 1), + 'c_Sn': ThermalTuple('sn', [50112, 50114, 50115, 50116, 50117, 50118, 50119, 50120, 50122, 50124], 1), + 'c_Sr_in_SrH2': ThermalTuple('srsrh2', [38084, 38086, 38087, 38088], 1), + 'c_Te_in_GeTe': ThermalTuple('tegete', [52120, 52122, 52123, 52124, 52125, 52126, 52128, 52130], 1), + 'c_Ti': ThermalTuple('ti', [22046, 22047, 22048, 22049, 22050], 1), 'c_U_metal_100p': ThermalTuple('u-100p', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_metal_10p': ThermalTuple('u-10p', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_metal_5p': ThermalTuple('u-5p', [92233, 92234, 92235, 92236, 92238], 1), @@ -132,7 +222,13 @@ _THERMAL_DATA = { 'c_U_in_UO2': ThermalTuple('uuo2', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UO2_HALEU': ThermalTuple('uo2hal', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UO2_HEU': ThermalTuple('uo2heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_V': ThermalTuple('v', [23050, 23051], 1), + 'c_W': ThermalTuple('w', [74180, 74182, 74183, 74184, 74186], 1), + 'c_Y_in_Y3Al5O12': ThermalTuple('yyag', [39089], 1), 'c_Y_in_YH2': ThermalTuple('yyh2', [39089], 1), + 'c_Zn': ThermalTuple('zn', [30064, 30066, 30067, 30068, 30070], 1), + 'c_Zn_in_ZnS': ThermalTuple('znzns', [30064, 30066, 30067, 30068, 30070], 1), + 'c_Zr': ThermalTuple('zr', [40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrC': ThermalTuple('zrzrc', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH': ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH2': ThermalTuple('zrzrh2', [40000, 40090, 40091, 40092, 40094, 40096], 1), @@ -163,14 +259,14 @@ broadr / %%%%%%%%%%%%%%%%%%%%%%% Doppler broaden XS %%%%%%%%%%%%%%%%%%%%%%%%%%%% _TEMPLATE_HEATR = """ heatr / %%%%%%%%%%%%%%%%%%%%%%%%% Add heating kerma %%%%%%%%%%%%%%%%%%%%%%%%%%%% {nendf} {nheatr_in} {nheatr} / -{mat} 4 0 0 0 / +{mat} 4 0 0 0 0 {ed}/ 302 318 402 444 / """ _TEMPLATE_HEATR_LOCAL = """ heatr / %%%%%%%%%%%%%%%%% Add heating kerma (local photons) %%%%%%%%%%%%%%%%%%%% {nendf} {nheatr_in} {nheatr_local} / -{mat} 4 0 0 1 / +{mat} 4 0 0 1 0 {ed}/ 302 318 402 444 / """ @@ -315,7 +411,7 @@ def make_pendf(filename, pendf='pendf', **kwargs): def make_ace(filename, temperatures=None, acer=True, xsdir=None, output_dir=None, pendf=False, error=0.001, broadr=True, heatr=True, gaspr=True, purr=True, evaluation=None, - smoothing=True, **kwargs): + smoothing=True, displacement_energy=None, **kwargs): """Generate incident neutron ACE file from an ENDF file File names can be passed to @@ -367,6 +463,9 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, indicates which evaluation should be used. smoothing : bool, optional If the smoothing option (ACER card 6) is on (True) or off (False). + displacement_energy : float, optional + Threshold displacement energy in [eV]. Used in HEATR to calculate + damage-energy cross section. When None, use NJOY defaults. **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` @@ -419,6 +518,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # heatr if heatr: + ed = displacement_energy if displacement_energy is not None else 0 nheatr_in = nlast nheatr_local = nheatr_in + 1 tapeout[nheatr_local] = (output_dir / "heatr_local") if heatr is True \ @@ -565,7 +665,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, else: with warnings.catch_warnings(record=True) as w: proper_name = openmc.data.get_thermal_name(zsymam_thermal) - if w: + if w or proper_name not in _THERMAL_DATA: raise RuntimeError( f"Thermal scattering material {zsymam_thermal} not " "recognized. Please contact OpenMC developers at " diff --git a/openmc/data/photon.py b/openmc/data/photon.py index 25ded24cb..13cd3ec95 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -15,7 +15,8 @@ from openmc.mixin import EqualityMixin from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Table, get_metadata, get_table from .data import ATOMIC_SYMBOL, EV_PER_MEV -from .endf import Evaluation, get_head_record, get_tab1_record, get_list_record +from .endf import ( + as_evaluation, get_head_record, get_tab1_record, get_list_record) from .function import Tabulated1D @@ -203,7 +204,9 @@ class AtomicRelaxation(EqualityMixin): for subshell, df in transitions.items(): cv.check_value('subshell', subshell, _SUBSHELLS) cv.check_type('transitions', df, pd.DataFrame) - self._transitions = transitions + self._transitions = { + subshell: df.convert_dtypes() for subshell, df in transitions.items() + } @classmethod def from_ace(cls, ace): @@ -270,7 +273,7 @@ class AtomicRelaxation(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF atomic relaxation evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -280,10 +283,7 @@ class AtomicRelaxation(EqualityMixin): Atomic relaxation data """ - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) # Atomic relaxation data is always MF=28, MT=533 if (28, 533) not in ev.section: @@ -604,10 +604,10 @@ class IncidentPhoton(EqualityMixin): Parameters ---------- - photoatomic : str or openmc.data.endf.Evaluation + photoatomic : str, openmc.data.endf.Evaluation, or endf.Material ENDF photoatomic data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. - relaxation : str or openmc.data.endf.Evaluation, optional + relaxation : str, openmc.data.endf.Evaluation, or endf.Material, optional ENDF atomic relaxation data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -617,10 +617,7 @@ class IncidentPhoton(EqualityMixin): Photon interaction data """ - if isinstance(photoatomic, Evaluation): - ev = photoatomic - else: - ev = Evaluation(photoatomic) + ev = as_evaluation(photoatomic) Z = ev.target['atomic_number'] data = cls(Z) @@ -1069,7 +1066,7 @@ class PhotonReaction(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF photo-atomic interaction data evaluation mt : int The MT value of the reaction to get data for @@ -1080,6 +1077,7 @@ class PhotonReaction(EqualityMixin): Photon reaction data """ + ev = as_evaluation(ev) rx = cls(mt) # Read photon cross section diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 65b59582c..44ced5511 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -13,14 +13,15 @@ from .angle_distribution import AngleDistribution from .angle_energy import AngleEnergy from .correlated import CorrelatedAngleEnergy from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV -from .endf import get_head_record, get_tab1_record, get_list_record, \ - get_tab2_record, get_cont_record +from .endf import as_evaluation, get_head_record, get_tab1_record, \ + get_list_record, get_tab2_record, get_cont_record from .energy_distribution import EnergyDistribution, LevelInelastic, \ DiscretePhoton from .function import Tabulated1D, Polynomial from .kalbach_mann import KalbachMann from .laboratory import LaboratoryAngleEnergy from .nbody import NBodyPhaseSpace +from .photon import _SUBSHELLS from .product import Product from .uncorrelated import UncorrelatedAngleEnergy @@ -54,6 +55,10 @@ REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 3: "(n,nonelastic)", 198: '(n,n3p)', 199: '(n,3n2pa)', 200: '(n,5n2p)', 203: '(n,Xp)', 204: '(n,Xd)', 205: '(n,Xt)', 206: '(n,X3He)', 207: '(n,Xa)', 301: 'heating', 444: 'damage-energy', + 501: 'photon-total', 502: 'coherent-scatter', + 504: 'incoherent-scatter', 515: 'pair-production-electron', + 516: 'pair-production', 517: 'pair-production-nuclear', + 522: 'photoelectric', 649: '(n,pc)', 699: '(n,dc)', 749: '(n,tc)', 799: '(n,3Hec)', 849: '(n,ac)', 891: '(n,2nc)', 901: 'heating-local'} REACTION_NAME.update({i: f'(n,n{i - 50})' for i in range(51, 91)}) @@ -63,9 +68,16 @@ REACTION_NAME.update({i: f'(n,t{i - 700})' for i in range(700, 749)}) REACTION_NAME.update({i: f'(n,3He{i - 750})' for i in range(750, 799)}) REACTION_NAME.update({i: f'(n,a{i - 800})' for i in range(800, 849)}) REACTION_NAME.update({i: f'(n,2n{i - 875})' for i in range(875, 891)}) +REACTION_NAME.update( + {534 + i: f'photoelectric-{shell}' for i, shell in enumerate(_SUBSHELLS[1:])} +) REACTION_MT = {name: mt for mt, name in REACTION_NAME.items()} +REACTION_MT['total'] = 1 +REACTION_MT['elastic'] = 2 REACTION_MT['fission'] = 18 +REACTION_MT['absorption'] = 27 +REACTION_MT['capture'] = 102 FISSION_MTS = (18, 19, 20, 21, 38) @@ -1139,7 +1151,7 @@ class Reaction(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation mt : int The MT value of the reaction to get data for @@ -1150,6 +1162,7 @@ class Reaction(EqualityMixin): Reaction data """ + ev = as_evaluation(ev) rx = Reaction(mt) # Integrated cross section diff --git a/openmc/data/resonance.py b/openmc/data/resonance.py index 31e230df5..52ee7f1a9 100644 --- a/openmc/data/resonance.py +++ b/openmc/data/resonance.py @@ -7,7 +7,9 @@ import pandas as pd import openmc.checkvalue as cv from .data import NEUTRON_MASS -from .endf import get_head_record, get_cont_record, get_tab1_record, get_list_record +from .endf import ( + as_evaluation, get_head_record, get_cont_record, get_tab1_record, + get_list_record) try: from .reconstruct import wave_number, penetration_shift, reconstruct_mlbw, \ reconstruct_slbw, reconstruct_rm @@ -77,7 +79,7 @@ class Resonances: Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation Returns @@ -86,6 +88,7 @@ class Resonances: Resonance data """ + ev = as_evaluation(ev) file_obj = io.StringIO(ev.section[2, 151]) # Determine whether discrete or continuous representation diff --git a/openmc/data/resonance_covariance.py b/openmc/data/resonance_covariance.py index 709657044..aa86ae33c 100644 --- a/openmc/data/resonance_covariance.py +++ b/openmc/data/resonance_covariance.py @@ -74,7 +74,7 @@ class ResonanceCovariances(Resonances): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation resonances : openmc.data.Resonance object openmc.data.Resonanance object generated from the same evaluation @@ -86,6 +86,7 @@ class ResonanceCovariances(Resonances): Resonance covariance data """ + ev = endf.as_evaluation(ev) file_obj = io.StringIO(ev.section[32, 151]) # Determine whether discrete or continuous representation diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 4ecc7c040..3b910c535 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -27,20 +27,41 @@ from .thermal_angle_energy import (CoherentElasticAE, IncoherentElasticAE, _THERMAL_NAMES = { + 'c_Ag': ('ag',), 'c_Al27': ('al', 'al27', 'al-27', '13-al- 27'), 'c_Al_in_Al2O3': ('asap00', 'asap', 'al(al2o3)'), - 'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be'), + 'c_Al_in_Y3Al5O12': ('al(y3al5o1', 'alyag'), + 'c_Au': ('au',), + 'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be', '4-be-'), 'c_BeO': ('beo',), 'c_Be_distinct': ('besd', 'be+sd'), 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)', 'be_beo'), 'c_Be_in_Be2C': ('bebe2c', 'be(be2c)'), 'c_Be_in_BeF2': ('bebef2', 'be in bef2'), 'c_Be_in_FLiBe': ('beflib', 'be(flibe)'), + 'c_Bi': ('83-bi-', 'bi'), + 'c_Bi_in_Ge3Bi4O12': ('bi(ge3bi4o', 'bigbo'), 'c_C6H6': ('benz', 'c6h6', 'benzine'), 'c_C_in_Be2C': ('cbe2c', 'c(be2c)'), + 'c_C_in_C19H16_liquid': ('c(c19h16)l', 'c19liq'), + 'c_C_in_C19H16_solid': ('c(c19h16)s', 'c19sol'), + 'c_C_in_C2H6O_liquid': ('c(c2h6o)l', 'ethliq'), + 'c_C_in_C2H6O_solid': ('c(c2h6o)s', 'ethsol'), 'c_C_in_C5O2H8': ('clucit', 'c(lucite)'), + 'c_C_in_C6H6_liquid': ('c(c6h6)l', 'benzlq'), + 'c_C_in_C6H6_solid': ('c(c6h6)s', 'benzsl'), + 'c_C_in_C7H8_liquid': ('c(c7h8)l', 'tolliq'), + 'c_C_in_C7H8_solid': ('c(c7h8)s', 'tolsol'), 'c_C_in_C8H8': ('cc8h8', 'c(polystyr'), + 'c_C_in_C8H10_liquid': ('c(m-c8h10)l', 'xylliq'), + 'c_C_in_C8H10_solid': ('c(m-c8h10)s', 'xylsol'), + 'c_C_in_C9H12_liquid': ('c(c9h12)l', 'mesliq'), + 'c_C_in_C9H12_solid': ('c(c9h12)s', 'messol'), 'c_C_in_CF2': ('ccf2', 'c(teflon)'), + 'c_C_in_CH2': ('c(c2h4)n r', 'cch2'), + 'c_C_in_CH4_liquid': ('c(ch4)l', 'cch4lq'), + 'c_C_in_CH4_solid': ('c(ch4)s', 'cch4sl'), + 'c_C_in_Diamond': ('c(c-diamon', 'cdiam'), 'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)', 'c_sic'), 'c_C_in_UC_100p': ('cuc100', 'cinuc_100p'), 'c_C_in_UC_10p': ('cuc10', 'cinuc_10p'), @@ -49,16 +70,29 @@ _THERMAL_NAMES = { 'c_C_in_UC_HALEU': ('cuchal', 'cinuc_haleu'), 'c_C_in_UC_HEU': ('cucheu', 'cinuc_heu'), 'c_C_in_ZrC': ('czrc', 'c(zrc)'), + 'c_Ca': ('ca',), 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)', 'ca_cah2'), + 'c_Ca_in_CaO2H2': ('ca(caoh2)', 'cacaoh'), + 'c_Cr': ('cr',), + 'c_Cu': ('cu',), 'c_D_in_7LiD': ('dlid', 'd(7lid)'), 'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00', 'd(d2o)'), 'c_D_in_D2O_solid': ('dice',), + 'c_D_in_MgD2': ('d(mgd2)', 'dmgd2'), 'c_F_in_Be2': ('fbef2', 'f in bef2'), 'c_F_in_CF2': ('fcf2', 'f(teflon)'), 'c_F_in_FLiBe': ('fflibe', 'f(flibe)'), 'c_F_in_HF': ('f_hf',), + 'c_F_in_LiF': ('f(lif)', 'flif'), 'c_F_in_MgF2': ('fmgf2', 'f in mgf2'), 'c_Fe56': ('fe', 'fe56', 'fe-56', '26-fe- 56'), + 'c_Fe_in_Fe_alpha': ('fe(fe-alph', 'fealph'), + 'c_Fe_in_Fe_gamma': ('fe(fe-gamm', 'fegamm'), + 'c_Ga_in_GaN': ('ga(gan)', 'gagan'), + 'c_Ga_in_GaSe': ('ga(gase)', 'gagase'), + 'c_Ge': ('ge',), + 'c_Ge_in_Ge3Bi4O12': ('ge(ge3bi4o', 'gegbo'), + 'c_Ge_in_GeTe': ('ge(gete)', 'gegete'), 'c_Graphite': ('graph', 'grph', 'gr', 'gr00', 'graphite'), 'c_Graphite_10p': ('grph10', '10p graphit'), 'c_Graphite_20p': ('grph20', '20 graphite'), @@ -68,41 +102,86 @@ _THERMAL_NAMES = { 'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci', 'h(lucite)'), 'c_H_in_C8H8': ('hc8h8', 'h(polystyr'), 'c_H_in_CaH2': ('hcah2', 'hca00', 'h(cah2)'), + 'c_H_in_CaO2H2': ('h(caoh2)', 'hcaoh'), 'c_H1_in_CaH2': ('h1cah2', 'h1_cah2'), 'c_H2_in_CaH2': ('h2cah2', 'h2_cah2'), - 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)'), - 'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4'), - 'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4'), + 'c_H_in_C19H16_liquid': ('h(c19h16)l', 'h19liq'), + 'c_H_in_C19H16_solid': ('h(c19h16)s', 'h19sol'), + 'c_H_in_C2H6O_liquid': ('h(c2h6o)l', 'hetliq'), + 'c_H_in_C2H6O_solid': ('h(c2h6o)s', 'hetsol'), + 'c_H_in_C6H6_liquid': ('h(c6h6)l', 'hbzliq'), + 'c_H_in_C6H6_solid': ('h(c6h6)s', 'hbzsol'), + 'c_H_in_C7H8_liquid': ('h(c7h8)l', 'htlliq'), + 'c_H_in_C7H8_solid': ('h(c7h8)s', 'htlsol'), + 'c_H_in_C8H10_liquid': ('h(m-c8h10)l', 'hxyliq'), + 'c_H_in_C8H10_solid': ('h(m-c8h10)s', 'hxysol'), + 'c_H_in_C9H12_liquid': ('h(c9h12)l', 'hmsliq'), + 'c_H_in_C9H12_solid': ('h(c9h12)s', 'hmssol'), + 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)', 'h(c2h4)n r'), + 'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4', 'h(ch4)l'), + 'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4', 'h(ch4)s'), 'c_H_in_CH4_solid_phase_II': ('sch4p2',), 'c_H_in_H2O': ('hh2o', 'h-h2o', 'lwtr', 'lw', 'lw00', 'h(h2o)'), 'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00', 'h(ice-ih)', 'h(ice)'), 'c_H_in_HF': ('hhf', 'h(hf)', 'h_hf'), + 'c_H_in_KOH': ('h(koh)', 'hkoh'), + 'c_H_in_LiH': ('h(lih)', 'hlih2'), 'c_H_in_Mesitylene': ('mesi00', 'mesi', 'mesi-phii'), + 'c_H_in_MgH2': ('h(mgh2)', 'hmgh2'), + 'c_H_in_MgOH2': ('h(mgoh2)', 'hmgoh'), + 'c_H_in_NaMgH3': ('h(namgh3)', 'hnamg'), + 'c_H_in_NaOH': ('h(naoh)', 'hnaoh'), 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin', 'h(paraffini'), + 'c_H_in_SrH2': ('h(srh2)', 'hsrh2'), 'c_H_in_Toluene': ('tol00', 'tol', 'tolue-phii'), 'c_H_in_UH3': ('huh3', 'h(uh3)'), 'c_H_in_YH2': ('hyh2', 'h-yh2', 'h(yh2)'), 'c_H_in_ZrH': ('hzrh', 'h-zrh', 'h-zr', 'h/zr', 'hzr', 'hzr00', 'h(zrh)'), - 'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)'), - 'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)'), + 'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)', 'h(zrh2) in'), + 'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)', 'h(zrh15) i'), + 'c_I_in_NaI': ('i(nai)', 'inai'), + 'c_K': ('k',), + 'c_K_in_KOH': ('k(koh)', 'kkoh'), 'c_Li_in_FLiBe': ('liflib', 'li(flibe)'), 'c_Li_in_7LiD': ('lilid', '7li(7lid)'), 'c_Li_in_7LiH': ('lilih', '7li(7lih)'), + 'c_Li_in_LiF': ('li(lif)', 'lilif'), + 'c_Li_in_LiH': ('li(lih)', 'lilih2'), 'c_Mg24': ('mg', 'mg24', 'mg00', '24-mg'), + 'c_Mg_in_MgD2': ('mg(mgd2)', 'mgmgd2'), 'c_Mg_in_MgF2': ('mgmgf2', 'mg in mgf2'), + 'c_Mg_in_MgH2': ('mg(mgh2)', 'mgmgh2'), 'c_Mg_in_MgO': ('mgmgo', 'mg in mgo'), + 'c_Mg_in_MgOH2': ('mg(mgoh2)', 'mgoh2'), + 'c_Mg_in_NaMgH3': ('mg(namgh3)', 'mgnamg'), + 'c_Mo': ('mo',), + 'c_N_in_GaN': ('n(gan)', 'ngan'), 'c_N_in_UN_100p': ('nun100', 'n-un-100p'), 'c_N_in_UN_10p': ('nun10', 'n-un-10p'), 'c_N_in_UN_5p': ('nun5', 'n-un-5p'), 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l', 'ninun'), 'c_N_in_UN_HALEU': ('nunhal', 'n-un-haleu'), 'c_N_in_UN_HEU': ('nunheu', 'n-un-heu'), + 'c_Na': ('na',), + 'c_Na_in_NaI': ('na(nai)', 'nanai'), + 'c_Na_in_NaMgH3': ('na(namgh3)', 'nanamg'), + 'c_Na_in_NaOH': ('na(naoh)', 'nanaoh'), + 'c_Nb': ('nb',), + 'c_Ni': ('ni',), 'c_O_in_Al2O3': ('osap00', 'osap', 'o(al2o3)'), 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)', 'o_beo'), + 'c_O_in_C2H6O_liquid': ('o(c2h6o)l', 'oetliq'), + 'c_O_in_C2H6O_solid': ('o(c2h6o)s', 'oetsol'), 'c_O_in_C5O2H8': ('olucit', 'o(lucite)'), + 'c_O_in_CaO2H2': ('o(caoh2)', 'ocaoh'), 'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00', 'o(d2o)'), + 'c_O_in_H2O': ('o(h2o)', 'oh2o'), 'c_O_in_H2O_solid': ('oice', 'o-ice', 'o(ice-ih)'), + 'c_O_in_Ge3Bi4O12': ('o(ge3bi4o1', 'ogbo'), + 'c_O_in_KOH': ('o(koh)', 'okoh'), 'c_O_in_MgO': ('omgo', 'o in mgo'), + 'c_O_in_MgOH2': ('o(mgoh2)', 'omgoh'), + 'c_O_in_NaOH': ('o(naoh)', 'onaoh'), 'c_O_in_PuO2': ('opuo2', 'o in puo2'), 'c_O_in_SiO2_alpha': ('osio2a', 'o_sio2a'), 'c_O_in_UO2_100p': ('ouo200', 'o-uo2-100p'), @@ -111,16 +190,26 @@ _THERMAL_NAMES = { 'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200', 'o(uo2)'), 'c_O_in_UO2_HALEU': ('ouo2hl', 'ouo2-haleu'), 'c_O_in_UO2_HEU': ('ouo2he', 'o_uo2-heu'), + 'c_O_in_Y3Al5O12': ('o(y3al5o12', 'oyag'), 'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200', 'ortod', 'ortho-d'), 'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200', 'ortoh', 'ortho-h'), 'c_para_D': ('parad', 'paraD', 'dpara', 'pd200', 'para-d'), 'c_para_H': ('parah', 'paraH', 'hpara', 'ph200', 'para-h'), + 'c_Pb': ('pb',), + 'c_Pd': ('pd',), + 'c_Pt': ('pt',), 'c_Pu_in_PuO2': ('puo2', 'pu in puo2'), 'c_Si28': ('si00', 'sili', 'si'), 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)', 'si_sic'), 'c_Si_in_SiO2_alpha': ('si_o2a', 'si_sio2a'), - 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha'), - 'c_SiO2_beta': ('sio2b', 'sio2beta'), + 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha', 'sio2-a'), + 'c_SiO2_beta': ('sio2b', 'sio2beta', 'sio2-b'), + 'c_S_in_ZnS': ('s(zns-spha', 'szns'), + 'c_Se_in_GaSe': ('se(gase)', 'segase'), + 'c_Sn': ('sn',), + 'c_Sr_in_SrH2': ('sr(srh2)', 'srsrh2'), + 'c_Te_in_GeTe': ('te(gete)', 'tegete'), + 'c_Ti': ('ti',), 'c_U_metal_100p': ('u-100p',), 'c_U_metal_10p': ('u-10p',), 'c_U_metal_5p': ('u-5p',), @@ -145,11 +234,17 @@ _THERMAL_NAMES = { 'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200', 'u(uo2)'), 'c_U_in_UO2_HALEU': ('uo2hal', 'uuo2-haleu'), 'c_U_in_UO2_HEU': ('uo2heu', 'u_uo2-heu'), + 'c_V': ('v',), + 'c_W': ('w',), + 'c_Y_in_Y3Al5O12': ('y(y3al5o12', 'yyag'), 'c_Y_in_YH2': ('yyh2', 'y-yh2', 'y(yh2)'), + 'c_Zn': ('zn',), + 'c_Zn_in_ZnS': ('zn(zns-sph', 'znzns'), + 'c_Zr': ('zr',), 'c_Zr_in_ZrC': ('zrzrc', 'zr(zrc)'), 'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h', 'zr(zrh)'), - 'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)'), - 'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)'), + 'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)', 'zr(zrh2) i'), + 'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)', 'zr(zrh15)'), } @@ -947,7 +1042,7 @@ class ThermalScattering(EqualityMixin): Parameters ---------- - ev_or_filename : openmc.data.endf.Evaluation or str + ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. divide_incoherent_elastic : bool @@ -961,10 +1056,7 @@ class ThermalScattering(EqualityMixin): Thermal scattering data """ - if isinstance(ev_or_filename, endf.Evaluation): - ev = ev_or_filename - else: - ev = endf.Evaluation(ev_or_filename) + ev = endf.as_evaluation(ev_or_filename) # Read incoherent inelastic data assert (7, 4) in ev.section, 'No MF=7, MT=4 found in thermal scattering' diff --git a/openmc/data/vectfit.py b/openmc/data/vectfit.py new file mode 100644 index 000000000..c5a783a3c --- /dev/null +++ b/openmc/data/vectfit.py @@ -0,0 +1,811 @@ +""" +Fast Relaxed Vector Fitting function + +Approximate f(s) with a rational function: + f(s)=R*(s*I-A)^(-1) + Polynomials*s +where f(s) is a vector of elements. + +When f(s) is a vector, all elements become fitted with a common pole set. The +identification is done using the pole relocating method known as Vector Fitting +[1] with relaxed non-triviality constraint for faster convergence and smaller +fitting errors [2], and utilization of matrix structure for fast solution of the +pole identifion step [3]. + +[1] B. Gustavsen and A. Semlyen, "Rational approximation of frequency + domain responses by Vector Fitting", IEEE Trans. Power Delivery, vol. 14, + no. 3, pp. 1052-1061, July 1999. +[2] B. Gustavsen, "Improving the pole relocating properties of vector + fitting", IEEE Trans. Power Delivery, vol. 21, no. 3, pp. 1587-1592, July + 2006. +[3] D. Deschrijver, M. Mrozowski, T. Dhaene, and D. De Zutter, + "Macromodeling of Multiport Systems Using a Fast Implementation of the + Vector Fitting Method", IEEE Microwave and Wireless Components Letters, vol. + 18, no. 6, pp. 383-385, June 2008. + +All credit goes to: + - Bjorn Gustavsen for his MATLAB implementation. + (http://www.sintef.no/Projectweb/VECTFIT/) + - Jingang Liang for his C++ implementation. + (https://github.com/mit-crpg/vectfit.git) + +""" + +from typing import Tuple + +import numpy as np +from scipy.linalg import eigvals, lstsq, norm, qr + + +def wlstsq(a, b): + """Apply least-squares solve with column normalization. + + Notes + ----- + This routine rescales columns of `a` to improve conditioning. Columns with + zero norm are left unscaled to avoid divide-by-zero warnings. + """ + col_norm = np.linalg.norm(a, axis=0) + scale = np.ones_like(col_norm, dtype=float) + nonzero = col_norm > 0.0 + scale[nonzero] = 1.0 / col_norm[nonzero] + scale = np.nan_to_num(scale, nan=1.0, posinf=1.0, neginf=1.0) + + sol = lstsq(a * scale, b) + return (sol[0] * scale, sol[1:]) + + +def evaluate( + eval_points: np.ndarray, + pole_values: np.ndarray, + residue_matrix: np.ndarray, + poly_coefficients: np.ndarray | None = None, +) -> np.ndarray: + """Evaluate the rational function approximation: + f(s) ≈ sum(residue / (s - pole)) + sum(poly_coefficients * s^j) + + Parameters + ---------- + eval_points : np.ndarray + 1D array of real scalar frequency values (s). + pole_values : np.ndarray + 1D array of complex poles. + residue_matrix : np.ndarray + 2D or 1D array of complex residues (shape: [num_vectors, num_poles] or [num_poles]). + poly_coefficients : np.ndarray, optional + 2D or 1D array of real polynomial coefficients (shape: [num_vectors, num_polys]). + + Returns + ------- + np.ndarray + 2D array of evaluated real function values (shape: [num_vectors, num_samples]). + + Raises + ------ + ValueError + If input arrays have incompatible shapes. + """ + eval_points = np.asarray(eval_points) + pole_values = np.asarray(pole_values) + residue_matrix = np.asarray(residue_matrix) + + if eval_points.ndim != 1: + raise ValueError("eval_points must be a 1D array") + if pole_values.ndim != 1: + raise ValueError("pole_values must be a 1D array") + + if residue_matrix.ndim == 1: + residue_matrix = residue_matrix.reshape((1, -1)) + num_vectors, _ = residue_matrix.shape + num_samples = len(eval_points) + + if poly_coefficients is not None and isinstance(poly_coefficients, list): + poly_coefficients = np.array(poly_coefficients) + if poly_coefficients is None or poly_coefficients.size == 0: + poly_coefficients = np.zeros((num_vectors, 0)) + else: + poly_coefficients = np.asarray(poly_coefficients) + if poly_coefficients.ndim == 1: + poly_coefficients = poly_coefficients.reshape((1, -1)) + elif poly_coefficients.shape[0] != num_vectors: + raise ValueError("Mismatch in residues and poly_coefficients shapes") + + num_coeffs = poly_coefficients.shape[1] + result = np.zeros((num_vectors, num_samples)) + + # term: sum over poles of (residues / (eval_points - poles)) + denominator = ( + eval_points[np.newaxis, :] - pole_values[:, np.newaxis] + ) # shape: (num_poles, num_eval) + pole_terms = residue_matrix @ (1.0 / denominator) # shape: (num_vectors, num_eval) + result = np.real(pole_terms) + + # polynomial part: sum over poly_idx of (coeff * eval_points**poly_idx) + if num_coeffs > 0: + powers = ( + eval_points[np.newaxis, :] ** np.arange(num_coeffs)[:, np.newaxis] + ) # shape: (num_coeffs, num_eval) + result += poly_coefficients @ powers # shape: (num_vectors, num_eval) + + return result + + +def vectfit( + response_matrix: np.ndarray, + eval_points: np.ndarray, + initial_poles: np.ndarray, + weights: np.ndarray, + n_polys: int = 0, + skip_pole_update: bool = False, + skip_residue_update: bool = False, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]: + """Perform vector fitting using the Fast Relaxed Vector Fitting algorithm. + + Parameters + ---------- + response_matrix : np.ndarray + Complex matrix of frequency responses (shape: [num_vectors, num_samples]). + eval_points : np.ndarray + Real frequency samples (s), shape (num_samples,). + initial_poles : np.ndarray + Initial guess for poles (complex), shape (num_poles,). + weights : np.ndarray + Weighting matrix for fitting (same shape as response_matrix). + n_polys : int, optional + Number of real polynomial terms to include. + skip_pole_update : bool, optional + Whether to skip pole relocation step. + skip_residue_update : bool, optional + Whether to skip residue fitting step. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float] + - Updated poles (np.ndarray) + - Residues (np.ndarray) + - Polynomial coefficients (np.ndarray) + - Fitted response matrix (np.ndarray) + - Root-mean-square error (float) + """ + tol_low = 1e-18 + tol_high = 1e18 + response_matrix = np.asarray(response_matrix) + eval_points = np.asarray(eval_points) + initial_poles = np.asarray(initial_poles) + weights = np.asarray(weights) + + num_vectors, num_samples = response_matrix.shape + num_poles = len(initial_poles) + + if n_polys < 0 or n_polys > 11: + raise ValueError("n_polys must be in [0, 11]") + + residue_matrix = np.zeros((num_vectors, num_poles), dtype=np.complex128) + poly_coefficients = np.zeros((num_vectors, n_polys)) + fit_result = np.zeros_like(response_matrix) + rms_error = 0.0 + + if num_poles == 0 and n_polys == 0: + rms_error = norm(response_matrix) / np.sqrt(num_vectors * num_samples) + return initial_poles, residue_matrix, poly_coefficients, fit_result, rms_error + + if not skip_pole_update and num_poles > 0: + updated_poles = identify_poles( + num_poles, + num_samples, + n_polys, + initial_poles, + eval_points, + tol_high, + weights, + response_matrix, + num_vectors, + tol_low, + ) + else: + updated_poles = initial_poles + + if not skip_residue_update: + fit_result, rms_error = identify_residues( + num_poles, + updated_poles, + num_samples, + n_polys, + eval_points, + num_vectors, + weights, + response_matrix, + poly_coefficients, + residue_matrix, + ) + + return updated_poles, residue_matrix, poly_coefficients, fit_result, rms_error + + +def compute_dk_matrix( + dk_matrix: np.ndarray, + eval_points: np.ndarray, + poles: np.ndarray, + conj_index: np.ndarray, + num_poles: int, + num_polys: int, + tol_high: float = None, +): + """Compute the dk_matrix used in windowed multipole evaluations. + + Parameters + ---------- + dk_matrix : ndarray of shape (len(eval_points), M) + The full matrix used in least-squares fitting or evaluation. + eval_points : ndarray of shape (N,) + Energy points at which to evaluate. + poles : ndarray of shape (num_poles,) + Complex poles used in the resonance model. + conj_index : ndarray of shape (num_poles,) + Index array indicating pole conjugacy behavior: 0 (normal), 1 (add conjugate), 2 (imaginary part). + num_poles : int + Number of complex poles. + num_polys : int + Number of polynomial terms (including constant term). + tol_high : float + Replacement value for infinities. + """ + # Broadcast shapes + eval_points_col = eval_points[:, np.newaxis] + poles_row = poles[np.newaxis, :] + + # Compute base terms + term1 = 1.0 / (eval_points_col - poles_row) + term2 = 1.0 / (eval_points_col - np.conj(poles_row)) + term3 = 1j / (eval_points_col - np.conj(poles_row)) - 1j / ( + eval_points_col - poles_row + ) + + # Masks for different conjugacy types + mask0 = conj_index == 0 + mask1 = conj_index == 1 + mask2 = conj_index == 2 + + # Fill dk_matrix with pole terms + dk_matrix[:, :num_poles][:, mask0] = term1[:, mask0] + dk_matrix[:, :num_poles][:, mask1] = term1[:, mask1] + term2[:, mask1] + dk_matrix[:, :num_poles][:, mask2] = term3[:, mask2] + + # Replace infinities with high tolerance value + if tol_high is not None: + inf_mask = np.isinf(dk_matrix) + dk_matrix[inf_mask] = tol_high + 0j + + # Add polynomial basis (Chebyshev-like, just powers here) + powers = np.arange(num_polys) + dk_matrix[:, num_poles : num_poles + num_polys] = eval_points_col**powers + 0j + return dk_matrix + + +def row_block_matrix( + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + vec_idx: int, + num_poles: int, + num_polys: int, +) -> np.ndarray: + """ + Construct a single matrix row block for the given vector index. + + Parameters + ---------- + dk_matrix : ndarray of shape (num_samples, num_poles + num_polys) + Basis function evaluations at each sample point. + weights : ndarray of shape (num_vectors, num_samples) + Sample weights for each vector. + response_matrix : ndarray of shape (num_vectors, num_samples) + Response values at each sample point. + vec_idx : int + Index of the vector to construct the A1 block for. + num_poles : int + Number of poles used in the model. + num_polys : int + Number of polynomial basis terms. + + Returns + ------- + A : ndarray of shape (num_samples, num_poles + num_polys + num_poles + 1) + Weighted and assembled matrix block for the current vector. + """ + num_samples = dk_matrix.shape[0] + A = np.zeros( + (num_samples, num_poles + num_polys + num_poles + 1), dtype=np.complex128 + ) + + # Weighted basis terms + A[:, : num_poles + num_polys] = ( + weights[vec_idx][:, np.newaxis] * dk_matrix[:, : num_poles + num_polys] + ) + + # Weighted response terms (includes poles + 1) + A[:, num_poles + num_polys : num_poles + num_polys + num_poles + 1] = ( + -weights[vec_idx][:, np.newaxis] + * dk_matrix[:, : num_poles + 1] + * response_matrix[vec_idx][:, np.newaxis] + ) + + return A + + +def process_constrained_block( + vec_idx: int, + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + num_samples: int, + num_poles: int, + num_polys: int, + scale_factor: float, + num_vectors: int, +) -> Tuple[int, np.ndarray, np.ndarray]: + """ + Construct a constrained least-squares system block for the given vector index. + + This function computes the A matrix using weighted evaluations of the basis functions + and response terms. It appends a constraint row to enforce physical properties + (e.g., normalization) **only for the final vector index**. The full matrix A is + decomposed via QR, and the resulting triangular block is returned. + + This routine is intended for use in the main vector fitting loop when the denominator + is well-conditioned but requires an additional constraint row for physical consistency. + + Parameters + ---------- + vec_idx : int + Index of the vector to process. + dk_matrix : ndarray of shape (num_samples, num_poles + num_polys) + Evaluated basis functions at sample points. + weights : ndarray of shape (num_vectors, num_samples) + Weight matrix per vector. + response_matrix : ndarray of shape (num_vectors, num_samples) + Response function values for each vector. + num_samples : int + Number of sample points. + num_poles : int + Number of poles in the model. + num_polys : int + Number of polynomial terms in the model. + scale_factor : float + Scaling factor applied to the final constraint row. + num_vectors : int + Total number of vectors to process. + + Returns + ------- + vec_idx : int + Index of the processed vector. + lhs_block : ndarray of shape (num_poles + 1, num_poles + 1) + Triangular matrix block from QR decomposition. + rhs_block : ndarray of shape (num_poles + 1,) or None + Right-hand side vector block (only returned for final vec_idx), else None. + """ + A1 = row_block_matrix( + dk_matrix, weights, response_matrix, vec_idx, num_poles, num_polys + ) + A = np.zeros((2 * num_samples + 1, num_poles + num_polys + num_poles + 1)) + A[:num_samples] = A1.real + A[num_samples : 2 * num_samples] = A1.imag + # Handle final row only if vec_idx is last + if vec_idx == num_vectors - 1: + A[ + 2 * num_samples, + num_poles + num_polys : num_poles + num_polys + num_poles + 1, + ] = scale_factor * np.real(dk_matrix[:, : num_poles + 1].sum(axis=0)) + + Q, R = qr(A, mode="economic") + + lhs_block = R[ + num_poles + num_polys : num_poles + num_polys + num_poles + 1, + num_poles + num_polys : num_poles + num_polys + num_poles + 1, + ] + + if vec_idx == num_vectors - 1: + rhs_block = ( + num_samples + * scale_factor + * Q[-1, num_poles + num_polys : num_poles + num_polys + num_poles + 1] + ) + else: + rhs_block = np.zeros_like( + Q[-1, num_poles + num_polys : num_poles + num_polys + num_poles + 1] + ) + + return vec_idx, lhs_block, rhs_block + + +def process_unconstrained_block( + vec_idx: int, + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + denom: float, + num_poles: int, + num_polys: int, +) -> Tuple[int, np.ndarray, np.ndarray]: + """ + Construct an unconstrained least-squares system block for the given vector index. + + This function is used when the fitting denominator becomes ill-conditioned + (too small or too large), and the original constrained system is replaced by + an alternative regularized least-squares problem. The A matrix is built by stacking + the real and imaginary parts of the basis evaluations, and the RHS vector b is + scaled by `denom`. + + A standard QR decomposition is used to extract the square block of the system, + which can be solved independently from the constrained system. + + Parameters + ---------- + vec_idx : int + Index of the vector to process. + dk_matrix : ndarray of shape (num_samples, num_poles + num_polys) + Evaluated basis functions at sample points. + weights : ndarray of shape (num_vectors, num_samples) + Weight matrix per vector. + response_matrix : ndarray of shape (num_vectors, num_samples) + Response function values for each vector. + denom : float + Scaling factor applied to the right-hand side vector b. + num_poles : int + Number of poles in the model. + num_polys : int + Number of polynomial terms in the model. + + Returns + ------- + vec_idx : int + Index of the processed vector. + lhs_block : ndarray of shape (num_poles, num_poles) + Triangular matrix block from QR decomposition. + rhs_block : ndarray of shape (num_poles,) + Right-hand side vector block for this vector. + """ + A1 = row_block_matrix( + dk_matrix, weights, response_matrix, vec_idx, num_poles, num_polys + ) + A = np.vstack((A1.real, A1.imag)) + + b1 = denom * weights[vec_idx] * response_matrix[vec_idx] + b = np.concatenate((b1.real, b1.imag)) + + Q, R = qr(A, mode="economic") + + lhs_block = R[ + num_poles + num_polys : num_poles + num_polys + num_poles, + num_poles + num_polys : num_poles + num_polys + num_poles, + ] + rhs_block = Q[:, num_poles + num_polys : num_poles + num_polys + num_poles].T @ b + return vec_idx, lhs_block, rhs_block + + +def identify_poles( + num_poles: int, + num_samples: int, + num_polys: int, + poles: np.ndarray, + eval_points: np.ndarray, + tol_high: float, + weights: np.ndarray, + response_matrix: np.ndarray, + num_vectors: int, + tol_low: float, +) -> np.ndarray: + """ + Internal routine to update poles via relaxed vector fitting. + + Parameters + ---------- + num_poles : int + Number of poles. + num_samples : int + Number of frequency samples. + num_polys : int + Number of polynomial terms. + poles : np.ndarray + Initial poles (complex), shape (num_poles,). + eval_points : np.ndarray + Real frequency values, shape (num_samples,). + tol_high : float + Upper tolerance threshold for denominator. + weights : np.ndarray + Weighting matrix, shape (num_vectors, num_samples). + response_matrix : np.ndarray + Complex frequency responses, shape (num_vectors, num_samples). + num_vectors : int + Number of response vectors. + tol_low : float + Lower tolerance threshold for denominator. + + Returns + ------- + np.ndarray + Updated poles as eigenvalues (shape: [num_poles]). + """ + conj_index = label_conjugate_poles(poles) + dk_matrix = np.zeros( + (num_samples, num_poles + max(num_polys, 1)), dtype=np.complex128 + ) + compute_dk_matrix( + dk_matrix, eval_points, poles, conj_index, num_poles, num_polys, tol_high + ) + # For relaxed vector fitting, include the constant term in the sigma(s) + # function even when no polynomial terms are requested. This ensures the + # constrained system is well-posed when n_polys == 0. + if num_polys == 0: + dk_matrix[:, num_poles] = 1.0 + 0j + + scale_factor = ( + np.sqrt( + sum(norm(weights[m] * response_matrix[m]) ** 2 for m in range(num_vectors)) + ) + / num_samples + ) + lhs_matrix = np.zeros((num_vectors * (num_poles + 1), num_poles + 1)) + rhs_vector = np.zeros(num_vectors * (num_poles + 1)) + + for vec_idx in range(num_vectors): + vec_idx, lhs_block, rhs_block = process_constrained_block( + vec_idx, + dk_matrix, + weights, + response_matrix, + num_samples, + num_poles, + num_polys, + scale_factor, + num_vectors, + ) + i0 = vec_idx * (num_poles + 1) + i1 = (vec_idx + 1) * (num_poles + 1) + lhs_matrix[i0:i1] = lhs_block + rhs_vector[i0:i1] = rhs_block + + solution, *_ = wlstsq(lhs_matrix, rhs_vector) + coeffs = solution[:-1] + denom = solution[-1] + + if abs(denom) < tol_low or abs(denom) > tol_high: + lhs_matrix = np.zeros((num_vectors * num_poles, num_poles)) + rhs_vector = np.zeros(num_vectors * num_poles) + # Adjust denom + if denom == 0.0: + denom = 1.0 + elif abs(denom) < tol_low: + denom = np.sign(denom) * tol_low + elif abs(denom) > tol_high: + denom = np.sign(denom) * tol_high + + # Allocate output + lhs_matrix = np.zeros((num_vectors * num_poles, num_poles)) + rhs_vector = np.zeros(num_vectors * num_poles) + + for vec_idx in range(num_vectors): + vec_idx, lhs_block, rhs_block = process_unconstrained_block( + vec_idx, + dk_matrix, + weights, + response_matrix, + denom, + num_poles, + num_polys, + ) + i0 = vec_idx * num_poles + i1 = (vec_idx + 1) * num_poles + lhs_matrix[i0:i1] = lhs_block + rhs_vector[i0:i1] = rhs_block + + coeffs, *_ = wlstsq(lhs_matrix, rhs_vector) + + lambda_matrix = np.zeros((num_poles, num_poles)) + scale_vector = np.ones((num_poles, 1)) + + # Mask for real poles (conj_index == 0) + mask_real = conj_index == 0 + real_indices = np.where(mask_real)[0] + lambda_matrix[real_indices, real_indices] = np.real(poles[real_indices]) + + # Mask for start of complex conjugate pairs (conj_index == 1) + mask_cplx_start = conj_index == 1 + cplx_indices = np.where(mask_cplx_start)[0] + + # Extract real and imaginary parts of complex conjugate poles + real_parts = np.real(poles[cplx_indices]) + imag_parts = np.imag(poles[cplx_indices]) + + # Diagonal assignments + lambda_matrix[cplx_indices, cplx_indices] = real_parts + lambda_matrix[cplx_indices + 1, cplx_indices + 1] = real_parts + + # Off-diagonal assignments + lambda_matrix[cplx_indices, cplx_indices + 1] = imag_parts + lambda_matrix[cplx_indices + 1, cplx_indices] = -imag_parts + + # Scaling vector adjustments + scale_vector[cplx_indices, 0] = 2.0 + scale_vector[cplx_indices + 1, 0] = 0.0 + + residue_matrix = lambda_matrix - np.outer(scale_vector.squeeze(), coeffs) / denom + return eigvals(residue_matrix) + + +def solve_vector_block( + vec_idx: int, + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + num_poles: int, + num_polys: int, +) -> Tuple[int, np.ndarray, np.ndarray]: + """ + Solve the least-squares system for a single vector index. + + Parameters + ---------- + vec_idx : int + Index of the vector to solve. + dk_matrix : ndarray + Basis function evaluations of shape (num_samples, num_poles + num_polys). + weights : ndarray + Weight array of shape (num_vectors, num_samples). + response_matrix : ndarray + Response array of shape (num_vectors, num_samples). + num_poles : int + Number of poles. + num_polys : int + Number of polynomial coefficients. + + Returns + ------- + vec_idx : int + The index of the solved vector. + residues : ndarray + Solution vector for the residues (length = num_poles). + poly_coeffs : ndarray or None + Solution vector for polynomial coefficients (length = num_polys), or None if num_polys == 0. + """ + A = dk_matrix * weights[vec_idx][:, np.newaxis] + b = weights[vec_idx] * response_matrix[vec_idx] + + lhs_matrix = np.vstack((A.real, A.imag)) + rhs_vector = np.concatenate((b.real, b.imag)) + + x = wlstsq(lhs_matrix, rhs_vector)[0] + + residues = x[:num_poles] + poly_coeffs = x[num_poles : num_poles + num_polys] if num_polys > 0 else None + + return vec_idx, residues, poly_coeffs + + +def identify_residues( + num_poles: int, + poles: np.ndarray, + num_samples: int, + num_polys: int, + eval_points: np.ndarray, + num_vectors: int, + weights: np.ndarray, + response_matrix: np.ndarray, + poly_coefficients: np.ndarray, + residue_matrix: np.ndarray, +) -> Tuple[np.ndarray, float]: + """ + Internal routine to compute residues and polynomial coefficients. + + Parameters + ---------- + num_poles : int + Number of poles. + poles : np.ndarray + Current poles (complex), shape (num_poles,). + num_samples : int + Number of frequency samples. + num_polys : int + Number of polynomial terms. + eval_points : np.ndarray + Real frequency values, shape (num_samples,). + num_vectors : int + Number of response vectors. + weights : np.ndarray + Weighting matrix, shape (num_vectors, num_samples). + response_matrix : np.ndarray + Complex frequency responses, shape (num_vectors, num_samples). + poly_coefficients : np.ndarray + Array to store output polynomial coefficients (in-place). + residue_matrix : np.ndarray + Array to store output residues (in-place). + + Returns + ------- + Tuple[np.ndarray, float] + - Fitted response matrix (np.ndarray) + - Root-mean-square fitting error (float) + """ + conj_index = label_conjugate_poles(poles) + dk_matrix = np.zeros((num_samples, num_poles + num_polys), dtype=np.complex128) + + compute_dk_matrix(dk_matrix, eval_points, poles, conj_index, num_poles, num_polys) + + real_residues = np.zeros((num_vectors, num_poles), dtype=np.float64) + + for vec_idx in range(num_vectors): + vec_idx, residues, poly_coeffs = solve_vector_block( + vec_idx, + dk_matrix, + weights, + response_matrix, + num_poles, + num_polys, + ) + real_residues[vec_idx] = residues + if poly_coeffs is not None: + poly_coefficients[vec_idx] = poly_coeffs + + # Mask for real poles + mask_real = conj_index == 0 + real_indices = np.where(mask_real)[0] + residue_matrix[:, real_indices] = real_residues[:, real_indices] + + # Mask for first of complex conjugate pairs + mask_cplx_start = conj_index == 1 + cplx_indices = np.where(mask_cplx_start)[0] + + # Compute complex residues using vectorized operations + residue_matrix[:, cplx_indices] = ( + real_residues[:, cplx_indices] + 1j * real_residues[:, cplx_indices + 1] + ) + residue_matrix[:, cplx_indices + 1] = ( + real_residues[:, cplx_indices] - 1j * real_residues[:, cplx_indices + 1] + ) + + fit_result = evaluate(eval_points, poles, residue_matrix, poly_coefficients) + rms_error = norm(fit_result - response_matrix) / np.sqrt(num_vectors * num_samples) + return fit_result, rms_error + + +def label_conjugate_poles(poles: np.ndarray) -> np.ndarray: + """ + Ensure complex poles appear in conjugate pairs and label them accordingly. + + Parameters + ---------- + poles : np.ndarray + 1D array of complex poles. + + Returns + ------- + np.ndarray + Array of integers indicating pole type: + - 0: real pole + - 1: first in a complex-conjugate pair + - 2: second in a complex-conjugate pair + + Raises + ------ + ValueError + If any complex pole does not have a valid conjugate pair. + """ + num_poles = len(poles) + conj_index = np.zeros(num_poles, dtype=int) + + # Identify complex poles (nonzero imaginary part) + is_complex = np.imag(poles) != 0.0 + + # Find conjugate pairs: poles[i+1] ≈ conj(poles[i]) + is_pair_start = is_complex[:-1] & np.isclose(np.conj(poles[:-1]), poles[1:]) + + # Mark valid conjugate pair entries + conj_index[:-1][is_pair_start] = 1 # mark i with 1 + conj_index[1:][is_pair_start] = 2 # mark i+1 with 2 + + # Now validate: all complex poles must be part of valid conjugate pairs + unmatched_complex = is_complex & (conj_index == 0) + if np.any(unmatched_complex): + raise ValueError("Complex poles must appear in conjugate pairs") + + return conj_index diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 32f468306..66bd7148d 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -31,6 +31,7 @@ from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ from .pool import deplete from .reaction_rates import ReactionRates from .transfer_rates import TransferRates, ExternalSourceRates +from .keff_search_control import _KeffSearchControl __all__ = [ @@ -159,7 +160,7 @@ class TransportOperator(ABC): self.prev_res = prev_results @abstractmethod - def __call__(self, vec, source_rate): + def __call__(self, vec, source_rate) -> OperatorResult: """Runs a simulation. Parameters @@ -201,7 +202,7 @@ class TransportOperator(ABC): Returns ------- volume : dict of str to float - Volumes corresponding to materials in burn_list + Volumes corresponding to materials in full_burn_list nuc_list : list of str A list of all nuclide names. Used for sorting the simulation. burn_list : list of int @@ -209,6 +210,8 @@ class TransportOperator(ABC): simulation. full_burn_list : list of int All burnable materials in the geometry. + name_list : list of str + Material names corresponding to materials in full_burn_list """ def finalize(self): @@ -538,17 +541,15 @@ class Integrator(ABC): iterable of float. Alternatively, units can be specified for each step by passing an iterable of (value, unit) tuples. power : float or iterable of float, optional - Power of the reactor in [W]. A single value indicates that - the power is constant over all timesteps. An iterable - indicates potentially different power levels for each timestep. - For a 2D problem, the power can be given in [W/cm] as long - as the "volume" assigned to a depletion material is actually - an area in [cm^2]. Either ``power``, ``power_density``, or + Power of the reactor in [W]. A single value indicates that the power is + constant over all timesteps. An iterable indicates potentially different + power levels for each timestep. For a 2D problem, the power can be given + in [W/cm] as long as the "volume" assigned to a depletion material is + actually an area in [cm^2]. Either ``power``, ``power_density``, or ``source_rates`` must be specified. power_density : float or iterable of float, optional - Power density of the reactor in [W/gHM]. It is multiplied by - initial heavy metal inventory to get total power if ``power`` - is not specified. + Power density of the reactor in [W/gHM]. It is multiplied by initial + heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each interval in :attr:`timesteps` @@ -560,8 +561,8 @@ class Integrator(ABC): and 'MWd/kg' indicates that the values are given in burnup (MW-d of energy deposited per kilogram of initial heavy metal). solver : str or callable, optional - If a string, must be the name of the solver responsible for - solving the Bateman equations. Current options are: + If a string, must be the name of the solver responsible for solving the + Bateman equations. Current options are: * ``cram16`` - 16th order IPF CRAM * ``cram48`` - 48th order IPF CRAM [default] @@ -570,15 +571,22 @@ class Integrator(ABC): :attr:`solver`. .. versionadded:: 0.12 + substeps : int, optional + Number of substeps per depletion interval. When greater than 1, each + interval is subdivided into `substeps` identical sub-intervals and LU + factorizations may be reused across them, improving accuracy for + nuclides with large decay-constant × timestep products. + + .. versionadded:: 0.15.4 continue_timesteps : bool, optional Whether or not to treat the current solve as a continuation of a previous simulation. Defaults to `False`. When `False`, the depletion steps provided are appended to any previous steps. If `True`, the - timesteps provided to the `Integrator` must exacly match any that - exist in the `prev_results` passed to the `Operator`. The `power`, - `power_density`, or `source_rates` must match as well. The - method of specifying `power`, `power_density`, or - `source_rates` should be the same as the initial run. + timesteps provided to the `Integrator` must exacly match any that exist + in the `prev_results` passed to the `Operator`. The `power`, + `power_density`, or `source_rates` must match as well. The method of + specifying `power`, `power_density`, or `source_rates` should be the + same as the initial run. .. versionadded:: 0.15.1 @@ -598,15 +606,19 @@ class Integrator(ABC): :math:`\frac{\partial}{\partial t}\vec{n} = A_i\vec{n}_i` with a step size :math:`t_i`. Can be configured using the ``solver`` argument. User-supplied functions are expected to have the following signature: - ``solver(A, n0, t) -> n1`` where + ``solver(A, n0, t, substeps=1) -> n1``, where - * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the - depletion matrix - * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions - for a given material in atoms/cm3 - * ``t`` is a float of the time step size in seconds, and - * ``n1`` is a :class:`numpy.ndarray` of compositions at the - next time step. Expected to be of the same shape as ``n0`` + * ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion + matrix + * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for + a given material in atoms/cm3 + * ``t`` is a float of the time step size in seconds + * ``substeps`` is an optional integer number of substeps, and + * ``n1`` is a :class:`numpy.ndarray` of compositions at the next + time step. Expected to be of the same shape as ``n0`` + + Solvers that do not support multiple substeps should raise an exception + when ``substeps > 1``. transfer_rates : openmc.deplete.TransferRates Transfer rates for the depletion system used to model continuous @@ -629,6 +641,7 @@ class Integrator(ABC): source_rates: Optional[Union[float, Sequence[float]]] = None, timestep_units: str = 's', solver: str = "cram48", + substeps: int = 1, continue_timesteps: bool = False, ): if continue_timesteps and operator.prev_res is None: @@ -650,6 +663,8 @@ class Integrator(ABC): # Normalize timesteps and source rates seconds, source_rates = _normalize_timesteps( timesteps, source_rates, timestep_units, operator) + check_type("substeps", substeps, Integral) + check_greater_than("substeps", substeps, 0) if continue_timesteps: # Get timesteps and source rates from previous results @@ -681,9 +696,11 @@ class Integrator(ABC): self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) + self.substeps = substeps self.transfer_rates = None self.external_source_rates = None + self._keff_search_control = None if isinstance(solver, str): # Delay importing of cram module, which requires this file @@ -717,23 +734,37 @@ class Integrator(ABC): self._solver = func return - # Inspect arguments - if len(sig.parameters) != 3: - raise ValueError("Function {} does not support three arguments: " - "{!s}".format(func, sig)) + params = list(sig.parameters.values()) - for ix, param in enumerate(sig.parameters.values()): - if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD}: + # Inspect arguments + if len(params) != 4: + raise ValueError( + "Function {} must support four arguments " + "(A, n0, t, substeps=1): {!s}" + .format(func, sig)) + + for ix, param in enumerate(params): + if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD, + param.VAR_POSITIONAL}: raise ValueError( f"Keyword arguments like {ix} at position {param} are not allowed") + if len(params) == 4 and params[3].default != 1: + raise ValueError( + f"Fourth solver argument must default to 1, not {params[3].default}") + self._solver = func def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None): start = time.time() results = deplete( self._solver, self.chain, n, rates, dt, i, matrix_func, - self.transfer_rates, self.external_source_rates) + self.transfer_rates, self.external_source_rates, self.substeps) + + # Clip unphysical negative number densities + for r in results: + r.clip(min=0.0, out=r) + return time.time() - start, results @abstractmethod @@ -837,6 +868,37 @@ class Integrator(ABC): return (self.operator.prev_res[-1].time[0], len(self.operator.prev_res) - 1) + def _restore_keff_search_control(self, res: StepResult): + """Restore keff search control from restart results.""" + keff_search_root = res.keff_search_root + if keff_search_root is None: + raise ValueError( + "Cannot restore keff search control from restart " + "results because no stored keff_search_root is " + "available." + ) + self._keff_search_control.function(keff_search_root) + return keff_search_root + + def _get_bos_data(self, step_index, source_rate, bos_conc): + """Get beginning-of-step concentrations, rates, and control state.""" + if step_index > 0 or self.operator.prev_res is None: + if self._keff_search_control is not None and source_rate != 0.0: + keff_search_root = self._keff_search_control.run(bos_conc) + else: + keff_search_root = None + bos_conc, res = self._get_bos_data_from_operator( + step_index, source_rate, bos_conc) + else: + bos_conc, res = self._get_bos_data_from_restart( + source_rate, bos_conc) + if self._keff_search_control is not None and source_rate != 0.0: + keff_search_root = self._restore_keff_search_control(self.operator.prev_res[-1]) + else: + keff_search_root = None + + return bos_conc, res, keff_search_root + def integrate( self, final_step: bool = True, @@ -875,11 +937,8 @@ class Integrator(ABC): if output and comm.rank == 0: print(f"[openmc.deplete] t={t} s, dt={dt} s, source={source_rate}") - # Solve transport equation (or obtain result from restart) - if i > 0 or self.operator.prev_res is None: - n, res = self._get_bos_data_from_operator(i, source_rate, n) - else: - n, res = self._get_bos_data_from_restart(source_rate, n) + # Get beginning-of-step data from operator or restart results + n, res, keff_search_root = self._get_bos_data(i, source_rate, n) # Solve Bateman equations over time interval proc_time, n_end = self(n, res.rates, dt, source_rate, i) @@ -893,6 +952,7 @@ class Integrator(ABC): self._i_res + i, proc_time, write_rates=write_rates, + keff_search_root=keff_search_root, path=path ) @@ -906,6 +966,10 @@ class Integrator(ABC): # solve) if output and final_step and comm.rank == 0: print(f"[openmc.deplete] t={t} (final operator evaluation)") + if self._keff_search_control is not None and source_rate != 0.0: + keff_search_root = self._keff_search_control.run(n) + else: + keff_search_root = None res_final = self.operator(n, source_rate if final_step else 0.0) StepResult.save( self.operator, @@ -916,6 +980,7 @@ class Integrator(ABC): self._i_res + len(self), proc_time, write_rates=write_rates, + keff_search_root=keff_search_root, path=path ) self.operator.write_bos_data(len(self) + self._i_res) @@ -1048,6 +1113,101 @@ class Integrator(ABC): self.transfer_rates.set_redox(material, buffer, oxidation_states, timesteps) + def add_keff_search_control( + self, + function: Callable, + x0: float, + x1: float, + bracket: Sequence[float], + **search_kwargs + ): + """Add keff search to the integrator scheme. + + This method causes OpenMC to perform a keff search during depletion to + maintain a target keff by adjusting a model parameter through the + provided function. + + .. important:: + The function **must** modify the model through ``openmc.lib`` (e.g., + ``openmc.lib.cells``, ``openmc.lib.materials``) and **NOT** through + ``openmc.Model``. The function is called within a + :class:`openmc.lib.TemporarySession` context where only the C API + (``openmc.lib``) is available for modifications. + + Parameters + ---------- + function : Callable + Function that takes a single float argument and modifies the model + through :mod:`openmc.lib`. + x0 : float + Initial lower bound for the keff search. + x1 : float + Initial upper bound for the keff search. + bracket : sequence of float + Bracket interval [x_min, x_max] that constrains the allowed parameter + values during the keff search. This is a required parameter + that defines the absolute bounds for the search. The bracket must contain + exactly 2 elements with bracket[0] < bracket[1]. These values are passed + directly to the ``x_min`` and ``x_max`` optional arguments in + :meth:`openmc.Model.keff_search`, which enforce hard limits on the + parameter range. If the keff search converges to a value outside this + bracket, it will be clamped to the nearest bracket bound with a warning. + **search_kwargs + Additional keyword arguments passed to + :meth:`openmc.Model.keff_search`. Common options include: + + * ``target`` : float, optional + Target keff value to search for. Defaults to 1.0. + * ``k_tol`` : float, optional + Stopping criterion on the function value. Defaults to 1e-4. + * ``sigma_final`` : float, optional + Maximum accepted k-effective uncertainty. Defaults to 3e-4. + * ``maxiter`` : int, optional + Maximum number of iterations. Defaults to 50. + + See :meth:`openmc.Model.keff_search` for a complete list of + available options. + + Examples + -------- + Add keff search that adjusts a control rod position: + + >>> def adjust_rod_position(position): + ... openmc.lib.cells[rod_cell.id].translation = [0, 0, position] + >>> integrator.add_keff_search_control( + ... adjust_rod_position, + ... x0=0.0, + ... x1=5.0, + ... bracket=[-10,10], + ... target=1.0, + ... k_tol=1e-4 + ... ) + + Add keff search that adjusts the U235 density: + + >>> def set_u235_density(u235_density): + ... # Get the material from openmc.lib + ... lib_mat = openmc.lib.materials[material_id] + ... # Get current nuclides and densities + ... nuclides = lib_mat.nuclides + ... densities = lib_mat.densities + ... u235_idx = nuclides.index('U235') + ... densities[u235_idx] = u235_density + ... lib_mat.set_densities(nuclides, densities) + >>> integrator.add_keff_search_control( + ... set_u235_density, + ... x0=5.0e-4, + ... x1=1.0e-3, + ... bracket=[1.0e-4, 2.0e-3], + ... target=1.0 + ... ) + + .. versionadded:: 0.15.4 + + """ + self._keff_search_control = _KeffSearchControl( + self.operator, function, x0, x1, bracket, **search_kwargs) + @add_params class SIIntegrator(Integrator): r"""Abstract class for the Stochastic Implicit Euler integrators @@ -1067,17 +1227,15 @@ class SIIntegrator(Integrator): iterable of float. Alternatively, units can be specified for each step by passing an iterable of (value, unit) tuples. power : float or iterable of float, optional - Power of the reactor in [W]. A single value indicates that - the power is constant over all timesteps. An iterable - indicates potentially different power levels for each timestep. - For a 2D problem, the power can be given in [W/cm] as long - as the "volume" assigned to a depletion material is actually - an area in [cm^2]. Either ``power``, ``power_density``, or + Power of the reactor in [W]. A single value indicates that the power is + constant over all timesteps. An iterable indicates potentially different + power levels for each timestep. For a 2D problem, the power can be given + in [W/cm] as long as the "volume" assigned to a depletion material is + actually an area in [cm^2]. Either ``power``, ``power_density``, or ``source_rates`` must be specified. power_density : float or iterable of float, optional - Power density of the reactor in [W/gHM]. It is multiplied by - initial heavy metal inventory to get total power if ``power`` - is not specified. + Power density of the reactor in [W/gHM]. It is multiplied by initial + heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each interval in :attr:`timesteps` @@ -1089,11 +1247,11 @@ class SIIntegrator(Integrator): that the values are given in burnup (MW-d of energy deposited per kilogram of initial heavy metal). n_steps : int, optional - Number of stochastic iterations per depletion interval. - Must be greater than zero. Default : 10 + Number of stochastic iterations per depletion interval. Must be greater + than zero. Default : 10 solver : str or callable, optional - If a string, must be the name of the solver responsible for - solving the Bateman equations. Current options are: + If a string, must be the name of the solver responsible for solving the + Bateman equations. Current options are: * ``cram16`` - 16th order IPF CRAM * ``cram48`` - 48th order IPF CRAM [default] @@ -1102,16 +1260,23 @@ class SIIntegrator(Integrator): :attr:`solver`. .. versionadded:: 0.12 + substeps : int, optional + Number of substeps per depletion interval. When greater than 1, each + interval is subdivided into `substeps` identical sub-intervals and LU + factorizations may be reused across them, improving accuracy for + nuclides with large decay-constant × timestep products. + + .. versionadded:: 0.15.4 continue_timesteps : bool, optional Whether or not to treat the current solve as a continuation of a - previous simulation. Defaults to `False`. If `False`, all time - steps and source rates will be run in an append fashion and will run - after whatever time steps exist, if any. If `True`, the timesteps - provided to the `Integrator` must match exactly those that exist - in the `prev_results` passed to the `Opereator`. The `power`, - `power_density`, or `source_rates` must match as well. The - method of specifying `power`, `power_density`, or - `source_rates` should be the same as the initial run. + previous simulation. Defaults to `False`. If `False`, all time steps and + source rates will be run in an append fashion and will run after + whatever time steps exist, if any. If `True`, the timesteps provided to + the `Integrator` must match exactly those that exist in the + `prev_results` passed to the `Opereator`. The `power`, `power_density`, + or `source_rates` must match as well. The method of specifying `power`, + `power_density`, or `source_rates` should be the same as the initial + run. .. versionadded:: 0.15.1 @@ -1132,15 +1297,19 @@ class SIIntegrator(Integrator): :math:`\frac{\partial}{\partial t}\vec{n} = A_i\vec{n}_i` with a step size :math:`t_i`. Can be configured using the ``solver`` argument. User-supplied functions are expected to have the following signature: - ``solver(A, n0, t) -> n1`` where + ``solver(A, n0, t, substeps=1) -> n1``, where - * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the - depletion matrix - * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions - for a given material in atoms/cm3 - * ``t`` is a float of the time step size in seconds, and - * ``n1`` is a :class:`numpy.ndarray` of compositions at the - next time step. Expected to be of the same shape as ``n0`` + * ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion + matrix + * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for + a given material in atoms/cm3 + * ``t`` is a float of the time step size in seconds + * ``substeps`` is an optional integer number of substeps, and + * ``n1`` is a :class:`numpy.ndarray` of compositions at the next + time step. Expected to be of the same shape as ``n0`` + + Solvers that do not support multiple substeps should raise an exception + when ``substeps > 1``. .. versionadded:: 0.12 @@ -1156,13 +1325,16 @@ class SIIntegrator(Integrator): timestep_units: str = 's', n_steps: int = 10, solver: str = "cram48", + substeps: int = 1, continue_timesteps: bool = False, ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( operator, timesteps, power, power_density, source_rates, - timestep_units=timestep_units, solver=solver, continue_timesteps=continue_timesteps) + timestep_units=timestep_units, solver=solver, + substeps=substeps, + continue_timesteps=continue_timesteps) self.n_steps = n_steps def _get_bos_data_from_operator(self, step_index, step_power, n_bos): @@ -1292,12 +1464,12 @@ class DepSystemSolver(ABC): """ @abstractmethod - def __call__(self, A, n0, dt): + def __call__(self, A, n0, dt, substeps=1): """Solve the linear system of equations for depletion Parameters ---------- - A : scipy.sparse.csc_matrix + A : scipy.sparse.csc_array Sparse transmutation matrix ``A[j, i]`` describing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray @@ -1305,6 +1477,8 @@ class DepSystemSolver(ABC): material or an atom density dt : float Time [s] of the specific interval to be solved + substeps : int, optional + Number of substeps to use when the solver supports substepping. Returns ------- diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 873d7ca89..a2ff6dea0 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -17,13 +17,13 @@ from warnings import warn from typing import List import lxml.etree as ET -import scipy.sparse as sp -from openmc.checkvalue import check_type, check_greater_than, PathLike +from openmc.checkvalue import check_type, check_length, check_greater_than, PathLike from openmc.data import gnds_name, zam from openmc.exceptions import DataError from .nuclide import FissionYieldDistribution, Nuclide from .._xml import get_text +from .._sparse_compat import csc_array, dok_array import openmc.data @@ -269,6 +269,7 @@ class Chain: self.reactions = [] self.nuclide_dict = {} self._fission_yields = None + self._decay_matrix = None def __contains__(self, nuclide): return nuclide in self.nuclide_dict @@ -318,16 +319,16 @@ class Chain: String arguments in ``decay_files``, ``fpy_files``, and ``neutron_files`` will be treated as file names to be read. - Alternatively, :class:`openmc.data.endf.Evaluation` instances - can be included in these arguments. + Alternatively, :class:`openmc.data.endf.Evaluation` or + ``endf.Material`` instances can be included in these arguments. Parameters ---------- - decay_files : list of str or openmc.data.endf.Evaluation + decay_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF decay sub-library files - fpy_files : list of str or openmc.data.endf.Evaluation + fpy_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF neutron-induced fission product yield sub-library files - neutron_files : list of str or openmc.data.endf.Evaluation + neutron_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF neutron reaction sub-library files reactions : iterable of str, optional Transmutation reactions to include in the depletion chain, e.g., @@ -362,7 +363,7 @@ class Chain: print('Processing neutron sub-library files...') reactions = {} for f in neutron_files: - evaluation = openmc.data.endf.Evaluation(f) + evaluation = openmc.data.endf.as_evaluation(f) name = evaluation.gnds_name reactions[name] = {} for mf, mt, nc, mod in evaluation.reaction_list: @@ -412,6 +413,8 @@ class Chain: type_ = ','.join(mode.modes) if mode.daughter in decay_data: target = mode.daughter + elif 'sf' in type_: + target = None else: print('missing {} {} {}'.format( parent, type_, mode.daughter)) @@ -604,8 +607,67 @@ class Chain: out[nuc.name] = dict(yield_obj) return out - def form_matrix(self, rates, fission_yields=None): - """Forms depletion matrix. + @property + def decay_matrix(self): + """Sparse CSC decay transmutation matrix. + + Contains only terms from radioactive decay: diagonal loss terms + and off-diagonal gain terms (branching ratios, alpha/proton + production). Independent of reaction rates, so computed once and + cached. + + See Also + -------- + :meth:`form_rxn_matrix`, :meth:`form_matrix` + """ + if self._decay_matrix is None: + n = len(self) + rows, cols, vals = [], [], [] + + def setval(i, j, val): + rows.append(i) + cols.append(j) + vals.append(val) + + for i, nuc in enumerate(self.nuclides): + # Loss from radioactive decay + if nuc.half_life is not None: + decay_constant = math.log(2) / nuc.half_life + if decay_constant != 0.0: + setval(i, i, -decay_constant) + + # Gain from radioactive decay + if nuc.n_decay_modes != 0: + for decay_type, target, branching_ratio in nuc.decay_modes: + branch_val = branching_ratio * decay_constant + + # Allow for total annihilation for debug purposes + if branch_val != 0.0: + if target is not None and 'sf' not in decay_type: + k = self.nuclide_dict[target] + setval(k, i, branch_val) + + # Produce alphas and protons from decay + if 'alpha' in decay_type: + k = self.nuclide_dict.get('He4') + if k is not None: + count = decay_type.count('alpha') + setval(k, i, count * branch_val) + elif 'p' in decay_type: + k = self.nuclide_dict.get('H1') + if k is not None: + count = decay_type.count('p') + setval(k, i, count * branch_val) + + self._decay_matrix = csc_array((vals, (rows, cols)), shape=(n, n)) + return self._decay_matrix + + def form_rxn_matrix(self, rates, fission_yields=None): + """Form the reaction-rate portion of the transmutation matrix. + + Builds only the terms that depend on reaction rates: transmutation + reactions and fission product yields. Does not include radioactive + decay terms (see :attr:`decay_matrix`). Parameters ---------- @@ -619,20 +681,20 @@ class Chain: Returns ------- - scipy.sparse.csc_matrix - Sparse matrix representing depletion. + scipy.sparse.csc_array + Sparse matrix representing reaction-rate terms. See Also -------- - :meth:`get_default_fission_yields` + :attr:`decay_matrix`, :meth:`form_matrix` """ reactions = set() - n = len(self) - # we accumulate indices and value entries for everything and create the matrix - # in one step at the end to avoid expensive index checks scipy otherwise does. + # Accumulate indices/values and then create the matrix at the end to + # avoid expensive index checks scipy otherwise does. rows, cols, vals = [], [], [] + def setval(i, j, val): rows.append(i) cols.append(j) @@ -641,79 +703,78 @@ class Chain: if fission_yields is None: fission_yields = self.get_default_fission_yields() + # Save local variables to avoid attribute lookups in loop + index_nuc = rates.index_nuc + index_rx = rates.index_rx + for i, nuc in enumerate(self.nuclides): - # Loss from radioactive decay - if nuc.half_life is not None: - decay_constant = math.log(2) / nuc.half_life - if decay_constant != 0.0: - setval(i, i, -decay_constant) + if nuc.name not in index_nuc: + continue - # Gain from radioactive decay - if nuc.n_decay_modes != 0: - for decay_type, target, branching_ratio in nuc.decay_modes: - branch_val = branching_ratio * decay_constant + nuc_ind = index_nuc[nuc.name] + nuc_rates = rates[nuc_ind, :] - # Allow for total annihilation for debug purposes - if branch_val != 0.0: - if target is not None: - k = self.nuclide_dict[target] - setval(k, i, branch_val) + for r_type, target, _, br in nuc.reactions: + r_id = index_rx[r_type] + path_rate = nuc_rates[r_id] - # Produce alphas and protons from decay - if 'alpha' in decay_type: - k = self.nuclide_dict.get('He4') - if k is not None: - count = decay_type.count('alpha') - setval(k, i, count * branch_val) - elif 'p' in decay_type: - k = self.nuclide_dict.get('H1') - if k is not None: - count = decay_type.count('p') - setval(k, i, count * branch_val) + # Loss term -- make sure we only count loss once for + # reactions with branching ratios + if r_type not in reactions: + reactions.add(r_type) + if path_rate != 0.0: + setval(i, i, -path_rate) - if nuc.name in rates.index_nuc: - # Extract all reactions for this nuclide in this cell - nuc_ind = rates.index_nuc[nuc.name] - nuc_rates = rates[nuc_ind, :] + # Gain term; allow for total annihilation for debug purposes + if r_type != 'fission': + if target is not None and path_rate != 0.0: + k = self.nuclide_dict[target] + setval(k, i, path_rate * br) - for r_type, target, _, br in nuc.reactions: - # Extract reaction index, and then final reaction rate - r_id = rates.index_rx[r_type] - path_rate = nuc_rates[r_id] - - # Loss term -- make sure we only count loss once for - # reactions with branching ratios - if r_type not in reactions: - reactions.add(r_type) - if path_rate != 0.0: - setval(i, i, -path_rate) - - # Gain term; allow for total annihilation for debug purposes - if r_type != 'fission': - if target is not None and path_rate != 0.0: - k = self.nuclide_dict[target] - setval(k, i, path_rate * br) - - # Determine light nuclide production, e.g., (n,d) should - # produce H2 + # Determine light nuclide production, e.g., (n,d) should + # produce H2 + if path_rate != 0.0: light_nucs = REACTIONS[r_type].secondaries for light_nuc in light_nucs: k = self.nuclide_dict.get(light_nuc) if k is not None: setval(k, i, path_rate * br) - else: - for product, y in fission_yields[nuc.name].items(): - yield_val = y * path_rate - if yield_val != 0.0: - k = self.nuclide_dict[product] - setval(k, i, yield_val) + else: + for product, y in fission_yields[nuc.name].items(): + yield_val = y * path_rate + if yield_val != 0.0: + k = self.nuclide_dict[product] + setval(k, i, yield_val) - # Clear set of reactions - reactions.clear() + reactions.clear() - # Return CSC representation instead of DOK - return sp.csc_matrix((vals, (rows, cols)), shape=(n, n)) + return csc_array((vals, (rows, cols)), shape=(n, n)) + + def form_matrix(self, rates, fission_yields=None): + """Form the full transmutation matrix (decay + reactions). + + Parameters + ---------- + rates : numpy.ndarray + 2D array indexed by (nuclide, reaction) + fission_yields : dict, optional + Option to use a custom set of fission yields. Expected + to be of the form ``{parent : {product : f_yield}}`` + with string nuclide names for ``parent`` and ``product``, + and ``f_yield`` as the respective fission yield + + Returns + ------- + scipy.sparse.csc_array + Sparse matrix representing depletion. + + See Also + -------- + :attr:`decay_matrix`, :meth:`form_rxn_matrix`, + :meth:`get_default_fission_yields` + """ + return self.decay_matrix + self.form_rxn_matrix(rates, fission_yields) def add_redox_term(self, matrix, buffer, oxidation_states): r"""Adds a redox term to the depletion matrix from data contained in @@ -731,7 +792,7 @@ class Chain: Parameters ---------- - matrix : scipy.sparse.csc_matrix + matrix : scipy.sparse.csc_array Sparse matrix representing depletion buffer : dict Dictionary of buffer nuclides used to maintain anoins net balance. @@ -743,7 +804,7 @@ class Chain: states as integers (e.g., +1, 0). Returns ------- - matrix : scipy.sparse.csc_matrix + matrix : scipy.sparse.csc_array Sparse matrix with redox term added """ # Elements list with the same size as self.nuclides @@ -769,7 +830,7 @@ class Chain: for nuc, idx in buffer_idx.items(): array[idx] -= redox_change * buffer[nuc] / os[idx] - return sp.csc_matrix(array) + return csc_array(array) def form_rr_term(self, tr_rates, current_timestep, mats): """Function to form the transfer rate term matrices. @@ -800,43 +861,35 @@ class Chain: Returns ------- - scipy.sparse.csc_matrix + scipy.sparse.csc_array Sparse matrix representing transfer term. """ # Use DOK as intermediate representation n = len(self) - matrix = sp.dok_matrix((n, n)) + matrix = dok_array((n, n)) + + check_type("mats", mats, (tuple, str)) + if not isinstance(mats, str): + check_type("mats", mats, tuple, str) + check_length("mats", mats, 2, 2) + dest_mat, mat = mats + else: + mat = mats + dest_mat = None + + # Build transfer term + components = tr_rates.get_components(mat, current_timestep, dest_mat) for i, nuc in enumerate(self.nuclides): elm = re.split(r'\d+', nuc.name)[0] - # Build transfer terms (nuclide transfer only) - if isinstance(mats, str): - mat = mats - components = tr_rates.get_components(mat, current_timestep) - if not components: - break - if elm in components: - matrix[i, i] = sum( - tr_rates.get_external_rate(mat, elm, current_timestep)) - elif nuc.name in components: - matrix[i, i] = sum( - tr_rates.get_external_rate(mat, nuc.name, current_timestep)) - else: - matrix[i, i] = 0.0 - - # Build transfer terms (transfer from one material into another) - elif isinstance(mats, tuple): - dest_mat, mat = mats - components = tr_rates.get_components(mat, current_timestep, dest_mat) - if elm in components: - matrix[i, i] = tr_rates.get_external_rate( - mat, elm, current_timestep, dest_mat)[0] - elif nuc.name in components: - matrix[i, i] = tr_rates.get_external_rate( - mat, nuc.name, current_timestep, dest_mat)[0] - else: - matrix[i, i] = 0.0 + if elm in components: + key = elm + elif nuc.name in components: + key = nuc.name + else: + continue + matrix[i, i] = sum(tr_rates.get_external_rate(mat, key, current_timestep, dest_mat)) # Return CSC instead of DOK return matrix.tocsc() @@ -857,7 +910,7 @@ class Chain: Returns ------- - scipy.sparse.csc_matrix + scipy.sparse.csc_array Sparse vector representing external source term. """ @@ -865,15 +918,14 @@ class Chain: return # Use DOK as intermediate representation n = len(self) - vector = sp.dok_matrix((n, 1)) + vector = dok_array((n, 1)) + components = ext_source_rates.get_components(mat, current_timestep) for i, nuc in enumerate(self.nuclides): # Build source term vector - if nuc.name in ext_source_rates.get_components(mat, current_timestep): + if nuc.name in components: vector[i] = sum(ext_source_rates.get_external_rate( mat, nuc.name, current_timestep)) - else: - vector[i] = 0.0 # Return CSC instead of DOK return vector.tocsc() @@ -1372,6 +1424,7 @@ def _get_chain( def _invalidate_chain_cache(chain): """Invalidate the cache for a specific Chain (when it is modifed).""" + chain._decay_matrix = None if hasattr(chain, '_xml_path'): # Remove all entries with the same path as self._xml_path for key in list(_CHAIN_CACHE.keys()): diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 34bb28b49..a21d57d46 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -399,7 +399,7 @@ class CoupledOperator(OpenMCOperator): self.materials.export_to_xml(nuclides_to_ignore=self._decay_nucs) - def __call__(self, vec, source_rate): + def __call__(self, vec, source_rate) -> OperatorResult: """Runs a simulation. Simulation will abort under the following circumstances: diff --git a/openmc/deplete/cram.py b/openmc/deplete/cram.py index 53de83bb6..3594ffbe8 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -3,14 +3,15 @@ Implements two different forms of CRAM for use in openmc.deplete. """ +from functools import partial import numbers import numpy as np -import scipy.sparse as sp -import scipy.sparse.linalg as sla +from scipy.sparse.linalg import spsolve, splu -from openmc.checkvalue import check_type, check_length +from openmc.checkvalue import check_type, check_length, check_greater_than from .abc import DepSystemSolver +from .._sparse_compat import csc_array, eye_array __all__ = ["CRAM16", "CRAM48", "Cram16Solver", "Cram48Solver", "IPFCramSolver"] @@ -24,6 +25,12 @@ class IPFCramSolver(DepSystemSolver): Chebyshev Rational Approximation Method and Application to Burnup Equations `_," Nucl. Sci. Eng., 182:3, 297-318. + When `substeps` > 1, the time interval is split into `substeps` identical + sub-intervals and LU factorizations are reused across them, as described + in: A. Isotalo and M. Pusa, "`Improving the Accuracy of the Chebyshev + Rational Approximation Method Using Substeps + `_," Nucl. Sci. Eng., 183:1, 65-77. + Parameters ---------- alpha : numpy.ndarray @@ -55,12 +62,12 @@ class IPFCramSolver(DepSystemSolver): self.theta = theta self.alpha0 = alpha0 - def __call__(self, A, n0, dt): + def __call__(self, A, n0, dt, substeps=1): """Solve depletion equations using IPF CRAM Parameters ---------- - A : scipy.sparse.csr_matrix + A : scipy.sparse.csc_array Sparse transmutation matrix ``A[j, i]`` desribing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray @@ -68,6 +75,8 @@ class IPFCramSolver(DepSystemSolver): material or an atom density dt : float Time [s] of the specific interval to be solved + substeps : int, optional + Number of substeps per depletion interval. Returns ------- @@ -75,12 +84,25 @@ class IPFCramSolver(DepSystemSolver): Final compositions after ``dt`` """ - A = dt * sp.csc_matrix(A, dtype=np.float64) + check_type("substeps", substeps, numbers.Integral) + check_greater_than("substeps", substeps, 0) + + step_dt = dt if substeps == 1 else dt / substeps + A = step_dt * csc_array(A, dtype=np.float64) + ident = eye_array(A.shape[0], format='csc') + + if substeps == 1: + solvers = [partial(spsolve, A - theta * ident) for theta in self.theta] + else: + # Pre-compute LU factorizations and reuse them across substeps. + solvers = [splu(A - theta * ident).solve for theta in self.theta] + y = n0.copy() - ident = sp.eye(A.shape[0], format='csc') - for alpha, theta in zip(self.alpha, self.theta): - y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) - return y * self.alpha0 + for _ in range(substeps): + for alpha, solve in zip(self.alpha, solvers): + y += 2 * np.real(alpha * solve(y)) + y *= self.alpha0 + return y # Coefficients for IPF Cram 16 diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index bc99fc42d..d85d2e8a7 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -186,8 +186,6 @@ def apply_time_correction( # Apply TCF, broadcasting to the correct dimensions tcf.shape = (1, -1, 1, 1, 1) - new_tally._sum = tally_sum * tcf - new_tally._sum_sq = tally_sum_sq * (tcf*tcf) new_tally._mean = tally_mean * tcf new_tally._std_dev = tally_std_dev * tcf @@ -196,6 +194,8 @@ def apply_time_correction( if sum_nuclides: # Sum over parent nuclides (note that when combining different bins for # parent nuclide, we can't work directly on sum_sq) + new_tally._sum = None + new_tally._sum_sq = None new_tally._mean = new_tally.mean.sum(axis=1).reshape(shape) new_tally._std_dev = np.linalg.norm(new_tally.std_dev, axis=1).reshape(shape) new_tally._derived = True @@ -203,9 +203,10 @@ def apply_time_correction( # Remove ParentNuclideFilter new_tally.filters.pop(i_filter) else: - # Change shape back to (filter combinations, nuclides, scores) - new_tally._sum.shape = shape - new_tally._sum_sq.shape = shape + # Apply TCF and change shape back to (filter combinations, nuclides, + # scores) + new_tally._sum = (tally_sum * tcf).reshape(shape) + new_tally._sum_sq = (tally_sum_sq * (tcf*tcf)).reshape(shape) new_tally._mean.shape = shape new_tally._std_dev.shape = shape diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index f24b89868..14780d61a 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -379,17 +379,17 @@ class FluxCollapseHelper(ReactionRateHelper): nuclides_direct = self._rate_tally.nuclides shape = (len(nuclides_direct), len(self._reactions_direct)) rx_rates = self.rate_tally_means[mat_index].reshape(shape) + direct_rx_index = {score: i for i, score in enumerate(self._reactions_direct)} + direct_nuc_index = {nuc: i for i, nuc in enumerate(nuclides_direct)} mat = self._materials[mat_index] for name, i_nuc in zip(self.nuclides, nuc_index): for mt, score, i_rx in zip(self._mts, self._scores, react_index): if score in self._reactions_direct and name in nuclides_direct: - # Determine index in rx_rates - i_rx_direct = self._reactions_direct.index(score) - i_nuc_direct = nuclides_direct.index(name) - # Get reaction rate from tally + i_rx_direct = direct_rx_index[score] + i_nuc_direct = direct_nuc_index[name] self._results_cache[i_nuc, i_rx] = rx_rates[i_nuc_direct, i_rx_direct] else: # Use flux to collapse reaction rate (per N) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index c192907cf..bdfc32763 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -339,8 +339,12 @@ class IndependentOperator(OpenMCOperator): for i_nuc in nuc_index: nuc = self.nuc_ind_map[i_nuc] + if nuc not in xs._index_nuc: + continue for i_rx in react_index: rx = self.rx_ind_map[i_rx] + if rx not in xs._index_rx: + continue # Determine reaction rate by multiplying xs in [b] by flux # in [n-cm/src] to give [(reactions/src)*b-cm/atom] @@ -384,7 +388,7 @@ class IndependentOperator(OpenMCOperator): # Return number density vector return super().initial_condition(self.materials) - def __call__(self, vec, source_rate): + def __call__(self, vec, source_rate) -> OperatorResult: """Obtain the reaction rates Parameters diff --git a/openmc/deplete/keff_search_control.py b/openmc/deplete/keff_search_control.py new file mode 100644 index 000000000..49f7cc4df --- /dev/null +++ b/openmc/deplete/keff_search_control.py @@ -0,0 +1,128 @@ +from typing import Callable +from warnings import warn + +import openmc.lib + + +class _KeffSearchControl: + """Controller for keff search during depletion calculations. + + This class performs keff searches to maintain a target keff by adjusting a + model parameter through a provided function. + + Parameters + ---------- + operator : openmc.deplete.Operator + Depletion operator instance + function : Callable + Function that modifies the model based on a parameter value + x0 : float + Initial lower bound for the keff search + x1 : float + Initial upper bound for the keff search + bracket : list[float] + Absolute bracketing interval lower and upper. If the keff search + solution lies off these limits the closest limit will be set as new + result. + **search_kwargs : dict, optional + Additional keyword arguments to pass to :meth:`openmc.Model.keff_search` + + """ + def __init__(self, operator, function: Callable, x0: float, x1: float, bracket: list[float], **search_kwargs): + if len(bracket) != 2: + raise ValueError(f"bracket must have exactly 2 elements, got {len(bracket)}") + if bracket[0] >= bracket[1]: + raise ValueError(f"bracket[0] must be < bracket[1], got {bracket}") + self.x0 = x0 + self.x1 = x1 + self.operator = operator + self.function = function + self.search_kwargs = search_kwargs + self.search_kwargs['x_min'] = bracket[0] + self.search_kwargs['x_max'] = bracket[1] + + def run(self, x): + """Perform keff search and update the atom density vector. + + Parameters + ---------- + x : list of numpy.ndarray + Current atom density vector (atoms per material) + + Returns + ------- + root : float + Parameter value that achieves target keff + """ + root = self._search_for_keff() + self._update_vec(x) + return root + + def _search_for_keff(self) -> float: + """Perform the keff search using the model's keff_search method. + + Returns + ------- + float + Parameter value that achieves target keff + + Raises + ------ + ValueError + If the keff search fails to converge + """ + with openmc.lib.TemporarySession(self.operator.model): + # Only pass the first 3 required args plus explicitly provided kwargs + result = self.operator.model.keff_search( + self.function, self.x0, self.x1, **self.search_kwargs + ) + if not result.converged: + raise ValueError( + f"Search for keff failed to converge. " + f"Termination reason: {result.flag}" + ) + + root = result.root + + # Check if root is outside the bracket bounds and give a warning + if root < self.search_kwargs['x_min']: + warn(f"keff search result ({root:.6f}) is below the lower bracket " + f"bound ({self.search_kwargs['x_min']:.6f}).", UserWarning) + elif root > self.search_kwargs['x_max']: + warn(f"keff search result ({root:.6f}) is above the upper bracket " + f"bound ({self.search_kwargs['x_max']:.6f}).", UserWarning) + + # Restore the number of initial batches + openmc.lib.settings.set_batches(self.operator.model.settings.batches) + + return root + + def _update_vec(self, x): + """Update the atom density vector from openmc.lib.materials and AtomNumber object. + + The depletion vector ``x`` is rank-local, matching the materials owned + by ``self.operator.number`` on the current MPI rank. We therefore only + update entries for locally owned materials using the compositions + currently stored in ``openmc.lib.materials``. + + Parameters + ---------- + x : list of numpy.ndarray + Atom density vector to update (atoms per material) + + """ + number = self.operator.number + + for mat_idx, mat in enumerate(number.materials): + lib_material = openmc.lib.materials[int(mat)] + nuclides = lib_material.nuclides + densities = 1e24 * lib_material.densities + volume = number.get_mat_volume(mat) + + for nuc_idx, nuc in enumerate(number.burnable_nuclides): + if nuc in nuclides: + lib_nuc_idx = nuclides.index(nuc) + atom_density = densities[lib_nuc_idx] + else: + atom_density = number.get_atom_density(mat, nuc) + x[mat_idx][nuc_idx] = atom_density * volume diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 879a2d4ee..7049f7d31 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -36,7 +36,8 @@ DomainTypes: TypeAlias = Union[ Sequence[openmc.Cell], Sequence[openmc.Universe], openmc.MeshBase, - openmc.Filter + openmc.Filter, + Sequence[openmc.Filter] ] @@ -50,7 +51,8 @@ def get_microxs_and_flux( chain_file: PathLike | Chain | None = None, path_statepoint: PathLike | None = None, path_input: PathLike | None = None, - run_kwargs=None + run_kwargs=None, + reaction_rate_opts: dict | None = None, ) -> tuple[list[np.ndarray], list[MicroXS]]: """Generate microscopic cross sections and fluxes for multiple domains. @@ -68,8 +70,12 @@ def get_microxs_and_flux( ---------- model : openmc.Model OpenMC model object. Must contain geometry, materials, and settings. - domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase, or openmc.Filter + domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase, or openmc.Filter, or list of openmc.Filter Domains in which to tally reaction rates, or a spatial tally filter. + A list of filters can be provided to create one set of tallies per + filter (e.g., one :class:`~openmc.MeshMaterialFilter` per mesh) that + are all evaluated in a single transport solve. Results are + concatenated across all filters in order. nuclides : list of str Nuclides to get cross sections for. If not specified, all burnable nuclides from the depletion chain file are used. @@ -78,12 +84,19 @@ def get_microxs_and_flux( reactions listed in the depletion chain file are used. energies : iterable of float or str Energy group boundaries in [eV] or the name of the group structure. - If left as None energies will default to [0.0, 100e6] + If left as None, no energy filter is applied to the flux tally. When + `reaction_rate_mode` is "direct", these boundaries define the output + flux and microscopic cross section energy group structure. When + `reaction_rate_mode` is "flux", these boundaries define the multigroup + flux tally used to collapse continuous-energy cross sections; returned + fluxes and microscopic cross sections are one-group. reaction_rate_mode : {"direct", "flux"}, optional - Indicate how reaction rates should be calculated. The "direct" method - tallies reaction rates directly. The "flux" method tallies a multigroup - flux spectrum and then collapses multigroup reaction rates after a - transport solve (with an option to tally some reaction rates directly). + The "direct" method tallies reaction rates directly (per energy + group). The "flux" method tallies a multigroup flux spectrum and then + collapses reaction rates after a transport solve. When + `reaction_rate_opts` is provided with `reaction_rate_mode='flux'`, the + specified nuclide/reaction pairs are tallied directly and those values + override the flux-collapsed values. chain_file : PathLike or Chain, optional Path to the depletion chain XML file or an instance of openmc.deplete.Chain. Used to determine cross sections for materials not @@ -99,6 +112,12 @@ def get_microxs_and_flux( not kept. run_kwargs : dict, optional Keyword arguments passed to :meth:`openmc.Model.run` + reaction_rate_opts : dict, optional + When `reaction_rate_mode="flux"`, allows selecting a subset of + nuclide/reaction pairs to be computed via direct reaction-rate tallies + over one energy bin spanning the full `energies` range. Supported keys: + "nuclides", "reactions". If "reactions" are specified without + "nuclides", all selected nuclides are used. Returns ------- @@ -127,39 +146,85 @@ def get_microxs_and_flux( nuclides = [nuc.name for nuc in chain.nuclides if nuc.name in nuclides_with_data] - # Set up the reaction rate and flux tallies + # Set up the reaction rate and flux tallies. When energies are omitted, no + # energy filter is needed for the transport calculation. A one-group energy + # range is still needed later if flux collapse is requested. + collapse_energies = energies if energies is None: - energies = [0.0, 100.0e6] - if isinstance(energies, str): + energy_filter = None + collapse_energies = [0.0, 100.0e6] + elif isinstance(energies, str): energy_filter = openmc.EnergyFilter.from_group_structure(energies) else: energy_filter = openmc.EnergyFilter(energies) + # Build list of domain filters if isinstance(domains, openmc.Filter): - domain_filter = domains + domain_filters = [domains] elif isinstance(domains, openmc.MeshBase): - domain_filter = openmc.MeshFilter(domains) + domain_filters = [openmc.MeshFilter(domains)] + elif isinstance(domains, Sequence) and len(domains) > 0 and \ + isinstance(domains[0], openmc.Filter): + domain_filters = list(domains) elif isinstance(domains[0], openmc.Material): - domain_filter = openmc.MaterialFilter(domains) + domain_filters = [openmc.MaterialFilter(domains)] elif isinstance(domains[0], openmc.Cell): - domain_filter = openmc.CellFilter(domains) + domain_filters = [openmc.CellFilter(domains)] elif isinstance(domains[0], openmc.Universe): - domain_filter = openmc.UniverseFilter(domains) + domain_filters = [openmc.UniverseFilter(domains)] else: raise ValueError(f"Unsupported domain type: {type(domains[0])}") - flux_tally = openmc.Tally(name='MicroXS flux') - flux_tally.filters = [domain_filter, energy_filter] - flux_tally.scores = ['flux'] - model.tallies = [flux_tally] - + # Prepare reaction-rate nuclides/reactions + rr_nuclides: list[str] = [] + rr_reactions: list[str] = [] if reaction_rate_mode == 'direct': - rr_tally = openmc.Tally(name='MicroXS RR') - rr_tally.filters = [domain_filter, energy_filter] - rr_tally.nuclides = nuclides - rr_tally.multiply_density = False - rr_tally.scores = reactions - model.tallies.append(rr_tally) + rr_nuclides = list(nuclides) + rr_reactions = list(reactions) + elif reaction_rate_mode == 'flux' and reaction_rate_opts: + opts = reaction_rate_opts or {} + rr_reactions = list(opts.get('reactions', [])) + if rr_reactions: + rr_nuclides = list(opts.get('nuclides', nuclides)) + else: + rr_nuclides = list(opts.get('nuclides', [])) + # Keep only requested pairs within overall sets + if rr_nuclides: + rr_nuclides = [n for n in rr_nuclides if n in set(nuclides)] + if rr_reactions: + rr_reactions = [r for r in rr_reactions if r in set(reactions)] + + # Use 1-group energy filter for RR in flux mode + has_rr = bool(rr_nuclides and rr_reactions) + if has_rr and reaction_rate_mode == 'flux' and energy_filter is not None: + rr_energy_filter = openmc.EnergyFilter( + [energy_filter.values[0], energy_filter.values[-1]]) + else: + rr_energy_filter = energy_filter + + # Create one flux tally (and optionally one RR tally) per domain filter. + flux_tallies = [] + rr_tallies = [] + model.tallies = [] + for i, domain_filter in enumerate(domain_filters): + flux_tally = openmc.Tally(name=f'MicroXS flux {i}') + flux_tally.filters = [domain_filter] + if energy_filter is not None: + flux_tally.filters.append(energy_filter) + flux_tally.scores = ['flux'] + model.tallies.append(flux_tally) + flux_tallies.append(flux_tally) + + if has_rr: + rr_tally = openmc.Tally(name=f'MicroXS RR {i}') + rr_tally.filters = [domain_filter] + if rr_energy_filter is not None: + rr_tally.filters.append(rr_energy_filter) + rr_tally.nuclides = rr_nuclides + rr_tally.multiply_density = False + rr_tally.scores = rr_reactions + model.tallies.append(rr_tally) + rr_tallies.append(rr_tally) if openmc.lib.is_initialized: openmc.lib.finalize() @@ -168,7 +233,8 @@ def get_microxs_and_flux( model.export_to_model_xml() comm.barrier() # Reinitialize with tallies - openmc.lib.init(intracomm=comm) + output = run_kwargs.get('output', True) if run_kwargs else True + openmc.lib.init(intracomm=comm, output=output) with TemporaryDirectory() as temp_dir: # Indicate to run in temporary directory unless being executed through @@ -196,44 +262,75 @@ def get_microxs_and_flux( # Read in tally results (on all ranks) with StatePoint(statepoint_path) as sp: - if reaction_rate_mode == 'direct': - rr_tally = sp.tallies[rr_tally.id] - rr_tally._read_results() - flux_tally = sp.tallies[flux_tally.id] - flux_tally._read_results() + for i in range(len(flux_tallies)): + flux_tallies[i] = sp.tallies[flux_tallies[i].id] + flux_tallies[i]._read_results() + if rr_tallies: + rr_tallies[i] = sp.tallies[rr_tallies[i].id] + rr_tallies[i]._read_results() - # Get flux values and make energy groups last dimension - flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) - flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + # Concatenate results across all domain filters + fluxes = [] + all_flux_arrays = [] + for flux_tally in flux_tallies: + # Get flux values and make energy groups last dimension + flux = flux_tally.get_reshaped_data() + if energy_filter is None: + flux = flux[..., np.newaxis] # (domains, 1, 1, groups) + else: + # (domains, groups, 1, 1) -> (domains, 1, 1, groups) + flux = np.moveaxis(flux, 1, -1) + all_flux_arrays.append(flux) + fluxes.extend(flux.squeeze((1, 2))) - # Create list where each item corresponds to one domain - fluxes = list(flux.squeeze((1, 2))) + # If we built reaction-rate tallies, compute microscopic cross sections + if rr_tallies: + direct_micros = [] + for flux_arr, rr_tally in zip(all_flux_arrays, rr_tallies): + flux = flux_arr + # Get reaction rates and make energy groups last dimension + reaction_rates = rr_tally.get_reshaped_data() + if rr_energy_filter is None: + # (domains, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = reaction_rates[..., np.newaxis] + else: + # (domains, groups, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = np.moveaxis(reaction_rates, 1, -1) - if reaction_rate_mode == 'direct': - # Get reaction rates - reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) + # If RR is 1-group, sum flux over groups + if reaction_rate_mode == "flux": + flux = flux.sum(axis=-1, keepdims=True) - # Make energy groups last dimension - reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) + xs = np.zeros_like(reaction_rates) + d, _, _, g = np.nonzero(flux) + xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g] + direct_micros.extend( + MicroXS(xs_i, rr_nuclides, rr_reactions) for xs_i in xs) - # Divide RR by flux to get microscopic cross sections. The indexing - # ensures that only non-zero flux values are used, and broadcasting is - # applied to align the shapes of reaction_rates and flux for division. - xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups) - d, _, _, g = np.nonzero(flux) - xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g] - - # Create lists where each item corresponds to one domain - micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs] - else: - micros = [MicroXS.from_multigroup_flux( - energies=energies, + if reaction_rate_mode == 'flux': + # Compute flux-collapsed microscopic XS + flux_micros = [MicroXS.from_multigroup_flux( + energies=collapse_energies, multigroup_flux=flux_i, chain_file=chain_file, nuclides=nuclides, reactions=reactions ) for flux_i in fluxes] + # We need to return one-group fluxes to match the microscopic cross + # sections, which are always one-group by virtue of the collapse + fluxes = [flux.sum(keepdims=True) for flux in fluxes] + + # Decide which micros to use and merge if needed + if reaction_rate_mode == 'flux' and rr_tallies: + micros = [m1.merge(m2) for m1, m2 in zip(flux_micros, direct_micros)] + elif rr_tallies: + micros = direct_micros + else: + micros = flux_micros + # Reset tallies model.tallies = original_tallies @@ -484,6 +581,79 @@ class MicroXS: return cls(data, nuclides, reactions) + def merge(self, other: Self, prefer: str = 'other') -> Self: + """Merge two MicroXS objects by taking the union of nuclides/reactions. + + If the two objects contain overlapping nuclide/reaction entries, values + from `other` will overwrite values from `self` when `prefer='other'`. + When `prefer='self'`, values from `self` are retained for overlapping + entries, and values from `other` are used only for non-overlapping + entries. + + Parameters + ---------- + other : MicroXS + Other MicroXS instance to merge with this one. + prefer : {"other", "self"} + Which instance's data should take precedence on overlap. + + Returns + ------- + MicroXS + New instance containing the merged data. + """ + check_value('prefer', prefer, {'other', 'self'}) + + # Require same number of energy groups + if self.data.shape[2] != other.data.shape[2]: + raise ValueError( + 'Cannot merge MicroXS with different number of energy groups: ' + f"{self.data.shape[2]} vs {other.data.shape[2]}. Ensure that " + 'both were generated with consistent group structures and ' + 'treatments (e.g., both multigroup or both collapsed).' + ) + + # Build unified axes preserving order (self first, then other's new) + new_nuclides = list(self.nuclides) + for nuc in other.nuclides: + if nuc not in self._index_nuc: + new_nuclides.append(nuc) + new_reactions = list(self.reactions) + for rx in other.reactions: + if rx not in self._index_rx: + new_reactions.append(rx) + + # Allocate and fill from self (self's nuclides/reactions map to the + # first indices of new_nuclides/new_reactions by construction) + groups = self.data.shape[2] + data = np.zeros((len(new_nuclides), len(new_reactions), groups)) + idx_n = {nuc: i for i, nuc in enumerate(new_nuclides)} + idx_r = {rx: i for i, rx in enumerate(new_reactions)} + + n_self = len(self.nuclides) + r_self = len(self.reactions) + data[:n_self, :r_self] = self.data + + # Build destination index arrays for other's nuclides/reactions + dst_n = np.array([idx_n[nuc] for nuc in other.nuclides]) + dst_r = np.array([idx_r[rx] for rx in other.reactions]) + + # Copy from other, respecting precedence + if prefer == 'other': + data[np.ix_(dst_n, dst_r)] = other.data + else: + # Copy only entries where nuc or rx is absent from self + nuc_is_new = np.array( + [nuc not in self._index_nuc for nuc in other.nuclides]) + rx_is_new = np.array( + [rx not in self._index_rx for rx in other.reactions]) + mask = nuc_is_new[:, np.newaxis] | rx_is_new[np.newaxis, :] + src_i, src_j = np.where(mask) + if src_i.size: + data[dst_n[src_i], dst_r[src_j]] = other.data[src_i, src_j] + + return MicroXS(data, new_nuclides, new_reactions) + def write_microxs_hdf5( micros: Sequence[MicroXS], diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 2d7694093..7928e89ee 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -211,7 +211,7 @@ class OpenMCOperator(TransportOperator): "section data.") warn(msg) if mat.depletable: - burnable_mats.add(str(mat.id)) + burnable_mats.add((str(mat.id), mat.name)) if mat.volume is None: if mat.name is None: msg = ("Volume not specified for depletable material " @@ -229,9 +229,12 @@ class OpenMCOperator(TransportOperator): "No depletable materials were found in the model.") # Sort the sets - burnable_mats = sorted(burnable_mats, key=int) + burnable_mats = sorted(burnable_mats, key=lambda x: int(x[0])) model_nuclides = sorted(model_nuclides) + # Store material names for later use + burnable_mats, self.name_list = zip(*burnable_mats) + # Construct a global nuclide dictionary, burned first nuclides = list(self.chain.nuclide_dict) for nuc in model_nuclides: @@ -541,6 +544,8 @@ class OpenMCOperator(TransportOperator): A list of all material IDs to be burned. Used for sorting the simulation. full_burn_list : list List of all burnable material IDs + name_list : list of str + Material names corresponding to materials in burn_list """ nuc_list = self.number.burnable_nuclides @@ -554,4 +559,4 @@ class OpenMCOperator(TransportOperator): volume_list = comm.allgather(volume) volume = {k: v for d in volume_list for k, v in d.items()} - return volume, nuc_list, burn_list, self.burnable_mats + return volume, nuc_list, burn_list, self.burnable_mats, self.name_list diff --git a/openmc/deplete/pool.py b/openmc/deplete/pool.py index aa348c02a..19ad0ada5 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -5,10 +5,11 @@ Provided to avoid some circular imports from itertools import repeat, starmap from multiprocessing import Pool -from scipy.sparse import bmat, hstack, vstack, csc_matrix import numpy as np +from scipy.sparse import hstack from openmc.mpi import comm +from .._sparse_compat import block_array # Configurable switch that enables / disables the use of # multiprocessing routines during depletion @@ -41,14 +42,15 @@ def _distribute(items): j += chunk_size def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, - transfer_rates=None, external_source_rates=None, *matrix_args): + transfer_rates=None, external_source_rates=None, substeps=1, + *matrix_args): """Deplete materials using given reaction rates for a specified time Parameters ---------- func : callable Function to use to get new compositions. Expected to have the signature - ``func(A, n0, t) -> n1`` + ``func(A, n0, t, substeps=1) -> n1``. chain : openmc.deplete.Chain Depletion chain n : list of numpy.ndarray @@ -73,6 +75,8 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, External source rates for continuous removal/feed. .. versionadded:: 0.15.3 + substeps : int, optional + Number of substeps to pass to solvers that support substepping. matrix_args: Any, optional Additional arguments passed to matrix_func @@ -159,11 +163,11 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, cols.append(None) rows.append(cols) - matrix = bmat(rows) + matrix = block_array(rows) # Concatenate vectors of nuclides in one n_multi = np.concatenate(n) - n_result = func(matrix, n_multi, dt) + n_result = func(matrix, n_multi, dt, substeps) # Split back the nuclide vector result into the original form n_result = np.split(n_result, np.cumsum([len(i) for i in n])[:-1]) @@ -194,10 +198,10 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, # of the nuclide vectors for i, matrix in enumerate(matrices): if not np.equal(*matrix.shape): - matrices[i] = vstack([matrix, csc_matrix([0]*matrix.shape[1])]) + matrix.resize(matrix.shape[1], matrix.shape[1]) n[i] = np.append(n[i], 1.0) - inputs = zip(matrices, n, repeat(dt)) + inputs = zip(matrices, n, repeat(dt), repeat(substeps)) if USE_MULTIPROCESSING: with Pool(NUM_PROCESSES) as pool: diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 97277cbda..6c19531e7 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -1,23 +1,26 @@ from __future__ import annotations from collections.abc import Sequence +from contextlib import nullcontext import copy from datetime import datetime import json +from numbers import Integral from pathlib import Path import numpy as np import openmc from . import IndependentOperator, PredictorIntegrator +from .chain import Chain from .microxs import get_microxs_and_flux, write_microxs_hdf5, read_microxs_hdf5 from .results import Results from ..checkvalue import PathLike from ..mpi import comm from openmc.lib import TemporarySession -from openmc.utility_funcs import change_directory def get_activation_materials( - model: openmc.Model, mmv: openmc.MeshMaterialVolumes + model: openmc.Model, + mmv_list: list[openmc.MeshMaterialVolumes] ) -> openmc.Materials: """Get a list of activation materials for each mesh element/material. @@ -31,35 +34,35 @@ def get_activation_materials( ---------- model : openmc.Model The full model containing the geometry and materials. - mmv : openmc.MeshMaterialVolumes - The mesh material volumes object containing the materials and their - volumes for each mesh element. + mmv_list : list of openmc.MeshMaterialVolumes + List of mesh material volumes objects, one per mesh, containing the + materials and their volumes for each mesh element. Returns ------- openmc.Materials A list of materials, each corresponding to a unique mesh element and - material combination. + material combination across all meshes. """ - # Get the material ID, volume, and element index for each element-material - # combination - mat_ids = mmv._materials[mmv._materials > -1] - volumes = mmv._volumes[mmv._materials > -1] - elems, _ = np.where(mmv._materials > -1) - # Get all materials in the model material_dict = model._get_all_materials() # Create a new activation material for each element-material combination + # across all meshes materials = openmc.Materials() - for elem, mat_id, vol in zip(elems, mat_ids, volumes): - mat = material_dict[mat_id] - new_mat = mat.clone() - new_mat.depletable = True - new_mat.name = f'Element {elem}, Material {mat_id}' - new_mat.volume = vol - materials.append(new_mat) + for mesh_idx, mmv in enumerate(mmv_list): + mat_ids = mmv._materials[mmv._materials > -1] + volumes = mmv._volumes[mmv._materials > -1] + elems, _ = np.where(mmv._materials > -1) + + for elem, mat_id, vol in zip(elems, mat_ids, volumes): + mat = material_dict[mat_id] + new_mat = mat.clone() + new_mat.depletable = True + new_mat.name = f'Mesh {mesh_idx}, Element {elem}, Material {mat_id}' + new_mat.volume = vol + materials.append(new_mat) return materials @@ -70,7 +73,9 @@ class R2SManager: This class is responsible for managing the materials and sources needed for mesh-based or cell-based R2S calculations. It provides methods to get activation materials and decay photon sources based on the mesh/cells and - materials in the OpenMC model. + materials in the OpenMC model. Multiple meshes can be specified as domains, + in which case each element--material combination of each mesh is treated as + an activation region (meshes are assumed to be non-overlapping). This class supports the use of a different models for the neutron and photon transport calculation. However, for cell-based calculations, it assumes that @@ -83,17 +88,20 @@ class R2SManager: ---------- neutron_model : openmc.Model The OpenMC model to use for neutron transport. - domains : openmc.MeshBase or Sequence[openmc.Cell] - The mesh or a sequence of cells that represent the spatial units over - which the R2S calculation will be performed. + domains : openmc.MeshBase or Sequence[openmc.MeshBase] or Sequence[openmc.Cell] + The mesh(es) or a sequence of cells that represent the spatial units + over which the R2S calculation will be performed. When a single + :class:`~openmc.MeshBase` or a sequence of meshes is given, each + element--material combination across all meshes is treated as an + activation region. photon_model : openmc.Model, optional The OpenMC model to use for photon transport calculations. If None, a shallow copy of the neutron_model will be created and used. Attributes ---------- - domains : openmc.MeshBase or Sequence[openmc.Cell] - The mesh or a sequence of cells that represent the spatial units over + domains : list of openmc.MeshBase or Sequence[openmc.Cell] + The meshes or a sequence of cells that represent the spatial units over which the R2S calculation will be performed. neutron_model : openmc.Model The OpenMC model used for neutron transport. @@ -101,7 +109,7 @@ class R2SManager: The OpenMC model used for photon transport calculations. method : {'mesh-based', 'cell-based'} Indicates whether the R2S calculation uses mesh elements ('mesh-based') - as the spatial discetization or a list of a cells ('cell-based'). + as the spatial discretization or a list of cells ('cell-based'). results : dict A dictionary that stores results from the R2S calculation. @@ -109,7 +117,7 @@ class R2SManager: def __init__( self, neutron_model: openmc.Model, - domains: openmc.MeshBase | Sequence[openmc.Cell], + domains: openmc.MeshBase | Sequence[openmc.MeshBase] | Sequence[openmc.Cell], photon_model: openmc.Model | None = None, ): self.neutron_model = neutron_model @@ -126,9 +134,14 @@ class R2SManager: self.photon_model = photon_model if isinstance(domains, openmc.MeshBase): self.method = 'mesh-based' + self.domains = [domains] + elif isinstance(domains, Sequence) and len(domains) > 0 and \ + isinstance(domains[0], openmc.MeshBase): + self.method = 'mesh-based' + self.domains = list(domains) else: self.method = 'cell-based' - self.domains = domains + self.domains = list(domains) self.results = {} def run( @@ -139,11 +152,12 @@ class R2SManager: photon_time_indices: Sequence[int] | None = None, output_dir: PathLike | None = None, bounding_boxes: dict[int, openmc.BoundingBox] | None = None, - chain_file: PathLike | None = None, + chain_file: PathLike | Chain | None = None, micro_kwargs: dict | None = None, mat_vol_kwargs: dict | None = None, run_kwargs: dict | None = None, operator_kwargs: dict | None = None, + by_parent_nuclide: bool = False, ): """Run the R2S calculation. @@ -167,7 +181,7 @@ class R2SManager: timesteps. For example, if two timesteps are specified, the array of times would contain three entries, and [2] would indicate computing photon results at the last time. A value of None indicates to run - photon transport for each time. + photon transport at each time that has a decay photon source. output_dir : PathLike, optional Path to directory where R2S calculation outputs will be saved. If not provided, a timestamped directory 'r2s_YYYY-MM-DDTHH-MM-SS' is @@ -177,9 +191,10 @@ class R2SManager: Dictionary mapping cell IDs to bounding boxes used for spatial source sampling in cell-based R2S calculations. Required if method is 'cell-based'. - chain_file : PathLike, optional - Path to the depletion chain XML file to use during activation. If - not provided, the default configured chain file will be used. + chain_file : PathLike or openmc.deplete.Chain, optional + Path to the depletion chain XML file or depletion chain object to + use during activation. If not provided, the default configured + chain file will be used. micro_kwargs : dict, optional Additional keyword arguments passed to :func:`openmc.deplete.get_microxs_and_flux` during the neutron @@ -194,6 +209,11 @@ class R2SManager: operator_kwargs : dict, optional Additional keyword arguments passed to :class:`openmc.deplete.IndependentOperator`. + by_parent_nuclide : bool, optional + Whether to score photon tallies separately for each parent + radionuclide. A :class:`~openmc.ParentNuclideFilter` is added to + tallies that do not already contain one, with bins determined from + the prepared decay photon sources. Returns ------- @@ -206,6 +226,8 @@ class R2SManager: # consistency (different ranks may have slightly different times) stamp = datetime.now().strftime('%Y-%m-%dT%H-%M-%S') output_dir = Path(comm.bcast(f'r2s_{stamp}')) + else: + output_dir = Path(output_dir) # Set run_kwargs for the neutron transport step if micro_kwargs is None: @@ -216,22 +238,39 @@ class R2SManager: operator_kwargs = {} run_kwargs.setdefault('output', False) micro_kwargs.setdefault('run_kwargs', run_kwargs) - # If a chain file is provided, prefer it for steps 1 and 2 - if chain_file is not None: - micro_kwargs.setdefault('chain_file', chain_file) - operator_kwargs.setdefault('chain_file', chain_file) - self.step1_neutron_transport( - output_dir / 'neutron_transport', mat_vol_kwargs, micro_kwargs - ) - self.step2_activation( - timesteps, source_rates, timestep_units, output_dir / 'activation', - operator_kwargs=operator_kwargs - ) - self.step3_photon_transport( - photon_time_indices, bounding_boxes, output_dir / 'photon_transport', - mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs + # DecaySpectrum distributions are resolved in the C++ solver using + # OPENMC_CHAIN_FILE. If a Chain object was passed, write an XML + # representation alongside the R2S outputs. + if isinstance(chain_file, Chain): + output_dir.mkdir(parents=True, exist_ok=True) + chain_path = output_dir / 'chain.xml' + if comm.rank == 0: + chain_file.export_to_xml(chain_path) + comm.barrier() + else: + chain_path = chain_file + + chain_context = ( + openmc.config.patch('chain_file', chain_path) + if chain_path is not None else nullcontext() ) + with chain_context: + self.step1_neutron_transport( + output_dir / 'neutron_transport', mat_vol_kwargs, micro_kwargs + ) + self.step2_activation( + timesteps, source_rates, timestep_units, + output_dir / 'activation', operator_kwargs=operator_kwargs + ) + self.step3_photon_source( + photon_time_indices, bounding_boxes, output_dir / 'photon_transport', + mat_vol_kwargs=mat_vol_kwargs, + ) + self.step4_photon_transport( + output_dir / 'photon_transport', run_kwargs=run_kwargs, + by_parent_nuclide=by_parent_nuclide, + ) return output_dir @@ -243,11 +282,13 @@ class R2SManager: ): """Run the neutron transport step. - This step computes the material volume fractions on the mesh, creates a - mesh-material filter, and retrieves the fluxes and microscopic cross - sections for each mesh/material combination. This step will populate the - 'fluxes' and 'micros' keys in the results dictionary. For a mesh-based - calculation, it will also populate the 'mesh_material_volumes' key. + This step computes the material volume fractions on each mesh, creates + mesh-material filters, and retrieves the fluxes and microscopic cross + sections for each mesh/material combination via a single transport + solve. This step will populate the 'fluxes' and 'micros' keys in the + results dictionary. For a mesh-based calculation, it will also populate + the 'mesh_material_volumes' key (a list of + :class:`~openmc.MeshMaterialVolumes`, one per mesh). Parameters ---------- @@ -266,18 +307,28 @@ class R2SManager: output_dir.mkdir(parents=True, exist_ok=True) if self.method == 'mesh-based': - # Compute material volume fractions on the mesh + # Compute material volume fractions on each mesh if mat_vol_kwargs is None: mat_vol_kwargs = {} - self.results['mesh_material_volumes'] = mmv = comm.bcast( - self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs)) + mat_vol_kwargs.setdefault('bounding_boxes', True) - # Save results to file - if comm.rank == 0: - mmv.save(output_dir / 'mesh_material_volumes.npz') + mmv_list = [] + domain_filters = [] + for i, mesh in enumerate(self.domains): + mmv = comm.bcast( + mesh.material_volumes(self.neutron_model, **mat_vol_kwargs)) + mmv_list.append(mmv) - # Create mesh-material filter based on what combos were found - domains = openmc.MeshMaterialFilter.from_volumes(self.domains, mmv) + # Save results to file + if comm.rank == 0: + mmv.save(output_dir / f'mesh_material_volumes_{i}.npz') + + # Create mesh-material filter for this mesh + domain_filters.append( + openmc.MeshMaterialFilter.from_volumes(mesh, mmv)) + + self.results['mesh_material_volumes'] = mmv_list + domains = domain_filters else: domains: Sequence[openmc.Cell] = self.domains @@ -307,7 +358,7 @@ class R2SManager: # Run neutron transport and get fluxes and micros. Run via openmc.lib to # maintain a consistent parallelism strategy with the activation step. - with TemporarySession(): + with TemporarySession(output=False): self.results['fluxes'], self.results['micros'] = get_microxs_and_flux( self.neutron_model, domains, **micro_kwargs) @@ -356,8 +407,9 @@ class R2SManager: if self.method == 'mesh-based': # Get unique material for each (mesh, material) combination - mmv = self.results['mesh_material_volumes'] - self.results['activation_materials'] = get_activation_materials(self.neutron_model, mmv) + mmv_list = self.results['mesh_material_volumes'] + self.results['activation_materials'] = get_activation_materials( + self.neutron_model, mmv_list) else: # Create unique material for each cell activation_mats = openmc.Materials() @@ -397,23 +449,20 @@ class R2SManager: # Get depletion results self.results['depletion_results'] = Results(output_path) - def step3_photon_transport( + def step3_photon_source( self, time_indices: Sequence[int] | None = None, bounding_boxes: dict[int, openmc.BoundingBox] | None = None, output_dir: PathLike = 'photon_transport', mat_vol_kwargs: dict | None = None, - run_kwargs: dict | None = None, ): - """Run the photon transport step. + """Create decay photon sources. - This step performs photon transport calculations using decay photon - sources created from the activated materials. For each specified time, - it creates appropriate photon sources and runs a transport calculation. - In mesh-based mode, the sources are created using the mesh material - volumes, while in cell-based mode, they are created using bounding boxes - for each cell. This step will populate the 'photon_tallies' key in the - results dictionary. + This step creates decay photon sources from the activated materials for + each specified time. In mesh-based mode, the sources are created using + mesh material volumes, while in cell-based mode, they are created using + bounding boxes for each cell. This step will populate the + 'photon_sources' key in the results dictionary. Parameters ---------- @@ -423,40 +472,54 @@ class R2SManager: timesteps. For example, if two timesteps are specified, the array of times would contain three entries, and [2] would indicate computing photon results at the last time. A value of None indicates to run - photon transport for each time. + photon transport at each time that has a decay photon source. bounding_boxes : dict[int, openmc.BoundingBox], optional Dictionary mapping cell IDs to bounding boxes used for spatial source sampling in cell-based R2S calculations. Required if method is 'cell-based'. output_dir : PathLike, optional - Path to directory where photon transport outputs will be saved. + Path to directory where photon source outputs will be saved. mat_vol_kwargs : dict, optional Additional keyword arguments passed to :meth:`openmc.MeshBase.material_volumes`. - run_kwargs : dict, optional - Additional keyword arguments passed to :meth:`openmc.Model.run` - during the photon transport step. By default, output is disabled. """ + # Do not retain sources from an earlier successful call if this source + # preparation attempt fails. + self.results.pop('photon_sources', None) + # TODO: Automatically determine bounding box for each cell if bounding_boxes is None and self.method == 'cell-based': raise ValueError("bounding_boxes must be provided for cell-based " "R2S calculations.") - # Set default run arguments if not provided - if run_kwargs is None: - run_kwargs = {} - run_kwargs.setdefault('output', False) - - # Write out JSON file with tally IDs that can be used for loading - # results output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) - # Get default time indices if not provided + # Determine and validate time indices before preparing source data. + n_steps = len(self.results['depletion_results']) + implicit_time_indices = time_indices is None if time_indices is None: - n_steps = len(self.results['depletion_results']) time_indices = list(range(n_steps)) + else: + time_indices = list(time_indices) + if not time_indices: + raise ValueError('time_indices must contain at least one index') + + normalized_indices = [] + for index in time_indices: + if isinstance(index, bool) or not isinstance(index, Integral): + raise TypeError('time_indices must contain only integers') + index = int(index) + if index < -n_steps or index >= n_steps: + raise IndexError( + f'Photon time index {index} is out of range for ' + f'{n_steps} depletion results') + normalized_index = index % n_steps + normalized_indices.append(normalized_index) + + # Remove duplicates while preserving order + time_indices = list(dict.fromkeys(normalized_indices)) # Check whether the photon model is different neutron_univ = self.neutron_model.geometry.root_universe @@ -467,12 +530,120 @@ class R2SManager: # photon model if it is different from the neutron model to account for # potential material changes if self.method == 'mesh-based' and different_photon_model: - self.results['mesh_material_volumes_photon'] = photon_mmv = comm.bcast( - self.domains.material_volumes(self.photon_model, **mat_vol_kwargs)) + if mat_vol_kwargs is None: + mat_vol_kwargs = {} + photon_mmv_list = [] + for i, mesh in enumerate(self.domains): + photon_mmv = comm.bcast( + mesh.material_volumes(self.photon_model, **mat_vol_kwargs)) + photon_mmv_list.append(photon_mmv) - # Save photon MMV results to file - if comm.rank == 0: - photon_mmv.save(output_dir / 'mesh_material_volumes.npz') + # Save photon MMV results to file + if comm.rank == 0: + photon_mmv.save( + output_dir / f'mesh_material_volumes_{i}.npz') + + self.results['mesh_material_volumes_photon'] = photon_mmv_list + + # Get dictionary of cells in the photon model + if different_photon_model: + photon_cells = self.photon_model.geometry.get_all_cells() + + # Determine eligible work items upfront (independent of time index). + if self.method == 'mesh-based': + work_items = self._get_mesh_work_items() + else: + work_items = [] + for cell, original_mat in zip( + self.domains, self.results['activation_materials']): + if different_photon_model: + if cell.id not in photon_cells or \ + cell.fill.id != photon_cells[cell.id].fill.id: + continue + work_items.append((cell, original_mat, bounding_boxes[cell.id])) + + # Create decay photon sources for each time index + photon_sources = { + time_index: self._create_photon_sources(time_index, work_items) + for time_index in time_indices + } + + # Determine if any times have no decay photon sources. If the user + # didn't specify any specific time indices, remove those times from the + # photon_sources dictionary. If the user did specify time indices, raise + # an error if any of those times have no decay photon sources. + empty_indices = [ + time_index for time_index, sources in photon_sources.items() + if not sources + ] + if implicit_time_indices: + for time_index in empty_indices: + del photon_sources[time_index] + if not photon_sources: + raise RuntimeError( + 'No decay photon sources were found at any depletion time') + elif empty_indices: + indices = ', '.join(str(index) for index in empty_indices) + raise RuntimeError( + f'No decay photon source was found for requested time ' + f'indices: {indices}') + + self.results['photon_sources'] = photon_sources + + def step4_photon_transport( + self, + output_dir: PathLike = 'photon_transport', + run_kwargs: dict | None = None, + by_parent_nuclide: bool = False, + ): + """Run photon transport using prepared decay photon sources. + + This step runs a photon transport calculation for each source list + created by :meth:`step3_photon_source`. It will populate the + 'photon_tallies' key in the results dictionary. + + Parameters + ---------- + output_dir : PathLike, optional + Path to directory where photon transport outputs will be saved. + run_kwargs : dict, optional + Additional keyword arguments passed to :meth:`openmc.Model.run`. + By default, output is disabled. + by_parent_nuclide : bool, optional + Whether to score photon tallies separately for each parent + radionuclide. A :class:`~openmc.ParentNuclideFilter` is added to + tallies that do not already contain one, with bins determined from + the prepared decay photon sources. + """ + if 'photon_sources' not in self.results: + raise RuntimeError( + 'Photon sources must be created with step3_photon_source ' + 'before running photon transport.') + photon_sources = self.results['photon_sources'] + if not photon_sources: + raise RuntimeError( + 'No decay photon sources are available for transport') + + if by_parent_nuclide: + radionuclides = sorted({ + nuclide + for sources in photon_sources.values() + for source in sources + for nuclide in source.energy.nuclides + }) + + if radionuclides: + parent_filter = openmc.ParentNuclideFilter(radionuclides) + for tally in self.photon_model.tallies: + if not tally.contains_filter(openmc.ParentNuclideFilter): + tally.filters.append(parent_filter) + + if run_kwargs is None: + run_kwargs = {} + run_kwargs.setdefault('output', False) + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) if comm.rank == 0: tally_ids = [tally.id for tally in self.photon_model.tallies] @@ -481,49 +652,11 @@ class R2SManager: self.results['photon_tallies'] = {} - # Get dictionary of cells in the photon model - if different_photon_model: - photon_cells = self.photon_model.geometry.get_all_cells() + # Ensure photon transport is enabled in settings. + self.photon_model.settings.photon_transport = True - for time_index in time_indices: - # Create decay photon source - if self.method == 'mesh-based': - self.photon_model.settings.source = \ - self.get_decay_photon_source_mesh(time_index) - else: - sources = [] - results = self.results['depletion_results'] - for cell, original_mat in zip(self.domains, self.results['activation_materials']): - # Skip if the cell is not in the photon model or the - # material has changed - if different_photon_model: - if cell.id not in photon_cells or \ - cell.fill.id != photon_cells[cell.id].fill.id: - continue - - # Get bounding box for the cell - bounding_box = bounding_boxes[cell.id] - - # Get activated material composition - activated_mat = results[time_index].get_material(str(original_mat.id)) - - # Create decay photon source source - space = openmc.stats.Box(*bounding_box) - energy = activated_mat.get_decay_photon_energy() - strength = energy.integral() if energy is not None else 0.0 - source = openmc.IndependentSource( - space=space, - energy=energy, - particle='photon', - strength=strength, - constraints={'domains': [cell]} - ) - sources.append(source) - self.photon_model.settings.source = sources - - # Convert time_index (which may be negative) to a normal index - if time_index < 0: - time_index = len(self.results['depletion_results']) + time_index + for time_index, sources in photon_sources.items(): + self.photon_model.settings.source = sources # Run photon transport calculation photon_dir = Path(output_dir) / f'time_{time_index}' @@ -536,98 +669,109 @@ class R2SManager: sp.tallies[tally.id] for tally in self.photon_model.tallies ] - def get_decay_photon_source_mesh( - self, - time_index: int = -1 - ) -> list[openmc.MeshSource]: - """Create decay photon source for a mesh-based calculation. + def _get_mesh_work_items(self): + """Enumerate mesh-based work items across all meshes. - This function creates N :class:`MeshSource` objects where N is the - maximum number of unique materials that appears in a single mesh - element. For each mesh element-material combination, and - IndependentSource instance is created with a spatial constraint limited - the sampled decay photons to the correct region. - - When the photon transport model is different from the neutron model, the - photon MeshMaterialVolumes is used to determine whether an (element, - material) combination exists in the photon model. - - Parameters - ---------- - time_index : int, optional - Time index for the decay photon source. Default is -1 (last time). + Returns a list of (index_mat, mat_id, bbox) tuples for each eligible + mesh element--material combination, where index_mat is the index into + the activation materials list, mat_id is the material ID, and bbox is + the bounding box for that mesh element--material combination. Returns ------- - list of openmc.MeshSource - A list of MeshSource objects, each containing IndependentSource - instances for the decay photons in the corresponding mesh element. - + list of tuple + Each tuple is (index_mat, mat_id, bbox). """ - mat_dict = self.neutron_model._get_all_materials() + mmv_list = self.results['mesh_material_volumes'] + photon_mmv_list = self.results.get('mesh_material_volumes_photon') - # Some MeshSource objects will have empty positions; create a "null source" - # that is used for this case - null_source = openmc.IndependentSource(particle='photon', strength=0.0) - - # List to hold sources for each MeshSource (length = N) - source_lists = [] - - # Index in the overall list of activated materials + work_items = [] index_mat = 0 + for mesh_idx, mat_vols in enumerate(mmv_list): + photon_mat_vols = photon_mmv_list[mesh_idx] \ + if photon_mmv_list is not None else None - # Get various results from previous steps - mat_vols = self.results['mesh_material_volumes'] - materials = self.results['activation_materials'] - results = self.results['depletion_results'] - photon_mat_vols = self.results.get('mesh_material_volumes_photon') + n_elements = mat_vols.num_elements + for index_elem in range(n_elements): + if photon_mat_vols is not None: + photon_materials = { + mat_id + for mat_id, _ in photon_mat_vols.by_element(index_elem) + if mat_id is not None + } - # Total number of mesh elements - n_elements = mat_vols.num_elements - - for index_elem in range(n_elements): - # Determine which materials exist in the photon model for this element - if photon_mat_vols is not None: - photon_materials = { - mat_id - for mat_id, _ in photon_mat_vols.by_element(index_elem) - if mat_id is not None - } - - for j, (mat_id, _) in enumerate(mat_vols.by_element(index_elem)): - # Skip void volume - if mat_id is None: - continue - - # Skip if this material doesn't exist in photon model - if photon_mat_vols is not None and mat_id not in photon_materials: + for mat_id, _, bbox in mat_vols.by_element( + index_elem, include_bboxes=True): + if mat_id is None: + continue + if photon_mat_vols is not None \ + and mat_id not in photon_materials: + index_mat += 1 + continue + work_items.append((index_mat, mat_id, bbox)) index_mat += 1 - continue - # Check whether a new MeshSource object is needed - if j >= len(source_lists): - source_lists.append([null_source]*n_elements) + return work_items - # Get activated material composition + def _create_photon_sources(self, time_index, work_items): + """Create decay photon sources for a set of regions. + + Builds :class:`openmc.IndependentSource` objects with + :class:`openmc.stats.DecaySpectrum` energy distributions that will be + serialized to XML and resolved against the depletion chain by the C++ + solver. + + Parameters + ---------- + time_index : int + Index into depletion results. + work_items : list of tuple + For mesh-based: list of (index_mat, mat_id, bbox). + For cell-based: list of (cell, original_mat, bbox). + + Returns + ------- + list of openmc.IndependentSource + Photon sources for each activated region. + """ + step_result = self.results['depletion_results'][time_index] + materials = self.results['activation_materials'] + mesh_based = self.method == 'mesh-based' + if mesh_based: + mat_dict = self.neutron_model._get_all_materials() + + sources = [] + for item in work_items: + if mesh_based: + index_mat, domain_id, bbox = item original_mat = materials[index_mat] - activated_mat = results[time_index].get_material(str(original_mat.id)) + domain = mat_dict[domain_id] + else: + cell, original_mat, bbox = item + domain = cell - # Create decay photon source source - energy = activated_mat.get_decay_photon_energy() - if energy is not None: - strength = energy.integral() - source_lists[j][index_elem] = openmc.IndependentSource( - energy=energy, - particle='photon', - strength=strength, - constraints={'domains': [mat_dict[mat_id]]} - ) + activated_mat = step_result.get_material(str(original_mat.id)) + nuclides = activated_mat.get_nuclide_atom_densities() + if not nuclides: + continue - # Increment index of activated material - index_mat += 1 + # Eliminate nuclides with zero density + nuclides = {nuclide: density for nuclide, density in nuclides.items() + if density > 0} - # Return list of mesh sources - return [openmc.MeshSource(self.domains, sources) for sources in source_lists] + energy = openmc.stats.DecaySpectrum(nuclides, activated_mat.volume) + energy.clip(inplace=True) + if not energy.nuclides: + continue + + sources.append(openmc.IndependentSource( + space=openmc.stats.Box(bbox.lower_left, bbox.upper_right), + energy=energy, + particle='photon', + constraints={'domains': [domain]}, + )) + + return sources def load_results(self, path: PathLike): """Load results from a previous R2S calculation. @@ -643,10 +787,13 @@ class R2SManager: # Load neutron transport results neutron_dir = path / 'neutron_transport' if self.method == 'mesh-based': - mmv_file = neutron_dir / 'mesh_material_volumes.npz' - if mmv_file.exists(): - self.results['mesh_material_volumes'] = \ - openmc.MeshMaterialVolumes.from_npz(mmv_file) + mmv_files = sorted(neutron_dir.glob('mesh_material_volumes*.npz'), + key=lambda p: int(p.stem.split('_')[-1]) + if p.stem[-1].isdigit() else 0) + if mmv_files: + self.results['mesh_material_volumes'] = [ + openmc.MeshMaterialVolumes.from_npz(f) for f in mmv_files + ] fluxes_file = neutron_dir / 'fluxes.npy' if fluxes_file.exists(): self.results['fluxes'] = list(np.load(fluxes_file, allow_pickle=True)) @@ -670,10 +817,15 @@ class R2SManager: # Load photon mesh material volumes if they exist (for mesh-based calculations) if self.method == 'mesh-based': - photon_mmv_file = photon_dir / 'mesh_material_volumes.npz' - if photon_mmv_file.exists(): - self.results['mesh_material_volumes_photon'] = \ - openmc.MeshMaterialVolumes.from_npz(photon_mmv_file) + photon_mmv_files = sorted( + photon_dir.glob('mesh_material_volumes*.npz'), + key=lambda p: int(p.stem.split('_')[-1]) + if p.stem[-1].isdigit() else 0) + if photon_mmv_files: + self.results['mesh_material_volumes_photon'] = [ + openmc.MeshMaterialVolumes.from_npz(f) + for f in photon_mmv_files + ] # Load tally IDs from JSON file tally_ids_path = photon_dir / 'tally_ids.json' diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index e1fcb26b6..0cf0141d2 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import numbers import bisect import math from collections.abc import Iterable +from typing import Literal from warnings import warn import h5py import numpy as np +import openmc +from .chain import Chain, _get_chain from .stepresult import StepResult, VERSION_RESULTS import openmc.checkvalue as cv from openmc.data import atomic_mass, AVOGADRO @@ -103,7 +108,8 @@ class Results(list): mat: Material | str, units: str = "Bq/cm3", by_nuclide: bool = False, - volume: float | None = None + volume: float | None = None, + chain_file: Literal[False] | None | PathLike | Chain = None ) -> tuple[np.ndarray, np.ndarray | list[dict]]: """Get activity of material over time. @@ -113,24 +119,33 @@ class Results(list): ---------- mat : openmc.Material, str Material object or material id to evaluate - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'} Specifies the type of activity to return, options include total - activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity [Bq/cm3]. + activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity + [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. volume : float, optional Volume of the material. If not passed, defaults to using the :attr:`Material.volume` attribute. + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For + ``None`` or an explicit chain, nuclides absent from the chain fall + back to ENDF/B-VIII.0 data. + + .. versionadded:: 0.15.4 Returns ------- times : numpy.ndarray Array of times in [s] activities : numpy.ndarray or List[dict] - Array of total activities if by_nuclide = False (default) - or list of dictionaries of activities by nuclide if - by_nuclide = True. + Array of total activities if by_nuclide = False (default) or list of + dictionaries of activities by nuclide if by_nuclide = True. """ if isinstance(mat, Material): @@ -140,6 +155,13 @@ class Results(list): else: raise TypeError('mat should be of type openmc.Material or str') + if chain_file is not False: + if chain_file is None: + if openmc.config.get('chain_file') is not None: + chain_file = _get_chain(None) + else: + chain_file = _get_chain(chain_file) + times = np.empty_like(self, dtype=float) if by_nuclide: activities = [None] * len(self) @@ -149,7 +171,8 @@ class Results(list): # Evaluate activity for each depletion time for i, result in enumerate(self): times[i] = result.time[0] - activities[i] = result.get_material(mat_id).get_activity(units, by_nuclide, volume) + activities[i] = result.get_material(mat_id).get_activity( + units, by_nuclide, volume, chain_file=chain_file) return times, activities @@ -231,7 +254,7 @@ class Results(list): ---------- mat : openmc.Material, str Material object or material id to evaluate. - units : {'W', 'W/g', 'W/kg', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3', 'W/m3'} Specifies the units of decay heat to return. Options include total heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. by_nuclide : bool diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index ff39e9acb..9f638a058 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -12,11 +12,12 @@ import h5py import numpy as np import openmc -from openmc.mpi import comm, MPI from openmc.checkvalue import PathLike +from openmc.mpi import MPI, comm + from .reaction_rates import ReactionRates -VERSION_RESULTS = (1, 2) +VERSION_RESULTS = (1, 3) __all__ = ["StepResult"] @@ -57,6 +58,8 @@ class StepResult: proc_time : int Average time spent depleting a material across all materials and processes + keff_search_root : float + The root returned by the keff search control. """ def __init__(self): @@ -70,8 +73,10 @@ class StepResult: self.index_mat = None self.index_nuc = None self.mat_to_hdf5_ind = None + self.name_list = None self.data = None + self.keff_search_root = None def __repr__(self): t = self.time[0] @@ -138,7 +143,7 @@ class StepResult: def n_hdf5_mats(self): return len(self.mat_to_hdf5_ind) - def allocate(self, volume, nuc_list, burn_list, full_burn_list): + def allocate(self, volume, nuc_list, burn_list, full_burn_list, name_list=None): """Allocate memory for depletion step data Parameters @@ -151,12 +156,15 @@ class StepResult: A list of all mat IDs to be burned. Used for sorting the simulation. full_burn_list : list of str List of all burnable material IDs + name_list : list of str, optional + Material names corresponding to materials in full_burn_list """ self.volume = copy.deepcopy(volume) self.index_nuc = {nuc: i for i, nuc in enumerate(nuc_list)} self.index_mat = {mat: i for i, mat in enumerate(burn_list)} self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} + self.mat_to_name = dict(zip(full_burn_list, name_list)) if name_list is not None else {} # Create storage array self.data = np.zeros((self.n_mat, self.n_nuc)) @@ -184,7 +192,7 @@ class StepResult: # Direct transfer direct_attrs = ("time", "k", "source_rate", "index_nuc", - "mat_to_hdf5_ind", "proc_time") + "mat_to_hdf5_ind", "mat_to_name", "proc_time") for attr in direct_attrs: setattr(new, attr, getattr(self, attr)) # Get applicable slice of data @@ -192,15 +200,15 @@ class StepResult: new.rates = self.rates[ranges] return new - def get_material(self, mat_id): + def get_material(self, mat_id: str | int) -> openmc.Material: """Return material object for given depleted composition .. versionadded:: 0.13.2 Parameters ---------- - mat_id : str - Material ID as a string + mat_id : str or int + Material ID as a string or integer Returns ------- @@ -213,6 +221,9 @@ class StepResult: If specified material ID is not found in the StepResult """ + # Coerce to str since internal dictionaries use str keys + mat_id = str(mat_id) + with warnings.catch_warnings(): warnings.simplefilter('ignore', openmc.IDWarning) material = openmc.Material(material_id=int(mat_id)) @@ -223,6 +234,8 @@ class StepResult: f'mat_id {mat_id} not found in StepResult. Available mat_id ' f'values are {list(self.volume.keys())}' ) from e + if mat_id in self.mat_to_name: + material.name = self.mat_to_name[mat_id] for nuc, _ in sorted(self.index_nuc.items(), key=lambda x: x[1]): atoms = self[mat_id, nuc] if atoms <= 0.0: @@ -313,6 +326,8 @@ class StepResult: mat_single_group = mat_group.create_group(mat) mat_single_group.attrs["index"] = self.mat_to_hdf5_ind[mat] mat_single_group.attrs["volume"] = self.volume[mat] + if mat in self.mat_to_name: + mat_single_group.attrs["name"] = self.mat_to_name[mat] nuc_group = handle.create_group("nuclides") @@ -356,6 +371,10 @@ class StepResult: "depletion time", (1,), maxshape=(None,), dtype="float64") + handle.create_dataset( + "keff_search_root", (1,), maxshape=(None,), + dtype="float64") + def _to_hdf5(self, handle, index, parallel=False, write_rates: bool = False): """Converts results object into an hdf5 object. @@ -388,6 +407,7 @@ class StepResult: time_dset = handle["/time"] source_rate_dset = handle["/source_rate"] proc_time_dset = handle["/depletion time"] + keff_search_root_dset = handle["/keff_search_root"] # Get number of results stored number_shape = list(number_dset.shape) @@ -421,6 +441,10 @@ class StepResult: proc_shape[0] = new_shape proc_time_dset.resize(proc_shape) + keff_search_root_shape = list(keff_search_root_dset.shape) + keff_search_root_shape[0] = new_shape + keff_search_root_dset.resize(keff_search_root_shape) + # If nothing to write, just return if len(self.index_mat) == 0: return @@ -440,6 +464,7 @@ class StepResult: proc_time_dset[index] = ( self.proc_time / (comm.size * self.n_hdf5_mats) ) + keff_search_root_dset[index] = self.keff_search_root @classmethod def from_hdf5(cls, handle, step): @@ -488,6 +513,10 @@ class StepResult: if step < proc_time_dset.shape[0]: results.proc_time = proc_time_dset[step] + if "keff_search_root" in handle: + keff_search_root_dset = handle["/keff_search_root"] + results.keff_search_root = keff_search_root_dset[step] + if results.proc_time is None: results.proc_time = np.array([np.nan]) @@ -495,6 +524,7 @@ class StepResult: results.volume = {} results.index_mat = {} results.index_nuc = {} + results.mat_to_name = {} rxn_nuc_to_ind = {} rxn_to_ind = {} @@ -504,6 +534,8 @@ class StepResult: results.volume[mat] = vol results.index_mat[mat] = ind + if "name" in mat_handle.attrs: + results.mat_to_name[mat] = mat_handle.attrs["name"] for nuc, nuc_handle in handle["/nuclides"].items(): ind_atom = nuc_handle.attrs["atom number index"] @@ -539,6 +571,7 @@ class StepResult: step_ind, proc_time=None, write_rates: bool = False, + keff_search_root=None, path: PathLike = "depletion_results.h5" ): """Creates and writes depletion results to disk @@ -563,17 +596,19 @@ class StepResult: processes. write_rates : bool, optional Whether reaction rates should be written to the results file. + keff_search_root : float + The root returned by the keff search control. path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. .. versionadded:: 0.14.0 """ # Get indexing terms - vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() + vol_dict, nuc_list, burn_list, full_burn_list, name_list = op.get_results_info() # Create results results = StepResult() - results.allocate(vol_dict, nuc_list, burn_list, full_burn_list) + results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, name_list) n_mat = len(burn_list) @@ -590,6 +625,7 @@ class StepResult: results.proc_time = proc_time if results.proc_time is not None: results.proc_time = comm.reduce(proc_time, op=MPI.SUM) + results.keff_search_root = keff_search_root if not Path(path).is_file(): Path(path).parent.mkdir(parents=True, exist_ok=True) diff --git a/openmc/element.py b/openmc/element.py index f1d8f5f24..5deede971 100644 --- a/openmc/element.py +++ b/openmc/element.py @@ -184,8 +184,6 @@ class Element(str): for nuclide in absent_nuclides: if nuclide in ['O17', 'O18'] and 'O16' in mutual_nuclides: abundances['O16'] += NATURAL_ABUNDANCE[nuclide] - elif nuclide == 'Ta180_m1' and 'Ta180' in library_nuclides: - abundances['Ta180'] = NATURAL_ABUNDANCE[nuclide] elif nuclide == 'Ta180_m1' and 'Ta181' in mutual_nuclides: abundances['Ta181'] += NATURAL_ABUNDANCE[nuclide] elif nuclide == 'W180' and 'W182' in mutual_nuclides: diff --git a/openmc/examples.py b/openmc/examples.py index 5578d513e..6895bd94b 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -4,7 +4,7 @@ import numpy as np import openmc - +PINCELL_PITCH = 1.26 # cm def pwr_pin_cell() -> openmc.Model: """Create a PWR pin-cell model. @@ -51,7 +51,7 @@ def pwr_pin_cell() -> openmc.Model: model.materials = (fuel, clad, hot_water) # Instantiate ZCylinder surfaces - pitch = 1.26 + pitch = PINCELL_PITCH fuel_or = openmc.ZCylinder(x0=0, y0=0, r=0.39218, name='Fuel OR') clad_or = openmc.ZCylinder(x0=0, y0=0, r=0.45720, name='Clad OR') left = openmc.XPlane(x0=-pitch/2, name='left', boundary_type='reflective') @@ -83,7 +83,7 @@ def pwr_pin_cell() -> openmc.Model: constraints={'fissionable': True} ) - plot = openmc.Plot.from_geometry(model.geometry) + plot = openmc.SlicePlot.from_geometry(model.geometry) plot.pixels = (300, 300) plot.color_by = 'material' model.plots.append(plot) @@ -150,7 +150,7 @@ def pwr_core() -> openmc.Model: rpv_steel.add_nuclide('Ni60', 0.0026776, 'wo') rpv_steel.add_nuclide('Mn55', 0.01, 'wo') rpv_steel.add_nuclide('Cr52', 0.002092475, 'wo') - rpv_steel.add_nuclide('C0', 0.0025, 'wo') + rpv_steel.add_element('C', 0.0025, 'wo') rpv_steel.add_nuclide('Cu63', 0.0013696, 'wo') lower_rad_ref = openmc.Material(6, name='Lower radial reflector') @@ -319,14 +319,14 @@ def pwr_core() -> openmc.Model: l100 = openmc.RectLattice( name='Fuel assembly (lower half)', lattice_id=100) l100.lower_left = (-10.71, -10.71) - l100.pitch = (1.26, 1.26) + l100.pitch = (PINCELL_PITCH, PINCELL_PITCH) l100.universes = np.tile(fuel_cold, (17, 17)) l100.universes[tube_x, tube_y] = tube_cold l101 = openmc.RectLattice( name='Fuel assembly (upper half)', lattice_id=101) l101.lower_left = (-10.71, -10.71) - l101.pitch = (1.26, 1.26) + l101.pitch = (PINCELL_PITCH, PINCELL_PITCH) l101.universes = np.tile(fuel_hot, (17, 17)) l101.universes[tube_x, tube_y] = tube_hot @@ -350,7 +350,7 @@ def pwr_core() -> openmc.Model: # Define core lattices l200 = openmc.RectLattice(name='Core lattice (lower half)', lattice_id=200) l200.lower_left = (-224.91, -224.91) - l200.pitch = (21.42, 21.42) + l200.pitch = (17 * PINCELL_PITCH, 17 * PINCELL_PITCH) l200.universes = [ [fa_cw]*21, [fa_cw]*21, @@ -376,7 +376,7 @@ def pwr_core() -> openmc.Model: l201 = openmc.RectLattice(name='Core lattice (lower half)', lattice_id=201) l201.lower_left = (-224.91, -224.91) - l201.pitch = (21.42, 21.42) + l201.pitch = (17 * PINCELL_PITCH, 17 * PINCELL_PITCH) l201.universes = [ [fa_hw]*21, [fa_hw]*21, @@ -429,7 +429,7 @@ def pwr_core() -> openmc.Model: model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( [-160, -160, -183], [160, 160, 183])) - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = (125, 125, 0) plot.width = (250, 250) plot.pixels = (3000, 3000) @@ -488,7 +488,7 @@ def pwr_assembly() -> openmc.Model: clad_or = openmc.ZCylinder(x0=0, y0=0, r=0.45720, name='Clad OR') # Create boundary planes to surround the geometry - pitch = 21.42 + pitch = 17 * PINCELL_PITCH min_x = openmc.XPlane(x0=-pitch/2, boundary_type='reflective') max_x = openmc.XPlane(x0=+pitch/2, boundary_type='reflective') min_y = openmc.YPlane(y0=-pitch/2, boundary_type='reflective') @@ -514,7 +514,7 @@ def pwr_assembly() -> openmc.Model: # Create fuel assembly Lattice assembly = openmc.RectLattice(name='Fuel Assembly') - assembly.pitch = (pitch/17, pitch/17) + assembly.pitch = (PINCELL_PITCH, PINCELL_PITCH) assembly.lower_left = (-pitch/2, -pitch/2) # Create array indices for guide tube locations in lattice @@ -544,7 +544,7 @@ def pwr_assembly() -> openmc.Model: constraints={'fissionable': True} ) - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = (0.0, 0.0, 0) plot.width = (21.42, 21.42) plot.pixels = (300, 300) @@ -656,28 +656,70 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5') -> openmc.Model return model -def random_ray_lattice() -> openmc.Model: - """Create a 2x2 PWR pincell asymmetrical lattic eexample. +def _generate_c5g7_materials(second_temp = False) -> openmc.Materials: + """Generate materials utilizing multi-group cross sections based on the + the C5G7 Benchmark. - This model is a 2x2 reflective lattice of fuel pins with one of the lattice - locations having just moderator instead of a fuel pin. It uses 7 group - cross section data. + Parameters + ---------- + second_temp : bool, optional + Whether or not the cross sections should contain two temperature datapoints. + The first data point is the C5G7 cross sections, which corresponds to a temperature + of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, + which corresponds to a temperature of 394 K. This temperature dependence is + fictitious; it is used for testing temperature feedback in the random ray solver. Returns ------- - model : openmc.Model - A PWR 2x2 lattice model + materials : openmc.Materials + Materials object containing UO2 and water materials. + Data Sources + ------------ + All cross section data are from: + Lewis et al., "Benchmark specification for determinisitc 2D/3D MOX fuel + assembly transport calculations without spatial homogenization" """ - model = openmc.Model() - - ########################################################################### - # Create MGXS data for the problem - # Instantiate the energy group data + # MGXS for the UO2 pins. group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] groups = openmc.mgxs.EnergyGroups(group_edges) + uo2_total = np.array([0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_abs = np.array([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, 3.0020e-02, + 1.1126e-01, 2.8278e-01]) + uo2_scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + uo2_scatter_matrix = np.rollaxis(uo2_scatter_matrix, 0, 3) + uo2_fission = np.array([7.21206e-03, 8.19301e-04, 6.45320e-03, 1.85648e-02, 1.78084e-02, + 8.30348e-02, 2.16004e-01]) + uo2_nu_fission = np.array([2.005998e-02, 2.027303e-03, 1.570599e-02, 4.518301e-02, + 4.334208e-02, 2.020901e-01, 5.257105e-01]) + uo2_chi = np.array([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + # MGXS for the H2O moderator. + h2o_total = np.array([0.15920605, 0.412969593, 0.59030986, 0.58435, 0.718, 1.2544497, + 2.650379]) + h2o_abs = np.array([6.0105e-04, 1.5793e-05, 3.3716e-04, 1.9406e-03, 5.7416e-03, + 1.5001e-02, 3.7239e-02]) + h2o_scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + h2o_scatter_matrix = np.rollaxis(h2o_scatter_matrix, 0, 3) + # Instantiate the 7-group (C5G7) cross section data uo2_xsdata = openmc.XSdata('UO2', groups) uo2_xsdata.order = 0 @@ -704,34 +746,39 @@ def random_ray_lattice() -> openmc.Model: uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, 1.85648e-02, 1.78084e-02, 8.30348e-02, 2.16004e-01]) - uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, - 4.518301e-02, 4.334208e-02, 2.020901e-01, - 5.257105e-01]) + nu_fission = np.array([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_nu_fission(nu_fission) uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, 0.0000e+00, 0.0000e+00]) + uo2_xsdata.set_total(uo2_total, temperature=294.0) + uo2_xsdata.set_absorption(uo2_abs, temperature=294.0) + uo2_xsdata.set_scatter_matrix(uo2_scatter_matrix, temperature=294.0) + uo2_xsdata.set_fission(uo2_fission, temperature=294.0) + uo2_xsdata.set_nu_fission(uo2_nu_fission, temperature=294.0) + uo2_xsdata.set_chi(uo2_chi, temperature=294.0) h2o_xsdata = openmc.XSdata('LWTR', groups) h2o_xsdata.order = 0 - h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, - 0.718, 1.2544497, 2.650379]) - h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, - 1.9406e-03, 5.7416e-03, 1.5001e-02, - 3.7239e-02]) - scatter_matrix = np.array( - [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], - [0.0000000, 0.2823340, 0.1299400, 0.0006234, - 0.0000480, 0.0000074, 0.0000010], - [0.0000000, 0.0000000, 0.3452560, 0.2245700, - 0.0169990, 0.0026443, 0.0005034], - [0.0000000, 0.0000000, 0.0000000, 0.0910284, - 0.4155100, 0.0637320, 0.0121390], - [0.0000000, 0.0000000, 0.0000000, 0.0000714, - 0.1391380, 0.5118200, 0.0612290], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, - 0.0022157, 0.6999130, 0.5373200], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) + h2o_xsdata.set_total(h2o_total, temperature=294.0) + h2o_xsdata.set_absorption(h2o_abs, temperature=294.0) + h2o_xsdata.set_scatter_matrix(h2o_scatter_matrix, temperature=294.0) + + # Add the second temperature data point if requested. + if second_temp: + uo2_xsdata.add_temperature(394.0) + uo2_xsdata.set_total(0.5 * uo2_total, temperature=394.0) + uo2_xsdata.set_absorption(0.5 * uo2_abs, temperature=394.0) + uo2_xsdata.set_scatter_matrix(0.5 * uo2_scatter_matrix, temperature=394.0) + uo2_xsdata.set_fission(0.5 * uo2_fission, temperature=394.0) + uo2_xsdata.set_nu_fission(0.5 * uo2_nu_fission, temperature=394.0) + uo2_xsdata.set_chi(uo2_chi, temperature=394.0) + + h2o_xsdata.add_temperature(394.0) + h2o_xsdata.set_total(0.5 * h2o_total, temperature=394.0) + h2o_xsdata.set_absorption(0.5 * h2o_abs, temperature=394.0) + h2o_xsdata.set_scatter_matrix(0.5 * h2o_scatter_matrix, temperature=394.0) mg_cross_sections = openmc.MGXSLibrary(groups) mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) @@ -752,14 +799,28 @@ def random_ray_lattice() -> openmc.Model: # Instantiate a Materials collection and export to XML materials = openmc.Materials([uo2, water]) materials.cross_sections = "mgxs.h5" + return materials - ########################################################################### - # Define problem geometry +def _generate_subdivided_pin_cell(uo2, water) -> openmc.Universe: + """Create a radially and azimuthally subdivided pin cell universe. Helper + function for random_ray_pin_cell() and random_ray_lattice() + + Parameters + ---------- + uo2 : openmc.Material + UO2 material + water : openmc.Material + Water material + + Returns + ------- + pincell : openmc.Universe + Universe containing an unbounded pin cell + + """ ######################################## - # Define an unbounded pincell universe - - pitch = 1.26 + # Define an unbounded pin cell universe # Create a surface for the fuel outer radius fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') @@ -781,7 +842,7 @@ def random_ray_lattice() -> openmc.Model: moderator_c = openmc.Cell( fill=water, region=+outer_ring_b, name='moderator outer c') - # Create pincell universe + # Create pin cell universe pincell_base = openmc.Universe() # Register Cells with Universe @@ -801,19 +862,143 @@ def random_ray_lattice() -> openmc.Model: for i in range(8): azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') azimuthal_cell.fill = pincell_base - azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cell.region = + \ + azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] azimuthal_cells.append(azimuthal_cell) # Create a geometry with the azimuthal universes pincell = openmc.Universe(cells=azimuthal_cells, name='pincell') + return pincell + + +def random_ray_pin_cell(second_temp = False) -> openmc.Model: + """Create a PWR pin cell example using C5G7 cross section data. + cross section data. + + Parameters + ---------- + second_temp : bool, optional + Whether or not the cross sections should contain two temperature datapoints. + The first data point is the C5G7 cross sections, which corresponds to a temperature + of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, + which corresponds to a temperature of 3934 K. This temperature dependence is + fictitious; it is used for testing temperature feedback in the random ray solver. + + Returns + ------- + model : openmc.Model + A PWR pin cell model + + """ + model = openmc.Model() + + ########################################################################### + # Create Materials for the problem + materials = _generate_c5g7_materials(second_temp) + uo2 = materials[0] + water = materials[1] + + ########################################################################### + # Define problem geometry + pincell = _generate_subdivided_pin_cell(uo2, water) + + ######################################## + # Define cell containing lattice and other stuff + pitch = PINCELL_PITCH + box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') + + pincell = openmc.Cell(fill=pincell, region=-box, name='pincell') + + # Create a geometry with the top-level cell + geometry = openmc.Geometry([pincell]) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 400 + settings.inactive = 200 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch / 2, -pitch / 2, -1) + upper_right = (pitch / 2, pitch / 2, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + ########################################################################### + # Define tallies + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Pin tally") + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ########################################################################### + # Exporting to OpenMC model + ########################################################################### + + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def random_ray_lattice(second_temp = False) -> openmc.Model: + """Create a 2x2 PWR pin cell asymmetrical lattice example. + + This model is a 2x2 reflective lattice of fuel pins with one of the lattice + locations having just moderator instead of a fuel pin. It uses C5G7 + cross section data. + + Parameters + ---------- + second_temp : bool, optional + Whether or not the cross sections should contain two temperature datapoints. + The first data point is the C5G7 cross sections, which corresponds to a temperature + of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, + which corresponds to a temperature of 3934 K. This temperature dependence is + fictitious; it is used for testing temperature feedback in the random ray solver. + + Returns + ------- + model : openmc.Model + A PWR 2x2 lattice model + + """ + model = openmc.Model() + + ########################################################################### + # Create Materials for the problem + materials = _generate_c5g7_materials(second_temp) + uo2 = materials[0] + water = materials[1] + + ########################################################################### + # Define problem geometry + pincell = _generate_subdivided_pin_cell(uo2, water) + ######################################## # Define a moderator lattice universe - moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + moderator_infinite = openmc.Cell(name='moderator infinite') + moderator_infinite.fill = water + mu = openmc.Universe() mu.add_cells([moderator_infinite]) + pitch = PINCELL_PITCH lattice = openmc.RectLattice() lattice.lower_left = [-pitch/2.0, -pitch/2.0] lattice.pitch = [pitch/10.0, pitch/10.0] @@ -837,8 +1022,7 @@ def random_ray_lattice() -> openmc.Model: ######################################## # Define cell containing lattice and other stuff - box = openmc.model.RectangularPrism( - pitch*2, pitch*2, boundary_type='reflective') + box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective') assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') @@ -1126,3 +1310,312 @@ def random_ray_three_region_cube() -> openmc.Model: model.tallies = tallies return model + +def random_ray_three_region_cube_with_detectors() -> openmc.Model: + """Create a three region cube model with two external tally regions. + + This is an adaptation of the simple monoenergetic problem of a cube with + three concentric cubic regions. The innermost region is near void (with + Sigma_t around 10^-5) and contains an external isotropic source term, the + middle region is a mild scatterer (with Sigma_t around 10^-3), and the + outer region of the cube is a scatterer and absorber (with Sigma_t around + 1). + + Two cubic "detector" regions are found outside this geometry, one along the + y-axis near z=0, and the other in the upper right corner of the system. + The size of each detector is scaled to be equal to that of the source + region. The model returned by this function contains cell tallies on each + detector. + + Returns + ------- + model : openmc.Model + A three region cube model + + """ + + model = openmc.Model() + + ########################################################################### + # Helper function creates a 3 region cube with different fills in each region + def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): + cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] + for i in range(N): + for j in range(N): + for k in range(N): + if i < n_1 and j >= (N-n_1) and k < n_1: + cube[i][j][k] = fill_1 + elif i < n_2 and j >= (N-n_2) and k < n_2: + cube[i][j][k] = fill_2 + else: + cube[i][j][k] = fill_3 + return cube + + ########################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + cavity_sigma_a = 4.0e-5 + cavity_sigma_s = 3.0e-3 + cavity_mat_data = openmc.XSdata('cavity', groups) + cavity_mat_data.order = 0 + cavity_mat_data.set_total([cavity_sigma_a + cavity_sigma_s]) + cavity_mat_data.set_absorption([cavity_sigma_a]) + cavity_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[cavity_sigma_s]]]), 0, 3)) + + absorber_sigma_a = 0.50 + absorber_sigma_s = 0.50 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[absorber_sigma_s]]]), 0, 3)) + + multiplier = 0.01 + source_sigma_a = cavity_sigma_a * multiplier + source_sigma_s = cavity_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[source_sigma_s]]]), 0, 3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas( + [source_mat_data, cavity_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ########################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + cavity_data = openmc.Macroscopic('cavity') + absorber_data = openmc.Macroscopic('absorber') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + cavity_mat = openmc.Material(name='cavity') + cavity_mat.set_density('macro', 1.0) + cavity_mat.add_macroscopic(cavity_data) + + absorber_mat = openmc.Material(name='absorber') + absorber_mat.set_density('macro', 1.0) + absorber_mat.add_macroscopic(absorber_data) + + # Instantiate a Materials collection + materials_file = openmc.Materials([source_mat, cavity_mat, absorber_mat]) + materials_file.cross_sections = "mgxs.h5" + + ########################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + cavity_cell = openmc.Cell(fill=cavity_mat, name='cube cavity region') + absorber_cell = openmc.Cell( + fill=absorber_mat, name='absorber region') + + source_universe = openmc.Universe(name='source universe') + source_universe.add_cells([source_cell]) + + cavity_universe = openmc.Universe() + cavity_universe.add_cells([cavity_cell]) + + absorber_universe = openmc.Universe() + absorber_universe.add_cells([absorber_cell]) + + absorber_width = 30.0 + n_base = 6 + + # This variable can be increased above 1 to refine the FSR mesh resolution further + refinement_level = 2 + + n = n_base * refinement_level + pitch = absorber_width / n + + pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, + source_universe, cavity_universe, absorber_universe) + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [pitch, pitch, pitch] + lattice.universes = pattern + + lattice_cell = openmc.Cell(fill=lattice) + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0, boundary_type='reflective') + x_high = openmc.XPlane(x0=absorber_width) + + y_low = openmc.YPlane(y0=0.0, boundary_type='reflective') + y_high = openmc.YPlane(y0=absorber_width) + + z_low = openmc.ZPlane(z0=0.0, boundary_type='reflective') + z_high = openmc.ZPlane(z0=absorber_width) + + cube_domain = openmc.Cell(fill=lattice_uni, region=+x_low & - + x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + detect_width = absorber_width / n_base + outer_width = absorber_width + detect_width + + x_outer = openmc.XPlane(x0=outer_width, boundary_type='vacuum') + y_outer = openmc.YPlane(y0=outer_width, boundary_type='vacuum') + z_outer = openmc.ZPlane(z0=outer_width, boundary_type='vacuum') + + detector1_right = openmc.XPlane(x0=detect_width) + detector1_top = openmc.ZPlane(z0=detect_width) + + detector1_region = ( + +x_low & -detector1_right & + +y_high & -y_outer & + +z_low & -detector1_top + ) + detector1 = openmc.Cell( + name='detector 1', + fill=absorber_mat, + region=detector1_region + ) + + detector2_region = ( + +x_high & -x_outer & + +y_high & -y_outer & + +z_high & -z_outer + ) + detector2 = openmc.Cell( + name='detector 2', + fill=absorber_mat, + region=detector2_region + ) + + external_x = ( + +x_high & +y_low & +z_low & -x_outer & + ((-y_outer & -z_high) | (-y_high & +z_high & -z_outer)) + ) + external_y = ( + +y_high & -y_outer & + ( + (+detector1_right & -x_high & +z_low & -z_outer) | + (-detector1_right & +x_low & +detector1_top & -z_outer) | + (+x_high & -x_outer & +z_low & -z_high) + ) + ) + external_z = ( + +x_low & +y_low & +z_high & -z_outer & + ((-y_outer & -x_high) | (-y_high & +x_high & -x_outer)) + ) + external_cell = openmc.Cell(fill=cavity_mat, + region=(external_x | external_y | external_z), + name='outside cube') + + root = openmc.Universe( + name='root universe', + cells=[cube_domain, detector1, detector2, external_cell] + ) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.inactive = 5 + settings.batches = 10 + settings.particles = 500 + settings.run_mode = 'fixed source' + + # Create an initial uniform spatial source for ray integration + lower_left_ray = [0.0, 0.0, 0.0] + upper_right_ray = [outer_width, outer_width, outer_width] + uniform_dist_ray = openmc.stats.Box( + lower_left_ray, upper_right_ray, only_fissionable=False) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + + settings.random_ray['distance_active'] = 800.0 + settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + # Create a rectilinear source region mesh + sr_mesh = openmc.RegularMesh() + sr_mesh.dimension = (14, 14, 14) + sr_mesh.lower_left = (0.0, 0.0, 0.0) + sr_mesh.upper_right = (outer_width, outer_width, outer_width) + settings.random_ray['source_region_meshes'] = [(sr_mesh, [root])] + + # Create the neutron source in the bottom right of the moderator + # Good - fast group appears largest (besides most thermal) + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + source = openmc.IndependentSource(energy=energy_distribution, constraints={ + 'domains': [source_universe]}, strength=3.14) + + settings.source = [source] + + ########################################################################### + # Define tallies + + estimator = 'tracklength' + + detector1_filter = openmc.CellFilter(detector1) + detector1_tally = openmc.Tally(name="Detector 1 Tally") + detector1_tally.filters = [detector1_filter] + detector1_tally.scores = ['flux'] + detector1_tally.estimator = estimator + + detector2_filter = openmc.CellFilter(detector2) + detector2_tally = openmc.Tally(name="Detector 2 Tally") + detector2_tally.filters = [detector2_filter] + detector2_tally.scores = ['flux'] + detector2_tally.estimator = estimator + + absorber_filter = openmc.MaterialFilter(absorber_mat) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + cavity_filter = openmc.MaterialFilter(cavity_mat) + cavity_tally = openmc.Tally(name="Cavity Tally") + cavity_tally.filters = [cavity_filter] + cavity_tally.scores = ['flux'] + cavity_tally.estimator = estimator + + source_filter = openmc.MaterialFilter(source_mat) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([detector1_tally, + detector2_tally, + absorber_tally, + cavity_tally, + source_tally]) + + ########################################################################### + # Assmble Model + + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.tallies = tallies + + return model diff --git a/openmc/executor.py b/openmc/executor.py index aacc48b3f..75094e738 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -103,6 +103,7 @@ def _run(args, output, cwd): # If OpenMC is finished, break loop line = p.stdout.readline() if not line and p.poll() is not None: + p.stdout.close() break lines.append(line) @@ -164,7 +165,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', path_input=None): Parameters ---------- - plots : Iterable of openmc.Plot + plots : Iterable of openmc.PlotBase Plots to display openmc_exec : str Path to OpenMC executable diff --git a/openmc/filter.py b/openmc/filter.py index 6a666d2a0..87aeb70c3 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -13,6 +13,7 @@ import pandas as pd import openmc import openmc.checkvalue as cv from .cell import Cell +from .data.reaction import REACTION_NAME, REACTION_MT from .material import Material from .mixin import IDManagerMixin from .surface import Surface @@ -22,11 +23,11 @@ from ._xml import get_elem_list, get_text _FILTER_TYPES = ( 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', - 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', - 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', - 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', - 'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface', - 'meshmaterial', + 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', + 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', + 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', + 'particleproduction', 'cellinstance', 'collision', 'time', 'parentnuclide', + 'weight', 'meshborn', 'meshsurface', 'meshmaterial', 'reaction', ) _CURRENT_NAMES = ( @@ -35,7 +36,6 @@ _CURRENT_NAMES = ( 'z-min out', 'z-min in', 'z-max out', 'z-max in' ) -_PARTICLES = {'neutron', 'photon', 'electron', 'positron'} class FilterMeta(ABCMeta): @@ -127,9 +127,9 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): def __gt__(self, other): if type(self) is not type(other): if self.short_name in _FILTER_TYPES and \ - other.short_name in _FILTER_TYPES: + other.short_name in _FILTER_TYPES: delta = _FILTER_TYPES.index(self.short_name) - \ - _FILTER_TYPES.index(other.short_name) + _FILTER_TYPES.index(other.short_name) return delta > 0 else: return False @@ -276,7 +276,6 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): if filter_type == subclass.short_name.lower(): return subclass.from_xml_element(elem, **kwargs) - def can_merge(self, other): """Determine if filter can be merged with another. @@ -427,6 +426,7 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): class WithIDFilter(Filter): """Abstract parent for filters of types with IDs (Cell, Material, etc.).""" + def __init__(self, bins, filter_id=None): bins = np.atleast_1d(bins) @@ -630,6 +630,7 @@ class CellInstanceFilter(Filter): DistribcellFilter """ + def __init__(self, bins, filter_id=None): self.bins = bins self.id = filter_id @@ -735,9 +736,8 @@ class ParticleFilter(Filter): Parameters ---------- - bins : str, or sequence of str - The particles to tally represented as strings ('neutron', 'photon', - 'electron', 'positron'). + bins : str, int, openmc.ParticleType, or sequence + The particle types to tally represented as names, PDG numbers, or types. filter_id : int Unique identifier for the filter @@ -751,6 +751,7 @@ class ParticleFilter(Filter): The number of filter bins """ + def __eq__(self, other): if type(self) is not type(other): return False @@ -763,11 +764,16 @@ class ParticleFilter(Filter): @Filter.bins.setter def bins(self, bins): - cv.check_type('bins', bins, Sequence, str) + if isinstance(bins, (str, Integral, openmc.ParticleType)): + bins = [bins] + else: + cv.check_type('bins', bins, Sequence, + (str, Integral, openmc.ParticleType)) bins = np.atleast_1d(bins) - for edge in bins: - cv.check_value('filter bin', edge, _PARTICLES) - self._bins = bins + normalized = [] + for entry in bins: + normalized.append(str(openmc.ParticleType(entry))) + self._bins = np.array(normalized, dtype=str) @classmethod def from_hdf5(cls, group, **kwargs): @@ -815,7 +821,7 @@ class ParentNuclideFilter(ParticleFilter): class MeshFilter(Filter): - """Bins tally event locations by mesh elements. + r"""Bins tally event locations by mesh elements. Parameters ---------- @@ -833,6 +839,25 @@ class MeshFilter(Filter): translation : Iterable of float This array specifies a vector that is used to translate (shift) the mesh for this filter + rotation : Iterable of float + This array specifies the angles in degrees about the x, y, and z axes + that the mesh should be rotated. The rotation applied is an intrinsic + rotation with specified Tait-Bryan angles. That is to say, if the angles + are :math:`(\phi, \theta, \psi)`, then the rotation matrix applied is + :math:`R_z(\psi) R_y(\theta) R_x(\phi)` or + + .. math:: + + \left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\phi \sin\psi + + \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi + \sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi + + \sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi + \sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi + \cos\theta \end{array} \right ] + + A rotation matrix can also be specified directly by setting this + attribute to a nested list (or 2D numpy array) that specifies each + element of the matrix. bins : list of tuple A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), ...] @@ -845,6 +870,7 @@ class MeshFilter(Filter): self.mesh = mesh self.id = filter_id self._translation = None + self._rotation = None def __hash__(self): string = type(self).__name__ + '\n' @@ -856,6 +882,7 @@ class MeshFilter(Filter): string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id) string += '{: <16}=\t{}\n'.format('\tID', self.id) string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation) + string += '{: <16}=\t{}\n'.format('\tRotation', self.rotation) return string @classmethod @@ -879,6 +906,10 @@ class MeshFilter(Filter): if translation: out.translation = translation[()] + rotation = group.get('rotation') + if rotation: + out.rotation = rotation[()] + return out @property @@ -911,6 +942,15 @@ class MeshFilter(Filter): cv.check_length('mesh filter translation', t, 3) self._translation = np.asarray(t) + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, rotation): + cv.check_length('mesh filter rotation', rotation, 3) + self._rotation = np.asarray(rotation) + def can_merge(self, other): # Mesh filters cannot have more than one bin return False @@ -932,53 +972,36 @@ class MeshFilter(Filter): Returns ------- pandas.DataFrame - A Pandas DataFrame with three columns describing the x,y,z mesh - cell indices corresponding to each filter bin. The number of rows - in the DataFrame is the same as the total number of bins in the - corresponding tally, with the filter bin appropriately tiled to map - to the corresponding tally bins. + A Pandas DataFrame with columns describing the mesh cell indices + corresponding to each filter bin. Column names depend on the mesh + type (e.g., x/y/z for RegularMesh, r/phi/z for CylindricalMesh, + r/theta/phi for SphericalMesh, or element index for + UnstructuredMesh). The number of rows in the DataFrame is the same + as the total number of bins in the corresponding tally, with the + filter bin appropriately tiled to map to the corresponding tally + bins. See also -------- Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() """ - # Initialize Pandas DataFrame - df = pd.DataFrame() - # Initialize dictionary to build Pandas Multi-index column filter_dict = {} # Append mesh ID as outermost index of multi-index mesh_key = f'mesh {self.mesh.id}' - # Find mesh dimensions - use 3D indices for simplicity - n_dim = len(self.mesh.dimension) - if n_dim == 3: - nx, ny, nz = self.mesh.dimension - elif n_dim == 2: - nx, ny = self.mesh.dimension - nz = 1 - else: - nx = self.mesh.dimension - ny = nz = 1 + # Determine index base (0-based for unstructured, 1-based otherwise) + idx_start = 0 if isinstance(self.mesh, openmc.UnstructuredMesh) else 1 - # Generate multi-index sub-column for x-axis - filter_dict[mesh_key, 'x'] = _repeat_and_tile( - np.arange(1, nx + 1), stride, data_size) + # Generate a multi-index sub-column for each axis + for label, dim_size in zip(self.mesh._axis_labels, self.mesh.dimension): + filter_dict[mesh_key, label] = _repeat_and_tile( + np.arange(idx_start, idx_start + dim_size), stride, data_size) + stride *= dim_size - # Generate multi-index sub-column for y-axis - filter_dict[mesh_key, 'y'] = _repeat_and_tile( - np.arange(1, ny + 1), nx * stride, data_size) - - # Generate multi-index sub-column for z-axis - filter_dict[mesh_key, 'z'] = _repeat_and_tile( - np.arange(1, nz + 1), nx * ny * stride, data_size) - - # Initialize a Pandas DataFrame from the mesh dictionary - df = pd.concat([df, pd.DataFrame(filter_dict)]) - - return df + return pd.DataFrame(filter_dict) def to_xml_element(self): """Return XML Element representing the Filter. @@ -996,6 +1019,8 @@ class MeshFilter(Filter): subelement.text = str(self.mesh.id) if self.translation is not None: element.set('translation', ' '.join(map(str, self.translation))) + if self.rotation is not None: + element.set('rotation', ' '.join(map(str, self.rotation.ravel()))) return element @classmethod @@ -1008,6 +1033,13 @@ class MeshFilter(Filter): translation = get_elem_list(elem, "translation", float) or [] if translation: out.translation = translation + + rotation = get_elem_list(elem, 'rotation', float) or [] + if rotation: + if len(rotation) == 3: + out.rotation = rotation + elif len(rotation) == 9: + out.rotation = np.array(rotation).reshape(3, 3) return out @@ -1057,6 +1089,7 @@ class MeshMaterialFilter(MeshFilter): Unique identifier for the filter """ + def __init__(self, mesh: openmc.MeshBase, bins, filter_id=None): self.mesh = mesh self.bins = bins @@ -1366,6 +1399,80 @@ class CollisionFilter(Filter): cv.check_greater_than('filter value', x, 0, equality=True) +class ReactionFilter(Filter): + """Bins tally events based on the reaction type (MT number). + + .. versionadded:: 0.15.4 + + Parameters + ---------- + bins : str, int, or iterable thereof + The reaction types to tally. Can be reaction name strings + (e.g., ``'(n,elastic)'``, ``'(n,gamma)'``) or integer MT numbers + (e.g., 2, 102). Integer MT values are automatically converted to their + canonical string representation. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : numpy.ndarray of str + Reaction name strings + id : int + Unique identifier for the filter + num_bins : int + The number of filter bins + + """ + + def __init__(self, bins, filter_id=None): + self.bins = bins + self.id = filter_id + + @Filter.bins.setter + def bins(self, bins): + if isinstance(bins, (str, Integral)): + bins = [bins] + elif not isinstance(bins, list): + bins = list(bins) + normalized = [] + for b in bins: + if isinstance(b, Integral): + if int(b) not in REACTION_NAME: + raise ValueError(f"No known reaction for MT={b}") + normalized.append(REACTION_NAME[int(b)]) + elif isinstance(b, str): + if b == 'total': + warnings.warn( + "The reaction name 'total' is ambiguous. Use " + "'(n,total)' for neutron total cross section or " + "'photon-total' for photon total. Interpreting as" + "'(n,total)'.") + if b not in REACTION_MT: + raise ValueError(f"Unknown reaction name '{b}'") + normalized.append(REACTION_NAME[REACTION_MT[b]]) + else: + raise TypeError(f"Expected str or int for reaction filter " + f"bin, got {type(b)}") + self._bins = np.array(normalized, dtype=str) + + @classmethod + def from_hdf5(cls, group, **kwargs): + if group['type'][()].decode() != cls.short_name.lower(): + raise ValueError("Expected HDF5 data for filter type '" + + cls.short_name.lower() + "' but got '" + + group['type'][()].decode() + "' instead") + bins = [b.decode() for b in group['bins'][()]] + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + return cls(bins, filter_id=filter_id) + + @classmethod + def from_xml_element(cls, elem, **kwargs): + filter_id = int(get_text(elem, "id")) + bins = get_elem_list(elem, "bins", str) or [] + return cls(bins, filter_id=filter_id) + + class RealFilter(Filter): """Tally modifier that describes phase-space and other characteristics @@ -1391,6 +1498,7 @@ class RealFilter(Filter): The number of filter bins """ + def __init__(self, values, filter_id=None): self.values = np.asarray(values) self.bins = np.vstack((self.values[:-1], self.values[1:])).T @@ -1666,7 +1774,8 @@ class EnergyFilter(RealFilter): """ - cv.check_value('group_structure', group_structure, openmc.mgxs.GROUP_STRUCTURES.keys()) + cv.check_value('group_structure', group_structure, + openmc.mgxs.GROUP_STRUCTURES.keys()) return cls(openmc.mgxs.GROUP_STRUCTURES[group_structure.upper()]) @@ -1696,6 +1805,226 @@ class EnergyoutFilter(EnergyFilter): """ + +class ParticleProductionFilter(Filter): + """Bins tally events based on secondary particle type and energy. + + This filter bins secondary particles (e.g., photons, electrons, or recoils) + produced in a reaction by particle type and, optionally, by energy. This is + useful for constructing production matrices or analyzing secondary particle + spectra. Note that unlike other energy filters, the weight that is applied + is equal to the weight of the secondary particle. Thus, to obtain secondary + particle production, it should be used in conjunction with the "events" + score. + + The incident particle type can be filtered using :class:`ParticleFilter`. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + particles : str, int, openmc.ParticleType, or iterable thereof + Type(s) of secondary particle(s) to tally ('photon', 'neutron', etc.) + energies : Iterable of Real or str, optional + A list of energy boundaries in [eV]; each successive pair defines a bin. + Alternatively, the name of the group structure can be given as a string + (must be a key in :data:`openmc.mgxs.GROUP_STRUCTURES`). If not + provided, the filter tallies total secondary particle production without + energy binning. + filter_id : int, optional + Unique identifier for the filter + + Attributes + ---------- + particles : list of openmc.ParticleType + The secondary particle types this filter applies to + energies : numpy.ndarray or None + Energy boundaries in [eV], or None if no energy binning + bins : list + A list of bins; each element fully describes one bin. When energies are + specified, each element is a tuple ``(particle, energy_low, + energy_high)``. When no energies are specified, each element is a + particle name string. + num_bins : int + Total number of filter bins + num_energy_bins : int + Number of energy bins (1 if no energies specified) + shape : tuple of int + Shape of the filter as (n_particles, n_energy_bins) + """ + + def __init__(self, particles, energies=None, filter_id=None): + self.particles = particles + self.energies = energies + self.id = filter_id + + def __repr__(self): + string = type(self).__name__ + '\n' + string += '{: <16}=\t{}\n'.format('\tParticles', + [str(p) for p in self.particles]) + if self.energies is not None: + string += '{: <16}=\t{}\n'.format('\tEnergies', self.energies) + string += '{: <16}=\t{}\n'.format('\tID', self.id) + return string + + @property + def particles(self): + return self._particles + + @particles.setter + def particles(self, particles): + if isinstance(particles, (str, int, openmc.ParticleType)): + self._particles = [openmc.ParticleType(particles)] + else: + self._particles = [openmc.ParticleType(p) for p in particles] + + @property + def energies(self): + return self._energies + + @energies.setter + def energies(self, energies): + if energies is None: + self._energies = None + elif isinstance(energies, str): + cv.check_value('energies', energies, + openmc.mgxs.GROUP_STRUCTURES.keys()) + self._energies = np.array( + openmc.mgxs.GROUP_STRUCTURES[energies.upper()]) + else: + energies = np.asarray(energies, dtype=float) + cv.check_length('energies', energies, 2) + for i in range(len(energies) - 1): + if energies[i + 1] <= energies[i]: + raise ValueError("Energy bins must be monotonically " + "increasing.") + self._energies = energies + + @property + def bins(self): + if self.energies is None: + return [str(p) for p in self.particles] + else: + result = [] + energy_pairs = np.vstack( + (self.energies[:-1], self.energies[1:])).T + for particle in self.particles: + for e_low, e_high in energy_pairs: + result.append((str(particle), e_low, e_high)) + return result + + @bins.setter + def bins(self, bins): + # bins is set indirectly through particles/energies + pass + + def check_bins(self, bins): + pass + + @property + def num_energy_bins(self): + if self.energies is None: + return 1 + else: + return len(self.energies) - 1 + + @property + def num_bins(self): + return len(self.particles) * self.num_energy_bins + + @property + def shape(self): + return (len(self.particles), self.num_energy_bins) + + def to_xml_element(self): + element = ET.Element('filter') + element.set('id', str(self.id)) + element.set('type', self.short_name.lower()) + + subelement = ET.SubElement(element, 'particles') + subelement.text = ' '.join(str(p) for p in self.particles) + + if self.energies is not None: + subelement = ET.SubElement(element, 'energies') + subelement.text = ' '.join(str(e) for e in self.energies) + + return element + + @classmethod + def from_xml_element(cls, elem, **kwargs): + filter_id = int(elem.get('id')) + particles = get_text(elem, 'particles').split() + + bins_elem = elem.find('energies') + if bins_elem is not None: + energies = [float(x) for x in bins_elem.text.split()] + else: + energies = None + + return cls(particles, energies=energies, filter_id=filter_id) + + @classmethod + def from_hdf5(cls, group, **kwargs): + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + + # Read particle types + particles = [b.decode() if isinstance(b, bytes) else b + for b in group['particles'][()]] + + # Read energy bins if present + if 'energies' in group: + energies = group['energies'][()] + else: + energies = None + + return cls(particles, energies=energies, filter_id=filter_id) + + def get_pandas_dataframe(self, data_size, stride, **kwargs): + """Builds a Pandas DataFrame for the Filter's bins. + + This method constructs a Pandas DataFrame object for the filter with + columns annotated by filter bin information. This is a helper method for + :meth:`Tally.get_pandas_dataframe`. + + Parameters + ---------- + data_size : int + The total number of bins in the tally corresponding to this filter + stride : int + Stride in memory for the filter + + Returns + ------- + pandas.DataFrame + A Pandas DataFrame with columns for particle type and, if energy + bins are specified, energy bin boundaries. + + See also + -------- + Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() + + """ + filter_dict = {} + key = self.short_name.lower() + n_ebins = self.num_energy_bins + + # Particle column — outer dimension (changes slowest) + particle_names = [str(p) for p in self.particles] + filter_dict[key, 'particle'] = _repeat_and_tile( + np.array(particle_names), n_ebins * stride, data_size) + + # Energy columns only if energies were specified + if self.energies is not None: + energy_pairs = np.vstack( + (self.energies[:-1], self.energies[1:])).T + filter_dict[key, 'energy low [eV]'] = _repeat_and_tile( + energy_pairs[:, 0], stride, data_size) + filter_dict[key, 'energy high [eV]'] = _repeat_and_tile( + energy_pairs[:, 1], stride, data_size) + + return pd.DataFrame(filter_dict) + + class TimeFilter(RealFilter): """Bins tally events based on the particle's time. @@ -1839,12 +2168,21 @@ class DistribcellFilter(Filter): @property def paths(self): - return self._paths + if self._paths is None: + if not hasattr(self, '_geometry'): + raise ValueError( + "Model must be exported before the 'paths' attribute is" \ + "available for a DistribcellFilter.") - @paths.setter - def paths(self, paths): - cv.check_iterable_type('paths', paths, str) - self._paths = paths + # Determine paths for cell instances + self._geometry.determine_paths() + + # Get paths for the corresponding cell + cell_id = self.bins[0] + cell = self._geometry.get_all_cells()[cell_id] + self._paths = cell.paths + + return self._paths @Filter.bins.setter def bins(self, bins): @@ -1854,7 +2192,7 @@ class DistribcellFilter(Filter): # Make sure there is only 1 bin. if not len(bins) == 1: msg = (f'Unable to add bins "{bins}" to a DistribcellFilter since ' - 'only a single distribcell can be used per tally') + 'only a single distribcell can be used per tally') raise ValueError(msg) # Check the type and extract the id, if necessary. @@ -2007,7 +2345,7 @@ class DistribcellFilter(Filter): # requests Summary geometric information filter_bins = _repeat_and_tile( np.arange(self.num_bins), stride, data_size) - df = pd.DataFrame({self.short_name.lower() : filter_bins}) + df = pd.DataFrame({self.short_name.lower(): filter_bins}) # Concatenate with DataFrame of distribcell instance IDs if level_df is not None: @@ -2046,6 +2384,7 @@ class MuFilter(RealFilter): The number of filter bins """ + def __init__(self, values, filter_id=None): if isinstance(values, Integral): values = np.linspace(-1., 1., values + 1) @@ -2211,6 +2550,7 @@ class DelayedGroupFilter(Filter): The number of filter bins """ + def check_bins(self, bins): # Check the bin values. for g in bins: @@ -2279,9 +2619,9 @@ class EnergyFunctionFilter(Filter): def __gt__(self, other): if type(self) is not type(other): if self.short_name in _FILTER_TYPES and \ - other.short_name in _FILTER_TYPES: + other.short_name in _FILTER_TYPES: delta = _FILTER_TYPES.index(self.short_name) - \ - _FILTER_TYPES.index(other.short_name) + _FILTER_TYPES.index(other.short_name) return delta > 0 else: return False @@ -2291,9 +2631,9 @@ class EnergyFunctionFilter(Filter): def __lt__(self, other): if type(self) is not type(other): if self.short_name in _FILTER_TYPES and \ - other.short_name in _FILTER_TYPES: + other.short_name in _FILTER_TYPES: delta = _FILTER_TYPES.index(self.short_name) - \ - _FILTER_TYPES.index(other.short_name) + _FILTER_TYPES.index(other.short_name) return delta < 0 else: return False @@ -2304,14 +2644,16 @@ class EnergyFunctionFilter(Filter): string = type(self).__name__ + '\n' string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy) string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y) - string += '{: <16}=\t{}\n'.format('\tInterpolation', self.interpolation) + string += '{: <16}=\t{}\n'.format('\tInterpolation', + self.interpolation) return hash(string) def __repr__(self): string = type(self).__name__ + '\n' string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy) string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y) - string += '{: <16}=\t{}\n'.format('\tInterpolation', self.interpolation) + string += '{: <16}=\t{}\n'.format('\tInterpolation', + self.interpolation) string += '{: <16}=\t{}\n'.format('\tID', self.id) return string @@ -2397,10 +2739,12 @@ class EnergyFunctionFilter(Filter): @interpolation.setter def interpolation(self, val): cv.check_type('interpolation', val, str) - cv.check_value('interpolation', val, self.INTERPOLATION_SCHEMES.values()) + cv.check_value('interpolation', val, + self.INTERPOLATION_SCHEMES.values()) if val == 'quadratic' and len(self.energy) < 3: - raise ValueError('Quadratic interpolation requires 3 or more values.') + raise ValueError( + 'Quadratic interpolation requires 3 or more values.') if val == 'cubic' and len(self.energy) < 4: raise ValueError('Cubic interpolation requires 3 or more values.') diff --git a/openmc/lattice.py b/openmc/lattice.py index f0e8b40b0..21849ec40 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -1083,8 +1083,11 @@ class HexLattice(Lattice): from the outermost ring), and i is the index with a ring starting from the top and proceeding clockwise. orientation : {'x', 'y'} - str by default 'y' orientation of main lattice diagonal another option - - 'x' + The orientation of the lattice. The 'x' orientation means that each + lattice element has two faces that are perpendicular to the x-axis, + while the 'y' orientation means that each lattice element has two faces + that are perpendicular to the y-axis. By default, the orientation is + 'y'. num_rings : int Number of radial ring positions in the xy-plane num_axial : int diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index d2a794eb1..9b135370f 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -49,6 +49,9 @@ def _libmesh_enabled(): def _uwuw_enabled(): return c_bool.in_dll(_dll, "UWUW_ENABLED").value +def _strict_fp_enabled(): + return c_bool.in_dll(_dll, "STRICT_FP_ENABLED").value + from .error import * from .core import * diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 02c7784d1..0ee8c3ff0 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -28,11 +28,13 @@ class _SourceSite(Structure): ('wgt', c_double), ('delayed_group', c_int), ('surf_id', c_int), - ('particle', c_int), + ('particle', c_int32), ('parent_nuclide', c_int), ('parent_id', c_int64), - ('progeny_id', c_int64)] - + ('progeny_id', c_int64), + ('wgt_born', c_double), + ('wgt_ww_born', c_double), + ('n_split', c_int64)] # Define input type for numpy arrays that will be passed into C++ functions # Must be an int or double array, with single dimension that is contiguous @@ -82,6 +84,7 @@ _dll.openmc_properties_import.restype = c_int _dll.openmc_properties_import.errcheck = _error_handler _dll.openmc_run.restype = c_int _dll.openmc_run.errcheck = _error_handler +_dll.openmc_run_random_ray.restype = None _dll.openmc_reset.restype = c_int _dll.openmc_reset.errcheck = _error_handler _dll.openmc_reset_timers.restype = c_int @@ -479,10 +482,23 @@ def run(output=True): _dll.openmc_run() +def run_random_ray(output=True): + """Run a random ray simulation + + Parameters + ---------- + output : bool, optional + Whether or not to show output. Defaults to showing output + """ + + with quiet_dll(output): + _dll.openmc_run_random_ray() + def sample_external_source( n_samples: int = 1000, - prn_seed: int | None = None -) -> openmc.ParticleList: + prn_seed: int | None = None, + as_array: bool = False +) -> openmc.ParticleList | np.ndarray: """Sample external source and return source particles. .. versionadded:: 0.13.1 @@ -494,11 +510,20 @@ def sample_external_source( prn_seed : int Pseudorandom number generator (PRNG) seed; if None, one will be generated randomly. + as_array : bool + If True, return a numpy structured array instead of a + :class:`~openmc.ParticleList`. The array has fields ``'r'`` (float64, + shape 3), ``'u'`` (float64, shape 3), ``'E'`` (float64), ``'time'`` + (float64), ``'wgt'`` (float64), ``'delayed_group'`` (int32), + ``'surf_id'`` (int32), and ``'particle'`` (int32). This avoids the + overhead of constructing individual :class:`~openmc.SourceParticle` + objects and is substantially faster for large sample counts. Returns ------- - openmc.ParticleList - List of sampled source particles + openmc.ParticleList or numpy.ndarray + List of sampled source particles, or a structured array when + *as_array* is True. """ if n_samples <= 0: @@ -506,18 +531,28 @@ def sample_external_source( if prn_seed is None: prn_seed = getrandbits(63) - # Call into C API to sample source - sites_array = (_SourceSite * n_samples)() - _dll.openmc_sample_external_source(c_size_t(n_samples), c_uint64(prn_seed), sites_array) + # Pre-allocate output array and sample all particles in a single C call + result = np.empty(n_samples, dtype=_SourceSite) + sites_array = (_SourceSite * n_samples).from_buffer(result) + _dll.openmc_sample_external_source( + c_size_t(n_samples), + c_uint64(prn_seed), + sites_array, + ) - # Convert to list of SourceParticle and return - return openmc.ParticleList([openmc.SourceParticle( - r=site.r, u=site.u, E=site.E, time=site.time, wgt=site.wgt, - delayed_group=site.delayed_group, surf_id=site.surf_id, - particle=openmc.ParticleType(site.particle) + if as_array: + return result + + particles = [ + openmc.SourceParticle( + r=site.r, u=site.u, E=site.E, time=site.time, + wgt=site.wgt, delayed_group=site.delayed_group, + surf_id=site.surf_id, + particle=openmc.ParticleType(site.particle), ) for site in sites_array - ]) + ] + return openmc.ParticleList(particles) def simulation_init(): @@ -661,8 +696,8 @@ class TemporarySession: self.model = model # Determine MPI intercommunicator - self.init_kwargs.setdefault('intracomm', comm) - self.comm = self.init_kwargs['intracomm'] + self.comm = self.init_kwargs.get('intracomm') or comm + self.init_kwargs['intracomm'] = self.comm def __enter__(self): """Initialize the OpenMC library in a temporary directory.""" diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 55b681d89..574a37443 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -22,9 +22,10 @@ __all__ = [ 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', 'MeshMaterialFilter', 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', - 'ParentNuclideFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', - 'SpatialLegendreFilter', 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', - 'WeightFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' + 'ParentNuclideFilter', 'ParticleFilter', 'ParticleProductionFilter', 'PolarFilter', + 'ReactionFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', + 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', 'WeightFilter', 'ZernikeFilter', + 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -97,6 +98,14 @@ _dll.openmc_mesh_filter_get_translation.errcheck = _error_handler _dll.openmc_mesh_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] _dll.openmc_mesh_filter_set_translation.restype = c_int _dll.openmc_mesh_filter_set_translation.errcheck = _error_handler +_dll.openmc_mesh_filter_get_rotation.argtypes = [c_int32, POINTER(c_double), + POINTER(c_size_t)] +_dll.openmc_mesh_filter_get_rotation.restype = c_int +_dll.openmc_mesh_filter_get_rotation.errcheck = _error_handler +_dll.openmc_mesh_filter_set_rotation.argtypes = [ + c_int32, POINTER(c_double), c_size_t] +_dll.openmc_mesh_filter_set_rotation.restype = c_int +_dll.openmc_mesh_filter_set_rotation.errcheck = _error_handler _dll.openmc_meshborn_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_meshborn_filter_get_mesh.restype = c_int _dll.openmc_meshborn_filter_get_mesh.errcheck = _error_handler @@ -124,6 +133,9 @@ _dll.openmc_meshsurface_filter_set_translation.errcheck = _error_handler _dll.openmc_new_filter.argtypes = [c_char_p, POINTER(c_int32)] _dll.openmc_new_filter.restype = c_int _dll.openmc_new_filter.errcheck = _error_handler +_dll.openmc_particle_filter_get_bins.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_particle_filter_get_bins.restype = c_int +_dll.openmc_particle_filter_get_bins.errcheck = _error_handler _dll.openmc_spatial_legendre_filter_get_order.argtypes = [c_int32, POINTER(c_int)] _dll.openmc_spatial_legendre_filter_get_order.restype = c_int _dll.openmc_spatial_legendre_filter_get_order.errcheck = _error_handler @@ -393,6 +405,10 @@ class MeshFilter(Filter): Mesh used for the filter translation : Iterable of float 3-D coordinates of the translation vector + rotation : Iterable of float + The rotation matrix or angles of the filter mesh. This can either be + a fully specified 3 x 3 rotation matrix or an Iterable of length 3 + with the angles in degrees about the x, y, and z axes, respectively. """ filter_type = 'mesh' @@ -422,6 +438,34 @@ class MeshFilter(Filter): def translation(self, translation): _dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation)) + @property + def rotation(self): + rotation_data = np.zeros(12) + rot_size = c_size_t() + + _dll.openmc_mesh_filter_get_rotation( + self._index, rotation_data.ctypes.data_as(POINTER(c_double)), + rot_size) + rot_size = rot_size.value + + if rot_size == 9: + return rotation_data[:rot_size].shape(3, 3) + elif rot_size in (0, 12): + # If size is 0, rotation_data[9:] will be zeros. This indicates no + # rotation and is the most straightforward way to always return + # an iterable of floats + return rotation_data[9:] + else: + raise ValueError( + f'Invalid size of rotation matrix: {rot_size}') + + @rotation.setter + def rotation(self, rotation_data): + flat_rotation = np.asarray(rotation_data, dtype=float).flatten() + + _dll.openmc_mesh_filter_set_rotation( + self._index, flat_rotation.ctypes.data_as(POINTER(c_double)), + c_size_t(len(flat_rotation))) class MeshBornFilter(Filter): """MeshBorn filter stored internally. @@ -558,16 +602,24 @@ class ParticleFilter(Filter): @property def bins(self): - particle_i = np.zeros((self.n_bins,), dtype=c_int) + particle_i = np.zeros((self.n_bins,), dtype=c_int32) _dll.openmc_particle_filter_get_bins( - self._index, particle_i.ctypes.data_as(POINTER(c_int))) + self._index, particle_i.ctypes.data_as(POINTER(c_int32))) return [ParticleType(i) for i in particle_i] +class ParticleProductionFilter(Filter): + filter_type = 'particleproduction' + + class PolarFilter(Filter): filter_type = 'polar' +class ReactionFilter(Filter): + filter_type = 'reaction' + + class SphericalHarmonicsFilter(Filter): filter_type = 'sphericalharmonics' @@ -668,7 +720,9 @@ _FILTER_TYPE_MAP = { 'musurface': MuSurfaceFilter, 'parentnuclide': ParentNuclideFilter, 'particle': ParticleFilter, + 'particleproduction': ParticleProductionFilter, 'polar': PolarFilter, + 'reaction': ReactionFilter, 'sphericalharmonics': SphericalHarmonicsFilter, 'spatiallegendre': SpatialLegendreFilter, 'surface': SurfaceFilter, diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 3a720f98a..19e6f74d7 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,5 +1,5 @@ from collections.abc import Mapping, Sequence -from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, +from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, c_void_p, create_string_buffer, c_size_t) from math import sqrt import sys @@ -47,7 +47,8 @@ _dll.openmc_mesh_bounding_box.argtypes = [ _dll.openmc_mesh_bounding_box.restype = c_int _dll.openmc_mesh_bounding_box.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ - c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double] + c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double, + c_void_p] _dll.openmc_mesh_material_volumes.restype = c_int _dll.openmc_mesh_material_volumes.errcheck = _error_handler _dll.openmc_mesh_get_plot_bins.argtypes = [ @@ -188,6 +189,7 @@ class Mesh(_FortranObjectWithID): n_samples: int | tuple[int, int, int] = 10_000, max_materials: int = 4, output: bool = True, + bounding_boxes: bool = False, ) -> MeshMaterialVolumes: """Determine volume of materials in each mesh element. @@ -213,6 +215,11 @@ class Mesh(_FortranObjectWithID): Estimated maximum number of materials in any given mesh element. output : bool, optional Whether or not to show output. + bounding_boxes : bool, optional + Whether or not to compute an axis-aligned bounding box for each + (mesh element, material) combination. When enabled, the bounding + box encloses the ray-estimator prisms used for the volume + estimation. Returns ------- @@ -243,23 +250,36 @@ class Mesh(_FortranObjectWithID): table_size = slot_factor*max_materials materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) volumes = np.zeros((n, table_size), dtype=np.float64) + bboxes = None + if bounding_boxes: + bboxes = np.empty((n, table_size, 6), dtype=np.float64) + bboxes[..., 0:3] = np.inf + bboxes[..., 3:6] = -np.inf # Run material volume calculation while True: try: + bboxes_ptr = None + if bboxes is not None: + bboxes_ptr = bboxes.ctypes.data_as(POINTER(c_double)) with quiet_dll(output): _dll.openmc_mesh_material_volumes( - self._index, nx, ny, nz, table_size, materials, volumes) + self._index, nx, ny, nz, table_size, materials, + volumes, bboxes_ptr) except AllocationError: # Increase size of result array and try again table_size *= 2 materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) volumes = np.zeros((n, table_size), dtype=np.float64) + if bounding_boxes: + bboxes = np.empty((n, table_size, 6), dtype=np.float64) + bboxes[..., 0:3] = np.inf + bboxes[..., 3:6] = -np.inf else: # If no error, break out of loop break - return MeshMaterialVolumes(materials, volumes) + return MeshMaterialVolumes(materials, volumes, bboxes) def get_plot_bins( self, diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index f97348b20..196682394 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -1,10 +1,15 @@ +from collections.abc import Mapping from ctypes import (c_bool, c_int, c_size_t, c_int32, - c_double, Structure, POINTER) + c_double, c_uint8, Structure, POINTER) +from weakref import WeakValueDictionary +from ..exceptions import AllocationError, InvalidIDError from . import _dll +from .core import _FortranObjectWithID from .error import _error_handler import numpy as np +import warnings class _Position(Structure): @@ -47,220 +52,637 @@ class _Position(Structure): return f"({self.x}, {self.y}, {self.z})" -class _PlotBase(Structure): - """A structure defining a 2-D geometry slice with underlying c-types +def _extract_slice_data_args(plot): + """Convert a legacy plot-like object into slice_data keyword arguments.""" + try: + kwargs = { + 'origin': tuple(plot.origin), + 'width': (plot.width, plot.height), + 'basis': plot.basis, + 'pixels': (plot.h_res, plot.v_res), + 'show_overlaps': getattr(plot, 'color_overlaps', False), + 'level': getattr(plot, 'level', -1), + } + except AttributeError as exc: + raise TypeError( + "plot must be a legacy plot-like object with origin, width, " + "height, basis, h_res, and v_res attributes." + ) from exc + return kwargs - C-Type Attributes - ----------------- - origin : openmc.lib.plot._Position - A position defining the origin of the plot. - width_ : openmc.lib.plot._Position - The width of the plot along the x, y, and z axes, respectively - basis_ : c_int - The axes basis of the plot view. - pixels_ : c_size_t[3] - The resolution of the plot in the horizontal and vertical dimensions - level_ : c_int - The universe level for the plot view - Attributes +_dll.openmc_slice_data.argtypes = [ + POINTER(c_double * 3), # origin + POINTER(c_double * 3), # u_span + POINTER(c_double * 3), # v_span + POINTER(c_size_t * 2), # pixels + c_bool, # show_overlaps + c_int, # level + c_int32, # filter_index + POINTER(c_int32), # geom_data + POINTER(c_double), # property_data (can be None) +] +_dll.openmc_slice_data.restype = c_int +_dll.openmc_slice_data.errcheck = _error_handler + + +def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, + pixels=None, show_overlaps=False, level=None, filter=None, + include_properties=True): + """Generate a 2D raster of geometry and property data for plotting. + + Parameters ---------- - origin : tuple or list of ndarray - Origin (center) of the plot - width : float - The horizontal dimension of the plot in geometry units (cm) - height : float - The vertical dimension of the plot in geometry units (cm) - basis : string - One of {'xy', 'xz', 'yz'} indicating the horizontal and vertical - axes of the plot. - h_res : int - The horizontal resolution of the plot in pixels - v_res : int - The vertical resolution of the plot in pixels - level : int - The universe level for the plot (default: -1 -> all universes shown) + origin : sequence of float + Center position of the plot [x, y, z] + width : sequence of float + Width of the plot [horizontal, vertical]. Mutually exclusive with + u_span/v_span. + basis : {'xy', 'xz', 'yz'} or int + Plot basis. Ignored if u_span/v_span are provided. + u_span : sequence of float, optional + Full-width span vector for the horizontal axis (3 values). Mutually + exclusive with width. + v_span : sequence of float, optional + Full-height span vector for the vertical axis (3 values). Mutually + exclusive with width. + pixels : sequence of int + Number of pixels [horizontal, vertical] + show_overlaps : bool, optional + Whether to detect overlapping cells + level : int, optional + Universe level (None for deepest) + filter : openmc.lib.Filter, optional + Filter for bin index lookup + include_properties : bool, optional + Whether to compute temperature/density + + Returns + ------- + geom_data : numpy.ndarray + Array of shape (v_res, h_res, 3) or (v_res, h_res, 4) with int32 dtype. + Contains [cell_id, cell_instance, material_id] when no filter is provided, + or [cell_id, cell_instance, material_id, filter_bin] when a filter is provided. + property_data : numpy.ndarray or None + Array of shape (v_res, h_res, 2) with float64 dtype containing + [temperature, density], or None if include_properties=False """ - _fields_ = [('origin_', _Position), - ('width_', _Position), - ('basis_', c_int), - ('pixels_', 3*c_size_t), - ('color_overlaps_', c_bool), - ('level_', c_int)] + # Set deepest level as default + if level is None: + level = -1 + if not isinstance(level, int): + raise TypeError("level must be an integer.") - def __init__(self): - self.level_ = -1 - self.basis_ = 1 - self.color_overlaps_ = False + if pixels is None: + raise ValueError("pixels must be specified.") + if len(pixels) != 2: + raise ValueError("pixels must be a length-2 sequence.") - @property - def origin(self): - return self.origin_ + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") - @origin.setter - def origin(self, origin): - self.origin_.x = origin[0] - self.origin_.y = origin[1] - self.origin_.z = origin[2] - - @property - def width(self): - return self.width_.x - - @width.setter - def width(self, width): - self.width_.x = width - - @property - def height(self): - return self.width_.y - - @height.setter - def height(self, height): - self.width_.y = height - - @property - def basis(self): - if self.basis_ == 1: - return 'xy' - elif self.basis_ == 2: - return 'xz' - elif self.basis_ == 3: - return 'yz' - - raise ValueError(f"Plot basis {self.basis_} is invalid") - - @basis.setter - def basis(self, basis): + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + u_span = np.asarray(u_span, dtype=float) + v_span = np.asarray(v_span, dtype=float) + if u_span.shape != (3,) or v_span.shape != (3,): + raise ValueError("u_span and v_span must be length-3 sequences.") + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + if u_norm == 0.0 or v_norm == 0.0: + raise ValueError("u_span and v_span must be non-zero vectors.") + dot = float(np.dot(u_span, v_span)) + ortho_tol = 1.0e-10 * u_norm * v_norm + if abs(dot) > ortho_tol: + raise ValueError("u_span and v_span must be orthogonal.") + else: + if width is None: + raise ValueError("width must be provided when u_span/v_span are not set.") + if len(width) != 2: + raise ValueError("width must be a length-2 sequence.") + basis_map = {'xy': 1, 'xz': 2, 'yz': 3} if isinstance(basis, str): - valid_bases = ('xy', 'xz', 'yz') basis = basis.lower() - if basis not in valid_bases: + if basis not in basis_map: raise ValueError(f"{basis} is not a valid plot basis.") - - if basis == 'xy': - self.basis_ = 1 - elif basis == 'xz': - self.basis_ = 2 - elif basis == 'yz': - self.basis_ = 3 - return - - if isinstance(basis, int): - valid_bases = (1, 2, 3) - if basis not in valid_bases: + basis = basis_map[basis] + elif isinstance(basis, int): + if basis not in basis_map.values(): raise ValueError(f"{basis} is not a valid plot basis.") - self.basis_ = basis - return + else: + raise ValueError(f"{basis} is not a valid plot basis.") - raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") + if basis == 1: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, width[1], 0.0], dtype=float) + elif basis == 2: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) + else: + u_span = np.array([0.0, width[0], 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) - @property - def h_res(self): - return self.pixels_[0] + origin = np.asarray(origin, dtype=float) + if origin.shape != (3,): + raise ValueError("origin must be a length-3 sequence.") - @h_res.setter - def h_res(self, h_res): - self.pixels_[0] = h_res + # Prepare ctypes arrays + origin_arr = (c_double * 3)(*origin) + u_span_arr = (c_double * 3)(*u_span) + v_span_arr = (c_double * 3)(*v_span) + pixels_arr = (c_size_t * 2)(*pixels) - @property - def v_res(self): - return self.pixels_[1] + # Get internal filter index from filter ID if filter is provided + if filter is not None: + filter_index = c_int32() + _dll.openmc_get_filter_index(filter.id, filter_index) + filter_index = filter_index.value + else: + filter_index = -1 - @v_res.setter - def v_res(self, v_res): - self.pixels_[1] = v_res + # Allocate output arrays with dynamic size based on filter + n_geom_fields = 4 if filter is not None else 3 + geom_data = np.zeros((pixels[1], pixels[0], n_geom_fields), dtype=np.int32) + if include_properties: + property_data = np.zeros((pixels[1], pixels[0], 2), dtype=np.float64) + prop_ptr = property_data.ctypes.data_as(POINTER(c_double)) + else: + property_data = None + prop_ptr = None - @property - def level(self): - return int(self.level_) + _dll.openmc_slice_data( + origin_arr, + u_span_arr, + v_span_arr, + pixels_arr, + show_overlaps, + level, + filter_index, + geom_data.ctypes.data_as(POINTER(c_int32)), + prop_ptr + ) - @level.setter - def level(self, level): - self.level_ = level - - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, color_overlaps): - self.color_overlaps_ = color_overlaps - - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, val): - self.color_overlaps_ = val - - def __repr__(self): - out_str = ["-----", - "Plot:", - "-----", - f"Origin: {self.origin}", - f"Width: {self.width}", - f"Height: {self.height}", - f"Basis: {self.basis}", - f"HRes: {self.h_res}", - f"VRes: {self.v_res}", - f"Color Overlaps: {self.color_overlaps}", - f"Level: {self.level}"] - return '\n'.join(out_str) - - -_dll.openmc_id_map.argtypes = [POINTER(_PlotBase), POINTER(c_int32)] -_dll.openmc_id_map.restype = c_int -_dll.openmc_id_map.errcheck = _error_handler + return geom_data, property_data def id_map(plot): + """Deprecated compatibility wrapper for geometry ID maps. + + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=False)` instead. """ - Generate a 2-D map of cell and material IDs. Used for in-memory image - generation. + warnings.warn( + "openmc.lib.id_map is deprecated and will be removed in a future " + "release; use openmc.lib.slice_data(..., include_properties=False).", + FutureWarning, + ) - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - id_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 3) of - OpenMC property ids with dtype int32. The last dimension of the array - contains, in order, cell IDs, cell instances, and material IDs. - - """ - img_data = np.zeros((plot.v_res, plot.h_res, 3), - dtype=np.dtype('int32')) - _dll.openmc_id_map(plot, img_data.ctypes.data_as(POINTER(c_int32))) - return img_data - - -_dll.openmc_property_map.argtypes = [POINTER(_PlotBase), POINTER(c_double)] -_dll.openmc_property_map.restype = c_int -_dll.openmc_property_map.errcheck = _error_handler + kwargs = _extract_slice_data_args(plot) + geom_data, _ = slice_data(include_properties=False, **kwargs) + return geom_data[:, :, :3] def property_map(plot): - """ - Generate a 2-D map of cell temperatures and material densities. Used for - in-memory image generation. + """Deprecated compatibility wrapper for temperature/density maps. - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=True)` instead. + """ + warnings.warn( + "openmc.lib.property_map is deprecated and will be removed in a " + "future release; use openmc.lib.slice_data(..., " + "include_properties=True).", + FutureWarning, + ) + + kwargs = _extract_slice_data_args(plot) + _, prop_data = slice_data(include_properties=True, **kwargs) + return prop_data + + +_dll.openmc_slice_data_overlap_count.argtypes = [POINTER(c_size_t)] +_dll.openmc_slice_data_overlap_count.restype = c_int +_dll.openmc_slice_data_overlap_count.errcheck = _error_handler + +_dll.openmc_slice_data_overlap_info.argtypes = [c_size_t, POINTER(c_int32)] +_dll.openmc_slice_data_overlap_info.restype = c_int +_dll.openmc_slice_data_overlap_info.errcheck = _error_handler + + +# Python wrappings for overlap functions +def slice_data_overlap_count() -> int: + """Return the number of unique overlaps from the last slice plot. Returns ------- - property_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 2) of - OpenMC property ids with dtype float - + int + Number of unique overlapping cell pairs detected by the most recent + :func:`slice_data` call with overlap checking enabled. """ - prop_data = np.zeros((plot.v_res, plot.h_res, 2)) - _dll.openmc_property_map(plot, prop_data.ctypes.data_as(POINTER(c_double))) - return prop_data + count = c_size_t() + _dll.openmc_slice_data_overlap_count(count) + return count.value + + +def slice_data_overlap_info() -> np.ndarray: + """Return identifying information for overlaps from the last slice plot. + + Returns + ------- + numpy.ndarray + Array of shape ``(n, 3)`` with int32 dtype, where ``n`` is the number + of unique overlaps detected by the most recent :func:`slice_data` call + with overlap checking enabled. Each row contains ``[universe_id, + cell1_id, cell2_id]``. + """ + n = slice_data_overlap_count() + overlap_info = np.empty((n, 3), dtype=np.int32) + + if n > 0: + _dll.openmc_slice_data_overlap_info( + n, + overlap_info.ctypes.data_as(POINTER(c_int32)), + ) + return overlap_info + + +_dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_get_plot_index.restype = c_int +_dll.openmc_get_plot_index.errcheck = _error_handler + +_dll.openmc_plot_get_id.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_plot_get_id.restype = c_int +_dll.openmc_plot_get_id.errcheck = _error_handler + +_dll.openmc_plot_set_id.argtypes = [c_int32, c_int32] +_dll.openmc_plot_set_id.restype = c_int +_dll.openmc_plot_set_id.errcheck = _error_handler + +_dll.openmc_plots_size.restype = c_size_t + +_dll.openmc_solidraytrace_plot_create.argtypes = [POINTER(c_int32)] +_dll.openmc_solidraytrace_plot_create.restype = c_int +_dll.openmc_solidraytrace_plot_create.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_pixels.argtypes = [ + c_int32, POINTER(c_int32), POINTER(c_int32)] +_dll.openmc_solidraytrace_plot_get_pixels.restype = c_int +_dll.openmc_solidraytrace_plot_get_pixels.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_pixels.argtypes = [c_int32, c_int32, c_int32] +_dll.openmc_solidraytrace_plot_set_pixels.restype = c_int +_dll.openmc_solidraytrace_plot_set_pixels.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_color_by.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_solidraytrace_plot_get_color_by.restype = c_int +_dll.openmc_solidraytrace_plot_get_color_by.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_color_by.argtypes = [c_int32, c_int32] +_dll.openmc_solidraytrace_plot_set_color_by.restype = c_int +_dll.openmc_solidraytrace_plot_set_color_by.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_default_colors.argtypes = [c_int32] +_dll.openmc_solidraytrace_plot_set_default_colors.restype = c_int +_dll.openmc_solidraytrace_plot_set_default_colors.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_all_opaque.argtypes = [c_int32] +_dll.openmc_solidraytrace_plot_set_all_opaque.restype = c_int +_dll.openmc_solidraytrace_plot_set_all_opaque.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_opaque.argtypes = [c_int32, c_int32, c_bool] +_dll.openmc_solidraytrace_plot_set_opaque.restype = c_int +_dll.openmc_solidraytrace_plot_set_opaque.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_color.argtypes = [c_int32, c_int32, c_uint8, c_uint8, c_uint8] +_dll.openmc_solidraytrace_plot_set_color.restype = c_int +_dll.openmc_solidraytrace_plot_set_color.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_camera_position.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_camera_position.restype = c_int +_dll.openmc_solidraytrace_plot_get_camera_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_camera_position.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_camera_position.restype = c_int +_dll.openmc_solidraytrace_plot_set_camera_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_look_at.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_look_at.restype = c_int +_dll.openmc_solidraytrace_plot_get_look_at.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_look_at.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_look_at.restype = c_int +_dll.openmc_solidraytrace_plot_set_look_at.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_up.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_up.restype = c_int +_dll.openmc_solidraytrace_plot_get_up.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_up.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_up.restype = c_int +_dll.openmc_solidraytrace_plot_set_up.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_light_position.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_light_position.restype = c_int +_dll.openmc_solidraytrace_plot_get_light_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_light_position.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_light_position.restype = c_int +_dll.openmc_solidraytrace_plot_set_light_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_fov.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_fov.restype = c_int +_dll.openmc_solidraytrace_plot_get_fov.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_fov.argtypes = [c_int32, c_double] +_dll.openmc_solidraytrace_plot_set_fov.restype = c_int +_dll.openmc_solidraytrace_plot_set_fov.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_update_view.argtypes = [c_int32] +_dll.openmc_solidraytrace_plot_update_view.restype = c_int +_dll.openmc_solidraytrace_plot_update_view.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_create_image.argtypes = [c_int32, POINTER(c_uint8), c_int32, c_int32] +_dll.openmc_solidraytrace_plot_create_image.restype = c_int +_dll.openmc_solidraytrace_plot_create_image.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_color.argtypes = [c_int32, c_int32, + POINTER(c_uint8), POINTER(c_uint8), POINTER(c_uint8)] +_dll.openmc_solidraytrace_plot_get_color.restype = c_int +_dll.openmc_solidraytrace_plot_get_color.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_diffuse_fraction.argtypes = [ + c_int32, POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_diffuse_fraction.restype = c_int +_dll.openmc_solidraytrace_plot_get_diffuse_fraction.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_diffuse_fraction.argtypes = [c_int32, c_double] +_dll.openmc_solidraytrace_plot_set_diffuse_fraction.restype = c_int +_dll.openmc_solidraytrace_plot_set_diffuse_fraction.errcheck = _error_handler + + +class SolidRayTracePlot(_FortranObjectWithID): + """Solid ray-traced plot stored internally. + + This class exposes a solid ray-traced plot that is stored internally in + the OpenMC library. To obtain a view of an existing plot with a given ID, + use the :data:`openmc.lib.plots` mapping. + + Parameters + ---------- + uid : int or None + Unique ID of the plot + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int or None + Index in the internal plots array. + + Attributes + ---------- + id : int + Unique ID of the plot. + pixels : tuple of int + Plot image dimensions as ``(width, height)``. + color_by : int + Coloring mode. Use :attr:`COLOR_BY_MATERIAL` or + :attr:`COLOR_BY_CELL`. + camera_position : tuple of float + Camera position as ``(x, y, z)``. + look_at : tuple of float + Point the camera is aimed at as ``(x, y, z)``. + up : tuple of float + Up direction as ``(x, y, z)``. + light_position : tuple of float + Position of the light source as ``(x, y, z)``. + fov : float + Horizontal field-of-view angle in degrees. + diffuse_fraction : float + Fraction of reflected light treated as diffuse (0 to 1). + """ + + COLOR_BY_MATERIAL = 0 + COLOR_BY_CELL = 1 + __instances = WeakValueDictionary() + + def __new__(cls, uid=None, new=True, index=None): + mapping = plots + if index is None: + if new: + if uid is not None and uid in mapping: + raise AllocationError( + f'A plot with ID={uid} has already been allocated.' + ) + index = c_int32() + _dll.openmc_solidraytrace_plot_create(index) + index = index.value + else: + index = mapping[uid]._index + + if index not in cls.__instances: + instance = super().__new__(cls) + instance._index = index + if uid is not None: + instance.id = uid + cls.__instances[index] = instance + + return cls.__instances[index] + + def __init__(self, uid=None, new=True, index=None): + super().__init__(uid, new, index) + + @property + def id(self): + plot_id = c_int32() + _dll.openmc_plot_get_id(self._index, plot_id) + return plot_id.value + + @id.setter + def id(self, plot_id): + _dll.openmc_plot_set_id(self._index, plot_id) + + @staticmethod + def _get_xyz(getter, index): + x = c_double() + y = c_double() + z = c_double() + getter(index, x, y, z) + return (x.value, y.value, z.value) + + @staticmethod + def _set_xyz(setter, index, xyz): + x, y, z = xyz + setter(index, float(x), float(y), float(z)) + + @property + def pixels(self): + width = c_int32() + height = c_int32() + _dll.openmc_solidraytrace_plot_get_pixels(self._index, width, height) + return (width.value, height.value) + + @pixels.setter + def pixels(self, pixels): + width, height = pixels + _dll.openmc_solidraytrace_plot_set_pixels( + self._index, int(width), int(height)) + + @property + def color_by(self): + color_by = c_int32() + _dll.openmc_solidraytrace_plot_get_color_by(self._index, color_by) + return color_by.value + + @color_by.setter + def color_by(self, color_by): + _dll.openmc_solidraytrace_plot_set_color_by(self._index, int(color_by)) + + def set_default_colors(self): + _dll.openmc_solidraytrace_plot_set_default_colors(self._index) + + def set_all_opaque(self): + _dll.openmc_solidraytrace_plot_set_all_opaque(self._index) + + def set_visibility(self, domain_id, visible): + _dll.openmc_solidraytrace_plot_set_opaque( + self._index, int(domain_id), bool(visible) + ) + + def set_color(self, domain_id, color): + r, g, b = [int(c) for c in color] + _dll.openmc_solidraytrace_plot_set_color( + self._index, int(domain_id), r, g, b) + + @property + def camera_position(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_camera_position, + self._index) + + @camera_position.setter + def camera_position(self, position): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_camera_position, + self._index, position) + + @property + def look_at(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_look_at, + self._index) + + @look_at.setter + def look_at(self, position): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_look_at, + self._index, position) + + @property + def up(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_up, self._index) + + @up.setter + def up(self, direction): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_up, self._index, + direction) + + @property + def light_position(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_light_position, + self._index) + + @light_position.setter + def light_position(self, position): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_light_position, + self._index, position) + + @property + def fov(self): + fov = c_double() + _dll.openmc_solidraytrace_plot_get_fov(self._index, fov) + return fov.value + + @fov.setter + def fov(self, fov): + _dll.openmc_solidraytrace_plot_set_fov(self._index, float(fov)) + + def update_view(self): + _dll.openmc_solidraytrace_plot_update_view(self._index) + + def create_image(self): + width, height = self.pixels + image = np.zeros((height, width, 3), dtype=np.uint8) + _dll.openmc_solidraytrace_plot_create_image( + self._index, + image.ctypes.data_as(POINTER(c_uint8)), + width, + height + ) + return image + + def get_color(self, domain_id): + r = c_uint8() + g = c_uint8() + b = c_uint8() + _dll.openmc_solidraytrace_plot_get_color( + self._index, int(domain_id), r, g, b) + return int(r.value), int(g.value), int(b.value) + + @property + def diffuse_fraction(self): + value = c_double() + _dll.openmc_solidraytrace_plot_get_diffuse_fraction(self._index, value) + return value.value + + @diffuse_fraction.setter + def diffuse_fraction(self, value): + _dll.openmc_solidraytrace_plot_set_diffuse_fraction( + self._index, float(value)) + + # Backward-compatible setter aliases + def set_pixels(self, width, height): + self.pixels = (width, height) + + def set_color_by(self, color_by): + self.color_by = color_by + + def set_camera_position(self, x, y, z): + self.camera_position = (x, y, z) + + def set_look_at(self, x, y, z): + self.look_at = (x, y, z) + + def set_up(self, x, y, z): + self.up = (x, y, z) + + def set_light_position(self, x, y, z): + self.light_position = (x, y, z) + + def set_fov(self, fov): + self.fov = fov + + def set_diffuse_fraction(self, value): + self.diffuse_fraction = value + + +class _PlotMapping(Mapping): + def __getitem__(self, key): + index = c_int32() + try: + _dll.openmc_get_plot_index(key, index) + except (AllocationError, InvalidIDError) as e: + raise KeyError(str(e)) + return SolidRayTracePlot(index=index.value) + + def __iter__(self): + for i in range(len(self)): + yield SolidRayTracePlot(index=i).id + + def __len__(self): + return _dll.openmc_plots_size() + + def __repr__(self): + return repr(dict(self)) + + +plots = _PlotMapping() diff --git a/openmc/lib/weight_windows.py b/openmc/lib/weight_windows.py index ed442d33f..2b26d3b55 100644 --- a/openmc/lib/weight_windows.py +++ b/openmc/lib/weight_windows.py @@ -53,11 +53,11 @@ _dll.openmc_weight_windows_get_energy_bounds.argtypes = [c_int32, POINTER(POINTE _dll.openmc_weight_windows_get_energy_bounds.restype = c_int _dll.openmc_weight_windows_get_energy_bounds.errcheck = _error_handler -_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int] +_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int32] _dll.openmc_weight_windows_set_particle.restype = c_int _dll.openmc_weight_windows_set_particle.errcheck = _error_handler -_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int)] +_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_weight_windows_get_particle.restype = c_int _dll.openmc_weight_windows_get_particle.errcheck = _error_handler @@ -201,16 +201,13 @@ class WeightWindows(_FortranObjectWithID): @property def particle(self): - val = c_int() + val = c_int32() _dll.openmc_weight_windows_get_particle(self._index, val) return ParticleType(val.value) @particle.setter def particle(self, p): - if isinstance(p, str): - p = ParticleType.from_string(p) - else: - p = ParticleType(p) + p = ParticleType(p) _dll.openmc_weight_windows_set_particle(self._index, int(p)) @property @@ -304,10 +301,10 @@ class WeightWindows(_FortranObjectWithID): ---------- tally : openmc.lib.Tally The tally used to create the WeightWindows instance. - particle : openmc.ParticleType or str, optional + particle : openmc.ParticleType or str or int, optional The particle type to use for the WeightWindows instance. Should be - specified as an instance of ParticleType or as a string with a value of - 'neutron' or 'photon'. + specified as an instance of ParticleType, a PDG number, or as a + name. Returns ------- @@ -317,7 +314,8 @@ class WeightWindows(_FortranObjectWithID): Raises ------ ValueError - If the particle parameter is not an instance of ParticleType or a string. + If the particle parameter is not an instance of ParticleType, a string, + or an integer PDG number. ValueError If the particle parameter is not a valid particle type (i.e., not 'neutron' or 'photon'). @@ -328,12 +326,13 @@ class WeightWindows(_FortranObjectWithID): If the tally does not have a MeshFilter. """ # do some checks on particle value - if not isinstance(particle, (ParticleType, str)): - raise ValueError(f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon').") + if not isinstance(particle, (ParticleType, str, int)): + raise ValueError( + f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon')." + ) # convert particle type if needed - if isinstance(particle, str): - particle = ParticleType.from_string(particle) + particle = ParticleType(particle) if particle not in (ParticleType.NEUTRON, ParticleType.PHOTON): raise ValueError('Weight windows can only be applied for neutrons or photons') diff --git a/openmc/material.py b/openmc/material.py index 1609da05a..3a92efda2 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections import defaultdict, namedtuple, Counter from collections.abc import Iterable from copy import deepcopy +from functools import reduce from numbers import Real from pathlib import Path import re import sys import tempfile -from typing import Sequence, Dict +from typing import TYPE_CHECKING, Literal, Sequence, Dict import warnings import lxml.etree as ET @@ -22,8 +23,13 @@ from .mixin import IDManagerMixin from .utility_funcs import input_path from . import waste from openmc.checkvalue import PathLike -from openmc.stats import Univariate, Discrete, Mixture -from openmc.data.data import _get_element_symbol +from openmc.stats import Univariate, Discrete, Mixture, Tabular +from openmc.data.data import _get_element_symbol, JOULE_PER_EV +from openmc.data.function import Tabulated1D +from openmc.data import mass_energy_absorption_coefficient, dose_coefficients + +if TYPE_CHECKING: + from openmc.deplete import Chain # Units for density supported by OpenMC @@ -60,6 +66,26 @@ class Material(IDManagerMixin): temperature : float, optional Temperature of the material in Kelvin. If not specified, the material inherits the default temperature applied to the model. + density : float, optional + Density of the material (units defined separately) + density_units : str + Units used for `density`. Can be one of 'g/cm3', 'g/cc', 'kg/m3', + 'atom/b-cm', 'atom/cm3', 'sum', or 'macro'. The 'macro' unit only + applies in the case of a multi-group calculation. Defaults to 'sum'. + depletable : bool, optional + Indicate whether the material is depletable. Defaults to False. + volume : float, optional + Volume of the material in cm^3. This can either be set manually or + calculated in a stochastic volume calculation and added via the + :meth:`Material.add_volume_information` method. + components : dict of str to float or dict + Dictionary mapping element or nuclide names to their atom or weight + percent. To specify enrichment of an element, the entry of + ``components`` for that element must instead be a dictionary containing + the keyword arguments as well as a value for ``'percent'`` + percent_type : {'ao', 'wo'} + Whether the values in `components` should be interpreted as atom percent + ('ao') or weight percent ('wo'). Attributes ---------- @@ -111,17 +137,28 @@ class Material(IDManagerMixin): next_id = 1 used_ids = set() - def __init__(self, material_id=None, name='', temperature=None): + def __init__( + self, + material_id: int | None = None, + name: str = "", + temperature: float | None = None, + density: float | None = None, + density_units: str = "sum", + depletable: bool | None = False, + volume: float | None = None, + components: dict | None = None, + percent_type: str = "ao", + ): # Initialize class attributes self.id = material_id self.name = name self.temperature = temperature self._density = None - self._density_units = 'sum' - self._depletable = False + self._density_units = density_units + self._depletable = depletable self._paths = None self._num_instances = None - self._volume = None + self._volume = volume self._atoms = {} self._isotropic = [] self._ncrystal_cfg = None @@ -136,6 +173,15 @@ class Material(IDManagerMixin): # If specified, a list of table names self._sab = [] + # Set density if provided + if density is not None: + self.set_density(density_units, density) + + # Add components if provided + if components is not None: + self.add_components(components, percent_type=percent_type) + + def __repr__(self) -> str: string = 'Material\n' string += '{: <16}=\t{}\n'.format('\tID', self._id) @@ -253,6 +299,8 @@ class Material(IDManagerMixin): mass += nuc.percent # Compute and return the molar mass + if moles == 0.0: + raise ValueError("Material has no nuclides; cannot compute molar mass") return mass / moles @property @@ -306,7 +354,7 @@ class Material(IDManagerMixin): clip_tolerance : float Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions that will be discarded. - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'} Specifies the units on the integral of the distribution. volume : float, optional Volume of the material. If not passed, defaults to using the @@ -324,7 +372,7 @@ class Material(IDManagerMixin): the total intensity of the photon source in the requested units. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'}) if exclude_nuclides is not None and include_nuclides is not None: raise ValueError("Cannot specify both exclude_nuclides and include_nuclides") @@ -335,6 +383,8 @@ class Material(IDManagerMixin): raise ValueError("volume must be specified if units='Bq'") elif units == 'Bq/cm3': multiplier = 1 + elif units == 'Bq/m3': + multiplier = 1e6 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() elif units == 'Bq/kg': @@ -369,6 +419,190 @@ class Material(IDManagerMixin): return combined + def get_photon_contact_dose_rate( + self, + dose_quantity: str = "absorbed-air", + build_up: float = 2.0, + by_nuclide: bool = False + ) -> float | dict[str, float]: + """Compute the photon contact dose rate (CDR) produced by radioactive decay + of the material. + + The contact dose rate is calculated from decay photon energy spectra for + each nuclide in the material, combined with photon mass attenuation data + for the material and the appropriate response function for the dose quantity. + A slab-geometry approximation and a photon build-up factor are used. + + Absorbed-air dose: + The approach follows the FISPACT-II manual (UKAEA-CCFE-RE(21)02 - May 2021). + Appendix C.7.1. + This method integrates over the photon energy: + + (B/2) * (mu_en_air(E) / mu_material(E)) * E * S(E) + + Effective dose: + The approach uses ICRP-116 effective dose coefficients to convert the photon + fluence due to decay photons to effective dose. + This method integrates over the photon energy: + + (B/2) * (h_e(E) / mu_material(E)) * S(E) + + where: + - mu_en_air(E) is the air mass energy-absorption coefficient, + - mu_material(E) is the photon mass attenuation coefficient of the material, + - S(E) is the photon emission spectrum per atom, + - h_e(E) is the ICRP-116 effective dose coefficient, + - B is the build-up factor, + - E is the photon energy. + + Parameters + ---------- + dose_quantity : {'absorbed-air', 'effective'}, optional + Specifies the dose quantity to be calculated. + The only supported options are 'absorbed-air' which implements the methodology + from FISPACT-II, and 'effective' which uses ICRP-116 effective dose coefficients. + build_up : float, optional. The default value is 2.0 as suggested in the FISPACT-II + manual. + by_nuclide : bool, optional + Specifies if the cdr should be returned for the material as a + whole or per nuclide. Default is False. + + Limitations + ---------- + This method does not implement correction from Bremsstrahlung particles which can be + relevant at close distances. + In addition, it computes the gamma contact dose rate only for the unstable nuclides + for which the radiation source specification is present in the chain file. + + Returns + ------- + cdr : float or dict[str, float] + Contact Dose Rate due to decay photons. + 'absorbed-air': returns the absorbed dose in air [Gy/hr]. + 'effective': returns the effective dose [Sv/hr]. + """ + + cv.check_type("by_nuclide", by_nuclide, bool) + cv.check_type("dose_quantity", dose_quantity, str) + cv.check_value("dose_quantity", dose_quantity, {'absorbed-air', 'effective'}) + cv.check_type("build_up", build_up, Real) + cv.check_greater_than("build_up", build_up, 0.0) + + nuc_densities = self.get_nuclide_atom_densities() + if not nuc_densities: + raise ValueError("Material has no nuclides; cannot compute mass attenuation") + + # Collect partial mass densities ρ_i [g/cm³] and elemental mass + # attenuation coefficients µ_i/ρ_i [cm²/g] per nuclide + nuc_attenuation = [] + for nuc, atom_density_bcm in nuc_densities.items(): + Z = openmc.data.zam(nuc)[0] + mu_over_rho = openmc.data.mass_attenuation_coefficient(Z) + rho_i = ( + atom_density_bcm * 1.0e24 + * openmc.data.atomic_mass(nuc) / openmc.data.AVOGADRO + ) + nuc_attenuation.append((rho_i, mu_over_rho)) + + # Build union energy grid across all nuclides + mu_e_vals = reduce(np.union1d, [t.x for _, t in nuc_attenuation]) + + # Build the material linear attenuation coefficient µ_material(E) [cm⁻¹] + # as the sum of ρ_i * (µ_i/ρ_i)(E) over all nuclides + mu_material_vals = np.zeros(len(mu_e_vals)) + for rho_i, mu_over_rho in nuc_attenuation: + mu_material_vals += rho_i * mu_over_rho(mu_e_vals) + mu_material = Tabulated1D( + mu_e_vals, mu_material_vals, breakpoints=[len(mu_e_vals)], interpolation=[5]) + + # CDR computation + cdr = {} + + geometry_factor_slab = 0.5 + + # ancillary conversion factors for clarity + seconds_per_hour = 3600.0 + grams_per_kg = 1000.0 + sv_per_psv = 1e-12 + + if dose_quantity == 'absorbed-air': + # mu_en/rho for air [cm²/g] as a function of energy [eV] + response_f = mass_energy_absorption_coefficient("air", data_source="nist126") + + # Factor to convert [eV cm²/(b g s)] to [Gy/h] + multiplier = (build_up * geometry_factor_slab * seconds_per_hour + * grams_per_kg * 1e24 * JOULE_PER_EV) + + elif dose_quantity == 'effective': + # effective dose as a function of photon fluence [pSv cm²] + response_f_x, response_f_y = dose_coefficients( + "photon", geometry='AP', data_source='icrp116') + response_f = Tabulated1D(response_f_x, response_f_y, breakpoints=[ + len(response_f_x)], interpolation=[5]) + + # Convert [pSv cm²/(b-s)] to [Sv/h] + multiplier = (build_up * geometry_factor_slab * seconds_per_hour + * sv_per_psv * 1e24) + + for nuc, nuc_atoms_per_bcm in self.get_nuclide_atom_densities().items(): + photon_source_per_atom = openmc.data.decay_photon_energy(nuc) + + # nuclides with no contribution + if photon_source_per_atom is None or nuc_atoms_per_bcm <= 0.0: + cdr[nuc] = 0.0 + continue + + if not isinstance(photon_source_per_atom, (Discrete, Tabular)): + raise ValueError( + f"Unknown decay photon energy data type for nuclide {nuc}" + f"value returned: {type(photon_source_per_atom)}" + ) + + e_vals = photon_source_per_atom.x + p_vals = photon_source_per_atom.p + + # Construct list of energies from (photon source, response function, + # mu_en_air) for clipping to common energy range + e_lists = [e_vals, response_f.x, mu_e_vals] + + # clip distributions for values outside the tabulated values + left_bound = max(a.min() for a in e_lists) + right_bound = min(a.max() for a in e_lists) + + mask = (e_vals >= left_bound) & (e_vals <= right_bound) + e_vals = e_vals[mask] + p_vals = p_vals[mask] + + if isinstance(photon_source_per_atom, Tabular): + # limit the computation to the tabulated mu_en_air range + e_union = reduce(np.union1d, e_lists) + e_union = e_union[(e_union >= left_bound) & (e_union <= right_bound)] + if len(e_union) < 2: + raise ValueError("Not enough overlapping energy points to compute CDR") + + # Histogram interpolation: each new point inherits the value of + # the nearest original point to its left + p_vals = p_vals[np.searchsorted(e_vals, e_union, side='right') - 1] + e_vals = e_union + + mu_vals = mu_material(e_vals) + if dose_quantity == 'absorbed-air': + # Compute (µ_en_air(E) / µ_material(E)) * E * S(E) + integrand = (response_f(e_vals) / mu_vals) * p_vals * e_vals + elif dose_quantity == 'effective': + # Compute (h_e(E) / µ_material(E)) * S(E) + integrand = (response_f(e_vals) / mu_vals) * p_vals + + if isinstance(photon_source_per_atom, Discrete): + cdr_nuc = np.sum(integrand) + elif isinstance(photon_source_per_atom, Tabular): + cdr_nuc = np.trapezoid(integrand, e_vals) + + # Compute air-absorbed dose [Gy/h] or effective dose [Sv/h] + cdr[nuc] = float(cdr_nuc * nuc_atoms_per_bcm * multiplier) + + return cdr if by_nuclide else sum(cdr.values()) + @classmethod def from_hdf5(cls, group: h5py.Group) -> Material: """Create material from HDF5 group @@ -1154,18 +1388,23 @@ class Material(IDManagerMixin): return densities - def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, - volume: float | None = None) -> dict[str, float] | float: - """Returns the activity of the material or of each nuclide within. + def get_activity( + self, + units: str = 'Bq/cm3', + by_nuclide: bool = False, + volume: float | None = None, + chain_file: Literal[False] | None | PathLike | Chain = None + ) -> dict[str, float] | float: + """Return the activity of the material or each nuclide within. .. versionadded:: 0.13.1 Parameters ---------- - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Ci', 'Ci/m3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3', 'Ci', 'Ci/m3'} Specifies the type of activity to return, options include total activity [Bq,Ci], specific [Bq/g, Bq/kg] or volumetric activity - [Bq/cm3,Ci/m3]. Default is volumetric activity [Bq/cm3]. + [Bq/cm3, Bq/m3, Ci/m3]. Default is volumetric activity [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. @@ -1174,25 +1413,39 @@ class Material(IDManagerMixin): :attr:`Material.volume` attribute. .. versionadded:: 0.13.3 + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For + ``None`` or an explicit chain, nuclides absent from the chain fall + back to ENDF/B-VIII.0 data. + + .. versionadded:: 0.15.4 Returns ------- Union[dict, float] - If by_nuclide is True then a dictionary whose keys are nuclide - names and values are activity is returned. Otherwise the activity - of the material is returned as a float. + If by_nuclide is True then a dictionary whose keys are nuclide names + and values are activity is returned. Otherwise the activity of the + material is returned as a float. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Ci', 'Ci/m3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3', 'Ci', 'Ci/m3'}) cv.check_type('by_nuclide', by_nuclide, bool) if volume is None: volume = self.volume + if units in {'Bq', 'Ci'} and volume is None: + raise ValueError(f"Volume must be set in order to compute activity in '{units}'.") + if units == 'Bq': multiplier = volume elif units == 'Bq/cm3': multiplier = 1 + elif units == 'Bq/m3': + multiplier = 1e6 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() elif units == 'Bq/kg': @@ -1204,23 +1457,23 @@ class Material(IDManagerMixin): activity = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): - inv_seconds = openmc.data.decay_constant(nuclide) + inv_seconds = openmc.data.decay_constant( + nuclide, chain_file=chain_file) activity[nuclide] = inv_seconds * 1e24 * atoms_per_bcm * multiplier return activity if by_nuclide else sum(activity.values()) def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: - """Returns the decay heat of the material or for each nuclide in the - material in units of [W], [W/g], [W/kg] or [W/cm3]. + """Return the decay heat of the material or each nuclide within. .. versionadded:: 0.13.3 Parameters ---------- - units : {'W', 'W/g', 'W/kg', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3', 'W/m3'} Specifies the units of decay heat to return. Options include total - heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. + heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3, W/m3]. Default is total heat [W]. by_nuclide : bool Specifies if the decay heat should be returned for the material as a @@ -1239,13 +1492,17 @@ class Material(IDManagerMixin): of the material is returned as a float. """ - cv.check_value('units', units, {'W', 'W/g', 'W/kg', 'W/cm3'}) + cv.check_value('units', units, {'W', 'W/g', 'W/kg', 'W/cm3', 'W/m3'}) cv.check_type('by_nuclide', by_nuclide, bool) if units == 'W': multiplier = volume if volume is not None else self.volume + if multiplier is None: + raise ValueError("Volume must be set in order to compute total decay heat.") elif units == 'W/cm3': multiplier = 1 + elif units == 'W/m3': + multiplier = 1e6 elif units == 'W/g': multiplier = 1.0 / self.get_mass_density() elif units == 'W/kg': @@ -2050,7 +2307,7 @@ class Materials(cv.CheckedList): multigroup_fluxes: Sequence[Sequence[float]] Energy-dependent multigroup flux values, where each sublist corresponds to a specific material. Will be normalized so that it sums to 1. - energy_group_structures': Sequence[Sequence[float] | str] + energy_group_structures: Sequence[Sequence[float] | str] Energy group boundaries in [eV] or the name of the group structure. timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are @@ -2085,6 +2342,11 @@ class Materials(cv.CheckedList): for mat in self: mat.depletable = True + if len(multigroup_fluxes) != len(self): + raise ValueError("multigroup_fluxes length must match number of materials") + if len(energy_group_structures) != len(self): + raise ValueError("energy_group_structures length must match number of materials") + chain = _get_chain(chain_file) # Create MicroXS objects for all materials @@ -2095,6 +2357,10 @@ class Materials(cv.CheckedList): for material, flux, energy in zip( self, multigroup_fluxes, energy_group_structures ): + if material.volume is None: + raise ValueError( + f"Material {material.id} has no volume; cannot deplete" + ) temperature = material.temperature or 293.6 micro_xs = openmc.deplete.MicroXS.from_multigroup_flux( energies=energy, diff --git a/openmc/mesh.py b/openmc/mesh.py index ce5218b5e..ff8e14577 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -3,7 +3,7 @@ import warnings from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence, Mapping from functools import wraps -from math import pi, sqrt, atan2 +import math from numbers import Integral, Real from pathlib import Path from typing import Protocol @@ -11,12 +11,11 @@ from typing import Protocol import h5py import lxml.etree as ET import numpy as np -from pathlib import Path import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from openmc.utility_funcs import change_directory +from .bounding_box import BoundingBox from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES @@ -40,6 +39,11 @@ class MeshMaterialVolumes(Mapping): Array of shape (elements, max_materials) storing material IDs volumes : numpy.ndarray Array of shape (elements, max_materials) storing material volumes + bboxes : numpy.ndarray, optional + Array of shape (elements, max_materials, 6) storing axis-aligned + bounding boxes for each (element, material) combination with ordering + (xmin, ymin, zmin, xmax, ymax, zmax). Bounding boxes enclose the + ray-estimator prisms used to compute volumes. See Also -------- @@ -64,9 +68,30 @@ class MeshMaterialVolumes(Mapping): [(2, 31.87963824195591), (1, 6.129949130817542)] """ - def __init__(self, materials: np.ndarray, volumes: np.ndarray): + def __init__( + self, + materials: np.ndarray, + volumes: np.ndarray, + bboxes: np.ndarray | None = None + ): self._materials = materials self._volumes = volumes + self._bboxes = bboxes + + if self._bboxes is not None: + if self._bboxes.shape[:2] != self._materials.shape: + raise ValueError( + 'bboxes must have shape (elements, max_materials, 6) ' + 'matching materials/volumes.' + ) + if self._bboxes.shape[2] != 6: + raise ValueError( + 'bboxes must have shape (elements, max_materials, 6).' + ) + + @property + def has_bounding_boxes(self) -> bool: + return self._bboxes is not None @property def num_elements(self) -> int: @@ -92,7 +117,11 @@ class MeshMaterialVolumes(Mapping): volumes[indices] = self._volumes[indices, i] return volumes - def by_element(self, index_elem: int) -> list[tuple[int | None, float]]: + def by_element( + self, + index_elem: int, + include_bboxes: bool = False + ) -> list[tuple[int | None, float] | tuple[int | None, float, BoundingBox | None]]: """Get a list of volumes for each material within a specific element. Parameters @@ -102,15 +131,32 @@ class MeshMaterialVolumes(Mapping): Returns ------- - list of tuple of (material ID, volume) + list of tuple + If ``include_bboxes`` is False (default), returns tuples of + (material ID, volume). If ``include_bboxes`` is True, returns + tuples of (material ID, volume, bounding box). """ table_size = self._volumes.shape[1] - return [ - (m if m > -1 else None, self._volumes[index_elem, i]) - for i in range(table_size) - if (m := self._materials[index_elem, i]) != -2 - ] + if include_bboxes and self._bboxes is None: + raise ValueError('Bounding boxes were not computed for this object.') + + results = [] + for i in range(table_size): + m = self._materials[index_elem, i] + if m == -2: + continue + mat_id = m if m > -1 else None + vol = self._volumes[index_elem, i] + + if include_bboxes: + vals = self._bboxes[index_elem, i] + bbox = BoundingBox(vals[0:3], vals[3:6]) + results.append((mat_id, vol, bbox)) + else: + results.append((mat_id, vol)) + + return results def save(self, filename: PathLike): """Save material volumes to a .npz file. @@ -120,8 +166,10 @@ class MeshMaterialVolumes(Mapping): filename : path-like Filename where data will be saved """ - np.savez_compressed( - filename, materials=self._materials, volumes=self._volumes) + kwargs = {'materials': self._materials, 'volumes': self._volumes} + if self._bboxes is not None: + kwargs['bboxes'] = self._bboxes + np.savez_compressed(filename, **kwargs) @classmethod def from_npz(cls, filename: PathLike) -> MeshMaterialVolumes: @@ -134,7 +182,8 @@ class MeshMaterialVolumes(Mapping): """ filedata = np.load(filename) - return cls(filedata['materials'], filedata['volumes']) + bboxes = filedata['bboxes'] if 'bboxes' in filedata.files else None + return cls(filedata['materials'], filedata['volumes'], bboxes) class MeshBase(IDManagerMixin, ABC): @@ -153,11 +202,17 @@ class MeshBase(IDManagerMixin, ABC): Unique identifier for the mesh name : str Name of the mesh + lower_left : Iterable of float + The lower-left coordinates + upper_right : Iterable of float + The upper-right coordinates bounding_box : openmc.BoundingBox Axis-aligned bounding box of the mesh as defined by the upper-right and lower-left coordinates. indices : Iterable of tuple An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, 1), ...] + n_elements : int + Number of elements in the mesh """ next_id = 1 @@ -180,6 +235,16 @@ class MeshBase(IDManagerMixin, ABC): else: self._name = '' + @property + @abstractmethod + def lower_left(self): + pass + + @property + @abstractmethod + def upper_right(self): + pass + @property def bounding_box(self) -> openmc.BoundingBox: return openmc.BoundingBox(self.lower_left, self.upper_right) @@ -189,6 +254,11 @@ class MeshBase(IDManagerMixin, ABC): def indices(self): pass + @property + @abstractmethod + def n_elements(self): + pass + def __repr__(self): string = type(self).__name__ + '\n' string += '{0: <16}{1}{2}\n'.format('\tID', '=\t', self._id) @@ -196,10 +266,11 @@ class MeshBase(IDManagerMixin, ABC): return string def _volume_dim_check(self): - if self.n_dimension != 3 or \ - any([d == 0 for d in self.dimension]): - raise RuntimeError(f'Mesh {self.id} is not 3D. ' - 'Volumes cannot be provided.') + if any(d == 0 for d in self.dimension): + raise RuntimeError( + f'Mesh {self.id} has a zero-size dimension. ' + 'Volumes cannot be provided.' + ) @classmethod def from_hdf5(cls, group: h5py.Group): @@ -218,7 +289,7 @@ class MeshBase(IDManagerMixin, ABC): """ mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode() mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - mesh_name = '' if not 'name' in group else group['name'][()].decode() + mesh_name = '' if 'name' not in group else group['name'][()].decode() if mesh_type == 'regular': return RegularMesh.from_hdf5(group, mesh_id, mesh_name) @@ -366,6 +437,7 @@ class MeshBase(IDManagerMixin, ABC): model: openmc.Model, n_samples: int | tuple[int, int, int] = 10_000, max_materials: int = 4, + bounding_boxes: bool = False, **kwargs ) -> MeshMaterialVolumes: """Determine volume of materials in each mesh element. @@ -388,6 +460,11 @@ class MeshBase(IDManagerMixin, ABC): the x, y, and z dimensions. max_materials : int, optional Estimated maximum number of materials in any given mesh element. + bounding_boxes : bool, optional + Whether to compute an axis-aligned bounding box for each + (mesh element, material) combination. When enabled, the bounding + box encloses the ray-estimator prisms used for the volume + estimation. **kwargs : dict Keyword arguments passed to :func:`openmc.lib.init` @@ -419,7 +496,8 @@ class MeshBase(IDManagerMixin, ABC): # Compute material volumes volumes = mesh.material_volumes( - n_samples, max_materials, output=kwargs['output']) + n_samples, max_materials, output=kwargs['output'], + bounding_boxes=bounding_boxes) # Restore original tallies model.tallies = original_tallies @@ -458,11 +536,20 @@ class StructuredMesh(MeshBase): def n_dimension(self): pass + @property + @abstractmethod + def _axis_labels(self): + pass + @property @abstractmethod def _grids(self): pass + @abstractmethod + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + pass + @property def vertices(self): """Return coordinates of mesh vertices in Cartesian coordinates. Also @@ -559,9 +646,18 @@ class StructuredMesh(MeshBase): return (vertices[s0] + vertices[s1]) / 2 @property - def num_mesh_cells(self): + def n_elements(self): return np.prod(self.dimension) + @property + def num_mesh_cells(self): + warnings.warn( + "The 'num_mesh_cells' attribute is deprecated and will be removed in a future version. " + "Use 'n_elements' instead.", + FutureWarning, stacklevel=2 + ) + return self.n_elements + def write_data_to_vtk(self, filename: PathLike, datasets: dict | None = None, @@ -822,10 +918,10 @@ class StructuredMesh(MeshBase): """ cv.check_type('data label', label, str) - if dataset.size != self.num_mesh_cells: + if dataset.size != self.n_elements: raise ValueError( f"The size of the dataset '{label}' ({dataset.size}) should be" - f" equal to the number of mesh cells ({self.num_mesh_cells})" + f" equal to the number of mesh cells ({self.n_elements})" ) # accept a flat array as-is, assuming it is in the correct order @@ -839,6 +935,87 @@ class StructuredMesh(MeshBase): f"with dimensions {self.dimension}" ) + @classmethod + def from_domain( + cls, + domain: HasBoundingBox | BoundingBox, + dimension: Sequence[int] | int | None = None, + mesh_id: int | None = None, + name: str = '', + **kwargs + ) -> StructuredMesh: + """Create a structured mesh from a domain using its bounding box. + + Parameters + ---------- + domain : HasBoundingBox | openmc.BoundingBox + Object used as a template for the mesh extents. If ``domain`` has a + ``bounding_box`` attribute, that bounding box is used directly. + dimension : Iterable of int or int, optional + Number of mesh cells. When omitted, the subclass-specific default is + used. If provided as a single integer, subclasses that support it + interpret it as a target total number of mesh cells. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. + **kwargs + Additional keyword arguments forwarded to + :meth:`from_bounding_box`. + + Returns + ------- + openmc.StructuredMesh + Structured mesh instance. + """ + if isinstance(domain, BoundingBox): + bbox = domain + elif hasattr(domain, 'bounding_box'): + bbox = domain.bounding_box + else: + raise TypeError("Domain must be a BoundingBox or have a " + "bounding_box property") + + if dimension is None: + return cls.from_bounding_box( + bbox, mesh_id=mesh_id, name=name, **kwargs) + + return cls.from_bounding_box( + bbox, dimension=dimension, mesh_id=mesh_id, name=name, **kwargs) + + @classmethod + @abstractmethod + def from_bounding_box( + cls, + bbox: openmc.BoundingBox, + dimension: Sequence[int] | int, + mesh_id: int | None = None, + name: str = '', + **kwargs + ) -> StructuredMesh: + """Create a structured mesh from a bounding box. + + Parameters + ---------- + bbox : openmc.BoundingBox + Bounding box used to define the mesh extents. + dimension : Iterable of int or int + Number of mesh cells. The interpretation and any default value are + defined by the concrete mesh type. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. + **kwargs + Additional keyword arguments accepted by specific subclasses. + + Returns + ------- + openmc.StructuredMesh + Structured mesh instance. + """ + pass + class HasBoundingBox(Protocol): """Object that has a ``bounding_box`` attribute.""" @@ -907,6 +1084,10 @@ class RegularMesh(StructuredMesh): else: return None + @property + def _axis_labels(self): + return ('x', 'y', 'z')[:self.n_dimension] + @property def lower_left(self): return self._lower_left @@ -1089,55 +1270,47 @@ class RegularMesh(StructuredMesh): return mesh @classmethod - def from_domain( + def from_bounding_box( cls, - domain: HasBoundingBox, + bbox: openmc.BoundingBox, dimension: Sequence[int] | int = 1000, mesh_id: int | None = None, - name: str = '' - ): - """Create RegularMesh from a domain using its bounding box. + name: str = '', + ) -> RegularMesh: + """Create a RegularMesh from a bounding box. Parameters ---------- - domain : HasBoundingBox - The object passed in will be used as a template for this mesh. The - bounding box of the property of the object passed will be used to - set the lower_left and upper_right and of the mesh instance - dimension : Iterable of int | int - The number of mesh cells in total or number of mesh cells in each - direction (x, y, z). If a single integer is provided, the domain - will will be divided into that many mesh cells with roughly equal - lengths in each direction (cubes). - mesh_id : int - Unique identifier for the mesh - name : str - Name of the mesh + bbox : openmc.BoundingBox + Bounding box used to set the mesh extents. + dimension : Iterable of int or int, optional + The number of mesh cells in each direction (x, y, z). If a single + integer is provided, the total number of cells is distributed + across directions to produce cells with roughly equal widths. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. Returns ------- openmc.RegularMesh - RegularMesh instance - + RegularMesh instance. """ - if not hasattr(domain, 'bounding_box'): - raise TypeError("Domain must have a bounding_box property") - mesh = cls(mesh_id=mesh_id, name=name) - mesh.lower_left = domain.bounding_box[0] - mesh.upper_right = domain.bounding_box[1] + mesh.lower_left = bbox[0] + mesh.upper_right = bbox[1] if isinstance(dimension, int): cv.check_greater_than("dimension", dimension, 1, equality=True) # If a single integer is provided, divide the domain into that many # mesh cells with roughly equal lengths in each direction - ideal_cube_volume = domain.bounding_box.volume / dimension + ideal_cube_volume = bbox.volume / dimension ideal_cube_size = ideal_cube_volume ** (1 / 3) dimension = [ max(1, int(round(side / ideal_cube_size))) - for side in domain.bounding_box.width + for side in bbox.width ] mesh.dimension = dimension - return mesh def to_xml_element(self): @@ -1328,6 +1501,47 @@ class RegularMesh(StructuredMesh): return root_cell, cells + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + """Finds the index of the mesh element at the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point. + + Returns + ------- + tuple + Mesh indices matching the dimensionality of the mesh + + """ + ndim = self.n_dimension + if len(coords) < ndim: + raise ValueError( + f"coords must have at least {ndim} values for a " + f"{ndim}D mesh, got {len(coords)}" + ) + + coords_array = np.array(coords[:ndim]) + lower_left = np.array(self.lower_left) + upper_right = np.array(self.upper_right) + dimension = np.array(self.dimension) + + if np.any(coords_array < lower_left) or np.any(coords_array > upper_right): + raise ValueError( + f"coords {tuple(coords_array)} are outside mesh bounds " + f"[{tuple(lower_left)}, {tuple(upper_right)}]" + ) + + # Calculate spacing for each dimension + spacing = (upper_right - lower_left) / dimension + + # Calculate indices for each coordinate + indices = np.floor((coords_array - lower_left) / spacing).astype(int) + return tuple(int(i) for i in indices[:ndim]) + def Mesh(*args, **kwargs): warnings.warn("Mesh has been renamed RegularMesh. Future versions of " @@ -1387,6 +1601,10 @@ class RectilinearMesh(StructuredMesh): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('x', 'y', 'z') + @property def x_grid(self): return self._x_grid @@ -1535,6 +1753,90 @@ class RectilinearMesh(StructuredMesh): return element + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple[int, int, int]: + """Find the mesh cell indices containing the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point as (x, y, z). + + Returns + ------- + tuple[int, int, int] + Mesh indices (ix, iy, iz). + + Raises + ------ + ValueError + If coords does not contain exactly 3 values, or if a coordinate is + outside the mesh grid boundaries. + """ + if len(coords) != 3: + raise ValueError( + f"coords must contain exactly 3 values for a rectilinear mesh, " + f"got {len(coords)}" + ) + + grids = (self.x_grid, self.y_grid, self.z_grid) + indices = [] + + for grid, value in zip(grids, coords): + if value < grid[0] or value > grid[-1]: + raise ValueError( + f"Coordinate value {value} is outside the mesh grid boundaries: " + f"[{grid[0]}, {grid[-1]}]" + ) + + idx = np.searchsorted(grid, value, side="right") - 1 + indices.append(int(min(idx, len(grid) - 2))) + + return tuple(indices) + + @classmethod + def from_bounding_box( + cls, + bbox: openmc.BoundingBox, + dimension: Sequence[int] | int = 1000, + mesh_id: int | None = None, + name: str = '', + ) -> RectilinearMesh: + """Create a RectilinearMesh from a bounding box with uniform grids. + + Parameters + ---------- + bbox : openmc.BoundingBox + Bounding box used to set the mesh extents. + dimension : Iterable of int or int, optional + The number of mesh cells in each direction (x, y, z). If a single + integer is provided, the total number of cells is distributed across + the three directions proportionally to the side lengths. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. + + Returns + ------- + openmc.RectilinearMesh + RectilinearMesh instance with uniform grids along each axis. + """ + if isinstance(dimension, int): + cv.check_greater_than("dimension", dimension, 1, equality=True) + ideal_cube_volume = bbox.volume / dimension + ideal_cube_size = ideal_cube_volume ** (1 / 3) + dimension = [ + max(1, int(round(side / ideal_cube_size))) + for side in bbox.width + ] + mesh = cls(mesh_id=mesh_id, name=name) + mesh.x_grid = np.linspace(bbox[0][0], bbox[1][0], num=dimension[0] + 1) + mesh.y_grid = np.linspace(bbox[0][1], bbox[1][1], num=dimension[1] + 1) + mesh.z_grid = np.linspace(bbox[0][2], bbox[1][2], num=dimension[2] + 1) + return mesh + class CylindricalMesh(StructuredMesh): """A 3D cylindrical mesh @@ -1599,7 +1901,7 @@ class CylindricalMesh(StructuredMesh): self, r_grid: Sequence[float], z_grid: Sequence[float], - phi_grid: Sequence[float] = (0, 2*pi), + phi_grid: Sequence[float] = (0, 2*math.pi), origin: Sequence[float] = (0., 0., 0.), mesh_id: int | None = None, name: str = '', @@ -1621,6 +1923,10 @@ class CylindricalMesh(StructuredMesh): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('r', 'phi', 'z') + @property def origin(self): return self._origin @@ -1652,7 +1958,7 @@ class CylindricalMesh(StructuredMesh): cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > 2*pi)): + if np.any((grid < 0.0) | (grid > 2*math.pi)): raise ValueError("phi_grid values must be in [0, 2π].") self._phi_grid = grid @@ -1720,17 +2026,17 @@ class CylindricalMesh(StructuredMesh): return string def get_indices_at_coords( - self, - coords: Sequence[float] - ) -> tuple[int, int, int]: - """Finds the index of the mesh voxel at the specified x,y,z coordinates. + self, + coords: Sequence[float] + ) -> tuple[int, int, int]: + """Finds the index of the mesh element at the specified coordinates. .. versionadded:: 0.15.0 Parameters ---------- coords : Sequence[float] - The x, y, z axis coordinates + Cartesian coordinates of the point. Returns ------- @@ -1738,7 +2044,7 @@ class CylindricalMesh(StructuredMesh): The r, phi, z indices """ - r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2) + r_value_from_origin = math.hypot(coords[0]-self.origin[0], coords[1]-self.origin[1]) if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]: raise ValueError( @@ -1762,13 +2068,13 @@ class CylindricalMesh(StructuredMesh): delta_x = coords[0] - self.origin[0] delta_y = coords[1] - self.origin[1] # atan2 returns values in -pi to +pi range - phi_value = atan2(delta_y, delta_x) + phi_value = math.atan2(delta_y, delta_x) if delta_x < 0 and delta_y < 0: # returned phi_value anticlockwise and negative - phi_value += 2 * pi + phi_value += 2 * math.pi if delta_x > 0 and delta_y < 0: # returned phi_value anticlockwise and negative - phi_value += 2 * pi + phi_value += 2 * math.pi phi_grid_values = np.array(self.phi_grid) @@ -1797,33 +2103,31 @@ class CylindricalMesh(StructuredMesh): return mesh @classmethod - def from_domain( + def from_bounding_box( cls, - domain: HasBoundingBox, + bbox: openmc.BoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), name: str = '', - enclose_domain: bool = False - ): - """Create CylindricalMesh from a domain using its bounding box. + phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi), + enclose_domain: bool = False, + ) -> CylindricalMesh: + """Create CylindricalMesh from a bounding box. Parameters ---------- - domain : HasBoundingBox - The object passed in will be used as a template for this mesh. The - bounding box of the property of the object passed will be used to - set the r_grid, z_grid ranges. + bbox : openmc.BoundingBox + Bounding box used to set the r_grid and z_grid ranges. dimension : Iterable of int The number of equally spaced mesh cells in each direction (r_grid, phi_grid, z_grid) - mesh_id : int + mesh_id : int, optional Unique identifier for the mesh + name : str, optional + Name of the mesh phi_grid_bounds : numpy.ndarray Mesh bounds points along the phi-axis in radians. The default value is (0, 2π), i.e., the full phi range. - name : str - Name of the mesh enclose_domain : bool If True, the mesh will encompass the bounding box of the domain. If False, the mesh will be inscribed within the domain's bounding box. @@ -1834,38 +2138,28 @@ class CylindricalMesh(StructuredMesh): CylindricalMesh instance """ - if not hasattr(domain, 'bounding_box'): - raise TypeError("Domain must have a bounding_box property") - - # loaded once to avoid recalculating bounding box - cached_bb = domain.bounding_box - if enclose_domain: - outer_radius = 0.5 * np.linalg.norm(cached_bb.width[:2]) + outer_radius = 0.5 * np.linalg.norm(bbox.width[:2]) else: - outer_radius = 0.5 * min(cached_bb.width[:2]) + outer_radius = 0.5 * min(bbox.width[:2]) - r_grid = np.linspace( - 0, - outer_radius, - num=dimension[0]+1 - ) + r_grid = np.linspace(0, outer_radius, num=dimension[0]+1) phi_grid = np.linspace( phi_grid_bounds[0], phi_grid_bounds[1], num=dimension[1]+1 ) z_grid = np.linspace( - cached_bb[0][2], - cached_bb[1][2], + bbox[0][2], + bbox[1][2], num=dimension[2]+1 ) - origin = (cached_bb.center[0], cached_bb.center[1], z_grid[0]) + origin = (bbox.center[0], bbox.center[1], z_grid[0]) # make z-grid relative to the origin z_grid -= origin[2] - mesh = cls( + return cls( r_grid=r_grid, z_grid=z_grid, phi_grid=phi_grid, @@ -1874,8 +2168,6 @@ class CylindricalMesh(StructuredMesh): origin=origin ) - return mesh - def to_xml_element(self): """Return XML representation of the mesh @@ -2045,8 +2337,8 @@ class SphericalMesh(StructuredMesh): def __init__( self, r_grid: Sequence[float], - phi_grid: Sequence[float] = (0, 2*pi), - theta_grid: Sequence[float] = (0, pi), + phi_grid: Sequence[float] = (0, 2*math.pi), + theta_grid: Sequence[float] = (0, math.pi), origin: Sequence[float] = (0., 0., 0.), mesh_id: int | None = None, name: str = '', @@ -2068,6 +2360,10 @@ class SphericalMesh(StructuredMesh): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('r', 'theta', 'phi') + @property def origin(self): return self._origin @@ -2099,7 +2395,7 @@ class SphericalMesh(StructuredMesh): cv.check_length('mesh theta_grid', grid, 2) cv.check_increasing('mesh theta_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > pi)): + if np.any((grid < 0.0) | (grid > math.pi)): raise ValueError("theta_grid values must be in [0, π].") self._theta_grid = grid @@ -2113,7 +2409,7 @@ class SphericalMesh(StructuredMesh): cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > 2*pi)): + if np.any((grid < 0.0) | (grid > 2*math.pi)): raise ValueError("phi_grid values must be in [0, 2π].") self._phi_grid = grid @@ -2179,38 +2475,36 @@ class SphericalMesh(StructuredMesh): return mesh @classmethod - def from_domain( + def from_bounding_box( cls, - domain: HasBoundingBox, + bbox: openmc.BoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), - theta_grid_bounds: Sequence[float] = (0.0, pi), name: str = '', - enclose_domain: bool = False - ): - """Create SphericalMesh from a domain using its bounding box. + phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi), + theta_grid_bounds: Sequence[float] = (0.0, math.pi), + enclose_domain: bool = False, + ) -> SphericalMesh: + """Create SphericalMesh from a bounding box. Parameters ---------- - domain : HasBoundingBox - The object passed in will be used as a template for this mesh. The - bounding box of the property of the object passed will be used to - set the r_grid, phi_grid, and theta_grid ranges. + bbox : openmc.BoundingBox + Bounding box used to set the r_grid, phi_grid, and theta_grid ranges. dimension : Iterable of int The number of equally spaced mesh cells in each direction (r_grid, phi_grid, theta_grid). Spacing is in angular space (radians) for phi and theta, and in absolute space for r. - mesh_id : int + mesh_id : int, optional Unique identifier for the mesh + name : str, optional + Name of the mesh phi_grid_bounds : numpy.ndarray Mesh bounds points along the phi-axis in radians. The default value is (0, 2π), i.e., the full phi range. theta_grid_bounds : numpy.ndarray Mesh bounds points along the theta-axis in radians. The default value is (0, π), i.e., the full theta range. - name : str - Name of the mesh enclose_domain : bool If True, the mesh will encompass the bounding box of the domain. If False, the mesh will be inscribed within the domain's bounding box. @@ -2221,16 +2515,10 @@ class SphericalMesh(StructuredMesh): SphericalMesh instance """ - if not hasattr(domain, 'bounding_box'): - raise TypeError("Domain must have a bounding_box property") - - # loaded once to avoid recalculating bounding box - cached_bb = domain.bounding_box - if enclose_domain: - outer_radius = 0.5 * np.linalg.norm(cached_bb.width) + outer_radius = 0.5 * np.linalg.norm(bbox.width) else: - outer_radius = 0.5 * min(cached_bb.width) + outer_radius = 0.5 * min(bbox.width) r_grid = np.linspace(0, outer_radius, num=dimension[0] + 1) theta_grid = np.linspace( @@ -2243,8 +2531,7 @@ class SphericalMesh(StructuredMesh): phi_grid_bounds[1], num=dimension[2]+1 ) - origin = np.array([ - cached_bb.center[0], cached_bb.center[1], cached_bb.center[2]]) + origin = np.array([bbox.center[0], bbox.center[1], bbox.center[2]]) return cls(r_grid=r_grid, phi_grid=phi_grid, theta_grid=theta_grid, origin=origin, mesh_id=mesh_id, name=name) @@ -2356,6 +2643,83 @@ class SphericalMesh(StructuredMesh): arr[..., 2] = z + origin[2] return arr + def get_indices_at_coords( + self, + coords: Sequence[float] + ) -> tuple[int, int, int]: + """Find the mesh cell indices containing the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point as (x, y, z). + + Returns + ------- + tuple[int, int, int] + The r, theta, phi indices. + + Raises + ------ + ValueError + If the coordinates fall outside the mesh grid boundaries. + + """ + dx = coords[0] - self.origin[0] + dy = coords[1] - self.origin[1] + dz = coords[2] - self.origin[2] + + r_value = math.hypot(dx, dy, dz) + + if r_value < self.r_grid[0] or r_value > self.r_grid[-1]: + raise ValueError( + f'The r value {r_value} computed from the specified ' + f'coordinates is outside the r grid range ' + f'[{self.r_grid[0]}, {self.r_grid[-1]}].' + ) + + r_index = int(min( + np.searchsorted(self.r_grid, r_value, side='right') - 1, + len(self.r_grid) - 2 + )) + + if r_value == 0.0: + theta_value = 0.0 + phi_value = 0.0 + else: + theta_value = math.acos(dz / r_value) + phi_value = math.atan2(dy, dx) + if phi_value < 0: + phi_value += 2 * math.pi + + if theta_value < self.theta_grid[0] or theta_value > self.theta_grid[-1]: + raise ValueError( + f'The theta value {theta_value} computed from the specified ' + f'coordinates is outside the theta grid range ' + f'[{self.theta_grid[0]}, {self.theta_grid[-1]}].' + ) + + theta_index = int(min( + np.searchsorted(self.theta_grid, theta_value, side='right') - 1, + len(self.theta_grid) - 2 + )) + + if phi_value < self.phi_grid[0] or phi_value > self.phi_grid[-1]: + raise ValueError( + f'The phi value {phi_value} computed from the specified ' + f'coordinates is outside the phi grid range ' + f'[{self.phi_grid[0]}, {self.phi_grid[-1]}].' + ) + + phi_index = int(min( + np.searchsorted(self.phi_grid, phi_value, side='right') - 1, + len(self.phi_grid) - 2 + )) + + return (r_index, theta_index, phi_index) + def require_statepoint_data(func): @wraps(func) @@ -2444,7 +2808,8 @@ class UnstructuredMesh(MeshBase): _UNSUPPORTED_ELEM = -1 _LINEAR_TET = 0 _LINEAR_HEX = 1 - _VTK_TETRA = 10 + _VTK_TET = 10 + _VTK_HEX = 12 def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None, name: str = '', length_multiplier: float = 1.0, @@ -2583,6 +2948,10 @@ class UnstructuredMesh(MeshBase): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('element_index',) + @property @require_statepoint_data def indices(self): @@ -2751,7 +3120,9 @@ class UnstructuredMesh(MeshBase): n_skipped += 1 continue else: - raise RuntimeError(f"Invalid element type {elem_type} found") + raise RuntimeError( + f"Invalid element type {elem_type} found in mesh {self.id}" + ) for i, c in enumerate(conn): if c == -1: @@ -2808,35 +3179,42 @@ class UnstructuredMesh(MeshBase): datasets: dict | None = None, volume_normalization: bool = True, ): - def append_dataset(dset, array): - """Convenience function to append data to an HDF5 dataset""" - origLen = dset.shape[0] - dset.resize(origLen + array.shape[0], axis=0) - dset[origLen:] = array + # This writer supports linear tetrahedra and linear hexahedra elements + conn_list = [] # flattened connectivity ids + cell_sizes = [] # number of points per cell + vtk_types = [] # VTK cell types per cell (uint8) + n_skipped = 0 - if self.library != "moab": - raise NotImplementedError("VTKHDF output is only supported for MOAB meshes") - - # the self.connectivity contains arrays of length 8 to support hex - # elements as well, in the case of tetrahedra mesh elements, the - # last 4 values are -1 and are removed - trimmed_connectivity = [] - for cell in self.connectivity: - # Find the index of the first -1 value, if any - first_negative_index = np.where(cell == -1)[0] - if first_negative_index.size > 0: - # Slice the array up to the first -1 value - trimmed_connectivity.append(cell[: first_negative_index[0]]) + for conn, etype in zip(self.connectivity, self.element_types): + if etype == self._LINEAR_TET: + ids = conn[:4] + vtk_types.append(self._VTK_TET) + elif etype == self._LINEAR_HEX: + ids = conn[:8] + vtk_types.append(self._VTK_HEX) + elif etype == self._UNSUPPORTED_ELEM: + n_skipped += 1 + continue else: - # No -1 values, append the whole cell - trimmed_connectivity.append(cell) - trimmed_connectivity = np.array(trimmed_connectivity, dtype="int32").flatten() + raise RuntimeError( + f"Invalid element type {etype} found in mesh {self.id}" + ) + conn_list.extend(ids.tolist()) + cell_sizes.append(len(ids)) - # MOAB meshes supports tet elements only so we know it has 4 points per cell - points_per_cell = 4 + if n_skipped > 0: + warnings.warn( + f"{n_skipped} elements were not written because " + "they are not of type linear tet/hex" + ) - # offsets are the indices of the first point of each cell in the array of points - offsets = np.arange(0, self.n_elements * points_per_cell + 1, points_per_cell) + connectivity = np.asarray(conn_list, dtype=np.int64) + + # Offsets must be length (numCells + 1) with a leading 0 and + # cumulative end-indices thereafter, per VTK's layout + cell_sizes_arr = np.asarray(cell_sizes, dtype=np.int64) + offsets = np.zeros(cell_sizes_arr.size + 1, dtype=np.int64) + np.cumsum(cell_sizes_arr, out=offsets[1:]) for name, data in datasets.items(): if data.shape != self.dimension: @@ -2858,42 +3236,27 @@ class UnstructuredMesh(MeshBase): dtype=h5py.string_dtype("ascii", len(ascii_type)), ) - # create hdf5 file structure - root.create_dataset("NumberOfPoints", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Types", (0,), maxshape=(None,), dtype="uint8") - root.create_dataset("Points", (0, 3), maxshape=(None, 3), dtype="f") - root.create_dataset( - "NumberOfConnectivityIds", (0,), maxshape=(None,), dtype="i8" - ) - root.create_dataset("NumberOfCells", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Offsets", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Connectivity", (0,), maxshape=(None,), dtype="i8") + # Create HDF5 file structure compliant with VTKHDF UnstructuredGrid + n_points = int(len(self.vertices)) + n_cells = int(len(cell_sizes)) + n_conn_ids = int(len(connectivity)) - append_dataset(root["NumberOfPoints"], np.array([len(self.vertices)])) - append_dataset(root["Points"], self.vertices) - append_dataset( - root["NumberOfConnectivityIds"], - np.array([len(trimmed_connectivity)]), - ) - append_dataset(root["Connectivity"], trimmed_connectivity) - append_dataset(root["NumberOfCells"], np.array([self.n_elements])) - append_dataset(root["Offsets"], offsets) - - append_dataset( - root["Types"], np.full(self.n_elements, self._VTK_TETRA, dtype="uint8") - ) + root.create_dataset("NumberOfPoints", data=(n_points,), dtype="i8") + root.create_dataset("NumberOfCells", data=(n_cells,), dtype="i8") + root.create_dataset("NumberOfConnectivityIds", data=(n_conn_ids,), dtype="i8") + root.create_dataset("Points", data=self.vertices.astype(np.float64, copy=False), dtype="f8") + root.create_dataset("Types", data=np.asarray(vtk_types, dtype=np.uint8), dtype="uint8") + root.create_dataset("Offsets", data=offsets.astype("i8"), dtype="i8") + root.create_dataset("Connectivity", data=connectivity.astype("i8"), dtype="i8") cell_data_group = root.create_group("CellData") for name, data in datasets.items(): - - cell_data_group.create_dataset( - name, (0,), maxshape=(None,), dtype="float64", chunks=True - ) - if volume_normalization: data /= self.volumes - append_dataset(cell_data_group[name], data) + cell_data_group.create_dataset( + name, data=data, dtype="float64", chunks=True + ) @classmethod def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 682b5d550..5adbac9c4 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -1,6 +1,6 @@ import numpy as np -from openmc.mgxs.groups import EnergyGroups +from openmc.mgxs.groups import EnergyGroups, convert_flux_groups from openmc.mgxs.library import Library from openmc.mgxs.mgxs import * from openmc.mgxs.mdgxs import * diff --git a/openmc/mgxs/groups.py b/openmc/mgxs/groups.py index 182402ed7..aea7a6d29 100644 --- a/openmc/mgxs/groups.py +++ b/openmc/mgxs/groups.py @@ -78,6 +78,7 @@ class EnergyGroups: @group_edges.setter def group_edges(self, edges): cv.check_type('group edges', edges, Iterable, Real) + cv.check_increasing('group edges', edges) cv.check_greater_than('number of group edges', len(edges), 1) self._group_edges = np.array(edges) @@ -300,3 +301,139 @@ class EnergyGroups: # Assign merged edges to merged groups merged_groups.group_edges = list(merged_edges) return merged_groups + + +def convert_flux_groups(flux, source_groups, target_groups): + """Convert flux spectrum between energy group structures. + + Uses flux-per-unit-lethargy conservation, which assumes constant flux per + unit lethargy within each source group and distributes flux to target + groups proportionally to their lethargy width. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + flux : Iterable of float + Flux values for source groups. Length must equal + source_groups.num_groups. + source_groups : EnergyGroups or str + Energy group structure of the input flux with boundaries in [eV]. + Can be an EnergyGroups instance or the name of a group structure + (e.g., 'CCFE-709'). + target_groups : EnergyGroups or str + Target energy group structure with boundaries in [eV]. Can be an + EnergyGroups instance or the name of a group structure + (e.g., 'UKAEA-1102'). + + Returns + ------- + numpy.ndarray + Flux values for target groups. Total flux is conserved for + overlapping energy regions. + + Raises + ------ + TypeError + If source_groups or target_groups is not EnergyGroups or str + ValueError + If flux length doesn't match source_groups, or flux contains + negative, NaN, or infinite values + + See Also + -------- + EnergyGroups : Energy group structure class + + Notes + ----- + The assumption of constant flux per unit lethargy within each source + group is physically reasonable for most reactor spectra but is not + exact. For best accuracy, use source spectra with sufficiently fine + energy resolution. + + Examples + -------- + Convert FNS 709-group flux to UKAEA-1102 structure: + + >>> import numpy as np + >>> flux_709 = np.load('tests/fns_flux_709.npy') + >>> flux_1102 = openmc.mgxs.convert_flux_groups(flux_709, 'CCFE-709', 'UKAEA-1102') + + Convert using EnergyGroups instances: + + >>> source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + >>> target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) + >>> flux_target = openmc.mgxs.convert_flux_groups([1e8, 2e8], source, target) + + References + ---------- + .. [1] J. J. Duderstadt and L. J. Hamilton, "Nuclear Reactor Analysis," + John Wiley & Sons, 1976. + .. [2] M. Fleming and J.-Ch. Sublet, "FISPACT-II User Manual," + UKAEA-R(18)001, UK Atomic Energy Authority, 2018. See GRPCONVERT keyword. + + """ + # Handle string group structure names + if isinstance(source_groups, str): + source_groups = EnergyGroups(source_groups) + if isinstance(target_groups, str): + target_groups = EnergyGroups(target_groups) + + # Type validation + cv.check_type('source_groups', source_groups, EnergyGroups) + cv.check_type('target_groups', target_groups, EnergyGroups) + + # Convert flux to numpy array + flux = np.asarray(flux, dtype=np.float64) + if flux.ndim != 1: + raise ValueError(f'flux must be 1-dimensional, got shape {flux.shape}') + + # Validate flux length matches source groups + if len(flux) != source_groups.num_groups: + raise ValueError( + f'Length of flux ({len(flux)}) must equal number of source ' + f'groups ({source_groups.num_groups})' + ) + + # Check for invalid flux values + if np.any(np.isnan(flux)): + raise ValueError('flux contains NaN values') + if np.any(np.isinf(flux)): + raise ValueError('flux contains infinite values') + if np.any(flux < 0): + raise ValueError('flux values must be non-negative') + + # Get energy edges + source_edges = source_groups.group_edges + target_edges = target_groups.group_edges + num_target = target_groups.num_groups + + # Initialize output array + flux_target = np.zeros(num_target) + + # Main conversion loop: distribute flux using lethargy weighting + for idx_src, flux_src in enumerate(flux): + if flux_src == 0: + continue + + e_low_src = source_edges[idx_src] + e_high_src = source_edges[idx_src + 1] + lethargy_src = np.log(e_high_src / e_low_src) + + for idx_tgt in range(num_target): + e_low_tgt = target_edges[idx_tgt] + e_high_tgt = target_edges[idx_tgt + 1] + + # Skip non-overlapping groups + if e_high_tgt <= e_low_src or e_low_tgt >= e_high_src: + continue + + # Calculate overlap region + e_low_overlap = max(e_low_src, e_low_tgt) + e_high_overlap = min(e_high_src, e_high_tgt) + lethargy_overlap = np.log(e_high_overlap / e_low_overlap) + + # Distribute flux proportionally to lethargy fraction + flux_target[idx_tgt] += flux_src * (lethargy_overlap / lethargy_src) + + return flux_target diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index b476de902..faa83c048 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -13,6 +13,7 @@ import openmc.checkvalue as cv from openmc.checkvalue import PathLike from ..tallies import ESTIMATOR_TYPES +ROOM_TEMPERATURE_KELVIN = 294.0 class Library: """A multi-energy-group and multi-delayed-group cross section library for @@ -954,7 +955,7 @@ class Library: return pickle.load(f) def get_xsdata(self, domain, xsdata_name, nuclide='total', xs_type='macro', - subdomain=None, apply_domain_chi=False): + subdomain=None, apply_domain_chi=False, temperature=ROOM_TEMPERATURE_KELVIN): """Generates an openmc.XSdata object describing a multi-group cross section dataset for writing to an openmc.MGXSLibrary object. @@ -990,6 +991,9 @@ class Library: downstream multigroup solvers that precompute a material-specific chi before the transport solve provides group-wise fluxes. Defaults to False. + temperature : float, optional + The temperature to set in the XSdata object. Defaults to 294 K + (room temperature). Returns ------- @@ -1036,6 +1040,7 @@ class Library: else: representation = 'isotropic' xsdata = openmc.XSdata(name, self.energy_groups, + temperatures=[temperature], representation=representation) xsdata.num_delayed_groups = self.num_delayed_groups if self.num_polar > 1 or self.num_azimuthal > 1: @@ -1053,45 +1058,61 @@ class Library: # Now get xs data itself if 'nu-transport' in self.mgxs_types and self.correction == 'P0': mymgxs = self.get_mgxs(domain, 'nu-transport') - xsdata.set_total_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], + xsdata.set_total_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) elif 'transport' in self.mgxs_types and self.correction == 'P0': mymgxs = self.get_mgxs(domain, 'transport') - xsdata.set_total_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], + xsdata.set_total_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) elif 'total' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'total') - xsdata.set_total_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], + xsdata.set_total_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) if 'absorption' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'absorption') - xsdata.set_absorption_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_absorption_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'fission') - xsdata.set_fission_mgxs(mymgxs, xs_type=xs_type, - nuclide=[nuclide], subdomain=subdomain) + xsdata.set_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], + subdomain=subdomain) if 'kappa-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'kappa-fission') - xsdata.set_kappa_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_kappa_fission_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'inverse-velocity' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'inverse-velocity') - xsdata.set_inverse_velocity_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_inverse_velocity_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'nu-fission matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'nu-fission matrix') - xsdata.set_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_nu_fission_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1101,7 +1122,9 @@ class Library: nuc = "sum" else: nuc = nuclide - xsdata.set_chi_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuc], + xsdata.set_chi_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuc], subdomain=subdomain) if 'chi-prompt' in self.mgxs_types: @@ -1110,8 +1133,10 @@ class Library: nuc = "sum" else: nuc = nuclide - xsdata.set_chi_prompt_mgxs(mymgxs, xs_type=xs_type, - nuclide=[nuc], subdomain=subdomain) + xsdata.set_chi_prompt_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuc], + subdomain=subdomain) if 'chi-delayed' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'chi-delayed') @@ -1119,53 +1144,61 @@ class Library: nuc = "sum" else: nuc = nuclide - xsdata.set_chi_delayed_mgxs(mymgxs, xs_type=xs_type, - nuclide=[nuc], subdomain=subdomain) + xsdata.set_chi_delayed_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuc], + subdomain=subdomain) if 'nu-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'nu-fission') - xsdata.set_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'prompt-nu-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'prompt-nu-fission') - xsdata.set_prompt_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_prompt_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'prompt-nu-fission matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'prompt-nu-fission matrix') - xsdata.set_prompt_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_prompt_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'delayed-nu-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'delayed-nu-fission') - xsdata.set_delayed_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_delayed_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'delayed-nu-fission matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'delayed-nu-fission matrix') - xsdata.set_delayed_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_delayed_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'beta' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'beta') - xsdata.set_beta_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], - subdomain=subdomain) + xsdata.set_beta_mgxs(mymgxs, temperature=temperature, xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) if 'decay-rate' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'decay-rate') - xsdata.set_decay_rate_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], - subdomain=subdomain) + xsdata.set_decay_rate_mgxs(mymgxs, temperature=temperature, xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) # If multiplicity matrix is available, prefer that if 'multiplicity matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'multiplicity matrix') - xsdata.set_multiplicity_matrix_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_multiplicity_matrix_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) using_multiplicity = True @@ -1176,6 +1209,7 @@ class Library: scatt_mgxs = self.get_mgxs(domain, 'scatter matrix') nuscatt_mgxs = self.get_mgxs(domain, 'nu-scatter matrix') xsdata.set_multiplicity_matrix_mgxs(nuscatt_mgxs, scatt_mgxs, + temperature=temperature, xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1188,6 +1222,7 @@ class Library: nuscatt_mgxs = \ self.get_mgxs(domain, 'consistent nu-scatter matrix') xsdata.set_multiplicity_matrix_mgxs(nuscatt_mgxs, scatt_mgxs, + temperature=temperature, xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1202,7 +1237,8 @@ class Library: else: nuscatt_mgxs = \ self.get_mgxs(domain, 'consistent nu-scatter matrix') - xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, xs_type=xs_type, + xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) else: @@ -1213,7 +1249,8 @@ class Library: else: nuscatt_mgxs = \ self.get_mgxs(domain, 'consistent nu-scatter matrix') - xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, xs_type=xs_type, + xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1253,14 +1290,23 @@ class Library: 'are ignored since multiplicity or nu-scatter matrices '\ 'were not tallied for ' + xsdata_name warn(msg, RuntimeWarning) - xsdata.set_scatter_matrix_mgxs(scatt_mgxs, xs_type=xs_type, + + if 'scatter matrix' in self.mgxs_types: + scatt_mgxs = self.get_mgxs(domain, 'scatter matrix') + elif 'consistent scatter matrix' in self.mgxs_types: + scatt_mgxs = self.get_mgxs(domain, 'consistent scatter matrix') + else: + raise ValueError(f'No scatter matrix found for {xsdata_name}.') + + xsdata.set_scatter_matrix_mgxs(scatt_mgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) return xsdata def create_mg_library(self, xs_type='macro', xsdata_names=None, - apply_domain_chi=False): + apply_domain_chi=False, temperature=ROOM_TEMPERATURE_KELVIN): """Creates an openmc.MGXSLibrary object to contain the MGXS data for the Multi-Group mode of OpenMC. @@ -1286,6 +1332,9 @@ class Library: downstream multigroup solvers that precompute a material-specific chi before the transport solve provides group-wise fluxes. Defaults to False. + temperature : float, optional + The temperature to set in the MGXSLibrary object. Defaults to 294 K + (room temperature). Returns ------- diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index c12c1a9ab..57cc955ba 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -877,8 +877,8 @@ class MDGXS(MGXS): # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': mesh_str = f'mesh {self.domain.id}' - df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), - (mesh_str, 'z')] + columns, inplace=True) + mesh_cols = [(mesh_str, label) for label in self.domain._axis_labels] + df.sort_values(by=mesh_cols + columns, inplace=True) else: df.sort_values(by=[self.domain_type] + columns, inplace=True) diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index f621db092..4c13c7508 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -1923,7 +1923,7 @@ class MGXS: # Create an HDF5 group for the subdomain if self.domain_type == 'distribcell': - group_name = ''.zfill(num_digits) + group_name = str(subdomain).zfill(num_digits) subdomain_group = domain_group.require_group(group_name) else: subdomain_group = domain_group @@ -2134,8 +2134,8 @@ class MGXS: # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': mesh_str = f'mesh {self.domain.id}' - df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), - (mesh_str, 'z')] + columns, inplace=True) + mesh_cols = [(mesh_str, label) for label in self.domain._axis_labels] + df.sort_values(by=mesh_cols + columns, inplace=True) else: df.sort_values(by=[self.domain_type] + columns, inplace=True) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 4bc2d4a5a..ab9b58b7a 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -484,7 +484,8 @@ class XSdata: check_type('temperature', temperature, Real) - temp_store = self.temperatures.tolist().append(temperature) + temp_store = self.temperatures.tolist() + temp_store.append(temperature) self.temperatures = temp_store self._total.append(None) @@ -514,6 +515,75 @@ class XSdata: if np.sum(array) > 0: self._fissionable = True + def add_temperature_data(self, other): + """This method adds temperature-dependent cross section + values from another XSdata object to this XSdata object. + Note: if a temperature datapoint from 'other' already exists in this + object, it will be overridden. + + Parameters + ---------- + other: openmc.XSdata + The other XSdata object to fetch data from + """ + + # Sanity check to make sure they have the same name, energy group structure, + # and delayed group structure + check_value('name', other.name, self.name) + check_value('energy_groups', other.energy_groups, [self.energy_groups]) + check_value('delayed_groups', other.num_delayed_groups, [self.num_delayed_groups]) + + # Add the temperature data. + for temp in other.temperatures: + if temp not in self.temperatures: + self.add_temperature(temp) + + if np.all(other.absorption[other._temperature_index(temp)] != None): + self.set_absorption(other.absorption[other._temperature_index(temp)], temp) + + if np.all(other.beta[other._temperature_index(temp)] != None): + self.set_beta(other.beta[other._temperature_index(temp)], temp) + + if np.all(other.chi[other._temperature_index(temp)] != None): + self.set_chi(other.chi[other._temperature_index(temp)], temp) + + if np.all(other.chi_delayed[other._temperature_index(temp)] != None): + self.set_chi_delayed(other.chi_delayed[other._temperature_index(temp)], temp) + + if np.all(other.chi_prompt[other._temperature_index(temp)] != None): + self.set_chi_prompt(other.chi_prompt[other._temperature_index(temp)], temp) + + if np.all(other.decay_rate[other._temperature_index(temp)] != None): + self.set_decay_rate(other.decay_rate[other._temperature_index(temp)], temp) + + if np.all(other.delayed_nu_fission[other._temperature_index(temp)] != None): + self.set_delayed_nu_fission(other.delayed_nu_fission[other._temperature_index(temp)], temp) + + if np.all(other.fission[other._temperature_index(temp)] != None): + self.set_fission(other.fission[other._temperature_index(temp)], temp) + + if np.all(other.inverse_velocity[other._temperature_index(temp)] != None): + self.set_inverse_velocity(other.inverse_velocity[other._temperature_index(temp)], temp) + + if np.all(other.kappa_fission[other._temperature_index(temp)] != None): + self.set_kappa_fission(other.kappa_fission[other._temperature_index(temp)], temp) + + if np.all(other.multiplicity_matrix[other._temperature_index(temp)] != None): + self.set_multiplicity_matrix(other.multiplicity_matrix[other._temperature_index(temp)], temp) + + if np.all(other.nu_fission[other._temperature_index(temp)] != None): + self.set_nu_fission(other.nu_fission[other._temperature_index(temp)], temp) + + if np.all(other.prompt_nu_fission[other._temperature_index(temp)] != None): + self.set_prompt_nu_fission(other.prompt_nu_fission[other._temperature_index(temp)], temp) + + if np.all(other.scatter_matrix[other._temperature_index(temp)] != None): + self.set_scatter_matrix(other.scatter_matrix[other._temperature_index(temp)], temp) + + if np.all(other.fission[other._temperature_index(temp)] != None): + self.set_total(other.total[other._temperature_index(temp)], temp) + + def set_total(self, total, temperature=ROOM_TEMPERATURE_KELVIN): """This method sets the cross section for this XSdata object at the provided temperature. @@ -2554,20 +2624,20 @@ class MGXSLibrary: "must be set") check_type('filename', filename, (str, PathLike)) - file = h5py.File(filename, 'r') + with h5py.File(filename, 'r') as file: - # Check filetype and version - check_filetype_version(file, _FILETYPE_MGXS_LIBRARY, - _VERSION_MGXS_LIBRARY) + # Check filetype and version + check_filetype_version(file, _FILETYPE_MGXS_LIBRARY, + _VERSION_MGXS_LIBRARY) - group_structure = file.attrs['group structure'] - num_delayed_groups = file.attrs['delayed_groups'] - energy_groups = openmc.mgxs.EnergyGroups(group_structure) - data = cls(energy_groups, num_delayed_groups) + group_structure = file.attrs['group structure'] + num_delayed_groups = file.attrs['delayed_groups'] + energy_groups = openmc.mgxs.EnergyGroups(group_structure) + data = cls(energy_groups, num_delayed_groups) - for group_name, group in file.items(): - data.add_xsdata(openmc.XSdata.from_hdf5(group, group_name, - energy_groups, - num_delayed_groups)) + for group_name, group in file.items(): + data.add_xsdata(openmc.XSdata.from_hdf5(group, group_name, + energy_groups, + num_delayed_groups)) return data diff --git a/openmc/mixin.py b/openmc/mixin.py index 0bc4128b0..28c97bfdd 100644 --- a/openmc/mixin.py +++ b/openmc/mixin.py @@ -39,6 +39,8 @@ class IDManagerMixin: """ + min_id = 0 + @property def id(self): return self._id @@ -64,7 +66,7 @@ class IDManagerMixin: else: name = cls.__name__ cv.check_type(f'{name} ID', uid, Integral) - cv.check_greater_than(f'{name} ID', uid, 0, equality=True) + cv.check_greater_than(f'{name} ID', uid, cls.min_id, equality=True) if uid in cls.used_ids: msg = f'Another {name} instance already exists with id={uid}.' warn(msg, IDWarning) diff --git a/openmc/model/funcs.py b/openmc/model/funcs.py index 41aa920ea..e076b080a 100644 --- a/openmc/model/funcs.py +++ b/openmc/model/funcs.py @@ -39,8 +39,8 @@ def borated_water(boron_ppm, temperature=293., pressure=0.1013, temp_unit='K', press_unit : {'MPa', 'psi'} The units used for the `pressure` argument. density : float - Water density in [g / cm^3]. If specified, this value overrides the - temperature and pressure arguments. + Water density in [g / cm^3]. If specified, this value overrides + the value that is computed from the temperature and pressure arguments. **kwargs All keyword arguments are passed to the created Material object. @@ -95,10 +95,7 @@ def borated_water(boron_ppm, temperature=293., pressure=0.1013, temp_unit='K', frac_B = boron_ppm * 1e-6 / M_B # Build the material. - if density is None: - out = openmc.Material(temperature=T, **kwargs) - else: - out = openmc.Material(**kwargs) + out = openmc.Material(temperature=T, **kwargs) out.add_element('H', frac_H, 'ao') out.add_element('O', frac_O, 'ao') out.add_element('B', frac_B, 'ao') diff --git a/openmc/model/model.py b/openmc/model/model.py index 48ae9f0b9..c437d2033 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -21,9 +21,10 @@ import openmc import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value, PathLike +from openmc.checkvalue import (check_type, check_value, check_greater_than, + check_length, PathLike) from openmc.exceptions import InvalidIDError -from openmc.plots import add_plot_params, _BASIS_INDICES +from openmc.plots import add_plot_params, _BASIS_INDICES, id_map_to_rgb from openmc.utility_funcs import change_directory @@ -33,6 +34,15 @@ class ModelModifier(Protocol): ... +def _check_pixels(pixels: int | Sequence[int]) -> None: + if isinstance(pixels, Integral): + check_greater_than('pixels', pixels, 0) + else: + check_length('pixels', pixels, 2) + for p in pixels: + check_greater_than('pixels', p, 0) + + class Model: """Model container. @@ -266,6 +276,46 @@ class Model: denom_tally.scores = ['ifp-denominator'] self.tallies.append(denom_tally) + # TODO: This should also be incorporated into lower-level calls in + # settings.py, but it requires information about the tallies currently + # on the active Model + def _assign_fw_cadis_tally_IDs(self): + # Verify that all tallies assigned as targets on WeightWindowGenerators + # exist within model.tallies. If this is the case, convert the .targets + # attribute of each WeightWindowGenerator to a sequence of tally IDs. + if len(self.settings.weight_window_generators) == 0: + return + + # List of valid tally IDs + reference_tally_ids = np.asarray([tal.id for tal in self.tallies]) + + for wwg in self.settings.weight_window_generators: + # Only proceeds if the "targets" attribute is an openmc.Tallies, + # which means it hasn't been checked against model.tallies. + if isinstance(wwg.targets, openmc.Tallies): + id_vec = [] + for tal in wwg.targets: + # check against model tallies for equivalence + id_next = None + for reference_tal in self.tallies: + if tal == reference_tal: + id_next = reference_tal.id + break + + if id_next is None: + raise RuntimeError( + f'Local FW-CADIS target tally {tal.id} not found on model.tallies!') + else: + id_vec.append(id_next) + + wwg.targets = id_vec + + elif isinstance(wwg.targets, np.ndarray): + invalid = wwg.targets[~np.isin(wwg.targets, reference_tally_ids)] + if len(invalid) > 0: + raise RuntimeError( + f'Local FW-CADIS target tally IDs {invalid} not found on model.tallies!') + @classmethod def from_xml( cls, @@ -427,6 +477,8 @@ class Model: This method iterates over all DAGMC universes in the geometry and synchronizes their cells with the current material assignments. Requires that the model has been initialized via :meth:`Model.init_lib`. + Synchronized DAGMC cells can then be edited and exported as nested + `` overrides inside each `` element. .. versionadded:: 0.15.1 @@ -546,6 +598,13 @@ class Model: depletion_operator.cleanup_when_done = True depletion_operator.finalize() + def _link_geometry_to_filters(self): + """Establishes a link between distribcell filters and the geometry""" + for tally in self.tallies: + for f in tally.filters: + if isinstance(f, openmc.DistribcellFilter): + f._geometry = self.geometry + def export_to_xml(self, directory: PathLike = '.', remove_surfs: bool = False, nuclides_to_ignore: Iterable[str] | None = None): """Export model to separate XML files. @@ -569,6 +628,7 @@ class Model: if not d.is_dir(): d.mkdir(parents=True, exist_ok=True) + self._assign_fw_cadis_tally_IDs() self.settings.export_to_xml(d) self.geometry.export_to_xml(d, remove_surfs=remove_surfs) @@ -587,6 +647,8 @@ class Model: if self.plots: self.plots.export_to_xml(d) + self._link_geometry_to_filters() + def export_to_model_xml(self, path: PathLike = 'model.xml', remove_surfs: bool = False, nuclides_to_ignore: Iterable[str] | None = None): """Export model to a single XML file. @@ -625,6 +687,9 @@ class Model: "set the Geometry.merge_surfaces attribute instead.") self.geometry.merge_surfaces = True + # Link FW-CADIS WeightWindowGenerator target tallies, if present + self._assign_fw_cadis_tally_IDs() + # provide a memo to track which meshes have been written mesh_memo = set() settings_element = self.settings.to_xml_element(mesh_memo) @@ -666,6 +731,8 @@ class Model: fh.write(ET.tostring(plots_element, encoding="unicode")) fh.write("\n") + self._link_geometry_to_filters() + def import_properties(self, filename: PathLike): """Import physical properties @@ -993,6 +1060,8 @@ class Model: pixels: int | Sequence[int], basis: str ): + _check_pixels(pixels) + x, y, _ = _BASIS_INDICES[basis] bb = self.bounding_box @@ -1026,6 +1095,7 @@ class Model: width: Sequence[float] | None = None, pixels: int | Sequence[int] = 40000, basis: str = 'xy', + color_overlaps: bool = False, **init_kwargs ) -> np.ndarray: """Generate an ID map for domains based on the plot parameters @@ -1054,6 +1124,10 @@ class Model: total and the image aspect ratio based on the width argument. basis : {'xy', 'yz', 'xz'}, optional Basis of the plot. + color_overlaps : bool, optional + Whether to assign unique IDs (-3) to overlapping regions. If False, + overlapping regions will be assigned the ID of the lowest-numbered + cell that occupies that region. Defaults to False. **init_kwargs Keyword arguments passed to :meth:`Model.init_lib`. @@ -1065,27 +1139,150 @@ class Model: array contains cell IDs, cell instances, and material IDs (in that order). """ + ids, _ = self.slice_data( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + show_overlaps=color_overlaps, + level=-1, + include_properties=False, + **init_kwargs, + ) + return ids + + def slice_data( + self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + u_span: Sequence[float] | None = None, + v_span: Sequence[float] | None = None, + show_overlaps: bool = False, + level: int = -1, + filter: openmc.Filter | None = None, + include_properties: bool = True, + **init_kwargs + ) -> tuple[np.ndarray, np.ndarray | None]: + """Generate geometry and property data for a 2D plot slice. + + This method combines the functionality of :meth:`id_map` and property + mapping into a single call, avoiding duplicate geometry lookups. It also + supports filter bin index lookup for tally visualization. + + .. versionadded:: 0.16.0 + + Parameters + ---------- + origin : Sequence[float], optional + Origin of the plot. If unspecified, this argument defaults to the + center of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (0.0, 0.0, 0.0). + width : Sequence[float], optional + Width of the plot. If unspecified, this argument defaults to the + width of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (10.0, 10.0). + pixels : int | Sequence[int], optional + If an iterable of ints is provided then this directly sets the + number of pixels to use in each basis direction. If a single int is + provided then this sets the total number of pixels in the plot and + the number of pixels in each basis direction is calculated from this + total and the image aspect ratio based on the width argument. + basis : {'xy', 'yz', 'xz'}, optional + Basis of the plot. + u_span : Sequence[float], optional + Full-width span vector for an oriented slice (3 values). Mutually + exclusive with width. + v_span : Sequence[float], optional + Full-height span vector for an oriented slice (3 values). Mutually + exclusive with width. + show_overlaps : bool, optional + Whether to identify and assign unique IDs (-3) to overlapping + regions. If False, overlapping regions will be assigned the ID of + the lowest-numbered cell that occupies that region. Defaults to + False. + level : int, optional + Universe level to plot (-1 for deepest). Defaults to -1. + filter : openmc.Filter, optional + If provided, the information for each pixel also includes an index + in the filter corresponding to the pixel position. + include_properties : bool, optional + Whether to include temperature/density data. Defaults to True. + **init_kwargs + Keyword arguments passed to :meth:`Model.init_lib`. + + Returns + ------- + geom_data : numpy.ndarray + Shape (v_res, h_res, 3) or (v_res, h_res, 4) int32 array. Contains + [cell_id, cell_instance, material_id] when no filter, or [cell_id, + cell_instance, material_id, filter_bin] with filter. + property_data : numpy.ndarray or None + Shape (v_res, h_res, 2) float64 array with [temperature, density], + or None if include_properties=False. + """ import openmc.lib - origin, width, pixels = self._set_plot_defaults( - origin, width, pixels, basis) + _check_pixels(pixels) - # initialize the openmc.lib.plot._PlotBase object - plot_obj = openmc.lib.plot._PlotBase() - plot_obj.origin = origin - plot_obj.width = width[0] - plot_obj.height = width[1] - plot_obj.h_res = pixels[0] - plot_obj.v_res = pixels[1] - plot_obj.basis = basis + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") + + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + if origin is None: + origin = (0.0, 0.0, 0.0) + if isinstance(pixels, int): + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + aspect_ratio = u_norm / v_norm + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + else: + origin, width, pixels = self._set_plot_defaults( + origin, width, pixels, basis) # Silence output by default. Also set arguments to start in volume # calculation mode to avoid loading cross sections init_kwargs.setdefault('output', False) init_kwargs.setdefault('args', ['-c']) + # If filter does not already appear in the model, temporarily add a + # tally with the filter + original_length = len(self.tallies) + if filter is not None: + filter_ids = {f.id for t in self.tallies for f in t.filters} + if filter.id not in filter_ids: + # Create temporary tally while preserving ID assignment + next_id = openmc.Tally.next_id + temp_tally = openmc.Tally() + temp_tally.filters = [filter] + temp_tally.scores = ['flux'] + self.tallies.append(temp_tally) + openmc.Tally.used_ids.remove(temp_tally.id) + openmc.Tally.next_id = next_id + with openmc.lib.TemporarySession(self, **init_kwargs): - return openmc.lib.id_map(plot_obj) + geom_data, property_data = openmc.lib.slice_data( + origin=origin, + width=width, + basis=basis, + u_span=u_span, + v_span=v_span, + pixels=pixels, + show_overlaps=show_overlaps, + level=level, + filter=filter, + include_properties=include_properties, + ) + + # If filter was temporarily added, remove it + if len(self.tallies) > original_length: + self.tallies.pop() + + return geom_data, property_data @add_plot_params def plot( @@ -1097,13 +1294,12 @@ class Model: color_by: str = 'cell', colors: dict | None = None, seed: int | None = None, - openmc_exec: PathLike = 'openmc', axes=None, legend: bool = False, axis_units: str = 'cm', outline: bool | str = False, show_overlaps: bool = False, - overlap_color: Sequence[int] | str | None = None, + overlap_color: Sequence[int] | str = (255, 0, 0), n_samples: int | None = None, plane_tolerance: float = 1., legend_kwargs: dict | None = None, @@ -1115,12 +1311,15 @@ class Model: .. versionadded:: 0.15.1 """ - import matplotlib.image as mpimg import matplotlib.patches as mpatches import matplotlib.pyplot as plt check_type('n_samples', n_samples, int | None) + if n_samples is not None: + check_greater_than('n_samples', n_samples, 0, equality=True) check_type('plane_tolerance', plane_tolerance, Real) + check_greater_than('plane_tolerance', plane_tolerance, 0.0) + if legend_kwargs is None: legend_kwargs = {} legend_kwargs.setdefault('bbox_to_anchor', (1.05, 1)) @@ -1146,124 +1345,118 @@ class Model: y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] # Determine whether any materials contains macroscopic data and if so, - # set energy mode accordingly - _energy_mode = self.settings._energy_mode + # set energy mode accordingly and check that mg cross sections path is accessible for mat in self.geometry.get_all_materials().values(): if mat._macroscopic is not None: self.settings.energy_mode = 'multi-group' + if 'mg_cross_sections' not in openmc.config: + raise RuntimeError("'mg_cross_sections' path must be set in " + "openmc.config before plotting.") break - with TemporaryDirectory() as tmpdir: - _plot_seed = self.settings.plot_seed - if seed is not None: - self.settings.plot_seed = seed + # Get plot IDs from the C API + id_map, _ = self.slice_data( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + show_overlaps=show_overlaps, + include_properties=False, + ) - # Create plot object matching passed arguments - plot = openmc.Plot() - plot.origin = origin - plot.width = width - plot.pixels = pixels - plot.basis = basis + # Generate colors if not provided + if colors is None and seed is not None: + # Use the colorize method to generate random colors + plot = openmc.SlicePlot() plot.color_by = color_by - plot.show_overlaps = show_overlaps - if overlap_color is not None: - plot.overlap_color = overlap_color - if colors is not None: - plot.colors = colors - self.plots.append(plot) + plot.colorize(self.geometry, seed=seed) + colors = plot.colors - # Run OpenMC in geometry plotting mode - self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) + # Convert ID map to RGB image + img = id_map_to_rgb( + id_map=id_map, + color_by=color_by, + colors=colors, + overlap_color=overlap_color + ) - # Undo changes to model - self.plots.pop() - self.settings._plot_seed = _plot_seed - self.settings._energy_mode = _energy_mode + # Create a figure sized such that the size of the axes within + # exactly matches the number of pixels specified + if axes is None: + px = 1/plt.rcParams['figure.dpi'] + fig, axes = plt.subplots() + axes.set_xlabel(xlabel) + axes.set_ylabel(ylabel) + params = fig.subplotpars + width_px = pixels[0]*px/(params.right - params.left) + height_px = pixels[1]*px/(params.top - params.bottom) + fig.set_size_inches(width_px, height_px) - # Read image from file - img_path = Path(tmpdir) / f'plot_{plot.id}.png' - if not img_path.is_file(): - img_path = img_path.with_suffix('.ppm') - img = mpimg.imread(str(img_path)) + if outline: + # Combine R, G, B values into a single int for contour detection + rgb = (img * 256).astype(int) + image_value = (rgb[..., 0] << 16) + \ + (rgb[..., 1] << 8) + (rgb[..., 2]) - # Create a figure sized such that the size of the axes within - # exactly matches the number of pixels specified - if axes is None: - px = 1/plt.rcParams['figure.dpi'] - fig, axes = plt.subplots() - axes.set_xlabel(xlabel) - axes.set_ylabel(ylabel) - params = fig.subplotpars - width = pixels[0]*px/(params.right - params.left) - height = pixels[1]*px/(params.top - params.bottom) - fig.set_size_inches(width, height) + # Set default arguments for contour() + if contour_kwargs is None: + contour_kwargs = {} + contour_kwargs.setdefault('colors', 'k') + contour_kwargs.setdefault('linestyles', 'solid') + contour_kwargs.setdefault('algorithm', 'serial') - if outline: - # Combine R, G, B values into a single int - rgb = (img * 256).astype(int) - image_value = (rgb[..., 0] << 16) + \ - (rgb[..., 1] << 8) + (rgb[..., 2]) + axes.contour( + image_value, + origin="upper", + levels=np.unique(image_value), + extent=(x_min, x_max, y_min, y_max), + **contour_kwargs + ) - # Set default arguments for contour() - if contour_kwargs is None: - contour_kwargs = {} - contour_kwargs.setdefault('colors', 'k') - contour_kwargs.setdefault('linestyles', 'solid') - contour_kwargs.setdefault('algorithm', 'serial') + # If only showing outline, set the axis limits and aspect explicitly + if outline == 'only': + axes.set_xlim(x_min, x_max) + axes.set_ylim(y_min, y_max) + axes.set_aspect('equal') - axes.contour( - image_value, - origin="upper", - levels=np.unique(image_value), - extent=(x_min, x_max, y_min, y_max), - **contour_kwargs - ) + # Add legend showing which colors represent which material or cell + if legend: + if colors is None or len(colors) == 0: + raise ValueError("Must pass 'colors' dictionary if you " + "are adding a legend via legend=True.") - # add legend showing which colors represent which material - # or cell if that was requested - if legend: - if plot.colors == {}: - raise ValueError("Must pass 'colors' dictionary if you " - "are adding a legend via legend=True.") + if color_by == "cell": + expected_key_type = openmc.Cell + else: + expected_key_type = openmc.Material - if color_by == "cell": - expected_key_type = openmc.Cell + patches = [] + for key, color in colors.items(): + if isinstance(key, int): + raise TypeError( + "Cannot use IDs in colors dict for auto legend.") + elif not isinstance(key, expected_key_type): + raise TypeError( + "Color dict key type does not match color_by") + + # this works whether we're doing cells or materials + label = key.name if key.name != '' else key.id + + # matplotlib takes RGB on 0-1 scale rather than 0-255 + if len(color) == 3 and not isinstance(color, str): + scaled_color = ( + color[0]/255, color[1]/255, color[2]/255) else: - expected_key_type = openmc.Material + scaled_color = color - patches = [] - for key, color in plot.colors.items(): + key_patch = mpatches.Patch(color=scaled_color, label=label) + patches.append(key_patch) - if isinstance(key, int): - raise TypeError( - "Cannot use IDs in colors dict for auto legend.") - elif not isinstance(key, expected_key_type): - raise TypeError( - "Color dict key type does not match color_by") - - # this works whether we're doing cells or materials - label = key.name if key.name != '' else key.id - - # matplotlib takes RGB on 0-1 scale rather than 0-255. at - # this point PlotBase has already checked that 3-tuple - # based colors are already valid, so if the length is three - # then we know it just needs to be converted to the 0-1 - # format. - if len(color) == 3 and not isinstance(color, str): - scaled_color = ( - color[0]/255, color[1]/255, color[2]/255) - else: - scaled_color = color - - key_patch = mpatches.Patch(color=scaled_color, label=label) - patches.append(key_patch) - - axes.legend(handles=patches, **legend_kwargs) - - # Plot image and return the axes - if outline != 'only': - axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) + axes.legend(handles=patches, **legend_kwargs) + # Plot image and return the axes + if outline != 'only': + axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) if n_samples: # Sample external source particles @@ -1276,8 +1469,8 @@ class Model: tol = plane_tolerance for particle in particles: if (slice_value - tol < particle.r[z] < slice_value + tol): - xs.append(particle.r[x]) - ys.append(particle.r[y]) + xs.append(particle.r[x] * axis_scaling_factor[axis_units]) + ys.append(particle.r[y] * axis_scaling_factor[axis_units]) axes.scatter(xs, ys, **source_kwargs) return axes @@ -1286,8 +1479,9 @@ class Model: self, n_samples: int = 1000, prn_seed: int | None = None, + as_array: bool = False, **init_kwargs - ) -> openmc.ParticleList: + ) -> openmc.ParticleList | np.ndarray: """Sample external source and return source particles. .. versionadded:: 0.15.1 @@ -1299,13 +1493,17 @@ class Model: prn_seed : int Pseudorandom number generator (PRNG) seed; if None, one will be generated randomly. + as_array : bool + If True, return a numpy structured array instead of a + :class:`~openmc.ParticleList`. **init_kwargs Keyword arguments passed to :func:`openmc.lib.init` Returns ------- - openmc.ParticleList - List of samples source particles + openmc.ParticleList or numpy.ndarray + List of sampled source particles, or a structured array when + *as_array* is True. """ import openmc.lib @@ -1316,7 +1514,7 @@ class Model: with openmc.lib.TemporarySession(self, **init_kwargs): return openmc.lib.sample_external_source( - n_samples=n_samples, prn_seed=prn_seed + n_samples=n_samples, prn_seed=prn_seed, as_array=as_array ) def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint): @@ -1687,20 +1885,16 @@ class Model: self.geometry.get_all_materials().values() ) - def _generate_infinite_medium_mgxs( - self, + @staticmethod + def _auto_generate_mgxs_lib( + model: openmc.model.Model, groups: openmc.mgxs.EnergyGroups, - nparticles: int, - mgxs_path: PathLike, correction: str | None, directory: PathLike, - ): - """Generate a MGXS library by running multiple OpenMC simulations, each - representing an infinite medium simulation of a single isolated - material. A discrete source is used to sample particles, with an equal - strength spread across each of the energy groups. This is a highly naive - method that ignores all spatial self shielding effects and all resonance - shielding effects between materials. + ) -> openmc.mgxs.Library: + """ + Automatically generate a multi-group cross section libray from a model + with the specified group structure. Parameters ---------- @@ -1715,100 +1909,350 @@ class Model: "P0". directory : str Directory to run the simulation in, so as to contain XML files. + + Returns + ------- + mgxs_lib : openmc.mgxs.Library + OpenMC MGXS Library object """ - warnings.warn("The infinite medium method of generating MGXS may hang " - "if a material has a k-infinity > 1.0.") - mgxs_sets = [] - for material in self.materials: - model = openmc.Model() - # Set materials on the model - model.materials = [material] + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(model.geometry) - # Settings - model.settings.batches = 100 - model.settings.particles = nparticles - model.settings.run_mode = 'fixed source' + # Pick energy group structure + mgxs_lib.energy_groups = groups - # Make a discrete source that is uniform over the bins of the group structure - n_groups = groups.num_groups - midpoints = [] - strengths = [] - for i in range(n_groups): - bounds = groups.get_group_bounds(i+1) - midpoints.append((bounds[0] + bounds[1]) / 2.0) - strengths.append(1.0) + # Disable transport correction + mgxs_lib.correction = correction - energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) - model.settings.source = openmc.IndependentSource( - space=openmc.stats.Point(), energy=energy_distribution) - model.settings.output = {'summary': True, 'tallies': False} + # Specify needed cross sections for random ray + if correction == 'P0': + mgxs_lib.mgxs_types = [ + 'nu-transport', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi', + 'kappa-fission' + ] + elif correction is None: + mgxs_lib.mgxs_types = [ + 'total', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi', + 'kappa-fission' + ] - # Geometry - box = openmc.model.RectangularPrism( - 100000.0, 100000.0, boundary_type='reflective') - name = material.name - infinite_cell = openmc.Cell(name=name, fill=material, region=-box) - infinite_universe = openmc.Universe(name=name, cells=[infinite_cell]) - model.geometry.root_universe = infinite_universe + # Specify a "material" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" - # Add MGXS Tallies + # Specify the domains over which to compute multi-group cross sections + mgxs_lib.domains = model.geometry.get_all_materials().values() - # Initialize MGXS library with a finished OpenMC geometry object - mgxs_lib = openmc.mgxs.Library(model.geometry) + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False - # Pick energy group structure - mgxs_lib.energy_groups = groups + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() - # Disable transport correction - mgxs_lib.correction = correction + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() - # Specify needed cross sections for random ray - if correction == 'P0': - mgxs_lib.mgxs_types = [ - 'nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - elif correction is None: - mgxs_lib.mgxs_types = [ - 'total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] + # Create a "tallies.xml" file for the MGXS Library + mgxs_lib.add_to_tallies(model.tallies, merge=True) - # Specify a "cell" domain type for the cross section tally filters - mgxs_lib.domain_type = "material" + # Run + statepoint_filename = model.run(cwd=directory) - # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = model.geometry.get_all_materials().values() + # Load MGXS + with openmc.StatePoint(statepoint_filename) as sp: + mgxs_lib.load_from_statepoint(sp) - # Do not compute cross sections on a nuclide-by-nuclide basis - mgxs_lib.by_nuclide = False + return mgxs_lib - # Check the library - if no errors are raised, then the library is satisfactory. - mgxs_lib.check_library_for_openmc_mgxs() + def _create_mgxs_sources( + self, + groups: openmc.mgxs.EnergyGroups, + spatial_dist: openmc.stats.Spatial, + source_energy: openmc.stats.Univariate | None = None, + ) -> list[openmc.IndependentSource]: + """Create a list of independent sources to use with MGXS generation. - # Construct all tallies needed for the multi-group cross section library - mgxs_lib.build_library() + Note that in all cases, a discrete source that is uniform over all + energy groups is created (strength = 0.01) to ensure that total cross + sections are generated for all energy groups. In the case that the user + has provided a source_energy distribution as an argument, an additional + source (strength = 0.99) is created using that energy distribution. If + the user has not provided a source_energy distribution, but the model + has sources defined, and all of those sources are of IndependentSource + type, then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being 0.99. + If the user has not provided a source_energy distribution and no sources + are defined on the model and the run mode is 'eigenvalue', then a + default Watt spectrum source (strength = 0.99) is added. - # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies(model.tallies, merge=True) + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + spatial_dist : openmc.stats.Spatial + Spatial distribution to use for all sources. + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. - # Run - statepoint_filename = model.run(cwd=directory) + Returns + ------- + list[openmc.IndependentSource] + A list of independent sources to use for MGXS generation. + """ + # Make a discrete source that is uniform over the bins of the group structure + midpoints = [] + strengths = [] + for i in range(groups.num_groups): + bounds = groups.get_group_bounds(i+1) + midpoints.append((bounds[0] + bounds[1]) / 2.0) + strengths.append(1.0) - # Load MGXS - with openmc.StatePoint(statepoint_filename) as sp: - mgxs_lib.load_from_statepoint(sp) + uniform_energy = openmc.stats.Discrete(x=midpoints, p=strengths) + uniform_distribution = openmc.IndependentSource(spatial_dist, energy=uniform_energy, strength=0.01) + sources = [uniform_distribution] - # Create a MGXS File which can then be written to disk - mgxs_set = mgxs_lib.get_xsdata(domain=material, xsdata_name=name) - mgxs_sets.append(mgxs_set) + # If the user provided an energy distribution, use that + if source_energy is not None: + user_energy = openmc.IndependentSource( + space=spatial_dist, energy=source_energy, strength=0.99) + sources.append(user_energy) - # Write the file to disk - mgxs_file = openmc.MGXSLibrary(energy_groups=groups) - for mgxs_set in mgxs_sets: - mgxs_file.add_xsdata(mgxs_set) - mgxs_file.export_to_hdf5(mgxs_path) + # If the user did not provide an energy distribution, create sources + # based on what is in their model, keeping the energy spectrum but + # replacing the spatial/angular distributions. We only do this if ALL + # sources are of IndependentSource type, as we can't pull the energy + # distribution from e.g. CompiledSource or FileSource types. + else: + if self.settings.source is not None: + for src in self.settings.source: + if not isinstance(src, openmc.IndependentSource): + break + else: + n_user_sources = len(self.settings.source) + for src in self.settings.source: + # Create a new IndependentSource with adjusted strength, space, and angle + user_source = openmc.IndependentSource( + space=spatial_dist, + energy=src.energy, + strength=0.99 / n_user_sources + ) + sources.append(user_source) + else: + # No user sources defined. If we are in eigenvalue mode, then use the default Watt spectrum. + if self.settings.run_mode == 'eigenvalue': + watt_energy = openmc.stats.Watt() + watt_source = openmc.IndependentSource( + space=spatial_dist, energy=watt_energy, strength=0.99) + sources.append(watt_source) + + return sources + + @staticmethod + def _isothermal_infinite_media_mgxs( + material: openmc.Material, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + correction: str | None, + directory: PathLike, + source: openmc.IndependentSource, + temperature_settings: dict, + temperature: float | None = None, + ) -> openmc.XSdata: + """Generate a single MGXS set for one material, where the geometry is an + infinite medium composed of that material at an isothermal temperature value. + + Parameters + ---------- + material : openmc.Material + The material to generate MGXS for + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + source : openmc.IndependentSource + Source to use when generating MGXS. + temperature_settings : dict + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + temperature : float, optional + The isothermal temperature value to apply to the material. If not specified, + defaults to the temperature in the material. + + Returns + ------- + data : openmc.XSdata + The material MGXS for the given temperature isotherm. + """ + model = openmc.Model() + + # Set materials on the model + model.materials = [material] + if temperature is not None: + model.materials[-1].temperature = temperature + + # Settings + model.settings.batches = 100 + model.settings.particles = nparticles + + model.settings.source = source + + model.settings.run_mode = 'fixed source' + model.settings.create_fission_neutrons = False + + model.settings.output = {'summary': True, 'tallies': False} + model.settings.temperature = temperature_settings + + # Geometry + box = openmc.model.RectangularPrism( + 100000.0, 100000.0, boundary_type='reflective') + name = material.name + infinite_cell = openmc.Cell(name=name, fill=model.materials[-1], region=-box) + infinite_universe = openmc.Universe(name=name, cells=[infinite_cell]) + model.geometry.root_universe = infinite_universe + + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( + model, groups, correction, directory) + + if temperature is not None: + return mgxs_lib.get_xsdata(domain=material, xsdata_name=name, + temperature=temperature) + else: + return mgxs_lib.get_xsdata(domain=material, xsdata_name=name) + + def _generate_infinite_medium_mgxs( + self, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + mgxs_path: PathLike, + correction: str | None, + directory: PathLike, + source_energy: openmc.stats.Univariate | None = None, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, + ) -> None: + """Generate a MGXS library by running multiple OpenMC simulations, each + representing an infinite medium simulation of a single isolated + material. A discrete source is used to sample particles, with an equal + strength spread across each of the energy groups. This is a highly naive + method that ignores all spatial self shielding effects and all resonance + shielding effects between materials. If temperature data points are provided, + isothermal cross sections are generated at each temperature point for + each material to build a temperature interpolation table. + + Note that in all cases, a discrete source that is uniform over all + energy groups is created (strength = 0.01) to ensure that total cross + sections are generated for all energy groups. In the case that the user + has provided a source_energy distribution as an argument, an additional + source (strength = 0.99) is created using that energy distribution. If + the user has not provided a source_energy distribution, but the model + has sources defined, and all of those sources are of IndependentSource + type, then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being 0.99. + If the user has not provided a source_energy distribution and no sources + are defined on the model and the run mode is 'eigenvalue', then a + default Watt spectrum source (strength = 0.99) is added. + + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + mgxs_path : str + Filename for the MGXS HDF5 file. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + """ + + src = self._create_mgxs_sources( + groups, + spatial_dist=openmc.stats.Point(), + source_energy=source_energy + ) + + temp_settings = {} + if temperature_settings is None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings + + if temperatures is None: + mgxs_sets = [] + for material in self.materials: + xs_data = Model._isothermal_infinite_media_mgxs( + material, + groups, + nparticles, + correction, + directory, + src, + temp_settings + ) + mgxs_sets.append(xs_data) + + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + else: + # Build a series of XSData objects, one for each isothermal temperature value. + raw_mgxs_sets = {} + for temperature in temperatures: + raw_mgxs_sets[temperature] = [] + for material in self.materials: + xs_data = Model._isothermal_infinite_media_mgxs( + material, + groups, + nparticles, + correction, + directory, + src, + temp_settings, + temperature + ) + raw_mgxs_sets[temperature].append(xs_data) + + # Unpack the isothermal XSData objects and build a single XSData object per material. + mgxs_sets = [] + for m in range(len(self.materials)): + mgxs_sets.append(openmc.XSdata(self.materials[m].name, groups, + temperatures=temperatures)) + mgxs_sets[-1].order = 0 + for temperature in temperatures: + mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][m]) + + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) @staticmethod def _create_stochastic_slab_geometry( @@ -1884,6 +2328,89 @@ class Model: return geometry, box + @staticmethod + def _isothermal_stochastic_slab_mgxs( + stoch_geom: openmc.Geometry, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + correction: str | None, + directory: PathLike, + source: openmc.IndependentSource, + temperature_settings: dict, + temperature: float | None = None, + ) -> dict[str, openmc.XSdata]: + """Generate MGXS assuming a stochastic "sandwich" of materials in a layered + slab geometry. If a temperature is specified, all materials in the slab have + their temperatures set to be isothermal at this temperature. + + Parameters + ---------- + stoch_geom : openmc.Geometry + The stochastic slab geometry. + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + source : openmc.IndependentSource + Source to use when generating MGXS. + temperature_settings : dict + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + temperature : float, optional + The isothermal temperature value to apply to the materials in the + slab. If not specified, defaults to the temperature in the materials. + + Returns + ------- + data : dict[str, openmc.XSdata] + A dictionary where the key is the name of the material and the value is the isothermal MGXS. + """ + + model = openmc.Model() + model.geometry = stoch_geom + + if temperature is not None: + for material in model.geometry.get_all_materials().values(): + material.temperature = temperature + + # Settings + model.settings.batches = 200 + model.settings.inactive = 100 + model.settings.particles = nparticles + model.settings.output = {'summary': True, 'tallies': False} + model.settings.temperature = temperature_settings + + # Define the sources + model.settings.source = source + + model.settings.run_mode = 'fixed source' + model.settings.create_fission_neutrons = False + + model.settings.output = {'summary': True, 'tallies': False} + + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( + model, groups, correction, directory) + + # Fetch all of the isothermal results. + if temperature is not None: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, + temperature=temperature) + for mat in mgxs_lib.domains + } + else: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name) + for mat in mgxs_lib.domains + } + def _generate_stochastic_slab_mgxs( self, groups: openmc.mgxs.EnergyGroups, @@ -1891,6 +2418,9 @@ class Model: mgxs_path: PathLike, correction: str | None, directory: PathLike, + source_energy: openmc.stats.Univariate | None = None, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, ) -> None: """Generate MGXS assuming a stochastic "sandwich" of materials in a layered slab geometry. While geometry-specific spatial shielding effects are not @@ -1900,7 +2430,9 @@ class Model: will generate cross sections for all materials in the problem regardless of type. If this is a fixed source problem, a discrete source is used to sample particles, with an equal strength spread across each of the - energy groups. + energy groups. If temperature data points are provided, + isothermal cross sections are generated at each temperature point for + the stochastic slab to build a temperature interpolation table. Parameters ---------- @@ -1915,85 +2447,169 @@ class Model: "P0". directory : str Directory to run the simulation in, so as to contain XML files. + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. In all cases, a discrete source that + is uniform over all energy groups is created (strength = 0.01) to + ensure that total cross sections are generated for all energy + groups. In the case that the user has provided a source_energy + distribution as an argument, an additional source (strength = 0.99) + is created using that energy distribution. If the user has not + provided a source_energy distribution, but the model has sources + defined, and all of those sources are of IndependentSource type, + then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being + 0.99. If the user has not provided a source_energy distribution and + no sources are defined on the model and the run mode is + 'eigenvalue', then a default Watt spectrum source (strength = 0.99) + is added. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ - model = openmc.Model() - model.materials = self.materials + + # Stochastic slab geometry + geo, spatial_distribution = Model._create_stochastic_slab_geometry( + self.materials) + + src = self._create_mgxs_sources( + groups, + spatial_dist=spatial_distribution, + source_energy=source_energy + ) + + temp_settings = {} + if temperature_settings is None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings + + if temperatures is None: + mgxs_sets = Model._isothermal_stochastic_slab_mgxs( + geo, + groups, + nparticles, + correction, + directory, + src, + temp_settings + ).values() + + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + else: + # Build a series of XSData objects, one for each isothermal temperature value. + raw_mgxs_sets = {} + for temperature in temperatures: + raw_mgxs_sets[temperature] = Model._isothermal_stochastic_slab_mgxs( + geo, + groups, + nparticles, + correction, + directory, + src, + temp_settings, + temperature + ) + + # Unpack the isothermal XSData objects and build a single XSData object per material. + mgxs_sets = [] + for mat in self.materials: + mgxs_sets.append(openmc.XSdata(mat.name, groups, temperatures=temperatures)) + mgxs_sets[-1].order = 0 + for temperature in temperatures: + mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name]) + + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + + @staticmethod + def _isothermal_materialwise_mgxs( + input_model: openmc.Model, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + correction: str | None, + directory: PathLike, + temperature_settings: dict, + temperature: float | None = None, + ) -> dict[str, openmc.XSdata]: + """Generate a material-wise MGXS library for the model by running the + original continuous energy OpenMC simulation. If a temperature is + specified, each material in the input model is set to that temperature. + Otherwise, the original material temperatures are used. If temperature + data points are provided, isothermal cross sections are generated at + each temperature point for the whole model to build a temperature + interpolation table. + + Parameters + ---------- + input_model : openmc.Model + The model to use when computing material-wise MGXS. + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + temperature_settings : dict + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + temperature : float, optional + The isothermal temperature value to apply to the materials in the + input model. If not specified, defaults to the temperatures in the + materials. + + Returns + ------- + data : dict[str, openmc.XSdata] + A dictionary where the key is the name of the material and the value is the isothermal MGXS. + """ + model = copy.deepcopy(input_model) + model.tallies = openmc.Tallies() + + if temperature is not None: + for material in model.geometry.get_all_materials().values(): + material.temperature = temperature # Settings model.settings.batches = 200 model.settings.inactive = 100 model.settings.particles = nparticles model.settings.output = {'summary': True, 'tallies': False} - model.settings.run_mode = self.settings.run_mode + model.settings.temperature = temperature_settings - # Stochastic slab geometry - model.geometry, spatial_distribution = Model._create_stochastic_slab_geometry( - model.materials) + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( + model, groups, correction, directory) - # Make a discrete source that is uniform over the bins of the group structure - n_groups = groups.num_groups - midpoints = [] - strengths = [] - for i in range(n_groups): - bounds = groups.get_group_bounds(i+1) - midpoints.append((bounds[0] + bounds[1]) / 2.0) - strengths.append(1.0) - - energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) - model.settings.source = [openmc.IndependentSource( - space=spatial_distribution, energy=energy_distribution, strength=1.0)] - - model.settings.output = {'summary': True, 'tallies': False} - - # Add MGXS Tallies - - # Initialize MGXS library with a finished OpenMC geometry object - mgxs_lib = openmc.mgxs.Library(model.geometry) - - # Pick energy group structure - mgxs_lib.energy_groups = groups - - # Disable transport correction - mgxs_lib.correction = correction - - # Specify needed cross sections for random ray - if correction == 'P0': - mgxs_lib.mgxs_types = ['nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi'] - elif correction is None: - mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi'] - - # Specify a "cell" domain type for the cross section tally filters - mgxs_lib.domain_type = "material" - - # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = model.geometry.get_all_materials().values() - - # Do not compute cross sections on a nuclide-by-nuclide basis - mgxs_lib.by_nuclide = False - - # Check the library - if no errors are raised, then the library is satisfactory. - mgxs_lib.check_library_for_openmc_mgxs() - - # Construct all tallies needed for the multi-group cross section library - mgxs_lib.build_library() - - # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies(model.tallies, merge=True) - - # Run - statepoint_filename = model.run(cwd=directory) - - # Load MGXS - with openmc.StatePoint(statepoint_filename) as sp: - mgxs_lib.load_from_statepoint(sp) - - names = [mat.name for mat in mgxs_lib.domains] - - # Create a MGXS File which can then be written to disk - mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) - mgxs_file.export_to_hdf5(mgxs_path) + # Fetch all of the isothermal results. + if temperature is not None: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, + temperature=temperature) + for mat in mgxs_lib.domains + } + else: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name) + for mat in mgxs_lib.domains + } def _generate_material_wise_mgxs( self, @@ -2002,6 +2618,8 @@ class Model: mgxs_path: PathLike, correction: str | None, directory: PathLike, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, ) -> None: """Generate a material-wise MGXS library for the model by running the original continuous energy OpenMC simulation of the full material @@ -2026,79 +2644,75 @@ class Model: "P0". directory : PathLike Directory to run the simulation in, so as to contain XML files. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ - model = copy.deepcopy(self) - model.tallies = openmc.Tallies() + temp_settings = {} + if temperature_settings is None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings - # Settings - model.settings.batches = 200 - model.settings.inactive = 100 - model.settings.particles = nparticles - model.settings.output = {'summary': True, 'tallies': False} + if temperatures is None: + mgxs_sets = Model._isothermal_materialwise_mgxs( + self, + groups, + nparticles, + correction, + directory, + temp_settings + ).values() - # Add MGXS Tallies + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + else: + # Build a series of XSData objects, one for each isothermal temperature value. + raw_mgxs_sets = {} + for temperature in temperatures: + raw_mgxs_sets[temperature] = Model._isothermal_materialwise_mgxs( + self, + groups, + nparticles, + correction, + directory, + temp_settings, + temperature + ) - # Initialize MGXS library with a finished OpenMC geometry object - mgxs_lib = openmc.mgxs.Library(model.geometry) + # Unpack the isothermal XSData objects and build a single XSData object per material. + mgxs_sets = [] + for mat in self.materials: + mgxs_sets.append(openmc.XSdata(mat.name, groups, temperatures=temperatures)) + mgxs_sets[-1].order = 0 + for temperature in temperatures: + mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name]) - # Pick energy group structure - mgxs_lib.energy_groups = groups - - # Disable transport correction - mgxs_lib.correction = correction - - # Specify needed cross sections for random ray - if correction == 'P0': - mgxs_lib.mgxs_types = [ - 'nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - elif correction is None: - mgxs_lib.mgxs_types = [ - 'total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - - # Specify a "cell" domain type for the cross section tally filters - mgxs_lib.domain_type = "material" - - # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = model.geometry.get_all_materials().values() - - # Do not compute cross sections on a nuclide-by-nuclide basis - mgxs_lib.by_nuclide = False - - # Check the library - if no errors are raised, then the library is satisfactory. - mgxs_lib.check_library_for_openmc_mgxs() - - # Construct all tallies needed for the multi-group cross section library - mgxs_lib.build_library() - - # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies(model.tallies, merge=True) - - # Run - statepoint_filename = model.run(cwd=directory) - - # Load MGXS - with openmc.StatePoint(statepoint_filename) as sp: - mgxs_lib.load_from_statepoint(sp) - - names = [mat.name for mat in mgxs_lib.domains] - - # Create a MGXS File which can then be written to disk - mgxs_file = mgxs_lib.create_mg_library( - xs_type='macro', xsdata_names=names) - mgxs_file.export_to_hdf5(mgxs_path) + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) def convert_to_multigroup( self, method: str = "material_wise", - groups: str = "CASMO-2", + groups: str | Sequence[float] | openmc.mgxs.EnergyGroups = "CASMO-2", nparticles: int = 2000, overwrite_mgxs_library: bool = False, mgxs_path: PathLike = "mgxs.h5", correction: str | None = None, + source_energy: openmc.stats.Univariate | None = None, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, ): """Convert all materials from continuous energy to multigroup. @@ -2109,9 +2723,13 @@ class Model: ---------- method : {"material_wise", "stochastic_slab", "infinite_medium"}, optional Method to generate the MGXS. - groups : openmc.mgxs.EnergyGroups or str, optional - Energy group structure for the MGXS or the name of the group - structure (based on keys from openmc.mgxs.GROUP_STRUCTURES). + groups : openmc.mgxs.EnergyGroups, str, or sequence of float, optional + Energy group structure for the MGXS. Can be an + :class:`openmc.mgxs.EnergyGroups` object, a string name of a + predefined group structure from :data:`openmc.mgxs.GROUP_STRUCTURES` + (e.g., ``"CASMO-2"``), or a sequence of floats specifying energy + bin boundaries in eV (e.g., ``[0.0, 1e6]`` for a single group). + Defaults to ``"CASMO-2"``. nparticles : int, optional Number of particles to simulate per batch when generating MGXS. overwrite_mgxs_library : bool, optional @@ -2121,8 +2739,34 @@ class Model: correction : str, optional Transport correction to apply to the MGXS. Options are None and "P0". + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. In all cases, a discrete source that + is uniform over all energy groups is created (strength = 0.01) to + ensure that total cross sections are generated for all energy + groups. In the case that the user has provided a source_energy + distribution as an argument, an additional source (strength = 0.99) + is created using that energy distribution. If the user has not + provided a source_energy distribution, but the model has sources + defined, and all of those sources are of IndependentSource type, + then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being + 0.99. If the user has not provided a source_energy distribution and + no sources are defined on the model and the run mode is + 'eigenvalue', then a default Watt spectrum source (strength = 0.99) + is added. Note that this argument is only used when using the + "stochastic_slab" or "infinite_medium" MGXS generation methods. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ - if isinstance(groups, str): + if not isinstance(groups, openmc.mgxs.EnergyGroups): groups = openmc.mgxs.EnergyGroups(groups) # Do all work (including MGXS generation) in a temporary directory @@ -2134,29 +2778,42 @@ class Model: # TODO: Can this be done without having to init/finalize? for univ in self.geometry.get_all_universes().values(): if isinstance(univ, openmc.DAGMCUniverse): + # Initialize in stochastic volume mode (non-transport mode) + # This mode doesn't require + # valid transport settings like particles/batches + original_run_mode = self.settings.run_mode + self.settings.run_mode = 'volume' self.init_lib(directory=tmpdir) self.sync_dagmc_universes() self.finalize_lib() + # Restore original run mode + self.settings.run_mode = original_run_mode break - # Make sure all materials have a name, and that the name is a valid HDF5 - # dataset name + # Temporarily replace each material's name with a unique, valid HDF5 + # dataset name (its name plus ID) for use as its MGXS library entry + # and macroscopic. The ID keeps the name unique even when materials + # share a name; the original names are restored at the end. + original_names = [material.name for material in self.materials] for material in self.materials: - if not material.name or not material.name.strip(): - material.name = f"material {material.id}" - material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + base = material.name if material.name and material.name.strip() \ + else "material" + material.name = re.sub(r'[^a-zA-Z0-9]', '_', base) + f"_{material.id}" # If needed, generate the needed MGXS data library file if not Path(mgxs_path).is_file() or overwrite_mgxs_library: if method == "infinite_medium": self._generate_infinite_medium_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy, + temperatures, temperature_settings) elif method == "material_wise": self._generate_material_wise_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir) + groups, nparticles, mgxs_path, correction, tmpdir, + temperatures, temperature_settings) elif method == "stochastic_slab": self._generate_stochastic_slab_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy, + temperatures, temperature_settings) else: raise ValueError( f'MGXS generation method "{method}" not recognized') @@ -2173,6 +2830,10 @@ class Model: self.settings.energy_mode = 'multi-group' + # Restore the user's original material names. + for material, name in zip(self.materials, original_names): + material.name = name + def convert_to_random_ray(self): """Convert a multigroup model to use random ray. @@ -2489,5 +3150,3 @@ class SearchResult: def total_batches(self) -> int: """Total number of active batches used across all evaluations.""" return sum(self.batches) - - diff --git a/openmc/particle_restart.py b/openmc/particle_restart.py index 317d8761b..44864107d 100644 --- a/openmc/particle_restart.py +++ b/openmc/particle_restart.py @@ -1,6 +1,7 @@ import h5py import openmc.checkvalue as cv +from .particle_type import ParticleType _VERSION_PARTICLE_RESTART = 2 @@ -28,8 +29,8 @@ class Particle: Type of simulation (criticality or fixed source) id : long Identifier of the particle - type : int - Particle type (1 = neutron, 2 = photon, 3 = electron, 4 = positron) + type : openmc.ParticleType + Particle type weight : float Weight of the particle energy : float @@ -52,7 +53,7 @@ class Particle: self.energy = f['energy'][()] self.generations_per_batch = f['generations_per_batch'][()] self.id = f['id'][()] - self.type = f['type'][()] + self.type = ParticleType(f['type'][()]) self.n_particles = f['n_particles'][()] self.run_mode = f['run_mode'][()].decode() self.uvw = f['uvw'][()] diff --git a/openmc/particle_type.py b/openmc/particle_type.py new file mode 100644 index 000000000..9d7713443 --- /dev/null +++ b/openmc/particle_type.py @@ -0,0 +1,228 @@ +from numbers import Integral + +from openmc.data import gnds_name, zam, ATOMIC_SYMBOL + + +_PDG_NAME = { + 2112: 'neutron', + 22: 'photon', + 11: 'electron', + -11: 'positron', + 2212: 'H1', +} + +_ALIAS_PDG = { + 'neutron': 2112, + 'n': 2112, + 'photon': 22, + 'gamma': 22, + 'electron': 11, + 'positron': -11, + 'proton': 2212, + 'p': 2212, + 'h1': 2212, + 'deuteron': 1000010020, + 'd': 1000010020, + 'h2': 1000010020, + 'triton': 1000010030, + 't': 1000010030, + 'h3': 1000010030, + 'alpha': 1000020040, + 'he4': 1000020040, +} + +_LEGACY_PARTICLE_INDEX = { + 0: 2112, + 1: 22, + 2: 11, + 3: -11, +} + + +class ParticleType: + """Particle type defined by a PDG number. + + ParticleType uses the Particle Data Group (PDG) Monte Carlo numbering scheme + to uniquely identify particle types. This includes elementary particles + (neutrons, photons, etc.) and nuclear codes for isotopes. + + Parameters + ---------- + value : str, int, or ParticleType + The particle identifier. Can be: + + - A string name (e.g., 'neutron', 'photon', 'He4', 'U235') + - An integer PDG number (e.g., 2112 for neutron) + - A string with PDG prefix (e.g., 'pdg:2112') + - An existing ParticleType instance + + Attributes + ---------- + pdg_number : int + The PDG number for this particle type + zam : tuple of int or None + For nuclear particles, the (Z, A, m) tuple where Z is atomic number, + A is mass number, and m is metastable state. None for elementary particles. + is_nucleus : bool + Whether this particle is a nucleus (ion) + + Examples + -------- + >>> neutron = ParticleType('neutron') + >>> neutron.pdg_number + 2112 + >>> he4 = ParticleType('He4') + >>> he4.zam + (2, 4, 0) + >>> ParticleType(2112) == ParticleType('neutron') + True + + """ + + __slots__ = ('_pdg_number',) + + def __init__(self, value: 'str | int | ParticleType'): + if isinstance(value, ParticleType): + pdg = value._pdg_number + elif isinstance(value, str): + pdg = self._pdg_number_from_string(value) + elif isinstance(value, Integral): + pdg = int(value) + # Handle legacy particle indices (0, 1, 2, 3) + if pdg in _LEGACY_PARTICLE_INDEX: + pdg = _LEGACY_PARTICLE_INDEX[pdg] + else: + raise TypeError(f"Cannot create ParticleType from {type(value).__name__}") + + self._pdg_number = pdg + + def __eq__(self, other): + if isinstance(other, ParticleType): + return self._pdg_number == other._pdg_number + if isinstance(other, Integral): + return self._pdg_number == int(other) + if isinstance(other, str): + try: + return self._pdg_number == ParticleType(other)._pdg_number + except (ValueError, TypeError): + return False + return NotImplemented + + def __hash__(self) -> int: + return hash(self._pdg_number) + + def __int__(self) -> int: + return self._pdg_number + + @property + def pdg_number(self) -> int: + return self._pdg_number + + @staticmethod + def _pdg_number_from_string(value: str) -> int: + """Parse a string to get a PDG number. + + Parameters + ---------- + value : str + Particle identifier string + + Returns + ------- + int + PDG number + + Raises + ------ + ValueError + If string cannot be parsed as a valid particle identifier + + """ + s = value.strip() + if not s: + raise ValueError('Particle identifier cannot be empty.') + + lower = s.lower() + if lower.startswith('pdg:'): + code_str = lower[4:] + try: + return int(code_str) + except ValueError: + raise ValueError(f'Invalid PDG number: {code_str}') + + if lower in _ALIAS_PDG: + return _ALIAS_PDG[lower] + + # Assume it is a GNDS nuclide name + Z, A, m = zam(s) + if Z <= 0 or Z > 999 or A <= 0 or A > 999 or m < 0 or m > 9: + raise ValueError('Invalid Z/A/m for nuclear PDG number.') + return 1000000000 + Z * 10000 + A * 10 + m + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + """Return a canonical string representation of the particle type. + + Returns + ------- + str + Canonical name (e.g., 'neutron', 'He4', 'pdg:12345') + + """ + if self._pdg_number in _PDG_NAME: + return _PDG_NAME[self._pdg_number] + + if (zam_tuple := self.zam) is not None: + Z, A, m = zam_tuple + if Z <= 0 or Z > max(ATOMIC_SYMBOL) or A <= 0 or A > 999: + raise ValueError(f"Invalid nuclear PDG number: {self._pdg_number}") + return gnds_name(Z, A, m) + + return f'pdg:{self._pdg_number}' + + @property + def zam(self) -> 'tuple[int, int, int] | None': + """Return the (Z, A, m) tuple for nuclear particles. + + Returns + ------- + tuple of int or None + For nuclear particles, returns (Z, A, m) where Z is atomic number, + A is mass number, and m is metastable state. Returns None for + elementary particles. + + """ + if self._pdg_number < 1000000000: + return None + Z = (self._pdg_number // 10000) % 1000 + A = (self._pdg_number // 10) % 1000 + m = self._pdg_number % 10 + if Z <= 0 or A <= 0: + return None + else: + return (Z, A, m) + + @property + def is_nucleus(self) -> bool: + """Return whether this particle is a nucleus. + + Returns + ------- + bool + True if the particle is a nucleus (ion), False otherwise + + """ + return self.zam is not None + + +# Define common particle constants +ParticleType.NEUTRON = ParticleType(2112) +ParticleType.PHOTON = ParticleType(22) +ParticleType.ELECTRON = ParticleType(11) +ParticleType.POSITRON = ParticleType(-11) +ParticleType.PROTON = ParticleType(2212) +ParticleType.DEUTERON = ParticleType(1000010020) +ParticleType.TRITON = ParticleType(1000010030) +ParticleType.ALPHA = ParticleType(1000020040) diff --git a/openmc/plots.py b/openmc/plots.py index a0bde3f00..531184095 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,7 +1,8 @@ -from collections.abc import Iterable, Mapping +from collections.abc import Iterable, Mapping, Sequence from numbers import Integral, Real from pathlib import Path from textwrap import dedent +import warnings import h5py import lxml.etree as ET @@ -196,13 +197,13 @@ _PLOT_PARAMS = dedent("""\ Assigns colors to specific materials or cells. Keys are instances of :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for example: + and alpha should all be integers in the range [0, 255], for example: .. code-block:: python # Make water blue water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) + universe.plot(..., colors={water: (0, 0, 255)) seed : int Seed for the random number generator openmc_exec : str @@ -354,6 +355,86 @@ def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'): return output +def id_map_to_rgb( + id_map: np.ndarray, + color_by: str = 'cell', + colors: dict | None = None, + overlap_color: Sequence[int] | str = (255, 0, 0) +) -> np.ndarray: + """Convert ID map array to RGB image array. + + Parameters + ---------- + id_map : numpy.ndarray + Array with shape (v_pixels, h_pixels, 3) containing cell IDs, + cell instances, and material IDs + color_by : {'cell', 'material'} + Whether to color by cell or material + colors : dict, optional + Dictionary mapping cells/materials to colors + overlap_color : sequence of int or str, optional + Color to use for overlaps. Defaults to red (255, 0, 0). + + Returns + ------- + numpy.ndarray + RGB image array with shape (v_pixels, h_pixels, 3) with values + in range [0, 1] for matplotlib + """ + # Initialize RGB array with white background (values between 0 and 1 for matplotlib) + img = np.ones(id_map.shape, dtype=float) + + # Get the appropriate index based on color_by + if color_by == 'cell': + id_index = 0 # Cell IDs are in the first channel + elif color_by == 'material': + id_index = 2 # Material IDs are in the third channel + else: + raise ValueError("color_by must be either 'cell' or 'material'") + + # Get all unique IDs in the plot + unique_ids = np.unique(id_map[:, :, id_index]) + + # Generate default colors if not provided + if colors is None: + colors = {} + + # Convert colors dict to use IDs as keys + color_map = {} + for key, color in colors.items(): + if isinstance(key, (openmc.Cell, openmc.Material)): + color_map[key.id] = color + else: + color_map[key] = color + + # Generate random colors for IDs not in color_map + rng = np.random.RandomState(1) + for uid in unique_ids: + if uid > 0 and uid not in color_map: + color_map[uid] = rng.randint(0, 256, (3,)) + + # Apply colors to each pixel + for uid in unique_ids: + if uid == -1: # Background/void + continue + elif uid == -3: # Overlap (only present if color_overlaps was True) + if isinstance(overlap_color, str): + rgb = _SVG_COLORS[overlap_color.lower()] + else: + rgb = overlap_color + mask = id_map[:, :, id_index] == uid + img[mask] = np.array(rgb) / 255.0 + elif uid in color_map: + color = color_map[uid] + if isinstance(color, str): + rgb = _SVG_COLORS[color.lower()] + else: + rgb = color + mask = id_map[:, :, id_index] == uid + img[mask] = np.array(rgb) / 255.0 + + return img + class PlotBase(IDManagerMixin): """ Parameters @@ -626,14 +707,15 @@ class PlotBase(IDManagerMixin): return element -class Plot(PlotBase): - """Definition of a finite region of space to be plotted. +class SlicePlot(PlotBase): + """Definition of a 2D slice plot of the geometry. - OpenMC is capable of generating two-dimensional slice plots, or - three-dimensional voxel or projection plots. Colors that are used in plots can be given as - RGB tuples, e.g. (255, 255, 255) would be white, or by a string indicating a + Colors that are used in plots can be given as RGB tuples, e.g. + (255, 255, 255) would be white, or by a string indicating a valid `SVG color `_. + .. versionadded:: 0.15.4 + Parameters ---------- plot_id : int @@ -648,7 +730,7 @@ class Plot(PlotBase): name : str Name of the plot pixels : Iterable of int - Number of pixels to use in each direction + Number of pixels to use in each direction (2 values) filename : str Path to write the plot to color_by : {'cell', 'material'} @@ -671,11 +753,9 @@ class Plot(PlotBase): level : int Universe depth to plot at width : Iterable of float - Width of the plot in each basis direction + Width of the plot in each basis direction (2 values) origin : tuple or list of ndarray - Origin (center) of the plot - type : {'slice', 'voxel'} - The type of the plot + Origin (center) of the plot (3 values) basis : {'xy', 'xz', 'yz'} The basis directions for the plot meshlines : dict @@ -688,10 +768,37 @@ class Plot(PlotBase): super().__init__(plot_id, name) self._width = [4.0, 4.0] self._origin = [0., 0., 0.] - self._type = 'slice' self._basis = 'xy' self._meshlines = None + @property + def type(self): + warnings.warn( + "The 'type' attribute is deprecated and will be removed in a future version. " + "This is a SlicePlot instance.", + FutureWarning, stacklevel=2 + ) + return 'slice' + + @type.setter + def type(self, value): + raise TypeError( + "Setting plot.type is no longer supported. " + "Use openmc.SlicePlot() for 2D slice plots or openmc.VoxelPlot() for 3D voxel plots." + ) + + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 2, 2) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + @property def width(self): return self._width @@ -699,7 +806,7 @@ class Plot(PlotBase): @width.setter def width(self, width): cv.check_type('plot width', width, Iterable, Real) - cv.check_length('plot width', width, 2, 3) + cv.check_length('plot width', width, 2, 2) self._width = width @property @@ -712,15 +819,6 @@ class Plot(PlotBase): cv.check_length('plot origin', origin, 3) self._origin = origin - @property - def type(self): - return self._type - - @type.setter - def type(self, plottype): - cv.check_value('plot type', plottype, ['slice', 'voxel']) - self._type = plottype - @property def basis(self): return self._basis @@ -763,11 +861,10 @@ class Plot(PlotBase): self._meshlines = meshlines def __repr__(self): - string = 'Plot\n' + string = 'SlicePlot\n' string += '{: <16}=\t{}\n'.format('\tID', self._id) string += '{: <16}=\t{}\n'.format('\tName', self._name) string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) - string += '{: <16}=\t{}\n'.format('\tType', self._type) string += '{: <16}=\t{}\n'.format('\tBasis', self._basis) string += '{: <16}=\t{}\n'.format('\tWidth', self._width) string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin) @@ -883,7 +980,7 @@ class Plot(PlotBase): self._colors[domain] = (r, g, b) def to_xml_element(self): - """Return XML representation of the slice/voxel plot + """Return XML representation of the slice plot Returns ------- @@ -893,10 +990,8 @@ class Plot(PlotBase): """ element = super().to_xml_element() - element.set("type", self._type) - - if self._type == 'slice': - element.set("basis", self._basis) + element.set("type", "slice") + element.set("basis", self._basis) subelement = ET.SubElement(element, "origin") subelement.text = ' '.join(map(str, self._origin)) @@ -942,8 +1037,8 @@ class Plot(PlotBase): Returns ------- - openmc.Plot - Plot object + openmc.SlicePlot + SlicePlot object """ plot_id = int(get_text(elem, "id")) @@ -952,9 +1047,7 @@ class Plot(PlotBase): if "filename" in elem.keys(): plot.filename = get_text(elem, "filename") plot.color_by = get_text(elem, "color_by") - plot.type = get_text(elem, "type") - if plot.type == 'slice': - plot.basis = get_text(elem, "basis") + plot.basis = get_text(elem, "basis") plot.origin = tuple(get_elem_list(elem, "origin", float)) plot.width = tuple(get_elem_list(elem, "width", float)) @@ -1036,9 +1129,215 @@ class Plot(PlotBase): # Return produced image return _get_plot_image(self, cwd) + + +class VoxelPlot(PlotBase): + """Definition of a 3D voxel plot of the geometry. + + Colors that are used in plots can be given as RGB tuples, e.g. + (255, 255, 255) would be white, or by a string indicating a + valid `SVG color `_. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + plot_id : int + Unique identifier for the plot + name : str + Name of the plot + + Attributes + ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction (3 values) + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + background : Iterable of int or str + Color of the background + mask_components : Iterable of openmc.Cell or openmc.Material or int + The cells or materials (or corresponding IDs) to mask + mask_background : Iterable of int or str + Color to apply to all cells/materials listed in mask_components + show_overlaps : bool + Indicate whether or not overlapping regions are shown + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + level : int + Universe depth to plot at + width : Iterable of float + Width of the plot in each dimension (3 values) + origin : tuple or list of ndarray + Origin (center) of the plot (3 values) + + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._width = [4.0, 4.0, 4.0] + self._origin = [0., 0., 0.] + self._pixels = [400, 400, 400] + + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 3, 3) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + cv.check_type('plot width', width, Iterable, Real) + cv.check_length('plot width', width, 3, 3) + self._width = width + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, origin): + cv.check_type('plot origin', origin, Iterable, Real) + cv.check_length('plot origin', origin, 3) + self._origin = origin + + def __repr__(self): + string = 'VoxelPlot\n' + string += '{: <16}=\t{}\n'.format('\tID', self._id) + string += '{: <16}=\t{}\n'.format('\tName', self._name) + string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) + string += '{: <16}=\t{}\n'.format('\tWidth', self._width) + string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin) + string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) + string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) + string += '{: <16}=\t{}\n'.format('\tBackground', self._background) + string += '{: <16}=\t{}\n'.format('\tMask components', + self._mask_components) + string += '{: <16}=\t{}\n'.format('\tMask background', + self._mask_background) + string += '{: <16}=\t{}\n'.format('\tOverlap Color', + self._overlap_color) + string += '{: <16}=\t{}\n'.format('\tColors', self._colors) + string += '{: <16}=\t{}\n'.format('\tLevel', self._level) + return string + + def to_xml_element(self): + """Return XML representation of the voxel plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + + element = super().to_xml_element() + element.set("type", "voxel") + + subelement = ET.SubElement(element, "origin") + subelement.text = ' '.join(map(str, self._origin)) + + subelement = ET.SubElement(element, "width") + subelement.text = ' '.join(map(str, self._width)) + + if self._colors: + self._colors_to_xml(element) + + if self._show_overlaps: + subelement = ET.SubElement(element, "show_overlaps") + subelement.text = "true" + + if self._overlap_color is not None: + color = self._overlap_color + if isinstance(color, str): + color = _SVG_COLORS[color.lower()] + subelement = ET.SubElement(element, "overlap_color") + subelement.text = ' '.join(str(x) for x in color) + + return element + + @classmethod + def from_xml_element(cls, elem): + """Generate plot object from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.VoxelPlot + VoxelPlot object + + """ + plot_id = int(get_text(elem, "id")) + name = get_text(elem, 'name', '') + plot = cls(plot_id, name) + if "filename" in elem.keys(): + plot.filename = get_text(elem, "filename") + plot.color_by = get_text(elem, "color_by") + + plot.origin = tuple(get_elem_list(elem, "origin", float)) + plot.width = tuple(get_elem_list(elem, "width", float)) + plot.pixels = tuple(get_elem_list(elem, "pixels")) + background = get_elem_list(elem, "background") + if background is not None: + plot._background = tuple(background) + + # Set plot colors + colors = {} + for color_elem in elem.findall("color"): + uid = int(get_text(color_elem, "id")) + colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) + plot.colors = colors + + # Set masking information + mask_elem = elem.find("mask") + if mask_elem is not None: + plot.mask_components = get_elem_list(mask_elem, "components", int) + background = get_elem_list(mask_elem, "background", int) + if background is not None: + plot.mask_background = tuple(background) + + # show overlaps + overlap = get_text(elem, "show_overlaps") + if overlap is not None: + plot.show_overlaps = (overlap in ('true', '1')) + overlap_color = get_elem_list(elem, "overlap_color", int) + if overlap_color is not None: + plot.overlap_color = tuple(overlap_color) + + # Set universe level + level = get_text(elem, "level") + if level is not None: + plot.level = int(level) + + return plot + def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): - """Render plot as an voxel image + """Render plot as a voxel image This method runs OpenMC in plotting mode to produce a .vti file. @@ -1059,10 +1358,6 @@ class Plot(PlotBase): Path of the .vti file produced """ - if self.type != 'voxel': - raise ValueError( - 'Generating a VTK file only works for voxel plots') - # Create plots.xml Plots([self]).export_to_xml(cwd) @@ -1082,6 +1377,20 @@ class Plot(PlotBase): return voxel_to_vtk(h5_voxel_file, output) +def Plot(plot_id=None, name=''): + """Legacy Plot class for backward compatibility. + + .. deprecated:: 0.15.4 + Use :class:`SlicePlot` for 2D slice plots or :class:`VoxelPlot` for 3D voxel plots. + + """ + warnings.warn( + "The Plot class is deprecated. Use SlicePlot for 2D slice plots " + "or VoxelPlot for 3D voxel plots.", FutureWarning + ) + return SlicePlot(plot_id, name) + + class RayTracePlot(PlotBase): """Definition of a camera's view of OpenMC geometry @@ -1737,16 +2046,16 @@ class SolidRayTracePlot(RayTracePlot): class Plots(cv.CheckedList): - """Collection of Plots used for an OpenMC simulation. + """Collection of plots used for an OpenMC simulation. This class corresponds directly to the plots.xml input file. It can be thought of as a normal Python list where each member is inherits from :class:`PlotBase`. It behaves like a list as the following example demonstrates: - >>> xz_plot = openmc.Plot() - >>> big_plot = openmc.Plot() - >>> small_plot = openmc.Plot() + >>> xz_plot = openmc.SlicePlot() + >>> big_plot = openmc.VoxelPlot() + >>> small_plot = openmc.SlicePlot() >>> p = openmc.Plots((xz_plot, big_plot)) >>> p.append(small_plot) >>> small_plot = p.pop() @@ -1782,7 +2091,7 @@ class Plots(cv.CheckedList): ---------- index : int Index in list - plot : openmc.Plot + plot : openmc.PlotBase Plot to insert """ @@ -1903,8 +2212,13 @@ class Plots(cv.CheckedList): plots.append(WireframeRayTracePlot.from_xml_element(e)) elif plot_type == 'solid_raytrace': plots.append(SolidRayTracePlot.from_xml_element(e)) - elif plot_type in ('slice', 'voxel'): - plots.append(Plot.from_xml_element(e)) + elif plot_type == 'slice': + plots.append(SlicePlot.from_xml_element(e)) + elif plot_type == 'voxel': + plots.append(VoxelPlot.from_xml_element(e)) + elif plot_type is None: + # For backward compatibility, assume slice if no type specified + plots.append(SlicePlot.from_xml_element(e)) else: raise ValueError("Unknown plot type: {}".format(plot_type)) return plots diff --git a/openmc/settings.py b/openmc/settings.py index 289fb35b7..8120eb073 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -4,6 +4,7 @@ import itertools from math import ceil from numbers import Integral, Real from pathlib import Path +import traceback import lxml.etree as ET import warnings @@ -40,6 +41,10 @@ class Settings: Attributes ---------- + atomic_relaxation : bool + Whether to simulate the atomic relaxation cascade (fluorescence photons + and Auger electrons) following photoelectric and incoherent scattering + interactions. batches : int Number of batches to simulate confidence_intervals : bool @@ -62,6 +67,10 @@ class Settings: (ex: ["(n,fission)", 2, "(n,2n)"] ). (list of str or int) :deposited_E_threshold: Number to define the minimum deposited energy during per collision to trigger banking. (float) + create_delayed_neutrons : bool + Whether delayed neutrons are created in fission. + + .. versionadded:: 0.13.3 create_fission_neutrons : bool Indicate whether fission neutrons should be created or not. cutoff : dict @@ -178,11 +187,14 @@ class Settings: Initial seed for randomly generated plot colors. ptables : bool Determine whether probability tables are used. + properties_file : PathLike + Location of the properties file to load cell temperatures/densities + and material densities random_ray : dict Options for configuring the random ray solver. Acceptable keys are: :distance_inactive: - Indicates the total active distance in [cm] a ray should travel + Indicates the total inactive distance in [cm] a ray should travel :distance_active: Indicates the total active distance in [cm] a ray should travel :ray_source: @@ -206,7 +218,11 @@ class Settings: default is 'False'. :sample_method: Sampling method for the ray starting location and direction of - travel. Options are `prng` (default) or 'halton`. + travel. Options are `prng` (default), `halton`, or `s2`. `s2` + modifies the `prng` sampling method such that rays are sampled + with directions (-1, 0, 0) or (1, 0, 0). This is used for verification + against analytic transport benchmarks which are often derivied with + a reduced angular domain. :source_region_meshes: List of tuples where each tuple contains a mesh and a list of domains. Each domain is an instance of openmc.Material, openmc.Cell, @@ -224,6 +240,9 @@ class Settings: stabilization, which may be desirable as stronger diagonal stabilization also tends to dampen the convergence rate of the solver, thus requiring more iterations to converge. + :adjoint_source: + Source object used to define localized adjoint source/detector response + function. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -241,8 +260,15 @@ class Settings: The type of calculation to perform (default is 'eigenvalue') seed : int Seed for the linear congruential pseudorandom number generator - stride : int - Number of random numbers allocated for each source particle history + shared_secondary_bank : bool + Whether to use a shared secondary particle bank. When enabled, + secondary particles are collected into a global bank, sorted for + reproducibility, and load-balanced across MPI ranks between + generations. If not specified, the shared secondary bank is + enabled automatically for fixed-source simulations with weight + windows active, and disabled otherwise. + + .. versionadded:: 0.15.4 source : Iterable of openmc.SourceBase Distribution of source sites in space, angle, and energy source_rejection_fraction : float @@ -262,6 +288,8 @@ class Settings: Options for writing state points. Acceptable keys are: :batches: list of batches at which to write statepoint files + stride : int + Number of random numbers allocated for each source particle history surf_source_read : dict Options for reading surface source points. Acceptable keys are: @@ -284,6 +312,14 @@ class Settings: :cellto: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles going to this declared cell will be banked (int) + surface_grazing_cutoff : float + Surface flux cosine cutoff. If not specified, the default value is + 0.001. For more information, see the surface tally section in the theory + manual. + surface_grazing_ratio : float + Surface flux cosine substitution ratio. If not specified, the default + value is 0.5. For more information, see the surface tally section in the + theory manual. survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict @@ -350,10 +386,6 @@ class Settings: .. versionadded:: 0.14.0 - create_delayed_neutrons : bool - Whether delayed neutrons are created in fission. - - .. versionadded:: 0.13.3 weight_windows_on : bool Whether weight windows are enabled @@ -389,11 +421,15 @@ class Settings: self._confidence_intervals = None self._electron_treatment = None self._photon_transport = None + self._atomic_relaxation = None self._plot_seed = None self._ptables = None + self._properties_file = None self._uniform_source_sampling = None self._seed = None self._stride = None + self._surface_grazing_cutoff = None + self._surface_grazing_ratio = None self._survival_biasing = None self._free_gas_threshold = None @@ -455,6 +491,7 @@ class Settings: self._weight_window_generators = cv.CheckedList( WeightWindowGenerator, 'weight window generators') self._weight_windows_on = None + self._shared_secondary_bank = None self._weight_windows_file = None self._weight_window_checkpoints = {} self._max_history_splits = None @@ -467,6 +504,16 @@ class Settings: for key, value in kwargs.items(): setattr(self, key, value) + def __setattr__(self, name: str, value): + if not name.startswith('_'): + try: + getattr(self, name) + except AttributeError as e: + msg, = traceback.format_exception_only(e) + msg = msg.strip().split(maxsplit=1)[-1] + warnings.warn(msg, stacklevel=2) + super().__setattr__(name, value) + @property def run_mode(self) -> str: return self._run_mode.value @@ -638,6 +685,15 @@ class Settings: electron_treatment, ['led', 'ttb']) self._electron_treatment = electron_treatment + @property + def atomic_relaxation(self) -> bool: + return self._atomic_relaxation + + @atomic_relaxation.setter + def atomic_relaxation(self, atomic_relaxation: bool): + cv.check_type('atomic relaxation', atomic_relaxation, bool) + self._atomic_relaxation = atomic_relaxation + @property def ptables(self) -> bool: return self._ptables @@ -695,6 +751,27 @@ class Settings: cv.check_greater_than('random number generator stride', stride, 0) self._stride = stride + @property + def surface_grazing_cutoff(self) -> float: + return self._surface_grazing_cutoff + + @surface_grazing_cutoff.setter + def surface_grazing_cutoff(self, surface_grazing_cutoff: float): + cv.check_type('surface grazing cutoff', surface_grazing_cutoff, float) + cv.check_greater_than('surface grazing cutoff', surface_grazing_cutoff, 0.0) + cv.check_less_than('surface grazing cutoff', surface_grazing_cutoff, 1.0) + self._surface_grazing_cutoff = surface_grazing_cutoff + + @property + def surface_grazing_ratio(self) -> float: + return self._surface_grazing_ratio + + @surface_grazing_ratio.setter + def surface_grazing_ratio(self, surface_grazing_ratio: float): + cv.check_type('surface grazing ratio', surface_grazing_ratio, float) + cv.check_greater_than('surface grazing ratio', surface_grazing_ratio, 0.0) + self._surface_grazing_ratio = surface_grazing_ratio + @property def survival_biasing(self) -> bool: return self._survival_biasing @@ -1007,6 +1084,18 @@ class Settings: self._temperature = temperature + @property + def properties_file(self) -> PathLike | None: + return self._properties_file + + @properties_file.setter + def properties_file(self, value: PathLike | None): + if value is None: + self._properties_file = None + else: + cv.check_type('properties file', value, PathLike) + self._properties_file = input_path(value) + @property def trace(self) -> Iterable: return self._trace @@ -1227,6 +1316,15 @@ class Settings: cv.check_type('weight windows on', value, bool) self._weight_windows_on = value + @property + def shared_secondary_bank(self) -> bool: + return self._shared_secondary_bank + + @shared_secondary_bank.setter + def shared_secondary_bank(self, value: bool): + cv.check_type('shared secondary bank', value, bool) + self._shared_secondary_bank = value + @property def weight_window_checkpoints(self) -> dict: return self._weight_window_checkpoints @@ -1340,11 +1438,19 @@ class Settings: 'openmc.Material, openmc.Cell, or openmc.Universe.') elif key == 'sample_method': cv.check_value('sample method', value, - ('prng', 'halton')) + ('prng', 'halton', 's2')) elif key == 'diagonal_stabilization_rho': cv.check_type('diagonal stabilization rho', value, Real) cv.check_greater_than('diagonal stabilization rho', value, 0.0, True) + elif key == 'adjoint_source': + if not isinstance(value, MutableSequence): + value = [value] + for source in value: + if not isinstance(source, SourceBase): + raise ValueError( + f'Invalid adjoint source type: {type(source)}. ' + 'Expected openmc.SourceBase.') else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1585,6 +1691,11 @@ class Settings: element = ET.SubElement(root, "electron_treatment") element.text = str(self._electron_treatment) + def _create_atomic_relaxation_subelement(self, root): + if self._atomic_relaxation is not None: + element = ET.SubElement(root, "atomic_relaxation") + element.text = str(self._atomic_relaxation).lower() + def _create_photon_transport_subelement(self, root): if self._photon_transport is not None: element = ET.SubElement(root, "photon_transport") @@ -1610,6 +1721,16 @@ class Settings: element = ET.SubElement(root, "stride") element.text = str(self._stride) + def _create_surface_grazing_cutoff_subelement(self, root): + if self._surface_grazing_cutoff is not None: + element = ET.SubElement(root, "surface_grazing_cutoff") + element.text = str(self._surface_grazing_cutoff) + + def _create_surface_grazing_ratio_subelement(self, root): + if self._surface_grazing_ratio is not None: + element = ET.SubElement(root, "surface_grazing_ratio") + element.text = str(self._surface_grazing_ratio) + def _create_survival_biasing_subelement(self, root): if self._survival_biasing is not None: element = ET.SubElement(root, "survival_biasing") @@ -1697,6 +1818,12 @@ class Settings: else: element.text = str(value) + def _create_properties_file_element(self, root): + if self.properties_file is not None: + element = ET.Element("properties_file") + element.text = str(self.properties_file) + root.append(element) + def _create_trace_subelement(self, root): if self._trace is not None: element = ET.SubElement(root, "trace") @@ -1816,6 +1943,11 @@ class Settings: elem = ET.SubElement(root, "weight_windows_on") elem.text = str(self._weight_windows_on).lower() + def _create_shared_secondary_bank_subelement(self, root): + if self._shared_secondary_bank is not None: + elem = ET.SubElement(root, "shared_secondary_bank") + elem.text = str(self._shared_secondary_bank).lower() + def _create_weight_window_generators_subelement(self, root, mesh_memo=None): if not self.weight_window_generators: return @@ -1876,8 +2008,13 @@ class Settings: element = ET.SubElement(root, "random_ray") for key, value in self._random_ray.items(): if key == 'ray_source' and isinstance(value, SourceBase): + subelement = ET.SubElement(element, 'ray_source') source_element = value.to_xml_element() - element.append(source_element) + if source_element.find('bias') is not None: + raise RuntimeError( + "Ray source distributions should not be biased.") + subelement.append(source_element) + elif key == 'source_region_meshes': subelement = ET.SubElement(element, 'source_region_meshes') for mesh, domains in value: @@ -1894,8 +2031,20 @@ class Settings: path = f"./mesh[@id='{mesh.id}']" if root.find(path) is None: root.append(mesh.to_xml_element()) - if mesh_memo is not None: + if mesh_memo is not None: mesh_memo.add(mesh.id) + elif key == 'adjoint_source': + subelement = ET.SubElement(element, 'adjoint_source') + # Check that all entries are valid SourceBase instances, in case + # the random_ray setter was not used to populate dict entries. + if not isinstance(value, MutableSequence): + value = [value] + for source in value: + if not isinstance(source, SourceBase): + raise ValueError( + f'Invalid adjoint source type: {type(source)}. ' + 'Expected openmc.SourceBase.') + subelement.append(source.to_xml_element()) elif isinstance(value, bool): subelement = ET.SubElement(element, key) subelement.text = str(value).lower() @@ -2069,6 +2218,11 @@ class Settings: if text is not None: self.electron_treatment = text + def _atomic_relaxation_from_xml_element(self, root): + text = get_text(root, 'atomic_relaxation') + if text is not None: + self.atomic_relaxation = text in ('true', '1') + def _energy_mode_from_xml_element(self, root): text = get_text(root, 'energy_mode') if text is not None: @@ -2109,6 +2263,16 @@ class Settings: if text is not None: self.stride = int(text) + def _surface_grazing_cutoff_from_xml_element(self, root): + text = get_text(root, 'surface_grazing_cutoff') + if text is not None: + self.surface_grazing_cutoff = float(text) + + def _surface_grazing_ratio_from_xml_element(self, root): + text = get_text(root, 'surface_grazing_ratio') + if text is not None: + self.surface_grazing_ratio = float(text) + def _survival_biasing_from_xml_element(self, root): text = get_text(root, 'survival_biasing') if text is not None: @@ -2190,6 +2354,11 @@ class Settings: if text is not None: self.temperature['multipole'] = text in ('true', '1') + def _properties_file_from_xml_element(self, root): + text = get_text(root, 'properties_file') + if text is not None: + self.properties_file = text + def _trace_from_xml_element(self, root): text = get_elem_list(root, "trace", int) if text is not None: @@ -2285,6 +2454,11 @@ class Settings: if text is not None: self.weight_windows_on = text in ('true', '1') + def _shared_secondary_bank_from_xml_element(self, root): + text = get_text(root, 'shared_secondary_bank') + if text is not None: + self.shared_secondary_bank = text in ('true', '1') + def _weight_windows_file_from_xml_element(self, root): text = get_text(root, 'weight_windows_file') if text is not None: @@ -2322,8 +2496,12 @@ class Settings: for child in elem: if child.tag in ('distance_inactive', 'distance_active', 'diagonal_stabilization_rho'): self.random_ray[child.tag] = float(child.text) - elif child.tag == 'source': - source = SourceBase.from_xml_element(child) + elif child.tag == 'ray_source': + source_element = child.find('source') + source = SourceBase.from_xml_element(source_element) + if child.find('bias') is not None: + raise RuntimeError( + "Ray source distributions should not be biased.") self.random_ray['ray_source'] = source elif child.tag == 'volume_estimator': self.random_ray['volume_estimator'] = child.text @@ -2337,6 +2515,12 @@ class Settings: self.random_ray['adjoint'] = ( child.text in ('true', '1') ) + elif child.tag == 'adjoint_source': + self.random_ray['adjoint_source'] = [] + for subelem in child.findall('source'): + src = SourceBase.from_xml_element(subelem) + # add newly constructed source object to the list + self.random_ray['adjoint_source'].append(src) elif child.tag == 'sample_method': self.random_ray['sample_method'] = child.text elif child.tag == 'source_region_meshes': @@ -2405,6 +2589,7 @@ class Settings: self._create_collision_track_subelement(element) self._create_confidence_intervals(element) self._create_electron_treatment_subelement(element) + self._create_atomic_relaxation_subelement(element) self._create_energy_mode_subelement(element) self._create_max_order_subelement(element) self._create_photon_transport_subelement(element) @@ -2413,6 +2598,8 @@ class Settings: self._create_ptables_subelement(element) self._create_seed_subelement(element) self._create_stride_subelement(element) + self._create_surface_grazing_cutoff_subelement(element) + self._create_surface_grazing_ratio_subelement(element) self._create_survival_biasing_subelement(element) self._create_cutoff_subelement(element) self._create_entropy_mesh_subelement(element, mesh_memo) @@ -2422,6 +2609,7 @@ class Settings: self._create_ifp_n_generation_subelement(element) self._create_tabular_legendre_subelements(element) self._create_temperature_subelements(element) + self._create_properties_file_element(element) self._create_trace_subelement(element) self._create_track_subelement(element) self._create_ufs_mesh_subelement(element, mesh_memo) @@ -2438,6 +2626,7 @@ class Settings: self._create_write_initial_source_subelement(element) self._create_weight_windows_subelement(element, mesh_memo) self._create_weight_windows_on_subelement(element) + self._create_shared_secondary_bank_subelement(element) self._create_weight_window_generators_subelement(element, mesh_memo) self._create_weight_windows_file_element(element) self._create_weight_window_checkpoints_subelement(element) @@ -2519,6 +2708,7 @@ class Settings: settings._collision_track_from_xml_element(elem) settings._confidence_intervals_from_xml_element(elem) settings._electron_treatment_from_xml_element(elem) + settings._atomic_relaxation_from_xml_element(elem) settings._energy_mode_from_xml_element(elem) settings._max_order_from_xml_element(elem) settings._photon_transport_from_xml_element(elem) @@ -2527,6 +2717,8 @@ class Settings: settings._ptables_from_xml_element(elem) settings._seed_from_xml_element(elem) settings._stride_from_xml_element(elem) + settings._surface_grazing_cutoff_from_xml_element(elem) + settings._surface_grazing_ratio_from_xml_element(elem) settings._survival_biasing_from_xml_element(elem) settings._cutoff_from_xml_element(elem) settings._entropy_mesh_from_xml_element(elem, meshes) @@ -2536,6 +2728,7 @@ class Settings: settings._ifp_n_generation_from_xml_element(elem) settings._tabular_legendre_from_xml_element(elem) settings._temperature_from_xml_element(elem) + settings._properties_file_from_xml_element(elem) settings._trace_from_xml_element(elem) settings._track_from_xml_element(elem) settings._ufs_mesh_from_xml_element(elem, meshes) @@ -2551,6 +2744,7 @@ class Settings: settings._write_initial_source_from_xml_element(elem) settings._weight_windows_from_xml_element(elem, meshes) settings._weight_windows_on_from_xml_element(elem) + settings._shared_secondary_bank_from_xml_element(elem) settings._weight_windows_file_from_xml_element(elem) settings._weight_window_generators_from_xml_element(elem, meshes) settings._weight_window_checkpoints_from_xml_element(elem) diff --git a/openmc/source.py b/openmc/source.py index 84d8a9619..5947540d6 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -1,8 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence -from enum import IntEnum -from numbers import Real +from numbers import Integral, Real from pathlib import Path import warnings from typing import Any @@ -19,6 +18,8 @@ from openmc.stats.multivariate import UnitSphere, Spatial from openmc.stats.univariate import Univariate from ._xml import get_elem_list, get_text from .mesh import MeshBase, StructuredMesh, UnstructuredMesh +from .particle_type import ParticleType +from .statepoint import _VERSION_STATEPOINT from .utility_funcs import input_path @@ -45,7 +46,7 @@ class SourceBase(ABC): Attributes ---------- - type : {'independent', 'file', 'compiled', 'mesh'} + type : {'independent', 'file', 'compiled', 'mesh', 'tokamak'} Indicator of source type. strength : float Strength of the source @@ -202,6 +203,8 @@ class SourceBase(ABC): return FileSource.from_xml_element(elem) elif source_type == 'mesh': return MeshSource.from_xml_element(elem, meshes) + elif source_type == 'tokamak': + return TokamakSource.from_xml_element(elem) else: raise ValueError( f'Source type {source_type} is not recognized') @@ -265,8 +268,8 @@ class IndependentSource(SourceBase): time distribution of source sites strength : float Strength of the source - particle : {'neutron', 'photon', 'electron', 'positron'} - Source particle type + particle : str or int or openmc.ParticleType + Source particle type (name, PDG number, or type) domains : iterable of openmc.Cell, openmc.Material, or openmc.Universe Domains to reject based on, i.e., if a sampled spatial location is not within one of these domains, it will be rejected. @@ -302,10 +305,9 @@ class IndependentSource(SourceBase): type : str Indicator of source type: 'independent' - .. versionadded:: 0.14.0 - - particle : {'neutron', 'photon', 'electron', 'positron'} - Source particle type + .. versionadded:: 0.14.0 + particle : str or int or openmc.ParticleType + Source particle type (alias, PDG number, or GNDS nuclide name) constraints : dict Constraints on sampled source particles. Valid keys include 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', @@ -320,7 +322,7 @@ class IndependentSource(SourceBase): energy: openmc.stats.Univariate | None = None, time: openmc.stats.Univariate | None = None, strength: float = 1.0, - particle: str = 'neutron', + particle: str | int | ParticleType = 'neutron', domains: Sequence[openmc.Cell | openmc.Material | openmc.Universe] | None = None, constraints: dict[str, Any] | None = None @@ -405,14 +407,12 @@ class IndependentSource(SourceBase): self._time = time @property - def particle(self): + def particle(self) -> ParticleType: return self._particle @particle.setter def particle(self, particle): - cv.check_value('source particle', particle, - ['neutron', 'photon', 'electron', 'positron']) - self._particle = particle + self._particle = ParticleType(particle) def populate_xml_element(self, element): """Add necessary source information to an XML element @@ -423,7 +423,7 @@ class IndependentSource(SourceBase): XML element containing source data """ - element.set("particle", self.particle) + element.set("particle", str(self.particle)) if self.space is not None: element.append(self.space.to_xml_element()) if self.angle is not None: @@ -572,10 +572,10 @@ class MeshSource(SourceBase): s = np.asarray(s) if isinstance(self.mesh, StructuredMesh): - if s.size != self.mesh.num_mesh_cells: + if s.size != self.mesh.n_elements: raise ValueError( f'The length of the source array ({s.size}) does not match ' - f'the number of mesh elements ({self.mesh.num_mesh_cells}).') + f'the number of mesh elements ({self.mesh.n_elements}).') # If user gave a multidimensional array, flatten in the order # of the mesh indices @@ -898,76 +898,430 @@ class FileSource(SourceBase): return cls(**kwargs) -class ParticleType(IntEnum): +class TokamakSource(SourceBase): + r"""A source representing neutron emission from a tokamak plasma. + + This source samples neutron positions from a tokamak plasma geometry using + Miller-style flux surface parameterization. The user provides an emission + profile S(r/a) as a function of normalized minor radius, along with one or + more energy distributions. + + The flux surface parameterization is + + .. math:: + + \begin{aligned} + R &= R_0 + r \cos\left(\alpha + \delta \sin\alpha\right) + + \Delta \left[1 - \left(\frac{r}{a}\right)^2\right] \\ + Z &= Z_\mathrm{shift} + \kappa r \sin\alpha + \end{aligned} + + where :math:`R_0` is major radius, :math:`a` is minor radius, + :math:`\kappa` is elongation, :math:`\delta` is triangularity, + :math:`\Delta` is the Shafranov shift, and :math:`Z_\mathrm{shift}` is + the vertical shift. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + major_radius : float + Major radius R0 in [cm] + minor_radius : float + Minor radius a in [cm] + elongation : float + Plasma elongation κ (must be > 0) + triangularity : float + Plasma triangularity δ (must be in [-1, 1]) + shafranov_shift : float + Shafranov shift Δ in [cm] (must be >= 0 and < a/2) + r_over_a : numpy.ndarray + Normalized minor radius grid points, must start at 0 and end at 1 + emission_density : numpy.ndarray + Emission density S(r) at each r/a point (arbitrary units, must be >= 0). + Values are linearly interpolated between grid points and refined on an + internal grid for radial sampling. Must have the same length as + ``r_over_a`` and contain at least one positive value. + energy : openmc.stats.Univariate or Sequence[openmc.stats.Univariate] + Energy distribution(s). Either a single distribution used at all radii, + or one distribution per ``r_over_a`` grid point. When one distribution + per grid point is given, the energy of a sampled particle is drawn from + one of the two distributions bracketing its sampled radius, selected + stochastically with probability proportional to the proximity of the + radius to each grid point (stochastic interpolation). + time : openmc.stats.Univariate, optional + Time distribution of the source. If None, particles are born at + :math:`t=0`, matching the default behavior of + :class:`openmc.IndependentSource`. + phi_start : float + Starting toroidal angle in [rad] (default: 0) + phi_extent : float + Toroidal angle extent in [rad] (default: 2π) + n_alpha : int + Number of poloidal angle grid points for CDF sampling (default: 101) + vertical_shift : float + Vertical shift of the plasma center in [cm] (default: 0) + strength : float + Strength of the source (default: 1.0) + constraints : dict + Constraints on sampled source particles. See :class:`SourceBase` for + valid keys and values. + + Attributes + ---------- + major_radius : float + Major radius R0 in [cm] + minor_radius : float + Minor radius a in [cm] + elongation : float + Plasma elongation κ + triangularity : float + Plasma triangularity δ + shafranov_shift : float + Shafranov shift Δ in [cm] + r_over_a : numpy.ndarray + Normalized minor radius grid points + emission_density : numpy.ndarray + Emission density S(r) at each r/a point + energy : list of openmc.stats.Univariate + Energy distribution(s) + time : openmc.stats.Univariate or None + Time distribution of the source + phi_start : float + Starting toroidal angle in [rad] + phi_extent : float + Toroidal angle extent in [rad] + n_alpha : int + Number of poloidal angle grid points + vertical_shift : float + Vertical shift of the plasma center in [cm] + strength : float + Strength of the source + type : str + Indicator of source type: 'tokamak' + constraints : dict + Constraints on sampled source particles + """ - IntEnum class representing a particle type. Type - values mirror those found in the C++ class. - """ - NEUTRON = 0 - PHOTON = 1 - ELECTRON = 2 - POSITRON = 3 - @classmethod - def from_string(cls, value: str): - """ - Constructs a ParticleType instance from a string. + def __init__( + self, + major_radius: float, + minor_radius: float, + elongation: float, + triangularity: float, + shafranov_shift: float, + r_over_a: Sequence[float], + emission_density: Sequence[float], + energy: Univariate | Sequence[Univariate], + time: Univariate | None = None, + phi_start: float = 0.0, + phi_extent: float = 2.0 * np.pi, + n_alpha: int = 101, + vertical_shift: float = 0.0, + strength: float = 1.0, + constraints: dict[str, Any] | None = None + ): + super().__init__(strength=strength, constraints=constraints) + self.major_radius = major_radius + self.minor_radius = minor_radius + self.elongation = elongation + self.triangularity = triangularity + self.shafranov_shift = shafranov_shift + self.r_over_a = r_over_a + self.emission_density = emission_density + self.phi_start = phi_start + self.phi_extent = phi_extent + self.n_alpha = n_alpha + self.vertical_shift = vertical_shift + self.energy = energy + self.time = time - Parameters - ---------- - value : str - The string representation of the particle type. + self._validate() - Returns - ------- - The corresponding ParticleType instance. - """ - try: - return cls[value.upper()] - except KeyError: + def _validate(self): + """Validate relationships between tokamak source parameters.""" + if self.minor_radius >= self.major_radius: raise ValueError( - f"Invalid string for creation of {cls.__name__}: {value}") + f"minor_radius ({self.minor_radius}) must be smaller than " + f"major_radius ({self.major_radius})") + if self.shafranov_shift >= 0.5 * self.minor_radius: + raise ValueError( + f"shafranov_shift ({self.shafranov_shift}) must be smaller " + f"than half the minor_radius ({0.5 * self.minor_radius})") + if len(self.emission_density) != len(self.r_over_a): + raise ValueError( + f"emission_density (length {len(self.emission_density)}) must " + f"have the same length as r_over_a (length {len(self.r_over_a)})") + if not np.any(self.emission_density > 0.0): + raise ValueError("emission_density must contain a positive value") + if len(self.energy) not in (1, len(self.r_over_a)): + raise ValueError( + f"Number of energy distributions ({len(self.energy)}) must be " + f"either 1 or equal to the number of r_over_a grid points " + f"({len(self.r_over_a)})") - @classmethod - def from_pdg_number(cls, pdg_number: int) -> ParticleType: - """Constructs a ParticleType instance from a PDG number. + @property + def type(self) -> str: + return "tokamak" - The Particle Data Group at LBNL publishes a Monte Carlo particle - numbering scheme as part of the `Review of Particle Physics - <10.1103/PhysRevD.110.030001>`_. This method maps PDG numbers to the - corresponding :class:`ParticleType`. + @property + def major_radius(self) -> float: + return self._major_radius - Parameters - ---------- - pdg_number : int - The PDG number of the particle type. + @major_radius.setter + def major_radius(self, value: float): + cv.check_type('major radius', value, Real) + cv.check_greater_than('major radius', value, 0.0) + self._major_radius = value + + @property + def minor_radius(self) -> float: + return self._minor_radius + + @minor_radius.setter + def minor_radius(self, value: float): + cv.check_type('minor radius', value, Real) + cv.check_greater_than('minor radius', value, 0.0) + self._minor_radius = value + + @property + def elongation(self) -> float: + return self._elongation + + @elongation.setter + def elongation(self, value: float): + cv.check_type('elongation', value, Real) + cv.check_greater_than('elongation', value, 0.0) + self._elongation = value + + @property + def triangularity(self) -> float: + return self._triangularity + + @triangularity.setter + def triangularity(self, value: float): + cv.check_type('triangularity', value, Real) + cv.check_greater_than('triangularity', value, -1.0, equality=True) + cv.check_less_than('triangularity', value, 1.0, equality=True) + self._triangularity = value + + @property + def shafranov_shift(self) -> float: + return self._shafranov_shift + + @shafranov_shift.setter + def shafranov_shift(self, value: float): + cv.check_type('Shafranov shift', value, Real) + cv.check_greater_than('Shafranov shift', value, 0.0, equality=True) + self._shafranov_shift = value + + @property + def r_over_a(self) -> np.ndarray: + return self._r_over_a + + @r_over_a.setter + def r_over_a(self, value: Sequence[float]): + value = np.asarray(value, dtype=float) + if value.ndim != 1 or len(value) < 2: + raise ValueError("r_over_a must be a 1-D array with at least 2 points") + if value[0] != 0.0: + raise ValueError("r_over_a must start at 0") + if value[-1] != 1.0: + raise ValueError("r_over_a must end at 1") + if not np.all(np.diff(value) > 0): + raise ValueError("r_over_a must be strictly increasing") + self._r_over_a = value + + @property + def emission_density(self) -> np.ndarray: + return self._emission_density + + @emission_density.setter + def emission_density(self, value: Sequence[float]): + value = np.asarray(value, dtype=float) + if value.ndim != 1: + raise ValueError("emission_density must be a 1-D array") + if np.any(value < 0): + raise ValueError("emission_density values cannot be negative") + self._emission_density = value + + @property + def energy(self) -> list[Univariate]: + return self._energy + + @energy.setter + def energy(self, value: Univariate | Sequence[Univariate]): + if isinstance(value, Univariate): + self._energy = [value] + else: + cv.check_iterable_type('energy distributions', value, Univariate) + self._energy = list(value) + + @property + def time(self) -> Univariate | None: + return self._time + + @time.setter + def time(self, value: Univariate | None): + if value is not None: + cv.check_type('time distribution', value, Univariate) + self._time = value + + @property + def phi_start(self) -> float: + return self._phi_start + + @phi_start.setter + def phi_start(self, value: float): + cv.check_type('phi_start', value, Real) + self._phi_start = value + + @property + def phi_extent(self) -> float: + return self._phi_extent + + @phi_extent.setter + def phi_extent(self, value: float): + cv.check_type('phi_extent', value, Real) + cv.check_greater_than('phi_extent', value, 0.0) + cv.check_less_than('phi_extent', value, 2.0 * np.pi, equality=True) + self._phi_extent = value + + @property + def n_alpha(self) -> int: + return self._n_alpha + + @n_alpha.setter + def n_alpha(self, value: int): + cv.check_type('n_alpha', value, Integral) + cv.check_greater_than('n_alpha', value, 2) + if value < 51: + warnings.warn( + "n_alpha values below 51 may introduce noticeable " + "discretization bias in tokamak source sampling", stacklevel=2) + self._n_alpha = value + + @property + def vertical_shift(self) -> float: + return self._vertical_shift + + @vertical_shift.setter + def vertical_shift(self, value: float): + cv.check_type('vertical shift', value, Real) + self._vertical_shift = value + + def populate_xml_element(self, element): + """Add necessary tokamak source information to an XML element Returns ------- - The corresponding ParticleType instance. - """ - try: - return { - 2112: ParticleType.NEUTRON, - 22: ParticleType.PHOTON, - 11: ParticleType.ELECTRON, - -11: ParticleType.POSITRON, - }[pdg_number] - except KeyError: - raise ValueError(f"Unrecognized PDG number: {pdg_number}") + element : lxml.etree._Element + XML element containing source data - def __repr__(self) -> str: """ - Returns a string representation of the ParticleType instance. + self._validate() + + # Geometry parameters + ET.SubElement(element, "major_radius").text = str(self.major_radius) + ET.SubElement(element, "minor_radius").text = str(self.minor_radius) + ET.SubElement(element, "elongation").text = str(self.elongation) + ET.SubElement(element, "triangularity").text = str(self.triangularity) + ET.SubElement(element, "shafranov_shift").text = str(self.shafranov_shift) + + # Toroidal angle bounds + ET.SubElement(element, "phi_start").text = str(self.phi_start) + ET.SubElement(element, "phi_extent").text = str(self.phi_extent) + + # Poloidal sampling resolution + ET.SubElement(element, "n_alpha").text = str(self.n_alpha) + + # Vertical shift + if self.vertical_shift != 0.0: + ET.SubElement(element, "vertical_shift").text = str(self.vertical_shift) + + # Emission profile + ET.SubElement(element, "r_over_a").text = ' '.join(str(r) for r in self.r_over_a) + ET.SubElement(element, "emission_density").text = ' '.join(str(s) for s in self.emission_density) + + # Energy distribution(s) + for dist in self.energy: + element.append(dist.to_xml_element('energy')) + + # Time distribution + if self.time is not None: + element.append(self.time.to_xml_element('time')) + + @classmethod + def from_xml_element(cls, elem: ET.Element) -> TokamakSource: + """Generate tokamak source from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.TokamakSource + Source generated from XML element - Returns: - str: The lowercase name of the ParticleType instance. """ - return self.name.lower() + # Read geometry parameters + major_radius = float(get_text(elem, 'major_radius')) + minor_radius = float(get_text(elem, 'minor_radius')) + elongation = float(get_text(elem, 'elongation')) + triangularity = float(get_text(elem, 'triangularity')) + shafranov_shift = float(get_text(elem, 'shafranov_shift')) - # needed for < Python 3.11 - def __str__(self) -> str: - return self.__repr__() + # Read optional parameters + phi_start_text = get_text(elem, 'phi_start') + phi_start = float(phi_start_text) if phi_start_text else 0.0 + + phi_extent_text = get_text(elem, 'phi_extent') + phi_extent = float(phi_extent_text) if phi_extent_text else 2.0 * np.pi + + n_alpha_text = get_text(elem, 'n_alpha') + n_alpha = int(n_alpha_text) if n_alpha_text else 101 + + vertical_shift_text = get_text(elem, 'vertical_shift') + vertical_shift = float(vertical_shift_text) if vertical_shift_text else 0.0 + + # Read emission profile + r_over_a = np.array([float(x) for x in get_text(elem, 'r_over_a').split()]) + emission_density = np.array([float(x) for x in get_text(elem, 'emission_density').split()]) + + # Read energy distributions + energy = [Univariate.from_xml_element(e) for e in elem.findall('energy')] + if len(energy) == 1: + energy = energy[0] + + # Read time distribution + time_elem = elem.find('time') + time = Univariate.from_xml_element(time_elem) if time_elem is not None else None + + # Read constraints and strength + constraints = cls._get_constraints(elem) + strength_text = get_text(elem, 'strength') + strength = float(strength_text) if strength_text else 1.0 + + return cls( + major_radius=major_radius, + minor_radius=minor_radius, + elongation=elongation, + triangularity=triangularity, + shafranov_shift=shafranov_shift, + r_over_a=r_over_a, + emission_density=emission_density, + energy=energy, + time=time, + phi_start=phi_start, + phi_extent=phi_extent, + n_alpha=n_alpha, + vertical_shift=vertical_shift, + strength=strength, + constraints=constraints + ) class SourceParticle: @@ -992,8 +1346,8 @@ class SourceParticle: Delayed group particle was created in (neutrons only) surf_id : int Surface ID where particle is at, if any. - particle : ParticleType - Type of the particle + particle : ParticleType or str or int + Type of the particle (type, name, or PDG number) """ @@ -1006,7 +1360,7 @@ class SourceParticle: wgt: float = 1.0, delayed_group: int = 0, surf_id: int = 0, - particle: ParticleType = ParticleType.NEUTRON + particle: ParticleType | str | int = ParticleType.NEUTRON ): self.r = tuple(r) @@ -1018,9 +1372,16 @@ class SourceParticle: self.surf_id = surf_id self.particle = particle + @property + def particle(self) -> ParticleType: + return self._particle + + @particle.setter + def particle(self, particle): + self._particle = ParticleType(particle) + def __repr__(self): - name = self.particle.name.lower() - return f'' + return f'' def to_tuple(self) -> tuple: """Return source particle attributes as a tuple @@ -1032,7 +1393,7 @@ class SourceParticle: """ return (self.r, self.u, self.E, self.time, self.wgt, - self.delayed_group, self.surf_id, self.particle.value) + self.delayed_group, self.surf_id, self.particle.pdg_number) def write_source_file( @@ -1116,12 +1477,7 @@ class ParticleList(list): particles = [] with mcpl.MCPLFile(filename) as f: for particle in f.particles: - # Determine particle type based on the PDG number - try: - particle_type = ParticleType.from_pdg_number( - particle.pdgcode) - except ValueError: - particle_type = "UNKNOWN" + particle_type = ParticleType(particle.pdgcode) # Create a source particle instance. Note that MCPL stores # energy in MeV and time in ms. @@ -1179,7 +1535,7 @@ class ParticleList(list): # Extract the attributes of the source particles into a list of tuples data = [(sp.r[0], sp.r[1], sp.r[2], sp.u[0], sp.u[1], sp.u[2], sp.E, sp.time, sp.wgt, sp.delayed_group, sp.surf_id, - sp.particle.name.lower()) for sp in self] + str(sp.particle)) for sp in self] # Define the column names for the DataFrame columns = ['x', 'y', 'z', 'u_x', 'u_y', 'u_z', 'E', 'time', 'wgt', @@ -1226,6 +1582,7 @@ class ParticleList(list): kwargs.setdefault('mode', 'w') with h5py.File(filename, **kwargs) as fh: fh.attrs['filetype'] = np.bytes_("source") + fh.attrs['version'] = np.array([_VERSION_STATEPOINT, 2]) fh.create_dataset('source_bank', data=arr, dtype=source_dtype) @@ -1337,7 +1694,7 @@ def read_collision_track_mcpl(file_path): data['material_id'].append(int(values_dict.get('material_id', 0))) data['universe_id'].append(int(values_dict.get('universe_id', 0))) data['n_collision'].append(int(values_dict.get('n_collision', 0))) - data['particle'].append(ParticleType.from_pdg_number(p.pdgcode)) + data['particle'].append(ParticleType(p.pdgcode)) data['parent_id'].append(int(values_dict.get('parent_id', 0))) data['progeny_id'].append(int(values_dict.get('progeny_id', 0))) diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 11986841f..a10ec3a83 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -723,7 +723,7 @@ class StatePoint: cell = cells[cell_id] if not cell._paths: summary.geometry.determine_paths() - tally_filter.paths = cell.paths + tally_filter._paths = cell.paths self._summary = summary diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 1ce998758..d48697037 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -12,7 +12,7 @@ import openmc import openmc.checkvalue as cv from .._xml import get_elem_list, get_text from ..mesh import MeshBase -from .univariate import PowerLaw, Uniform, Univariate +from .univariate import PowerLaw, Uniform, Univariate, delta_function class UnitSphere(ABC): @@ -81,7 +81,7 @@ class PolarAzimuthal(UnitSphere): z-direction. reference_vwu : Iterable of float Direction from which azimuthal angle is measured. Defaults to the positive - x-direction. + x-direction. Attributes ---------- @@ -104,7 +104,7 @@ class PolarAzimuthal(UnitSphere): self.phi = phi else: self.phi = Uniform(0., 2*pi) - + @property def reference_vwu(self): return self._reference_vwu @@ -114,8 +114,8 @@ class PolarAzimuthal(UnitSphere): cv.check_type('reference v direction', vwu, Iterable, Real) vwu = np.asarray(vwu) uvw = self.reference_uvw - cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu)) - vwu -= vwu.dot(uvw)*uvw + cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu)) + vwu -= vwu.dot(uvw)*uvw cv.check_less_than('reference v direction must be orthogonal to reference u direction', np.abs(vwu.dot(uvw)), 1e-6) self._reference_vwu = vwu/np.linalg.norm(vwu) @@ -137,21 +137,30 @@ class PolarAzimuthal(UnitSphere): cv.check_type('azimuthal angle', phi, Univariate) self._phi = phi - def to_xml_element(self): + def to_xml_element(self, element_name: str = None): """Return XML representation of the angular distribution + Parameters + ---------- + element_name : str, optional + XML element name + Returns ------- element : lxml.etree._Element XML element containing angular distribution data """ - element = ET.Element('angle') + if element_name is not None: + element = ET.Element(element_name) + else: + element = ET.Element('angle') + element.set("type", "mu-phi") if self.reference_uvw is not None: element.set("reference_uvw", ' '.join(map(str, self.reference_uvw))) if self.reference_vwu is not None: - element.set("reference_vwu", ' '.join(map(str, self.reference_vwu))) + element.set("reference_vwu", ' '.join(map(str, self.reference_vwu))) element.append(self.mu.to_xml_element('mu')) element.append(self.phi.to_xml_element('phi')) return element @@ -177,17 +186,48 @@ class PolarAzimuthal(UnitSphere): mu_phi.reference_uvw = uvw vwu = get_elem_list(elem, "reference_vwu", float) if vwu is not None: - mu_phi.reference_vwu = vwu + mu_phi.reference_vwu = vwu mu_phi.mu = Univariate.from_xml_element(elem.find('mu')) mu_phi.phi = Univariate.from_xml_element(elem.find('phi')) return mu_phi class Isotropic(UnitSphere): - """Isotropic angular distribution.""" + """Isotropic angular distribution. - def __init__(self): + Parameters + ---------- + bias : openmc.stats.PolarAzimuthal, optional + Distribution for biased sampling. + + Attributes + ---------- + bias : openmc.stats.PolarAzimuthal or None + Distribution for biased sampling + + """ + + def __init__(self, bias: PolarAzimuthal | None = None): super().__init__() + self.bias = bias + + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, bias): + cv.check_type('Biasing distribution', bias, PolarAzimuthal, none_ok=True) + if bias is not None: + if (bias.mu.bias is not None) or (bias.phi.bias is not None): + raise RuntimeError('Biasing distributions should not have their own bias.') + elif (bias.mu.support != (-1., 1.) + or not np.all(np.isclose(bias.phi.support, (0., 2*np.pi)))): + raise ValueError("Biasing distribution for an isotropic " + "distribution should be supported on " + "mu=(-1.0,1.0) and phi=(0.0,2*pi).") + + self._bias = bias def to_xml_element(self): """Return XML representation of the isotropic distribution @@ -200,6 +240,15 @@ class Isotropic(UnitSphere): """ element = ET.Element('angle') element.set("type", "isotropic") + + if self.bias is not None: + bias_dist = self.bias + if (bias_dist.mu.bias is not None) or (bias_dist.phi.bias is not None): + raise RuntimeError('Biasing distributions should not have their own bias!') + else: + bias_elem = self.bias.to_xml_element("bias") + element.append(bias_elem) + return element @classmethod @@ -217,7 +266,13 @@ class Isotropic(UnitSphere): Isotropic distribution generated from XML element """ - return cls() + bias_elem = elem.find('bias') + if bias_elem is not None: + bias_dist = PolarAzimuthal.from_xml_element(bias_elem) + return cls(bias=bias_dist) + else: + return cls() + class Monodirectional(UnitSphere): @@ -555,6 +610,10 @@ class CylindricalIndependent(Spatial): origin: Iterable of float, optional coordinates (x0, y0, z0) of the center of the cylindrical reference frame. Defaults to (0.0, 0.0, 0.0) + r_dir : Iterable of float, optional + Unit vector of the cylinder r axis at phi=0. + z_dir : Iterable of float, optional + Unit vector of the cylinder z axis direction. Attributes ---------- @@ -568,14 +627,21 @@ class CylindricalIndependent(Spatial): origin: Iterable of float, optional coordinates (x0, y0, z0) of the center of the cylindrical reference frame. Defaults to (0.0, 0.0, 0.0) + r_dir : Iterable of float, optional + Unit vector of the cylinder r axis at phi=0. + z_dir : Iterable of float, optional + Unit vector of the cylinder z axis direction. """ - def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0)): + def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0), r_dir=(1.0, 0.0, 0.0), + z_dir=(0.0, 0.0, 1.0)): self.r = r self.phi = phi self.z = z self.origin = origin + self.z_dir = z_dir + self.r_dir = r_dir @property def r(self): @@ -614,6 +680,33 @@ class CylindricalIndependent(Spatial): origin = np.asarray(origin) self._origin = origin + @property + def z_dir(self): + return self._z_dir + + @z_dir.setter + def z_dir(self, z_dir): + cv.check_type('z-axis direction', z_dir, Iterable, Real) + z_dir = np.array(z_dir) + norm = np.linalg.norm(z_dir) + cv.check_greater_than('z-axis direction magnitude', norm, 0.0) + z_dir /= norm + self._z_dir = z_dir + + @property + def r_dir(self): + return self._r_dir + + @r_dir.setter + def r_dir(self, r_dir): + cv.check_type('r-axis direction', r_dir, Iterable, Real) + r_dir = np.array(r_dir) + r_dir -= np.dot(r_dir, self.z_dir) * self.z_dir + norm = np.linalg.norm(r_dir) + cv.check_greater_than('r-axis direction magnitude', norm, 0.0) + r_dir /= norm + self._r_dir = r_dir + def to_xml_element(self): """Return XML representation of the spatial distribution @@ -628,7 +721,12 @@ class CylindricalIndependent(Spatial): element.append(self.r.to_xml_element('r')) element.append(self.phi.to_xml_element('phi')) element.append(self.z.to_xml_element('z')) - element.set("origin", ' '.join(map(str, self.origin))) + if not np.allclose(self.origin, [0., 0., 0.]): + element.set("origin", ' '.join(map(str, self.origin))) + if not np.allclose(self.r_dir, [1., 0., 0.]): + element.set("r_dir", ' '.join(map(str, self.r_dir))) + if not np.allclose(self.z_dir, [0., 0., 1.]): + element.set("z_dir", ' '.join(map(str, self.z_dir))) return element @classmethod @@ -649,8 +747,10 @@ class CylindricalIndependent(Spatial): r = Univariate.from_xml_element(elem.find('r')) phi = Univariate.from_xml_element(elem.find('phi')) z = Univariate.from_xml_element(elem.find('z')) - origin = get_elem_list(elem, "origin", float) - return cls(r, phi, z, origin=origin) + origin = get_elem_list(elem, "origin", float) or [0.0, 0.0, 0.0] + r_dir = get_elem_list(elem, "r_dir", float) or [1.0, 0.0, 0.0] + z_dir = get_elem_list(elem, "z_dir", float) or [0.0, 0.0, 1.0] + return cls(r, phi, z, origin=origin, r_dir=r_dir, z_dir=z_dir) class MeshSpatial(Spatial): @@ -672,6 +772,9 @@ class MeshSpatial(Spatial): volume_normalized : bool, optional Whether or not the strengths will be multiplied by element volumes at runtime. Default is True. + bias : iterable of float, optional + An iterable of values giving the selection weights assigned to each + element during biased sampling. Attributes ---------- @@ -682,12 +785,16 @@ class MeshSpatial(Spatial): volume_normalized : bool Whether or not the strengths will be multiplied by element volumes at runtime. + bias : numpy.ndarray or None + Distribution for biased sampling """ - def __init__(self, mesh, strengths=None, volume_normalized=True): + def __init__(self, mesh, strengths=None, volume_normalized=True, + bias: Sequence[float] | None = None): self.mesh = mesh self.strengths = strengths self.volume_normalized = volume_normalized + self.bias = bias @property def mesh(self): @@ -720,6 +827,23 @@ class MeshSpatial(Spatial): else: self._strengths = None + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, given_bias): + if given_bias is not None: + cv.check_type('Biasing strengths array', given_bias, Iterable, Real) + bias_array = np.asarray(given_bias, dtype=float).flatten() + if bias_array.size != self.strengths.size: + raise ValueError( + 'Bias strengths array must have same size as strengths array.') + else: + self._bias = bias_array + else: + self._bias = None + @property def num_strength_bins(self): if self.strengths is None: @@ -745,6 +869,9 @@ class MeshSpatial(Spatial): subelement = ET.SubElement(element, 'strengths') subelement.text = ' '.join(str(e) for e in self.strengths) + if self.bias is not None: + Univariate._append_array_bias_to_xml(self, element) + return element @classmethod @@ -774,7 +901,8 @@ class MeshSpatial(Spatial): volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true' strengths = get_elem_list(elem, 'strengths', float) - return cls(meshes[mesh_id], strengths, volume_normalized) + bias_strengths = Univariate._read_array_bias_from_xml(elem) + return cls(meshes[mesh_id], strengths, volume_normalized, bias=bias_strengths) class PointCloud(Spatial): @@ -792,6 +920,9 @@ class PointCloud(Spatial): strengths : iterable of float, optional An iterable of values that represents the relative probabilty of each point. + bias : iterable of float, optional + An iterable of values representing the relative probability of each + point under biased sampling. Attributes ---------- @@ -799,15 +930,19 @@ class PointCloud(Spatial): The points in space to be sampled with shape (N, 3) strengths : numpy.ndarray or None An array of relative probabilities for each mesh point + bias : numpy.ndarray or None + An array of relative probabilities for biased sampling of mesh points """ def __init__( self, positions: Sequence[Sequence[float]], - strengths: Sequence[float] | None = None + strengths: Sequence[float] | None = None, + bias: Sequence[float] | None = None ): self.positions = positions self.strengths = strengths + self.bias = bias @property def positions(self) -> np.ndarray: @@ -836,6 +971,23 @@ class PointCloud(Spatial): raise ValueError('strengths must have the same length as positions') self._strengths = strengths + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, given_bias): + if given_bias is not None: + cv.check_type('Biasing strengths array', given_bias, Iterable, Real) + bias_array = np.asarray(given_bias, dtype=float).flatten() + if bias_array.size != self.strengths.size: + raise ValueError( + 'Bias strengths array must have same size as strengths array.') + else: + self._bias = bias_array + else: + self._bias = None + @property def num_strength_bins(self) -> int: if self.strengths is None: @@ -861,6 +1013,9 @@ class PointCloud(Spatial): subelement = ET.SubElement(element, 'strengths') subelement.text = ' '.join(str(e) for e in self.strengths) + if self.bias is not None: + Univariate._append_array_bias_to_xml(self, element) + return element @classmethod @@ -883,8 +1038,8 @@ class PointCloud(Spatial): positions = np.array(coord_data).reshape((-1, 3)) strengths = get_elem_list(elem, 'strengths', float) - - return cls(positions, strengths) + bias_strengths = Univariate._read_array_bias_from_xml(elem) + return cls(positions, strengths, bias=bias_strengths) class Box(Spatial): @@ -1109,3 +1264,49 @@ def spherical_uniform( phis_dist = Uniform(phis[0], phis[1]) return SphericalIndependent(r_dist, cos_thetas_dist, phis_dist, origin) + + +def cylindrical_uniform( + r_outer: float, + height: float, + r_inner: float = 0.0, + phis: Sequence[float] = (0., 2*pi), + **kwargs, +): + """Return a uniform spatial distribution over a cylindrical shell. + + This function provides a uniform spatial distribution over a cylindrical + shell between `r_inner` and `r_outer`. When `height` is zero, a delta + function is used for the z-distribution, giving a uniform distribution over + a flat ring (annulus) at z=0 in the local coordinate frame. Optionally, the + range of angles can be restricted by the `phis` argument. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + r_outer : float + Outer radius of the cylindrical shell in [cm] + height : float + Height of the cylindrical shell in [cm]. When 0, the distribution is a + flat ring at z=0 in the local frame. + r_inner : float + Inner radius of the cylindrical shell in [cm] + phis : iterable of float + Starting and ending phi coordinates (azimuthal angle) in radians in a + reference frame centered at `origin`. + **kwargs + Keyword arguments passed directly to + :class:`~openmc.stats.CylindricalIndependent` (e.g., ``origin``, + ``r_dir``, ``z_dir``). + + Returns + ------- + openmc.stats.CylindricalIndependent + Uniform distribution over the cylindrical shell + """ + + r_dist = PowerLaw(r_inner, r_outer, 1) + phis_dist = Uniform(phis[0], phis[1]) + z_dist = delta_function(0.0) if height == 0.0 else Uniform(-height/2, height/2) + return CylindricalIndependent(r_dist, phis_dist, z_dist, **kwargs) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index c48cc0075..1b18bcb17 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1,17 +1,22 @@ from __future__ import annotations -import math from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable, Sequence -from copy import deepcopy +from functools import cache +from math import sqrt, pi, exp, log from numbers import Real +from pathlib import Path from warnings import warn import lxml.etree as ET import numpy as np from scipy.integrate import trapezoid +from scipy.special import exprel, hyp1f1, lambertw +import scipy import openmc.checkvalue as cv +from openmc.data import atomic_mass, NEUTRON_MASS +import openmc.data from .._xml import get_elem_list, get_text from ..mixin import EqualityMixin @@ -24,13 +29,71 @@ _INTERPOLATION_SCHEMES = { } +def exprel2(x): + """Evaluate 2*(exp(x)-1-x)/x^2 without loss of precision near 0""" + return hyp1f1(1, 3, x) + + +def log1prel(x): + """Evaluate log(1+x)/x without loss of precision near 0""" + return np.where(np.abs(x) < 1e-16, 1.0, np.log1p(x) / x) + + class Univariate(EqualityMixin, ABC): """Probability distribution of a single random variable. The Univariate class is an abstract class that can be derived to implement a specific probability distribution. + Parameters + ---------- + bias : Iterable of float, optional + Distribution or discrete probabilities for biased sampling or discrete + probabilities for biased sampling. + """ + def __init__(self, bias: Univariate | Sequence[float] | None = None): + self.bias = bias + + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, bias): + check_bias_support(self, bias) + self._bias = bias + + def _append_bias_to_xml(self, element: ET.Element) -> None: + """Append bias distribution element to XML if present.""" + if self.bias is not None: + if self.bias.bias is not None: + raise RuntimeError('Biasing distributions should not have their own bias.') + bias_elem = self.bias.to_xml_element("bias") + element.append(bias_elem) + + @classmethod + def _read_bias_from_xml(cls, elem: ET.Element): + """Read bias distribution from XML element if present.""" + bias_elem = elem.find('bias') + if bias_elem is not None: + return Univariate.from_xml_element(bias_elem) + return None + + def _append_array_bias_to_xml(self, element: ET.Element) -> None: + """Append array-based bias probabilities to XML.""" + if self.bias is not None: + bias_elem = ET.SubElement(element, "bias") + bias_elem.text = ' '.join(map(str, self.bias)) + + @classmethod + def _read_array_bias_from_xml(cls, elem: ET.Element): + """Read array-based bias probabilities from XML.""" + bias_elem = elem.find('bias') + if bias_elem is not None: + return get_elem_list(elem, "bias", float) + return None + @abstractmethod def to_xml_element(self, element_name): return '' @@ -64,10 +127,12 @@ class Univariate(EqualityMixin, ABC): return Legendre.from_xml_element(elem) elif distribution == 'mixture': return Mixture.from_xml_element(elem) + elif distribution == 'decay_spectrum': + return DecaySpectrum.from_xml_element(elem) @abstractmethod - def sample(n_samples: int = 1, seed: int | None = None): - """Sample the univariate distribution + def _sample_unbiased(self, n_samples: int = 1, seed: int | None = None): + """Sample without bias handling. Parameters ---------- @@ -79,10 +144,35 @@ class Univariate(EqualityMixin, ABC): Returns ------- numpy.ndarray - A 1-D array of sampled values + The array of sampled values """ pass + def sample(self, n_samples: int = 1, seed: int | None = None): + """Sample the univariate distribution, handling biasing automatically. + + Parameters + ---------- + n_samples : int + Number of sampled values to generate + seed : int or None + Initial random number seed. + + Returns + ------- + tuple of numpy.ndarray + A tuple of (samples, weights) + """ + if self.bias is None: + x = self._sample_unbiased(n_samples, seed) + return x, np.ones_like(x) + else: + if self.bias.bias is not None: + raise RuntimeError('Biasing distributions should not have their own bias.') + x, _ = self.bias.sample(n_samples=n_samples, seed=seed) + weight = self.evaluate(x) / self.bias.evaluate(x) + return x, weight + def integral(self): """Return integral of distribution @@ -95,6 +185,36 @@ class Univariate(EqualityMixin, ABC): """ return 1.0 + @abstractmethod + def evaluate(self, x: float | Sequence[float]): + """Evaluate the probability density at the provided value. + + Parameters + ---------- + x : float or sequence of float + Location to evaluate p(x) + + Returns + ------- + float or numpy.ndarray + Value of p(x) + """ + pass + + @property + @abstractmethod + def support(self): + """Return the support of the probability distribution. + + Returns + ------- + set or tuple of float or dict + Returns the set of unique points assigned probability mass in a + discrete distribution, the sampling interval for a continuous + distribution, or a dictionary storing the discrete and continuous + parts of the support of a mixed random variable + """ + pass def _intensity_clip(intensity: Sequence[float], tolerance: float = 1e-6) -> np.ndarray: """Clip low-importance points from an array of intensities. @@ -148,6 +268,9 @@ class Discrete(Univariate): Values of the random variable p : Iterable of float Discrete probability for each value + bias : Iterable of float, optional + Alternative discrete probabilities for biased sampling. Defaults to + None for unbiased sampling. Attributes ---------- @@ -155,12 +278,18 @@ class Discrete(Univariate): Values of the random variable p : numpy.ndarray Discrete probability for each value + support : set + Values of the random variable over which the distribution is + nonzero-valued + bias : numpy.ndarray or None + Discrete probabilities for biased sampling """ - def __init__(self, x, p): + def __init__(self, x, p, bias=None): self.x = x self.p = p + super().__init__(bias) def __len__(self): return len(self.x) @@ -189,10 +318,42 @@ class Discrete(Univariate): cv.check_greater_than('discrete probability', pk, 0.0, True) self._p = np.array(p, dtype=float) + @property + def support(self): + return set(np.unique(self._x)) + + @Univariate.bias.setter + def bias(self, bias): + if bias is None: + self._bias = bias + else: + if isinstance(bias, Real): + bias = [bias] + cv.check_type('discrete bias probabilities', bias, Iterable, Real) + for bk in bias: + cv.check_greater_than('discrete probability', bk, 0.0, True) + if len(bias) != len(self.x): + raise RuntimeError("Discrete distribution has unequal number of " + "biased and unbiased probability entries.") + self._bias = np.array(bias, dtype=float) + def cdf(self): return np.insert(np.cumsum(self.p), 0, 0.0) def sample(self, n_samples=1, seed=None): + if self.bias is None: + samples = self._sample_unbiased(n_samples, seed) + return samples, np.ones_like(samples) + else: + rng = np.random.RandomState(seed) + p = self.p / self.p.sum() + b = self.bias / self.bias.sum() + indices = rng.choice(self.x.size, n_samples, p=b) + biased_sample = self.x[indices] + wgt = p[indices] / b[indices] + return biased_sample, wgt + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) p = self.p / self.p.sum() return rng.choice(self.x, n_samples, p=p) @@ -202,6 +363,9 @@ class Discrete(Univariate): norm = sum(self.p) self.p = [val / norm for val in self.p] + def evaluate(self, x): + raise NotImplementedError + def to_xml_element(self, element_name): """Return XML representation of the discrete distribution @@ -221,7 +385,7 @@ class Discrete(Univariate): params = ET.SubElement(element, "parameters") params.text = ' '.join(map(str, self.x)) + ' ' + ' '.join(map(str, self.p)) - + self._append_array_bias_to_xml(element) return element @classmethod @@ -242,13 +406,14 @@ class Discrete(Univariate): params = get_elem_list(elem, "parameters", float) x = params[:len(params)//2] p = params[len(params)//2:] - return cls(x, p) + bias_dist = cls._read_array_bias_from_xml(elem) + return cls(x, p, bias=bias_dist) @classmethod def merge( cls, dists: Sequence[Discrete], - probs: Sequence[int] + probs: Sequence[float] ): """Merge multiple discrete distributions into a single distribution @@ -270,18 +435,52 @@ class Discrete(Univariate): if len(dists) != len(probs): raise ValueError("Number of distributions and probabilities must match.") + biasing = False + for d in dists: + if d.bias is not None: + # If we find that at least one distribution is biased, all + # distributions which are not biased will be assigned their + # default probability vector as a "bias" so that biased + # sampling can occur on the merged distribution. + biasing = True + break + # Combine distributions accounting for duplicate x values x_merged = set() p_merged = defaultdict(float) - for dist, p_dist in zip(dists, probs): - for x, p in zip(dist.x, dist.p): - x_merged.add(x) - p_merged[x] += p*p_dist + new_bias = None - # Create values and probabilities as arrays - x_arr = np.array(sorted(x_merged)) + if biasing: + b_merged = defaultdict(float) + + # Generate any missing bias distributions + dists = dists.copy() + for i, d in enumerate(dists): + if d.bias is None: + dists[i] = Discrete(d.x, d.p, bias=d.p) + + for dist, p_dist in zip(dists, probs): + for x, p, b in zip(dist.x, dist.p, dist.bias): + x_merged.add(x) + p_merged[x] += p*p_dist + b_merged[x] += b*p_dist + + # Create values and bias probabilities as arrays + x_arr = np.array(sorted(x_merged)) + new_bias = np.array([b_merged[x] for x in x_arr]) + + else: + for dist, p_dist in zip(dists, probs): + for x, p in zip(dist.x, dist.p): + x_merged.add(x) + p_merged[x] += p*p_dist + + # Create values as array + x_arr = np.array(sorted(x_merged)) + + # Create probabilities as array p_arr = np.array([p_merged[x] for x in x_arr]) - return cls(x_arr, p_arr) + return cls(x_arr, p_arr, new_bias) def integral(self): """Return integral of distribution @@ -318,6 +517,9 @@ class Discrete(Univariate): function will remove any low-importance points such that :math:`\sum_i x_i p_i` is preserved to within some threshold. + For biased distributions, clipping should be performed before the bias + probabilities are added. + .. versionadded:: 0.14.0 Parameters @@ -332,6 +534,10 @@ class Discrete(Univariate): Discrete distribution with low-importance points removed """ + if self.bias is not None: + raise RuntimeError("Biased discrete distributions should be clipped " + "before applying bias.") + cv.check_less_than("tolerance", tolerance, 1.0, equality=True) cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) @@ -382,6 +588,8 @@ class Uniform(Univariate): Lower bound of the sampling interval. Defaults to zero. b : float, optional Upper bound of the sampling interval. Defaults to unity. + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -389,12 +597,19 @@ class Uniform(Univariate): Lower bound of the sampling interval b : float Upper bound of the sampling interval + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, a: float = 0.0, b: float = 1.0): + def __init__(self, a: float = 0.0, b: float = 1.0, + bias: Univariate | None = None): self.a = a self.b = b + super().__init__(bias) def __len__(self): return 2 @@ -417,16 +632,25 @@ class Uniform(Univariate): cv.check_type('Uniform b', b, Real) self._b = b + @property + def support(self): + return (self._a, self._b) + def to_tabular(self): + if self.bias is not None: + raise RuntimeError("to_tabular() is not permitted for biased distributions.") prob = 1./(self.b - self.a) t = Tabular([self.a, self.b], [prob, prob], 'histogram') t.c = [0., 1.] return t - def sample(self, n_samples=1, seed=None): + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) return rng.uniform(self.a, self.b, n_samples) + def evaluate(self, x): + return np.where((self.a <= x) & (x <= self.b), 1/(self.b - self.a), 0.0) + def mean(self) -> float: """Return mean of the uniform distribution @@ -456,6 +680,7 @@ class Uniform(Univariate): element = ET.Element(element_name) element.set("type", "uniform") element.set("parameters", f'{self.a} {self.b}') + self._append_bias_to_xml(element) return element @classmethod @@ -474,7 +699,8 @@ class Uniform(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*params, bias=bias_dist) class PowerLaw(Univariate): @@ -493,6 +719,8 @@ class PowerLaw(Univariate): n : float, optional Power law exponent. Defaults to zero, which is equivalent to a uniform distribution. + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -502,16 +730,23 @@ class PowerLaw(Univariate): Upper bound of the sampling interval n : float Power law exponent + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, a: float = 0.0, b: float = 1.0, n: float = 0.): + def __init__(self, a: float = 0.0, b: float = 1.0, n: float = 0., + bias: Univariate | None = None): if a >= b: raise ValueError( "Lower bound of sampling interval must be less than upper bound.") self.a = a self.b = b self.n = n + super().__init__(bias) def __len__(self): return 3 @@ -549,7 +784,11 @@ class PowerLaw(Univariate): cv.check_type('power law exponent', n, Real) self._n = n - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (self._a, self._b) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) xi = rng.random(n_samples) pwr = self.n + 1 @@ -557,6 +796,10 @@ class PowerLaw(Univariate): span = self.b**pwr - offset return np.power(offset + xi * span, 1/pwr) + def evaluate(self, x): + c = (self.n + 1)/(self.b**(self.n + 1) - self.a**(self.n + 1)) + return np.where((self.a <= x) & (x <= self.b), c * np.abs(x)**self.n, 0.0) + def to_xml_element(self, element_name: str): """Return XML representation of the power law distribution @@ -574,6 +817,7 @@ class PowerLaw(Univariate): element = ET.Element(element_name) element.set("type", "powerlaw") element.set("parameters", f'{self.a} {self.b} {self.n}') + self._append_bias_to_xml(element) return element @classmethod @@ -592,7 +836,8 @@ class PowerLaw(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*map(float, params), bias=bias_dist) class Maxwell(Univariate): @@ -606,16 +851,24 @@ class Maxwell(Univariate): ---------- theta : float Effective temperature for distribution in eV + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- theta : float Effective temperature for distribution in eV + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, theta): + def __init__(self, theta, bias: Univariate | None = None): self.theta = theta + super().__init__(bias) def __len__(self): return 1 @@ -630,7 +883,11 @@ class Maxwell(Univariate): cv.check_greater_than('Maxwell temperature', theta, 0.0) self._theta = theta - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (0.0, np.inf) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) return self.sample_maxwell(self.theta, n_samples, rng=rng) @@ -638,9 +895,10 @@ class Maxwell(Univariate): def sample_maxwell(t, n_samples: int, rng=None): if rng is None: rng = np.random.default_rng() - r1, r2, r3 = rng.random((3, n_samples)) - c = np.cos(0.5 * np.pi * r3) - return -t * (np.log(r1) + np.log(r2) * c * c) + return rng.gamma(1.5, t, n_samples) + + def evaluate(self, E): + return scipy.stats.gamma.pdf(E, 1.5, scale=self.theta) def to_xml_element(self, element_name: str): """Return XML representation of the Maxwellian distribution @@ -659,6 +917,7 @@ class Maxwell(Univariate): element = ET.Element(element_name) element.set("type", "maxwell") element.set("parameters", str(self.theta)) + self._append_bias_to_xml(element) return element @classmethod @@ -677,7 +936,8 @@ class Maxwell(Univariate): """ theta = float(get_text(elem, 'parameters')) - return cls(theta) + bias_dist = cls._read_bias_from_xml(elem) + return cls(theta, bias=bias_dist) class Watt(Univariate): @@ -693,6 +953,8 @@ class Watt(Univariate): First parameter of distribution in units of eV b : float Second parameter of distribution in units of 1/eV + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -700,12 +962,18 @@ class Watt(Univariate): First parameter of distribution in units of eV b : float Second parameter of distribution in units of 1/eV + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, a=0.988e6, b=2.249e-6): + def __init__(self, a=0.988e6, b=2.249e-6, bias: Univariate | None = None): self.a = a self.b = b + super().__init__(bias) def __len__(self): return 2 @@ -730,13 +998,21 @@ class Watt(Univariate): cv.check_greater_than('Watt b', b, 0.0) self._b = b - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (0.0, np.inf) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) w = Maxwell.sample_maxwell(self.a, n_samples, rng=rng) u = rng.uniform(-1., 1., n_samples) aab = self.a * self.a * self.b return w + 0.25*aab + u*np.sqrt(aab*w) + def evaluate(self, E): + c = 2.0/(sqrt(pi * self.b) * (self.a**1.5) * exp(self.a*self.b/4)) + return c*np.exp(-E/self.a)*np.sinh(np.sqrt(self.b*E)) + def to_xml_element(self, element_name: str): """Return XML representation of the Watt distribution @@ -754,6 +1030,7 @@ class Watt(Univariate): element = ET.Element(element_name) element.set("type", "watt") element.set("parameters", f'{self.a} {self.b}') + self._append_bias_to_xml(element) return element @classmethod @@ -772,22 +1049,34 @@ class Watt(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*map(float, params), bias=bias_dist) class Normal(Univariate): - r"""Normally distributed sampling. + r"""Normally distributed sampling with optional truncation. - The Normal Distribution is characterized by two parameters - :math:`\mu` and :math:`\sigma` and has density function - :math:`p(X) dX = 1/(\sqrt{2\pi}\sigma) e^{-(X-\mu)^2/(2\sigma^2)}` + The normal distribution is characterized by parameters :math:`\mu` and + :math:`\sigma` and has density function :math:`p(X) = 1/(\sqrt{2\pi}\sigma) + e^{-(X-\mu)^2/(2\sigma^2)}`. When truncated to the interval [lower, upper], + the distribution is renormalized so that the PDF integrates to 1 over the + truncation interval. + + .. versionchanged:: 0.15.4 + Added optional truncation bounds via `lower` and `upper` parameters. Parameters ---------- mean_value : float - Mean value of the distribution + Mean value of the distribution std_dev : float Standard deviation of the Normal distribution + lower : float, optional + Lower truncation bound. Defaults to -infinity (no lower bound). + upper : float, optional + Upper truncation bound. Defaults to +infinity (no upper bound). + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -795,13 +1084,29 @@ class Normal(Univariate): Mean of the Normal distribution std_dev : float Standard deviation of the Normal distribution + lower : float + Lower truncation bound + upper : float + Upper truncation bound + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, mean_value, std_dev): + def __init__(self, mean_value, std_dev, lower=-np.inf, upper=np.inf, + bias: Univariate | None = None): self.mean_value = mean_value self.std_dev = std_dev + self.lower = lower + self.upper = upper + self._compute_normalization() + super().__init__(bias) def __len__(self): + if self._is_truncated: + return 4 return 2 @property @@ -823,9 +1128,69 @@ class Normal(Univariate): cv.check_greater_than('Normal std_dev', std_dev, 0.0) self._std_dev = std_dev - def sample(self, n_samples=1, seed=None): + @property + def lower(self): + return self._lower + + @lower.setter + def lower(self, lower): + cv.check_type('Normal lower bound', lower, Real) + self._lower = lower + + @property + def upper(self): + return self._upper + + @upper.setter + def upper(self, upper): + cv.check_type('Normal upper bound', upper, Real) + self._upper = upper + + def _compute_normalization(self): + """Compute normalization factor for truncated distribution.""" + # Check if truncation bounds are finite + self._is_truncated = (self._lower > -np.inf or self._upper < np.inf) + + if self._lower >= self._upper: + raise ValueError("Normal distribution lower bound must be less " + "than upper bound.") + + if self._is_truncated: + alpha = (self._lower - self._mean_value) / self._std_dev + beta = (self._upper - self._mean_value) / self._std_dev + cdf_diff = scipy.stats.norm.cdf(beta) - scipy.stats.norm.cdf(alpha) + if cdf_diff <= 0: + raise ValueError("Truncation bounds exclude entire distribution") + self._norm_factor = 1.0 / cdf_diff + else: + self._norm_factor = 1.0 + + @property + def support(self): + return (self._lower, self._upper) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) - return rng.normal(self.mean_value, self.std_dev, n_samples) + if not self._is_truncated: + return rng.normal(self.mean_value, self.std_dev, n_samples) + else: + # Use scipy's truncated normal for efficient direct sampling + a = (self._lower - self._mean_value) / self._std_dev + b = (self._upper - self._mean_value) / self._std_dev + return scipy.stats.truncnorm.rvs( + a, b, loc=self._mean_value, scale=self._std_dev, + size=n_samples, random_state=rng + ) + + def evaluate(self, x): + """Evaluate PDF at x, returning normalized value for truncated dist.""" + x = np.asarray(x) + f = scipy.stats.norm.pdf(x, self.mean_value, self.std_dev) + if self._is_truncated: + # PDF is zero outside bounds + in_bounds = (x >= self._lower) & (x <= self._upper) + f = np.where(in_bounds, f * self._norm_factor, 0.0) + return f def to_xml_element(self, element_name: str): """Return XML representation of the Normal distribution @@ -838,12 +1203,17 @@ class Normal(Univariate): Returns ------- element : lxml.etree._Element - XML element containing Watt distribution data + XML element containing Normal distribution data """ element = ET.Element(element_name) element.set("type", "normal") - element.set("parameters", f'{self.mean_value} {self.std_dev}') + if self._is_truncated: + element.set("parameters", + f'{self.mean_value} {self.std_dev} {self.lower} {self.upper}') + else: + element.set("parameters", f'{self.mean_value} {self.std_dev}') + self._append_bias_to_xml(element) return element @classmethod @@ -862,10 +1232,14 @@ class Normal(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + if len(params) == 4: + return cls(params[0], params[1], params[2], params[3], bias=bias_dist) + else: + return cls(params[0], params[1], bias=bias_dist) -def muir(e0: float, m_rat: float, kt: float): +def muir(e0: float, m_rat: float, kt: float, bias: Univariate | None = None): """Generate a Muir energy spectrum The Muir energy spectrum is a normal distribution, but for convenience @@ -883,6 +1257,8 @@ def muir(e0: float, m_rat: float, kt: float): Ratio of the sum of the masses of the reaction inputs to 1 amu kt : float Ion temperature for the Muir distribution in [eV] + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Returns ------- @@ -891,8 +1267,8 @@ def muir(e0: float, m_rat: float, kt: float): """ # https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-05411-MS - std_dev = math.sqrt(2 * e0 * kt / m_rat) - return Normal(e0, std_dev) + std_dev = sqrt(2 * e0 * kt / m_rat) + return Normal(e0, std_dev, bias=bias) # Retain deprecated name for the time being @@ -906,6 +1282,138 @@ def Muir(*args, **kwargs): return muir(*args, **kwargs) +def fusion_neutron_spectrum( + ion_temp: float, + reactants: str = 'DD', + bias: Univariate | None = None +) -> Normal: + r"""Return a Gaussian energy distribution for fusion neutron emission. + + Computes the mean energy and spectral width of the neutron energy spectrum + from thermonuclear fusion reactions in a plasma with Maxwellian ion velocity + distributions. The mean neutron energy is calculated as + + .. math:: + + \langle E_n \rangle = E_0 + \Delta E_\text{th} + + where :math:`E_0` is the neutron energy at zero ion temperature and + :math:`\Delta E_\text{th}` is the thermal peak shift due to the motion of + the reacting ions. The spectral width is characterized by the FWHM: + + .. math:: + + W_{1/2} = \omega_0 (1 + \delta_\omega) \sqrt{T_i} + + where :math:`\omega_0` is the width at the :math:`T_i \to 0` limit and + :math:`\delta_\omega` is a temperature-dependent correction term. Both + :math:`\Delta E_\text{th}` and :math:`\delta_\omega` are evaluated using + interpolation formulas from `Ballabio et al. + `_: Table III for :math:`0 < + T_i \le 40` keV and Table IV for :math:`40 < T_i < 100` keV. The returned + distribution is a normal (Gaussian) approximation to the spectrum. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + ion_temp : float + Ion temperature of the plasma in [eV]. + reactants : {'DD', 'DT'} + Fusion reactants. 'DD' corresponds to the D(d,n)\ :sup:`3`\ He reaction + and 'DT' to the T(d,n)\ :math:`\alpha` reaction. + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. + + Returns + ------- + openmc.stats.Normal + Normal distribution with mean and standard deviation corresponding to + the first and second moments of the fusion neutron energy spectrum. Both + the mean and standard deviation are in [eV]. + + """ + if ion_temp < 0.0 or ion_temp > 100e3: + raise ValueError("Ion temperature must be between 0 and 100 keV.") + + # Formulas from doi:10.1088/0029-5515/38/11/310 + mn = NEUTRON_MASS + md = atomic_mass('H2') + ev_per_c2 = 931.49410372*1e6 + if reactants == 'DD': + mhe3 = atomic_mass('He3') + Q = (md + md - mhe3 - mn)*ev_per_c2 + E_n = mhe3/(mhe3 + mn)*Q + w0 = 82.542 + + # Low-T constants for peak shift (Table III) + a1 = 4.69515 + a2 = -0.040729 + a3 = 0.47 + a4 = 0.81844 + + # Low-T constants for width correction (Table III) + b1 = 1.7013e-3 + b2 = 0.16888 + b3 = 0.49 + b4 = 7.9460e-4 + + # High-T constants for peak shift (Table IV) + a5 = 18.225 + a6 = 2.1525 + + # High-T constants for width correction (Table IV) + b5 = 8.4619e-3 + b6 = 8.3241e-4 + + elif reactants == 'DT': + mt = atomic_mass('H3') + ma = atomic_mass('He4') + Q = (md + mt - ma - mn)*ev_per_c2 + E_n = ma/(ma + mn)*Q + w0 = 177.259 + + # Low-T constants for peak shift (Table III) + a1 = 5.30509 + a2 = 2.4736e-3 + a3 = 1.84 + a4 = 1.3818 + + # Low-T constants for width correction (Table III) + b1 = 5.1068e-4 + b2 = 7.6223e-3 + b3 = 1.78 + b4 = 8.7691e-5 + + # High-T constants for peak shift (Table IV) + a5 = 37.771 + a6 = 0.92181 + + # High-T constants for width correction (Table IV) + b5 = 2.0199e-3 + b6 = 5.9501e-5 + else: + raise ValueError("Invalid reactants specified. Must be 'DD' or 'DT'.") + + # Ion temperature in keV + T = ion_temp * 1e-3 + + if T <= 40.0: + # Low-temperature interpolation (Table III, 0 < T_i <= 40 keV) + Delta_E = a1/(1 + a2*T**a3)*T**(2/3) + a4*T + delta_w = b1/(1 + b2*T**b3)*T**(2/3) + b4*T + else: + # High-temperature interpolation (Table IV, 40 < T_i < 100 keV) + Delta_E = a5 + a6*T + delta_w = b5 + b6*T + + # Calculate FWHM + fwhm = (w0*(1 + delta_w) * sqrt(T))*1e3 + + sigma = fwhm / (2*sqrt(2*log(2))) + return Normal(E_n + Delta_E * 1e3, sigma, bias=bias) + + class Tabular(Univariate): """Piecewise continuous probability distribution. @@ -926,6 +1434,8 @@ class Tabular(Univariate): points. Defaults to 'linear-linear'. ignore_negative : bool Ignore negative probabilities + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -936,6 +1446,11 @@ class Tabular(Univariate): interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'} Indicates how the density function is interpolated between tabulated points. Defaults to 'linear-linear'. + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling Notes ----- @@ -953,7 +1468,8 @@ class Tabular(Univariate): x: Sequence[float], p: Sequence[float], interpolation: str = 'linear-linear', - ignore_negative: bool = False + ignore_negative: bool = False, + bias: Univariate | None = None ): self.interpolation = interpolation @@ -975,6 +1491,7 @@ class Tabular(Univariate): self._x = x self._p = p + super().__init__(bias) def __len__(self): return self.p.size @@ -996,6 +1513,10 @@ class Tabular(Univariate): cv.check_value('interpolation', interpolation, _INTERPOLATION_SCHEMES) self._interpolation = interpolation + @property + def support(self): + return (self._x[0], self._x[-1]) + def cdf(self): c = np.zeros_like(self.x) x = self.x @@ -1005,51 +1526,71 @@ class Tabular(Univariate): c[1:] = p[:x.size-1] * np.diff(x) elif self.interpolation == 'linear-linear': c[1:] = 0.5 * (p[:-1] + p[1:]) * np.diff(x) + elif self.interpolation == "linear-log": + m = np.diff(p) / np.diff(np.log(x)) + c[1:] = p[:-1] * np.diff(x) + m * ( + x[1:] * (np.diff(np.log(x)) - 1.0) + x[:-1] + ) + elif self.interpolation == "log-linear": + m = np.diff(np.log(p)) / np.diff(x) + c[1:] = p[:-1] * np.diff(x) * exprel(m * np.diff(x)) + elif self.interpolation == "log-log": + m = np.diff(np.log(x * p)) / np.diff(np.log(x)) + c[1:] = (x * p)[:-1] * np.diff(np.log(x)) * exprel(m * np.diff(np.log(x))) else: - raise NotImplementedError('Can only generate CDFs for tabular ' - 'distributions using histogram or ' - 'linear-linear interpolation') - + raise NotImplementedError( + f"Cannot generate CDFs for tabular " + f"distributions using {self.interpolation} interpolation" + ) return np.cumsum(c) def mean(self): """Compute the mean of the tabular distribution""" - if self.interpolation == 'linear-linear': - mean = 0.0 - for i in range(1, len(self.x)): - y_min = self.p[i-1] - y_max = self.p[i] - x_min = self.x[i-1] - x_max = self.x[i] - m = (y_max - y_min) / (x_max - x_min) + # use normalized probabilities when computing mean + p = self.p / self.cdf().max() + x = self.x + x_min = x[:-1] + x_max = x[1:] + p_min = p[: x.size - 1] - exp_val = (1./3.) * m * (x_max**3 - x_min**3) - exp_val += 0.5 * m * x_min * (x_min**2 - x_max**2) - exp_val += 0.5 * y_min * (x_max**2 - x_min**2) - mean += exp_val - - elif self.interpolation == 'histogram': - x_l = self.x[:-1] - x_r = self.x[1:] - p_l = self.p[:self.x.size-1] - mean = (0.5 * (x_l + x_r) * (x_r - x_l) * p_l).sum() + if self.interpolation == "linear-linear": + m = np.diff(p) / np.diff(x) + mean = ((1.0 / 3.0) * m * np.diff(x**3) + + 0.5 * (p_min - m * x_min) * np.diff(x**2)).sum() + elif self.interpolation == "linear-log": + m = np.diff(p) / np.diff(np.log(x)) + mean = ( + (1.0 / 4.0) * m * x_min**2 + * ((x_max / x_min)**2 * (2 * np.diff(np.log(x)) - 1) + 1) + + 0.5 * p_min * np.diff(x**2) + ).sum() + elif self.interpolation == "log-linear": + m = np.diff(np.log(p)) / np.diff(x) + mean = (p_min * ( + np.diff(x) ** 2 + * ((0.5 * exprel2(m * np.diff(x)) * (m * np.diff(x) - 1) + 1)) + + np.diff(x) * x_min * exprel(m * np.diff(x))) + ).sum() + elif self.interpolation == "log-log": + m = np.diff(np.log(p)) / np.diff(np.log(x)) + mean = (p_min * x_min**2 * np.diff(np.log(x)) + * exprel((m + 2) * np.diff(np.log(x)))).sum() + elif self.interpolation == "histogram": + mean = (0.5 * (x_min + x_max) * np.diff(x) * p_min).sum() else: - raise NotImplementedError('Can only compute mean for tabular ' - 'distributions using histogram ' - 'or linear-linear interpolation.') - - # Normalize for when integral of distribution is not 1 - mean /= self.integral() - + raise NotImplementedError( + f"Cannot compute mean for tabular " + f"distributions using {self.interpolation} interpolation" + ) return mean def normalize(self): """Normalize the probabilities stored on the distribution""" self._p /= self.cdf().max() - def sample(self, n_samples: int = 1, seed: int | None = None): + def _sample_unbiased(self, n_samples: int = 1, seed: int | None = None): rng = np.random.RandomState(seed) xi = rng.random(n_samples) @@ -1101,15 +1642,93 @@ class Tabular(Univariate): quad[quad < 0.0] = 0.0 m[non_zero] = x_i[non_zero] + (np.sqrt(quad) - p_i[non_zero]) / m[non_zero] samples_out = m + elif self.interpolation == "linear-log": + # get variable and probability values for the + # next entry + x_i1 = self.x[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] + # compute slope between entries + m = (p_i1 - p_i) / np.log(x_i1 / x_i) + # set values for zero slope + zero = m == 0.0 + m[zero] = x_i[zero] + (xi[zero] - c_i[zero]) / p_i[zero] + positive = m > 0 + negative = m < 0 + a = p_i / m - 1 + m[positive] = ( + x_i + * ((xi - c_i) / (m * x_i) + a) + / np.real(lambertw((((xi - c_i) / (m * x_i) + a)) * np.exp(a))) + )[positive] + m[negative] = ( + x_i + * ((xi - c_i) / (m * x_i) + a) + / np.real(lambertw((((xi - c_i) / (m * x_i) + a)) * np.exp(a), -1.0)) + )[negative] + samples_out = m + elif self.interpolation == "log-linear": + # get variable and probability values for the + # next entry + x_i1 = self.x[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] + # compute slope between entries + m = np.log(p_i1 / p_i) / (x_i1 - x_i) + f = (xi - c_i) / p_i + + samples_out = x_i + f * log1prel(m * f) + elif self.interpolation == "log-log": + # get variable and probability values for the + # next entry + x_i1 = self.x[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] + # compute slope between entries + m = np.log((x_i1 * p_i1) / (x_i * p_i)) / np.log(x_i1 / x_i) + f = (xi - c_i) / (x_i * p_i) + + samples_out = x_i * np.exp(f * log1prel(m * f)) else: - raise NotImplementedError('Can only sample tabular distributions ' - 'using histogram or ' - 'linear-linear interpolation') + raise NotImplementedError( + f"Cannot sample tabular distributions " + f"for {self.inteprolation} interpolation " + ) assert all(samples_out < self.x[-1]) return samples_out + def sample(self, n_samples: int = 1, seed: int | None = None): + if self.bias is None: + samples = self._sample_unbiased(n_samples, seed) + return samples, np.ones_like(samples) + else: + if self.bias.bias is not None: + raise RuntimeError('Biasing distributions should not have their own bias.') + biased_sample, _ = self.bias.sample(n_samples=n_samples, seed=seed) + self.normalize() # must have normalized probabilities to apply correct weights + wgt = np.array([self.evaluate(s) / self.bias.evaluate(s) for s in biased_sample]) + return biased_sample, wgt + + def evaluate(self, x): + if self.interpolation == 'linear-linear': + i = np.searchsorted(self.x, x, side='left') - 1 + if i < 0 or i >= len(self.p) - 1: + return 0.0 + x0, x1 = self.x[i], self.x[i + 1] + p0, p1 = self.p[i], self.p[i + 1] + t = (x - x0) / (x1 - x0) + return (1 - t) * p0 + t * p1 + + elif self.interpolation == 'histogram': + i = np.searchsorted(self.x, x, side='right') - 1 + if i < 0 or i >= len(self.p): + return 0.0 + return self.p[i] + + else: + raise NotImplementedError('Can only evaluate tabular ' + 'distributions using histogram ' + 'or linear-linear interpolation.') + def to_xml_element(self, element_name: str): """Return XML representation of the tabular distribution @@ -1130,7 +1749,7 @@ class Tabular(Univariate): params = ET.SubElement(element, "parameters") params.text = ' '.join(map(str, self.x)) + ' ' + ' '.join(map(str, self.p)) - + self._append_bias_to_xml(element) return element @classmethod @@ -1153,7 +1772,8 @@ class Tabular(Univariate): m = (len(params) + 1)//2 # +1 for when len(params) is odd x = params[:m] p = params[m:] - return cls(x, p, interpolation) + bias_dist = cls._read_bias_from_xml(elem) + return cls(x, p, interpolation, bias=bias_dist) def integral(self): """Return integral of distribution @@ -1169,9 +1789,22 @@ class Tabular(Univariate): return np.sum(np.diff(self.x) * self.p[:self.x.size-1]) elif self.interpolation == 'linear-linear': return trapezoid(self.p, self.x) + elif self.interpolation == "linear-log": + m = np.diff(self.p) / np.diff(np.log(self.x)) + return np.sum( + self.p[:-1] * np.diff(self.x) + + m * (self.x[1:] * (np.diff(np.log(self.x)) - 1.0) + self.x[:-1]) + ) + elif self.interpolation == "log-linear": + m = np.diff(np.log(self.p)) / np.diff(self.x) + return np.sum(self.p[:-1] * np.diff(self.x) * exprel(m * np.diff(self.x))) + elif self.interpolation == "log-log": + m = np.diff(np.log(self.p)) / np.diff(np.log(self.x)) + return np.sum(self.p[:-1] * self.x[:-1] * np.diff(np.log(self.x)) + * exprel((m + 1) * np.diff(np.log(self.x)))) else: raise NotImplementedError( - f'integral() not supported for {self.inteprolation} interpolation') + f'integral() not supported for {self.interpolation} interpolation') class Legendre(Univariate): @@ -1183,16 +1816,24 @@ class Legendre(Univariate): coefficients : Iterable of Real Expansion coefficients :math:`a_\ell`. Note that the :math:`(2\ell + 1)/2` factor should not be included. + bias : openmc.stats.Univariate or None, optional + Distribution for biased sampling. Attributes ---------- coefficients : Iterable of Real Expansion coefficients :math:`a_\ell`. Note that the :math:`(2\ell + 1)/2` factor should not be included. + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, coefficients: Sequence[float]): + def __init__(self, coefficients: Sequence[float], bias: Univariate | None = None): + super().__init__(bias) self.coefficients = coefficients self._legendre_poly = None @@ -1216,9 +1857,19 @@ class Legendre(Univariate): def coefficients(self, coefficients): self._coefficients = np.asarray(coefficients) + @property + def support(self): + raise NotImplementedError + + def _sample_unbiased(self, n_samples=1, seed=None): + raise NotImplementedError + def sample(self, n_samples=1, seed=None): raise NotImplementedError + def evaluate(self, x): + raise NotImplementedError + def to_xml_element(self, element_name): raise NotImplementedError @@ -1236,6 +1887,9 @@ class Mixture(Univariate): Probability of selecting a particular distribution distribution : Iterable of Univariate List of distributions with corresponding probabilities + bias : Iterable of Real, optional + Probability of selecting a particular distribution under biased + sampling Attributes ---------- @@ -1243,14 +1897,20 @@ class Mixture(Univariate): Probability of selecting a particular distribution distribution : Iterable of Univariate List of distributions with corresponding probabilities + support : dict + Dictionary containing discrete and continuous parts of the support + bias : numpy.ndarray or None + Probability of selecting each distribution under biased sampling """ def __init__( self, probability: Sequence[float], - distribution: Sequence[Univariate] + distribution: Sequence[Univariate], + bias: Sequence[float] | None = None ): + super().__init__(bias) self.probability = probability self.distribution = distribution @@ -1280,10 +1940,52 @@ class Mixture(Univariate): Iterable, Univariate) self._distribution = distribution + @Univariate.bias.setter + def bias(self, bias): + if bias is None: + self._bias = bias + else: + cv.check_type('biased mixture distribution probabilities', bias, + Iterable, Real) + for b in bias: + cv.check_greater_than('biased mixture distribution probabilities', + b, 0.0, True) + self._bias = np.array(bias, dtype=float) + + @property + def support(self): + discrete_points = set() + intervals = [] + + for dist in self.distribution: + if isinstance(dist, Discrete): + discrete_points |= dist.support + else: + intervals.append(tuple(dist.support)) + + if intervals: + # simplify union by combining intervals when able + sorted_intervals = sorted(intervals, key=lambda x: x[0]) + merged = [sorted_intervals[0]] + + for current in sorted_intervals[1:]: + prev_start, prev_end = merged[-1] + curr_start, curr_end = current + + if curr_start <= prev_end: + merged[-1] = (prev_start, max(prev_end, curr_end)) + else: + merged.append(current) + + intervals = merged + + return {"discrete": discrete_points, "continuous": intervals} + def cdf(self): return np.insert(np.cumsum(self.probability), 0, 0.0) - def sample(self, n_samples=1, seed=None): + def _sample_unbiased(self, n_samples=1, seed=None): + # Mixture uses internal bias mechanism, not base class bias rng = np.random.RandomState(seed) # Get probability of each distribution accounting for its intensity @@ -1296,11 +1998,49 @@ class Mixture(Univariate): # Draw samples from the distributions sampled above out = np.empty_like(idx, dtype=float) + out_wgt = np.empty_like(idx, dtype=float) for i in np.unique(idx): n_dist_samples = np.count_nonzero(idx == i) - samples = self.distribution[i].sample(n_dist_samples) + samples, weights = self.distribution[i].sample(n_dist_samples) out[idx == i] = samples - return out + out_wgt[idx == i] = weights + return out, out_wgt + + def sample(self, n_samples=1, seed=None): + # Mixture uses internal bias mechanism, not base class bias + if self.bias is None: + return self._sample_unbiased(n_samples, seed) + + rng = np.random.RandomState(seed) + + # Get probability of each distribution accounting for its intensity + p = np.array([prob*dist.integral() for prob, dist in + zip(self.probability, self.distribution)]) + p /= p.sum() + + b = np.array([prob*dist.integral() for prob, dist in + zip(self.bias, self.distribution)]) + b /= b.sum() + + # Sample from the distributions using biased probabilities + idx = rng.choice(range(len(self.distribution)), n_samples, p=b) + idx_wgt = np.ones(n_samples) + for i in np.unique(idx): + idx_wgt[idx == i] = p[i]/b[i] + + # Draw samples from the distributions sampled above + out = np.empty_like(idx, dtype=float) + out_wgt = np.empty_like(idx, dtype=float) + for i in np.unique(idx): + n_dist_samples = np.count_nonzero(idx == i) + samples, weights = self.distribution[i].sample(n_dist_samples) + out[idx == i] = samples + out_wgt[idx == i] = weights * idx_wgt[idx == i] + return out, out_wgt + + def evaluate(self, x): + raise NotImplementedError( + "evaluate() is undefined for Mixture distributions") def normalize(self): """Normalize the probabilities stored on the distribution""" @@ -1331,6 +2071,7 @@ class Mixture(Univariate): data.set("probability", str(p)) data.append(d.to_xml_element("dist")) + self._append_array_bias_to_xml(element) return element @classmethod @@ -1356,7 +2097,8 @@ class Mixture(Univariate): probability.append(float(get_text(pair, 'probability'))) distribution.append(Univariate.from_xml_element(pair.find("dist"))) - return cls(probability, distribution) + bias_dist = cls._read_array_bias_from_xml(elem) + return cls(probability, distribution, bias=bias_dist) def integral(self): """Return integral of the distribution @@ -1459,54 +2201,421 @@ class Mixture(Univariate): return new_dist +class DecaySpectrum(Univariate): + """Energy distribution from decay photon spectra of a mixture of nuclides. + + This distribution stores nuclide names, their atom densities, and the volume + of the region. When written to XML and read by the C++ solver, the nuclide + names are resolved against the depletion chain to obtain the decay photon + energy spectra and decay constants. The resulting distribution is a mixture + of per-nuclide photon spectra weighted by absolute activity. The volume is + necessary so that the C++ solver can compute the total photon emission rate + in [photons/s], which is used as the source strength. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + nuclides : dict + Dictionary mapping nuclide name (str) to atom density (float) in units + of [atom/b-cm]. + volume : float + Volume of the source region in [cm³]. Used together with atom densities + to compute the absolute photon emission rate. + + Attributes + ---------- + nuclides : dict + Dictionary mapping nuclide name to atom density in [atom/b-cm]. + volume : float + Volume of the source region in [cm³]. + + """ + + def __init__(self, nuclides: dict[str, float], volume: float): + super().__init__(bias=None) + self._dist_cache = None + self._dist_cache_key = None + self.nuclides = nuclides + self.volume = volume + + def __len__(self): + return len(self.nuclides) + + @property + def nuclides(self): + return self._nuclides + + @nuclides.setter + def nuclides(self, nuclides): + cv.check_type('nuclides', nuclides, dict) + for name, density in nuclides.items(): + cv.check_type('nuclide name', name, str) + cv.check_type(f'atom density for {name}', density, Real) + cv.check_greater_than(f'atom density for {name}', density, 0.0, True) + self._nuclides = dict(nuclides) + self._dist_cache = None + self._dist_cache_key = None + + @property + def volume(self): + return self._volume + + @volume.setter + def volume(self, volume): + cv.check_type('volume', volume, Real) + cv.check_greater_than('volume', volume, 0.0) + self._volume = float(volume) + self._dist_cache = None + self._dist_cache_key = None + + @staticmethod + def _chain_file_cache_key(): + """Return a hashable key for the active depletion chain.""" + chain_file = openmc.config.get('chain_file') + if chain_file is None: + return None + + path = Path(chain_file).resolve() + try: + stat = path.stat() + except OSError: + return (path, None, None) + return (path, stat.st_mtime, stat.st_size) + + def to_distribution(self): + """Convert to a concrete distribution using decay chain data. + + Builds a combined photon energy distribution by looking up each nuclide + in the depletion chain via :func:`openmc.data.decay_photon_energy` and + weighting by absolute atom count (``density * 1e24 * volume``). The + result is cached on the object; the cache is invalidated automatically + when :attr:`nuclides` or :attr:`volume` are reassigned. + + Requires ``openmc.config['chain_file']`` to be set. + + Returns + ------- + openmc.stats.Univariate or None + Combined photon energy distribution, or ``None`` if no nuclide in + :attr:`nuclides` has a photon source in the chain. + + """ + chain_key = self._chain_file_cache_key() + if self._dist_cache is not None and self._dist_cache_key == chain_key: + return self._dist_cache + + dists = [] + weights = [] + for name, density in self.nuclides.items(): + dist = openmc.data.decay_photon_energy(name) + if dist is not None: + dists.append(dist) + weights.append(density * 1e24 * self.volume) + + if not dists: + return None + + self._dist_cache = combine_distributions(dists, weights) + self._dist_cache_key = chain_key + return self._dist_cache + + def to_xml_element(self, element_name: str): + """Return XML representation of the decay photon distribution + + Parameters + ---------- + element_name : str + XML element name + + Returns + ------- + element : lxml.etree._Element + XML element containing decay photon distribution data + + """ + element = ET.Element(element_name) + element.set("type", "decay_spectrum") + element.set("volume", str(self.volume)) + nuclides = ET.SubElement(element, "nuclides") + nuclides.text = ' '.join(self.nuclides) + parameters = ET.SubElement(element, "parameters") + parameters.text = ' '.join(str(density) for density in self.nuclides.values()) + return element + + @classmethod + def from_xml_element(cls, elem: ET.Element): + """Generate decay photon distribution from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.stats.DecaySpectrum + Decay photon distribution generated from XML element + + """ + volume = float(elem.get('volume')) + names = get_elem_list(elem, 'nuclides', str) + densities = get_elem_list(elem, 'parameters', float) + nuclides = dict(zip(names, densities)) + return cls(nuclides, volume) + + def _sample_unbiased(self, n_samples=1, seed=None): + dist = self.to_distribution() + if dist is None: + raise RuntimeError( + "DecaySpectrum._sample_unbiased requires chain data but none " + "was found. Ensure openmc.config['chain_file'] is set and the " + "chain contains photon sources for the nuclides present." + ) + return dist.sample(n_samples, seed)[0] + + def integral(self): + """Return integral of the distribution + + Returns the total photon emission rate in [photons/s] by delegating to + :meth:`to_distribution`. Returns ``0.0`` when no chain data is + available (e.g., ``openmc.config['chain_file']`` is not set). + + Returns + ------- + float + Total photon emission rate in [photons/s], or ``0.0`` if chain + data is unavailable. + """ + try: + dist = self.to_distribution() + except Exception: + return 0.0 + if dist is None: + return 0.0 + return dist.integral() + + @staticmethod + @cache + def _photon_integral(nuclide: str, chain_key) -> float | None: + """Return the per-atom photon emission integral for a nuclide""" + dist = openmc.data.decay_photon_energy(nuclide) + return dist.integral() if dist is not None else None + + def clip(self, tolerance: float = 1e-9, inplace: bool = False): + """Remove nuclides with negligible contribution to photon emission. + + Nuclides that are stable or have no photon source in the depletion + chain are removed unconditionally. The remaining nuclides are ranked + by their photon emission rate (proportional to + ``atom_density * decay_constant * photon_yield``) and the least + important are discarded until the cumulative discarded fraction of the + total emission rate exceeds *tolerance*. + + Requires ``openmc.config['chain_file']`` to be set. + + Parameters + ---------- + tolerance : float + Maximum fraction of total photon emission rate that may be + discarded. + inplace : bool + Whether to modify the current object in-place or return a new one. + + Returns + ------- + openmc.stats.DecaySpectrum + Distribution with negligible nuclides removed. + + """ + # Compute per-nuclide emission rate; drop non-emitters + emitting_names = [] + emitting_densities = [] + rates = [] + chain_key = self._chain_file_cache_key() + for name, density in self.nuclides.items(): + integral = DecaySpectrum._photon_integral(name, chain_key) + if integral is None: + continue + emitting_names.append(name) + emitting_densities.append(density) + rates.append(density * self.volume * integral) + + if not emitting_names: + new_nuclides = {} + else: + indices = _intensity_clip(rates, tolerance=tolerance) + new_nuclides = { + emitting_names[i]: emitting_densities[i] for i in indices + } + + if inplace: + self._nuclides = new_nuclides + self._dist_cache = None + self._dist_cache_key = None + return self + return type(self)(new_nuclides, self.volume) + + @property + def support(self): + return (0.0, np.inf) + + def evaluate(self, x): + """Evaluate the probability density at a given value. + + Delegates to the combined distribution built from chain data. Raises + ``NotImplementedError`` if the combined distribution is a + :class:`~openmc.stats.Mixture` (which does not support + ``evaluate()``). + + Parameters + ---------- + x : float + Value at which to evaluate the PDF. + + Returns + ------- + float + Probability density at *x*. + """ + dist = self.to_distribution() + if dist is None: + raise RuntimeError( + "DecaySpectrum.evaluate requires chain data. Ensure " + "openmc.config['chain_file'] is set." + ) + return dist.evaluate(x) + + def mean(self): + """Return the mean of the distribution. + + Delegates to the combined distribution built from chain data. + + Returns + ------- + float + Mean photon energy in [eV]. + """ + dist = self.to_distribution() + if dist is None: + raise RuntimeError( + "DecaySpectrum.mean requires chain data. Ensure " + "openmc.config['chain_file'] is set." + ) + return dist.mean() + + def combine_distributions( - dists: Sequence[Univariate], + dists: Sequence[Discrete | Tabular | Mixture], probs: Sequence[float] ): """Combine distributions with specified probabilities This function can be used to combine multiple instances of - :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular`. Multiple - discrete distributions are merged into a single distribution and the - remainder of the distributions are put into a :class:`~openmc.stats.Mixture` - distribution. + :class:`~openmc.stats.Discrete`, :class:`~openmc.stats.Tabular` and + :class:`~openmc.stats.Mixture` of them. Multiple discrete distributions are + merged into a single distribution and the remainder of the distributions are + put into a :class:`~openmc.stats.Mixture` distribution. .. versionadded:: 0.13.1 Parameters ---------- - dists : iterable of openmc.stats.Univariate + dists : sequence of openmc.stats.Discrete, openmc.stats.Tabular, or openmc.stats.Mixture Distributions to combine - probs : iterable of float + probs : sequence of float Probability (or intensity) of each distribution """ - # Get copy of distribution list so as not to modify the argument - dist_list = deepcopy(dists) + new_probs = [] + new_dists = [] + for i, dist in enumerate(dists): + cv.check_type(f'dists[{i}]', dist, (Discrete, Tabular, Mixture)) + cv.check_type(f'probs[{i}]', probs[i], Real) + cv.check_greater_than(f'probs[{i}]', probs[i], 0.0) + if isinstance(dist, Mixture): + if dist.bias is not None: + warn("A Mixture distribution with a bias specified was passed " + "to combine_distributions. The bias will be discarded " + "during flattening.") + for j, d in enumerate(dist.distribution): + cv.check_type(f'dists[{i}].distribution[{j}]', d, (Discrete, Tabular)) + new_probs.append(probs[i]*dist.probability[j]) + new_dists.append(d) + else: + new_probs.append(probs[i]) + new_dists.append(dist) + + probs = new_probs + dists = new_dists # Get list of discrete/continuous distribution indices - discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] - cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)] + discrete_index = [i for i, d in enumerate(dists) if isinstance(d, Discrete)] + cont_index = [i for i, d in enumerate(dists) if isinstance(d, Tabular)] - # Apply probabilites to continuous distributions - for i in cont_index: - dist = dist_list[i] - dist._p *= probs[i] + cont_dists = [dists[i] for i in cont_index] + cont_probs = [probs[i] for i in cont_index] if discrete_index: # Create combined discrete distribution - dist_discrete = [dist_list[i] for i in discrete_index] + dist_discrete = [dists[i] for i in discrete_index] discrete_probs = [probs[i] for i in discrete_index] combined_dist = Discrete.merge(dist_discrete, discrete_probs) + if cont_index: + return Mixture(cont_probs + [1.0], cont_dists + [combined_dist]) + else: + return combined_dist + else: + if len(cont_dists) == 1: + dist = cont_dists[0] + return Tabular(dist.x, dist.p * cont_probs[0], + dist.interpolation, bias=dist.bias) + else: + return Mixture(cont_probs, cont_dists) - # Replace multiple discrete distributions with merged - for idx in reversed(discrete_index): - dist_list.pop(idx) - dist_list.append(combined_dist) - # Combine discrete and continuous if present - if len(dist_list) > 1: - probs = [1.0]*len(dist_list) - dist_list[:] = [Mixture(probs, dist_list.copy())] +def check_bias_support(parent: Univariate, bias: Univariate | None): + """Ensure that bias distributions share the support of the univariate + distribution they are biasing. - return dist_list[0] + Parameters + ---------- + parent : openmc.stats.Univariate + Distributions to be biased + bias : openmc.stats.Univariate or None + Proposed bias distribution + + """ + if bias is None: + return + + def mismatch_error(err_type, msg): + raise err_type(f"Support of parent {type(parent).__name__} and bias " + f"{type(bias).__name__} distributions do not match. " + f"{msg}") + + p_sup, b_sup = parent.support, bias.support + + if isinstance(p_sup, set) or isinstance(b_sup, set): + raise RuntimeError("Discrete distributions cannot be used as biasing " + "distributions or be biased by another Univariate " + "distribution. Instead, assign a vector of " + "alternate probabilities to the bias attribute.") + + elif isinstance(p_sup, dict) or isinstance (b_sup, dict): + raise RuntimeError("Mixture distributions cannot be used as biasing " + "distributions or be biased by another Univariate " + "distribution. Instead, instantiate the Mixture " + "object using biased member distributions, or " + "assign a vector of alternative probabilities to " + "the bias attribute.") + + elif isinstance(p_sup, tuple): + if isinstance(b_sup, tuple): + if p_sup != b_sup: + mismatch_error(ValueError, "") + else: + mismatch_error(TypeError, "Incompatible support types.") + + else: + raise TypeError("Unrecognized type for parent distribution support") diff --git a/openmc/surface.py b/openmc/surface.py index 4839783ff..c2afeb613 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -38,10 +38,13 @@ class SurfaceCoefficient: value : float or str Value of the coefficient (float) or the name of the coefficient that it is equivalent to (str). + positive : bool + Does the surface coefficient must be positive. Defaults to False. """ - def __init__(self, value): + def __init__(self, value, positive=False): self.value = value + self.positive = positive def __get__(self, instance, owner=None): if instance is None: @@ -56,6 +59,8 @@ class SurfaceCoefficient: if isinstance(self.value, Real): raise AttributeError('This coefficient is read-only') check_type(f'{self.value} coefficient', value, Real) + if self.positive: + check_greater_than(f'{self.value} coefficient', value, 0.0) instance._coefficients[self.value] = value @@ -123,9 +128,8 @@ class Surface(IDManagerMixin, ABC): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Note that periodic boundary conditions - can only be applied to x-, y-, and z-planes, and only axis-aligned - periodicity is supported. + freely pass through the surface. Note that only axis-aligned + periodicity is supported around the x-, y-, and z-axes. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -152,6 +156,7 @@ class Surface(IDManagerMixin, ABC): """ + min_id = 1 next_id = 1 used_ids = set() _atol = 1.e-12 @@ -822,8 +827,7 @@ class XPlane(PlaneMixin, Surface): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., x-planes can only be paired with x-planes. + freely pass through the surface. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -887,8 +891,7 @@ class YPlane(PlaneMixin, Surface): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., y-planes can only be paired with y-planes. + freely pass through the surface. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -952,8 +955,7 @@ class ZPlane(PlaneMixin, Surface): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., z-planes can only be paired with z-planes. + freely pass through the surface. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -1264,7 +1266,7 @@ class Cylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient('dx') dy = SurfaceCoefficient('dy') dz = SurfaceCoefficient('dz') @@ -1430,7 +1432,7 @@ class XCylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient(0.) y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient(1.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(0.) @@ -1528,7 +1530,7 @@ class YCylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient(0.) z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(1.) dz = SurfaceCoefficient(0.) @@ -1626,7 +1628,7 @@ class ZCylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient(0.) - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(1.) @@ -1726,7 +1728,7 @@ class Sphere(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) def _get_base_coeffs(self): x0, y0, z0, r = self.x0, self.y0, self.z0, self.r @@ -1852,7 +1854,7 @@ class Cone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient('dx') dy = SurfaceCoefficient('dy') dz = SurfaceCoefficient('dz') @@ -1988,7 +1990,7 @@ class XCone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient(1.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(0.) @@ -2090,7 +2092,7 @@ class YCone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(1.) dz = SurfaceCoefficient(0.) @@ -2192,7 +2194,7 @@ class ZCone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(1.) @@ -2295,9 +2297,9 @@ class TorusMixin: x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - a = SurfaceCoefficient('a') - b = SurfaceCoefficient('b') - c = SurfaceCoefficient('c') + a = SurfaceCoefficient('a', positive=True) + b = SurfaceCoefficient('b', positive=True) + c = SurfaceCoefficient('c', positive=True) def translate(self, vector, inplace=False): surf = self if inplace else self.clone() diff --git a/openmc/tallies.py b/openmc/tallies.py index add356579..ceced4255 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -12,11 +12,28 @@ import lxml.etree as ET import h5py import numpy as np import pandas as pd -import scipy.sparse as sps from scipy.stats import chi2, norm import openmc import openmc.checkvalue as cv +from openmc.filter import ( + Filter, + DistribcellFilter, + EnergyFunctionFilter, + DelayedGroupFilter, + FilterMeta, + MeshFilter, + MeshBornFilter, +) +from openmc.arithmetic import ( + CrossFilter, + AggregateFilter, + CrossScore, + AggregateScore, + CrossNuclide, + AggregateNuclide, +) +from ._sparse_compat import lil_array from ._xml import clean_indentation, get_elem_list, get_text from .mixin import IDManagerMixin from .mesh import MeshBase @@ -31,9 +48,9 @@ _PRODUCT_TYPES = ['tensor', 'entrywise'] # The following indicate acceptable types when setting Tally.scores, # Tally.nuclides, and Tally.filters -_SCORE_CLASSES = (str, openmc.CrossScore, openmc.AggregateScore) -_NUCLIDE_CLASSES = (str, openmc.CrossNuclide, openmc.AggregateNuclide) -_FILTER_CLASSES = (openmc.Filter, openmc.CrossFilter, openmc.AggregateFilter) +_SCORE_CLASSES = (str, CrossScore, AggregateScore) +_NUCLIDE_CLASSES = (str, CrossNuclide, AggregateNuclide) +_FILTER_CLASSES = (Filter, CrossFilter, AggregateFilter) # Valid types of estimators ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog'} @@ -50,6 +67,18 @@ class Tally(IDManagerMixin): will automatically be assigned name : str, optional Name of the tally. If not specified, the name is the empty string. + scores : list of str, optional + List of scores, e.g. ['flux', 'fission'] + filters : list of openmc.Filter, optional + List of filters for the tally + nuclides : list of str, optional + List of nuclides to score results for + estimator : {'analog', 'tracklength', 'collision'}, optional + Type of estimator for the tally + triggers : list of openmc.Trigger, optional + List of tally triggers + derivative : openmc.TallyDerivative, optional + A material perturbation derivative to apply to all scores in the tally Attributes ---------- @@ -124,7 +153,9 @@ class Tally(IDManagerMixin): next_id = 1 used_ids = set() - def __init__(self, tally_id=None, name=''): + def __init__(self, tally_id=None, name='', scores=None, filters=None, + nuclides=None, estimator=None, triggers=None, + derivative=None): # Initialize Tally class attributes self.id = tally_id self.name = name @@ -155,6 +186,19 @@ class Tally(IDManagerMixin): self._sp_filename = None self._results_read = False + if filters is not None: + self.filters = filters + if nuclides is not None: + self.nuclides = nuclides + if scores is not None: + self.scores = scores + if estimator is not None: + self.estimator = estimator + if triggers is not None: + self.triggers = triggers + if derivative is not None: + self.derivative = derivative + def __eq__(self, other): if other.id != self.id: return False @@ -290,7 +334,7 @@ class Tally(IDManagerMixin): @property def num_nuclides(self): - return len(self._nuclides) + return max(len(self._nuclides), 1) @property def scores(self): @@ -393,6 +437,11 @@ class Tally(IDManagerMixin): group = f[f'tallies/tally {self.id}'] self._num_realizations = int(group['n_realizations'][()]) + for filt in self.filters: + if isinstance(filt, DistribcellFilter): + filter_group = f[f'tallies/filters/filter {filt.id}'] + filt._num_bins = int(filter_group['n_bins'][()]) + # Update nuclides nuclide_names = group['nuclides'][()] self._nuclides = [name.decode().strip() for name in nuclide_names] @@ -430,10 +479,10 @@ class Tally(IDManagerMixin): # Convert NumPy arrays to SciPy sparse LIL matrices if self.sparse: - self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) - self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape) - self._sum_third = sps.lil_matrix(self._sum_third.flatten(), self._sum_third.shape) - self._sum_fourth = sps.lil_matrix(self.sum_fourth.flatten(), self._sum_fourth.shape) + self._sum = lil_array(self._sum.flatten(), self._sum.shape) + self._sum_sq = lil_array(self._sum_sq.flatten(), self._sum_sq.shape) + self._sum_third = lil_array(self._sum_third.flatten(), self._sum_third.shape) + self._sum_fourth = lil_array(self._sum_fourth.flatten(), self._sum_fourth.shape) # Read simulation time (needed for figure of merit) self._simulation_time = f["runtime"]["simulation"][()] @@ -529,8 +578,7 @@ class Tally(IDManagerMixin): # Convert NumPy array to SciPy sparse LIL matrix if self.sparse: - self._mean = sps.lil_matrix(self._mean.flatten(), - self._mean.shape) + self._mean = lil_array(self._mean.flatten(), self._mean.shape) if self.sparse: return np.reshape(self._mean.toarray(), self.shape) @@ -551,8 +599,7 @@ class Tally(IDManagerMixin): # Convert NumPy array to SciPy sparse LIL matrix if self.sparse: - self._std_dev = sps.lil_matrix(self._std_dev.flatten(), - self._std_dev.shape) + self._std_dev = lil_array(self._std_dev.flatten(), self._std_dev.shape) self.with_batch_statistics = True @@ -583,7 +630,7 @@ class Tally(IDManagerMixin): self._vov[mask] = numerator[mask]/denominator[mask] - 1.0/n if self.sparse: - self._vov = sps.lil_matrix(self._vov.flatten(), self._vov.shape) + self._vov = lil_array(self._vov.flatten(), self._vov.shape) if self.sparse: return np.reshape(self._vov.toarray(), self.shape) @@ -958,22 +1005,17 @@ class Tally(IDManagerMixin): # Convert NumPy arrays to SciPy sparse LIL matrices if sparse and not self.sparse: if self._sum is not None: - self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) + self._sum = lil_array(self._sum.flatten(), self._sum.shape) if self._sum_sq is not None: - self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), - self._sum_sq.shape) + self._sum_sq = lil_array(self._sum_sq.flatten(), self._sum_sq.shape) if self._sum_third is not None: - self._sum_third = sps.lil_matrix(self._sum_third.flatten(), - self._sum_third.shape) + self._sum_third = lil_array(self._sum_third.flatten(), self._sum_third.shape) if self._sum_fourth is not None: - self._sum_fourth = sps.lil_matrix(self._sum_fourth.flatten(), - self._sum_fourth.shape) + self._sum_fourth = lil_array(self._sum_fourth.flatten(), self._sum_fourth.shape) if self._mean is not None: - self._mean = sps.lil_matrix(self._mean.flatten(), - self._mean.shape) + self._mean = lil_array(self._mean.flatten(), self._mean.shape) if self._std_dev is not None: - self._std_dev = sps.lil_matrix(self._std_dev.flatten(), - self._std_dev.shape) + self._std_dev = lil_array(self._std_dev.flatten(), self._std_dev.shape) self._sparse = True @@ -1064,8 +1106,8 @@ class Tally(IDManagerMixin): return False # Return False if only one tally has a delayed group filter - tally1_dg = self.contains_filter(openmc.DelayedGroupFilter) - tally2_dg = other.contains_filter(openmc.DelayedGroupFilter) + tally1_dg = self.contains_filter(DelayedGroupFilter) + tally2_dg = other.contains_filter(DelayedGroupFilter) if tally1_dg != tally2_dg: return False @@ -1577,7 +1619,7 @@ class Tally(IDManagerMixin): # Also check to see if the desired filter is wrapped up in an # aggregate - elif isinstance(test_filter, openmc.AggregateFilter): + elif isinstance(test_filter, AggregateFilter): if isinstance(test_filter.aggregate_filter, filter_type): return test_filter @@ -1679,7 +1721,7 @@ class Tally(IDManagerMixin): """ - cv.check_type('filters', filters, Iterable, openmc.FilterMeta) + cv.check_type('filters', filters, Iterable, FilterMeta) cv.check_type('filter_bins', filter_bins, Iterable, tuple) # If user did not specify any specific Filters, use them all @@ -1762,7 +1804,7 @@ class Tally(IDManagerMixin): """ for score in scores: - if not isinstance(score, (str, openmc.CrossScore)): + if not isinstance(score, (str, CrossScore)): msg = f'Unable to get score indices for score "{score}" in ' \ f'ID="{self.id}" since it is not a string or CrossScore ' \ 'Tally' @@ -1959,9 +2001,9 @@ class Tally(IDManagerMixin): column_name = 'score' for score in self.scores: - if isinstance(score, (str, openmc.CrossScore)): + if isinstance(score, (str, CrossScore)): scores.append(str(score)) - elif isinstance(score, openmc.AggregateScore): + elif isinstance(score, AggregateScore): scores.append(score.name) column_name = f'{score.aggregate_op}(score)' @@ -1984,7 +2026,7 @@ class Tally(IDManagerMixin): # Expand the columns into Pandas MultiIndices for readability if pd.__version__ >= '0.16': - columns = copy.deepcopy(df.columns.values) + columns = copy.deepcopy(list(df.columns.values)) # Convert all elements in columns list to tuples for i, column in enumerate(columns): @@ -2061,7 +2103,7 @@ class Tally(IDManagerMixin): for i, f in enumerate(self.filters): if expand_dims: # Mesh filter indices are backwards so we need to flip them - if type(f) in {openmc.MeshFilter, openmc.MeshBornFilter}: + if type(f) in {MeshFilter, MeshBornFilter}: fshape = f.shape[::-1] new_shape += fshape idx0, idx1 = i, i + len(fshape) - 1 @@ -2248,7 +2290,7 @@ class Tally(IDManagerMixin): else: all_filters = [self_copy.filters, other_copy.filters] for self_filter, other_filter in product(*all_filters): - new_filter = openmc.CrossFilter(self_filter, other_filter, + new_filter = CrossFilter(self_filter, other_filter, binary_op) new_tally.filters.append(new_filter) @@ -2259,7 +2301,7 @@ class Tally(IDManagerMixin): else: all_nuclides = [self_copy.nuclides, other_copy.nuclides] for self_nuclide, other_nuclide in product(*all_nuclides): - new_nuclide = openmc.CrossNuclide(self_nuclide, other_nuclide, + new_nuclide = CrossNuclide(self_nuclide, other_nuclide, binary_op) new_tally.nuclides.append(new_nuclide) @@ -2270,9 +2312,9 @@ class Tally(IDManagerMixin): if score1 == score2: return score1 else: - return openmc.CrossScore(score1, score2, binary_op) + return CrossScore(score1, score2, binary_op) else: - return openmc.CrossScore(score1, score2, binary_op) + return CrossScore(score1, score2, binary_op) # Add scores to the new tally if score_product == 'entrywise': @@ -2481,16 +2523,16 @@ class Tally(IDManagerMixin): # Construct lists of tuples for the bins in each of the two filters filters = [type(filter1), type(filter2)] - if isinstance(filter1, openmc.DistribcellFilter): + if isinstance(filter1, DistribcellFilter): filter1_bins = [b for b in range(filter1.num_bins)] - elif isinstance(filter1, openmc.EnergyFunctionFilter): + elif isinstance(filter1, EnergyFunctionFilter): filter1_bins = [None] else: filter1_bins = filter1.bins - if isinstance(filter2, openmc.DistribcellFilter): + if isinstance(filter2, DistribcellFilter): filter2_bins = [b for b in range(filter2.num_bins)] - elif isinstance(filter2, openmc.EnergyFunctionFilter): + elif isinstance(filter2, EnergyFunctionFilter): filter2_bins = [None] else: filter2_bins = filter2.bins @@ -2623,11 +2665,11 @@ class Tally(IDManagerMixin): raise ValueError(msg) # Check that the scores are valid - if not isinstance(score1, (str, openmc.CrossScore)): + if not isinstance(score1, (str, CrossScore)): msg = 'Unable to swap score1 "{}" in Tally ID="{}" since it is ' \ 'not a string or CrossScore'.format(score1, self.id) raise ValueError(msg) - elif not isinstance(score2, (str, openmc.CrossScore)): + elif not isinstance(score2, (str, CrossScore)): msg = 'Unable to swap score2 "{}" in Tally ID="{}" since it is ' \ 'not a string or CrossScore'.format(score2, self.id) raise ValueError(msg) @@ -3271,7 +3313,7 @@ class Tally(IDManagerMixin): new_filter.bins = [f.bins[i] for i in bin_indices] # Set number of bins manually for mesh/distribcell filters - if filter_type is openmc.DistribcellFilter: + if filter_type is DistribcellFilter: new_filter._num_bins = f._num_bins # Replace existing filter with new one @@ -3337,16 +3379,16 @@ class Tally(IDManagerMixin): std_dev = self.get_reshaped_data(value='std_dev') # Sum across any filter bins specified by the user - if isinstance(filter_type, openmc.FilterMeta): + if isinstance(filter_type, FilterMeta): find_filter = self.find_filter(filter_type) # If user did not specify filter bins, sum across all bins if len(filter_bins) == 0: bin_indices = np.arange(find_filter.num_bins) - if isinstance(find_filter, openmc.DistribcellFilter): + if isinstance(find_filter, DistribcellFilter): filter_bins = np.arange(find_filter.num_bins) - elif isinstance(find_filter, openmc.EnergyFunctionFilter): + elif isinstance(find_filter, EnergyFunctionFilter): filter_bins = [None] else: filter_bins = find_filter.bins @@ -3375,7 +3417,7 @@ class Tally(IDManagerMixin): # Add AggregateFilter to the tally sum if not remove_filter: - filter_sum = openmc.AggregateFilter(self_filter, + filter_sum = AggregateFilter(self_filter, [tuple(filter_bins)], 'sum') tally_sum.filters.append(filter_sum) @@ -3398,7 +3440,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateNuclide to the tally sum - nuclide_sum = openmc.AggregateNuclide(nuclides, 'sum') + nuclide_sum = AggregateNuclide(nuclides, 'sum') tally_sum.nuclides.append(nuclide_sum) # Add a copy of this tally's nuclides to the tally sum @@ -3416,7 +3458,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateScore to the tally sum - score_sum = openmc.AggregateScore(scores, 'sum') + score_sum = AggregateScore(scores, 'sum') tally_sum.scores.append(score_sum) # Add a copy of this tally's scores to the tally sum @@ -3489,16 +3531,16 @@ class Tally(IDManagerMixin): std_dev = self.get_reshaped_data(value='std_dev') # Average across any filter bins specified by the user - if isinstance(filter_type, openmc.FilterMeta): + if isinstance(filter_type, FilterMeta): find_filter = self.find_filter(filter_type) # If user did not specify filter bins, average across all bins if len(filter_bins) == 0: bin_indices = np.arange(find_filter.num_bins) - if isinstance(find_filter, openmc.DistribcellFilter): + if isinstance(find_filter, DistribcellFilter): filter_bins = np.arange(find_filter.num_bins) - elif isinstance(find_filter, openmc.EnergyFunctionFilter): + elif isinstance(find_filter, EnergyFunctionFilter): filter_bins = [None] else: filter_bins = find_filter.bins @@ -3528,7 +3570,7 @@ class Tally(IDManagerMixin): # Add AggregateFilter to the tally avg if not remove_filter: - filter_sum = openmc.AggregateFilter(self_filter, + filter_sum = AggregateFilter(self_filter, [tuple(filter_bins)], 'avg') tally_avg.filters.append(filter_sum) @@ -3552,7 +3594,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateNuclide to the tally avg - nuclide_avg = openmc.AggregateNuclide(nuclides, 'avg') + nuclide_avg = AggregateNuclide(nuclides, 'avg') tally_avg.nuclides.append(nuclide_avg) # Add a copy of this tally's nuclides to the tally avg @@ -3571,7 +3613,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateScore to the tally avg - score_sum = openmc.AggregateScore(scores, 'avg') + score_sum = AggregateScore(scores, 'avg') tally_avg.scores.append(score_sum) # Add a copy of this tally's scores to the tally avg @@ -3704,43 +3746,17 @@ class Tallies(cv.CheckedList): if possible. Defaults to False. """ - if not isinstance(tally, Tally): - msg = f'Unable to add a non-Tally "{tally}" to the Tallies instance' - raise TypeError(msg) - if merge: - merged = False - # Look for a tally to merge with this one for i, tally2 in enumerate(self): - # If a mergeable tally is found if tally2.can_merge(tally): # Replace tally2 with the merged tally merged_tally = tally2.merge(tally) self[i] = merged_tally - merged = True - break + return - # If no mergeable tally was found, simply add this tally - if not merged: - super().append(tally) - - else: - super().append(tally) - - def insert(self, index, item): - """Insert tally before index - - Parameters - ---------- - index : int - Index in list - item : openmc.Tally - Tally to insert - - """ - super().insert(index, item) + super().append(tally) def merge_tallies(self): """Merge any mergeable tallies together. Note that n-way merges are @@ -3787,7 +3803,7 @@ class Tallies(cv.CheckedList): already_written = memo if memo else set() for tally in self: for f in tally.filters: - if isinstance(f, openmc.MeshFilter): + if isinstance(f, MeshFilter): if f.mesh.id in already_written: continue if len(f.mesh.name) > 0: @@ -3882,7 +3898,7 @@ class Tallies(cv.CheckedList): # Read filter elements filters = {} for e in elem.findall('filter'): - filter = openmc.Filter.from_xml_element(e, meshes=meshes) + filter = Filter.from_xml_element(e, meshes=meshes) filters[filter.id] = filter # Read derivative elements diff --git a/openmc/tracks.py b/openmc/tracks.py index 81646e7d2..44e3c0eba 100644 --- a/openmc/tracks.py +++ b/openmc/tracks.py @@ -4,7 +4,8 @@ from collections.abc import Sequence import h5py from .checkvalue import check_filetype_version -from .source import SourceParticle, ParticleType +from .particle_type import ParticleType +from .source import SourceParticle from pathlib import Path @@ -25,7 +26,7 @@ states : numpy.ndarray """ def _particle_track_repr(self): - return f"" + return f"" ParticleTrack.__repr__ = _particle_track_repr @@ -92,8 +93,8 @@ class Track(Sequence): Parameters ---------- - particle : {'neutron', 'photon', 'electron', 'positron'} - Matching particle type + particle : str or int or openmc.ParticleType + Matching particle type (name, PDG number, or type) state_filter : function Function that takes a state (structured datatype) and returns a bool depending on some criteria. @@ -126,7 +127,7 @@ class Track(Sequence): for t in self: # Check for matching particle if particle is not None: - if t.particle.name.lower() != particle: + if t.particle != ParticleType(particle): continue # Apply arbitrary state filter @@ -184,7 +185,7 @@ class Track(Sequence): def sources(self): sources = [] for particle_track in self: - particle_type = ParticleType(particle_track.particle) + particle_type = particle_track.particle state = particle_track.states[0] sources.append( SourceParticle( diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 5d52a579a..63af2596e 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -10,13 +10,13 @@ import numpy as np import h5py import openmc -from openmc.filter import _PARTICLES from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh +from openmc.tallies import Tallies import openmc.checkvalue as cv from openmc.checkvalue import PathLike from ._xml import get_elem_list, get_text, clean_indentation from .mixin import IDManagerMixin -from .utility_funcs import change_directory +from .particle_type import ParticleType class WeightWindows(IDManagerMixin): @@ -51,7 +51,7 @@ class WeightWindows(IDManagerMixin): A list of values for which each successive pair constitutes a range of energies in [eV] for a single bin. If no energy bins are provided, the maximum and minimum energy for the data available at runtime. - particle_type : {'neutron', 'photon'} + particle_type : str or int or openmc.ParticleType Particle type the weight windows apply to survival_ratio : float Ratio of the survival weight to the lower weight window bound for @@ -116,7 +116,7 @@ class WeightWindows(IDManagerMixin): upper_ww_bounds: Iterable[float] | None = None, upper_bound_ratio: float | None = None, energy_bounds: Iterable[Real] | None = None, - particle_type: str = 'neutron', + particle_type: str | int | openmc.ParticleType = 'neutron', survival_ratio: float = 3.0, max_lower_bound_ratio: float | None = None, max_split: int = 10, @@ -213,13 +213,15 @@ class WeightWindows(IDManagerMixin): self._mesh = mesh @property - def particle_type(self) -> str: + def particle_type(self) -> ParticleType: return self._particle_type @particle_type.setter - def particle_type(self, pt: str): - cv.check_value('Particle type', pt, _PARTICLES) - self._particle_type = pt + def particle_type(self, pt): + ptype = ParticleType(pt) + if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}: + raise ValueError("Weight windows can only be applied for neutrons or photons") + self._particle_type = ptype @property def energy_bounds(self) -> Iterable[Real]: @@ -329,7 +331,7 @@ class WeightWindows(IDManagerMixin): subelement.text = str(self.mesh.id) subelement = ET.SubElement(element, 'particle_type') - subelement.text = self.particle_type + subelement.text = str(self.particle_type) if self.energy_bounds is not None: subelement = ET.SubElement(element, 'energy_bounds') @@ -494,10 +496,12 @@ class WeightWindowGenerator: A list of values for which each successive pair constitutes a range of energies in [eV] for a single bin. If no energy bins are provided, the maximum and minimum energy for the data available at runtime. - particle_type : {'neutron', 'photon'} + particle_type : str or int or openmc.ParticleType Particle type the weight windows apply to method : {'magic', 'fw_cadis'} The weight window generation methodology applied during an update. + targets : :class:`openmc.Tallies` or iterable of int + Target tallies for local variance reduction via FW-CADIS. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -513,10 +517,12 @@ class WeightWindowGenerator: energy_bounds : Iterable of Real A list of values for which each successive pair constitutes a range of energies in [eV] for a single bin - particle_type : {'neutron', 'photon'} + particle_type : openmc.ParticleType Particle type the weight windows apply to method : {'magic', 'fw_cadis'} The weight window generation methodology applied during an update. + targets : :class:`openmc.Tallies` or numpy.ndarray + Target tallies for local variance reduction via FW-CADIS. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -528,14 +534,15 @@ class WeightWindowGenerator: Whether or not to apply weight windows on the fly. """ - _MAGIC_PARAMS = {'value': str, 'threshold': float, 'ratio': float} + _WWG_PARAMS = {'value': str, 'threshold': float, 'ratio': float} def __init__( self, mesh: openmc.MeshBase, energy_bounds: Sequence[float] | None = None, - particle_type: str = 'neutron', + particle_type: str | int | openmc.ParticleType = 'neutron', method: str = 'magic', + targets: openmc.Tallies | Iterable[int] | None = None, max_realizations: int = 1, update_interval: int = 1, on_the_fly: bool = True @@ -548,6 +555,7 @@ class WeightWindowGenerator: self.energy_bounds = energy_bounds self.particle_type = particle_type self.method = method + self.targets = targets self.max_realizations = max_realizations self.update_interval = update_interval self.on_the_fly = on_the_fly @@ -555,7 +563,7 @@ class WeightWindowGenerator: def __repr__(self): string = type(self).__name__ + '\n' string += f'\t{"Mesh":<20}=\t{self.mesh.id}\n' - string += f'\t{"Particle:":<20}=\t{self.particle_type}\n' + string += f'\t{"Particle:":<20}=\t{str(self.particle_type)}\n' string += f'\t{"Energy Bounds:":<20}=\t{self.energy_bounds}\n' string += f'\t{"Method":<20}=\t{self.method}\n' string += f'\t{"Max Realizations:":<20}=\t{self.max_realizations}\n' @@ -586,13 +594,15 @@ class WeightWindowGenerator: self._energy_bounds = eb @property - def particle_type(self) -> str: + def particle_type(self) -> ParticleType: return self._particle_type @particle_type.setter - def particle_type(self, pt: str): - cv.check_value('particle type', pt, ('neutron', 'photon')) - self._particle_type = pt + def particle_type(self, pt): + ptype = ParticleType(pt) + if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}: + raise ValueError("Weight windows can only be applied for neutrons or photons") + self._particle_type = ptype @property def method(self) -> str: @@ -608,6 +618,22 @@ class WeightWindowGenerator: self._check_update_parameters() except (TypeError, KeyError): warnings.warn(f'Update parameters are invalid for the "{m}" method.') + + @property + def targets(self) -> openmc.Tallies: + return self._targets + + @targets.setter + def targets(self, t): + if t is None: + self._targets = t + else: + cv.check_type('Local FW-CADIS target tallies', t, Iterable) + cv.check_greater_than('Local FW-CADIS target tallies', len(t), 0) + if not isinstance(t, openmc.Tallies): + cv.check_iterable_type('Local FW-CADIS target tallies', t, int) + t = np.asarray(list(t), dtype=int) + self._targets = t @property def max_realizations(self) -> int: @@ -635,13 +661,13 @@ class WeightWindowGenerator: def _check_update_parameters(self, params: dict): if self.method == 'magic' or self.method == 'fw_cadis': - check_params = self._MAGIC_PARAMS + check_params = self._WWG_PARAMS for key, val in params.items(): if key not in check_params: raise ValueError(f'Invalid param "{key}" for {self.method} ' 'weight window generation') - cv.check_type(f'weight window generation param: "{key}"', val, self._MAGIC_PARAMS[key]) + cv.check_type(f'weight window generation param: "{key}"', val, self._WWG_PARAMS[key]) @update_parameters.setter def update_parameters(self, params: dict): @@ -678,7 +704,7 @@ class WeightWindowGenerator: The update parameters as-read from the XML node (keys: str, values: str) """ if method == 'magic' or method == 'fw_cadis': - check_params = cls._MAGIC_PARAMS + check_params = cls._WWG_PARAMS for param, param_type in check_params.items(): if param in update_parameters: @@ -695,7 +721,7 @@ class WeightWindowGenerator: subelement = ET.SubElement(element, 'energy_bounds') subelement.text = ' '.join(str(e) for e in self.energy_bounds) particle_elem = ET.SubElement(element, 'particle_type') - particle_elem.text = self.particle_type + particle_elem.text = str(self.particle_type) realizations_elem = ET.SubElement(element, 'max_realizations') realizations_elem.text = str(self.max_realizations) update_interval_elem = ET.SubElement(element, 'update_interval') @@ -704,6 +730,20 @@ class WeightWindowGenerator: otf_elem.text = str(self.on_the_fly).lower() method_elem = ET.SubElement(element, 'method') method_elem.text = self.method + if self.targets is not None: + if self.method != 'fw_cadis': + raise ValueError( + "FW-CADIS update method is required in order to use " \ + "target tallies for WeightWindowGenerator.") + elif isinstance(self.targets, openmc.Tallies): + raise RuntimeError( + "FW-CADIS target tallies must be checked to ensure they are " \ + "present on model.tallies. Use model.export_to_xml() or " \ + "model.export_to_model_xml() to link FW-CADIS target tallies.") + else: + targets_elem = ET.SubElement(element, 'targets') + targets_elem.text = ' '.join(str(tally_id) for tally_id in self.targets) + if self.update_parameters is not None: self._update_parameters_subelement(element) @@ -731,7 +771,7 @@ class WeightWindowGenerator: mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - energy_bounds = get_elem_list(elem, "energy_bounds, float") + energy_bounds = get_elem_list(elem, "energy_bounds", float) particle_type = get_text(elem, 'particle_type') wwg = cls(mesh, energy_bounds, particle_type) @@ -740,6 +780,14 @@ class WeightWindowGenerator: wwg.update_interval = int(get_text(elem, 'update_interval')) wwg.on_the_fly = bool(get_text(elem, 'on_the_fly')) wwg.method = get_text(elem, 'method') + targets_elem = elem.find('targets') + if targets_elem is not None: + if wwg.method != 'fw_cadis': + raise ValueError( + "FW-CADIS update method is required in order to use " \ + "target tallies for WeightWindowGenerator.") + else: + wwg.targets = get_elem_list(elem, "targets") if elem.find('update_parameters') is not None: update_parameters = {} diff --git a/pyproject.toml b/pyproject.toml index 2d67e8340..c769fb7d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] description = "OpenMC" dynamic = ["version"] -requires-python = ">=3.11" +requires-python = ">=3.12" license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", @@ -21,9 +21,9 @@ classifiers = [ "Topic :: Scientific/Engineering", "Programming Language :: C++", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "numpy", @@ -34,13 +34,13 @@ dependencies = [ "pandas", "lxml", "uncertainties", - "setuptools", "endf", ] [project.optional-dependencies] depletion-mpi = ["mpi4py"] docs = [ + "breathe", "sphinx", "sphinxcontrib-katex", "sphinx-numfig", @@ -70,7 +70,7 @@ include = ['openmc*'] exclude = ['tests*'] [tool.setuptools.package-data] -"openmc.data.effective_dose" = ["**/*.txt"] +"openmc.data.dose" = ["**/*.txt", "*.h5"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] diff --git a/src/atomic_mass.cpp b/src/atomic_mass.cpp new file mode 100644 index 000000000..0e63c9603 --- /dev/null +++ b/src/atomic_mass.cpp @@ -0,0 +1,3569 @@ +#include "openmc/atomic_mass.h" + +namespace openmc { + +// Atomic masses in [u] from AME2020 and CODATA 2018 +std::unordered_map ATOMIC_MASS = { + {11, MASS_ELECTRON}, + {22, 0.0}, + {2112, MASS_NEUTRON}, + {2212, MASS_PROTON}, + {1000010020, MASS_DEUTRON}, + {1000020030, MASS_HELION}, + {1000020040, MASS_ALPHA}, + {1000010030, 3.01604928132}, + {1000030030, 3.030775}, + {1000010040, 4.026431867}, + {1000030040, 4.027185561}, + {1000010050, 5.035311492}, + {1000020050, 5.012057224}, + {1000030050, 5.0125378}, + {1000040050, 5.03987}, + {1000010060, 6.044955437}, + {1000020060, 6.018885889}, + {1000030060, 6.01512288742}, + {1000040060, 6.0197264090000004}, + {1000050060, 6.0508}, + {1000010070, 7.052749}, + {1000020070, 7.027990652}, + {1000030070, 7.01600343426}, + {1000040070, 7.016928714}, + {1000050070, 7.029712}, + {1000020080, 8.033934388}, + {1000030080, 8.022486244}, + {1000040080, 8.005305102}, + {1000050080, 8.024607315}, + {1000060080, 8.037643039}, + {1000020090, 9.043946414}, + {1000030090, 9.026790191}, + {1000040090, 9.012183062}, + {1000050090, 9.013329645}, + {1000060090, 9.031037202}, + {1000020100, 10.052815306}, + {1000030100, 10.035483453}, + {1000040100, 10.013534692}, + {1000050100, 10.012936862}, + {1000060100, 10.016853217}, + {1000070100, 10.04165354}, + {1000030110, 11.043723581}, + {1000040110, 11.02166108}, + {1000050110, 11.009305166}, + {1000060110, 11.011432597}, + {1000070110, 11.026157593}, + {1000080110, 11.051249828}, + {1000030120, 12.052613942}, + {1000040120, 12.026922082}, + {1000050120, 12.014352638}, + {1000060120, 12.0}, + {1000070120, 12.01861318}, + {1000080120, 12.034367726}, + {1000030130, 13.061171503}, + {1000040130, 13.036134506}, + {1000050130, 13.017779981}, + {1000060130, 13.00335483534}, + {1000070130, 13.005738609}, + {1000080130, 13.024815435}, + {1000090130, 13.045121}, + {1000040140, 14.04289292}, + {1000050140, 14.02540401}, + {1000060140, 14.00324198862}, + {1000070140, 14.00307400425}, + {1000080140, 14.008596706}, + {1000090140, 14.034315196}, + {1000040150, 15.053490215}, + {1000050150, 15.031087023}, + {1000060150, 15.010599256}, + {1000070150, 15.00010889827}, + {1000080150, 15.003065636}, + {1000090150, 15.017785139}, + {1000100150, 15.043172977}, + {1000040160, 16.061672036}, + {1000050160, 16.039841045}, + {1000060160, 16.014701255}, + {1000070160, 16.006101925}, + {1000080160, 15.99491461926}, + {1000090160, 16.011460278}, + {1000100160, 16.02575086}, + {1000050170, 17.046931399}, + {1000060170, 17.02257865}, + {1000070170, 17.008448876}, + {1000080170, 16.99913175595}, + {1000090170, 17.002095237}, + {1000100170, 17.017713962}, + {1000110170, 17.037273}, + {1000050180, 18.055601683}, + {1000060180, 18.02675193}, + {1000070180, 18.014077563}, + {1000080180, 17.99915961214}, + {1000090180, 18.000937324}, + {1000100180, 18.005708696}, + {1000110180, 18.026879388}, + {1000050190, 19.064166}, + {1000060190, 19.034797594}, + {1000070190, 19.017022389}, + {1000080190, 19.003577969}, + {1000090190, 18.99840316207}, + {1000100190, 19.001880906}, + {1000110190, 19.013880264}, + {1000120190, 19.03417992}, + {1000050200, 20.074505644}, + {1000060200, 20.040261732}, + {1000070200, 20.023367295}, + {1000080200, 20.004075357}, + {1000090200, 19.999981252}, + {1000100200, 19.99244017525}, + {1000110200, 20.007354301}, + {1000120200, 20.018763075}, + {1000050210, 21.084147485}, + {1000060210, 21.049}, + {1000070210, 21.027087573}, + {1000080210, 21.008654948}, + {1000090210, 20.999948893}, + {1000100210, 20.993846685}, + {1000110210, 20.997654459}, + {1000120210, 21.011705764}, + {1000130210, 21.029082}, + {1000060220, 22.05755399}, + {1000070220, 22.034100918}, + {1000080220, 22.009965744}, + {1000090220, 22.002998812}, + {1000100220, 21.991385113}, + {1000110220, 21.994437547}, + {1000120220, 21.999570597}, + {1000130220, 22.01954}, + {1000140220, 22.036114}, + {1000060230, 23.06889}, + {1000070230, 23.039421}, + {1000080230, 23.015696686}, + {1000090230, 23.003526875}, + {1000100230, 22.994466905}, + {1000110230, 22.98976928195}, + {1000120230, 22.994123768}, + {1000130230, 23.007244351}, + {1000140230, 23.025711}, + {1000070240, 24.05039}, + {1000080240, 24.019861}, + {1000090240, 24.00809937}, + {1000100240, 23.993610649}, + {1000110240, 23.990963012}, + {1000120240, 23.985041689}, + {1000130240, 23.999947598}, + {1000140240, 24.01153543}, + {1000150240, 24.036522}, + {1000070250, 25.0601}, + {1000080250, 25.029338919}, + {1000090250, 25.012167727}, + {1000100250, 24.997814797}, + {1000110250, 24.989953974}, + {1000120250, 24.985836966}, + {1000130250, 24.990428308}, + {1000140250, 25.004108798}, + {1000150250, 25.021675}, + {1000080260, 26.037210155}, + {1000090260, 26.020048065}, + {1000100260, 26.000516496}, + {1000110260, 25.992634649}, + {1000120260, 25.982592972}, + {1000130260, 25.986891876}, + {1000140260, 25.992333818}, + {1000150260, 26.01178}, + {1000160260, 26.029716}, + {1000080270, 27.047955}, + {1000090270, 27.026981897}, + {1000100270, 27.007569462}, + {1000110270, 26.994076408}, + {1000120270, 26.984340647}, + {1000130270, 26.981538408}, + {1000140270, 26.986704687}, + {1000150270, 26.999292499}, + {1000160270, 27.018777}, + {1000080280, 28.05591}, + {1000090280, 28.035860448}, + {1000100280, 28.012130767}, + {1000110280, 27.998939}, + {1000120280, 27.983875426}, + {1000130280, 27.981910009}, + {1000140280, 27.97692653442}, + {1000150280, 27.99232646}, + {1000160280, 28.004372762}, + {1000170280, 28.030349}, + {1000090290, 29.043103}, + {1000100290, 29.019753}, + {1000110290, 29.002877091}, + {1000120290, 28.988607163}, + {1000130290, 28.980453164}, + {1000140290, 28.97649466434}, + {1000150290, 28.981800368}, + {1000160290, 28.996678}, + {1000170290, 29.015053}, + {1000180290, 29.040761}, + {1000090300, 30.052561}, + {1000100300, 30.024992235}, + {1000110300, 30.009097931}, + {1000120300, 29.990465454}, + {1000130300, 29.982969171}, + {1000140300, 29.973770137}, + {1000150300, 29.97831349}, + {1000160300, 29.98490677}, + {1000170300, 30.005018333}, + {1000180300, 30.023694}, + {1000090310, 31.061023}, + {1000100310, 31.033474816}, + {1000110310, 31.013146654}, + {1000120310, 30.996648232}, + {1000130310, 30.983949754}, + {1000140310, 30.975363196}, + {1000150310, 30.97376199768}, + {1000160310, 30.979557002}, + {1000170310, 30.992448097}, + {1000180310, 31.012158}, + {1000190310, 31.03678}, + {1000100320, 32.03972}, + {1000110320, 32.020011024}, + {1000120320, 31.999110138}, + {1000130320, 31.988084338}, + {1000140320, 31.974151538}, + {1000150320, 31.973907643}, + {1000160320, 31.97207117354}, + {1000170320, 31.985684605}, + {1000180320, 31.997637824}, + {1000190320, 32.023607}, + {1000100330, 33.049523}, + {1000110330, 33.025529}, + {1000120330, 33.005327862}, + {1000130330, 32.990877685}, + {1000140330, 32.977976964}, + {1000150330, 32.971725692}, + {1000160330, 32.97145890862}, + {1000170330, 32.977451988}, + {1000180330, 32.989925545}, + {1000190330, 33.008095}, + {1000200330, 33.033312}, + {1000100340, 34.056728}, + {1000110340, 34.03401}, + {1000120340, 34.008935455}, + {1000130340, 33.996781924}, + {1000140340, 33.978538045}, + {1000150340, 33.973645886}, + {1000160340, 33.967867011}, + {1000170340, 33.97376249}, + {1000180340, 33.980270092}, + {1000190340, 33.99869}, + {1000200340, 34.015985}, + {1000110350, 35.040614}, + {1000120350, 35.01679}, + {1000130350, 34.999759816}, + {1000140350, 34.984550111}, + {1000150350, 34.973314045}, + {1000160350, 34.969032321}, + {1000170350, 34.968852694}, + {1000180350, 34.975257719}, + {1000190350, 34.988005406}, + {1000200350, 35.005572}, + {1000210350, 35.029093}, + {1000110360, 36.049279}, + {1000120360, 36.021879}, + {1000130360, 36.006388}, + {1000140360, 35.986649271}, + {1000150360, 35.97825961}, + {1000160360, 35.967080692}, + {1000170360, 35.968306822}, + {1000180360, 35.967545106}, + {1000190360, 35.981301887}, + {1000200360, 35.993074388}, + {1000210360, 36.017338}, + {1000110370, 37.057042}, + {1000120370, 37.030286265}, + {1000130370, 37.010531}, + {1000140370, 36.992945191}, + {1000150370, 36.979606942}, + {1000160370, 36.9711255}, + {1000170370, 36.965902573}, + {1000180370, 36.966776301}, + {1000190370, 36.97337589}, + {1000200370, 36.985897849}, + {1000210370, 37.004058}, + {1000220370, 37.027021}, + {1000110380, 38.066458}, + {1000120380, 38.03658}, + {1000130380, 38.017681}, + {1000140380, 37.995523}, + {1000150380, 37.984303105}, + {1000160380, 37.9711633}, + {1000170380, 37.968010408}, + {1000180380, 37.962732102}, + {1000190380, 37.969081114}, + {1000200380, 37.976319223}, + {1000210380, 37.995438}, + {1000220380, 38.012206}, + {1000110390, 39.075123}, + {1000120390, 39.045921}, + {1000130390, 39.02307}, + {1000140390, 39.002491}, + {1000150390, 38.986285865}, + {1000160390, 38.97513385}, + {1000170390, 38.968008151}, + {1000180390, 38.964313037}, + {1000190390, 38.96370648482}, + {1000200390, 38.970710811}, + {1000210390, 38.984784953}, + {1000220390, 39.002684}, + {1000230390, 39.02423}, + {1000120400, 40.053194}, + {1000130400, 40.03094}, + {1000140400, 40.006083641}, + {1000150400, 39.991262221}, + {1000160400, 39.975482561}, + {1000170400, 39.970415466}, + {1000180400, 39.96238312204}, + {1000190400, 39.963998165}, + {1000200400, 39.96259085}, + {1000210400, 39.977967275}, + {1000220400, 39.990345146}, + {1000230400, 40.013387}, + {1000120410, 41.062373}, + {1000130410, 41.037134}, + {1000140410, 41.014171}, + {1000150410, 40.994654}, + {1000160410, 40.979593451}, + {1000170410, 40.970684525}, + {1000180410, 40.96450057}, + {1000190410, 40.96182525611}, + {1000200410, 40.962277905}, + {1000210410, 40.969251163}, + {1000220410, 40.983148}, + {1000230410, 41.000333}, + {1000240410, 41.021911}, + {1000130420, 42.045078}, + {1000140420, 42.018078}, + {1000150420, 42.00117214}, + {1000160420, 41.9810651}, + {1000170420, 41.973342}, + {1000180420, 41.963045737}, + {1000190420, 41.962402305}, + {1000200420, 41.95861778}, + {1000210420, 41.965516686}, + {1000220420, 41.973049369}, + {1000230420, 41.99182}, + {1000240420, 42.007579}, + {1000130430, 43.05182}, + {1000140430, 43.026119}, + {1000150430, 43.005411}, + {1000160430, 42.986907635}, + {1000170430, 42.9740637}, + {1000180430, 42.965636056}, + {1000190430, 42.960734701}, + {1000200430, 42.958766381}, + {1000210430, 42.961150425}, + {1000220430, 42.96852842}, + {1000230430, 42.980766}, + {1000240430, 42.997885}, + {1000250430, 43.018647}, + {1000140440, 44.031466}, + {1000150440, 44.011927}, + {1000160440, 43.990118846}, + {1000170440, 43.978014918}, + {1000180440, 43.964923814}, + {1000190440, 43.961586984}, + {1000200440, 43.955481489}, + {1000210440, 43.959402818}, + {1000220440, 43.959689936}, + {1000230440, 43.974440977}, + {1000240440, 43.985591}, + {1000250440, 44.008009}, + {1000140450, 45.039818}, + {1000150450, 45.017134}, + {1000160450, 44.996414}, + {1000170450, 44.980394353}, + {1000180450, 44.968039731}, + {1000190450, 44.960691491}, + {1000200450, 44.95618627}, + {1000210450, 44.955907051}, + {1000220450, 44.958120758}, + {1000230450, 44.965768498}, + {1000240450, 44.97905}, + {1000250450, 44.994654}, + {1000260450, 45.015467}, + {1000150460, 46.02452}, + {1000160460, 46.000687}, + {1000170460, 45.985254926}, + {1000180460, 45.968039243999996}, + {1000190460, 45.961981584}, + {1000200460, 45.953687726}, + {1000210460, 45.955167034}, + {1000220460, 45.952626356}, + {1000230460, 45.960197389}, + {1000240460, 45.968360969}, + {1000250460, 45.986669}, + {1000260460, 46.001299}, + {1000150470, 47.030929}, + {1000160470, 47.00773}, + {1000170470, 46.989715}, + {1000180470, 46.972767112}, + {1000190470, 46.961661612}, + {1000200470, 46.954541134}, + {1000210470, 46.952402444}, + {1000220470, 46.951757491}, + {1000230470, 46.954903558}, + {1000240470, 46.962894995}, + {1000250470, 46.975774}, + {1000260470, 46.992346}, + {1000270470, 47.011401}, + {1000160480, 48.013301}, + {1000170480, 47.995405}, + {1000180480, 47.976001}, + {1000190480, 47.965341184}, + {1000200480, 47.952522654}, + {1000210480, 47.952222903}, + {1000220480, 47.947940677}, + {1000230480, 47.9522509}, + {1000240480, 47.954029431}, + {1000250480, 47.96854876}, + {1000260480, 47.980667}, + {1000270480, 48.001857}, + {1000280480, 48.019515}, + {1000160490, 49.021891}, + {1000170490, 49.000794}, + {1000180490, 48.981685}, + {1000190490, 48.968210753}, + {1000200490, 48.955662625}, + {1000210490, 48.950013159}, + {1000220490, 48.947864391}, + {1000230490, 48.948510509}, + {1000240490, 48.95133372}, + {1000250490, 48.95961335}, + {1000260490, 48.973429}, + {1000270490, 48.989501}, + {1000280490, 49.009157}, + {1000170500, 50.008266}, + {1000180500, 49.985797}, + {1000190500, 49.972380015}, + {1000200500, 49.957499215}, + {1000210500, 49.952187437}, + {1000220500, 49.944785622}, + {1000230500, 49.947156681}, + {1000240500, 49.946042209}, + {1000250500, 49.954238157}, + {1000260500, 49.962988}, + {1000270500, 49.981117}, + {1000280500, 49.996286}, + {1000170510, 51.015341}, + {1000180510, 50.993033}, + {1000190510, 50.975828664}, + {1000200510, 50.960995663}, + {1000210510, 50.953568838}, + {1000220510, 50.946609468}, + {1000230510, 50.943957664}, + {1000240510, 50.944765388}, + {1000250510, 50.94820877}, + {1000260510, 50.956855137}, + {1000270510, 50.970647}, + {1000280510, 50.987493}, + {1000170520, 52.024004}, + {1000180520, 51.998519}, + {1000190520, 51.981602}, + {1000200520, 51.963213646}, + {1000210520, 51.95649617}, + {1000220520, 51.946883509}, + {1000230520, 51.944773636}, + {1000240520, 51.940504714}, + {1000250520, 51.94555909}, + {1000260520, 51.948113364}, + {1000270520, 51.963130224}, + {1000280520, 51.975781}, + {1000290520, 51.997982}, + {1000180530, 53.00729}, + {1000190530, 52.9868}, + {1000200530, 52.968451}, + {1000210530, 52.958379173}, + {1000220530, 52.949670714}, + {1000230530, 52.94433494}, + {1000240530, 52.940646304}, + {1000250530, 52.941287497}, + {1000260530, 52.945305629}, + {1000270530, 52.954203278}, + {1000280530, 52.96819}, + {1000290530, 52.985894}, + {1000180540, 54.013484}, + {1000190540, 53.994471}, + {1000200540, 53.972989}, + {1000210540, 53.963029359}, + {1000220540, 53.950892}, + {1000230540, 53.946432009}, + {1000240540, 53.938877359}, + {1000250540, 53.940355772}, + {1000260540, 53.939608189}, + {1000270540, 53.948459075}, + {1000280540, 53.957833}, + {1000290540, 53.977198}, + {1000300540, 53.993879}, + {1000190550, 55.000505}, + {1000200550, 54.979978}, + {1000210550, 54.966889637}, + {1000220550, 54.955091}, + {1000230550, 54.947262}, + {1000240550, 54.940836637}, + {1000250550, 54.93804304}, + {1000260550, 54.938291158}, + {1000270550, 54.941996416}, + {1000280550, 54.951329846}, + {1000290550, 54.966038}, + {1000300550, 54.984681}, + {1000190560, 56.008567}, + {1000200560, 55.985496}, + {1000210560, 55.972607611}, + {1000220560, 55.957677675}, + {1000230560, 55.950420082}, + {1000240560, 55.940648977}, + {1000250560, 55.938902816}, + {1000260560, 55.934935537}, + {1000270560, 55.939838032}, + {1000280560, 55.942127761}, + {1000290560, 55.958529278}, + {1000300560, 55.972743}, + {1000310560, 55.995878}, + {1000190570, 57.015169}, + {1000200570, 56.992958}, + {1000210570, 56.977047999999996}, + {1000220570, 56.963068098}, + {1000230570, 56.952297}, + {1000240570, 56.943612112}, + {1000250570, 56.938285944}, + {1000260570, 56.93539195}, + {1000270570, 56.936289819}, + {1000280570, 56.939791394}, + {1000290570, 56.949211686}, + {1000300570, 56.965056}, + {1000310570, 56.983457}, + {1000190580, 58.023543}, + {1000200580, 57.998357}, + {1000210580, 57.983382}, + {1000220580, 57.966808519}, + {1000230580, 57.956595985}, + {1000240580, 57.944184501}, + {1000250580, 57.940066643}, + {1000260580, 57.933273575}, + {1000270580, 57.935751292}, + {1000280580, 57.93534165}, + {1000290580, 57.944532283}, + {1000300580, 57.954590296}, + {1000310580, 57.974728999999996}, + {1000320580, 57.991863}, + {1000190590, 59.030864}, + {1000200590, 59.006237}, + {1000210590, 58.988374}, + {1000220590, 58.972217}, + {1000230590, 58.959623343}, + {1000240590, 58.948345426}, + {1000250590, 58.940391111}, + {1000260590, 58.934873492}, + {1000270590, 58.933193523999996}, + {1000280590, 58.934345442}, + {1000290590, 58.939496713}, + {1000300590, 58.949311886}, + {1000310590, 58.963757}, + {1000320590, 58.982426}, + {1000200600, 60.011809}, + {1000210600, 59.995115}, + {1000220600, 59.976275}, + {1000230600, 59.964479215}, + {1000240600, 59.949641656}, + {1000250600, 59.943136574}, + {1000260600, 59.934070249}, + {1000270600, 59.933815536}, + {1000280600, 59.930785129}, + {1000290600, 59.937363787}, + {1000300600, 59.941841317}, + {1000310600, 59.957498}, + {1000320600, 59.970445}, + {1000330600, 59.993945}, + {1000200610, 61.020408}, + {1000210610, 61.000537}, + {1000220610, 60.982426}, + {1000230610, 60.967603529}, + {1000240610, 60.95437813}, + {1000250610, 60.944452541}, + {1000260610, 60.936746241}, + {1000270610, 60.932476031}, + {1000280610, 60.931054819}, + {1000290610, 60.933457375}, + {1000300610, 60.939506964}, + {1000310610, 60.949398861}, + {1000320610, 60.963725}, + {1000330610, 60.981535}, + {1000210620, 62.007848}, + {1000220620, 61.986903}, + {1000230620, 61.972932556}, + {1000240620, 61.95614292}, + {1000250620, 61.947907384}, + {1000260620, 61.936791809}, + {1000270620, 61.934058198}, + {1000280620, 61.928344753}, + {1000290620, 61.932594803}, + {1000300620, 61.934333359}, + {1000310620, 61.944189639}, + {1000320620, 61.954761}, + {1000330620, 61.973784}, + {1000210630, 63.014031}, + {1000220630, 62.993709}, + {1000230630, 62.976661}, + {1000240630, 62.961161}, + {1000250630, 62.949664672}, + {1000260630, 62.940272698}, + {1000270630, 62.93359963}, + {1000280630, 62.929669021}, + {1000290630, 62.929597119}, + {1000300630, 62.93321114}, + {1000310630, 62.939294194}, + {1000320630, 62.949628}, + {1000330630, 62.964036}, + {1000340630, 62.981911}, + {1000220640, 63.998411}, + {1000230640, 63.98248}, + {1000240640, 63.963886}, + {1000250640, 63.953849369}, + {1000260640, 63.940987761}, + {1000270640, 63.935810176}, + {1000280640, 63.927966228}, + {1000290640, 63.929764001}, + {1000300640, 63.929141776}, + {1000310640, 63.936840366}, + {1000320640, 63.941689912}, + {1000330640, 63.95756}, + {1000340640, 63.971165}, + {1000220650, 65.005593}, + {1000230650, 64.986999}, + {1000240650, 64.969608}, + {1000250650, 64.956019749}, + {1000260650, 64.945015323}, + {1000270650, 64.936462071}, + {1000280650, 64.930084585}, + {1000290650, 64.927789476}, + {1000300650, 64.929240534}, + {1000310650, 64.932734424}, + {1000320650, 64.939368136}, + {1000330650, 64.949611}, + {1000340650, 64.964552}, + {1000350650, 64.982297}, + {1000230660, 65.993237}, + {1000240660, 65.973011}, + {1000250660, 65.960546833}, + {1000260660, 65.946249958}, + {1000270660, 65.939442943}, + {1000280660, 65.929139333}, + {1000290660, 65.928868804}, + {1000300660, 65.926033639}, + {1000310660, 65.931589766}, + {1000320660, 65.933862124}, + {1000330660, 65.944148778}, + {1000340660, 65.955276}, + {1000350660, 65.974697}, + {1000230670, 66.998128}, + {1000240670, 66.979313}, + {1000250670, 66.96395}, + {1000260670, 66.95093}, + {1000270670, 66.940609625}, + {1000280670, 66.931569413}, + {1000290670, 66.92772949}, + {1000300670, 66.927127422}, + {1000310670, 66.928202276}, + {1000320670, 66.932716999}, + {1000330670, 66.93925111}, + {1000340670, 66.949994}, + {1000350670, 66.965078}, + {1000360670, 66.983305}, + {1000240680, 67.983156}, + {1000250680, 67.968953}, + {1000260680, 67.952875}, + {1000270680, 67.944559401}, + {1000280680, 67.931868787}, + {1000290680, 67.929610887}, + {1000300680, 67.924844232}, + {1000310680, 67.927980161}, + {1000320680, 67.928095305}, + {1000330680, 67.936774127}, + {1000340680, 67.941825236}, + {1000350680, 67.958356}, + {1000360680, 67.972489}, + {1000240690, 68.989662}, + {1000250690, 68.972775}, + {1000260690, 68.957918}, + {1000270690, 68.945909}, + {1000280690, 68.935610267}, + {1000290690, 68.929429267}, + {1000300690, 68.92655036}, + {1000310690, 68.925573528}, + {1000320690, 68.927964467}, + {1000330690, 68.932246289}, + {1000340690, 68.939414845}, + {1000350690, 68.95033841}, + {1000360690, 68.965496}, + {1000240700, 69.993945}, + {1000250700, 69.978046}, + {1000260700, 69.960397}, + {1000270700, 69.9500534}, + {1000280700, 69.9364313}, + {1000290700, 69.932392078}, + {1000300700, 69.925319175}, + {1000310700, 69.926021914}, + {1000320700, 69.924248542}, + {1000330700, 69.930934642}, + {1000340700, 69.933515521}, + {1000350700, 69.944792321}, + {1000360700, 69.955877}, + {1000250710, 70.982158}, + {1000260710, 70.965722}, + {1000270710, 70.952366923}, + {1000280710, 70.940518962}, + {1000290710, 70.932676831}, + {1000300710, 70.927719578}, + {1000310710, 70.924702554}, + {1000320710, 70.92495212}, + {1000330710, 70.927113594}, + {1000340710, 70.932209431}, + {1000350710, 70.939342153}, + {1000360710, 70.950265695}, + {1000370710, 70.965335}, + {1000250720, 71.988009}, + {1000260720, 71.968599}, + {1000270720, 71.956736}, + {1000280720, 71.941785924}, + {1000290720, 71.935820306}, + {1000300720, 71.926842806}, + {1000310720, 71.926367452}, + {1000320720, 71.922075824}, + {1000330720, 71.926752291}, + {1000340720, 71.927140506}, + {1000350720, 71.936594606}, + {1000360720, 71.942092406}, + {1000370720, 71.958851}, + {1000250730, 72.992807}, + {1000260730, 72.974246}, + {1000270730, 72.959238}, + {1000280730, 72.946206681}, + {1000290730, 72.936674376}, + {1000300730, 72.92958258}, + {1000310730, 72.92517468}, + {1000320730, 72.923458954}, + {1000330730, 72.923829086}, + {1000340730, 72.926754881}, + {1000350730, 72.931673441}, + {1000360730, 72.939289193}, + {1000370730, 72.950604506}, + {1000380730, 72.9657}, + {1000260740, 73.977821}, + {1000270740, 73.963993}, + {1000280740, 73.947718}, + {1000290740, 73.93987486}, + {1000300740, 73.92940726}, + {1000310740, 73.926945725}, + {1000320740, 73.92117776}, + {1000330740, 73.923928596}, + {1000340740, 73.922475933}, + {1000350740, 73.929910279}, + {1000360740, 73.933084016}, + {1000370740, 73.944265867}, + {1000380740, 73.95617}, + {1000260750, 74.984219}, + {1000270750, 74.967192}, + {1000280750, 74.952506}, + {1000290750, 74.941523817}, + {1000300750, 74.932840244}, + {1000310750, 74.926504484}, + {1000320750, 74.92285837}, + {1000330750, 74.921594562}, + {1000340750, 74.92252287}, + {1000350750, 74.925810566}, + {1000360750, 74.930945744}, + {1000370750, 74.9385732}, + {1000380750, 74.949952767}, + {1000390750, 74.96584}, + {1000260760, 75.988631}, + {1000270760, 75.972453}, + {1000280760, 75.954707}, + {1000290760, 75.945268974}, + {1000300760, 75.933114956}, + {1000310760, 75.928827624}, + {1000320760, 75.921402725}, + {1000330760, 75.922392011}, + {1000340760, 75.919213702}, + {1000350760, 75.924541574}, + {1000360760, 75.925910743}, + {1000370760, 75.935073031}, + {1000380760, 75.94176276}, + {1000390760, 75.958937}, + {1000270770, 76.976479}, + {1000280770, 76.959903}, + {1000290770, 76.947543599}, + {1000300770, 76.936887197}, + {1000310770, 76.929154299}, + {1000320770, 76.923549843}, + {1000330770, 76.920647555}, + {1000340770, 76.91991415}, + {1000350770, 76.921379193}, + {1000360770, 76.924669999}, + {1000370770, 76.930401599}, + {1000380770, 76.937945454}, + {1000390770, 76.950146}, + {1000400770, 76.966076}, + {1000270780, 77.983553}, + {1000280780, 77.962555}, + {1000290780, 77.951916524}, + {1000300780, 77.938289204}, + {1000310780, 77.931610854}, + {1000320780, 77.922852911}, + {1000330780, 77.921827771}, + {1000340780, 77.917309244}, + {1000350780, 77.921145858}, + {1000360780, 77.920366341}, + {1000370780, 77.928141866}, + {1000380780, 77.932179979}, + {1000390780, 77.94399}, + {1000400780, 77.956146}, + {1000280790, 78.969769}, + {1000290790, 78.9544731}, + {1000300790, 78.942638067}, + {1000310790, 78.932851582}, + {1000320790, 78.925359506}, + {1000330790, 78.920948419}, + {1000340790, 78.918499252}, + {1000350790, 78.918337574}, + {1000360790, 78.920082919}, + {1000370790, 78.923990095}, + {1000380790, 78.929704692}, + {1000390790, 78.937946}, + {1000400790, 78.94979}, + {1000410790, 78.966022}, + {1000280800, 79.975051}, + {1000290800, 79.960623}, + {1000300800, 79.944552929}, + {1000310800, 79.936420773}, + {1000320800, 79.925350773}, + {1000330800, 79.92247444}, + {1000340800, 79.916521761}, + {1000350800, 79.918529784}, + {1000360800, 79.91637794}, + {1000370800, 79.922516442}, + {1000380800, 79.924517538}, + {1000390800, 79.93435475}, + {1000400800, 79.941213}, + {1000410800, 79.958754}, + {1000280810, 80.982727}, + {1000290810, 80.965743}, + {1000300810, 80.950402617}, + {1000310810, 80.938133841}, + {1000320810, 80.928832941}, + {1000330810, 80.922132288}, + {1000340810, 80.917993019}, + {1000350810, 80.916288197}, + {1000360810, 80.916589703}, + {1000370810, 80.9189939}, + {1000380810, 80.923211393}, + {1000390810, 80.929454283}, + {1000400810, 80.938245}, + {1000410810, 80.95023}, + {1000420810, 80.966226}, + {1000280820, 81.988492}, + {1000290820, 81.972378}, + {1000300820, 81.954574097}, + {1000310820, 81.943176531}, + {1000320820, 81.929774031}, + {1000330820, 81.924738731}, + {1000340820, 81.916699531}, + {1000350820, 81.916801752}, + {1000360820, 81.91348115368}, + {1000370820, 81.918209023}, + {1000380820, 81.918399845}, + {1000390820, 81.926930189}, + {1000400820, 81.931707497}, + {1000410820, 81.94438}, + {1000420820, 81.956661}, + {1000290830, 82.97811}, + {1000300830, 82.961041}, + {1000310830, 82.9471203}, + {1000320830, 82.9345391}, + {1000330830, 82.9252069}, + {1000340830, 82.919118604}, + {1000350830, 82.915175285}, + {1000360830, 82.914126516}, + {1000370830, 82.915114181}, + {1000380830, 82.917554372}, + {1000390830, 82.922484026}, + {1000400830, 82.929240926}, + {1000410830, 82.93815}, + {1000420830, 82.950252}, + {1000430830, 82.966377}, + {1000290840, 83.985271}, + {1000300840, 83.965829}, + {1000310840, 83.952663}, + {1000320840, 83.93757509}, + {1000330840, 83.92930329000001}, + {1000340840, 83.918466761}, + {1000350840, 83.916496417}, + {1000360840, 83.91149772708}, + {1000370840, 83.914375223}, + {1000380840, 83.913419118}, + {1000390840, 83.92067106}, + {1000400840, 83.923325663}, + {1000410840, 83.934305711}, + {1000420840, 83.941846}, + {1000430840, 83.959527}, + {1000300850, 84.973054}, + {1000310850, 84.957333}, + {1000320850, 84.942969658}, + {1000330850, 84.932163658}, + {1000340850, 84.922260758}, + {1000350850, 84.915645758}, + {1000360850, 84.91252726}, + {1000370850, 84.91178973604}, + {1000380850, 84.912932041}, + {1000390850, 84.916433039}, + {1000400850, 84.921443199}, + {1000410850, 84.928845836}, + {1000420850, 84.938260736}, + {1000430850, 84.950778}, + {1000440850, 84.967117}, + {1000300860, 85.978463}, + {1000310860, 85.963757}, + {1000320860, 85.946967}, + {1000330860, 85.936701532}, + {1000340860, 85.924311732}, + {1000350860, 85.918805432}, + {1000360860, 85.91061062468}, + {1000370860, 85.911167443}, + {1000380860, 85.90926072473}, + {1000390860, 85.914886095}, + {1000400860, 85.916296814}, + {1000410860, 85.925781536}, + {1000420860, 85.931174092}, + {1000430860, 85.944637}, + {1000440860, 85.957305}, + {1000310870, 86.969007}, + {1000320870, 86.953204}, + {1000330870, 86.940291716}, + {1000340870, 86.928688616}, + {1000350870, 86.920674016}, + {1000360870, 86.913354759}, + {1000370870, 86.909180529}, + {1000380870, 86.90887749454}, + {1000390870, 86.9108761}, + {1000400870, 86.914817338}, + {1000410870, 86.920692473}, + {1000420870, 86.928196198}, + {1000430870, 86.938067185}, + {1000440870, 86.950907}, + {1000310880, 87.975963}, + {1000320880, 87.957574}, + {1000330880, 87.94584}, + {1000340880, 87.93141749}, + {1000350880, 87.92408329}, + {1000360880, 87.914447879}, + {1000370880, 87.91131559}, + {1000380880, 87.905612253}, + {1000390880, 87.909501274}, + {1000400880, 87.910220715}, + {1000410880, 87.918226476}, + {1000420880, 87.921967779}, + {1000430880, 87.933794211}, + {1000440880, 87.941664}, + {1000450880, 87.960429}, + {1000320890, 88.96453}, + {1000330890, 88.950048}, + {1000340890, 88.936669058}, + {1000350890, 88.926704558}, + {1000360890, 88.917835449}, + {1000370890, 88.912278136}, + {1000380890, 88.907450808}, + {1000390890, 88.905838156}, + {1000400890, 88.908879751}, + {1000410890, 88.913444696}, + {1000420890, 88.919468149}, + {1000430890, 88.927648649}, + {1000440890, 88.937337849}, + {1000450890, 88.950992}, + {1000320900, 89.969436}, + {1000330900, 89.955995}, + {1000340900, 89.940096}, + {1000350900, 89.931292848}, + {1000360900, 89.919527929}, + {1000370900, 89.914797557}, + {1000380900, 89.90772787}, + {1000390900, 89.907141749}, + {1000400900, 89.904698755}, + {1000410900, 89.911259201}, + {1000420900, 89.91393127}, + {1000430900, 89.924073919}, + {1000440900, 89.930344378}, + {1000450900, 89.944569}, + {1000460900, 89.95737}, + {1000330910, 90.960816}, + {1000340910, 90.9457}, + {1000350910, 90.934398617}, + {1000360910, 90.923806309}, + {1000370910, 90.916537261}, + {1000380910, 90.910195942}, + {1000390910, 90.907298048}, + {1000400910, 90.905640205}, + {1000410910, 90.906990256}, + {1000420910, 90.91174519}, + {1000430910, 90.918424972}, + {1000440910, 90.92674153}, + {1000450910, 90.937123}, + {1000460910, 90.950435}, + {1000330920, 91.967386}, + {1000340920, 91.94984}, + {1000350920, 91.939631595}, + {1000360920, 91.926173092}, + {1000370920, 91.919728477}, + {1000380920, 91.911038222}, + {1000390920, 91.908945752}, + {1000400920, 91.905035336}, + {1000410920, 91.90718858}, + {1000420920, 91.906807153}, + {1000430920, 91.915269777}, + {1000440920, 91.920234373}, + {1000450920, 91.932367692}, + {1000460920, 91.941192225}, + {1000470920, 91.95971}, + {1000340930, 92.956135}, + {1000350930, 92.94322}, + {1000360930, 92.931147172}, + {1000370930, 92.922039334}, + {1000380930, 92.914024314}, + {1000390930, 92.909578434}, + {1000400930, 92.906470661}, + {1000410930, 92.90637317}, + {1000420930, 92.906808772}, + {1000430930, 92.910245147}, + {1000440930, 92.917104442}, + {1000450930, 92.925912778}, + {1000460930, 92.936680426}, + {1000470930, 92.950188}, + {1000340940, 93.96049}, + {1000350940, 93.948846}, + {1000360940, 93.934140452}, + {1000370940, 93.926394819}, + {1000380940, 93.915355641}, + {1000390940, 93.911592062}, + {1000400940, 93.906312523}, + {1000410940, 93.907279001}, + {1000420940, 93.905083586}, + {1000430940, 93.909652319}, + {1000440940, 93.91134286}, + {1000450940, 93.92173045}, + {1000460940, 93.929036286}, + {1000470940, 93.943744}, + {1000480940, 93.956586}, + {1000340950, 94.9673}, + {1000350950, 94.952925}, + {1000360950, 94.939710922}, + {1000370950, 94.929263849}, + {1000380950, 94.919358282}, + {1000390950, 94.912819697}, + {1000400950, 94.908040276}, + {1000410950, 94.90683111}, + {1000420950, 94.905837436}, + {1000430950, 94.907652281}, + {1000440950, 94.910404415}, + {1000450950, 94.915897893}, + {1000460950, 94.924888506}, + {1000470950, 94.935688}, + {1000480950, 94.949483}, + {1000350960, 95.95898}, + {1000360960, 95.943014473}, + {1000370960, 95.934133398}, + {1000380960, 95.921719045}, + {1000390960, 95.915909305}, + {1000400960, 95.908277615}, + {1000410960, 95.908101586}, + {1000420960, 95.90467477}, + {1000430960, 95.907866675}, + {1000440960, 95.90758891}, + {1000450960, 95.914451705}, + {1000460960, 95.918213739}, + {1000470960, 95.930743903}, + {1000480960, 95.940341}, + {1000490960, 95.959109}, + {1000350970, 96.963499}, + {1000360970, 96.949088782}, + {1000370970, 96.937177117}, + {1000380970, 96.926375621}, + {1000390970, 96.918286702}, + {1000400970, 96.910963802}, + {1000410970, 96.908101622}, + {1000420970, 96.906016903}, + {1000430970, 96.90636072}, + {1000440970, 96.907545776}, + {1000450970, 96.911327872}, + {1000460970, 96.916471985}, + {1000470970, 96.9238814}, + {1000480970, 96.934799343}, + {1000490970, 96.949125}, + {1000350980, 97.969887}, + {1000360980, 97.952635}, + {1000370980, 97.941632317}, + {1000380980, 97.928692636}, + {1000390980, 97.922394841}, + {1000400980, 97.912740448}, + {1000410980, 97.910332645}, + {1000420980, 97.905403609}, + {1000430980, 97.907211206}, + {1000440980, 97.905286709}, + {1000450980, 97.910707734}, + {1000460980, 97.912698335}, + {1000470980, 97.92155997}, + {1000480980, 97.927389315}, + {1000490980, 97.942129}, + {1000360990, 98.958776}, + {1000370990, 98.94511919}, + {1000380990, 98.932883604}, + {1000390990, 98.924160839}, + {1000400990, 98.916675081}, + {1000410990, 98.911609377}, + {1000420990, 98.907707299}, + {1000430990, 98.906249681}, + {1000440990, 98.905930284}, + {1000450990, 98.908121241}, + {1000460990, 98.911773073}, + {1000470990, 98.917645766}, + {1000480990, 98.924925845}, + {1000490990, 98.93411}, + {1000500990, 98.948495}, + {1000361000, 99.962995}, + {1000371000, 99.950331532}, + {1000381000, 99.93578327}, + {1000391000, 99.927727678}, + {1000401000, 99.918010499}, + {1000411000, 99.914340578}, + {1000421000, 99.907467982}, + {1000431000, 99.907652715}, + {1000441000, 99.90421046}, + {1000451000, 99.908114147}, + {1000461000, 99.908520438}, + {1000471000, 99.916115443}, + {1000481000, 99.920348829}, + {1000491000, 99.931101929}, + {1000501000, 99.938648944}, + {1000361010, 100.969318}, + {1000371010, 100.954302}, + {1000381010, 100.940606264}, + {1000391010, 100.930160817}, + {1000401010, 100.921458454}, + {1000411010, 100.915306508}, + {1000421010, 100.910337648}, + {1000431010, 100.907305271}, + {1000441010, 100.905573086}, + {1000451010, 100.906158903}, + {1000461010, 100.908284824}, + {1000471010, 100.912683951}, + {1000481010, 100.918586209}, + {1000491010, 100.926414025}, + {1000501010, 100.935259252}, + {1000371020, 101.960008}, + {1000381020, 101.944004679}, + {1000391020, 101.934328471}, + {1000401020, 101.923154181}, + {1000411020, 101.918090447}, + {1000421020, 101.910293725}, + {1000431020, 101.909207239}, + {1000441020, 101.904340312}, + {1000451020, 101.906834282}, + {1000461020, 101.905632292}, + {1000471020, 101.911704538}, + {1000481020, 101.914481797}, + {1000491020, 101.924105911}, + {1000501020, 101.930289525}, + {1000511020, 101.945142}, + {1000371030, 102.964401}, + {1000381030, 102.949243}, + {1000391030, 102.937243796}, + {1000401030, 102.927204054}, + {1000411030, 102.919453416}, + {1000421030, 102.913091954}, + {1000431030, 102.90917396}, + {1000441030, 102.906314846}, + {1000451030, 102.905494081}, + {1000461030, 102.906111074}, + {1000471030, 102.908960558}, + {1000481030, 102.913416922}, + {1000491030, 102.91987883}, + {1000501030, 102.927973}, + {1000511030, 102.939162}, + {1000371040, 103.970531}, + {1000381040, 103.953022}, + {1000391040, 103.941943}, + {1000401040, 103.929449193}, + {1000411040, 103.922907728}, + {1000421040, 103.913747443}, + {1000431040, 103.911433718}, + {1000441040, 103.905425312}, + {1000451040, 103.906645309}, + {1000461040, 103.904030393}, + {1000471040, 103.908623715}, + {1000481040, 103.909856228}, + {1000491040, 103.918214538}, + {1000501040, 103.923105195}, + {1000511040, 103.936344}, + {1000521040, 103.946723408}, + {1000381050, 104.959001}, + {1000391050, 104.945711}, + {1000401050, 104.934021832}, + {1000411050, 104.924942577}, + {1000421050, 104.91698198899999}, + {1000431050, 104.911662024}, + {1000441050, 104.907745478}, + {1000451050, 104.905687787}, + {1000461050, 104.905079479}, + {1000471050, 104.906525604}, + {1000481050, 104.909463893}, + {1000491050, 104.914502322}, + {1000501050, 104.921268421}, + {1000511050, 104.931276547}, + {1000521050, 104.943304516}, + {1000381060, 105.963177}, + {1000391060, 105.950842}, + {1000401060, 105.93693}, + {1000411060, 105.928928505}, + {1000421060, 105.918273231}, + {1000431060, 105.914356674}, + {1000441060, 105.907328181}, + {1000451060, 105.907285879}, + {1000461060, 105.903480287}, + {1000471060, 105.906663499}, + {1000481060, 105.906459791}, + {1000491060, 105.913463596}, + {1000501060, 105.916957394}, + {1000511060, 105.928637979}, + {1000521060, 105.937498521}, + {1000531060, 105.953516}, + {1000381070, 106.969672}, + {1000391070, 106.954943}, + {1000401070, 106.942007}, + {1000411070, 106.931589685}, + {1000421070, 106.92211977}, + {1000431070, 106.915458437}, + {1000441070, 106.909969837}, + {1000451070, 106.906747975}, + {1000461070, 106.905128058}, + {1000471070, 106.905091509}, + {1000481070, 106.906612049}, + {1000491070, 106.910287497}, + {1000501070, 106.915713649}, + {1000511070, 106.924150621}, + {1000521070, 106.934882}, + {1000531070, 106.946935}, + {1000391080, 107.960515}, + {1000401080, 107.945303}, + {1000411080, 107.936075604}, + {1000421080, 107.924047508}, + {1000431080, 107.918493493}, + {1000441080, 107.910185793}, + {1000451080, 107.908715304}, + {1000461080, 107.903891806}, + {1000471080, 107.905950245}, + {1000481080, 107.904183588}, + {1000491080, 107.909693654}, + {1000501080, 107.91189429}, + {1000511080, 107.922226731}, + {1000521080, 107.929380469}, + {1000531080, 107.943348}, + {1000541080, 107.954232285}, + {1000391090, 108.965131}, + {1000401090, 108.950907}, + {1000411090, 108.939141}, + {1000421090, 108.928438318}, + {1000431090, 108.920254107}, + {1000441090, 108.913323707}, + {1000451090, 108.908749555}, + {1000461090, 108.905950576}, + {1000471090, 108.904755778}, + {1000481090, 108.904986697}, + {1000491090, 108.907149679}, + {1000501090, 108.911292857}, + {1000511090, 108.918141203}, + {1000521090, 108.927304532}, + {1000531090, 108.938086022}, + {1000541090, 108.950434955}, + {1000401100, 109.954675}, + {1000411100, 109.943843}, + {1000421100, 109.930717956}, + {1000431100, 109.923741263}, + {1000441100, 109.914038501}, + {1000451100, 109.911079745}, + {1000461100, 109.905172878}, + {1000471100, 109.906110724}, + {1000481100, 109.90300747}, + {1000491100, 109.907170674}, + {1000501100, 109.907844835}, + {1000511100, 109.916854283}, + {1000521100, 109.922458102}, + {1000531100, 109.935085102}, + {1000541100, 109.944258759}, + {1000401110, 110.960837}, + {1000411110, 110.947439}, + {1000421110, 110.935651966}, + {1000431110, 110.925898966}, + {1000441110, 110.917567566}, + {1000451110, 110.911643164}, + {1000461110, 110.907690358}, + {1000471110, 110.905296827}, + {1000481110, 110.904183776}, + {1000491110, 110.905107236}, + {1000501110, 110.907741143}, + {1000511110, 110.913218187}, + {1000521110, 110.921000587}, + {1000531110, 110.930269236}, + {1000541110, 110.94147}, + {1000551110, 110.953945}, + {1000401120, 111.965196}, + {1000411120, 111.952689}, + {1000421120, 111.938293}, + {1000431120, 111.929941658}, + {1000441120, 111.918806922}, + {1000451120, 111.914405199}, + {1000461120, 111.907330557}, + {1000471120, 111.907048548}, + {1000481120, 111.902763896}, + {1000491120, 111.905538718}, + {1000501120, 111.904824894}, + {1000511120, 111.912399903}, + {1000521120, 111.916727848}, + {1000531120, 111.928004548}, + {1000541120, 111.935559068}, + {1000551120, 111.950172}, + {1000401130, 112.971723}, + {1000411130, 112.956833}, + {1000421130, 112.943478}, + {1000431130, 112.932569032}, + {1000441130, 112.922846729}, + {1000451130, 112.915440212}, + {1000461130, 112.910261912}, + {1000471130, 112.906572865}, + {1000481130, 112.904408105}, + {1000491130, 112.904060451}, + {1000501130, 112.905175857}, + {1000511130, 112.909374664}, + {1000521130, 112.915891}, + {1000531130, 112.923650062}, + {1000541130, 112.933221663}, + {1000551130, 112.944428484}, + {1000561130, 112.95737}, + {1000411140, 113.962469}, + {1000421140, 113.946666}, + {1000431140, 113.93709}, + {1000441140, 113.92461443}, + {1000451140, 113.91872168}, + {1000461140, 113.91036943}, + {1000471140, 113.908823029}, + {1000481140, 113.903364998}, + {1000491140, 113.904916405}, + {1000501140, 113.90278013}, + {1000511140, 113.909289155}, + {1000521140, 113.91208782}, + {1000531140, 113.9220189}, + {1000541140, 113.927980329}, + {1000551140, 113.941292244}, + {1000561140, 113.950718489}, + {1000411150, 114.966849}, + {1000421150, 114.952174}, + {1000431150, 114.9401}, + {1000441150, 114.929033049}, + {1000451150, 114.920311649}, + {1000461150, 114.913659333}, + {1000471150, 114.908767445}, + {1000481150, 114.905437426}, + {1000491150, 114.903878772}, + {1000501150, 114.903344695}, + {1000511150, 114.906598}, + {1000521150, 114.911902}, + {1000531150, 114.918048}, + {1000541150, 114.926293943}, + {1000551150, 114.93591}, + {1000561150, 114.947482}, + {1000411160, 115.972914}, + {1000421160, 115.955759}, + {1000431160, 115.94502}, + {1000441160, 115.931219191}, + {1000451160, 115.92406206}, + {1000461160, 115.914297872}, + {1000471160, 115.911386809}, + {1000481160, 115.90476323}, + {1000491160, 115.905259992}, + {1000501160, 115.901742825}, + {1000511160, 115.906792732}, + {1000521160, 115.908465558}, + {1000531160, 115.916885513}, + {1000541160, 115.921580955}, + {1000551160, 115.933395}, + {1000561160, 115.941621}, + {1000571160, 115.957005}, + {1000421170, 116.961686}, + {1000431170, 116.94832}, + {1000441170, 116.936135}, + {1000451170, 116.926036291}, + {1000461170, 116.917955584}, + {1000471170, 116.911774086}, + {1000481170, 116.907226039}, + {1000491170, 116.904515729}, + {1000501170, 116.902954036}, + {1000511170, 116.904841519}, + {1000521170, 116.908646227}, + {1000531170, 116.913645649}, + {1000541170, 116.920358758}, + {1000551170, 116.928616723}, + {1000561170, 116.938316403}, + {1000571170, 116.950326}, + {1000421180, 117.965249}, + {1000431180, 117.953526}, + {1000441180, 117.938808}, + {1000451180, 117.930341116}, + {1000461180, 117.919067273}, + {1000471180, 117.914595484}, + {1000481180, 117.906921956}, + {1000491180, 117.906356705}, + {1000501180, 117.90160663}, + {1000511180, 117.905532194}, + {1000521180, 117.905860104}, + {1000531180, 117.913074}, + {1000541180, 117.916178678}, + {1000551180, 117.926559517}, + {1000561180, 117.933226}, + {1000571180, 117.946731}, + {1000421190, 118.971465}, + {1000431190, 118.956876}, + {1000441190, 118.94409}, + {1000451190, 118.932556951}, + {1000461190, 118.923341138}, + {1000471190, 118.915570309}, + {1000481190, 118.909847052}, + {1000491190, 118.905851622}, + {1000501190, 118.903311266}, + {1000511190, 118.903944062}, + {1000521190, 118.906405699}, + {1000531190, 118.91006091}, + {1000541190, 118.915410641}, + {1000551190, 118.922377327}, + {1000561190, 118.930659683}, + {1000571190, 118.940934}, + {1000581190, 118.952957}, + {1000431200, 119.962426}, + {1000441200, 119.946623}, + {1000451200, 119.937069}, + {1000461200, 119.924551745}, + {1000471200, 119.918784765}, + {1000481200, 119.909868065}, + {1000491200, 119.907967489}, + {1000501200, 119.902202557}, + {1000511200, 119.905080308}, + {1000521200, 119.904065779}, + {1000531200, 119.910093729}, + {1000541200, 119.911784267}, + {1000551200, 119.920677277}, + {1000561200, 119.926044997}, + {1000571200, 119.938196}, + {1000581200, 119.946613}, + {1000431210, 120.96614}, + {1000441210, 120.952098}, + {1000451210, 120.939613}, + {1000461210, 120.928950342}, + {1000471210, 120.920125279}, + {1000481210, 120.91296366}, + {1000491210, 120.907852778}, + {1000501210, 120.904243488}, + {1000511210, 120.903811353}, + {1000521210, 120.904945065}, + {1000531210, 120.907411492}, + {1000541210, 120.911453012}, + {1000551210, 120.917227235}, + {1000561210, 120.924052286}, + {1000571210, 120.933236}, + {1000581210, 120.943435}, + {1000591210, 120.955393}, + {1000431220, 121.97176}, + {1000441220, 121.955147}, + {1000451220, 121.944305}, + {1000461220, 121.930631693}, + {1000471220, 121.923664446}, + {1000481220, 121.91345905}, + {1000491220, 121.910282458}, + {1000501220, 121.903445494}, + {1000511220, 121.905169335}, + {1000521220, 121.903044708}, + {1000531220, 121.907590094}, + {1000541220, 121.908367655}, + {1000551220, 121.916108144}, + {1000561220, 121.919904}, + {1000571220, 121.93071}, + {1000581220, 121.93787}, + {1000591220, 121.951927}, + {1000441230, 122.960762}, + {1000451230, 122.947192}, + {1000461230, 122.935126}, + {1000471230, 122.92531506}, + {1000481230, 122.91689246}, + {1000491230, 122.910435252}, + {1000501230, 122.905727065}, + {1000511230, 122.904215292}, + {1000521230, 122.904271022}, + {1000531230, 122.905589753}, + {1000541230, 122.908482235}, + {1000551230, 122.91299606}, + {1000561230, 122.91878106}, + {1000571230, 122.9263}, + {1000581230, 122.93528}, + {1000591230, 122.946076}, + {1000441240, 123.96394}, + {1000451240, 123.952002}, + {1000461240, 123.937305}, + {1000471240, 123.928899227}, + {1000481240, 123.917659772}, + {1000491240, 123.913184873}, + {1000501240, 123.905279619}, + {1000511240, 123.905937065}, + {1000521240, 123.902818341}, + {1000531240, 123.906210297}, + {1000541240, 123.905885174}, + {1000551240, 123.912247366}, + {1000561240, 123.915093627}, + {1000571240, 123.924574275}, + {1000581240, 123.93031}, + {1000591240, 123.94294}, + {1000601240, 123.951873}, + {1000441250, 124.969544}, + {1000451250, 124.955094}, + {1000461250, 124.942072}, + {1000471250, 124.930735}, + {1000481250, 124.92125759}, + {1000491250, 124.913673841}, + {1000501250, 124.90778937}, + {1000511250, 124.905254264}, + {1000521250, 124.904431178}, + {1000531250, 124.90463061}, + {1000541250, 124.90638764}, + {1000551250, 124.909725953}, + {1000561250, 124.91447184}, + {1000571250, 124.92081593099999}, + {1000581250, 124.92844}, + {1000591250, 124.937659}, + {1000601250, 124.948395}, + {1000451260, 125.960064}, + {1000461260, 125.944401}, + {1000471260, 125.934814}, + {1000481260, 125.92243029}, + {1000491260, 125.916468202}, + {1000501260, 125.907658958}, + {1000511260, 125.907253158}, + {1000521260, 125.903312144}, + {1000531260, 125.905624205}, + {1000541260, 125.904297422}, + {1000551260, 125.909445821}, + {1000561260, 125.911250202}, + {1000571260, 125.919512667}, + {1000581260, 125.923971}, + {1000591260, 125.93524}, + {1000601260, 125.942694}, + {1000611260, 125.957327}, + {1000451270, 126.963789}, + {1000461270, 126.949307}, + {1000471270, 126.937037}, + {1000481270, 126.926203291}, + {1000491270, 126.91746604}, + {1000501270, 126.910391726}, + {1000511270, 126.906925557}, + {1000521270, 126.905226993}, + {1000531270, 126.904472592}, + {1000541270, 126.905183636}, + {1000551270, 126.907417527}, + {1000561270, 126.911091272}, + {1000571270, 126.916375083}, + {1000581270, 126.922727}, + {1000591270, 126.93071}, + {1000601270, 126.939978}, + {1000611270, 126.951358}, + {1000451280, 127.970649}, + {1000461280, 127.952345}, + {1000471280, 127.941266}, + {1000481280, 127.927816778}, + {1000491280, 127.920353637}, + {1000501280, 127.910507828}, + {1000511280, 127.909146121}, + {1000521280, 127.904461237}, + {1000531280, 127.905809355}, + {1000541280, 127.90353075341}, + {1000551280, 127.907748452}, + {1000561280, 127.908352446}, + {1000571280, 127.915592123}, + {1000581280, 127.918911}, + {1000591280, 127.928791}, + {1000601280, 127.935018}, + {1000611280, 127.948234}, + {1000621280, 127.957971}, + {1000461290, 128.959334}, + {1000471290, 128.944315}, + {1000481290, 128.932235597}, + {1000491290, 128.921808534}, + {1000501290, 128.91348244}, + {1000511290, 128.909146623}, + {1000521290, 128.906596419}, + {1000531290, 128.904983643}, + {1000541290, 128.90478085742}, + {1000551290, 128.90606591}, + {1000561290, 128.908683409}, + {1000571290, 128.912695592}, + {1000581290, 128.918102}, + {1000591290, 128.925095}, + {1000601290, 128.933038}, + {1000611290, 128.942909}, + {1000621290, 128.954557}, + {1000461300, 129.964863}, + {1000471300, 129.950727}, + {1000481300, 129.934387563}, + {1000491300, 129.924952257}, + {1000501300, 129.913974531}, + {1000511300, 129.911662686}, + {1000521300, 129.906222745}, + {1000531300, 129.906670168}, + {1000541300, 129.903509346}, + {1000551300, 129.906709281}, + {1000561300, 129.906326002}, + {1000571300, 129.912369413}, + {1000581300, 129.914736}, + {1000591300, 129.92359}, + {1000601300, 129.928506}, + {1000611300, 129.940451}, + {1000621300, 129.948792}, + {1000631300, 129.964022}, + {1000461310, 130.972367}, + {1000471310, 130.956253}, + {1000481310, 130.94072774}, + {1000491310, 130.926972839}, + {1000501310, 130.917053067}, + {1000511310, 130.911989339}, + {1000521310, 130.90852221}, + {1000531310, 130.906126375}, + {1000541310, 130.90508412808}, + {1000551310, 130.905468457}, + {1000561310, 130.906946315}, + {1000571310, 130.91007}, + {1000581310, 130.914429465}, + {1000591310, 130.92023496}, + {1000601310, 130.92724802}, + {1000611310, 130.935834}, + {1000621310, 130.946022}, + {1000631310, 130.957634}, + {1000471320, 131.96307}, + {1000481320, 131.945823136}, + {1000491320, 131.932998444}, + {1000501320, 131.917823898}, + {1000511320, 131.914508013}, + {1000521320, 131.908546713}, + {1000531320, 131.907993511}, + {1000541320, 131.90415508346}, + {1000551320, 131.90643774}, + {1000561320, 131.905061231}, + {1000571320, 131.910119047}, + {1000581320, 131.911466226}, + {1000591320, 131.91924}, + {1000601320, 131.923321237}, + {1000611320, 131.93384}, + {1000621320, 131.940805}, + {1000631320, 131.954696}, + {1000471330, 132.968781}, + {1000481330, 132.952614}, + {1000491330, 132.938067}, + {1000501330, 132.923913753}, + {1000511330, 132.915272128}, + {1000521330, 132.91096333}, + {1000531330, 132.9078284}, + {1000541330, 132.905910748}, + {1000551330, 132.905451958}, + {1000561330, 132.906007443}, + {1000571330, 132.908218}, + {1000581330, 132.911520402}, + {1000591330, 132.916330558}, + {1000601330, 132.922348}, + {1000611330, 132.929782}, + {1000621330, 132.93856}, + {1000631330, 132.94929}, + {1000641330, 132.961288}, + {1000481340, 133.957638}, + {1000491340, 133.944208}, + {1000501340, 133.92868043}, + {1000511340, 133.920537334}, + {1000521340, 133.911396376}, + {1000531340, 133.90977566}, + {1000541340, 133.90539303}, + {1000551340, 133.906718501}, + {1000561340, 133.904508249}, + {1000571340, 133.908514011}, + {1000581340, 133.908928142}, + {1000591340, 133.915696729}, + {1000601340, 133.918790207}, + {1000611340, 133.928326}, + {1000621340, 133.93411}, + {1000631340, 133.946537}, + {1000641340, 133.955416}, + {1000481350, 134.964766}, + {1000491350, 134.949425}, + {1000501350, 134.934908603}, + {1000511350, 134.925184354}, + {1000521350, 134.916554715}, + {1000531350, 134.910059355}, + {1000541350, 134.907231441}, + {1000551350, 134.905976907}, + {1000561350, 134.905688447}, + {1000571350, 134.906984427}, + {1000581350, 134.909160662}, + {1000591350, 134.913111772}, + {1000601350, 134.918181318}, + {1000611350, 134.92478499999999}, + {1000621350, 134.93252}, + {1000631350, 134.94187}, + {1000641350, 134.952496}, + {1000651350, 134.964516}, + {1000491360, 135.956017}, + {1000501360, 135.939699}, + {1000511360, 135.930749009}, + {1000521360, 135.92010118}, + {1000531360, 135.914604693}, + {1000541360, 135.907214474}, + {1000551360, 135.907311431}, + {1000561360, 135.9045758}, + {1000571360, 135.907634962}, + {1000581360, 135.907129256}, + {1000591360, 135.91267747}, + {1000601360, 135.914976061}, + {1000611360, 135.923595949}, + {1000621360, 135.928275553}, + {1000631360, 135.93962}, + {1000641360, 135.9473}, + {1000651360, 135.96146}, + {1000491370, 136.961535}, + {1000501370, 136.946162}, + {1000511370, 136.935522519}, + {1000521370, 136.925599354}, + {1000531370, 136.918028178}, + {1000541370, 136.911557771}, + {1000551370, 136.907089296}, + {1000561370, 136.905827207}, + {1000571370, 136.906450438}, + {1000581370, 136.907762416}, + {1000591370, 136.910679183}, + {1000601370, 136.914563099}, + {1000611370, 136.920479519}, + {1000621370, 136.927007959}, + {1000631370, 136.935430719}, + {1000641370, 136.94502}, + {1000651370, 136.95602}, + {1000501380, 137.951143}, + {1000511380, 137.941331}, + {1000521380, 137.929472452}, + {1000531380, 137.922726392}, + {1000541380, 137.914146268}, + {1000551380, 137.911017119}, + {1000561380, 137.905247059}, + {1000571380, 137.907124041}, + {1000581380, 137.90599418}, + {1000591380, 137.910757495}, + {1000601380, 137.911950938}, + {1000611380, 137.919576119}, + {1000621380, 137.923243988}, + {1000631380, 137.933709}, + {1000641380, 137.940247}, + {1000651380, 137.953193}, + {1000661380, 137.9625}, + {1000501390, 138.957799}, + {1000511390, 138.946269}, + {1000521390, 138.935367191}, + {1000531390, 138.9264934}, + {1000541390, 138.9187922}, + {1000551390, 138.913363822}, + {1000561390, 138.908841164}, + {1000571390, 138.906362927}, + {1000581390, 138.906647029}, + {1000591390, 138.9089327}, + {1000601390, 138.911951208}, + {1000611390, 138.916799228}, + {1000621390, 138.922296631}, + {1000631390, 138.929792307}, + {1000641390, 138.93813}, + {1000651390, 138.94833}, + {1000661390, 138.959527}, + {1000501400, 139.962973}, + {1000511400, 139.952345}, + {1000521400, 139.939487057}, + {1000531400, 139.931715914}, + {1000541400, 139.921645814}, + {1000551400, 139.917283707}, + {1000561400, 139.910608231}, + {1000571400, 139.909487285}, + {1000581400, 139.905448433}, + {1000591400, 139.9090856}, + {1000601400, 139.90954613}, + {1000611400, 139.916035918}, + {1000621400, 139.918994714}, + {1000631400, 139.928087633}, + {1000641400, 139.933674}, + {1000651400, 139.945805048}, + {1000661400, 139.95402}, + {1000671400, 139.968526}, + {1000511410, 140.957552}, + {1000521410, 140.945604}, + {1000531410, 140.935666081}, + {1000541410, 140.926787181}, + {1000551410, 140.920045279}, + {1000561410, 140.914403653}, + {1000571410, 140.910971155}, + {1000581410, 140.908285991}, + {1000591410, 140.907659604}, + {1000601410, 140.90961669}, + {1000611410, 140.913555081}, + {1000621410, 140.918481545}, + {1000631410, 140.924931734}, + {1000641410, 140.932126}, + {1000651410, 140.941448}, + {1000661410, 140.95128}, + {1000671410, 140.963108}, + {1000511420, 141.963918}, + {1000521420, 141.950027}, + {1000531420, 141.941166595}, + {1000541420, 141.929973095}, + {1000551420, 141.924299514}, + {1000561420, 141.916432904}, + {1000571420, 141.91409076}, + {1000581420, 141.909250208}, + {1000591420, 141.91005164}, + {1000601420, 141.907728824}, + {1000611420, 141.912890982}, + {1000621420, 141.915209415}, + {1000631420, 141.923446719}, + {1000641420, 141.928116}, + {1000651420, 141.939280858}, + {1000661420, 141.946194}, + {1000671420, 141.96001}, + {1000681420, 141.970016}, + {1000521430, 142.956489}, + {1000531430, 142.945475}, + {1000541430, 142.93536955}, + {1000551430, 142.927347346}, + {1000561430, 142.920625149}, + {1000571430, 142.916079482}, + {1000581430, 142.912391953}, + {1000591430, 142.910822624}, + {1000601430, 142.909819815}, + {1000611430, 142.910938068}, + {1000621430, 142.914634848}, + {1000631430, 142.920298678}, + {1000641430, 142.926750678}, + {1000651430, 142.935137332}, + {1000661430, 142.943994332}, + {1000671430, 142.95486}, + {1000681430, 142.966548}, + {1000521440, 143.961116}, + {1000531440, 143.951336}, + {1000541440, 143.938945076}, + {1000551440, 143.932075402}, + {1000561440, 143.922954821}, + {1000571440, 143.919645589}, + {1000581440, 143.913652763}, + {1000591440, 143.913310682}, + {1000601440, 143.910092798}, + {1000611440, 143.912596208}, + {1000621440, 143.912006285}, + {1000631440, 143.918819481}, + {1000641440, 143.922963}, + {1000651440, 143.933045}, + {1000661440, 143.939269512}, + {1000671440, 143.952109712}, + {1000681440, 143.9607}, + {1000691440, 143.976211}, + {1000521450, 144.967783}, + {1000531450, 144.955845}, + {1000541450, 144.944719631}, + {1000551450, 144.935528927}, + {1000561450, 144.9275184}, + {1000571450, 144.921808065}, + {1000581450, 144.917265113}, + {1000591450, 144.914517987}, + {1000601450, 144.912579151}, + {1000611450, 144.912755748}, + {1000621450, 144.913417157}, + {1000631450, 144.916272659}, + {1000641450, 144.921710051}, + {1000651450, 144.928717001}, + {1000661450, 144.937473992}, + {1000671450, 144.947267392}, + {1000681450, 144.957874}, + {1000691450, 144.970389}, + {1000531460, 145.961846}, + {1000541460, 145.948518245}, + {1000551460, 145.940621867}, + {1000561460, 145.9303632}, + {1000571460, 145.925688017}, + {1000581460, 145.918812294}, + {1000591460, 145.91768763}, + {1000601460, 145.913122459}, + {1000611460, 145.91470224}, + {1000621460, 145.913046835}, + {1000631460, 145.917210852}, + {1000641460, 145.918318513}, + {1000651460, 145.927252739}, + {1000661460, 145.932844526}, + {1000671460, 145.944993503}, + {1000681460, 145.952418357}, + {1000691460, 145.966661}, + {1000531470, 146.966505}, + {1000541470, 146.954482}, + {1000551470, 146.944261512}, + {1000561470, 146.9353039}, + {1000571470, 146.9284178}, + {1000581470, 146.9226899}, + {1000591470, 146.919007438}, + {1000601470, 146.916105969}, + {1000611470, 146.915144944}, + {1000621470, 146.914904401}, + {1000631470, 146.91675244}, + {1000641470, 146.919101014}, + {1000651470, 146.92405462}, + {1000661470, 146.931082712}, + {1000671470, 146.940142293}, + {1000681470, 146.949964456}, + {1000691470, 146.961379887}, + {1000541480, 147.958508}, + {1000551480, 147.949639026}, + {1000561480, 147.938223}, + {1000571480, 147.9326794}, + {1000581480, 147.924424186}, + {1000591480, 147.922129992}, + {1000601480, 147.916899027}, + {1000611480, 147.917481091}, + {1000621480, 147.914829233}, + {1000631480, 147.918091288}, + {1000641480, 147.918121414}, + {1000651480, 147.924275476}, + {1000661480, 147.927149944}, + {1000671480, 147.937743925}, + {1000681480, 147.944735026}, + {1000691480, 147.958384026}, + {1000701480, 147.967547}, + {1000541490, 148.964573}, + {1000551490, 148.953516}, + {1000561490, 148.943284}, + {1000571490, 148.935351259}, + {1000581490, 148.9284269}, + {1000591490, 148.9237361}, + {1000601490, 148.920154583}, + {1000611490, 148.918341507}, + {1000621490, 148.917191211}, + {1000631490, 148.917936875}, + {1000641490, 148.919347666}, + {1000651490, 148.923253792}, + {1000661490, 148.927327516}, + {1000671490, 148.933820457}, + {1000681490, 148.942306}, + {1000691490, 148.952828}, + {1000701490, 148.964219}, + {1000541500, 149.968878}, + {1000551500, 149.959023}, + {1000561500, 149.9464411}, + {1000571500, 149.9395475}, + {1000581500, 149.930384032}, + {1000591500, 149.926676391}, + {1000601500, 149.920901322}, + {1000611500, 149.920990014}, + {1000621500, 149.917281993}, + {1000631500, 149.919707092}, + {1000641500, 149.918663949}, + {1000651500, 149.923664799}, + {1000661500, 149.925593068}, + {1000671500, 149.933498353}, + {1000681500, 149.937915524}, + {1000691500, 149.95009}, + {1000701500, 149.958314}, + {1000711500, 149.973407}, + {1000551510, 150.963199}, + {1000561510, 150.951755}, + {1000571510, 150.942769}, + {1000581510, 150.9342722}, + {1000591510, 150.928309066}, + {1000601510, 150.923839363}, + {1000611510, 150.921216613}, + {1000621510, 150.919938859}, + {1000631510, 150.919856606}, + {1000641510, 150.920354922}, + {1000651510, 150.92310897}, + {1000661510, 150.926191279}, + {1000671510, 150.931698176}, + {1000681510, 150.937448567}, + {1000691510, 150.945494433}, + {1000701510, 150.955402453}, + {1000711510, 150.967471}, + {1000551520, 151.968728}, + {1000561520, 151.95533}, + {1000571520, 151.947085}, + {1000581520, 151.936682}, + {1000591520, 151.9315529}, + {1000601520, 151.924691242}, + {1000611520, 151.923505185}, + {1000621520, 151.919738646}, + {1000631520, 151.92175098}, + {1000641520, 151.919798414}, + {1000651520, 151.924081855}, + {1000661520, 151.924725274}, + {1000671520, 151.931717618}, + {1000681520, 151.935050347}, + {1000691520, 151.944476}, + {1000701520, 151.950326699}, + {1000711520, 151.96412}, + {1000561530, 152.960848}, + {1000571530, 152.950553}, + {1000581530, 152.941052}, + {1000591530, 152.933903511}, + {1000601530, 152.927717868}, + {1000611530, 152.924156252}, + {1000621530, 152.922103576}, + {1000631530, 152.921236789}, + {1000641530, 152.921756945}, + {1000651530, 152.923441694}, + {1000661530, 152.925771729}, + {1000671530, 152.930206671}, + {1000681530, 152.93508635}, + {1000691530, 152.942058023}, + {1000701530, 152.949372}, + {1000711530, 152.958802248}, + {1000721530, 152.970692}, + {1000561540, 153.964659}, + {1000571540, 153.955416}, + {1000581540, 153.94394}, + {1000591540, 153.937885165}, + {1000601540, 153.929597404}, + {1000611540, 153.926712791}, + {1000621540, 153.922215756}, + {1000631540, 153.922985699}, + {1000641540, 153.920872974}, + {1000651540, 153.924683681}, + {1000661540, 153.92442892}, + {1000671540, 153.930606776}, + {1000681540, 153.932790799}, + {1000691540, 153.941570062}, + {1000701540, 153.946395696}, + {1000711540, 153.957416}, + {1000721540, 153.964863}, + {1000571550, 154.95928}, + {1000581550, 154.948706}, + {1000591550, 154.940509193}, + {1000601550, 154.933135598}, + {1000611550, 154.928136951}, + {1000621550, 154.924646645}, + {1000631550, 154.922899847}, + {1000641550, 154.922629356}, + {1000651550, 154.923509511}, + {1000661550, 154.925758049}, + {1000671550, 154.929103363}, + {1000681550, 154.93321571}, + {1000691550, 154.939209576}, + {1000701550, 154.945783216}, + {1000711550, 154.954326005}, + {1000721550, 154.963167}, + {1000731550, 154.974248}, + {1000571560, 155.964519}, + {1000581560, 155.951884}, + {1000591560, 155.9447669}, + {1000601560, 155.935370358}, + {1000611560, 155.931114059}, + {1000621560, 155.925538191}, + {1000631560, 155.924762976}, + {1000641560, 155.92213012}, + {1000651560, 155.924754209}, + {1000661560, 155.924283593}, + {1000671560, 155.929641634}, + {1000681560, 155.931065926}, + {1000691560, 155.938985746}, + {1000701560, 155.942817096}, + {1000711560, 155.953086606}, + {1000721560, 155.959399083}, + {1000731560, 155.972087}, + {1000571570, 156.968792}, + {1000581570, 156.957133}, + {1000591570, 156.9480031}, + {1000601570, 156.939351074}, + {1000611570, 156.933121298}, + {1000621570, 156.928418598}, + {1000631570, 156.925432556}, + {1000641570, 156.923967424}, + {1000651570, 156.924031888}, + {1000661570, 156.925469555}, + {1000671570, 156.928251974}, + {1000681570, 156.931922652}, + {1000691570, 156.936973}, + {1000701570, 156.94265136799999}, + {1000711570, 156.950144807}, + {1000721570, 156.958288}, + {1000731570, 156.968227445}, + {1000741570, 156.978862}, + {1000581580, 157.960773}, + {1000591580, 157.952603}, + {1000601580, 157.94220562}, + {1000611580, 157.936546948}, + {1000621580, 157.929949262}, + {1000631580, 157.927782192}, + {1000641580, 157.9241112}, + {1000651580, 157.925419942}, + {1000661580, 157.924414817}, + {1000671580, 157.92894491}, + {1000681580, 157.929893474}, + {1000691580, 157.936979525}, + {1000701580, 157.939871202}, + {1000711580, 157.94931562}, + {1000721580, 157.954801217}, + {1000731580, 157.966593}, + {1000741580, 157.974565}, + {1000581590, 158.966355}, + {1000591590, 158.956232}, + {1000601590, 158.946619085}, + {1000611590, 158.939286409}, + {1000621590, 158.93321713}, + {1000631590, 158.929099512}, + {1000641590, 158.926395822}, + {1000651590, 158.925353707}, + {1000661590, 158.925745938}, + {1000671590, 158.927718683}, + {1000681590, 158.93069079}, + {1000691590, 158.934975}, + {1000701590, 158.940060257}, + {1000711590, 158.946635615}, + {1000721590, 158.953995837}, + {1000731590, 158.963028046}, + {1000741590, 158.972696}, + {1000751590, 158.984106}, + {1000591600, 159.961138}, + {1000601600, 159.949839172}, + {1000611600, 159.943215272}, + {1000621600, 159.935337032}, + {1000631600, 159.931836982}, + {1000641600, 159.927061202}, + {1000651600, 159.927174553}, + {1000661600, 159.925203578}, + {1000671600, 159.928735538}, + {1000681600, 159.929077193}, + {1000691600, 159.935264177}, + {1000701600, 159.93755921}, + {1000711600, 159.946033}, + {1000721600, 159.950682728}, + {1000731600, 159.961541678}, + {1000741600, 159.968513946}, + {1000751600, 159.98188}, + {1000591610, 160.965121}, + {1000601610, 160.954664}, + {1000611610, 160.946229837}, + {1000621610, 160.939160062}, + {1000631610, 160.933663991}, + {1000641610, 160.929676267}, + {1000651610, 160.927576806}, + {1000661610, 160.926939425}, + {1000671610, 160.927861815}, + {1000681610, 160.93000353}, + {1000691610, 160.933549}, + {1000701610, 160.937912384}, + {1000711610, 160.943572}, + {1000721610, 160.950277927}, + {1000731610, 160.958369489}, + {1000741610, 160.967249}, + {1000751610, 160.977624313}, + {1000761610, 160.989054}, + {1000601620, 161.958121}, + {1000611620, 161.950574}, + {1000621620, 161.941621687}, + {1000631620, 161.936958329}, + {1000641620, 161.930991812}, + {1000651620, 161.9292754}, + {1000661620, 161.92680450699999}, + {1000671620, 161.929102543}, + {1000681620, 161.928787299}, + {1000691620, 161.934001211}, + {1000701620, 161.935779342}, + {1000711620, 161.943282776}, + {1000721620, 161.947215526}, + {1000731620, 161.957292907}, + {1000741620, 161.963500341}, + {1000751620, 161.975896}, + {1000761620, 161.984434}, + {1000601630, 162.963414}, + {1000611630, 162.953881}, + {1000621630, 162.945679085}, + {1000631630, 162.93926551}, + {1000641630, 162.93409664}, + {1000651630, 162.930653609}, + {1000661630, 162.928737221}, + {1000671630, 162.92874026}, + {1000681630, 162.930039908}, + {1000691630, 162.932658282}, + {1000701630, 162.936345406}, + {1000711630, 162.941179}, + {1000721630, 162.947107211}, + {1000731630, 162.954337194}, + {1000741630, 162.962524251}, + {1000751630, 162.972085434}, + {1000761630, 162.982462}, + {1000771630, 162.994299}, + {1000611640, 163.958819}, + {1000621640, 163.948550061}, + {1000631640, 163.942852943}, + {1000641640, 163.935916193}, + {1000651640, 163.933327561}, + {1000661640, 163.929180819}, + {1000671640, 163.930240548}, + {1000681640, 163.929207739}, + {1000691640, 163.933538019}, + {1000701640, 163.934500743}, + {1000711640, 163.941339}, + {1000721640, 163.944370709}, + {1000731640, 163.953534}, + {1000741640, 163.958952445}, + {1000751640, 163.970507122}, + {1000761640, 163.978073158}, + {1000771640, 163.991966}, + {1000611650, 164.96278}, + {1000621650, 164.95329}, + {1000631650, 164.94554007}, + {1000641650, 164.93931708}, + {1000651650, 164.934955198}, + {1000661650, 164.931709402}, + {1000671650, 164.930329116}, + {1000681650, 164.930733482}, + {1000691650, 164.932441843}, + {1000701650, 164.935270241}, + {1000711650, 164.939406758}, + {1000721650, 164.944567}, + {1000731650, 164.950780287}, + {1000741650, 164.958280663}, + {1000751650, 164.967085831}, + {1000761650, 164.976654}, + {1000771650, 164.987552}, + {1000781650, 164.999658}, + {1000621660, 165.956575}, + {1000631660, 165.949813}, + {1000641660, 165.941630413}, + {1000651660, 165.937939727}, + {1000661660, 165.93281281}, + {1000671660, 165.932291209}, + {1000681660, 165.930301067}, + {1000691660, 165.933562136}, + {1000701660, 165.933876439}, + {1000711660, 165.939859}, + {1000721660, 165.94218}, + {1000731660, 165.950512}, + {1000741660, 165.955031952}, + {1000751660, 165.965821216}, + {1000761660, 165.972698135}, + {1000771660, 165.985716}, + {1000781660, 165.994866}, + {1000621670, 166.962072}, + {1000631670, 166.953011}, + {1000641670, 166.945490012}, + {1000651670, 166.940007046}, + {1000661670, 166.935682415}, + {1000671670, 166.933140254}, + {1000681670, 166.932056192}, + {1000691670, 166.932857206}, + {1000701670, 166.934954069}, + {1000711670, 166.938243}, + {1000721670, 166.9426}, + {1000731670, 166.948093}, + {1000741670, 166.95481108}, + {1000751670, 166.962604}, + {1000761670, 166.971552304}, + {1000771670, 166.981671973}, + {1000781670, 166.99275}, + {1000621680, 167.966033}, + {1000631680, 167.957863}, + {1000641680, 167.948309}, + {1000651680, 167.943337074}, + {1000661680, 167.937134977}, + {1000671680, 167.935523766}, + {1000681680, 167.932378282}, + {1000691680, 167.934178457}, + {1000701680, 167.933891297}, + {1000711680, 167.938729798}, + {1000721680, 167.940568}, + {1000731680, 167.948047}, + {1000741680, 167.951805459}, + {1000751680, 167.961572607}, + {1000761680, 167.96779905}, + {1000771680, 167.979960978}, + {1000781680, 167.988180196}, + {1000791680, 168.002716}, + {1000631690, 168.961717}, + {1000641690, 168.952882}, + {1000651690, 168.945807}, + {1000661690, 168.940315231}, + {1000671690, 168.93687989}, + {1000681690, 168.934598444}, + {1000691690, 168.934218956}, + {1000701690, 168.935184208}, + {1000711690, 168.937645845}, + {1000721690, 168.941259}, + {1000731690, 168.946011}, + {1000741690, 168.951778689}, + {1000751690, 168.958765979}, + {1000761690, 168.967017521}, + {1000771690, 168.976281743}, + {1000781690, 168.986619}, + {1000791690, 168.99808}, + {1000631700, 169.96687}, + {1000641700, 169.956146}, + {1000651700, 169.949855}, + {1000661700, 169.94234}, + {1000671700, 169.939626548}, + {1000681700, 169.935471933}, + {1000691700, 169.935807093}, + {1000701700, 169.934767242}, + {1000711700, 169.93847923}, + {1000721700, 169.939609}, + {1000731700, 169.946175}, + {1000741700, 169.949231235}, + {1000751700, 169.958234844}, + {1000761700, 169.963579273}, + {1000771700, 169.975113}, + {1000781700, 169.982502087}, + {1000791700, 169.996024}, + {1000801700, 170.005814}, + {1000641710, 170.961127}, + {1000651710, 170.953011}, + {1000661710, 170.946312}, + {1000671710, 170.941472713}, + {1000681710, 170.938037372}, + {1000691710, 170.936435162}, + {1000701710, 170.936331515}, + {1000711710, 170.937918591}, + {1000721710, 170.940492}, + {1000731710, 170.944476}, + {1000741710, 170.949451}, + {1000751710, 170.955716}, + {1000761710, 170.963180402}, + {1000771710, 170.97164552}, + {1000781710, 170.981248868}, + {1000791710, 170.991881533}, + {1000801710, 171.003585}, + {1000641720, 171.964605}, + {1000651720, 171.957391}, + {1000661720, 171.948728}, + {1000671720, 171.94473}, + {1000681720, 171.939363461}, + {1000691720, 171.938406959}, + {1000701720, 171.936386654}, + {1000711720, 171.93909132}, + {1000721720, 171.939449716}, + {1000731720, 171.944895}, + {1000741720, 171.947292}, + {1000751720, 171.955376165}, + {1000761720, 171.960017309}, + {1000771720, 171.970607035}, + {1000781720, 171.977341059}, + {1000791720, 171.989996704}, + {1000801720, 171.998860581}, + {1000651730, 172.960805}, + {1000661730, 172.953043}, + {1000671730, 172.94702}, + {1000681730, 172.9424}, + {1000691730, 172.93960663}, + {1000701730, 172.938216211}, + {1000711730, 172.938935722}, + {1000721730, 172.940513}, + {1000731730, 172.94375}, + {1000741730, 172.947689}, + {1000751730, 172.953243}, + {1000761730, 172.959808387}, + {1000771730, 172.967505477}, + {1000781730, 172.976449922}, + {1000791730, 172.986224263}, + {1000801730, 172.997143}, + {1000651740, 173.965679}, + {1000661740, 173.955845}, + {1000671740, 173.950757}, + {1000681740, 173.94423}, + {1000691740, 173.942174061}, + {1000701740, 173.938867545}, + {1000711740, 173.94034284}, + {1000721740, 173.940048377}, + {1000731740, 173.944454}, + {1000741740, 173.946079}, + {1000751740, 173.953115}, + {1000761740, 173.957063192}, + {1000771740, 173.966949939}, + {1000781740, 173.972820431}, + {1000791740, 173.984908}, + {1000801740, 173.992870575}, + {1000661750, 174.960569}, + {1000671750, 174.953516}, + {1000681750, 174.94777}, + {1000691750, 174.94384231}, + {1000701750, 174.941281907}, + {1000711750, 174.940777211}, + {1000721750, 174.941511424}, + {1000731750, 174.943737}, + {1000741750, 174.946717}, + {1000751750, 174.951381}, + {1000761750, 174.956945126}, + {1000771750, 174.964149519}, + {1000781750, 174.972400593}, + {1000791750, 174.981316375}, + {1000801750, 174.991444451}, + {1000661760, 175.963918}, + {1000671760, 175.957713}, + {1000681760, 175.94994}, + {1000691760, 175.946997707}, + {1000701760, 175.942574706}, + {1000711760, 175.942691711}, + {1000721760, 175.941409797}, + {1000731760, 175.944857}, + {1000741760, 175.945634}, + {1000751760, 175.951623}, + {1000761760, 175.954770315}, + {1000771760, 175.963626261}, + {1000781760, 175.968938162}, + {1000791760, 175.980116925}, + {1000801760, 175.98734867}, + {1000811760, 176.000627731}, + {1000671770, 176.961052}, + {1000681770, 176.95399}, + {1000691770, 176.948932}, + {1000701770, 176.945263846}, + {1000711770, 176.94376357}, + {1000721770, 176.943230187}, + {1000731770, 176.94448194}, + {1000741770, 176.946643}, + {1000751770, 176.950328}, + {1000761770, 176.954957902}, + {1000771770, 176.9613015}, + {1000781770, 176.968469541}, + {1000791770, 176.976869701}, + {1000801770, 176.98628459}, + {1000811770, 176.996414252}, + {1000671780, 177.965507}, + {1000681780, 177.956779}, + {1000691780, 177.952506}, + {1000701780, 177.9466694}, + {1000711780, 177.945960065}, + {1000721780, 177.943708322}, + {1000731780, 177.94568}, + {1000741780, 177.945885791}, + {1000751780, 177.950989}, + {1000761780, 177.953253334}, + {1000771780, 177.961079395}, + {1000781780, 177.965649288}, + {1000791780, 177.976056714}, + {1000801780, 177.982484756}, + {1000811780, 177.995047}, + {1000821780, 178.003836171}, + {1000681790, 178.961267}, + {1000691790, 178.955018}, + {1000701790, 178.94993}, + {1000711790, 178.947332985}, + {1000721790, 178.945825705}, + {1000731790, 178.94593905}, + {1000741790, 178.947079378}, + {1000751790, 178.949989686}, + {1000761790, 178.953815985}, + {1000771790, 178.959117594}, + {1000781790, 178.965358742}, + {1000791790, 178.973173666}, + {1000801790, 178.981821759}, + {1000811790, 178.991122185}, + {1000821790, 179.002202492}, + {1000681800, 179.96438}, + {1000691800, 179.959023}, + {1000701800, 179.951991}, + {1000711800, 179.949890744}, + {1000721800, 179.946559537}, + {1000731800, 179.947467589}, + {1000741800, 179.946713304}, + {1000751800, 179.950791568}, + {1000761800, 179.952381665}, + {1000771800, 179.959229446}, + {1000781800, 179.96303801}, + {1000791800, 179.972489738}, + {1000801800, 179.97826018}, + {1000811800, 179.98991895}, + {1000821800, 179.997916177}, + {1000691810, 180.961954}, + {1000701810, 180.95589}, + {1000711810, 180.951908}, + {1000721810, 180.949110834}, + {1000731810, 180.947998528}, + {1000741810, 180.948218733}, + {1000751810, 180.950061507}, + {1000761810, 180.953247188}, + {1000771810, 180.957634691}, + {1000781810, 180.963089946}, + {1000791810, 180.970079102}, + {1000801810, 180.977819368}, + {1000811810, 180.986259978}, + {1000821810, 180.9966606}, + {1000691820, 181.966194}, + {1000701820, 181.958239}, + {1000711820, 181.955158}, + {1000721820, 181.950563684}, + {1000731820, 181.950154612}, + {1000741820, 181.948205636}, + {1000751820, 181.95121156}, + {1000761820, 181.952110154}, + {1000771820, 181.958076296}, + {1000781820, 181.961171605}, + {1000791820, 181.969614433}, + {1000801820, 181.974689173}, + {1000811820, 181.985692649}, + {1000821820, 181.992673537}, + {1000701830, 182.962426}, + {1000711830, 182.957363}, + {1000721830, 182.953533203}, + {1000731830, 182.95137538}, + {1000741830, 182.950224416}, + {1000751830, 182.950821306}, + {1000761830, 182.953125028}, + {1000771830, 182.956841231}, + {1000781830, 182.961595895}, + {1000791830, 182.967588106}, + {1000801830, 182.974444652}, + {1000811830, 182.982192843}, + {1000821830, 182.991862527}, + {1000701840, 183.965002}, + {1000711840, 183.96103}, + {1000721840, 183.955448507}, + {1000731840, 183.954009958}, + {1000741840, 183.95093318}, + {1000751840, 183.952528073}, + {1000761840, 183.952492919}, + {1000771840, 183.957476}, + {1000781840, 183.959921929}, + {1000791840, 183.967451523}, + {1000801840, 183.971717709}, + {1000811840, 183.981874973}, + {1000821840, 183.988135634}, + {1000831840, 184.001347}, + {1000701850, 184.969425}, + {1000711850, 184.963542}, + {1000721850, 184.958862}, + {1000731850, 184.955561317}, + {1000741850, 184.953421206}, + {1000751850, 184.95295832}, + {1000761850, 184.954045969}, + {1000771850, 184.956698}, + {1000781850, 184.960613659}, + {1000791850, 184.965798871}, + {1000801850, 184.971890696}, + {1000811850, 184.978789189}, + {1000821850, 184.98761}, + {1000831850, 184.9976}, + {1000711860, 185.96745}, + {1000721860, 185.960897}, + {1000731860, 185.958553036}, + {1000741860, 185.95436514}, + {1000751860, 185.954989172}, + {1000761860, 185.953837569}, + {1000771860, 185.957946754}, + {1000781860, 185.959350845}, + {1000791860, 185.965952703}, + {1000801860, 185.969362061}, + {1000811860, 185.978654787}, + {1000821860, 185.984239409}, + {1000831860, 185.996623169}, + {1000841860, 186.004403174}, + {1000711870, 186.970188}, + {1000721870, 186.964573}, + {1000731870, 186.960391}, + {1000741870, 186.957161249}, + {1000751870, 186.955752217}, + {1000761870, 186.955749569}, + {1000771870, 186.957542}, + {1000781870, 186.960616646}, + {1000791870, 186.964542147}, + {1000801870, 186.96981354}, + {1000811870, 186.97590474}, + {1000821870, 186.983910842}, + {1000831870, 186.993147272}, + {1000841870, 187.003031482}, + {1000711880, 187.974428}, + {1000721880, 187.966903}, + {1000731880, 187.963596}, + {1000741880, 187.958488325}, + {1000751880, 187.958113658}, + {1000761880, 187.955837292}, + {1000771880, 187.958834999}, + {1000781880, 187.959397521}, + {1000791880, 187.965247966}, + {1000801880, 187.967580738}, + {1000811880, 187.976020886}, + {1000821880, 187.980879079}, + {1000831880, 187.992276064}, + {1000841880, 187.999415586}, + {1000721890, 188.970853}, + {1000731890, 188.96569}, + {1000741890, 188.961557}, + {1000751890, 188.959227764}, + {1000761890, 188.958145949}, + {1000771890, 188.958722602}, + {1000781890, 188.960848485}, + {1000791890, 188.963948286}, + {1000801890, 188.968194776}, + {1000811890, 188.973573525}, + {1000821890, 188.980843658}, + {1000831890, 188.989195139}, + {1000841890, 188.998473425}, + {1000721900, 189.973376}, + {1000731900, 189.969168}, + {1000741900, 189.963103542}, + {1000751900, 189.961800064}, + {1000761900, 189.958445442}, + {1000771900, 189.960543374}, + {1000781900, 189.959949823}, + {1000791900, 189.964751746}, + {1000801900, 189.96632225}, + {1000811900, 189.973841771}, + {1000821900, 189.978081872}, + {1000831900, 189.988624828}, + {1000841900, 189.995101731}, + {1000731910, 190.97153}, + {1000741910, 190.966531}, + {1000751910, 190.963123322}, + {1000761910, 190.960928105}, + {1000771910, 190.960591455}, + {1000781910, 190.961676261}, + {1000791910, 190.963716452}, + {1000801910, 190.967158301}, + {1000811910, 190.971784093}, + {1000821910, 190.978216455}, + {1000831910, 190.985786972}, + {1000841910, 190.994558494}, + {1000851910, 191.004148081}, + {1000731920, 191.975201}, + {1000741920, 191.968202}, + {1000751920, 191.966088}, + {1000761920, 191.961478765}, + {1000771920, 191.962602414}, + {1000781920, 191.961042667}, + {1000791920, 191.964817615}, + {1000801920, 191.965634263}, + {1000811920, 191.972225}, + {1000821920, 191.975789598}, + {1000831920, 191.985470077}, + {1000841920, 191.991340274}, + {1000851920, 192.003140912}, + {1000731930, 192.97766}, + {1000741930, 192.971884}, + {1000751930, 192.967545}, + {1000761930, 192.964149637}, + {1000771930, 192.962923753}, + {1000781930, 192.962984546}, + {1000791930, 192.964138442}, + {1000801930, 192.966653395}, + {1000811930, 192.970501994}, + {1000821930, 192.976135914}, + {1000831930, 192.98294722}, + {1000841930, 192.991062421}, + {1000851930, 192.999927725}, + {1000861930, 193.009707973}, + {1000731940, 193.98161}, + {1000741940, 193.973795}, + {1000751940, 193.970735}, + {1000761940, 193.965179407}, + {1000771940, 193.965075703}, + {1000781940, 193.962683498}, + {1000791940, 193.965419051}, + {1000801940, 193.965449108}, + {1000811940, 193.971081408}, + {1000821940, 193.974011788}, + {1000831940, 193.982798581}, + {1000841940, 193.988186058}, + {1000851940, 193.999230816}, + {1000861940, 194.006145636}, + {1000741950, 194.977735}, + {1000751950, 194.97256}, + {1000761950, 194.968318}, + {1000771950, 194.965976898}, + {1000781950, 194.964794325}, + {1000791950, 194.965037823}, + {1000801950, 194.966705809}, + {1000811950, 194.969774052}, + {1000821950, 194.974516167}, + {1000831950, 194.980648759}, + {1000841950, 194.988065781}, + {1000851950, 194.99627448}, + {1000861950, 195.005421703}, + {1000741960, 195.979882}, + {1000751960, 195.975996}, + {1000761960, 195.969643261}, + {1000771960, 195.968399669}, + {1000781960, 195.964954648}, + {1000791960, 195.966571213}, + {1000801960, 195.965833445}, + {1000811960, 195.970481189}, + {1000821960, 195.972787552}, + {1000831960, 195.980666509}, + {1000841960, 195.985540722}, + {1000851960, 195.995799034}, + {1000861960, 196.002120431}, + {1000741970, 196.984036}, + {1000751970, 196.978153}, + {1000761970, 196.973076}, + {1000771970, 196.969657217}, + {1000781970, 196.96734303}, + {1000791970, 196.966570103}, + {1000801970, 196.967213715}, + {1000811970, 196.969560492}, + {1000821970, 196.973434737}, + {1000831970, 196.978864927}, + {1000841970, 196.985621939}, + {1000851970, 196.993177353}, + {1000861970, 197.001621446}, + {1000871970, 197.011008086}, + {1000751980, 197.98176}, + {1000761980, 197.974664}, + {1000771980, 197.972399}, + {1000781980, 197.967896718}, + {1000791980, 197.968243714}, + {1000801980, 197.966769177}, + {1000811980, 197.970446669}, + {1000821980, 197.97201545}, + {1000831980, 197.979201316}, + {1000841980, 197.983388753}, + {1000851980, 197.992797864}, + {1000861980, 197.998679197}, + {1000871980, 198.010282081}, + {1000751990, 198.984187}, + {1000761990, 198.978239}, + {1000771990, 198.973807097}, + {1000781990, 198.970597022}, + {1000791990, 198.968766573}, + {1000801990, 198.968280994}, + {1000811990, 198.969877}, + {1000821990, 198.97291262}, + {1000831990, 198.977672841}, + {1000841990, 198.983640445}, + {1000851990, 198.990527715}, + {1000861990, 198.998325436}, + {1000871990, 199.007269384}, + {1000762000, 199.980086}, + {1000772000, 199.976844}, + {1000782000, 199.971444609}, + {1000792000, 199.970756558}, + {1000802000, 199.968326941}, + {1000812000, 199.970963608}, + {1000822000, 199.971818546}, + {1000832000, 199.97813129}, + {1000842000, 199.981812355}, + {1000852000, 199.990351099}, + {1000862000, 199.995705335}, + {1000872000, 200.006584666}, + {1000762010, 200.984069}, + {1000772010, 200.978701}, + {1000782010, 200.974513305}, + {1000792010, 200.971657678}, + {1000802010, 200.970303054}, + {1000812010, 200.970820235}, + {1000822010, 200.972870431}, + {1000832010, 200.976995017}, + {1000842010, 200.982263799}, + {1000852010, 200.988417058}, + {1000862010, 200.995590511}, + {1000872010, 201.003852491}, + {1000882010, 201.012814699}, + {1000762020, 201.986548}, + {1000772020, 201.982136}, + {1000782020, 201.975639}, + {1000792020, 201.973856}, + {1000802020, 201.970643604}, + {1000812020, 201.972108874}, + {1000822020, 201.972151613}, + {1000832020, 201.977723042}, + {1000842020, 201.980738934}, + {1000852020, 201.988625686}, + {1000862020, 201.993263982}, + {1000872020, 202.003329637}, + {1000882020, 202.009742305}, + {1000762030, 202.992195}, + {1000772030, 202.984573}, + {1000782030, 202.979055}, + {1000792030, 202.975154492}, + {1000802030, 202.972872396}, + {1000812030, 202.972344098}, + {1000822030, 202.973390617}, + {1000832030, 202.976892077}, + {1000842030, 202.981416072}, + {1000852030, 202.986942904}, + {1000862030, 202.993361155}, + {1000872030, 203.000940867}, + {1000882030, 203.009233907}, + {1000772040, 203.989726}, + {1000782040, 203.981084}, + {1000792040, 203.97811}, + {1000802040, 203.973494037}, + {1000812040, 203.97386342}, + {1000822040, 203.973043506}, + {1000832040, 203.977835687}, + {1000842040, 203.980310078}, + {1000852040, 203.987251393}, + {1000862040, 203.991443729}, + {1000872040, 204.000651972}, + {1000882040, 204.006506855}, + {1000772050, 204.993988}, + {1000782050, 204.986237}, + {1000792050, 204.980064}, + {1000802050, 204.976073151}, + {1000812050, 204.974427318}, + {1000822050, 204.974481682}, + {1000832050, 204.977385182}, + {1000842050, 204.981190006}, + {1000852050, 204.986060546}, + {1000862050, 204.991723228}, + {1000872050, 204.998593854}, + {1000882050, 205.006230692}, + {1000892050, 205.015144152}, + {1000782060, 205.99008}, + {1000792060, 205.984766}, + {1000802060, 205.977513837}, + {1000812060, 205.976110108}, + {1000822060, 205.97446521}, + {1000832060, 205.978498843}, + {1000842060, 205.980473662}, + {1000852060, 205.986645768}, + {1000862060, 205.990195409}, + {1000872060, 205.998661441}, + {1000882060, 206.003827842}, + {1000892060, 206.014476477}, + {1000782070, 206.995556}, + {1000792070, 206.988577}, + {1000802070, 206.9823}, + {1000812070, 206.977418605}, + {1000822070, 206.975896821}, + {1000832070, 206.978470551}, + {1000842070, 206.981593334}, + {1000852070, 206.985799715}, + {1000862070, 206.990730224}, + {1000872070, 206.99694145}, + {1000882070, 207.00377242}, + {1000892070, 207.011965967}, + {1000782080, 207.999463}, + {1000792080, 207.993655}, + {1000802080, 207.985759}, + {1000812080, 207.982018006}, + {1000822080, 207.976652005}, + {1000832080, 207.97974206}, + {1000842080, 207.981246035}, + {1000852080, 207.986613011}, + {1000862080, 207.989634513}, + {1000872080, 207.997139082}, + {1000882080, 208.001855012}, + {1000892080, 208.011552251}, + {1000902080, 208.017915348}, + {1000792090, 208.997606}, + {1000802090, 208.990757}, + {1000812090, 208.985351713}, + {1000822090, 208.981089978}, + {1000832090, 208.980398599}, + {1000842090, 208.982430361}, + {1000852090, 208.986168701}, + {1000862090, 208.990401389}, + {1000872090, 208.995939701}, + {1000882090, 209.001994902}, + {1000892090, 209.009495375}, + {1000902090, 209.017601}, + {1000792100, 210.002877}, + {1000802100, 209.99431}, + {1000812100, 209.990072942}, + {1000822100, 209.984188381}, + {1000832100, 209.984120237}, + {1000842100, 209.982873686}, + {1000852100, 209.987147423}, + {1000862100, 209.989688862}, + {1000872100, 209.996410596}, + {1000882100, 210.000475406}, + {1000892100, 210.009408625}, + {1000902100, 210.015093515}, + {1000802110, 210.999581}, + {1000812110, 210.993475}, + {1000822110, 210.988735288}, + {1000832110, 210.987268715}, + {1000842110, 210.986653171}, + {1000852110, 210.987496226}, + {1000862110, 210.990600767}, + {1000872110, 210.995555189}, + {1000882110, 211.000893049}, + {1000892110, 211.007668846}, + {1000902110, 211.014896923}, + {1000912110, 211.023674036}, + {1000802120, 212.003242}, + {1000812120, 211.998335}, + {1000822120, 211.991895891}, + {1000832120, 211.99128503}, + {1000842120, 211.988867982}, + {1000852120, 211.990737301}, + {1000862120, 211.990703946}, + {1000872120, 211.99622542}, + {1000882120, 211.999786619}, + {1000892120, 212.007836442}, + {1000902120, 212.01300157}, + {1000912120, 212.023184819}, + {1000802130, 213.008803}, + {1000812130, 213.001915}, + {1000822130, 212.996560796}, + {1000832130, 212.99438357}, + {1000842130, 212.992857154}, + {1000852130, 212.992936593}, + {1000862130, 212.993885147}, + {1000872130, 212.99618441}, + {1000882130, 213.000370971}, + {1000892130, 213.006592665}, + {1000902130, 213.01301147}, + {1000912130, 213.021099644}, + {1000802140, 214.012636}, + {1000812140, 214.00694}, + {1000822140, 213.999803521}, + {1000832140, 213.998710909}, + {1000842140, 213.995201287}, + {1000852140, 213.996372331}, + {1000862140, 213.99536265}, + {1000872140, 213.998971193}, + {1000882140, 214.00009956}, + {1000892140, 214.0069064}, + {1000902140, 214.01148148}, + {1000912140, 214.020891055}, + {1000802150, 215.018368}, + {1000812150, 215.010768}, + {1000822150, 215.004661591}, + {1000832150, 215.001749095}, + {1000842150, 214.999418385}, + {1000852150, 214.998651002}, + {1000862150, 214.998745037}, + {1000872150, 215.000341534}, + {1000882150, 215.002718208}, + {1000892150, 215.006474061}, + {1000902150, 215.01172464}, + {1000912150, 215.019113955}, + {1000922150, 215.026719774}, + {1000802160, 216.022459}, + {1000812160, 216.015964}, + {1000822160, 216.008062}, + {1000832160, 216.006305985}, + {1000842160, 216.001913416}, + {1000852160, 216.002422643}, + {1000862160, 216.000271942}, + {1000872160, 216.003189523}, + {1000882160, 216.003533534}, + {1000892160, 216.008749101}, + {1000902160, 216.011055933}, + {1000912160, 216.019134633}, + {1000922160, 216.024762829}, + {1000812170, 217.020032}, + {1000822170, 217.013162}, + {1000832170, 217.009372}, + {1000842170, 217.006316145}, + {1000852170, 217.004717794}, + {1000862170, 217.003927632}, + {1000872170, 217.00463198}, + {1000882170, 217.006322676}, + {1000892170, 217.009342325}, + {1000902170, 217.013103443}, + {1000912170, 217.018309024}, + {1000922170, 217.02466}, + {1000812180, 218.025454}, + {1000822180, 218.016779}, + {1000832180, 218.014188}, + {1000842180, 218.008971234}, + {1000852180, 218.008695941}, + {1000862180, 218.005601123}, + {1000872180, 218.00757862}, + {1000882180, 218.007134297}, + {1000892180, 218.01164886}, + {1000902180, 218.013276248}, + {1000912180, 218.020021133}, + {1000922180, 218.023504877}, + {1000822190, 219.022136}, + {1000832190, 219.01752}, + {1000842190, 219.013614}, + {1000852190, 219.011160587}, + {1000862190, 219.009478683}, + {1000872190, 219.009250664}, + {1000882190, 219.010084715}, + {1000892190, 219.012420425}, + {1000902190, 219.015526432}, + {1000912190, 219.019949909}, + {1000922190, 219.025009233}, + {1000932190, 219.031601865}, + {1000822200, 220.025905}, + {1000832200, 220.022501}, + {1000842200, 220.016386}, + {1000852200, 220.015433}, + {1000862200, 220.011392443}, + {1000872200, 220.012326789}, + {1000882200, 220.011027542}, + {1000892200, 220.014754527}, + {1000902200, 220.015769866}, + {1000912200, 220.021769753}, + {1000922200, 220.024706}, + {1000932200, 220.03271628}, + {1000832210, 221.02598}, + {1000842210, 221.021228}, + {1000852210, 221.018017}, + {1000862210, 221.015535637}, + {1000872210, 221.014253714}, + {1000882210, 221.013917293}, + {1000892210, 221.015599721}, + {1000902210, 221.018185757}, + {1000912210, 221.021873393}, + {1000922210, 221.026323297}, + {1000932210, 221.03211}, + {1000942210, 221.038572}, + {1000832220, 222.031079}, + {1000842220, 222.02414}, + {1000852220, 222.022494}, + {1000862220, 222.017576017}, + {1000872220, 222.017582615}, + {1000882220, 222.015373371}, + {1000892220, 222.017844232}, + {1000902220, 222.01846822}, + {1000912220, 222.023687064}, + {1000922220, 222.026057957}, + {1000932220, 222.033574706}, + {1000942220, 222.037638}, + {1000832230, 223.034611}, + {1000842230, 223.02907}, + {1000852230, 223.025151}, + {1000862230, 223.021889283}, + {1000872230, 223.019734241}, + {1000882230, 223.018500648}, + {1000892230, 223.019135982}, + {1000902230, 223.020811083}, + {1000912230, 223.023980414}, + {1000922230, 223.027960754}, + {1000932230, 223.03291334}, + {1000942230, 223.038777}, + {1000952230, 223.04584}, + {1000832240, 224.039796}, + {1000842240, 224.03211}, + {1000852240, 224.029749}, + {1000862240, 224.024095803}, + {1000872240, 224.023348096}, + {1000882240, 224.020210361}, + {1000892240, 224.021722249}, + {1000902240, 224.021466137}, + {1000912240, 224.025617286}, + {1000922240, 224.027635913}, + {1000932240, 224.03438803}, + {1000942240, 224.037875}, + {1000952240, 224.046442}, + {1000842250, 225.037123}, + {1000852250, 225.032528}, + {1000862250, 225.028485572}, + {1000872250, 225.025572466}, + {1000882250, 225.023610502}, + {1000892250, 225.023228601}, + {1000902250, 225.023950975}, + {1000912250, 225.026147927}, + {1000922250, 225.02938505}, + {1000932250, 225.033943422}, + {1000942250, 225.03897}, + {1000952250, 225.045508}, + {1000842260, 226.04031}, + {1000852260, 226.037209}, + {1000862260, 226.03086138}, + {1000872260, 226.029544512}, + {1000882260, 226.025408186}, + {1000892260, 226.026096999}, + {1000902260, 226.024903699}, + {1000912260, 226.027948217}, + {1000922260, 226.029338669}, + {1000932260, 226.035230364}, + {1000942260, 226.03825}, + {1000952260, 226.04613}, + {1000842270, 227.04539}, + {1000852270, 227.040183}, + {1000862270, 227.035304393}, + {1000872270, 227.031865413}, + {1000882270, 227.029176205}, + {1000892270, 227.027750594}, + {1000902270, 227.027702546}, + {1000912270, 227.028803586}, + {1000922270, 227.031181124}, + {1000932270, 227.034975012}, + {1000942270, 227.039474}, + {1000952270, 227.045282}, + {1000852280, 228.04496}, + {1000862280, 228.037835415}, + {1000872280, 228.035839433}, + {1000882280, 228.031068574}, + {1000892280, 228.031019685}, + {1000902280, 228.028739741}, + {1000912280, 228.031050758}, + {1000922280, 228.031368959}, + {1000932280, 228.036313}, + {1000942280, 228.038763325}, + {1000952280, 228.046001}, + {1000852290, 229.048191}, + {1000862290, 229.042257272}, + {1000872290, 229.038291443}, + {1000882290, 229.034956703}, + {1000892290, 229.032947}, + {1000902290, 229.031761357}, + {1000912290, 229.032095585}, + {1000922290, 229.033505976}, + {1000932290, 229.036287269}, + {1000942290, 229.040145099}, + {1000952290, 229.045282534}, + {1000862300, 230.045271}, + {1000872300, 230.042390787}, + {1000882300, 230.037054776}, + {1000892300, 230.036327}, + {1000902300, 230.033132267}, + {1000912300, 230.034539717}, + {1000922300, 230.033940114}, + {1000932300, 230.03782806}, + {1000942300, 230.039648313}, + {1000952300, 230.046025}, + {1000862310, 231.049973}, + {1000872310, 231.045175353}, + {1000882310, 231.041027085}, + {1000892310, 231.038393}, + {1000902310, 231.036302764}, + {1000912310, 231.0358825}, + {1000922310, 231.03629218}, + {1000932310, 231.038243598}, + {1000942310, 231.041125946}, + {1000952310, 231.045529}, + {1000962310, 231.050746}, + {1000872320, 232.049461219}, + {1000882320, 232.043475267}, + {1000892320, 232.042034}, + {1000902320, 232.038053606}, + {1000912320, 232.038590205}, + {1000922320, 232.037154765}, + {1000932320, 232.040107}, + {1000942320, 232.041182133}, + {1000952320, 232.046613}, + {1000962320, 232.04974}, + {1000872330, 233.052517833}, + {1000882330, 233.04759457}, + {1000892330, 233.044346}, + {1000902330, 233.041580126}, + {1000912330, 233.040246535}, + {1000922330, 233.039634294}, + {1000932330, 233.040739421}, + {1000942330, 233.042997411}, + {1000952330, 233.046468}, + {1000962330, 233.050771485}, + {1000972330, 233.056652}, + {1000882340, 234.0503821}, + {1000892340, 234.048139}, + {1000902340, 234.043599801}, + {1000912340, 234.043305555}, + {1000922340, 234.040950296}, + {1000932340, 234.042893245}, + {1000942340, 234.043317489}, + {1000952340, 234.047731}, + {1000962340, 234.050158568}, + {1000972340, 234.057322}, + {1000882350, 235.05489}, + {1000892350, 235.05084}, + {1000902350, 235.047255}, + {1000912350, 235.045399}, + {1000922350, 235.043928117}, + {1000932350, 235.044061518}, + {1000942350, 235.045284609}, + {1000952350, 235.047906478}, + {1000962350, 235.051545}, + {1000972350, 235.056651}, + {1000892360, 236.054988}, + {1000902360, 236.049657}, + {1000912360, 236.048668}, + {1000922360, 236.04556613}, + {1000932360, 236.046568296}, + {1000942360, 236.046056661}, + {1000952360, 236.049427}, + {1000962360, 236.051372112}, + {1000972360, 236.057479}, + {1000892370, 237.057993}, + {1000902370, 237.053629}, + {1000912370, 237.051023}, + {1000922370, 237.048728309}, + {1000932370, 237.04817164}, + {1000942370, 237.048407888}, + {1000952370, 237.049995}, + {1000962370, 237.052868988}, + {1000972370, 237.057123}, + {1000982370, 237.062199272}, + {1000902380, 238.056388}, + {1000912380, 238.054637}, + {1000922380, 238.050786936}, + {1000932380, 238.050944603}, + {1000942380, 238.049558175}, + {1000952380, 238.051982531}, + {1000962380, 238.053081606}, + {1000972380, 238.058204}, + {1000982380, 238.06149}, + {1000902390, 239.060655}, + {1000912390, 239.05726}, + {1000922390, 239.054291989}, + {1000932390, 239.052937538}, + {1000942390, 239.052161596}, + {1000952390, 239.053022729}, + {1000962390, 239.054908519}, + {1000972390, 239.058239}, + {1000982390, 239.062482}, + {1000992390, 239.06831}, + {1000912400, 240.061203}, + {1000922400, 240.056592411}, + {1000932400, 240.056163778}, + {1000942400, 240.05381174}, + {1000952400, 240.055298374}, + {1000962400, 240.055528233}, + {1000972400, 240.059758}, + {1000982400, 240.062253447}, + {1000992400, 240.068949}, + {1000912410, 241.064134}, + {1000922410, 241.06033}, + {1000932410, 241.058309671}, + {1000942410, 241.056849651}, + {1000952410, 241.056827343}, + {1000962410, 241.057651218}, + {1000972410, 241.060098}, + {1000982410, 241.06369}, + {1000992410, 241.068592}, + {1001002410, 241.074311}, + {1000922420, 242.062931}, + {1000932420, 242.061639548}, + {1000942420, 242.058740979}, + {1000952420, 242.059547358}, + {1000962420, 242.058834187}, + {1000972420, 242.061999}, + {1000982420, 242.063754544}, + {1000992420, 242.069567}, + {1001002420, 242.07343}, + {1000922430, 243.067075}, + {1000932430, 243.064204}, + {1000942430, 243.062002068}, + {1000952430, 243.061379889}, + {1000962430, 243.061387329}, + {1000972430, 243.063005905}, + {1000982430, 243.065475}, + {1000992430, 243.069508}, + {1001002430, 243.074414}, + {1000932440, 244.067891}, + {1000942440, 244.064204401}, + {1000952440, 244.064282892}, + {1000962440, 244.062750622}, + {1000972440, 244.065178969}, + {1000982440, 244.065999447}, + {1000992440, 244.070881}, + {1001002440, 244.074036}, + {1001012440, 244.081157}, + {1000932450, 245.070693}, + {1000942450, 245.067824554}, + {1000952450, 245.066452827}, + {1000962450, 245.065491047}, + {1000972450, 245.066359814}, + {1000982450, 245.068046755}, + {1000992450, 245.071192}, + {1001002450, 245.075354}, + {1001012450, 245.080864}, + {1000942460, 246.070204172}, + {1000952460, 246.069774}, + {1000962460, 246.067222016}, + {1000972460, 246.0686713}, + {1000982460, 246.068803685}, + {1000992460, 246.072806474}, + {1001002460, 246.075353334}, + {1001012460, 246.081713}, + {1000942470, 247.0743}, + {1000952470, 247.072092}, + {1000962470, 247.070352678}, + {1000972470, 247.070305889}, + {1000982470, 247.070971348}, + {1000992470, 247.073621929}, + {1001002470, 247.076944}, + {1001012470, 247.08152}, + {1000952480, 248.075752}, + {1000962480, 248.072349086}, + {1000972480, 248.073141689}, + {1000982480, 248.072182905}, + {1000992480, 248.075469}, + {1001002480, 248.077185451}, + {1001012480, 248.082607}, + {1001022480, 248.086623}, + {1000952490, 249.07848}, + {1000962490, 249.075953992}, + {1000972490, 249.074983118}, + {1000982490, 249.074850428}, + {1000992490, 249.076409}, + {1001002490, 249.078926042}, + {1001012490, 249.082857155}, + {1001022490, 249.087802}, + {1000962500, 250.078357541}, + {1000972500, 250.078317195}, + {1000982500, 250.076404494}, + {1000992500, 250.078611}, + {1001002500, 250.079519765}, + {1001012500, 250.084164934}, + {1001022500, 250.087565}, + {1000962510, 251.082284988}, + {1000972510, 251.080760555}, + {1000982510, 251.079587171}, + {1000992510, 251.079991431}, + {1001002510, 251.08154513}, + {1001012510, 251.084774287}, + {1001022510, 251.088942}, + {1001032510, 251.094289}, + {1000962520, 252.08487}, + {1000972520, 252.08431}, + {1000982520, 252.081626507}, + {1000992520, 252.082979173}, + {1001002520, 252.082466019}, + {1001012520, 252.086385}, + {1001022520, 252.08896607}, + {1001032520, 252.095048}, + {1000972530, 253.08688}, + {1000982530, 253.085133723}, + {1000992530, 253.084821241}, + {1001002530, 253.085180945}, + {1001012530, 253.087143}, + {1001022530, 253.09056278}, + {1001032530, 253.09503385}, + {1001042530, 253.100528}, + {1000972540, 254.0906}, + {1000982540, 254.087323575}, + {1000992540, 254.088024337}, + {1001002540, 254.086852424}, + {1001012540, 254.08959}, + {1001022540, 254.090954211}, + {1001032540, 254.096238813}, + {1001042540, 254.100055}, + {1000982550, 255.091046}, + {1000992550, 255.090273504}, + {1001002550, 255.089963495}, + {1001012550, 255.091081702}, + {1001022550, 255.093196439}, + {1001032550, 255.096562399}, + {1001042550, 255.101267}, + {1001052550, 255.106919}, + {1000982560, 256.093442}, + {1000992560, 256.093597}, + {1001002560, 256.091771699}, + {1001012560, 256.093888}, + {1001022560, 256.094281912}, + {1001032560, 256.098494024}, + {1001042560, 256.101151464}, + {1001052560, 256.107674}, + {1000992570, 257.095979}, + {1001002570, 257.095105419}, + {1001012570, 257.095537343}, + {1001022570, 257.096884203}, + {1001032570, 257.09948}, + {1001042570, 257.102916796}, + {1001052570, 257.107520042}, + {1000992580, 258.09952}, + {1001002580, 258.097077}, + {1001012580, 258.098433634}, + {1001022580, 258.098205}, + {1001032580, 258.101753}, + {1001042580, 258.103429895}, + {1001052580, 258.108972995}, + {1001062580, 258.11304}, + {1001002590, 259.100596}, + {1001012590, 259.100445}, + {1001022590, 259.100998364}, + {1001032590, 259.1029}, + {1001042590, 259.105601}, + {1001052590, 259.109491859}, + {1001062590, 259.114353}, + {1001002600, 260.102809}, + {1001012600, 260.10365}, + {1001022600, 260.102641}, + {1001032600, 260.105504}, + {1001042600, 260.10644}, + {1001052600, 260.111297}, + {1001062600, 260.114383435}, + {1001072600, 260.121443}, + {1001012610, 261.105828}, + {1001022610, 261.105696}, + {1001032610, 261.106879}, + {1001042610, 261.108769591}, + {1001052610, 261.111979}, + {1001062610, 261.115948135}, + {1001072610, 261.121395733}, + {1001012620, 262.109144}, + {1001022620, 262.107463}, + {1001032620, 262.109615}, + {1001042620, 262.109923}, + {1001052620, 262.114067}, + {1001062620, 262.116338978}, + {1001072620, 262.122654688}, + {1001022630, 263.110714}, + {1001032630, 263.111293}, + {1001042630, 263.112461}, + {1001052630, 263.114987}, + {1001062630, 263.118299}, + {1001072630, 263.122916}, + {1001082630, 263.128479}, + {1001022640, 264.112734}, + {1001032640, 264.114198}, + {1001042640, 264.113876}, + {1001052640, 264.117297}, + {1001062640, 264.11893}, + {1001072640, 264.124486}, + {1001082640, 264.12835633}, + {1001032650, 265.116193}, + {1001042650, 265.116683}, + {1001052650, 265.1185}, + {1001062650, 265.121089}, + {1001072650, 265.124955}, + {1001082650, 265.129791744}, + {1001092650, 265.135937}, + {1001032660, 266.119874}, + {1001042660, 266.118236}, + {1001052660, 266.121032}, + {1001062660, 266.121973}, + {1001072660, 266.12679}, + {1001082660, 266.130048783}, + {1001092660, 266.137062253}, + {1001042670, 267.121787}, + {1001052670, 267.122399}, + {1001062670, 267.124323}, + {1001072670, 267.127499}, + {1001082670, 267.131678}, + {1001092670, 267.137189}, + {1001102670, 267.143726}, + {1001042680, 268.123968}, + {1001052680, 268.125669}, + {1001062680, 268.125389}, + {1001072680, 268.129584}, + {1001082680, 268.132011}, + {1001092680, 268.138649}, + {1001102680, 268.143477}, + {1001052690, 269.127911}, + {1001062690, 269.128495}, + {1001072690, 269.130411}, + {1001082690, 269.133649}, + {1001092690, 269.138809}, + {1001102690, 269.144750965}, + {1001052700, 270.131399}, + {1001062700, 270.130362}, + {1001072700, 270.133366}, + {1001082700, 270.134313}, + {1001092700, 270.140322}, + {1001102700, 270.14458662}, + {1001062710, 271.133782}, + {1001072710, 271.135115}, + {1001082710, 271.137082}, + {1001092710, 271.140741}, + {1001102710, 271.145951}, + {1001062720, 272.135825}, + {1001072720, 272.138259}, + {1001082720, 272.138492}, + {1001092720, 272.143298}, + {1001102720, 272.146091}, + {1001112720, 272.153273}, + {1001062730, 273.139475}, + {1001072730, 273.140294}, + {1001082730, 273.141458}, + {1001092730, 273.144695}, + {1001102730, 273.148455}, + {1001112730, 273.153393}, + {1001072740, 274.143599}, + {1001082740, 274.143217}, + {1001092740, 274.147343}, + {1001102740, 274.149434}, + {1001112740, 274.155247}, + {1001072750, 275.145766}, + {1001082750, 275.14653}, + {1001092750, 275.148972}, + {1001102750, 275.152085}, + {1001112750, 275.156088}, + {1001072760, 276.149169}, + {1001082760, 276.148348}, + {1001092760, 276.151705}, + {1001102760, 276.153022}, + {1001112760, 276.158226}, + {1001122760, 276.161418}, + {1001072770, 277.151477}, + {1001082770, 277.151772}, + {1001092770, 277.153525}, + {1001102770, 277.155763}, + {1001112770, 277.159322}, + {1001122770, 277.163535}, + {1001072780, 278.154988}, + {1001082780, 278.153753}, + {1001092780, 278.156487}, + {1001102780, 278.157007}, + {1001112780, 278.16159}, + {1001122780, 278.164083}, + {1001132780, 278.170725}, + {1001082790, 279.157274}, + {1001092790, 279.158439}, + {1001102790, 279.159984}, + {1001112790, 279.16288}, + {1001122790, 279.166422}, + {1001132790, 279.171187}, + {1001082800, 280.159335}, + {1001092800, 280.161579}, + {1001102800, 280.161375}, + {1001112800, 280.165204}, + {1001122800, 280.167102}, + {1001132800, 280.173098}, + {1001092810, 281.163608}, + {1001102810, 281.164545}, + {1001112810, 281.166757}, + {1001122810, 281.169563}, + {1001132810, 281.17371}, + {1001092820, 282.166888}, + {1001102820, 282.166174}, + {1001112820, 282.169343}, + {1001122820, 282.170507}, + {1001132820, 282.17577}, + {1001102830, 283.169437}, + {1001112830, 283.171101}, + {1001122830, 283.173202}, + {1001132830, 283.176666}, + {1001102840, 284.171187}, + {1001112840, 284.173882}, + {1001122840, 284.17436}, + {1001132840, 284.178843}, + {1001142840, 284.181192}, + {1001112850, 285.175771}, + {1001122850, 285.177227}, + {1001132850, 285.180106}, + {1001142850, 285.183503}, + {1001112860, 286.178756}, + {1001122860, 286.178691}, + {1001132860, 286.182456}, + {1001142860, 286.184226}, + {1001122870, 287.181826}, + {1001132870, 287.184064}, + {1001142870, 287.18672}, + {1001152870, 287.19082}, + {1001122880, 288.183501}, + {1001132880, 288.186764}, + {1001142880, 288.187781}, + {1001152880, 288.192879}, + {1001132890, 289.188461}, + {1001142890, 289.190517}, + {1001152890, 289.193971}, + {1001162890, 289.198023}, + {1001132900, 290.191429}, + {1001142900, 290.191875}, + {1001152900, 290.196235}, + {1001162900, 290.198635}, + {1001142910, 291.194848}, + {1001152910, 291.197725}, + {1001162910, 291.201014}, + {1001172910, 291.205748}, + {1001152920, 292.200323}, + {1001162920, 292.201969}, + {1001172920, 292.207861}, + {1001162930, 293.204583}, + {1001172930, 293.208727}, + {1001182930, 293.213423}, + {1001172940, 294.21084}, + {1001182940, 294.213979}, + {1001182950, 295.216178}, +}; + +} // namespace openmc diff --git a/src/bank.cpp b/src/bank.cpp index 33790379b..5b12b48fd 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -7,6 +7,8 @@ #include "openmc/vector.h" #include +#include +#include namespace openmc { @@ -42,6 +44,13 @@ vector> ifp_fission_lifetime_bank; // used to efficiently sort the fission bank after each iteration. vector progeny_per_particle; +// When shared secondary bank mode is enabled, secondaries produced during +// transport are collected in the write bank. When a secondary generation is +// complete, write is moved to read for transport, and a new empty write bank +// is created. This repeats until no secondaries remain. +SharedArray shared_secondary_bank_read; +SharedArray shared_secondary_bank_write; + } // namespace simulation //============================================================================== @@ -59,6 +68,8 @@ void free_memory_bank() simulation::ifp_source_lifetime_bank.clear(); simulation::ifp_fission_delayed_group_bank.clear(); simulation::ifp_fission_lifetime_bank.clear(); + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); } void init_fission_bank(int64_t max) @@ -67,13 +78,13 @@ void init_fission_bank(int64_t max) simulation::progeny_per_particle.resize(simulation::work_per_rank); } -// Performs an O(n) sort on the fission bank, by leveraging +// Performs an O(n) sort on a fission or secondary bank, by leveraging // the parent_id and progeny_id fields of banked particles. See the following // paper for more details: // "Reproducibility and Monte Carlo Eigenvalue Calculations," F.B. Brown and // T.M. Sutton, 1992 ANS Annual Meeting, Transactions of the American Nuclear // Society, Volume 65, Page 235. -void sort_fission_bank() +void sort_bank(SharedArray& bank, bool is_fission_bank) { // Ensure we don't read off the end of the array if we ran with 0 particles if (simulation::progeny_per_particle.size() == 0) { @@ -95,44 +106,200 @@ void sort_fission_bank() vector> sorted_ifp_lifetime_bank; // If there is not enough space, allocate a temporary vector and point to it - if (simulation::fission_bank.size() > - simulation::fission_bank.capacity() / 2) { - sorted_bank_holder.resize(simulation::fission_bank.size()); + if (bank.size() > bank.capacity() / 2) { + sorted_bank_holder.resize(bank.size()); sorted_bank = sorted_bank_holder.data(); } else { // otherwise, point sorted_bank to unused portion of the fission bank - sorted_bank = &simulation::fission_bank[simulation::fission_bank.size()]; + sorted_bank = bank.data() + bank.size(); } - if (settings::ifp_on) { + if (settings::ifp_on && is_fission_bank) { allocate_temporary_vector_ifp( sorted_ifp_delayed_group_bank, sorted_ifp_lifetime_bank); } - // Use parent and progeny indices to sort fission bank - for (int64_t i = 0; i < simulation::fission_bank.size(); i++) { - const auto& site = simulation::fission_bank[i]; - int64_t offset = site.parent_id - 1 - simulation::work_index[mpi::rank]; - int64_t idx = simulation::progeny_per_particle[offset] + site.progeny_id; - if (idx >= simulation::fission_bank.size()) { + // Use parent and progeny indices to sort bank + for (int64_t i = 0; i < bank.size(); i++) { + const auto& site = bank[i]; + if (site.parent_id < 0 || + site.parent_id >= + static_cast(simulation::progeny_per_particle.size())) { + fatal_error(fmt::format("Invalid parent_id {} for banked site (expected " + "range [0, {})).", + site.parent_id, simulation::progeny_per_particle.size())); + } + int64_t idx = + simulation::progeny_per_particle[site.parent_id] + site.progeny_id; + if (idx < 0 || idx >= bank.size()) { fatal_error("Mismatch detected between sum of all particle progeny and " - "shared fission bank size."); + "bank size during sorting."); } sorted_bank[idx] = site; - if (settings::ifp_on) { + if (settings::ifp_on && is_fission_bank) { copy_ifp_data_from_fission_banks( i, sorted_ifp_delayed_group_bank[idx], sorted_ifp_lifetime_bank[idx]); } } // Copy sorted bank into the fission bank - std::copy(sorted_bank, sorted_bank + simulation::fission_bank.size(), - simulation::fission_bank.data()); - if (settings::ifp_on) { + std::copy(sorted_bank, sorted_bank + bank.size(), bank.data()); + if (settings::ifp_on && is_fission_bank) { copy_ifp_data_to_fission_banks( sorted_ifp_delayed_group_bank.data(), sorted_ifp_lifetime_bank.data()); } } +// This function redistributes SourceSite particles across MPI ranks to +// achieve load balancing while preserving the global ordering of particles. +// +// GUARANTEES: +// ----------- +// 1. Global Order Preservation: After redistribution, each rank holds a +// contiguous slice of the original global ordering. For example, if the +// input across 3 ranks was: +// - Rank 0: IDs 0-4 +// - Rank 1: IDs 5-6 +// - Rank 2: IDs 7-200 +// Then after redistribution (assuming ~67 particles per rank): +// - Rank 0: IDs 0-66 (contiguous) +// - Rank 1: IDs 67-133 (contiguous) +// - Rank 2: IDs 134-200 (contiguous) +// The global ordering is always preserved - no rank will ever hold +// non-contiguous ID ranges like "0-4 and 100-200". +// +// 2. Even Load Balancing: Particles are distributed as evenly as possible. +// If total % n_procs != 0, the first 'remainder' ranks each get one extra +// particle (i.e., floor division with remainder distributed to lower +// ranks). This follows the same logic as calculate_work(). +// +// HOW IT WORKS: +// ------------- +// The algorithm uses overlap-based redistribution: +// 1. Each rank's current data occupies a range [cumulative_before[rank], +// cumulative_before[rank+1]) in the global index space. +// 2. Each rank's target data should occupy [cumulative_target[rank], +// cumulative_target[rank+1]) in the same global index space. +// 3. For each pair of (source_rank, dest_rank), we calculate the overlap +// between what source_rank currently has and what dest_rank needs. +// 4. MPI_Alltoallv transfers exactly these overlapping regions, with +// displacements ensuring data lands at the correct position in the +// receiving buffer. +// +// EDGE CASES HANDLED: +// ------------------- +// - Single rank (n_procs == 1): Returns immediately with local size, no MPI. +// - Empty total (all ranks have 0 particles): Returns 0 immediately. +// - Imbalanced input (e.g., one rank has all particles): Works correctly; +// that rank will send portions to all other ranks based on target ranges. +// - Non-divisible totals: First 'remainder' ranks get one extra particle. +int64_t synchronize_global_secondary_bank( + SharedArray& shared_secondary_bank) +{ + // Get current size of local bank + int64_t local_size = shared_secondary_bank.size(); + + if (mpi::n_procs == 1) { + return local_size; + } + +#ifdef OPENMC_MPI + // Gather all sizes to all ranks + vector all_sizes(mpi::n_procs); + MPI_Allgather(&local_size, 1, MPI_INT64_T, all_sizes.data(), 1, MPI_INT64_T, + mpi::intracomm); + + // Calculate total and check for empty case + int64_t total = 0; + for (int64_t size : all_sizes) { + total += size; + } + + // If we don't have any items to distribute, return + if (total == 0) { + return total; + } + + int64_t base_count = total / mpi::n_procs; + int64_t remainder = total % mpi::n_procs; + + // Calculate target size for each rank + // First 'remainder' ranks get base_count + 1, rest get base_count + vector target_sizes(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; ++i) { + target_sizes[i] = base_count + (i < remainder ? 1 : 0); + } + + // Calculate send and receive counts in terms of SourceSite objects + // (not bytes) + vector send_counts(mpi::n_procs, 0); + vector recv_counts(mpi::n_procs, 0); + vector send_displs(mpi::n_procs, 0); + vector recv_displs(mpi::n_procs, 0); + + // Calculate cumulative positions (starting index for each rank in the + // global array) + vector cumulative_before(mpi::n_procs + 1, 0); + vector cumulative_target(mpi::n_procs + 1, 0); + for (int i = 0; i < mpi::n_procs; ++i) { + cumulative_before[i + 1] = cumulative_before[i] + all_sizes[i]; + cumulative_target[i + 1] = cumulative_target[i] + target_sizes[i]; + } + + // Determine send and receive amounts for each rank + int64_t my_start = cumulative_before[mpi::rank]; + int64_t my_end = cumulative_before[mpi::rank + 1]; + int64_t my_target_start = cumulative_target[mpi::rank]; + int64_t my_target_end = cumulative_target[mpi::rank + 1]; + + for (int r = 0; r < mpi::n_procs; ++r) { + // Send: overlap between my current range and rank r's target range + int64_t send_overlap_start = std::max(my_start, cumulative_target[r]); + int64_t send_overlap_end = std::min(my_end, cumulative_target[r + 1]); + if (send_overlap_start < send_overlap_end) { + int64_t count = send_overlap_end - send_overlap_start; + int64_t displ = send_overlap_start - my_start; + if (count > std::numeric_limits::max() || + displ > std::numeric_limits::max()) { + fatal_error("Secondary bank size exceeds MPI_Alltoallv int limit."); + } + send_counts[r] = static_cast(count); + send_displs[r] = static_cast(displ); + } + + // Recv: overlap between rank r's current range and my target range + int64_t recv_overlap_start = + std::max(cumulative_before[r], my_target_start); + int64_t recv_overlap_end = + std::min(cumulative_before[r + 1], my_target_end); + if (recv_overlap_start < recv_overlap_end) { + int64_t count = recv_overlap_end - recv_overlap_start; + int64_t displ = recv_overlap_start - my_target_start; + if (count > std::numeric_limits::max() || + displ > std::numeric_limits::max()) { + fatal_error("Secondary bank size exceeds MPI_Alltoallv int limit."); + } + recv_counts[r] = static_cast(count); + recv_displs[r] = static_cast(displ); + } + } + + // Prepare receive buffer with target size + SharedArray new_bank(target_sizes[mpi::rank]); + + // Perform all-to-all redistribution using the custom MPI type + MPI_Alltoallv(shared_secondary_bank.data(), send_counts.data(), + send_displs.data(), mpi::source_site, new_bank.data(), recv_counts.data(), + recv_displs.data(), mpi::source_site, mpi::intracomm); + + // Replace old bank with redistributed data + shared_secondary_bank = std::move(new_bank); + + return total; +#else + return local_size; +#endif +} + //============================================================================== // C API //============================================================================== diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 7216ac896..d166f1247 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -38,6 +38,9 @@ void VacuumBC::handle_particle(Particle& p, const Surface& surf) const void ReflectiveBC::handle_particle(Particle& p, const Surface& surf) const { Direction u = surf.reflect(p.r(), p.u(), &p); + + // normalize reflected u to ensure no floating point error leads to + // unnormalized directions u /= u.norm(); // Handle the effects of the surface albedo on the particle's weight. @@ -53,6 +56,9 @@ void ReflectiveBC::handle_particle(Particle& p, const Surface& surf) const void WhiteBC::handle_particle(Particle& p, const Surface& surf) const { Direction u = surf.diffuse_reflect(p.r(), p.u(), p.current_seed()); + + // normalize outgoing u to ensure no floating point error leads to + // unnormalized directions u /= u.norm(); // Handle the effects of the surface albedo on the particle's weight. @@ -129,23 +135,8 @@ TranslationalPeriodicBC::TranslationalPeriodicBC(int i_surf, int j_surf) void TranslationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - int i_particle_surf = p.surface_index(); - - // Figure out which of the two BC surfaces were struck then find the - // particle's new location and surface. - Position new_r; - int new_surface; - if (i_particle_surf == i_surf_) { - new_r = p.r() + translation_; - new_surface = p.surface() > 0 ? j_surf_ + 1 : -(j_surf_ + 1); - } else if (i_particle_surf == j_surf_) { - new_r = p.r() - translation_; - new_surface = p.surface() > 0 ? i_surf_ + 1 : -(i_surf_ + 1); - } else { - throw std::runtime_error( - "Called BoundaryCondition::handle_particle after " - "hitting a surface, but that surface is not recognized by the BC."); - } + auto new_r = p.r() + translation_; + int new_surface = p.surface() > 0 ? j_surf_ + 1 : -(j_surf_ + 1); // Handle the effects of the surface albedo on the particle's weight. BoundaryCondition::handle_albedo(p, surf); @@ -158,63 +149,45 @@ void TranslationalPeriodicBC::handle_particle( // RotationalPeriodicBC implementation //============================================================================== -RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) - : PeriodicBC(i_surf, j_surf) +RotationalPeriodicBC::RotationalPeriodicBC( + int i_surf, int j_surf, PeriodicAxis axis) + : PeriodicBC(std::abs(i_surf) - 1, std::abs(j_surf) - 1) { Surface& surf1 {*model::surfaces[i_surf_]}; Surface& surf2 {*model::surfaces[j_surf_]}; - // Check the type of the first surface - bool surf1_is_xyplane; - if (const auto* ptr = dynamic_cast(&surf1)) { - surf1_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf1)) { - surf1_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf1)) { - surf1_is_xyplane = false; - } else { - throw std::invalid_argument(fmt::format( - "Surface {} is an invalid type for " - "rotational periodic BCs. Only x-planes, y-planes, or general planes " - "(that are perpendicular to z) are supported for these BCs.", - surf1.id_)); + // below convention for right handed coordinate system + switch (axis) { + case x: + zero_axis_idx_ = 0; // x component of plane must be zero + axis_1_idx_ = 1; // y component independent + axis_2_idx_ = 2; // z component dependent + break; + case y: + zero_axis_idx_ = 1; // y component of plane must be zero + axis_1_idx_ = 2; // z component independent + axis_2_idx_ = 0; // x component dependent + break; + case z: + zero_axis_idx_ = 2; // z component of plane must be zero + axis_1_idx_ = 0; // x component independent + axis_2_idx_ = 1; // y component dependent + break; + default: + throw std::invalid_argument( + fmt::format("You've specified an axis that is not x, y, or z.")); } - // Check the type of the second surface - bool surf2_is_xyplane; - if (const auto* ptr = dynamic_cast(&surf2)) { - surf2_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf2)) { - surf2_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf2)) { - surf2_is_xyplane = false; - } else { - throw std::invalid_argument(fmt::format( - "Surface {} is an invalid type for " - "rotational periodic BCs. Only x-planes, y-planes, or general planes " - "(that are perpendicular to z) are supported for these BCs.", - surf2.id_)); - } + Direction ax = {0.0, 0.0, 0.0}; + ax[zero_axis_idx_] = 1.0; + + auto i_sign = std::copysign(1, i_surf); + auto j_sign = -std::copysign(1, j_surf); // Compute the surface normal vectors and make sure they are perpendicular - // to the z-axis - Direction norm1 = surf1.normal({0, 0, 0}); - Direction norm2 = surf2.normal({0, 0, 0}); - if (std::abs(norm1.z) > FP_PRECISION) { - throw std::invalid_argument(fmt::format( - "Rotational periodic BCs are only " - "supported for rotations about the z-axis, but surface {} is not " - "perpendicular to the z-axis.", - surf1.id_)); - } - if (std::abs(norm2.z) > FP_PRECISION) { - throw std::invalid_argument(fmt::format( - "Rotational periodic BCs are only " - "supported for rotations about the z-axis, but surface {} is not " - "perpendicular to the z-axis.", - surf2.id_)); - } - + // to the correct axis + Direction norm1 = i_sign * surf1.normal({0, 0, 0}); + Direction norm2 = j_sign * surf2.normal({0, 0, 0}); // Make sure both surfaces intersect the origin if (std::abs(surf1.evaluate({0, 0, 0})) > FP_COINCIDENT) { throw std::invalid_argument(fmt::format( @@ -231,15 +204,15 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) surf2.id_)); } - // Compute the BC rotation angle. Here it is assumed that both surface - // normal vectors point inwards---towards the valid geometry region. - // Consequently, the rotation angle is not the difference between the two - // normals, but is instead the difference between one normal and one - // anti-normal. (An incident ray on one surface must be an outgoing ray on - // the other surface after rotation hence the anti-normal.) - double theta1 = std::atan2(norm1.y, norm1.x); - double theta2 = std::atan2(norm2.y, norm2.x) + PI; - angle_ = theta2 - theta1; + // Compute the signed rotation angle about the periodic axis. Note that + // (n1×n2)·a = |n1||n2|sin(θ) and n1·n2 = |n1||n2|cos(θ), where a is the axis + // of rotation. + auto c = norm1.cross(norm2); + angle_ = std::atan2(c.dot(ax), norm1.dot(norm2)); + + // If the normals point in the same general direction, the surface sense + // should change when crossing the boundary + flip_sense_ = (i_sign * j_sign > 0.0); // Warn the user if the angle does not evenly divide a circle double rem = std::abs(std::remainder((2 * PI / angle_), 1.0)); @@ -254,34 +227,29 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) void RotationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - int i_particle_surf = p.surface_index(); + int new_surface = p.surface() > 0 ? -(j_surf_ + 1) : j_surf_ + 1; + if (flip_sense_) + new_surface = -new_surface; - // Figure out which of the two BC surfaces were struck to figure out if a - // forward or backward rotation is required. Specify the other surface as - // the particle's new surface. - double theta; - int new_surface; - if (i_particle_surf == i_surf_) { - theta = angle_; - new_surface = p.surface() > 0 ? -(j_surf_ + 1) : j_surf_ + 1; - } else if (i_particle_surf == j_surf_) { - theta = -angle_; - new_surface = p.surface() > 0 ? -(i_surf_ + 1) : i_surf_ + 1; - } else { - throw std::runtime_error( - "Called BoundaryCondition::handle_particle after " - "hitting a surface, but that surface is not recognized by the BC."); - } - - // Rotate the particle's position and direction about the z-axis. + // Rotate the particle's position and direction. Position r = p.r(); Direction u = p.u(); - double cos_theta = std::cos(theta); - double sin_theta = std::sin(theta); - Position new_r = { - cos_theta * r.x - sin_theta * r.y, sin_theta * r.x + cos_theta * r.y, r.z}; - Direction new_u = { - cos_theta * u.x - sin_theta * u.y, sin_theta * u.x + cos_theta * u.y, u.z}; + double cos_theta = std::cos(angle_); + double sin_theta = std::sin(angle_); + + Position new_r; + new_r[zero_axis_idx_] = r[zero_axis_idx_]; + new_r[axis_1_idx_] = cos_theta * r[axis_1_idx_] - sin_theta * r[axis_2_idx_]; + new_r[axis_2_idx_] = sin_theta * r[axis_1_idx_] + cos_theta * r[axis_2_idx_]; + + Direction new_u; + new_u[zero_axis_idx_] = u[zero_axis_idx_]; + new_u[axis_1_idx_] = cos_theta * u[axis_1_idx_] - sin_theta * u[axis_2_idx_]; + new_u[axis_2_idx_] = sin_theta * u[axis_1_idx_] + cos_theta * u[axis_2_idx_]; + + // normalize new_u to ensure no floating point error leads to unnormalized + // directions + new_u /= new_u.norm(); // Handle the effects of the surface albedo on the particle's weight. BoundaryCondition::handle_albedo(p, surf); diff --git a/src/bremsstrahlung.cpp b/src/bremsstrahlung.cpp index a2320e0b4..ec1088101 100644 --- a/src/bremsstrahlung.cpp +++ b/src/bremsstrahlung.cpp @@ -6,7 +6,7 @@ #include "openmc/search.h" #include "openmc/settings.h" -#include "xtensor/xmath.hpp" +#include "openmc/tensor.h" namespace openmc { @@ -16,8 +16,8 @@ namespace openmc { namespace data { -xt::xtensor ttb_e_grid; -xt::xtensor ttb_k_grid; +tensor::Tensor ttb_e_grid; +tensor::Tensor ttb_k_grid; vector ttb; } // namespace data @@ -31,13 +31,13 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost) if (p.material() == MATERIAL_VOID) return; - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); if (p.E() < settings::energy_cutoff[photon]) return; // Get bremsstrahlung data for this material and particle type BremsstrahlungData* mat; - if (p.type() == ParticleType::positron) { + if (p.type() == ParticleType::positron()) { mat = &model::materials[p.material()]->ttb_->positron; } else { mat = &model::materials[p.material()]->ttb_->electron; @@ -119,7 +119,7 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost) } // Create secondary photon - p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon); + p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon()); *E_lost += w; } } diff --git a/src/cell.cpp b/src/cell.cpp index e2479994a..06bc29a66 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -57,7 +57,7 @@ void Cell::set_rotation(const vector& rot) fatal_error(fmt::format("Non-3D rotation vector applied to cell {}", id_)); } - // Compute and store the rotation matrix. + // Compute and store the inverse rotation matrix for the angles given. rotation_.clear(); rotation_.reserve(rot.size() == 9 ? 9 : 12); if (rot.size() == 3) { @@ -340,6 +340,63 @@ void Cell::to_hdf5(hid_t cell_group) const close_group(group); } +//============================================================================== +// XML parsing helpers for nodes +//============================================================================== + +vector parse_cell_material_xml(pugi::xml_node node, int32_t cell_id) +{ + vector mats { + get_node_array(node, "material", true)}; + if (mats.empty()) { + fatal_error(fmt::format( + "An empty material element was specified for cell {}", cell_id)); + } + vector material; + material.reserve(mats.size()); + for (const auto& mat : mats) { + if (mat == "void") { + material.push_back(MATERIAL_VOID); + } else { + material.push_back(std::stoi(mat)); + } + } + return material; +} + +vector parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id) +{ + auto temperatures = get_node_array(node, "temperature"); + if (temperatures.empty()) { + fatal_error(fmt::format( + "An empty temperature element was specified for cell {}", cell_id)); + } + for (auto T : temperatures) { + if (T < 0) { + fatal_error(fmt::format( + "Cell {} was specified with a negative temperature", cell_id)); + } + } + return temperatures; +} + +vector parse_cell_density_xml(pugi::xml_node node, int32_t cell_id) +{ + auto densities = get_node_array(node, "density"); + if (densities.empty()) { + fatal_error(fmt::format( + "An empty density element was specified for cell {}", cell_id)); + } + for (auto rho : densities) { + if (rho <= 0) { + fatal_error(fmt::format( + "Cell {} was specified with a density less than or equal to zero", + cell_id)); + } + } + return densities; +} + //============================================================================== // CSGCell implementation //============================================================================== @@ -390,26 +447,12 @@ CSGCell::CSGCell(pugi::xml_node cell_node) // universe), more than one material (distribmats), and some materials may // be "void". if (material_present) { - vector mats { - get_node_array(cell_node, "material", true)}; - if (mats.size() > 0) { - material_.reserve(mats.size()); - for (std::string mat : mats) { - if (mat.compare("void") == 0) { - material_.push_back(MATERIAL_VOID); - } else { - material_.push_back(std::stoi(mat)); - } - } - } else { - fatal_error(fmt::format( - "An empty material element was specified for cell {}", id_)); - } + material_ = parse_cell_material_xml(cell_node, id_); } // Read the temperature element which may be distributed like materials. if (check_for_node(cell_node, "temperature")) { - sqrtkT_ = get_node_array(cell_node, "temperature"); + sqrtkT_ = parse_cell_temperature_xml(cell_node, id_); sqrtkT_.shrink_to_fit(); // Make sure this is a material-filled cell. @@ -420,14 +463,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node) id_)); } - // Make sure all temperatures are non-negative. - for (auto T : sqrtkT_) { - if (T < 0) { - fatal_error(fmt::format( - "Cell {} was specified with a negative temperature", id_)); - } - } - // Convert to sqrt(k*T). for (auto& T : sqrtkT_) { T = std::sqrt(K_BOLTZMANN * T); @@ -440,7 +475,7 @@ CSGCell::CSGCell(pugi::xml_node cell_node) // Note: calculating the actual density multiplier is deferred until materials // are finalized. density_mult_ contains the true density in the meantime. if (check_for_node(cell_node, "density")) { - density_mult_ = get_node_array(cell_node, "density"); + density_mult_ = parse_cell_density_xml(cell_node, id_); density_mult_.shrink_to_fit(); // Make sure this is a material-filled cell. @@ -461,15 +496,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node) id_)); } } - - // Make sure all densities are non-negative and greater than zero. - for (auto rho : density_mult_) { - if (rho <= 0) { - fatal_error(fmt::format( - "Cell {} was specified with a density less than or equal to zero", - id_)); - } - } } // Read the region specification. @@ -660,7 +686,7 @@ Region::Region(std::string region_spec, int32_t cell_id) if (token == OP_UNION) { simple_ = false; // Ensure intersections have precedence over unions - add_precedence(); + enforce_precedence(); break; } } @@ -703,7 +729,7 @@ void Region::apply_demorgan( //! precedence than unions using parentheses. //============================================================================== -int64_t Region::add_parentheses(int64_t start) +void Region::add_parentheses(int64_t start) { int32_t start_token = expression_[start]; // Add left parenthesis and set new position to be after parenthesis @@ -712,14 +738,6 @@ int64_t Region::add_parentheses(int64_t start) } expression_.insert(expression_.begin() + start - 1, OP_LEFT_PAREN); - // Keep track of return iterator distance. If we don't encounter a left - // parenthesis, we return an iterator corresponding to wherever the right - // parenthesis is inserted. If a left parenthesis is encountered, an iterator - // corresponding to the left parenthesis is returned. Also note that we keep - // track of a *distance* instead of an iterator because the underlying memory - // allocation may change. - std::size_t return_it_dist = 0; - // Add right parenthesis // While the start iterator is within the bounds of infix while (start + 1 < expression_.size()) { @@ -733,7 +751,6 @@ int64_t Region::add_parentheses(int64_t start) // in the region, when the operator is an intersection then include the // operator and next surface if (expression_[start] == OP_LEFT_PAREN) { - return_it_dist = start; int depth = 1; do { start++; @@ -750,54 +767,73 @@ int64_t Region::add_parentheses(int64_t start) --start; } expression_.insert(expression_.begin() + start, OP_RIGHT_PAREN); - if (return_it_dist > 0) { - return return_it_dist; - } else { - return start - 1; - } + return; } } } - // If we get here a right parenthesis hasn't been placed, - // return iterator + // If we get here a right parenthesis hasn't been placed expression_.push_back(OP_RIGHT_PAREN); - if (return_it_dist > 0) { - return return_it_dist; - } else { - return start - 1; - } } +//============================================================================== +//! Add parentheses to enforce operator precedence in region expressions +//! +//! This function ensures that intersection operators have higher precedence +//! than union operators by adding parentheses where needed. For example: +//! "1 2 | 3" becomes "(1 2) | 3" +//! "1 | 2 3" becomes "1 | (2 3)" +//! +//! The algorithm uses stacks to track the current operator type and its +//! position at each parenthesis depth level. When it encounters a different +//! operator at the same depth, it adds parentheses to group the +//! higher-precedence operations. //============================================================================== -void Region::add_precedence() +void Region::enforce_precedence() { - int32_t current_op = 0; - std::size_t current_dist = 0; + // Stack tracking the operator type at each depth (0 = no operator seen yet) + vector op_stack = {0}; - for (int64_t i = 0; i < expression_.size(); i++) { + // Stack tracking where the operator sequence started at each depth + vector pos_stack = {0}; + + for (int64_t i = 0; i < expression_.size(); ++i) { int32_t token = expression_[i]; - if (token == OP_UNION || token == OP_INTERSECTION) { - if (current_op == 0) { - // Set the current operator if is hasn't been set - current_op = token; - current_dist = i; - } else if (token != current_op) { - // If the current operator doesn't match the token, add parenthesis to - // assert precedence - if (current_op == OP_INTERSECTION) { - i = add_parentheses(current_dist); - } else { - i = add_parentheses(i); - } - current_op = 0; - current_dist = 0; + if (token == OP_LEFT_PAREN) { + // Entering a new parenthesis level - push new tracking state + op_stack.push_back(0); + pos_stack.push_back(0); + continue; + } else if (token == OP_RIGHT_PAREN) { + // Exiting a parenthesis level - pop tracking state (keep at least one) + if (op_stack.size() > 1) { + op_stack.pop_back(); + pos_stack.pop_back(); + } + continue; + } + + if (token == OP_UNION || token == OP_INTERSECTION) { + if (op_stack.back() == 0) { + // First operator at this depth - record it and its position + op_stack.back() = token; + pos_stack.back() = i; + } else if (token != op_stack.back()) { + // Encountered a different operator at the same depth - need to add + // parentheses to enforce precedence. Intersection has higher + // precedence, so we parenthesize the intersection terms. + if (op_stack.back() == OP_INTERSECTION) { + add_parentheses(pos_stack.back()); + } else { + add_parentheses(i); + } + + // Restart the scan since we modified the expression + i = -1; // Will be incremented to 0 by the for loop + op_stack = {0}; + pos_stack = {0}; } - } else if (token > OP_COMPLEMENT) { - // If the token is a parenthesis reset the current operator - current_op = 0; - current_dist = 0; } } } @@ -1334,14 +1370,14 @@ extern "C" int openmc_cell_bounding_box( bbox = c->bounding_box(); // set lower left corner values - llc[0] = bbox.xmin; - llc[1] = bbox.ymin; - llc[2] = bbox.zmin; + llc[0] = bbox.min.x; + llc[1] = bbox.min.y; + llc[2] = bbox.min.z; // set upper right corner values - urc[0] = bbox.xmax; - urc[1] = bbox.ymax; - urc[2] = bbox.zmax; + urc[0] = bbox.max.x; + urc[1] = bbox.max.y; + urc[2] = bbox.max.z; return 0; } diff --git a/src/chain.cpp b/src/chain.cpp index e279d1f59..3915c016c 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -42,7 +42,7 @@ ChainNuclide::ChainNuclide(pugi::xml_node node) branching_ratio = std::stod(get_node_value(reaction_node, "branching_ratio")); } - int mt = reaction_type(rx_name); + int mt = reaction_mt(rx_name); reaction_products_[mt].push_back({rx_target, branching_ratio}); } @@ -70,8 +70,15 @@ ChainNuclide::~ChainNuclide() void DecayPhotonAngleEnergy::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { - E_out = photon_energy_->sample(seed); - mu = Uniform(-1., 1.).sample(seed); + E_out = photon_energy_->sample(seed).first; + mu = Uniform(-1., 1.).sample(seed).first; +} + +double DecayPhotonAngleEnergy::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + E_out = photon_energy_->sample(seed).first; + return 0.5; } //============================================================================== @@ -91,6 +98,8 @@ vector> chain_nuclides; void read_chain_file_xml() { + free_memory_chain(); + char* chain_file_path = std::getenv("OPENMC_CHAIN_FILE"); if (!chain_file_path) { return; @@ -113,4 +122,10 @@ void read_chain_file_xml() } } +void free_memory_chain() +{ + data::chain_nuclides.clear(); + data::chain_nuclide_map.clear(); +} + } // namespace openmc diff --git a/src/cmfd_solver.cpp b/src/cmfd_solver.cpp index 943042f67..714a5bf3a 100644 --- a/src/cmfd_solver.cpp +++ b/src/cmfd_solver.cpp @@ -5,7 +5,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/bank.h" #include "openmc/capi.h" @@ -36,7 +36,7 @@ double spectral; int nx, ny, nz, ng; -xt::xtensor indexmap; +tensor::Tensor indexmap; int use_all_threads; @@ -79,15 +79,14 @@ int get_cmfd_energy_bin(const double E) // COUNT_BANK_SITES bins fission sites according to CMFD mesh and energy //============================================================================== -xt::xtensor count_bank_sites( - xt::xtensor& bins, bool* outside) +tensor::Tensor count_bank_sites( + tensor::Tensor& bins, bool* outside) { // Determine shape of array for counts std::size_t cnt_size = cmfd::nx * cmfd::ny * cmfd::nz * cmfd::ng; - vector cnt_shape = {cnt_size}; // Create array of zeros - xt::xarray cnt {cnt_shape, 0.0}; + tensor::Tensor cnt = tensor::zeros({cnt_size}); bool outside_ = false; auto bank_size = simulation::source_bank.size(); @@ -113,29 +112,22 @@ xt::xtensor count_bank_sites( bins[i] = mesh_bin * cmfd::ng + energy_bin; } - // Create copy of count data. Since ownership will be acquired by xtensor, - // std::allocator must be used to avoid Valgrind mismatched free() / delete - // warnings. int total = cnt.size(); - double* cnt_reduced = std::allocator {}.allocate(total); + tensor::Tensor counts = tensor::zeros({cnt_size}); #ifdef OPENMC_MPI // collect values from all processors MPI_Reduce( - cnt.data(), cnt_reduced, total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); + cnt.data(), counts.data(), total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); // Check if there were sites outside the mesh for any processor MPI_Reduce(&outside_, outside, 1, MPI_C_BOOL, MPI_LOR, 0, mpi::intracomm); #else - std::copy(cnt.data(), cnt.data() + total, cnt_reduced); + std::copy(cnt.data(), cnt.data() + total, counts.data()); *outside = outside_; #endif - // Adapt reduced values in array back into an xarray - auto arr = xt::adapt(cnt_reduced, total, xt::acquire_ownership(), cnt_shape); - xt::xarray counts = arr; - return counts; } @@ -151,19 +143,19 @@ extern "C" void openmc_cmfd_reweight( std::size_t src_size = cmfd::nx * cmfd::ny * cmfd::nz * cmfd::ng; // count bank sites for CMFD mesh, store bins in bank_bins for reweighting - xt::xtensor bank_bins({bank_size}, 0); + tensor::Tensor bank_bins = tensor::zeros({bank_size}); bool sites_outside; - xt::xtensor sourcecounts = + tensor::Tensor sourcecounts = count_bank_sites(bank_bins, &sites_outside); // Compute CMFD weightfactors - xt::xtensor weightfactors = xt::xtensor({src_size}, 1.); + tensor::Tensor weightfactors = tensor::ones({src_size}); if (mpi::master) { if (sites_outside) { fatal_error("Source sites outside of the CMFD mesh"); } - double norm = xt::sum(sourcecounts)() / cmfd::norm; + double norm = sourcecounts.sum() / cmfd::norm; for (int i = 0; i < src_size; i++) { if (sourcecounts[i] > 0 && cmfd_src[i] > 0) { weightfactors[i] = cmfd_src[i] * norm / sourcecounts[i]; @@ -561,7 +553,7 @@ void free_memory_cmfd() cmfd::indices.clear(); cmfd::egrid.clear(); - // Resize xtensors to be empty + // Resize tensors to be empty cmfd::indexmap.resize({0}); // Set pointers to null diff --git a/src/collision_track.cpp b/src/collision_track.cpp index 75e56574a..2e749007f 100644 --- a/src/collision_track.cpp +++ b/src/collision_track.cpp @@ -78,10 +78,10 @@ void write_collision_track_bank(hid_t group_id, hid_t banktype = h5_collision_track_banktype(); #ifdef OPENMC_MPI write_bank_dataset("collision_track_bank", group_id, collision_track_bank, - bank_index, banktype, mpi::collision_track_site); + bank_index, banktype, banktype, mpi::collision_track_site); #else write_bank_dataset("collision_track_bank", group_id, collision_track_bank, - bank_index, banktype); + bank_index, banktype, banktype); #endif H5Tclose(banktype); @@ -200,8 +200,13 @@ void collision_track_record(Particle& particle) return; int cell_id = model::cells[cell_index]->id_; - const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); - std::string nuclide = nuclide_ptr->name_; + std::string nuclide {}; + int nuclide_id = 0; + if (particle.event_nuclide() != NUCLIDE_NONE) { + const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); + nuclide = nuclide_ptr->name_; + nuclide_id = nuclide_ptr->particle_type().pdg_number(); + } int universe_id = model::universes[particle.lowest_coord().universe()]->id_; double delta_E = particle.E_last() - particle.E(); int material_index = particle.material(); @@ -224,8 +229,7 @@ void collision_track_record(Particle& particle) site.event_mt = particle.event_mt(); site.delayed_group = particle.delayed_group(); site.cell_id = cell_id; - site.nuclide_id = - 10000 * nuclide_ptr->Z_ + 10 * nuclide_ptr->A_ + nuclide_ptr->metastable_; + site.nuclide_id = nuclide_id; site.material_id = material_id; site.universe_id = universe_id; site.n_collision = particle.n_collision(); diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index b1bfde03d..ec9da5b8a 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -254,7 +254,7 @@ void read_ce_cross_sections(const vector>& nuc_temps, if (settings::photon_transport && settings::electron_treatment == ElectronTreatment::TTB) { // Take logarithm of energies since they are log-log interpolated - data::ttb_e_grid = xt::log(data::ttb_e_grid); + data::ttb_e_grid = tensor::log(data::ttb_e_grid); } // Show minimum/maximum temperature diff --git a/src/dagmc.cpp b/src/dagmc.cpp index b2ebe89c0..a5cc71b52 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -50,6 +50,10 @@ namespace openmc { DAGUniverse::DAGUniverse(pugi::xml_node node) { + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + DensityOverrides density_overrides; + if (check_for_node(node, "id")) { id_ = std::stoi(get_node_value(node, "id")); } else { @@ -76,24 +80,77 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) adjust_material_ids_ = get_node_value_bool(node, "auto_mat_ids"); } - // get material assignment overloading - if (check_for_node(node, "material_overrides")) { - auto mat_node = node.child("material_overrides"); - // loop over all subelements (each subelement corresponds to a material) - for (pugi::xml_node cell_node : mat_node.children("cell_override")) { - // Store assignment reference name - int32_t ref_assignment = std::stoi(get_node_value(cell_node, "id")); + // Get material assignment overrides from nested DAGMC cell elements. + if (node.child("cell")) { + for (pugi::xml_node cell_node : node.children("cell")) { + if (!check_for_node(cell_node, "id")) { + fatal_error( + "Must specify id for each DAGMC cell override in ."); + } - // Get mat name for each assignement instances - vector instance_mats = - get_node_array(cell_node, "material_ids"); + int32_t cell_id = std::stoi(get_node_value(cell_node, "id")); - // Store mat name for each instances - material_overrides_.emplace(ref_assignment, instance_mats); + if (check_for_node(cell_node, "region")) { + fatal_error(fmt::format( + "DAGMC cell {} override cannot specify a region.", cell_id)); + } + if (check_for_node(cell_node, "fill")) { + fatal_error(fmt::format( + "DAGMC cell {} override currently only supports material fills.", + cell_id)); + } + if (check_for_node(cell_node, "universe")) { + fatal_error(fmt::format( + "DAGMC cell {} override cannot specify a universe.", cell_id)); + } + if (check_for_node(cell_node, "translation") || + check_for_node(cell_node, "rotation")) { + fatal_error(fmt::format( + "DAGMC cell {} override does not support translation or rotation.", + cell_id)); + } + if (!check_for_node(cell_node, "material")) { + fatal_error(fmt::format( + "DAGMC cell {} override must specify material.", cell_id)); + } + + auto inserted = material_overrides.emplace( + cell_id, parse_cell_material_xml(cell_node, cell_id)); + if (!inserted.second) { + fatal_error(fmt::format( + "Duplicate DAGMC cell override specified for cell {}", cell_id)); + } + + if (check_for_node(cell_node, "temperature")) { + temperature_overrides.emplace( + cell_id, parse_cell_temperature_xml(cell_node, cell_id)); + } + + if (check_for_node(cell_node, "density")) { + density_overrides.emplace( + cell_id, parse_cell_density_xml(cell_node, cell_id)); + } + } + } else if (check_for_node(node, "material_overrides")) { + if (node.child("cell")) { + fatal_error("DAGMCUniverse cannot specify both and " + " sub-elements. Use elements only."); + } + warning("DAGMCUniverse is deprecated. Use nested " + " elements under instead."); + for (pugi::xml_node co : + node.child("material_overrides").children("cell_override")) { + int32_t cell_id = std::stoi(get_node_value(co, "id")); + std::istringstream iss(co.child("material_ids").text().get()); + vector mats; + for (std::string s; iss >> s;) { + mats.push_back(s == "void" ? MATERIAL_VOID : std::stoi(s)); + } + material_overrides.emplace(cell_id, mats); } } - initialize(); + initialize(material_overrides, temperature_overrides, density_overrides); } DAGUniverse::DAGUniverse( @@ -110,9 +167,12 @@ DAGUniverse::DAGUniverse(std::shared_ptr dagmc_ptr, : dagmc_instance_(dagmc_ptr), filename_(filename), adjust_geometry_ids_(auto_geom_ids), adjust_material_ids_(auto_mat_ids) { + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + DensityOverrides density_overrides; set_id(); init_metadata(); - init_geometry(); + init_geometry(material_overrides, temperature_overrides, density_overrides); } void DAGUniverse::set_id() @@ -130,6 +190,15 @@ void DAGUniverse::set_id() } void DAGUniverse::initialize() +{ + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + initialize(material_overrides, temperature_overrides); +} + +void DAGUniverse::initialize(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides) { #ifdef OPENMC_UWUW_ENABLED // read uwuw materials from the .h5m file if present @@ -140,7 +209,7 @@ void DAGUniverse::initialize() init_metadata(); - init_geometry(); + init_geometry(material_overrides, temperature_overrides, density_overrides); } void DAGUniverse::init_dagmc() @@ -176,7 +245,9 @@ void DAGUniverse::init_metadata() MB_CHK_ERR_CONT(rval); } -void DAGUniverse::init_geometry() +void DAGUniverse::init_geometry(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides) { moab::ErrorCode rval; @@ -202,6 +273,9 @@ void DAGUniverse::init_geometry() : dagmc_instance_->id_by_index(3, c->dag_index()); c->universe_ = this->id_; c->fill_ = C_NONE; // no fill, single universe + if (dagmc_instance_->is_implicit_complement(vol_handle)) { + c->name_ = "implicit complement"; + } auto in_map = model::cell_map.find(c->id_); if (in_map == model::cell_map.end()) { @@ -230,16 +304,68 @@ void DAGUniverse::init_geometry() if (mat_str == "graveyard") { graveyard = vol_handle; } - // material void checks - if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") { + if (material_overrides.count(c->id_)) { + override_assign_material(c, material_overrides); + } else if (mat_str == "void" || mat_str == "vacuum" || + mat_str == "graveyard") { c->material_.push_back(MATERIAL_VOID); + } else if (uses_uwuw()) { + uwuw_assign_material(vol_handle, c); } else { - if (material_overrides_.count(c->id_)) { - override_assign_material(c); - } else if (uses_uwuw()) { - uwuw_assign_material(vol_handle, c); - } else { - legacy_assign_material(mat_str, c); + legacy_assign_material(mat_str, c); + } + + if (temperature_overrides.count(c->id_)) { + if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) { + fatal_error(fmt::format("DAGMC cell {} was specified with a " + "temperature but no non-void material.", + c->id_)); + } + + c->sqrtkT_.clear(); + const auto& temp_overrides = temperature_overrides.at(c->id_); + c->sqrtkT_.reserve(temp_overrides.size()); + for (auto T : temp_overrides) { + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * T)); + } + + if (settings::verbosity >= 10) { + std::stringstream override_values; + for (size_t i = 0; i < temp_overrides.size(); ++i) { + if (i > 0) { + override_values << " "; + } + override_values << temp_overrides[i]; + } + auto msg = fmt::format("Overriding DAGMC cell {} property " + "'temperature [K]' with value(s): {}", + c->id_, override_values.str()); + write_message(msg, 10); + } + } + + if (density_overrides.count(c->id_)) { + if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) { + fatal_error(fmt::format("DAGMC cell {} was specified with a density " + "but no non-void material.", + c->id_)); + } + // density_mult_ holds the true density until materials are finalized, + // at which point it is converted to a proper multiplier (same as CSG). + c->density_mult_ = density_overrides.at(c->id_); + + if (settings::verbosity >= 10) { + const auto& dens = density_overrides.at(c->id_); + std::stringstream override_values; + for (size_t i = 0; i < dens.size(); ++i) { + if (i > 0) + override_values << " "; + override_values << dens[i]; + } + write_message(fmt::format("Overriding DAGMC cell {} property " + "'density [g/cm³]' with value(s): {}", + c->id_, override_values.str()), + 10); } } @@ -252,18 +378,21 @@ void DAGUniverse::init_geometry() continue; } - // assign cell temperature - const auto& mat = model::materials[model::material_map.at(c->material_[0])]; - if (dagmc_instance_->has_prop(vol_handle, "temp")) { - rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value); - MB_CHK_ERR_CONT(rval); - double temp = std::stod(temp_value); - c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * temp)); - } else if (mat->temperature() > 0.0) { - c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * mat->temperature())); - } else { - c->sqrtkT_.push_back( - std::sqrt(K_BOLTZMANN * settings::temperature_default)); + // assign cell temperature if not explicitly overridden + if (c->sqrtkT_.empty()) { + const auto& mat = + model::materials[model::material_map.at(c->material_[0])]; + if (dagmc_instance_->has_prop(vol_handle, "temp")) { + rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value); + MB_CHK_ERR_CONT(rval); + double temp = std::stod(temp_value); + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * temp)); + } else if (mat->temperature() > 0.0) { + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * mat->temperature())); + } else { + c->sqrtkT_.push_back( + std::sqrt(K_BOLTZMANN * settings::temperature_default)); + } } model::cells.emplace_back(std::move(c)); @@ -630,7 +759,8 @@ void DAGUniverse::uwuw_assign_material( #endif // OPENMC_UWUW_ENABLED } -void DAGUniverse::override_assign_material(std::unique_ptr& c) const +void DAGUniverse::override_assign_material(std::unique_ptr& c, + const MaterialOverrides& material_overrides) const { // if Cell ID matches an override key, use it to override the material // assignment else if UWUW is used, get the material assignment from the DAGMC @@ -638,17 +768,30 @@ void DAGUniverse::override_assign_material(std::unique_ptr& c) const // Notify User that an override is being applied on a DAGMCCell write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8); + const auto& mat_overrides = material_overrides.at(c->id_); if (settings::verbosity >= 10) { - auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on " - "override information (see input XML).", - c->id_); + std::stringstream override_values; + for (size_t i = 0; i < mat_overrides.size(); ++i) { + if (i > 0) { + override_values << " "; + } + if (mat_overrides[i] == MATERIAL_VOID) { + override_values << "void"; + } else { + override_values << mat_overrides[i]; + } + } + auto msg = fmt::format("Overriding DAGMC cell {} property 'material' " + "with value(s): {}", + c->id_, override_values.str()); write_message(msg, 10); } // Override the material assignment for each cell instance using the legacy // assignement - for (auto mat_id : material_overrides_.at(c->id_)) { - if (model::material_map.find(mat_id) == model::material_map.end()) { + for (auto mat_id : mat_overrides) { + if (mat_id != MATERIAL_VOID && + model::material_map.find(mat_id) == model::material_map.end()) { fatal_error(fmt::format( "Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_)); } @@ -753,7 +896,7 @@ BoundingBox DAGCell::bounding_box() const double min[3], max[3]; rval = dagmc_ptr_->getobb(vol, min, max); MB_CHK_ERR_CONT(rval); - return {min[0], max[0], min[1], max[1], min[2], max[2]}; + return {{min[0], min[1], min[2]}, {max[0], max[1], max[2]}}; } //============================================================================== diff --git a/src/distribution.cpp b/src/distribution.cpp index ca00cbde4..cbdd13127 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -7,15 +7,90 @@ #include // for accumulate #include // for runtime_error #include // for string, stod +#include +#include "openmc/chain.h" +#include "openmc/constants.h" #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/random_dist.h" #include "openmc/random_lcg.h" #include "openmc/xml_interface.h" +namespace { +std::unordered_set decay_spectrum_missing_chain_nuclides; +} + namespace openmc { +//============================================================================== +// Helper function for computing importance weights from biased sampling +//============================================================================== + +vector compute_importance_weights( + const vector& p, const vector& b) +{ + std::size_t n = p.size(); + + // Normalize original probabilities + double sum_p = std::accumulate(p.begin(), p.end(), 0.0); + vector p_norm(n); + for (std::size_t i = 0; i < n; ++i) { + p_norm[i] = p[i] / sum_p; + } + + // Normalize bias probabilities + double sum_b = std::accumulate(b.begin(), b.end(), 0.0); + vector b_norm(n); + for (std::size_t i = 0; i < n; ++i) { + b_norm[i] = b[i] / sum_b; + } + + // Compute importance weights + vector weights(n); + for (std::size_t i = 0; i < n; ++i) { + weights[i] = (b_norm[i] == 0.0) ? INFTY : p_norm[i] / b_norm[i]; + } + return weights; +} + +std::pair Distribution::sample(uint64_t* seed) const +{ + if (bias_) { + // Sample from the bias distribution and compute importance weight + double val = bias_->sample_unbiased(seed); + double wgt = this->evaluate(val) / bias_->evaluate(val); + return {val, wgt}; + } else { + // Unbiased sampling: return sampled value with weight 1.0 + double val = sample_unbiased(seed); + return {val, 1.0}; + } +} + +// PDF evaluation not supported for all distribution types +double Distribution::evaluate(double x) const +{ + throw std::runtime_error( + "PDF evaluation not implemented for this distribution type."); +} + +void Distribution::read_bias_from_xml(pugi::xml_node node) +{ + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + + if (check_for_node(bias_node, "bias")) { + openmc::fatal_error( + "Distribution has a bias distribution with its own bias distribution. " + "Please ensure bias distributions do not have their own bias."); + } + + UPtrDist bias = distribution_from_xml(bias_node); + this->set_bias(std::move(bias)); + } +} + //============================================================================== // DiscreteIndex implementation //============================================================================== @@ -36,7 +111,6 @@ DiscreteIndex::DiscreteIndex(span p) void DiscreteIndex::assign(span p) { prob_.assign(p.begin(), p.end()); - this->init_alias(); } @@ -115,24 +189,55 @@ void DiscreteIndex::normalize() // Discrete implementation //============================================================================== -Discrete::Discrete(pugi::xml_node node) : di_(node) +Discrete::Discrete(pugi::xml_node node) { auto params = get_node_array(node, "parameters"); - std::size_t n = params.size() / 2; + // First half is x values, second half is probabilities x_.assign(params.begin(), params.begin() + n); + const double* p = params.data() + n; + + // Check for bias + if (check_for_node(node, "bias")) { + // Get bias probabilities + auto bias_params = get_node_array(node, "bias"); + if (bias_params.size() != n) { + openmc::fatal_error( + "Size mismatch: Attempted to bias Discrete distribution with " + + std::to_string(n) + " probability entries using a bias with " + + std::to_string(bias_params.size()) + + " entries. Please ensure distributions have the same size."); + } + + // Compute importance weights + vector p_vec(p, p + n); + weight_ = compute_importance_weights(p_vec, bias_params); + + // Initialize DiscreteIndex with bias probabilities for sampling + di_.assign(bias_params); + } else { + // Unbiased case: weight_ stays empty + di_.assign({p, n}); + } } Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n}) { - x_.assign(x, x + n); } -double Discrete::sample(uint64_t* seed) const +std::pair Discrete::sample(uint64_t* seed) const { - return x_[di_.sample(seed)]; + size_t idx = di_.sample(seed); + double wgt = weight_.empty() ? 1.0 : weight_[idx]; + return {x_[idx], wgt}; +} + +double Discrete::sample_unbiased(uint64_t* seed) const +{ + size_t idx = di_.sample(seed); + return x_[idx]; } //============================================================================== @@ -149,13 +254,26 @@ Uniform::Uniform(pugi::xml_node node) a_ = params.at(0); b_ = params.at(1); + + read_bias_from_xml(node); } -double Uniform::sample(uint64_t* seed) const +double Uniform::sample_unbiased(uint64_t* seed) const { return a_ + prn(seed) * (b_ - a_); } +double Uniform::evaluate(double x) const +{ + if (x <= a()) { + return 0.0; + } else if (x >= b()) { + return 0.0; + } else { + return 1 / (b() - a()); + } +} + //============================================================================== // PowerLaw implementation //============================================================================== @@ -175,9 +293,24 @@ PowerLaw::PowerLaw(pugi::xml_node node) offset_ = std::pow(a, n + 1); span_ = std::pow(b, n + 1) - offset_; ninv_ = 1 / (n + 1); + + read_bias_from_xml(node); } -double PowerLaw::sample(uint64_t* seed) const +double PowerLaw::evaluate(double x) const +{ + if (x <= a()) { + return 0.0; + } else if (x >= b()) { + return 0.0; + } else { + int pwr = n() + 1; + double norm = pwr / span_; + return norm * std::pow(std::fabs(x), n()); + } +} + +double PowerLaw::sample_unbiased(uint64_t* seed) const { return std::pow(offset_ + prn(seed) * span_, ninv_); } @@ -189,13 +322,21 @@ double PowerLaw::sample(uint64_t* seed) const Maxwell::Maxwell(pugi::xml_node node) { theta_ = std::stod(get_node_value(node, "parameters")); + + read_bias_from_xml(node); } -double Maxwell::sample(uint64_t* seed) const +double Maxwell::sample_unbiased(uint64_t* seed) const { return maxwell_spectrum(theta_, seed); } +double Maxwell::evaluate(double x) const +{ + double c = (2.0 / SQRT_PI) * std::pow(theta_, -1.5); + return c * std::sqrt(x) * std::exp(-x / theta_); +} + //============================================================================== // Watt implementation //============================================================================== @@ -209,31 +350,111 @@ Watt::Watt(pugi::xml_node node) a_ = params.at(0); b_ = params.at(1); + + read_bias_from_xml(node); } -double Watt::sample(uint64_t* seed) const +double Watt::sample_unbiased(uint64_t* seed) const { return watt_spectrum(a_, b_, seed); } +double Watt::evaluate(double x) const +{ + double c = + 2.0 / (std::sqrt(PI * b_) * std::pow(a_, 1.5) * std::exp(a_ * b_ / 4.0)); + return c * std::exp(-x / a_) * std::sinh(std::sqrt(b_ * x)); +} + //============================================================================== // Normal implementation //============================================================================== + +Normal::Normal(double mean_value, double std_dev, double lower, double upper) + : mean_value_ {mean_value}, std_dev_ {std_dev}, lower_ {lower}, upper_ {upper} +{ + compute_normalization(); +} + Normal::Normal(pugi::xml_node node) { auto params = get_node_array(node, "parameters"); - if (params.size() != 2) { + if (params.size() != 2 && params.size() != 4) { openmc::fatal_error("Normal energy distribution must have two " - "parameters specified."); + "parameters (mean, std_dev) or four parameters " + "(mean, std_dev, lower, upper) specified."); } mean_value_ = params.at(0); std_dev_ = params.at(1); + + // Optional truncation bounds + if (params.size() == 4) { + lower_ = params.at(2); + upper_ = params.at(3); + } else { + lower_ = -INFTY; + upper_ = INFTY; + } + + compute_normalization(); + read_bias_from_xml(node); } -double Normal::sample(uint64_t* seed) const +void Normal::compute_normalization() { - return normal_variate(mean_value_, std_dev_, seed); + // Validate bounds + if (lower_ >= upper_) { + openmc::fatal_error( + "Normal distribution lower bound must be less than upper bound."); + } + + // Check if truncation bounds are finite + is_truncated_ = (lower_ > -INFTY || upper_ < INFTY); + + if (is_truncated_) { + double alpha = (lower_ - mean_value_) / std_dev_; + double beta = (upper_ - mean_value_) / std_dev_; + double cdf_diff = standard_normal_cdf(beta) - standard_normal_cdf(alpha); + + if (cdf_diff <= 0.0) { + openmc::fatal_error( + "Normal distribution truncation bounds exclude entire distribution."); + } + norm_factor_ = 1.0 / cdf_diff; + } else { + norm_factor_ = 1.0; + } +} + +double Normal::sample_unbiased(uint64_t* seed) const +{ + if (!is_truncated_) { + return normal_variate(mean_value_, std_dev_, seed); + } + + // Rejection sampling for truncated normal + double x; + do { + x = normal_variate(mean_value_, std_dev_, seed); + } while (x < lower_ || x > upper_); + return x; +} + +double Normal::evaluate(double x) const +{ + // Return 0 outside truncation bounds + if (x < lower_ || x > upper_) { + return 0.0; + } + + // Standard normal PDF value + double pdf = (1.0 / (std::sqrt(2.0 * PI) * std_dev_)) * + std::exp(-std::pow((x - mean_value_), 2.0) / + (2.0 * std::pow(std_dev_, 2.0))); + + // Apply normalization for truncation + return pdf * norm_factor_; } //============================================================================== @@ -248,6 +469,10 @@ Tabular::Tabular(pugi::xml_node node) interp_ = Interpolation::histogram; } else if (temp == "linear-linear") { interp_ = Interpolation::lin_lin; + } else if (temp == "log-linear") { + interp_ = Interpolation::log_lin; + } else if (temp == "log-log") { + interp_ = Interpolation::log_log; } else { openmc::fatal_error( "Unsupported interpolation type for distribution: " + temp); @@ -266,6 +491,8 @@ Tabular::Tabular(pugi::xml_node node) const double* x = params.data(); const double* p = x + n; init(x, p, n); + + read_bias_from_xml(node); } Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp, @@ -282,13 +509,6 @@ void Tabular::init( std::copy(x, x + n, std::back_inserter(x_)); std::copy(p, p + n, std::back_inserter(p_)); - // Check interpolation parameter - if (interp_ != Interpolation::histogram && - interp_ != Interpolation::lin_lin) { - openmc::fatal_error("Only histogram and linear-linear interpolation " - "for tabular distribution is supported."); - } - // Calculate cumulative distribution function if (c) { std::copy(c, c + n, std::back_inserter(c_)); @@ -300,6 +520,18 @@ void Tabular::init( c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]); } else if (interp_ == Interpolation::lin_lin) { c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]); + } else if (interp_ == Interpolation::log_lin) { + double m = std::log(p_[i] / p_[i - 1]) / (x_[i] - x_[i - 1]); + c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]) * + exprel(m * (x_[i] - x_[i - 1])); + } else if (interp_ == Interpolation::log_log) { + double m = std::log((x_[i] * p_[i]) / (x_[i - 1] * p_[i - 1])) / + std::log(x_[i] / x_[i - 1]); + c_[i] = c_[i - 1] + x_[i - 1] * p_[i - 1] * + std::log(x_[i] / x_[i - 1]) * + exprel(m * std::log(x_[i] / x_[i - 1])); + } else { + UNREACHABLE(); } } } @@ -314,20 +546,15 @@ void Tabular::init( } } -double Tabular::sample(uint64_t* seed) const +double Tabular::sample_unbiased(uint64_t* seed) const { // Sample value of CDF double c = prn(seed); // Find first CDF bin which is above the sampled value - double c_i = c_[0]; - int i; - std::size_t n = c_.size(); - for (i = 0; i < n - 1; ++i) { - if (c <= c_[i + 1]) - break; - c_i = c_[i + 1]; - } + auto c_iter = std::lower_bound(c_.begin() + 1, c_.end(), c); + int i = std::distance(c_.begin(), c_iter) - 1; + double c_i = c_[i]; // Determine bounding PDF values double x_i = x_[i]; @@ -340,7 +567,7 @@ double Tabular::sample(uint64_t* seed) const } else { return x_i; } - } else { + } else if (interp_ == Interpolation::lin_lin) { // Linear-linear interpolation double x_i1 = x_[i + 1]; double p_i1 = p_[i + 1]; @@ -353,6 +580,52 @@ double Tabular::sample(uint64_t* seed) const (std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) / m; } + } else if (interp_ == Interpolation::log_lin) { + // Log-linear interpolation + double x_i1 = x_[i + 1]; + double p_i1 = p_[i + 1]; + + double m = std::log(p_i1 / p_i) / (x_i1 - x_i); + double f = (c - c_i) / p_i; + return x_i + f * log1prel(m * f); + } else if (interp_ == Interpolation::log_log) { + // Log-Log interpolation + double x_i1 = x_[i + 1]; + double p_i1 = p_[i + 1]; + + double m = std::log((x_i1 * p_i1) / (x_i * p_i)) / std::log(x_i1 / x_i); + double f = (c - c_i) / (p_i * x_i); + return x_i * std::exp(f * log1prel(m * f)); + } else { + UNREACHABLE(); + } +} + +double Tabular::evaluate(double x) const +{ + int i; + + if (interp_ == Interpolation::histogram) { + i = std::upper_bound(x_.begin(), x_.end(), x) - x_.begin() - 1; + if (i < 0 || i >= static_cast(p_.size())) { + return 0.0; + } else { + return p_[i]; + } + } else { + i = std::lower_bound(x_.begin(), x_.end(), x) - x_.begin() - 1; + + if (i < 0 || i >= static_cast(p_.size()) - 1) { + return 0.0; + } else { + double x0 = x_[i]; + double x1 = x_[i + 1]; + double p0 = p_[i]; + double p1 = p_[i + 1]; + + double t = (x - x0) / (x1 - x0); + return (1 - t) * p0 + t * p1; + } } } @@ -360,7 +633,7 @@ double Tabular::sample(uint64_t* seed) const // Equiprobable implementation //============================================================================== -double Equiprobable::sample(uint64_t* seed) const +double Equiprobable::sample_unbiased(uint64_t* seed) const { std::size_t n = x_.size(); @@ -372,13 +645,27 @@ double Equiprobable::sample(uint64_t* seed) const return xl + ((n - 1) * r - i) * (xr - xl); } +double Equiprobable::evaluate(double x) const +{ + double x_min = *std::min_element(x_.begin(), x_.end()); + double x_max = *std::max_element(x_.begin(), x_.end()); + + if (x < x_min || x > x_max) { + return 0.0; + } else { + return 1.0 / (x_max - x_min); + } +} + //============================================================================== // Mixture implementation //============================================================================== Mixture::Mixture(pugi::xml_node node) { - double cumsum = 0.0; + vector probabilities; + + // First pass: collect distributions and their probabilities for (pugi::xml_node pair : node.children("pair")) { // Check that required data exists if (!pair.attribute("probability")) @@ -386,39 +673,60 @@ Mixture::Mixture(pugi::xml_node node) if (!pair.child("dist")) fatal_error("Mixture pair element does not have a distribution."); - // cummulative sum of probabilities + // Get probability and distribution double p = std::stod(pair.attribute("probability").value()); - - // Save cummulative probability and distribution auto dist = distribution_from_xml(pair.child("dist")); - cumsum += p * dist->integral(); - distribution_.push_back(std::make_pair(cumsum, std::move(dist))); + // Weight probability by the distribution's integral + double weighted_prob = p * dist->integral(); + probabilities.push_back(weighted_prob); + distribution_.push_back(std::move(dist)); } - // Save integral of distribution - integral_ = cumsum; + // Save sum of weighted probabilities + integral_ = std::accumulate(probabilities.begin(), probabilities.end(), 0.0); - // Normalize cummulative probabilities to 1 - for (auto& pair : distribution_) { - pair.first /= cumsum; + std::size_t n = probabilities.size(); + + // Check for bias + if (check_for_node(node, "bias")) { + // Get bias probabilities + auto bias_params = get_node_array(node, "bias"); + if (bias_params.size() != n) { + openmc::fatal_error( + "Size mismatch: Attempted to bias Mixture distribution with " + + std::to_string(n) + " components using a bias with " + + std::to_string(bias_params.size()) + + " entries. Please ensure distributions have the same size."); + } + + // Compute importance weights + weight_ = compute_importance_weights(probabilities, bias_params); + + // Initialize DiscreteIndex with bias probabilities for sampling + di_.assign(bias_params); + } else { + // Unbiased case: weight_ stays empty + di_.assign(probabilities); } } -double Mixture::sample(uint64_t* seed) const +std::pair Mixture::sample(uint64_t* seed) const { - // Sample value of CDF - const double p = prn(seed); - - // find matching distribution - const auto it = std::lower_bound(distribution_.cbegin(), distribution_.cend(), - p, [](const DistPair& pair, double p) { return pair.first < p; }); - - // This should not happen. Catch it - assert(it != distribution_.cend()); + size_t idx = di_.sample(seed); // Sample the chosen distribution - return it->second->sample(seed); + auto [val, sub_wgt] = distribution_[idx]->sample(seed); + + // Multiply by component selection weight + double mix_wgt = weight_.empty() ? 1.0 : weight_[idx]; + return {val, mix_wgt * sub_wgt}; +} + +double Mixture::sample_unbiased(uint64_t* seed) const +{ + size_t idx = di_.sample(seed); + return distribution_[idx]->sample(seed).first; } //============================================================================== @@ -451,6 +759,8 @@ UPtrDist distribution_from_xml(pugi::xml_node node) dist = UPtrDist {new Tabular(node)}; } else if (type == "mixture") { dist = UPtrDist {new Mixture(node)}; + } else if (type == "decay_spectrum") { + dist = UPtrDist {new DecaySpectrum(node)}; } else if (type == "muir") { openmc::fatal_error( "'muir' distributions are now specified using the openmc.stats.muir() " @@ -461,4 +771,120 @@ UPtrDist distribution_from_xml(pugi::xml_node node) return dist; } +//============================================================================== +// DecaySpectrum implementation +//============================================================================== + +DecaySpectrum::DecaySpectrum(pugi::xml_node node) +{ + // Read the region volume [cm^3] needed for absolute emission rate + if (!check_for_node(node, "volume")) + fatal_error("DecaySpectrum: 'volume' attribute is required."); + double volume = std::stod(get_node_value(node, "volume")); + + // Read nuclide names and atom densities from XML + vector nuclide_indices; + vector atoms; + auto names = get_node_array(node, "nuclides"); + auto densities = get_node_array(node, "parameters"); + if (names.size() != densities.size()) { + fatal_error("DecaySpectrum nuclides and parameters must have the same " + "length."); + } + + for (size_t i = 0; i < names.size(); ++i) { + const auto& name = names[i]; + double density = densities[i]; + + // Look up nuclide in the depletion chain + auto it = data::chain_nuclide_map.find(name); + if (it == data::chain_nuclide_map.end()) { + if (decay_spectrum_missing_chain_nuclides.insert(name).second) { + warning("Nuclide '" + name + + "' appears in a DecaySpectrum source but is not present in " + "the depletion chain; it will be ignored."); + } + continue; + } + + int nuclide_index = it->second; + const auto& chain_nuc = data::chain_nuclides[nuclide_index]; + const Distribution* photon_dist = chain_nuc->photon_energy(); + if (!photon_dist) + continue; + + // Skip non-positive densities and warn if negative + if (density <= 0.0) { + if (density < 0.0) { + warning("Nuclide '" + name + + "' has a negative density in a DecaySpectrum source; it will " + "be ignored."); + } + continue; + } + + // atoms = density [atom/b-cm] * 1e24 [b/cm^2] * volume [cm^3] + double atoms_i = density * 1.0e24 * volume; + + nuclide_indices.push_back(nuclide_index); + atoms.push_back(atoms_i); + } + + init(std::move(nuclide_indices), atoms); +} + +void DecaySpectrum::init( + vector nuclide_indices, const vector& atoms) +{ + if (nuclide_indices.size() != atoms.size()) { + fatal_error("DecaySpectrum nuclide index and atoms arrays must have " + "the same length."); + } + + vector probs; + probs.reserve(nuclide_indices.size()); + for (size_t i = 0; i < nuclide_indices.size(); ++i) { + // Distribution integral is in [photons/s/atom]; multiplying by atoms gives + // the total emission rate [photons/s] for this nuclide. + const auto* dist = + data::chain_nuclides[nuclide_indices[i]]->photon_energy(); + probs.push_back(atoms[i] * dist->integral()); + } + + nuclide_indices_ = std::move(nuclide_indices); + integral_ = std::accumulate(probs.begin(), probs.end(), 0.0); + if (nuclide_indices_.empty() || integral_ <= 0.0) { + fatal_error("DecaySpectrum source did not resolve any nuclides with decay " + "photon spectra and positive atom densities. Ensure " + "OPENMC_CHAIN_FILE is set and matches the nuclides in the " + "source definition."); + } + di_.assign(probs); +} + +DecaySpectrum::Sample DecaySpectrum::sample_with_parent(uint64_t* seed) const +{ + size_t idx = di_.sample(seed); + int parent_nuclide = nuclide_indices_[idx]; + const auto* dist = data::chain_nuclides[parent_nuclide]->photon_energy(); + auto [energy, weight] = dist->sample(seed); + return {energy, weight, parent_nuclide}; +} + +std::pair DecaySpectrum::sample(uint64_t* seed) const +{ + auto sample = sample_with_parent(seed); + return {sample.energy, sample.weight}; +} + +double DecaySpectrum::integral() const +{ + return integral_; +} + +double DecaySpectrum::sample_unbiased(uint64_t* seed) const +{ + return sample_with_parent(seed).energy; +} + } // namespace openmc diff --git a/src/distribution_angle.cpp b/src/distribution_angle.cpp index 5e92b794a..ecb5961f6 100644 --- a/src/distribution_angle.cpp +++ b/src/distribution_angle.cpp @@ -2,11 +2,11 @@ #include // for abs, copysign -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" #include "openmc/vector.h" // for vector @@ -29,7 +29,7 @@ AngleDistribution::AngleDistribution(hid_t group) hid_t dset = open_dataset(group, "mu"); read_attribute(dset, "offsets", offsets); read_attribute(dset, "interpolation", interp); - xt::xarray temp; + tensor::Tensor temp; read_dataset(dset, temp); close_dataset(dset); @@ -40,13 +40,13 @@ AngleDistribution::AngleDistribution(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = temp.shape()[1] - j; + n = temp.shape(1) - j; } // Create and initialize tabular distribution - auto xs = xt::view(temp, 0, xt::range(j, j + n)); - auto ps = xt::view(temp, 1, xt::range(j, j + n)); - auto cs = xt::view(temp, 2, xt::range(j, j + n)); + tensor::View xs = temp.slice(0, tensor::range(j, j + n)); + tensor::View ps = temp.slice(1, tensor::range(j, j + n)); + tensor::View cs = temp.slice(2, tensor::range(j, j + n)); vector x {xs.begin(), xs.end()}; vector p {ps.begin(), ps.end()}; vector c {cs.begin(), cs.end()}; @@ -64,30 +64,17 @@ AngleDistribution::AngleDistribution(hid_t group) double AngleDistribution::sample(double E, uint64_t* seed) const { - // Determine number of incoming energies - auto n = energy_.size(); - - // Find energy bin and calculate interpolation factor -- if the energy is - // outside the range of the tabulated energies, choose the first or last bins + // Find energy bin and calculate interpolation factor int i; double r; - if (E < energy_[0]) { - i = 0; - r = 0.0; - } else if (E > energy_[n - 1]) { - i = n - 2; - r = 1.0; - } else { - i = lower_bound_index(energy_.begin(), energy_.end(), E); - r = (E - energy_[i]) / (energy_[i + 1] - energy_[i]); - } + get_energy_index(energy_, E, i, r); // Sample between the ith and (i+1)th bin if (r > prn(seed)) ++i; // Sample i-th distribution - double mu = distribution_[i]->sample(seed); + double mu = distribution_[i]->sample(seed).first; // Make sure mu is in range [-1,1] and return if (std::abs(mu) > 1.0) @@ -95,4 +82,19 @@ double AngleDistribution::sample(double E, uint64_t* seed) const return mu; } +double AngleDistribution::evaluate(double E, double mu) const +{ + // Find energy bin and calculate interpolation factor + int i; + double r; + get_energy_index(energy_, E, i, r); + + double pdf = 0.0; + if (r > 0.0) + pdf += r * distribution_[i + 1]->evaluate(mu); + if (r < 1.0) + pdf += (1.0 - r) * distribution_[i]->evaluate(mu); + return pdf; +} + } // namespace openmc diff --git a/src/distribution_energy.cpp b/src/distribution_energy.cpp index a4a5ce9e1..2f8e6cf1a 100644 --- a/src/distribution_energy.cpp +++ b/src/distribution_energy.cpp @@ -4,7 +4,7 @@ #include // for size_t #include // for back_inserter -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" @@ -60,11 +60,11 @@ ContinuousTabular::ContinuousTabular(hid_t group) hid_t dset = open_dataset(group, "energy"); // Get interpolation parameters - xt::xarray temp; + tensor::Tensor temp; read_attribute(dset, "interpolation", temp); - auto temp_b = xt::view(temp, 0); // view of breakpoints - auto temp_i = xt::view(temp, 1); // view of interpolation parameters + tensor::View temp_b = temp.slice(0); // breakpoints + tensor::View temp_i = temp.slice(1); // interpolation parameters std::copy(temp_b.begin(), temp_b.end(), std::back_inserter(breakpoints_)); for (const auto i : temp_i) @@ -85,7 +85,7 @@ ContinuousTabular::ContinuousTabular(hid_t group) read_attribute(dset, "interpolation", interp); read_attribute(dset, "n_discrete_lines", n_discrete); - xt::xarray eout; + tensor::Tensor eout; read_dataset(dset, eout); close_dataset(dset); @@ -96,7 +96,7 @@ ContinuousTabular::ContinuousTabular(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = eout.shape()[1] - j; + n = eout.shape(1) - j; } // Assign interpolation scheme and number of discrete lines @@ -105,15 +105,15 @@ ContinuousTabular::ContinuousTabular(hid_t group) d.n_discrete = n_discrete[i]; // Copy data - d.e_out = xt::view(eout, 0, xt::range(j, j + n)); - d.p = xt::view(eout, 1, xt::range(j, j + n)); + d.e_out = eout.slice(0, tensor::range(j, j + n)); + d.p = eout.slice(1, tensor::range(j, j + n)); // To get answers that match ACE data, for now we still use the tabulated // CDF values that were passed through to the HDF5 library. At a later // time, we can remove the CDF values from the HDF5 library and // reconstruct them using the PDF if (true) { - d.c = xt::view(eout, 2, xt::range(j, j + n)); + d.c = eout.slice(2, tensor::range(j, j + n)); } else { // Calculate cumulative distribution function -- discrete portion for (int k = 0; k < d.n_discrete; ++k) { diff --git a/src/distribution_multi.cpp b/src/distribution_multi.cpp index cdb33adc2..47785649b 100644 --- a/src/distribution_multi.cpp +++ b/src/distribution_multi.cpp @@ -1,6 +1,6 @@ #include "openmc/distribution_multi.h" -#include // for move +#include // for move, clamp #include // for sqrt, sin, cos, max #include "openmc/constants.h" @@ -20,7 +20,7 @@ unique_ptr UnitSphereDistribution::create( if (check_for_node(node, "type")) type = get_node_value(node, "type", true, true); if (type == "isotropic") { - return UPtrAngle {new Isotropic()}; + return UPtrAngle {new Isotropic(node)}; } else if (type == "monodirectional") { return UPtrAngle {new Monodirectional(node)}; } else if (type == "mu-phi") { @@ -44,6 +44,7 @@ UnitSphereDistribution::UnitSphereDistribution(pugi::xml_node node) fatal_error("Angular distribution reference direction must have " "three parameters specified."); u_ref_ = Direction(u_ref.data()); + u_ref_ /= u_ref_.norm(); } } @@ -65,6 +66,7 @@ PolarAzimuthal::PolarAzimuthal(pugi::xml_node node) fatal_error("Angular distribution reference v direction must have " "three parameters specified."); v_ref_ = Direction(v_ref.data()); + v_ref_ /= v_ref_.norm(); } w_ref_ = u_ref_.cross(v_ref_); if (check_for_node(node, "mu")) { @@ -82,27 +84,79 @@ PolarAzimuthal::PolarAzimuthal(pugi::xml_node node) } } -Direction PolarAzimuthal::sample(uint64_t* seed) const +std::pair PolarAzimuthal::sample(uint64_t* seed) const +{ + return sample_impl(seed, false); +} + +std::pair PolarAzimuthal::sample_as_bias( + uint64_t* seed) const +{ + return sample_impl(seed, true); +} + +std::pair PolarAzimuthal::sample_impl( + uint64_t* seed, bool return_pdf) const { // Sample cosine of polar angle - double mu = mu_->sample(seed); - if (mu == 1.0) - return u_ref_; - if (mu == -1.0) - return -u_ref_; + auto [mu, mu_wgt] = mu_->sample(seed); // Sample azimuthal angle - double phi = phi_->sample(seed); + auto [phi, phi_wgt] = phi_->sample(seed); + + // Compute either the PDF value or the importance weight + double weight = + return_pdf ? (mu_->evaluate(mu) * phi_->evaluate(phi)) : (mu_wgt * phi_wgt); + + if (mu == 1.0) + return {u_ref_, weight}; + if (mu == -1.0) + return {-u_ref_, weight}; double f = std::sqrt(1 - mu * mu); + return {mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_, + weight}; +} - return mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_; +double PolarAzimuthal::evaluate(Direction u) const +{ + double mu = std::clamp(u.dot(u_ref_), -1.0, 1.0); + double phi = 0.0; + double sin_theta_sq = std::max(0.0, 1.0 - mu * mu); + if (sin_theta_sq > 0.0) { + double sin_theta = std::sqrt(sin_theta_sq); + double cos_phi = u.dot(v_ref_) / sin_theta; + double sin_phi = u.dot(w_ref_) / sin_theta; + phi = std::atan2(sin_phi, cos_phi); + if (phi < 0.0) + phi += 2.0 * PI; + } + return mu_->evaluate(mu) * phi_->evaluate(phi); } //============================================================================== // Isotropic implementation //============================================================================== +Isotropic::Isotropic(pugi::xml_node node) : UnitSphereDistribution {node} +{ + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + std::string bias_type = get_node_value(bias_node, "type", true, true); + if (bias_type != "mu-phi") { + openmc::fatal_error( + "Isotropic distributions may only be biased by a PolarAzimuthal."); + } + auto bias = std::make_unique(bias_node); + if (bias->mu()->bias() || bias->phi()->bias()) { + openmc::fatal_error( + "Attempted to bias Isotropic distribution with a biased PolarAzimuthal " + "distribution. Please ensure bias distributions are unbiased."); + } + this->set_bias(std::move(bias)); + } +} + Direction isotropic_direction(uint64_t* seed) { double phi = uniform_distribution(0., 2.0 * PI, seed); @@ -111,18 +165,28 @@ Direction isotropic_direction(uint64_t* seed) std::sqrt(1.0 - mu * mu) * std::sin(phi)}; } -Direction Isotropic::sample(uint64_t* seed) const +std::pair Isotropic::sample(uint64_t* seed) const { - return isotropic_direction(seed); + if (bias()) { + auto [val, eval] = bias()->sample_as_bias(seed); + return {val, 1.0 / (4.0 * PI * eval)}; + } else { + return {isotropic_direction(seed), 1.0}; + } +} + +double Isotropic::evaluate(Direction u) const +{ + return 1.0 / (4.0 * PI); } //============================================================================== // Monodirectional implementation //============================================================================== -Direction Monodirectional::sample(uint64_t* seed) const +std::pair Monodirectional::sample(uint64_t* seed) const { - return u_ref_; + return {u_ref_, 1.0}; } } // namespace openmc diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index d2b0f413b..5a3452422 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -80,9 +80,13 @@ CartesianIndependent::CartesianIndependent(pugi::xml_node node) } } -Position CartesianIndependent::sample(uint64_t* seed) const +std::pair CartesianIndependent::sample(uint64_t* seed) const { - return {x_->sample(seed), y_->sample(seed), z_->sample(seed)}; + auto [x_val, x_wgt] = x_->sample(seed); + auto [y_val, y_wgt] = y_->sample(seed); + auto [z_val, z_wgt] = z_->sample(seed); + Position xi {x_val, y_val, z_val}; + return {xi, x_wgt * y_wgt * z_wgt}; } //============================================================================== @@ -137,16 +141,51 @@ CylindricalIndependent::CylindricalIndependent(pugi::xml_node node) // If no coordinates were specified, default to (0, 0, 0) origin_ = {0.0, 0.0, 0.0}; } + + // Read cylinder z_dir + if (check_for_node(node, "z_dir")) { + auto z_dir = get_node_array(node, "z_dir"); + if (z_dir.size() == 3) { + z_dir_ = z_dir; + z_dir_ /= z_dir_.norm(); + } else { + fatal_error("z_dir for cylindrical source distribution must be length 3"); + } + } else { + // If no z_dir was specified, default to (0, 0, 1) + z_dir_ = {0.0, 0.0, 1.0}; + } + + // Read cylinder r_dir + if (check_for_node(node, "r_dir")) { + auto r_dir = get_node_array(node, "r_dir"); + if (r_dir.size() == 3) { + r_dir_ = r_dir; + r_dir_ /= r_dir_.norm(); + } else { + fatal_error("r_dir for cylindrical source distribution must be length 3"); + } + } else { + // If no r_dir was specified, default to (1, 0, 0) + r_dir_ = {1.0, 0.0, 0.0}; + } + + if (r_dir_.dot(z_dir_) > 1e-12) + fatal_error("r_dir must be perpendicular to z_dir"); + + auto phi_dir = z_dir_.cross(r_dir_); + phi_dir /= phi_dir.norm(); + phi_dir_ = phi_dir; } -Position CylindricalIndependent::sample(uint64_t* seed) const +std::pair CylindricalIndependent::sample(uint64_t* seed) const { - double r = r_->sample(seed); - double phi = phi_->sample(seed); - double x = r * cos(phi) + origin_.x; - double y = r * sin(phi) + origin_.y; - double z = z_->sample(seed) + origin_.z; - return {x, y, z}; + auto [r, r_wgt] = r_->sample(seed); + auto [phi, phi_wgt] = phi_->sample(seed); + auto [z, z_wgt] = z_->sample(seed); + Position xi = + r * (cos(phi) * r_dir_ + sin(phi) * phi_dir_) + z * z_dir_ + origin_; + return {xi, r_wgt * phi_wgt * z_wgt}; } //============================================================================== @@ -203,16 +242,17 @@ SphericalIndependent::SphericalIndependent(pugi::xml_node node) } } -Position SphericalIndependent::sample(uint64_t* seed) const +std::pair SphericalIndependent::sample(uint64_t* seed) const { - double r = r_->sample(seed); - double cos_theta = cos_theta_->sample(seed); - double phi = phi_->sample(seed); + auto [r, r_wgt] = r_->sample(seed); + auto [cos_theta, cos_theta_wgt] = cos_theta_->sample(seed); + auto [phi, phi_wgt] = phi_->sample(seed); // sin(theta) by sin**2 + cos**2 = 1 double x = r * std::sqrt(1 - cos_theta * cos_theta) * cos(phi) + origin_.x; double y = r * std::sqrt(1 - cos_theta * cos_theta) * sin(phi) + origin_.y; double z = r * cos_theta + origin_.z; - return {x, y, z}; + Position xi {x, y, z}; + return {xi, r_wgt * cos_theta_wgt * phi_wgt}; } //============================================================================== @@ -222,9 +262,11 @@ Position SphericalIndependent::sample(uint64_t* seed) const MeshSpatial::MeshSpatial(pugi::xml_node node) { - if (get_node_value(node, "type", true, true) != "mesh") { - fatal_error(fmt::format( - "Incorrect spatial type '{}' for a MeshSpatial distribution")); + auto spatial_type = get_node_value(node, "type", true, true); + if (spatial_type != "mesh") { + fatal_error( + fmt::format("Incorrect spatial type '{}' for a MeshSpatial distribution", + spatial_type)); } // No in-tet distributions implemented, could include distributions for the @@ -260,6 +302,37 @@ MeshSpatial::MeshSpatial(pugi::xml_node node) } elem_idx_dist_.assign(strengths); + + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + + if (check_for_node(bias_node, "strengths")) { + std::vector bias_strengths(n_bins, 1.0); + bias_strengths = get_node_array(node, "strengths"); + + if (bias_strengths.size() != n_bins) { + fatal_error( + fmt::format("Number of entries in the bias strengths array {} does " + "not match the number of entities in mesh {} ({}).", + bias_strengths.size(), mesh_id, n_bins)); + } + + if (get_node_value_bool(node, "volume_normalized")) { + for (int i = 0; i < n_bins; i++) { + bias_strengths[i] *= this->mesh()->volume(i); + } + } + + // Compute importance weights + weight_ = compute_importance_weights(strengths, bias_strengths); + + // Re-initialize DiscreteIndex with bias strengths for sampling + elem_idx_dist_.assign(bias_strengths); + } else { + fatal_error(fmt::format( + "Bias node for mesh {} found without strengths array.", mesh_id)); + } + } } MeshSpatial::MeshSpatial(int32_t mesh_idx, span strengths) @@ -295,9 +368,11 @@ std::pair MeshSpatial::sample_mesh(uint64_t* seed) const return {elem_idx, mesh()->sample_element(elem_idx, seed)}; } -Position MeshSpatial::sample(uint64_t* seed) const +std::pair MeshSpatial::sample(uint64_t* seed) const { - return this->sample_mesh(seed).second; + auto [elem_idx, u] = this->sample_mesh(seed); + double wgt = weight_.empty() ? 1.0 : weight_[elem_idx]; + return {u, wgt}; } //============================================================================== @@ -328,6 +403,31 @@ PointCloud::PointCloud(pugi::xml_node node) } point_idx_dist_.assign(strengths); + + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + + if (check_for_node(bias_node, "strengths")) { + std::vector bias_strengths(point_cloud_.size(), 1.0); + bias_strengths = get_node_array(node, "strengths"); + + if (bias_strengths.size() != point_cloud_.size()) { + fatal_error( + fmt::format("Number of entries in the bias strengths array {} does " + "not match the number of spatial points provided {}.", + bias_strengths.size(), point_cloud_.size())); + } + + // Compute importance weights + weight_ = compute_importance_weights(strengths, bias_strengths); + + // Re-initialize DiscreteIndex with bias strengths for sampling + point_idx_dist_.assign(bias_strengths); + } else { + fatal_error( + fmt::format("Bias node for PointCloud found without strengths array.")); + } + } } PointCloud::PointCloud( @@ -337,10 +437,11 @@ PointCloud::PointCloud( point_idx_dist_.assign(strengths); } -Position PointCloud::sample(uint64_t* seed) const +std::pair PointCloud::sample(uint64_t* seed) const { int32_t index = point_idx_dist_.sample(seed); - return point_cloud_[index]; + double wgt = weight_.empty() ? 1.0 : weight_[index]; + return {point_cloud_[index], wgt}; } //============================================================================== @@ -360,10 +461,15 @@ SpatialBox::SpatialBox(pugi::xml_node node, bool fission) upper_right_ = Position {params[3], params[4], params[5]}; } -Position SpatialBox::sample(uint64_t* seed) const +SpatialBox::SpatialBox(Position lower_left, Position upper_right, bool fission) + : lower_left_(lower_left), upper_right_(upper_right), + only_fissionable_(fission) +{} + +std::pair SpatialBox::sample(uint64_t* seed) const { Position xi {prn(seed), prn(seed), prn(seed)}; - return lower_left_ + xi * (upper_right_ - lower_left_); + return {lower_left_ + xi * (upper_right_ - lower_left_), 1.0}; } //============================================================================== @@ -382,9 +488,9 @@ SpatialPoint::SpatialPoint(pugi::xml_node node) r_ = Position {params.data()}; } -Position SpatialPoint::sample(uint64_t* seed) const +std::pair SpatialPoint::sample(uint64_t* seed) const { - return r_; + return {r_, 1.0}; } } // namespace openmc diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index a2120a006..bc8dcc9ea 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -1,9 +1,6 @@ #include "openmc/eigenvalue.h" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xtensor.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/array.h" #include "openmc/bank.h" @@ -39,7 +36,7 @@ namespace simulation { double keff_generation; array k_sum; vector entropy; -xt::xtensor source_frac; +tensor::Tensor source_frac; } // namespace simulation @@ -192,9 +189,9 @@ void synchronize_bank() // TODO: protect for MPI_Exscan at rank 0 // Allocate space for bank_position if this hasn't been done yet - int64_t bank_position[mpi::n_procs]; - MPI_Allgather( - &start, 1, MPI_INT64_T, bank_position, 1, MPI_INT64_T, mpi::intracomm); + std::vector bank_position(mpi::n_procs); + MPI_Allgather(&start, 1, MPI_INT64_T, bank_position.data(), 1, MPI_INT64_T, + mpi::intracomm); #else start = 0; finish = index_temp; @@ -248,9 +245,12 @@ void synchronize_bank() if (settings::ifp_on) { // Send IFP data - send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, - temp_delayed_groups, send_delayed_groups, temp_lifetimes, - send_lifetimes); + if (is_beta_effective_or_both()) + send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, + temp_delayed_groups, send_delayed_groups); + if (is_generation_time_or_both()) + send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, + temp_lifetimes, send_lifetimes); } } @@ -286,7 +286,7 @@ void synchronize_bank() neighbor = mpi::n_procs - 1; } else { neighbor = - upper_bound_index(bank_position, bank_position + mpi::n_procs, start); + upper_bound_index(bank_position.begin(), bank_position.end(), start); } // Resize IFP receive buffers @@ -316,8 +316,12 @@ void synchronize_bank() if (settings::ifp_on) { // Receive IFP data - receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, - recv_delayed_groups, recv_lifetimes, deserialization_info); + if (is_beta_effective_or_both()) + receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, + recv_delayed_groups, deserialization_info); + if (is_generation_time_or_both()) + receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, + recv_lifetimes, deserialization_info); } } else { @@ -348,8 +352,12 @@ void synchronize_bank() MPI_Waitall(n_request, requests.data(), MPI_STATUSES_IGNORE); if (settings::ifp_on) { - deserialize_ifp_info(ifp_n_generation, deserialization_info, - recv_delayed_groups, recv_lifetimes); + if (is_beta_effective_or_both()) + deserialize_ifp_info(ifp_n_generation, recv_delayed_groups, + simulation::ifp_source_delayed_group_bank, deserialization_info); + if (is_generation_time_or_both()) + deserialize_ifp_info(ifp_n_generation, recv_lifetimes, + simulation::ifp_source_lifetime_bank, deserialization_info); } #else @@ -403,6 +411,16 @@ void calculate_average_keff() t_value * std::sqrt( (simulation::k_sum[1] / n - std::pow(simulation::keff, 2)) / (n - 1)); + + // In some cases (such as an infinite medium problem), random ray + // may estimate k exactly and in an unvarying manner between iterations. + // In this case, the floating point roundoff between the division and the + // power operations may cause an extremely small negative value to occur + // inside the sqrt operation, leading to NaN. If this occurs, we check for + // it and set the std dev to zero. + if (!std::isfinite(simulation::keff_std)) { + simulation::keff_std = 0.0; + } } } } @@ -431,7 +449,7 @@ int openmc_get_keff(double* k_combined) const auto& gt = simulation::global_tallies; array kv {}; - xt::xtensor cov = xt::zeros({3, 3}); + tensor::Tensor cov = tensor::zeros({3, 3}); kv[0] = gt(GlobalTally::K_COLLISION, TallyResult::SUM) / n; kv[1] = gt(GlobalTally::K_ABSORPTION, TallyResult::SUM) / n; kv[2] = gt(GlobalTally::K_TRACKLENGTH, TallyResult::SUM) / n; @@ -570,7 +588,7 @@ void shannon_entropy() { // Get source weight in each mesh bin bool sites_outside; - xt::xtensor p = + tensor::Tensor p = simulation::entropy_mesh->count_sites(simulation::fission_bank.data(), simulation::fission_bank.size(), &sites_outside); @@ -582,7 +600,7 @@ void shannon_entropy() if (mpi::master) { // Normalize to total weight of bank sites - p /= xt::sum(p); + p /= p.sum(); // Sum values to obtain Shannon entropy double H = 0.0; @@ -606,7 +624,7 @@ void ufs_count_sites() std::size_t n = simulation::ufs_mesh->n_bins(); double vol_frac = simulation::ufs_mesh->volume_frac_; - simulation::source_frac = xt::xtensor({n}, vol_frac); + simulation::source_frac = tensor::Tensor({n}, vol_frac); } else { // count number of source sites in each ufs mesh cell @@ -628,7 +646,7 @@ void ufs_count_sites() #endif // Normalize to total weight to get fraction of source in each cell - double total = xt::sum(simulation::source_frac)(); + double total = simulation::source_frac.sum(); simulation::source_frac /= total; // Since the total starting weight is not equal to n_particles, we need to diff --git a/src/endf.cpp b/src/endf.cpp index c0c1d2e7e..8a9c5e48a 100644 --- a/src/endf.cpp +++ b/src/endf.cpp @@ -5,8 +5,7 @@ #include // for back_inserter #include // for runtime_error -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/array.h" #include "openmc/constants.h" @@ -88,6 +87,83 @@ bool is_inelastic_scatter(int mt) } } +bool mt_matches(int event_mt, int target_mt) +{ + // Direct match + if (event_mt == target_mt) + return true; + + // Check if event_mt is a component of target_mt summation reaction + switch (target_mt) { + case TOTAL_XS: + return event_mt == ELASTIC || mt_matches(event_mt, N_NONELASTIC); + + case N_NONELASTIC: { + static constexpr int components[] = {4, 5, 11, 16, 17, 22, 23, 24, 25, 27, + 28, 29, 30, 32, 33, 34, 35, 36, 37, 41, 42, 44, 45, 152, 153, 154, 156, + 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, + 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 183, 184, 185, 186, 187, + 188, 189, 190, 194, 195, 196, 198, 199, 200}; + for (int mt : components) { + if (mt_matches(event_mt, mt)) + return true; + } + return false; + } + + case N_LEVEL: + // Inelastic scattering levels + return event_mt >= 50 && event_mt <= N_NC; + + case N_2N: + // (n,2n) to excited states + return event_mt >= N_2N0 && event_mt <= N_2NC; + + case N_FISSION: + return is_fission(event_mt); + + case 27: + return is_fission(event_mt) || is_disappearance(event_mt); + + case N_DISAPPEAR: { + return is_disappearance(event_mt); + } + + case N_P: + // (n,p) to excited states + return event_mt >= N_P0 && event_mt <= N_PC; + + case N_D: + // (n,d) to excited states + return event_mt >= N_D0 && event_mt <= N_DC; + + case N_T: + // (n,t) to excited states + return event_mt >= N_T0 && event_mt <= N_TC; + + case N_3HE: + // (n,3He) to excited states + return event_mt >= N_3HE0 && event_mt <= N_3HEC; + + case N_A: + // (n,alpha) to excited states + return event_mt >= N_A0 && event_mt <= N_AC; + + case 501: + return event_mt == 502 || event_mt == 504 || mt_matches(event_mt, 516) || + mt_matches(event_mt, 522); + + case PAIR_PROD: + return event_mt == PAIR_PROD_ELEC || event_mt == PAIR_PROD_NUC; + + case PHOTOELECTRIC: + return event_mt >= 534 && event_mt < 573; + + default: + return false; + } +} + unique_ptr read_function(hid_t group, const char* name) { hid_t obj_id = open_object(group, name); @@ -153,11 +229,11 @@ Tabulated1D::Tabulated1D(hid_t dset) for (const auto i : int_temp) int_.push_back(int2interp(i)); - xt::xarray arr; + tensor::Tensor arr; read_dataset(dset, arr); - auto xs = xt::view(arr, 0); - auto ys = xt::view(arr, 1); + tensor::View xs = arr.slice(0); + tensor::View ys = arr.slice(1); std::copy(xs.begin(), xs.end(), std::back_inserter(x_)); std::copy(ys.begin(), ys.end(), std::back_inserter(y_)); @@ -229,12 +305,12 @@ double Tabulated1D::operator()(double x) const CoherentElasticXS::CoherentElasticXS(hid_t dset) { // Read 2D array from dataset - xt::xarray arr; + tensor::Tensor arr; read_dataset(dset, arr); // Get views for Bragg edges and structure factors - auto E = xt::view(arr, 0); - auto s = xt::view(arr, 1); + tensor::View E = arr.slice(0); + tensor::View s = arr.slice(1); // Copy Bragg edges and partial sums of structure factors std::copy(E.begin(), E.end(), std::back_inserter(bragg_edges_)); diff --git a/src/event.cpp b/src/event.cpp index f33e132d0..2d436bb9d 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,6 +1,9 @@ #include "openmc/event.h" +#include "openmc/bank.h" +#include "openmc/error.h" #include "openmc/material.h" +#include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/timer.h" @@ -64,7 +67,8 @@ void process_init_events(int64_t n_particles, int64_t source_offset) simulation::time_event_init.start(); #pragma omp parallel for schedule(runtime) for (int64_t i = 0; i < n_particles; i++) { - initialize_history(simulation::particles[i], source_offset + i + 1); + initialize_particle_track( + simulation::particles[i], source_offset + i + 1, false); dispatch_xs_event(i); } simulation::time_event_init.stop(); @@ -136,7 +140,7 @@ void process_surface_crossing_events() int64_t buffer_idx = simulation::surface_crossing_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; p.event_cross_surface(); - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); if (p.alive()) dispatch_xs_event(buffer_idx); } @@ -155,7 +159,7 @@ void process_collision_events() int64_t buffer_idx = simulation::collision_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; p.event_collide(); - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); if (p.alive()) dispatch_xs_event(buffer_idx); } @@ -176,4 +180,45 @@ void process_death_events(int64_t n_particles) simulation::time_event_death.stop(); } +void process_transport_events() +{ + while (true) { + int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), + simulation::calculate_nonfuel_xs_queue.size(), + simulation::advance_particle_queue.size(), + simulation::surface_crossing_queue.size(), + simulation::collision_queue.size()}); + + if (max == 0) { + break; + } else if (max == simulation::calculate_fuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_fuel_xs_queue); + } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); + } else if (max == simulation::advance_particle_queue.size()) { + process_advance_particle_events(); + } else if (max == simulation::surface_crossing_queue.size()) { + process_surface_crossing_events(); + } else if (max == simulation::collision_queue.size()) { + process_collision_events(); + } + } +} + +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank) +{ + simulation::time_event_init.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + initialize_particle_track(simulation::particles[i], offset + i + 1, true); + const SourceSite& site = shared_secondary_bank[offset + i]; + simulation::particles[i].event_revive_from_secondary(site); + if (simulation::particles[i].alive()) { + dispatch_xs_event(i); + } + } + simulation::time_event_init.stop(); +} + } // namespace openmc diff --git a/src/external/quartic_solver.cpp b/src/external/quartic_solver.cpp index 915020ffa..71bdb25c3 100644 --- a/src/external/quartic_solver.cpp +++ b/src/external/quartic_solver.cpp @@ -1,5 +1,5 @@ -#include #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include #include #include #include diff --git a/src/finalize.cpp b/src/finalize.cpp index 9ee943409..fd891d9dd 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -2,6 +2,7 @@ #include "openmc/bank.h" #include "openmc/capi.h" +#include "openmc/chain.h" #include "openmc/cmfd_solver.h" #include "openmc/collision_track.h" #include "openmc/constants.h" @@ -14,6 +15,7 @@ #include "openmc/material.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" #include "openmc/photon.h" #include "openmc/plot.h" @@ -29,7 +31,7 @@ #include "openmc/volume_calc.h" #include "openmc/weight_windows.h" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" namespace openmc { @@ -43,6 +45,7 @@ void free_memory() free_memory_photon(); free_memory_settings(); free_memory_thermal(); + free_memory_chain(); library_clear(); nuclides_clear(); free_memory_source(); @@ -122,6 +125,9 @@ int openmc_finalize() settings::restart_run = false; settings::run_CE = true; settings::run_mode = RunMode::UNSET; + settings::surface_grazing_cutoff = 0.001; + settings::surface_grazing_ratio = 0.5; + settings::solver_type = SolverType::MONTE_CARLO; settings::source_latest = false; settings::source_rejection_fraction = 0.05; settings::source_separate = false; @@ -136,13 +142,16 @@ int openmc_finalize() settings::temperature_multipole = false; settings::temperature_range = {0.0, 0.0}; settings::temperature_tolerance = 10.0; + settings::properties_file.clear(); settings::trigger_on = false; settings::trigger_predict = false; settings::trigger_batch_interval = 1; settings::uniform_source_sampling = false; settings::ufs_on = false; settings::urr_ptables_on = true; - settings::verbosity = 7; + settings::use_decay_photons = false; + settings::use_shared_secondary_bank = false; + settings::verbosity = -1; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; settings::weight_windows_file.clear(); @@ -162,6 +171,7 @@ int openmc_finalize() data::energy_min = {0.0, 0.0, 0.0, 0.0}; data::temperature_min = 0.0; data::temperature_max = INFTY; + data::mg = {}; model::root_universe = -1; model::plotter_seed = 1; openmc::openmc_set_seed(DEFAULT_SEED); @@ -184,7 +194,7 @@ int openmc_finalize() } #endif - openmc_reset_random_ray(); + openmc_finalize_random_ray(); return 0; } @@ -200,7 +210,7 @@ int openmc_reset() // Reset global tallies simulation::n_realizations = 0; - xt::view(simulation::global_tallies, xt::all()) = 0.0; + simulation::global_tallies.fill(0.0); simulation::k_col_abs = 0.0; simulation::k_col_tra = 0.0; @@ -211,6 +221,7 @@ int openmc_reset() settings::cmfd_run = false; simulation::n_lost_particles = 0; + simulation::simulation_tracks_completed = 0; return 0; } diff --git a/src/geometry.cpp b/src/geometry.cpp index 5ff15097b..bf654f4f9 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -26,16 +26,22 @@ int n_coord_levels; vector overlap_check_count; +vector overlap_keys; +std::unordered_map overlap_key_index; + } // namespace model //============================================================================== // Non-member functions //============================================================================== -bool check_cell_overlap(GeometryState& p, bool error) +int check_cell_overlap(GeometryState& p, bool error) { int n_coord = p.n_coord(); + // If no overlap found, return a nonphysical index + int overlap_index = -1; + // Loop through each coordinate level for (int j = 0; j < n_coord; j++) { Universe& univ = *model::universes[p.coord(j).universe()]; @@ -44,21 +50,40 @@ bool check_cell_overlap(GeometryState& p, bool error) for (auto index_cell : univ.cells_) { Cell& c = *model::cells[index_cell]; if (c.contains(p.coord(j).r(), p.coord(j).u(), p.surface())) { +#pragma omp atomic + ++model::overlap_check_count[index_cell]; if (index_cell != p.coord(j).cell()) { if (error) { fatal_error( fmt::format("Overlapping cells detected: {}, {} on universe {}", c.id_, model::cells[p.coord(j).cell()]->id_, univ.id_)); } - return true; + + // With no fatal error (plotter is calling), now adds overlaps and + // ensures order does not matter when making overlap key + int cell_a = model::cells[index_cell]->id_; + int cell_b = model::cells[p.coord(j).cell()]->id_; + int a = std::min(cell_a, cell_b); + int b = std::max(cell_a, cell_b); + OverlapKey key {univ.id_, a, b}; +#pragma omp critical(overlap_key_update) + { + auto it = model::overlap_key_index.find(key); + if (it != model::overlap_key_index.end()) { + overlap_index = it->second; // already exists, reuse index + } else { + int idx = int(model::overlap_keys.size()); + model::overlap_keys.push_back(key); + model::overlap_key_index[key] = idx; + overlap_index = idx; + } + } + break; } -#pragma omp atomic - ++model::overlap_check_count[index_cell]; } } } - - return false; + return overlap_index; } //============================================================================== @@ -480,14 +505,14 @@ extern "C" int openmc_global_bounding_box(double* llc, double* urc) auto bbox = model::universes.at(model::root_universe)->bounding_box(); // set lower left corner values - llc[0] = bbox.xmin; - llc[1] = bbox.ymin; - llc[2] = bbox.zmin; + llc[0] = bbox.min.x; + llc[1] = bbox.min.y; + llc[2] = bbox.min.z; // set upper right corner values - urc[0] = bbox.xmax; - urc[1] = bbox.ymax; - urc[2] = bbox.zmax; + urc[0] = bbox.max.x; + urc[1] = bbox.max.y; + urc[2] = bbox.max.z; return 0; } diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 8a145fb1f..a740740c1 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -55,8 +55,13 @@ void read_geometry_xml() void read_geometry_xml(pugi::xml_node root) { // Read surfaces, cells, lattice - read_surfaces(root); + std::set> periodic_pairs; + std::unordered_map albedo_map; + std::unordered_map periodic_sense_map; + + read_surfaces(root, periodic_pairs, albedo_map, periodic_sense_map); read_cells(root); + prepare_boundary_conditions(periodic_pairs, albedo_map, periodic_sense_map); read_lattices(root); // Check to make sure a boundary condition was applied to at least one diff --git a/src/hdf5_interface.cpp b/src/hdf5_interface.cpp index c56d485e2..00c6a4399 100644 --- a/src/hdf5_interface.cpp +++ b/src/hdf5_interface.cpp @@ -4,8 +4,7 @@ #include #include -#include "xtensor/xarray.hpp" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include "hdf5.h" @@ -466,22 +465,19 @@ void read_dataset_lowlevel(hid_t obj_id, const char* name, hid_t mem_type_id, } template<> -void read_dataset(hid_t dset, xt::xarray>& arr, bool indep) +void read_dataset( + hid_t dset, tensor::Tensor>& tensor, bool indep) { // Get shape of dataset vector shape = object_shape(dset); - // Allocate new array to read data into - std::size_t size = 1; - for (const auto x : shape) - size *= x; - vector> buffer(size); + // Resize tensor and read data directly + vector tshape(shape.begin(), shape.end()); + tensor.resize(tshape); - // Read data from attribute - read_complex(dset, nullptr, buffer.data(), indep); - - // Adapt into xarray - arr = xt::adapt(buffer, shape); + // Read data from dataset + read_complex(dset, nullptr, + reinterpret_cast*>(tensor.data()), indep); } void read_double(hid_t obj_id, const char* name, double* buffer, bool indep) diff --git a/src/ifp.cpp b/src/ifp.cpp index cc4a76538..f0f98b8ef 100644 --- a/src/ifp.cpp +++ b/src/ifp.cpp @@ -32,13 +32,13 @@ void ifp(const Particle& p, int64_t idx) { if (is_beta_effective_or_both()) { const auto& delayed_groups = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_source_delayed_group_bank[p.current_work()]; simulation::ifp_fission_delayed_group_bank[idx] = _ifp(p.delayed_group(), delayed_groups); } if (is_generation_time_or_both()) { const auto& lifetimes = - simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_source_lifetime_bank[p.current_work()]; simulation::ifp_fission_lifetime_bank[idx] = _ifp(p.lifetime(), lifetimes); } } @@ -63,7 +63,6 @@ void copy_ifp_data_from_fission_banks( } #ifdef OPENMC_MPI - void broadcast_ifp_n_generation(int& n_generation, const vector>& delayed_groups, const vector>& lifetimes) @@ -78,61 +77,6 @@ void broadcast_ifp_n_generation(int& n_generation, MPI_Bcast(&n_generation, 1, MPI_INT, 0, mpi::intracomm); } -void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, const vector>& delayed_groups, - vector& send_delayed_groups, const vector>& lifetimes, - vector& send_lifetimes) -{ - // Copy data in send buffers - for (int i = idx; i < idx + n; i++) { - if (is_beta_effective_or_both()) { - std::copy(delayed_groups[i].begin(), delayed_groups[i].end(), - send_delayed_groups.begin() + i * n_generation); - } - if (is_generation_time_or_both()) { - std::copy(lifetimes[i].begin(), lifetimes[i].end(), - send_lifetimes.begin() + i * n_generation); - } - } - // Send delayed groups - if (is_beta_effective_or_both()) { - requests.emplace_back(); - MPI_Isend(&send_delayed_groups[n_generation * idx], - n_generation * static_cast(n), MPI_INT, neighbor, mpi::rank, - mpi::intracomm, &requests.back()); - } - // Send lifetimes - if (is_generation_time_or_both()) { - requests.emplace_back(); - MPI_Isend(&send_lifetimes[n_generation * idx], - n_generation * static_cast(n), MPI_DOUBLE, neighbor, mpi::rank, - mpi::intracomm, &requests.back()); - } -} - -void receive_ifp_data(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, vector& delayed_groups, - vector& lifetimes, vector& deserialization) -{ - // Receive delayed groups - if (is_beta_effective_or_both()) { - requests.emplace_back(); - MPI_Irecv(&delayed_groups[n_generation * idx], - n_generation * static_cast(n), MPI_INT, neighbor, neighbor, - mpi::intracomm, &requests.back()); - } - // Receive lifetimes - if (is_generation_time_or_both()) { - requests.emplace_back(); - MPI_Irecv(&lifetimes[n_generation * idx], - n_generation * static_cast(n), MPI_DOUBLE, neighbor, neighbor, - mpi::intracomm, &requests.back()); - } - // Deserialization info to reconstruct data later - DeserializationInfo info = {idx, n}; - deserialization.push_back(info); -} - void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, const vector>& delayed_groups, const vector>& lifetimes) @@ -146,31 +90,6 @@ void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, &simulation::ifp_source_lifetime_bank[i_bank]); } } - -void deserialize_ifp_info(int n_generation, - const vector& deserialization, - const vector& delayed_groups, const vector& lifetimes) -{ - for (auto info : deserialization) { - int64_t index_local = info.index_local; - int64_t n = info.n; - - for (int i = index_local; i < index_local + n; i++) { - if (is_beta_effective_or_both()) { - vector delayed_groups_received( - delayed_groups.begin() + n_generation * i, - delayed_groups.begin() + n_generation * (i + 1)); - simulation::ifp_source_delayed_group_bank[i] = delayed_groups_received; - } - if (is_generation_time_or_both()) { - vector lifetimes_received(lifetimes.begin() + n_generation * i, - lifetimes.begin() + n_generation * (i + 1)); - simulation::ifp_source_lifetime_bank[i] = lifetimes_received; - } - } - } -} - #endif void copy_complete_ifp_data_to_source_banks( diff --git a/src/initialize.cpp b/src/initialize.cpp index e2a5b9743..78b414f78 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -118,6 +118,10 @@ int openmc_init(int argc, char* argv[], const void* intracomm) if (!read_model_xml()) read_separate_xml_files(); + if (!settings::properties_file.empty()) { + openmc_properties_import(settings::properties_file.c_str()); + } + // Reset locale to previous state if (std::setlocale(LC_ALL, prev_locale.c_str()) == NULL) { fatal_error("Cannot reset locale."); @@ -157,7 +161,7 @@ void initialize_mpi(MPI_Comm intracomm) // Create bank datatype SourceSite b; - MPI_Aint disp[11]; + MPI_Aint disp[14]; MPI_Get_address(&b.r, &disp[0]); MPI_Get_address(&b.u, &disp[1]); MPI_Get_address(&b.E, &disp[2]); @@ -169,14 +173,35 @@ void initialize_mpi(MPI_Comm intracomm) MPI_Get_address(&b.parent_nuclide, &disp[8]); MPI_Get_address(&b.parent_id, &disp[9]); MPI_Get_address(&b.progeny_id, &disp[10]); - for (int i = 10; i >= 0; --i) { + MPI_Get_address(&b.wgt_born, &disp[11]); + MPI_Get_address(&b.wgt_ww_born, &disp[12]); + MPI_Get_address(&b.n_split, &disp[13]); + for (int i = 13; i >= 0; --i) { disp[i] -= disp[0]; } - int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - MPI_Datatype types[] {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, - MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; - MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site); + // Block counts for each field + int blocks[] = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + // Types for each field + MPI_Datatype types[] = { + MPI_DOUBLE, // r (3 doubles) + MPI_DOUBLE, // u (3 doubles) + MPI_DOUBLE, // E + MPI_DOUBLE, // time + MPI_DOUBLE, // wgt + MPI_INT, // delayed_group + MPI_INT, // surf_id + MPI_INT, // particle (enum) + MPI_INT, // parent_nuclide + MPI_INT64_T, // parent_id + MPI_INT64_T, // progeny_id + MPI_DOUBLE, // wgt_born + MPI_DOUBLE, // wgt_ww_born + MPI_INT64_T // n_split + }; + + MPI_Type_create_struct(14, blocks, disp, types, &mpi::source_site); MPI_Type_commit(&mpi::source_site); CollisionTrackSite bc; @@ -226,6 +251,15 @@ int parse_command_line(int argc, char* argv[]) i += 1; settings::n_particles = std::stoll(argv[i]); + } else if (arg == "-q" || arg == "--verbosity") { + i += 1; + settings::verbosity = std::stoi(argv[i]); + if (settings::verbosity > 10 || settings::verbosity < 1) { + auto msg = fmt::format("Invalid verbosity: {}.", settings::verbosity); + strcpy(openmc_err_msg, msg.c_str()); + return OPENMC_E_INVALID_ARGUMENT; + } + } else if (arg == "-e" || arg == "--event") { settings::event_based = true; } else if (arg == "-r" || arg == "--restart") { @@ -287,6 +321,11 @@ int parse_command_line(int argc, char* argv[]) settings::run_mode = RunMode::VOLUME; } else if (arg == "-s" || arg == "--threads") { // Read number of threads + if (i + 1 >= argc) { + std::string msg {"Number of threads not specified."}; + strcpy(openmc_err_msg, msg.c_str()); + return OPENMC_E_INVALID_ARGUMENT; + } i += 1; #ifdef _OPENMP @@ -348,6 +387,28 @@ int parse_command_line(int argc, char* argv[]) return 0; } +// TODO: Pulse-height tallies require per-history scoring across the full +// particle tree (parent + all descendants). The shared secondary bank +// transports each secondary as an independent Particle, breaking this +// assumption. A proper fix would defer pulse-height scoring: save +// (root_source_id, cell, pht_storage) per particle, then aggregate by +// root_source_id after all secondary generations complete before scoring +// into the histogram. For now, disable shared secondary when pulse-height +// tallies are present. +static void check_pulse_height_compatibility() +{ + if (settings::use_shared_secondary_bank) { + for (const auto& t : model::tallies) { + if (t->type_ == TallyType::PULSE_HEIGHT) { + settings::use_shared_secondary_bank = false; + warning("Pulse-height tallies are not yet compatible with the shared " + "secondary bank. Disabling shared secondary bank."); + break; + } + } + } +} + bool read_model_xml() { std::string model_filename = settings::path_input; @@ -376,8 +437,10 @@ bool read_model_xml() auto settings_root = root.child("settings"); // Verbosity - if (check_for_node(settings_root, "verbosity")) { + if (check_for_node(settings_root, "verbosity") && settings::verbosity == -1) { settings::verbosity = std::stoi(get_node_value(settings_root, "verbosity")); + } else if (settings::verbosity == -1) { + settings::verbosity = 7; } // To this point, we haven't displayed any output since we didn't know what @@ -390,6 +453,10 @@ bool read_model_xml() write_message( fmt::format("Reading model XML file '{}' ...", model_filename), 5); + // Read chain data before settings so DecaySpectrum source distributions can + // resolve nuclides while sources are constructed. + read_chain_file_xml(); + read_settings_xml(settings_root); // If other XML files are present, display warning @@ -405,9 +472,6 @@ bool read_model_xml() } } - // Read data from chain file - read_chain_file_xml(); - // Read materials and cross sections if (!check_for_node(root, "materials")) { fatal_error(fmt::format( @@ -439,6 +503,8 @@ bool read_model_xml() if (check_for_node(root, "tallies")) read_tallies_xml(root.child("tallies")); + check_pulse_height_compatibility(); + // Initialize distribcell_filters prepare_distribcell(); @@ -460,14 +526,15 @@ bool read_model_xml() void read_separate_xml_files() { + // Read chain data before settings so DecaySpectrum source distributions can + // resolve nuclides while sources are constructed. + read_chain_file_xml(); + read_settings_xml(); if (settings::run_mode != RunMode::PLOTTING) { read_cross_sections_xml(); } - // Read data from chain file - read_chain_file_xml(); - read_materials_xml(); read_geometry_xml(); @@ -483,6 +550,8 @@ void read_separate_xml_files() read_tallies_xml(); + check_pulse_height_compatibility(); + // Initialize distribcell_filters prepare_distribcell(); diff --git a/src/lattice.cpp b/src/lattice.cpp index efbfcb216..9e56c58e8 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -10,6 +10,7 @@ #include "openmc/geometry.h" #include "openmc/geometry_aux.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/string_utils.h" #include "openmc/vector.h" #include "openmc/xml_interface.h" @@ -261,47 +262,33 @@ std::pair> RectLattice::distance( double x0 {copysign(0.5 * pitch_[0], u.x)}; double y0 {copysign(0.5 * pitch_[1], u.y)}; - // Left and right sides - double d {INFTY}; - array lattice_trans; - if ((std::abs(x - x0) > FP_PRECISION) && u.x != 0) { - d = (x0 - x) / u.x; - if (u.x > 0) { - lattice_trans = {1, 0, 0}; - } else { - lattice_trans = {-1, 0, 0}; - } - } - - // Front and back sides - if ((std::abs(y - y0) > FP_PRECISION) && u.y != 0) { - double this_d = (y0 - y) / u.y; - if (this_d < d) { - d = this_d; - if (u.y > 0) { - lattice_trans = {0, 1, 0}; - } else { - lattice_trans = {0, -1, 0}; - } - } - } - - // Top and bottom sides + // Evaluate the distance to each oncoming edge independently. Comparing these + // distances directly (rather than reconstructing the crossing position) + // avoids the floating-point cancellation that occurs for large pitches. + double dx = u.x != 0.0 ? (x0 - x) / u.x : INFTY; + double dy = u.y != 0.0 ? (y0 - y) / u.y : INFTY; + double dz = INFTY; if (is_3d_) { double z0 {copysign(0.5 * pitch_[2], u.z)}; - if ((std::abs(z - z0) > FP_PRECISION) && u.z != 0) { - double this_d = (z0 - z) / u.z; - if (this_d < d) { - d = this_d; - if (u.z > 0) { - lattice_trans = {0, 0, 1}; - } else { - lattice_trans = {0, 0, -1}; - } - } - } + dz = u.z != 0.0 ? (z0 - z) / u.z : INFTY; } + // The distance to the nearest lattice boundary is the smallest axial + // distance. + double d = std::min({dx, dy, dz}); + + // Determine which lattice boundaries are being crossed. The axis attaining + // the minimum is exactly equal to d, so at least one translation is always + // set for a finite crossing; a near-equal second axis indicates a corner + // crossing. + array lattice_trans = {0, 0, 0}; + if (isclose(d, dx, FP_COINCIDENT, FP_PRECISION)) + lattice_trans[0] = copysign(1, u.x); + if (isclose(d, dy, FP_COINCIDENT, FP_PRECISION)) + lattice_trans[1] = copysign(1, u.y); + if (is_3d_ && isclose(d, dz, FP_COINCIDENT, FP_PRECISION)) + lattice_trans[2] = copysign(1, u.z); + return {d, lattice_trans}; } @@ -362,6 +349,26 @@ Position RectLattice::get_local_position( //============================================================================== +Direction RectLattice::get_normal( + const array& i_xyz, bool& is_valid) const +{ + is_valid = false; + Direction dir = {0.0, 0.0, 0.0}; + if ((std::abs(i_xyz[0]) == 1) && (i_xyz[1] == 0) && (i_xyz[2] == 0)) { + is_valid = true; + dir[0] = std::copysign(1.0, i_xyz[0]); + } else if ((i_xyz[0] == 0) && (std::abs(i_xyz[1]) == 1) && (i_xyz[2] == 0)) { + is_valid = true; + dir[1] = std::copysign(1.0, i_xyz[1]); + } else if ((i_xyz[0] == 0) && (i_xyz[1] == 0) && (std::abs(i_xyz[2]) == 1)) { + is_valid = true; + dir[2] = std::copysign(1.0, i_xyz[2]); + } + return dir; +} + +//============================================================================== + int32_t& RectLattice::offset(int map, const array& i_xyz) { return offsets_[n_cells_[0] * n_cells_[1] * n_cells_[2] * map + @@ -1008,6 +1015,91 @@ Position HexLattice::get_local_position( //============================================================================== +Direction HexLattice::get_normal( + const array& i_xyz, bool& is_valid) const +{ + // Short description of the direction vectors used here. The beta, gamma, and + // delta vectors point towards the flat sides of each hexagonal tile. + // Y - orientation: + // basis0 = (1, 0) + // basis1 = (-1/sqrt(3), 1) = +120 degrees from basis0 + // beta = (sqrt(3)/2, 1/2) = +30 degrees from basis0 + // gamma = (sqrt(3)/2, -1/2) = -60 degrees from beta + // delta = (0, 1) = +60 degrees from beta + // X - orientation: + // basis0 = (1/sqrt(3), -1) + // basis1 = (0, 1) = +120 degrees from basis0 + // beta = (1, 0) = +30 degrees from basis0 + // gamma = (1/2, -sqrt(3)/2) = -60 degrees from beta + // delta = (1/2, sqrt(3)/2) = +60 degrees from beta + + is_valid = false; + Direction dir = {0.0, 0.0, 0.0}; + if ((i_xyz[0] == 0) && (i_xyz[1] == 0) && (std::abs(i_xyz[2]) == 1)) { + is_valid = true; + dir[2] = std::copysign(1.0, i_xyz[2]); + } else if ((i_xyz[2] == 0) && + std::max({std::abs(i_xyz[0]), std::abs(i_xyz[1]), + std::abs(i_xyz[0] + i_xyz[1])}) == 1) { + is_valid = true; + // beta direction + if ((i_xyz[0] == 1) && (i_xyz[1] == 0)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.5 * std::sqrt(3.0); + dir[1] = 0.5; + } else { + dir[0] = 1.0; + dir[1] = 0.0; + } + } else if ((i_xyz[0] == -1) && (i_xyz[1] == 0)) { + if (orientation_ == Orientation::y) { + dir[0] = -0.5 * std::sqrt(3.0); + dir[1] = -0.5; + } else { + dir[0] = -1.0; + dir[1] = 0.0; + } + // gamma direction + } else if ((i_xyz[0] == 1) && (i_xyz[1] == -1)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.5 * std::sqrt(3.0); + dir[1] = -0.5; + } else { + dir[0] = 0.5; + dir[1] = -0.5 * std::sqrt(3.0); + } + } else if ((i_xyz[0] == -1) && (i_xyz[1] == 1)) { + if (orientation_ == Orientation::y) { + dir[0] = -0.5 * std::sqrt(3.0); + dir[1] = 0.5; + } else { + dir[0] = -0.5; + dir[1] = 0.5 * std::sqrt(3.0); + } + // delta direction + } else if ((i_xyz[0] == 0) && (i_xyz[1] == 1)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.0; + dir[1] = 1.0; + } else { + dir[0] = 0.5; + dir[1] = 0.5 * std::sqrt(3.0); + } + } else if ((i_xyz[0] == 0) && (i_xyz[1] == -1)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.0; + dir[1] = -1.0; + } else { + dir[0] = -0.5; + dir[1] = -0.5 * std::sqrt(3.0); + } + } + } + return dir; +} + +//============================================================================== + bool HexLattice::is_valid_index(int indx) const { int nx {2 * n_rings_ - 1}; diff --git a/src/material.cpp b/src/material.cpp index 54caa3840..21b11b8b9 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -8,9 +8,7 @@ #include #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xoperation.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/capi.h" #include "openmc/container_util.h" @@ -216,7 +214,7 @@ Material::Material(pugi::xml_node node) // allocate arrays in Material object auto n = names.size(); nuclide_.reserve(n); - atom_density_ = xt::empty({n}); + atom_density_ = tensor::Tensor({n}); if (settings::photon_transport) element_.reserve(n); @@ -290,14 +288,14 @@ Material::Material(pugi::xml_node node) // Check to make sure either all atom percents or all weight percents are // given - if (!(xt::all(atom_density_ >= 0.0) || xt::all(atom_density_ <= 0.0))) { + if (!((atom_density_ >= 0.0).all() || (atom_density_ <= 0.0).all())) { fatal_error( "Cannot mix atom and weight percents in material " + std::to_string(id_)); } // Determine density if it is a sum value if (sum_density) - density_ = xt::sum(atom_density_)(); + density_ = atom_density_.sum(); if (check_for_node(node, "temperature")) { temperature_ = std::stod(get_node_value(node, "temperature")); @@ -435,7 +433,7 @@ void Material::normalize_density() // determine normalized atom percents. if given atom percents, this is // straightforward. if given weight percents, the value is w/awr and is // divided by sum(w/awr) - atom_density_ /= xt::sum(atom_density_)(); + atom_density_ /= atom_density_.sum(); // Change density in g/cm^3 to atom/b-cm. Since all values are now in // atom percent, the sum needs to be re-evaluated as 1/sum(x*awr) @@ -641,14 +639,14 @@ void Material::init_bremsstrahlung() bool positron = (particle == 1); // Allocate arrays for TTB data - ttb->pdf = xt::zeros({n_e, n_e}); - ttb->cdf = xt::zeros({n_e, n_e}); - ttb->yield = xt::zeros({n_e}); + ttb->pdf = tensor::zeros({n_e, n_e}); + ttb->cdf = tensor::zeros({n_e, n_e}); + ttb->yield = tensor::zeros({n_e}); // Allocate temporary arrays - xt::xtensor stopping_power_collision({n_e}, 0.0); - xt::xtensor stopping_power_radiative({n_e}, 0.0); - xt::xtensor dcs({n_e, n_k}, 0.0); + auto stopping_power_collision = tensor::zeros({n_e}); + auto stopping_power_radiative = tensor::zeros({n_e}); + auto dcs = tensor::zeros({n_e, n_k}); double Z_eq_sq = 0.0; double sum_density = 0.0; @@ -698,18 +696,18 @@ void Material::init_bremsstrahlung() 1.0595e-3 * std::pow(t, 5) + 7.0568e-5 * std::pow(t, 6) - 1.808e-6 * std::pow(t, 7)); stopping_power_radiative(i) *= r; - auto dcs_i = xt::view(dcs, i, xt::all()); + tensor::View dcs_i = dcs.slice(i); dcs_i *= r; } } // Total material stopping power - xt::xtensor stopping_power = + tensor::Tensor stopping_power = stopping_power_collision + stopping_power_radiative; // Loop over photon energies - xt::xtensor f({n_e}, 0.0); - xt::xtensor z({n_e}, 0.0); + auto f = tensor::zeros({n_e}); + auto z = tensor::zeros({n_e}); for (int i = 0; i < n_e - 1; ++i) { double w = data::ttb_e_grid(i); @@ -797,7 +795,8 @@ void Material::init_bremsstrahlung() } // Use logarithm of number yield since it is log-log interpolated - ttb->yield = xt::where(ttb->yield > 0.0, xt::log(ttb->yield), -500.0); + ttb->yield = + tensor::where(ttb->yield > 0.0, tensor::log(ttb->yield), -500.0); } } @@ -819,9 +818,9 @@ void Material::calculate_xs(Particle& p) const p.macro_xs().fission = 0.0; p.macro_xs().nu_fission = 0.0; - if (p.type() == ParticleType::neutron) { + if (p.type().is_neutron()) { this->calculate_neutron_xs(p); - } else if (p.type() == ParticleType::photon) { + } else if (p.type().is_photon()) { this->calculate_photon_xs(p); } } @@ -829,7 +828,7 @@ void Material::calculate_xs(Particle& p) const void Material::calculate_neutron_xs(Particle& p) const { // Find energy index on energy grid - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); int i_grid = std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing; @@ -979,7 +978,7 @@ void Material::set_density(double density, const std::string& units) density_ = density; // Determine normalized atom percents - double sum_percent = xt::sum(atom_density_)(); + double sum_percent = atom_density_.sum(); atom_density_ /= sum_percent; // Recalculate nuclide atom densities based on given density @@ -1020,7 +1019,7 @@ void Material::set_densities( if (n != nuclide_.size()) { nuclide_.resize(n); - atom_density_ = xt::zeros({n}); + atom_density_ = tensor::zeros({n}); if (settings::photon_transport) element_.resize(n); } @@ -1181,8 +1180,8 @@ void Material::add_nuclide(const std::string& name, double density) auto n = nuclide_.size(); // Create copy of atom_density_ array with one extra entry - xt::xtensor atom_density = xt::zeros({n}); - xt::view(atom_density, xt::range(0, n - 1)) = atom_density_; + tensor::Tensor atom_density = tensor::zeros({n}); + atom_density.slice(tensor::range(0, n - 1)) = atom_density_; atom_density(n - 1) = density; atom_density_ = atom_density; diff --git a/src/math_functions.cpp b/src/math_functions.cpp index 5469b56c8..ddacc2bd9 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -1,5 +1,7 @@ #include "openmc/math_functions.h" +#include // for numeric_limits + #include "openmc/external/Faddeeva.hh" #include "openmc/constants.h" @@ -95,6 +97,13 @@ double t_percentile(double p, int df) return t; } +double standard_normal_cdf(double z) +{ + // Use the complementary error function to compute the standard normal CDF + // Phi(z) = 0.5 * (1 + erf(z / sqrt(2))) = 0.5 * erfc(-z / sqrt(2)) + return 0.5 * std::erfc(-z / std::sqrt(2.0)); +} + void calc_pn_c(int n, double x, double pnx[]) { pnx[0] = 1.; @@ -919,4 +928,85 @@ std::complex w_derivative(std::complex z, int order) } } +double exprel(double x) +{ + if (std::abs(x) < 1e-16) + return 1.0; + else { + return std::expm1(x) / x; + } +} + +double log1prel(double x) +{ + if (std::abs(x) < 1e-16) + return 1.0; + else { + return std::log1p(x) / x; + } +} + +double cyl_bessel_j(int n, double x) +{ + // Handle negative arguments via the parity relation + // J_n(-x) = (-1)^n J_n(x); std::cyl_bessel_j has a domain error for x < 0. + double sign = 1.0; + if (x < 0.0) { + x = -x; + if (n % 2 == 1) + sign = -1.0; + } + +#if defined(__cpp_lib_math_special_functions) && \ + __cpp_lib_math_special_functions >= 201603L + return sign * std::cyl_bessel_j(static_cast(n), x); +#else + // Ascending power series (e.g., Abramowitz & Stegun eq. 9.1.10): + // J_n(x) = sum_{m=0}^inf (-1)^m / (m! (m+n)!) * (x/2)^(2m+n) + // The term ratio is -(x/2)^2 / (m*(m+n)), so for |x| <= 2 the series + // converges to machine precision within ~20 terms. + double half_x = 0.5 * x; + + // First term: (x/2)^n / n! + double term = 1.0; + for (int k = 1; k <= n; ++k) { + term *= half_x / k; + } + + double sum = term; + double neg_half_x_sq = -half_x * half_x; + for (int m = 1; m <= 50; ++m) { + term *= neg_half_x_sq / (m * (m + n)); + sum += term; + if (std::abs(term) <= + std::numeric_limits::epsilon() * std::abs(sum)) + break; + } + return sign * sum; +#endif +} + +// Helper function to get index and interpolation function on an incident energy +// grid +void get_energy_index( + const vector& energies, double E, int& i, double& f) +{ + // Get index and interpolation factor for linear-linear energy grid + i = 0; + f = 0.0; + if (E >= energies.front()) { + i = lower_bound_index(energies.begin(), energies.end(), E); + if (i + 1 < energies.size()) + f = (E - energies[i]) / (energies[i + 1] - energies[i]); + } +} + +// Return true if two floating-point values are approximately equal within a +// combined relative and absolute tolerance. +bool isclose(double a, double b, double rel_tol, double abs_tol) +{ + return std::abs(a - b) <= + std::max(rel_tol * std::max(std::abs(a), std::abs(b)), abs_tol); +} + } // namespace openmc diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 129407301..30c41ec5a 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -63,7 +63,7 @@ using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* file_handle, - const char* key, int32_t ldata, const char* data); + const char* key, uint32_t datalength, const char* data); using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); using mcpl_hdr_set_srcname_fpt = void (*)( mcpl_outfile_t* outfile_handle, const char* srcname); @@ -150,13 +150,20 @@ struct McplApi { load_symbol_platform("mcpl_create_outfile")); hdr_set_srcname = reinterpret_cast( load_symbol_platform("mcpl_hdr_set_srcname")); - hdr_add_data = reinterpret_cast( - load_symbol_platform("mcpl_hdr_add_data")); add_particle = reinterpret_cast( load_symbol_platform("mcpl_add_particle")); close_outfile = reinterpret_cast( load_symbol_platform("mcpl_close_outfile")); + // Try to load mcpl_hdr_add_data (available in MCPL >= 2.1.0) + // Set to nullptr if not available for graceful fallback + try { + hdr_add_data = reinterpret_cast( + load_symbol_platform("mcpl_hdr_add_data")); + } catch (const std::runtime_error&) { + hdr_add_data = nullptr; + } + // Try to load mcpl_hdr_add_stat_sum (available in MCPL >= 2.1.0) // Set to nullptr if not available for graceful fallback try { @@ -314,25 +321,7 @@ inline void ensure_mcpl_ready_or_fatal() SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr) { SourceSite site; - switch (particle_repr->pdgcode) { - case 2112: - site.particle = ParticleType::neutron; - break; - case 22: - site.particle = ParticleType::photon; - break; - case 11: - site.particle = ParticleType::electron; - break; - case -11: - site.particle = ParticleType::positron; - break; - default: - fatal_error(fmt::format( - "MCPL: Encountered unexpected PDG code {} when converting to SourceSite.", - particle_repr->pdgcode)); - break; - } + site.particle = ParticleType {particle_repr->pdgcode}; // Copy position and direction site.r.x = particle_repr->position[0]; @@ -361,7 +350,6 @@ vector mcpl_source_sites(std::string path) } size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file); - size_t n_skipped = 0; if (n_particles_in_file > 0) { sites.reserve(n_particles_in_file); } @@ -374,31 +362,16 @@ vector mcpl_source_sites(std::string path) path, sites.size(), n_particles_in_file)); break; } - if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 || - p_repr->pdgcode == 11 || p_repr->pdgcode == -11) { - sites.push_back(mcpl_particle_to_site(p_repr)); - } else { - n_skipped++; - } + sites.push_back(mcpl_particle_to_site(p_repr)); } g_mcpl_api->close_file(mcpl_file); - if (n_skipped > 0 && n_particles_in_file > 0) { - double percent_skipped = - 100.0 * static_cast(n_skipped) / n_particles_in_file; - warning(fmt::format( - "MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because " - "their type is not supported by OpenMC.", - n_skipped, n_particles_in_file, percent_skipped, path)); - } - if (sites.empty()) { if (n_particles_in_file > 0) { fatal_error(fmt::format( - "MCPL file '{}' contained {} particles, but none were of the supported " - "types (neutron, photon, electron, positron). OpenMC cannot proceed " - "without source particles.", + "MCPL file '{}' contained {} particles, but no particles could be " + "read.", path, n_particles_in_file)); } else { fatal_error(fmt::format( @@ -454,22 +427,7 @@ void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id, p_repr.ekin = site.E * 1e-6; p_repr.time = site.time * 1e3; p_repr.weight = site.wgt; - switch (site.particle) { - case ParticleType::neutron: - p_repr.pdgcode = 2112; - break; - case ParticleType::photon: - p_repr.pdgcode = 22; - break; - case ParticleType::electron: - p_repr.pdgcode = 11; - break; - case ParticleType::positron: - p_repr.pdgcode = -11; - break; - default: - continue; - } + p_repr.pdgcode = site.particle.pdg_number(); g_mcpl_api->add_particle(file_id, &p_repr); } } @@ -626,22 +584,7 @@ void write_mcpl_collision_track_internal(mcpl_outfile_t* file_id, p_repr.ekin = site.E * 1e-6; p_repr.time = site.time * 1e3; p_repr.weight = site.wgt; - switch (site.particle) { - case ParticleType::neutron: - p_repr.pdgcode = 2112; - break; - case ParticleType::photon: - p_repr.pdgcode = 22; - break; - case ParticleType::electron: - p_repr.pdgcode = 11; - break; - case ParticleType::positron: - p_repr.pdgcode = -11; - break; - default: - continue; - } + p_repr.pdgcode = site.particle.pdg_number(); g_mcpl_api->add_particle(file_id, &p_repr); } } else { diff --git a/src/mesh.cpp b/src/mesh.cpp index 610d057cf..b05560d2a 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,9 +1,12 @@ #include "openmc/mesh.h" #include // for copy, equal, min, min_element #include -#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers -#include // for ceil -#include // for size_t +#include // for ceil +#include // for size_t +#include // for uint64_t +#include // for memcpy +#include +#include // for accumulate #include #ifdef _MSC_VER @@ -14,13 +17,7 @@ #include "mpi.h" #endif -#include "xtensor/xadapt.hpp" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xeval.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xsort.hpp" -#include "xtensor/xtensor.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include // for fmt #include "openmc/capi.h" @@ -51,6 +48,7 @@ #include "libmesh/mesh_modification.h" #include "libmesh/mesh_tools.h" #include "libmesh/numeric_vector.h" +#include "libmesh/replicated_mesh.h" #endif #ifdef OPENMC_DAGMC_ENABLED @@ -139,6 +137,63 @@ inline bool atomic_cas_int32(int32_t* ptr, int32_t& expected, int32_t desired) #endif } +// Helper function equivalent to std::bit_cast in C++20 +template +inline To bit_cast_value(const From& value) +{ + To out; + std::memcpy(&out, &value, sizeof(To)); + return out; +} + +inline void atomic_update_double(double* ptr, double value, bool is_min) +{ +#if defined(__GNUC__) || defined(__clang__) + using may_alias_uint64_t [[gnu::may_alias]] = uint64_t; + auto* bits_ptr = reinterpret_cast(ptr); + uint64_t current_bits = __atomic_load_n(bits_ptr, __ATOMIC_SEQ_CST); + double current = bit_cast_value(current_bits); + while (is_min ? (value < current) : (value > current)) { + uint64_t desired_bits = bit_cast_value(value); + uint64_t expected_bits = current_bits; + if (__atomic_compare_exchange_n(bits_ptr, &expected_bits, desired_bits, + false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) { + return; + } + current_bits = expected_bits; + current = bit_cast_value(current_bits); + } + +#elif defined(_MSC_VER) + auto* bits_ptr = reinterpret_cast(ptr); + long long current_bits = *bits_ptr; + double current = bit_cast_value(current_bits); + while (is_min ? (value < current) : (value > current)) { + long long desired_bits = bit_cast_value(value); + long long old_bits = + _InterlockedCompareExchange64(bits_ptr, desired_bits, current_bits); + if (old_bits == current_bits) { + return; + } + current_bits = old_bits; + current = bit_cast_value(current_bits); + } + +#else +#error "No compare-and-swap implementation available for this compiler." +#endif +} + +inline void atomic_max_double(double* ptr, double value) +{ + atomic_update_double(ptr, value, false); +} + +inline void atomic_min_double(double* ptr, double value) +{ + atomic_update_double(ptr, value, true); +} + namespace detail { //============================================================================== @@ -146,7 +201,7 @@ namespace detail { //============================================================================== void MaterialVolumes::add_volume( - int index_elem, int index_material, double volume) + int index_elem, int index_material, double volume, const BoundingBox* bbox) { // This method handles adding elements to the materials hash table, // implementing open addressing with linear probing. Consistency across @@ -165,10 +220,18 @@ void MaterialVolumes::add_volume( // Non-atomic read of current material int32_t current_val = *slot_ptr; - // Found the desired material; accumulate volume + // Found the desired material; accumulate volume and bbox if (current_val == index_material) { #pragma omp atomic this->volumes(index_elem, slot) += volume; + if (bbox) { + atomic_min_double(&this->bboxes(index_elem, slot, 0), bbox->min.x); + atomic_min_double(&this->bboxes(index_elem, slot, 1), bbox->min.y); + atomic_min_double(&this->bboxes(index_elem, slot, 2), bbox->min.z); + atomic_max_double(&this->bboxes(index_elem, slot, 3), bbox->max.x); + atomic_max_double(&this->bboxes(index_elem, slot, 4), bbox->max.y); + atomic_max_double(&this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } @@ -184,6 +247,14 @@ void MaterialVolumes::add_volume( if (claimed_slot || (expected_val == index_material)) { #pragma omp atomic this->volumes(index_elem, slot) += volume; + if (bbox) { + atomic_min_double(&this->bboxes(index_elem, slot, 0), bbox->min.x); + atomic_min_double(&this->bboxes(index_elem, slot, 1), bbox->min.y); + atomic_min_double(&this->bboxes(index_elem, slot, 2), bbox->min.z); + atomic_max_double(&this->bboxes(index_elem, slot, 3), bbox->max.x); + atomic_max_double(&this->bboxes(index_elem, slot, 4), bbox->max.y); + atomic_max_double(&this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } } @@ -194,7 +265,7 @@ void MaterialVolumes::add_volume( } void MaterialVolumes::add_volume_unsafe( - int index_elem, int index_material, double volume) + int index_elem, int index_material, double volume, const BoundingBox* bbox) { // Linear probe for (int attempt = 0; attempt < table_size_; ++attempt) { @@ -206,9 +277,23 @@ void MaterialVolumes::add_volume_unsafe( // Read current material int32_t current_val = this->materials(index_elem, slot); - // Found the desired material; accumulate volume + // Found the desired material; accumulate volume and bbox if (current_val == index_material) { this->volumes(index_elem, slot) += volume; + if (bbox) { + this->bboxes(index_elem, slot, 0) = + std::min(this->bboxes(index_elem, slot, 0), bbox->min.x); + this->bboxes(index_elem, slot, 1) = + std::min(this->bboxes(index_elem, slot, 1), bbox->min.y); + this->bboxes(index_elem, slot, 2) = + std::min(this->bboxes(index_elem, slot, 2), bbox->min.z); + this->bboxes(index_elem, slot, 3) = + std::max(this->bboxes(index_elem, slot, 3), bbox->max.x); + this->bboxes(index_elem, slot, 4) = + std::max(this->bboxes(index_elem, slot, 4), bbox->max.y); + this->bboxes(index_elem, slot, 5) = + std::max(this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } @@ -216,6 +301,20 @@ void MaterialVolumes::add_volume_unsafe( if (current_val == EMPTY) { this->materials(index_elem, slot) = index_material; this->volumes(index_elem, slot) += volume; + if (bbox) { + this->bboxes(index_elem, slot, 0) = + std::min(this->bboxes(index_elem, slot, 0), bbox->min.x); + this->bboxes(index_elem, slot, 1) = + std::min(this->bboxes(index_elem, slot, 1), bbox->min.y); + this->bboxes(index_elem, slot, 2) = + std::min(this->bboxes(index_elem, slot, 2), bbox->min.z); + this->bboxes(index_elem, slot, 3) = + std::max(this->bboxes(index_elem, slot, 3), bbox->max.x); + this->bboxes(index_elem, slot, 4) = + std::max(this->bboxes(index_elem, slot, 4), bbox->max.y); + this->bboxes(index_elem, slot, 5) = + std::max(this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } } @@ -332,6 +431,12 @@ vector Mesh::volumes() const void Mesh::material_volumes(int nx, int ny, int nz, int table_size, int32_t* materials, double* volumes) const +{ + this->material_volumes(nx, ny, nz, table_size, materials, volumes, nullptr); +} + +void Mesh::material_volumes(int nx, int ny, int nz, int table_size, + int32_t* materials, double* volumes, double* bboxes) const { if (mpi::master) { header("MESH MATERIAL VOLUMES CALCULATION", 7); @@ -350,7 +455,8 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, timer.start(); // Create object for keeping track of materials/volumes - detail::MaterialVolumes result(materials, volumes, table_size); + detail::MaterialVolumes result(materials, volumes, bboxes, table_size); + bool compute_bboxes = bboxes != nullptr; // Determine bounding box auto bbox = this->bounding_box(); @@ -358,9 +464,10 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, std::array n_rays = {nx, ny, nz}; // Determine effective width of rays - Position width((nx > 0) ? (bbox.xmax - bbox.xmin) / nx : 0.0, - (ny > 0) ? (bbox.ymax - bbox.ymin) / ny : 0.0, - (nz > 0) ? (bbox.zmax - bbox.zmin) / nz : 0.0); + Position width = bbox.max - bbox.min; + width.x = (nx > 0) ? width.x / nx : 0.0; + width.y = (ny > 0) ? width.y / ny : 0.0; + width.z = (nz > 0) ? width.z / nz : 0.0; // Set flag for mesh being contained within model bool out_of_model = false; @@ -368,26 +475,26 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, #pragma omp parallel { // Preallocate vector for mesh indices and length fractions and particle - std::vector bins; - std::vector length_fractions; + vector bins; + vector length_fractions; Particle p; SourceSite site; site.E = 1.0; - site.particle = ParticleType::neutron; + site.particle = ParticleType::neutron(); for (int axis = 0; axis < 3; ++axis) { // Set starting position and direction site.r = {0.0, 0.0, 0.0}; - site.r[axis] = bbox.min()[axis]; + site.r[axis] = bbox.min[axis]; site.u = {0.0, 0.0, 0.0}; site.u[axis] = 1.0; // Determine width of rays and number of rays in other directions int ax1 = (axis + 1) % 3; int ax2 = (axis + 2) % 3; - double min1 = bbox.min()[ax1]; - double min2 = bbox.min()[ax2]; + double min1 = bbox.min[ax1]; + double min2 = bbox.min[ax2]; double d1 = width[ax1]; double d2 = width[ax2]; int n1 = n_rays[ax1]; @@ -432,7 +539,7 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, while (true) { // Ray trace from r_start to r_end Position r0 = p.r(); - double max_distance = bbox.max()[axis] - r0[axis]; + double max_distance = bbox.max[axis] - r0[axis]; // Find the distance to the nearest boundary BoundaryInfo boundary = distance_to_boundary(p); @@ -451,12 +558,36 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, if (i_material != C_NONE) { i_material = model::materials[i_material]->id(); } + double cumulative_frac = 0.0; for (int i_bin = 0; i_bin < bins.size(); i_bin++) { int mesh_index = bins[i_bin]; double length = distance * length_fractions[i_bin]; + double volume = length * d1 * d2; - // Add volume to result - result.add_volume(mesh_index, i_material, length * d1 * d2); + if (compute_bboxes) { + double axis_start = r0[axis] + distance * cumulative_frac; + double axis_end = axis_start + length; + cumulative_frac += length_fractions[i_bin]; + + Position contrib_min = site.r; + Position contrib_max = site.r; + + contrib_min[ax1] = site.r[ax1] - 0.5 * d1; + contrib_max[ax1] = site.r[ax1] + 0.5 * d1; + contrib_min[ax2] = site.r[ax2] - 0.5 * d2; + contrib_max[ax2] = site.r[ax2] + 0.5 * d2; + contrib_min[axis] = std::min(axis_start, axis_end); + contrib_max[axis] = std::max(axis_start, axis_end); + + BoundingBox contrib_bbox {contrib_min, contrib_max}; + contrib_bbox &= bbox; + + result.add_volume( + mesh_index, i_material, volume, &contrib_bbox); + } else { + // Add volume to result + result.add_volume(mesh_index, i_material, volume); + } } if (distance == max_distance) @@ -503,10 +634,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, // Combine results from multiple MPI processes if (mpi::n_procs > 1) { int total = this->n_bins() * table_size; + int total_bbox = total * 6; if (mpi::master) { // Allocate temporary buffer for receiving data - std::vector mats(total); - std::vector vols(total); + vector mats(total); + vector vols(total); + vector recv_bboxes; + if (compute_bboxes) { + recv_bboxes.resize(total_bbox); + } for (int i = 1; i < mpi::n_procs; ++i) { // Receive material indices and volumes from process i @@ -514,6 +650,10 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, MPI_STATUS_IGNORE); MPI_Recv(vols.data(), total, MPI_DOUBLE, i, i, mpi::intracomm, MPI_STATUS_IGNORE); + if (compute_bboxes) { + MPI_Recv(recv_bboxes.data(), total_bbox, MPI_DOUBLE, i, i, + mpi::intracomm, MPI_STATUS_IGNORE); + } // Combine with existing results; we can call thread unsafe version of // add_volume because each thread is operating on a different element @@ -522,7 +662,18 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int k = 0; k < table_size; ++k) { int index = index_elem * table_size + k; if (mats[index] != EMPTY) { - result.add_volume_unsafe(index_elem, mats[index], vols[index]); + if (compute_bboxes) { + int bbox_index = index * 6; + BoundingBox slot_bbox { + {recv_bboxes[bbox_index + 0], recv_bboxes[bbox_index + 1], + recv_bboxes[bbox_index + 2]}, + {recv_bboxes[bbox_index + 3], recv_bboxes[bbox_index + 4], + recv_bboxes[bbox_index + 5]}}; + result.add_volume_unsafe( + index_elem, mats[index], vols[index], &slot_bbox); + } else { + result.add_volume_unsafe(index_elem, mats[index], vols[index]); + } } } } @@ -531,6 +682,9 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, // Send material indices and volumes to process 0 MPI_Send(materials, total, MPI_INT32_T, 0, mpi::rank, mpi::intracomm); MPI_Send(volumes, total, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); + if (compute_bboxes) { + MPI_Send(bboxes, total_bbox, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); + } } } @@ -613,11 +767,9 @@ std::string StructuredMesh::bin_label(int bin) const } } -xt::xtensor StructuredMesh::get_x_shape() const +tensor::Tensor StructuredMesh::get_shape_tensor() const { - // because method is const, shape_ is const as well and can't be adapted - auto tmp_shape = shape_; - return xt::adapt(tmp_shape, {n_dimension_}); + return tensor::Tensor(shape_.data(), static_cast(n_dimension_)); } Position StructuredMesh::sample_element( @@ -802,10 +954,11 @@ void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const write_dataset(mesh_group, "length_multiplier", length_multiplier_); // write vertex coordinates - xt::xtensor vertices({static_cast(this->n_vertices()), 3}); + tensor::Tensor vertices( + {static_cast(this->n_vertices()), static_cast(3)}); for (int i = 0; i < this->n_vertices(); i++) { auto v = this->vertex(i); - xt::view(vertices, i, xt::all()) = xt::xarray({v.x, v.y, v.z}); + vertices.slice(i) = {v.x, v.y, v.z}; } write_dataset(mesh_group, "vertices", vertices); @@ -813,8 +966,10 @@ void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const // write element types and connectivity vector volumes; - xt::xtensor connectivity({static_cast(this->n_bins()), 8}); - xt::xtensor elem_types({static_cast(this->n_bins()), 1}); + tensor::Tensor connectivity( + {static_cast(this->n_bins()), static_cast(8)}); + tensor::Tensor elem_types( + {static_cast(this->n_bins()), static_cast(1)}); for (int i = 0; i < this->n_bins(); i++) { auto conn = this->connectivity(i); @@ -822,21 +977,18 @@ void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const // write linear tet element if (conn.size() == 4) { - xt::view(elem_types, i, xt::all()) = - static_cast(ElementType::LINEAR_TET); - xt::view(connectivity, i, xt::all()) = - xt::xarray({conn[0], conn[1], conn[2], conn[3], -1, -1, -1, -1}); + elem_types.slice(i) = static_cast(ElementType::LINEAR_TET); + connectivity.slice(i) = { + conn[0], conn[1], conn[2], conn[3], -1, -1, -1, -1}; // write linear hex element } else if (conn.size() == 8) { - xt::view(elem_types, i, xt::all()) = - static_cast(ElementType::LINEAR_HEX); - xt::view(connectivity, i, xt::all()) = xt::xarray({conn[0], conn[1], - conn[2], conn[3], conn[4], conn[5], conn[6], conn[7]}); + elem_types.slice(i) = static_cast(ElementType::LINEAR_HEX); + connectivity.slice(i) = { + conn[0], conn[1], conn[2], conn[3], conn[4], conn[5], conn[6], conn[7]}; } else { num_elem_skipped++; - xt::view(elem_types, i, xt::all()) = - static_cast(ElementType::UNSUPPORTED); - xt::view(connectivity, i, xt::all()) = -1; + elem_types.slice(i) = static_cast(ElementType::UNSUPPORTED); + connectivity.slice(i) = -1; } } @@ -928,16 +1080,29 @@ int StructuredMesh::get_bin(Position r) const int StructuredMesh::n_bins() const { - return std::accumulate( - shape_.begin(), shape_.begin() + n_dimension_, 1, std::multiplies<>()); + // Bin indices are stored as 32-bit ints in the tally system. + int64_t n = 1; + for (int i = 0; i < n_dimension_; ++i) + n *= shape_[i]; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many bins ({}) for 32-bit tally indexing", id_, n)); + } + return static_cast(n); } int StructuredMesh::n_surface_bins() const { - return 4 * n_dimension_ * n_bins(); + // Surface bin indices are stored as 32-bit ints in the tally system. + int64_t n = static_cast(n_bins()) * 4 * n_dimension_; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many surface bins ({}) for tally indexing", id_, n)); + } + return static_cast(n); } -xt::xtensor StructuredMesh::count_sites( +tensor::Tensor StructuredMesh::count_sites( const SourceSite* bank, int64_t length, bool* outside) const { // Determine shape of array for counts @@ -945,7 +1110,7 @@ xt::xtensor StructuredMesh::count_sites( vector shape = {m}; // Create array of zeros - xt::xarray cnt {shape, 0.0}; + auto cnt = tensor::zeros(shape); bool outside_ = false; for (int64_t i = 0; i < length; i++) { @@ -964,31 +1129,25 @@ xt::xtensor StructuredMesh::count_sites( cnt(mesh_bin) += site.wgt; } - // Create copy of count data. Since ownership will be acquired by xtensor, - // std::allocator must be used to avoid Valgrind mismatched free() / delete - // warnings. + // Create reduced count data + auto counts = tensor::zeros(shape); int total = cnt.size(); - double* cnt_reduced = std::allocator {}.allocate(total); #ifdef OPENMC_MPI // collect values from all processors MPI_Reduce( - cnt.data(), cnt_reduced, total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); + cnt.data(), counts.data(), total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); // Check if there were sites outside the mesh for any processor if (outside) { MPI_Reduce(&outside_, outside, 1, MPI_C_BOOL, MPI_LOR, 0, mpi::intracomm); } #else - std::copy(cnt.data(), cnt.data() + total, cnt_reduced); + std::copy(cnt.data(), cnt.data() + total, counts.data()); if (outside) *outside = outside_; #endif - // Adapt reduced values in array back into an xarray - auto arr = xt::adapt(cnt_reduced, total, xt::acquire_ownership(), shape); - xt::xarray counts = arr; - return counts; } @@ -1181,10 +1340,10 @@ void StructuredMesh::surface_bins_crossed( int RegularMesh::set_grid() { - auto shape = xt::adapt(shape_, {n_dimension_}); + tensor::Tensor shape(shape_.data(), static_cast(n_dimension_)); // Check that dimensions are all greater than zero - if (xt::any(shape <= 0)) { + if ((shape <= 0).any()) { set_errmsg("All entries for a regular mesh dimensions " "must be positive."); return OPENMC_E_INVALID_ARGUMENT; @@ -1206,13 +1365,13 @@ int RegularMesh::set_grid() } // Check for negative widths - if (xt::any(width_ < 0.0)) { + if ((width_ < 0.0).any()) { set_errmsg("Cannot have a negative width on a regular mesh."); return OPENMC_E_INVALID_ARGUMENT; } // Set width and upper right coordinate - upper_right_ = xt::eval(lower_left_ + shape * width_); + upper_right_ = lower_left_ + shape * width_; } else if (upper_right_.size() > 0) { @@ -1224,7 +1383,7 @@ int RegularMesh::set_grid() } // Check that upper-right is above lower-left - if (xt::any(upper_right_ < lower_left_)) { + if ((upper_right_ < lower_left_).any()) { set_errmsg( "The upper_right coordinates of a regular mesh must be greater than " "the lower_left coordinates."); @@ -1232,11 +1391,11 @@ int RegularMesh::set_grid() } // Set width - width_ = xt::eval((upper_right_ - lower_left_) / shape); + width_ = (upper_right_ - lower_left_) / shape; } // Set material volumes - volume_frac_ = 1.0 / xt::prod(shape)(); + volume_frac_ = 1.0 / shape.prod(); element_volume_ = 1.0; for (int i = 0; i < n_dimension_; i++) { @@ -1252,7 +1411,7 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Must specify on a regular mesh."); } - xt::xtensor shape = get_node_xarray(node, "dimension"); + tensor::Tensor shape = get_node_tensor(node, "dimension"); int n = n_dimension_ = shape.size(); if (n != 1 && n != 2 && n != 3) { fatal_error("Mesh must be one, two, or three dimensions."); @@ -1262,7 +1421,7 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} // Check for lower-left coordinates if (check_for_node(node, "lower_left")) { // Read mesh lower-left corner location - lower_left_ = get_node_xarray(node, "lower_left"); + lower_left_ = get_node_tensor(node, "lower_left"); } else { fatal_error("Must specify on a mesh."); } @@ -1273,11 +1432,11 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Cannot specify both and on a mesh."); } - width_ = get_node_xarray(node, "width"); + width_ = get_node_tensor(node, "width"); } else if (check_for_node(node, "upper_right")) { - upper_right_ = get_node_xarray(node, "upper_right"); + upper_right_ = get_node_tensor(node, "upper_right"); } else { fatal_error("Must specify either or on a mesh."); @@ -1295,7 +1454,7 @@ RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group} fatal_error("Must specify on a regular mesh."); } - xt::xtensor shape; + tensor::Tensor shape; read_dataset(group, "dimension", shape); int n = n_dimension_ = shape.size(); if (n != 1 && n != 2 && n != 3) { @@ -1410,13 +1569,13 @@ std::pair, vector> RegularMesh::plot( void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "dimension", get_x_shape()); + write_dataset(mesh_group, "dimension", get_shape_tensor()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); write_dataset(mesh_group, "width", width_); } -xt::xtensor RegularMesh::count_sites( +tensor::Tensor RegularMesh::count_sites( const SourceSite* bank, int64_t length, bool* outside) const { // Determine shape of array for counts @@ -1424,7 +1583,7 @@ xt::xtensor RegularMesh::count_sites( vector shape = {m}; // Create array of zeros - xt::xarray cnt {shape, 0.0}; + auto cnt = tensor::zeros(shape); bool outside_ = false; for (int64_t i = 0; i < length; i++) { @@ -1443,31 +1602,25 @@ xt::xtensor RegularMesh::count_sites( cnt(mesh_bin) += site.wgt; } - // Create copy of count data. Since ownership will be acquired by xtensor, - // std::allocator must be used to avoid Valgrind mismatched free() / delete - // warnings. + // Create reduced count data + auto counts = tensor::zeros(shape); int total = cnt.size(); - double* cnt_reduced = std::allocator {}.allocate(total); #ifdef OPENMC_MPI // collect values from all processors MPI_Reduce( - cnt.data(), cnt_reduced, total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); + cnt.data(), counts.data(), total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); // Check if there were sites outside the mesh for any processor if (outside) { MPI_Reduce(&outside_, outside, 1, MPI_C_BOOL, MPI_LOR, 0, mpi::intracomm); } #else - std::copy(cnt.data(), cnt.data() + total, cnt_reduced); + std::copy(cnt.data(), cnt.data() + total, counts.data()); if (outside) *outside = outside_; #endif - // Adapt reduced values in array back into an xarray - auto arr = xt::adapt(cnt_reduced, total, xt::acquire_ownership(), shape); - xt::xarray counts = arr; - return counts; } @@ -1675,7 +1828,7 @@ StructuredMesh::MeshIndex CylindricalMesh::get_indices( } else { mapped_r[1] = std::atan2(r.y, r.x); if (mapped_r[1] < 0) - mapped_r[1] += 2 * M_PI; + mapped_r[1] += 2 * PI; } MeshIndex idx = StructuredMesh::get_indices(mapped_r, in_mesh); @@ -1734,18 +1887,19 @@ double CylindricalMesh::find_r_crossing( const double inv_denominator = 1.0 / denominator; const double p = (u.x * r.x + u.y * r.y) * inv_denominator; - double c = r.x * r.x + r.y * r.y - r0 * r0; - double D = p * p - c * inv_denominator; + double R = std::sqrt(r.x * r.x + r.y * r.y); + double D = p * p - (R - r0) * (R + r0) * inv_denominator; if (D < 0.0) return INFTY; D = std::sqrt(D); - // the solution -p - D is always smaller as -p + D : Check this one first - if (std::abs(c) <= RADIAL_MESH_TOL) + // Particle is already on the shell surface; avoid spurious crossing + if (std::abs(R - r0) <= RADIAL_MESH_TOL * (1.0 + std::abs(r0))) return INFTY; + // Check -p - D first because it is always smaller as -p + D if (-p - D > l) return -p - D; if (-p + D > l) @@ -1968,7 +2122,7 @@ StructuredMesh::MeshIndex SphericalMesh::get_indices( mapped_r[1] = std::acos(r.z / mapped_r.x); mapped_r[2] = std::atan2(r.y, r.x); if (mapped_r[2] < 0) - mapped_r[2] += 2 * M_PI; + mapped_r[2] += 2 * PI; } MeshIndex idx = StructuredMesh::get_indices(mapped_r, in_mesh); @@ -2019,15 +2173,16 @@ double SphericalMesh::find_r_crossing( if (r0 == 0.0) return INFTY; const double p = r.dot(u); - double c = r.dot(r) - r0 * r0; - double D = p * p - c; + double R = r.norm(); + double D = p * p - (R - r0) * (R + r0); - if (std::abs(c) <= RADIAL_MESH_TOL) + // Particle is already on the shell surface; avoid spurious crossing + if (std::abs(R - r0) <= RADIAL_MESH_TOL * (1.0 + std::abs(r0))) return INFTY; if (D >= 0.0) { D = std::sqrt(D); - // the solution -p - D is always smaller as -p + D : Check this one first + // Check -p - D first because it is always smaller as -p + D if (-p - D > l) return -p - D; if (-p + D > l) @@ -2414,26 +2569,26 @@ extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) BoundingBox bbox = model::meshes[index]->bounding_box(); // set lower left corner values - ll[0] = bbox.xmin; - ll[1] = bbox.ymin; - ll[2] = bbox.zmin; + ll[0] = bbox.min.x; + ll[1] = bbox.min.y; + ll[2] = bbox.min.z; // set upper right corner values - ur[0] = bbox.xmax; - ur[1] = bbox.ymax; - ur[2] = bbox.zmax; + ur[0] = bbox.max.x; + ur[1] = bbox.max.y; + ur[2] = bbox.max.z; return 0; } extern "C" int openmc_mesh_material_volumes(int32_t index, int nx, int ny, - int nz, int table_size, int32_t* materials, double* volumes) + int nz, int table_size, int32_t* materials, double* volumes, double* bboxes) { if (int err = check_mesh(index)) return err; try { model::meshes[index]->material_volumes( - nx, ny, nz, table_size, materials, volumes); + nx, ny, nz, table_size, materials, volumes, bboxes); } catch (const std::exception& e) { set_errmsg(e.what()); if (starts_with(e.what(), "Mesh")) { @@ -2537,7 +2692,7 @@ extern "C" int openmc_regular_mesh_get_params( return err; RegularMesh* m = dynamic_cast(model::meshes[index].get()); - if (m->lower_left_.dimension() == 0) { + if (m->lower_left_.empty()) { set_errmsg("Mesh parameters have not been set."); return OPENMC_E_ALLOCATE; } @@ -2564,17 +2719,17 @@ extern "C" int openmc_regular_mesh_set_params( vector shape = {static_cast(n)}; if (ll && ur) { - m->lower_left_ = xt::adapt(ll, n, xt::no_ownership(), shape); - m->upper_right_ = xt::adapt(ur, n, xt::no_ownership(), shape); - m->width_ = (m->upper_right_ - m->lower_left_) / m->get_x_shape(); + m->lower_left_ = tensor::Tensor(ll, n); + m->upper_right_ = tensor::Tensor(ur, n); + m->width_ = (m->upper_right_ - m->lower_left_) / m->get_shape_tensor(); } else if (ll && width) { - m->lower_left_ = xt::adapt(ll, n, xt::no_ownership(), shape); - m->width_ = xt::adapt(width, n, xt::no_ownership(), shape); - m->upper_right_ = m->lower_left_ + m->get_x_shape() * m->width_; + m->lower_left_ = tensor::Tensor(ll, n); + m->width_ = tensor::Tensor(width, n); + m->upper_right_ = m->lower_left_ + m->get_shape_tensor() * m->width_; } else if (ur && width) { - m->upper_right_ = xt::adapt(ur, n, xt::no_ownership(), shape); - m->width_ = xt::adapt(width, n, xt::no_ownership(), shape); - m->lower_left_ = m->upper_right_ - m->get_x_shape() * m->width_; + m->upper_right_ = tensor::Tensor(ur, n); + m->width_ = tensor::Tensor(width, n); + m->lower_left_ = m->upper_right_ - m->get_shape_tensor() * m->width_; } else { set_errmsg("At least two parameters must be specified."); return OPENMC_E_INVALID_ARGUMENT; @@ -2584,7 +2739,7 @@ extern "C" int openmc_regular_mesh_set_params( // TODO: incorporate this into method in RegularMesh that can be called from // here and from constructor - m->volume_frac_ = 1.0 / xt::prod(m->get_x_shape())(); + m->volume_frac_ = 1.0 / m->get_shape_tensor().prod(); m->element_volume_ = 1.0; for (int i = 0; i < m->n_dimension_; i++) { m->element_volume_ *= m->width_[i]; @@ -2633,7 +2788,7 @@ int openmc_structured_mesh_get_grid_impl(int32_t index, double** grid_x, return err; C* m = dynamic_cast(model::meshes[index].get()); - if (m->lower_left_.dimension() == 0) { + if (m->lower_left_.empty()) { set_errmsg("Mesh parameters have not been set."); return OPENMC_E_ALLOCATE; } @@ -3435,7 +3590,7 @@ LibMesh::LibMesh(hid_t group) : UnstructuredMesh(group) // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) { - if (!dynamic_cast(&input_mesh)) { + if (!input_mesh.is_replicated()) { fatal_error("At present LibMesh tallies require a replicated mesh. Please " "ensure 'input_mesh' is a libMesh::ReplicatedMesh."); } @@ -3483,9 +3638,6 @@ void LibMesh::initialize() // assuming that unstructured meshes used in OpenMC are 3D n_dimension_ = 3; - if (length_multiplier_ > 0.0) { - libMesh::MeshTools::Modification::scale(*m_, length_multiplier_); - } // if OpenMC is managing the libMesh::MeshBase instance, prepare the mesh. // Otherwise assume that it is prepared by its owning application if (unique_m_) { @@ -3513,8 +3665,15 @@ void LibMesh::initialize() bbox_ = libMesh::MeshTools::create_bounding_box(*m_); libMesh::Point ll = bbox_.min(); libMesh::Point ur = bbox_.max(); - lower_left_ = {ll(0), ll(1), ll(2)}; - upper_right_ = {ur(0), ur(1), ur(2)}; + if (length_multiplier_ > 0.0) { + lower_left_ = {length_multiplier_ * ll(0), length_multiplier_ * ll(1), + length_multiplier_ * ll(2)}; + upper_right_ = {length_multiplier_ * ur(0), length_multiplier_ * ur(1), + length_multiplier_ * ur(2)}; + } else { + lower_left_ = {ll(0), ll(1), ll(2)}; + upper_right_ = {ur(0), ur(1), ur(2)}; + } } // Sample position within a tet for LibMesh type tets @@ -3524,18 +3683,27 @@ Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const // Get tet vertex coordinates from LibMesh std::array tet_verts; for (int i = 0; i < elem.n_nodes(); i++) { - auto node_ref = elem.node_ref(i); + const auto& node_ref = elem.node_ref(i); tet_verts[i] = {node_ref(0), node_ref(1), node_ref(2)}; } // Samples position within tet using Barycentric coordinates - return this->sample_tet(tet_verts, seed); + Position sampled_position = this->sample_tet(tet_verts, seed); + if (length_multiplier_ > 0.0) { + return length_multiplier_ * sampled_position; + } else { + return sampled_position; + } } Position LibMesh::centroid(int bin) const { const auto& elem = this->get_element_from_bin(bin); auto centroid = elem.vertex_average(); - return {centroid(0), centroid(1), centroid(2)}; + if (length_multiplier_ > 0.0) { + return length_multiplier_ * Position(centroid(0), centroid(1), centroid(2)); + } else { + return {centroid(0), centroid(1), centroid(2)}; + } } int LibMesh::n_vertices() const @@ -3545,8 +3713,12 @@ int LibMesh::n_vertices() const Position LibMesh::vertex(int vertex_id) const { - const auto node_ref = m_->node_ref(vertex_id); - return {node_ref(0), node_ref(1), node_ref(2)}; + const auto& node_ref = m_->node_ref(vertex_id); + if (length_multiplier_ > 0.0) { + return length_multiplier_ * Position(node_ref(0), node_ref(1), node_ref(2)); + } else { + return {node_ref(0), node_ref(1), node_ref(2)}; + } } std::vector LibMesh::connectivity(int elem_id) const @@ -3687,6 +3859,11 @@ int LibMesh::get_bin(Position r) const // look-up a tet using the point locator libMesh::Point p(r.x, r.y, r.z); + if (length_multiplier_ > 0.0) { + // Scale the point down + p /= length_multiplier_; + } + // quick rejection check if (!bbox_.contains_point(p)) { return -1; @@ -3720,22 +3897,32 @@ const libMesh::Elem& LibMesh::get_element_from_bin(int bin) const double LibMesh::volume(int bin) const { - return this->get_element_from_bin(bin).volume(); + return this->get_element_from_bin(bin).volume() * length_multiplier_ * + length_multiplier_ * length_multiplier_; } -AdaptiveLibMesh::AdaptiveLibMesh( - libMesh::MeshBase& input_mesh, double length_multiplier) - : LibMesh(input_mesh, length_multiplier), num_active_(m_->n_active_elem()) +AdaptiveLibMesh::AdaptiveLibMesh(libMesh::MeshBase& input_mesh, + double length_multiplier, + const std::set& block_ids) + : LibMesh(input_mesh, length_multiplier), block_ids_(block_ids), + block_restrict_(!block_ids_.empty()), + num_active_( + block_restrict_ + ? std::distance(m_->active_subdomain_set_elements_begin(block_ids_), + m_->active_subdomain_set_elements_end(block_ids_)) + : m_->n_active_elem()) { // if the mesh is adaptive elements aren't guaranteed by libMesh to be // contiguous in ID space, so we need to map from bin indices (defined over // active elements) to global dof ids bin_to_elem_map_.reserve(num_active_); elem_to_bin_map_.resize(m_->n_elem(), -1); - for (auto it = m_->active_elements_begin(); it != m_->active_elements_end(); - it++) { - auto elem = *it; - + auto begin = block_restrict_ + ? m_->active_subdomain_set_elements_begin(block_ids_) + : m_->active_elements_begin(); + auto end = block_restrict_ ? m_->active_subdomain_set_elements_end(block_ids_) + : m_->active_elements_end(); + for (const auto& elem : libMesh::as_range(begin, end)) { bin_to_elem_map_.push_back(elem->id()); elem_to_bin_map_[elem->id()] = bin_to_elem_map_.size() - 1; } @@ -3768,6 +3955,27 @@ void AdaptiveLibMesh::write(const std::string& filename) const this->id_)); } +int AdaptiveLibMesh::get_bin(Position r) const +{ + // look-up a tet using the point locator + libMesh::Point p(r.x, r.y, r.z); + + if (length_multiplier_ > 0.0) { + // Scale the point down + p /= length_multiplier_; + } + + // quick rejection check + if (!bbox_.contains_point(p)) { + return -1; + } + + const auto& point_locator = pl_.at(thread_num()); + + const auto elem_ptr = (*point_locator)(p, &block_ids_); + return elem_ptr ? get_bin_from_element(elem_ptr) : -1; +} + int AdaptiveLibMesh::get_bin_from_element(const libMesh::Elem* elem) const { int bin = elem_to_bin_map_[elem->id()]; diff --git a/src/message_passing.cpp b/src/message_passing.cpp index a160f6d73..cf6113ed6 100644 --- a/src/message_passing.cpp +++ b/src/message_passing.cpp @@ -39,6 +39,14 @@ vector calculate_parallel_index_vector(int64_t size) return result; } +#ifdef OPENMC_MPI +// Specializations of the MPITypeMap template struct +template<> +const MPI_Datatype MPITypeMap::mpi_type = MPI_INT; +template<> +const MPI_Datatype MPITypeMap::mpi_type = MPI_DOUBLE; +#endif + } // namespace mpi } // namespace openmc diff --git a/src/mgxs.cpp b/src/mgxs.cpp index a2c479f21..1dc090ab9 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -5,15 +5,13 @@ #include #include -#include "xtensor/xadapt.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xsort.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/mgxs_interface.h" +#include "openmc/nuclide.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" #include "openmc/string_utils.h" @@ -32,8 +30,7 @@ void Mgxs::init(const std::string& in_name, double in_awr, // Set the metadata name = in_name; awr = in_awr; - // TODO: Remove adapt when in_KTs is an xtensor - kTs = xt::adapt(in_kTs); + kTs = tensor::Tensor(in_kTs.data(), in_kTs.size()); fissionable = in_fissionable; scatter_format = in_scatter_format; xs.resize(in_kTs.size()); @@ -72,7 +69,7 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, } get_datasets(kT_group, dset_names); vector shape = {num_temps}; - xt::xarray temps_available(shape); + tensor::Tensor temps_available(shape); for (int i = 0; i < num_temps; i++) { read_double(kT_group, dset_names[i], &temps_available[i], true); @@ -85,6 +82,13 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, delete[] dset_names; std::sort(temps_available.begin(), temps_available.end()); + // Set the global upper and lower interpolation bounds to avoid errors + // involving C-API functions. + data::temperature_min = + std::min(data::temperature_min, temps_available.front()); + data::temperature_max = + std::max(data::temperature_max, temps_available.back()); + // If only one temperature is available, lets just use nearest temperature // interpolation if ((num_temps == 1) && @@ -100,19 +104,7 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, // Determine actual temperatures to read for (const auto& T : temperature) { // Determine the closest temperature value - // NOTE: the below block could be replaced with the following line, - // though this gives a runtime error if using LLVM 20 or newer, - // likely due to a bug in xtensor. - // auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; - double closest = std::numeric_limits::max(); - int i_closest = 0; - for (int i = 0; i < temps_available.size(); i++) { - double diff = std::abs(temps_available[i] - T); - if (diff < closest) { - closest = diff; - i_closest = i; - } - } + auto i_closest = tensor::abs(temps_available - T).argmin(); double temp_actual = temps_available[i_closest]; if (std::fabs(temp_actual - T) < settings::temperature_tolerance) { @@ -347,7 +339,7 @@ Mgxs::Mgxs(const std::string& in_name, const vector& mat_kTs, for (int m = 0; m < micros.size(); m++) { switch (settings::temperature_method) { case TemperatureMethod::NEAREST: { - micro_t[m] = xt::argmin(xt::abs(micros[m]->kTs - temp_desired))[0]; + micro_t[m] = tensor::abs(micros[m]->kTs - temp_desired).argmin(); auto temp_actual = micros[m]->kTs[micro_t[m]]; if (std::abs(temp_actual - temp_desired) >= @@ -360,7 +352,7 @@ Mgxs::Mgxs(const std::string& in_name, const vector& mat_kTs, case TemperatureMethod::INTERPOLATION: // Get a list of bounding temperatures for each actual temperature // present in the model - for (int k = 0; k < micros[m]->kTs.shape()[0] - 1; k++) { + for (int k = 0; k < micros[m]->kTs.shape(0) - 1; k++) { if ((micros[m]->kTs[k] <= temp_desired) && (temp_desired < micros[m]->kTs[k + 1])) { micro_t[m] = k; @@ -373,7 +365,7 @@ Mgxs::Mgxs(const std::string& in_name, const vector& mat_kTs, } } } // end switch - } // end microscopic temperature loop + } // end microscopic temperature loop // Now combine the microscopic data at each relevant temperature // We will do this by treating the multiple temperatures of a nuclide as @@ -466,7 +458,7 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, val = xs_t->delayed_nu_fission(a, *dg, gin); } else { val = 0.; - for (int d = 0; d < xs_t->delayed_nu_fission.shape()[1]; d++) { + for (int d = 0; d < xs_t->delayed_nu_fission.shape(1); d++) { val += xs_t->delayed_nu_fission(a, d, gin); } } @@ -481,7 +473,7 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, } else { // provide an outgoing group-wise sum val = 0.; - for (int g = 0; g < xs_t->chi_prompt.shape()[2]; g++) { + for (int g = 0; g < xs_t->chi_prompt.shape(2); g++) { val += xs_t->chi_prompt(a, gin, g); } } @@ -500,13 +492,13 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, } else { if (dg != nullptr) { val = 0.; - for (int g = 0; g < xs_t->delayed_nu_fission.shape()[2]; g++) { + for (int g = 0; g < xs_t->delayed_nu_fission.shape(2); g++) { val += xs_t->delayed_nu_fission(a, *dg, gin, g); } } else { val = 0.; - for (int g = 0; g < xs_t->delayed_nu_fission.shape()[2]; g++) { - for (int d = 0; d < xs_t->delayed_nu_fission.shape()[3]; d++) { + for (int g = 0; g < xs_t->delayed_nu_fission.shape(2); g++) { + for (int d = 0; d < xs_t->delayed_nu_fission.shape(3); d++) { val += xs_t->delayed_nu_fission(a, d, gin, g); } } @@ -518,6 +510,8 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, break; case MgxsType::INVERSE_VELOCITY: val = xs_t->inverse_velocity(a, gin); + if (!(val > 0)) + val = data::mg.default_inverse_velocity_[gin]; break; case MgxsType::DECAY_RATE: if (dg != nullptr) { @@ -642,7 +636,7 @@ bool Mgxs::equiv(const Mgxs& that) int Mgxs::get_temperature_index(double sqrtkT) const { - return xt::argmin(xt::abs(kTs - sqrtkT * sqrtkT))[0]; + return tensor::abs(kTs - sqrtkT * sqrtkT).argmin(); } //============================================================================== diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index ed734d401..865c56580 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -169,13 +169,13 @@ vector> MgxsInterface::get_mat_kTs() continue; // Get temperature of cell (rounding to nearest integer) - double sqrtkT = - cell->sqrtkT_.size() == 1 ? cell->sqrtkT_[j] : cell->sqrtkT_[0]; - double kT = sqrtkT * sqrtkT; + for (int k = 0; k < cell->sqrtkT_.size(); ++k) { + double kT = cell->sqrtkT_[k] * cell->sqrtkT_[k]; - // Add temperature if it hasn't already been added - if (!contains(kTs[i_material], kT)) { - kTs[i_material].push_back(kT); + // Add temperature if it hasn't already been added + if (!contains(kTs[i_material], kT)) { + kTs[i_material].push_back(kT); + } } } } @@ -237,6 +237,19 @@ void MgxsInterface::read_header(const std::string& path_cross_sections) "library file!"); } + // Calculate approximate default inverse velocity data + for (int i = 0; i < energy_bins_.size() - 1; ++i) { + double e_min = std::max(energy_bins_[i + 1], 1e-5); + double e_max = energy_bins_[i]; + double alpha = 1.0 / (C_LIGHT * std::log(e_max / e_min)); + double k_max = std::sqrt(1 + 2.0 * MASS_NEUTRON_EV / e_max); + double k_min = std::sqrt(1 + 2.0 * MASS_NEUTRON_EV / e_min); + double inv_v = + alpha * (2.0 * (std::atanh(1.0 / k_max) - std::atanh(1.0 / k_min)) - + (k_max - k_min)); + default_inverse_velocity_.push_back(inv_v); + } + // Close MGXS HDF5 file file_close(file_id); } @@ -244,7 +257,7 @@ void MgxsInterface::read_header(const std::string& path_cross_sections) void put_mgxs_header_data_to_globals() { // Get the minimum and maximum energies - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); data::energy_min[neutron] = data::mg.energy_bins_.back(); data::energy_max[neutron] = data::mg.energy_bins_.front(); diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 5ae6e30ee..17d6e952c 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -17,8 +17,7 @@ #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include // for sort, min_element #include @@ -361,8 +360,7 @@ void Nuclide::create_derived( { for (const auto& grid : grid_) { // Allocate and initialize cross section - array shape {grid.energy.size(), 5}; - xs_.emplace_back(shape, 0.0); + xs_.push_back(tensor::zeros({grid.energy.size(), 5})); } reaction_index_.fill(C_NONE); @@ -375,11 +373,10 @@ void Nuclide::create_derived( for (int t = 0; t < kTs_.size(); ++t) { int j = rx->xs_[t].threshold; int n = rx->xs_[t].value.size(); - auto xs = xt::adapt(rx->xs_[t].value); - auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD); - + auto xs = tensor::Tensor( + rx->xs_[t].value.data(), rx->xs_[t].value.size()); for (const auto& p : rx->products_) { - if (p.particle_ == ParticleType::photon) { + if (p.particle_.is_photon()) { for (int k = 0; k < n; ++k) { double E = grid_[t].energy[k + j]; @@ -396,7 +393,7 @@ void Nuclide::create_derived( } } - pprod[k] += f * xs[k] * (*p.yield_)(E); + xs_[t](j + k, XS_PHOTON_PROD) += f * xs[k] * (*p.yield_)(E); } } } @@ -406,20 +403,17 @@ void Nuclide::create_derived( continue; // Add contribution to total cross section - auto total = xt::view(xs_[t], xt::range(j, j + n), XS_TOTAL); - total += xs; + xs_[t].slice(tensor::range(j, j + n), XS_TOTAL) += xs; // Add contribution to absorption cross section - auto absorption = xt::view(xs_[t], xt::range(j, j + n), XS_ABSORPTION); if (is_disappearance(rx->mt_)) { - absorption += xs; + xs_[t].slice(tensor::range(j, j + n), XS_ABSORPTION) += xs; } if (is_fission(rx->mt_)) { fissionable_ = true; - auto fission = xt::view(xs_[t], xt::range(j, j + n), XS_FISSION); - fission += xs; - absorption += xs; + xs_[t].slice(tensor::range(j, j + n), XS_FISSION) += xs; + xs_[t].slice(tensor::range(j, j + n), XS_ABSORPTION) += xs; // Keep track of fission reactions if (t == 0) { @@ -501,7 +495,7 @@ void Nuclide::create_derived( void Nuclide::init_grid() { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); double E_min = data::energy_min[neutron]; double E_max = data::energy_max[neutron]; int M = settings::n_log_bins; @@ -510,7 +504,7 @@ void Nuclide::init_grid() double spacing = std::log(E_max / E_min) / M; // Create equally log-spaced energy grid - auto umesh = xt::linspace(0.0, M * spacing, M + 1); + auto umesh = tensor::linspace(0.0, M * spacing, M + 1); for (auto& grid : grid_) { // Resize array for storing grid indices @@ -552,7 +546,7 @@ double Nuclide::nu(double E, EmissionMode mode, int group) const for (int i = 1; i < rx->products_.size(); ++i) { // Skip any non-neutron products const auto& product = rx->products_[i]; - if (product.particle_ != ParticleType::neutron) + if (!product.particle_.is_neutron()) continue; // Evaluate yield diff --git a/src/output.cpp b/src/output.cpp index 0a14e8843..04a8caa60 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -17,7 +17,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/capi.h" #include "openmc/cell.h" @@ -44,6 +44,12 @@ namespace openmc { +#ifdef OPENMC_ENABLE_STRICT_FP +const bool STRICT_FP_ENABLED = true; +#else +const bool STRICT_FP_ENABLED = false; +#endif + //============================================================================== void title() @@ -75,7 +81,7 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2025 MIT, UChicago Argonne LLC, and contributors\n" + " Copyright | 2011-2026 MIT, UChicago Argonne LLC, and contributors\n" " License | https://docs.openmc.org/en/latest/license.html\n" " Version | {}.{}.{}{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : "", @@ -155,21 +161,21 @@ std::string time_stamp() void print_particle(Particle& p) { // Display particle type and ID. - switch (p.type()) { - case ParticleType::neutron: + switch (p.type().pdg_number()) { + case PDG_NEUTRON: fmt::print("Neutron "); break; - case ParticleType::photon: + case PDG_PHOTON: fmt::print("Photon "); break; - case ParticleType::electron: + case PDG_ELECTRON: fmt::print("Electron "); break; - case ParticleType::positron: + case PDG_POSITRON: fmt::print("Positron "); break; default: - fmt::print("Unknown Particle "); + fmt::print("Particle {} ", p.type().str()); } fmt::print("{}\n", p.id()); @@ -281,6 +287,7 @@ void print_usage() " -t, --track Write tracks for all particles (up to " "max_tracks)\n" " -e, --event Run using event-based parallelism\n" + " -q, --verbosity Output verbosity\n" " -v, --version Show version information\n" " -h, --help Show this message\n"); } @@ -294,7 +301,7 @@ void print_version() fmt::print("OpenMC version {}.{}.{}{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : "", VERSION_COMMIT_COUNT); fmt::print("Commit hash: {}\n", VERSION_COMMIT_HASH); - fmt::print("Copyright (c) 2011-2025 MIT, UChicago Argonne LLC, and " + fmt::print("Copyright (c) 2011-2026 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } @@ -314,8 +321,8 @@ void print_build_info() std::string png(n); std::string profiling(n); std::string coverage(n); - std::string mcpl(n); std::string uwuw(n); + std::string strict_fp(n); #ifdef PHDF5 phdf5 = y; @@ -329,9 +336,6 @@ void print_build_info() #ifdef OPENMC_LIBMESH_ENABLED libmesh = y; #endif -#ifdef OPENMC_MCPL - mcpl = y; -#endif #ifdef USE_LIBPNG png = y; #endif @@ -344,6 +348,9 @@ void print_build_info() #ifdef OPENMC_UWUW_ENABLED uwuw = y; #endif +#ifdef OPENMC_ENABLE_STRICT_FP + strict_fp = y; +#endif // Wraps macro variables in quotes #define STRINGIFY(x) STRINGIFY2(x) @@ -358,10 +365,10 @@ void print_build_info() fmt::print("PNG support: {}\n", png); fmt::print("DAGMC support: {}\n", dagmc); fmt::print("libMesh support: {}\n", libmesh); - fmt::print("MCPL support: {}\n", mcpl); fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); fmt::print("UWUW support: {}\n", uwuw); + fmt::print("Strict FP: {}\n", strict_fp); } } @@ -494,6 +501,14 @@ void print_runtime() show_rate("Calculation Rate (inactive)", speed_inactive); } show_rate("Calculation Rate (active)", speed_active); + + // Display track rate when weight windows are enabled + if (settings::weight_windows_on) { + double speed_tracks = + simulation::simulation_tracks_completed / time_active.elapsed(); + fmt::print( + " {:<33} = {:.6} tracks/second\n", "Track Rate (active)", speed_tracks); + } } //============================================================================== @@ -606,8 +621,15 @@ void write_tallies() if (model::tallies.empty()) return; + // Tag tallies.out written during the forward solve of an adjoint run + const char* forward = + (FlatSourceDomain::solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) + ? "forward." + : ""; + // Set filename for tallies_out - std::string filename = fmt::format("{}tallies.out", settings::path_output); + std::string filename = + fmt::format("{}tallies.{}out", settings::path_output, forward); // Open the tallies.out file. std::ofstream tallies_out; diff --git a/src/particle.cpp b/src/particle.cpp index 2d70d715e..10e8f213d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -14,6 +14,7 @@ #include "openmc/error.h" #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" +#include "openmc/lattice.h" #include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -47,28 +48,33 @@ double Particle::speed() const { if (settings::run_CE) { // Determine mass in eV/c^2 - double mass; - switch (this->type()) { - case ParticleType::neutron: - mass = MASS_NEUTRON_EV; - break; - case ParticleType::photon: - mass = 0.0; - break; - case ParticleType::electron: - case ParticleType::positron: - mass = MASS_ELECTRON_EV; - break; - } + double mass = this->mass(); + // Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<E() * (this->E() + 2 * mass)) / (this->E() + mass); } else { - auto& macro_xs = data::mg.macro_xs_[this->material()]; + auto mat = this->material(); + if (mat == MATERIAL_VOID) + return 1.0 / data::mg.default_inverse_velocity_[this->g()]; + auto& macro_xs = data::mg.macro_xs_[mat]; int macro_t = this->mg_xs_cache().t; int macro_a = macro_xs.get_angle_index(this->u()); - return 1.0 / macro_xs.get_xs(MgxsType::INVERSE_VELOCITY, this->g(), nullptr, - nullptr, nullptr, macro_t, macro_a); + return 1.0 / macro_xs.get_xs( + MgxsType::INVERSE_VELOCITY, this->g(), macro_t, macro_a); + } +} + +double Particle::mass() const +{ + switch (type().pdg_number()) { + case PDG_NEUTRON: + return MASS_NEUTRON_EV; + case PDG_ELECTRON: + case PDG_POSITRON: + return MASS_ELECTRON_EV; + default: + return this->type().mass() * AMU_EV; } } @@ -77,11 +83,18 @@ bool Particle::create_secondary( { // If energy is below cutoff for this particle, don't create secondary // particle - if (E < settings::energy_cutoff[static_cast(type)]) { + int idx = type.transport_index(); + if (idx == C_NONE) { + return false; + } + if (E < settings::energy_cutoff[idx]) { return false; } - auto& bank = secondary_bank().emplace_back(); + // Increment number of secondaries created (for ParticleProductionFilter) + n_secondaries()++; + + SourceSite bank; bank.particle = type; bank.wgt = wgt; bank.r = r(); @@ -89,12 +102,21 @@ bool Particle::create_secondary( bank.E = settings::run_CE ? E : g(); bank.time = time(); bank_second_E() += bank.E; + bank.parent_id = current_work(); + if (settings::use_shared_secondary_bank) { + bank.progeny_id = n_progeny()++; + } + bank.wgt_born = wgt_born(); + bank.wgt_ww_born = wgt_ww_born(); + bank.n_split = n_split(); + + local_secondary_bank().emplace_back(bank); return true; } void Particle::split(double wgt) { - auto& bank = secondary_bank().emplace_back(); + SourceSite bank; bank.particle = type(); bank.wgt = wgt; bank.r = r(); @@ -109,6 +131,16 @@ void Particle::split(double wgt) int surf_id = model::surfaces[surface_index()]->id_; bank.surf_id = (surface() > 0) ? surf_id : -surf_id; } + + bank.wgt_born = wgt_born(); + bank.wgt_ww_born = wgt_ww_born(); + bank.n_split = n_split(); + bank.parent_id = current_work(); + if (settings::use_shared_secondary_bank) { + bank.progeny_id = n_progeny()++; + } + + local_secondary_bank().emplace_back(bank); } void Particle::from_source(const SourceSite* src) @@ -155,6 +187,10 @@ void Particle::from_source(const SourceSite* src) int index_plus_one = model::surface_map[std::abs(src->surf_id)] + 1; surface() = (src->surf_id > 0) ? index_plus_one : -index_plus_one; } + + wgt_born() = src->wgt_born; + wgt_ww_born() = src->wgt_ww_born; + n_split() = src->n_split; } void Particle::event_calculate_xs() @@ -235,7 +271,8 @@ void Particle::event_advance() boundary() = distance_to_boundary(*this); // Sample a distance to collision - if (type() == ParticleType::electron || type() == ParticleType::positron) { + if (type() == ParticleType::electron() || + type() == ParticleType::positron()) { collision_distance() = material() == MATERIAL_VOID ? INFINITY : 0.0; } else if (macro_xs().total == 0.0) { collision_distance() = INFINITY; @@ -244,7 +281,7 @@ void Particle::event_advance() } double speed = this->speed(); - double time_cutoff = settings::time_cutoff[static_cast(type())]; + double time_cutoff = settings::time_cutoff[type().transport_index()]; double distance_cutoff = (time_cutoff < INFTY) ? (time_cutoff - time()) * speed : INFTY; @@ -269,8 +306,7 @@ void Particle::event_advance() } // Score track-length estimate of k-eff - if (settings::run_mode == RunMode::EIGENVALUE && - type() == ParticleType::neutron) { + if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission; } @@ -302,37 +338,56 @@ void Particle::event_cross_surface() boundary().lattice_translation()[2] != 0) { // Particle crosses lattice boundary + int i_lattice = coord(boundary().coord_level() - 1).lattice(); bool verbose = settings::verbosity >= 10 || trace(); cross_lattice(*this, boundary(), verbose); event() = TallyEvent::LATTICE; - } else { - // Particle crosses surface - const auto& surf {model::surfaces[surface_index()].get()}; - // If BC, add particle to surface source before crossing surface - if (surf->surf_source_ && surf->bc_) { - add_surf_source_to_bank(*this, *surf); + + // Score cell to cell partial currents + if (!model::active_surface_tallies.empty()) { + auto& lat {*model::lattices[i_lattice]}; + bool is_valid; + Direction normal = + lat.get_normal(boundary().lattice_translation(), is_valid); + if (is_valid) { + normal /= normal.norm(); + score_surface_tally(*this, model::active_surface_tallies, normal); + } } - this->cross_surface(*surf); + + } else { + + const auto& surf {*model::surfaces[surface_index()].get()}; + + // Particle crosses surface + // If BC, add particle to surface source before crossing surface + if (surf.surf_source_ && surf.bc_) { + add_surf_source_to_bank(*this, surf); + } + this->cross_surface(surf); // If no BC, add particle to surface source after crossing surface - if (surf->surf_source_ && !surf->bc_) { - add_surf_source_to_bank(*this, *surf); + if (surf.surf_source_ && !surf.bc_) { + add_surf_source_to_bank(*this, surf); } if (settings::weight_window_checkpoint_surface) { apply_weight_windows(*this); } event() = TallyEvent::SURFACE; - } - // Score cell to cell partial currents - if (!model::active_surface_tallies.empty()) { - score_surface_tally(*this, model::active_surface_tallies); + + // Score cell to cell partial currents + if (!model::active_surface_tallies.empty()) { + Direction normal = surf.normal(r()); + normal /= normal.norm(); + score_surface_tally(*this, model::active_surface_tallies, normal); + } } } void Particle::event_collide() { + // Score collision estimate of keff - if (settings::run_mode == RunMode::EIGENVALUE && - type() == ParticleType::neutron) { + if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total; } @@ -341,7 +396,7 @@ void Particle::event_collide() // pre-collision direction to figure out what mesh surfaces were crossed if (!model::active_meshsurf_tallies.empty()) - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); // Clear surface component surface() = SURFACE_NONE; @@ -370,8 +425,7 @@ void Particle::event_collide() } } - if (!model::active_pulse_height_tallies.empty() && - type() == ParticleType::photon) { + if (!model::active_pulse_height_tallies.empty() && type().is_photon()) { pht_collision_energy(); } @@ -379,6 +433,11 @@ void Particle::event_collide() n_bank() = 0; bank_second_E() = 0.0; wgt_bank() = 0.0; + + // Clear number of secondaries in this collision. This is + // distinct from the number of created neutrons n_bank() above! + n_secondaries() = 0; + zero_delayed_bank(); // Reset fission logical @@ -414,61 +473,72 @@ void Particle::event_collide() #endif } -void Particle::event_revive_from_secondary() +void Particle::event_revive_from_secondary(const SourceSite& site) +{ + // Write final position for the previous track (skip if this is a freshly + // constructed particle with no prior track, e.g., Phase 2 of shared + // secondary transport) + if (write_track() && n_event() > 0) { + write_particle_track(*this); + } + + from_source(&site); + + n_event() = 0; + if (!settings::use_shared_secondary_bank) { + n_tracks()++; + } + bank_second_E() = 0.0; + + // Subtract secondary particle energy from interim pulse-height results. + // In shared secondary mode, this subtraction was already done on the parent + // particle during create_secondary(), so skip it here. + if (!settings::use_shared_secondary_bank && + !model::active_pulse_height_tallies.empty() && this->type().is_photon()) { + // Since the birth cell of the particle has not been set we + // have to determine it before the energy of the secondary particle can be + // removed from the pulse-height of this cell. + if (lowest_coord().cell() == C_NONE) { + bool verbose = settings::verbosity >= 10 || trace(); + if (!exhaustive_find_cell(*this, verbose)) { + mark_as_lost("Could not find the cell containing particle " + + std::to_string(id())); + return; + } + // Set birth cell attribute + if (cell_born() == C_NONE) + cell_born() = lowest_coord().cell(); + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell(); + } + n_coord_last() = n_coord(); + } + pht_secondary_particles(); + } + + // Enter new particle in particle track file + if (write_track()) + add_particle_track(*this); +} + +void Particle::event_check_limit_and_revive() { // If particle has too many events, display warning and kill it - ++n_event(); + n_event()++; if (n_event() == settings::max_particle_events) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; } - // Check for secondary particles if this particle is dead - if (!alive()) { - // Write final position for this particle - if (write_track()) { - write_particle_track(*this); - } - - // If no secondary particles, break out of event loop - if (secondary_bank().empty()) - return; - - from_source(&secondary_bank().back()); - secondary_bank().pop_back(); - n_event() = 0; - bank_second_E() = 0.0; - - // Subtract secondary particle energy from interim pulse-height results - if (!model::active_pulse_height_tallies.empty() && - this->type() == ParticleType::photon) { - // Since the birth cell of the particle has not been set we - // have to determine it before the energy of the secondary particle can be - // removed from the pulse-height of this cell. - if (lowest_coord().cell() == C_NONE) { - bool verbose = settings::verbosity >= 10 || trace(); - if (!exhaustive_find_cell(*this, verbose)) { - mark_as_lost("Could not find the cell containing particle " + - std::to_string(id())); - return; - } - // Set birth cell attribute - if (cell_born() == C_NONE) - cell_born() = lowest_coord().cell(); - - // Initialize last cells from current cell - for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell(); - } - n_coord_last() = n_coord(); - } - pht_secondary_particles(); - } - - // Enter new particle in particle track file - if (write_track()) - add_particle_track(*this); + // In non-shared-secondary mode, revive from local secondary bank + if (!alive() && !settings::use_shared_secondary_bank && + !local_secondary_bank().empty()) { + SourceSite& site = local_secondary_bank().back(); + event_revive_from_secondary(site); + local_secondary_bank().pop_back(); } } @@ -480,18 +550,34 @@ void Particle::event_death() // Finish particle track output. if (write_track()) { + write_particle_track(*this); finalize_particle_track(*this); } -// Contribute tally reduction variables to global accumulator + // Contribute tally reduction variables to global accumulator + const auto k_absorption = keff_tally_absorption(); + const auto k_collision = keff_tally_collision(); + const auto k_tracklength = keff_tally_tracklength(); + const auto leakage = keff_tally_leakage(); + + if (settings::run_mode == RunMode::EIGENVALUE) { + if (k_absorption != 0.0) { #pragma omp atomic - global_tally_absorption += keff_tally_absorption(); + global_tally_absorption += k_absorption; + } + if (k_collision != 0.0) { #pragma omp atomic - global_tally_collision += keff_tally_collision(); + global_tally_collision += k_collision; + } + if (k_tracklength != 0.0) { #pragma omp atomic - global_tally_tracklength += keff_tally_tracklength(); + global_tally_tracklength += k_tracklength; + } + } + if (leakage != 0.0) { #pragma omp atomic - global_tally_leakage += keff_tally_leakage(); + global_tally_leakage += leakage; + } // Reset particle tallies once accumulated keff_tally_absorption() = 0.0; @@ -503,11 +589,17 @@ void Particle::event_death() score_pulse_height_tally(*this, model::active_pulse_height_tallies); } + // Accumulate track count for this particle history + if (!settings::use_shared_secondary_bank) { +#pragma omp atomic + simulation::simulation_tracks_completed += n_tracks(); + } + // Record the number of progeny created by this particle. // This data will be used to efficiently sort the fission bank. - if (settings::run_mode == RunMode::EIGENVALUE) { - int64_t offset = id() - 1 - simulation::work_index[mpi::rank]; - simulation::progeny_per_particle[offset] = n_progeny(); + if (settings::run_mode == RunMode::EIGENVALUE || + settings::use_shared_secondary_bank) { + simulation::progeny_per_particle[current_work()] = n_progeny(); } } @@ -525,7 +617,7 @@ void Particle::pht_collision_energy() // If the energy of the particle is below the cutoff, it will not be sampled // so its energy is added to the pulse-height in the cell - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); if (E() < settings::energy_cutoff[photon]) { pht_storage()[index] += E(); } @@ -639,7 +731,7 @@ void Particle::cross_vacuum_bc(const Surface& surf) // physically moving the particle forward slightly r() += TINY_BIT * u(); - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); } // Score to global leakage tally @@ -671,13 +763,15 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // with a mesh boundary if (!model::active_surface_tallies.empty()) { - score_surface_tally(*this, model::active_surface_tallies); + Direction normal = surf.normal(r()); + normal /= normal.norm(); + score_surface_tally(*this, model::active_surface_tallies, normal); } if (!model::active_meshsurf_tallies.empty()) { Position r {this->r()}; this->r() -= TINY_BIT * u(); - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); this->r() = r; } @@ -727,7 +821,7 @@ void Particle::cross_periodic_bc( if (!model::active_meshsurf_tallies.empty()) { Position r {this->r()}; this->r() -= TINY_BIT * u(); - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); this->r() = r; } @@ -744,9 +838,7 @@ void Particle::cross_periodic_bc( if (!neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after hitting periodic " "boundary on surface " + - std::to_string(surf.id_) + - ". The normal vector " - "of one periodic surface may need to be reversed."); + std::to_string(surf.id_) + "."); return; } @@ -826,30 +918,29 @@ void Particle::write_restart() const break; } write_dataset(file_id, "id", id()); - write_dataset(file_id, "type", static_cast(type())); + write_dataset(file_id, "type", type().pdg_number()); + // Get source site data for the particle that got lost int64_t i = current_work(); + SourceSite site; if (settings::run_mode == RunMode::EIGENVALUE) { - // take source data from primary bank for eigenvalue simulation - write_dataset(file_id, "weight", simulation::source_bank[i - 1].wgt); - write_dataset(file_id, "energy", simulation::source_bank[i - 1].E); - write_dataset(file_id, "xyz", simulation::source_bank[i - 1].r); - write_dataset(file_id, "uvw", simulation::source_bank[i - 1].u); - write_dataset(file_id, "time", simulation::source_bank[i - 1].time); + site = simulation::source_bank[i]; + } else if (settings::run_mode == RunMode::FIXED_SOURCE && + settings::use_shared_secondary_bank && + i < simulation::shared_secondary_bank_read.size()) { + site = simulation::shared_secondary_bank_read[i]; } else if (settings::run_mode == RunMode::FIXED_SOURCE) { - // re-sample using rng random number seed used to generate source particle - int64_t id = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - simulation::work_index[mpi::rank] + i; + // Re-sample using the same seed used to generate the source particle. + // current_work() is 0-indexed, compute_particle_id expects 1-indexed. + int64_t id = compute_transport_seed(compute_particle_id(i + 1)); uint64_t seed = init_seed(id, STREAM_SOURCE); - // re-sample source site - auto site = sample_external_source(&seed); - write_dataset(file_id, "weight", site.wgt); - write_dataset(file_id, "energy", site.E); - write_dataset(file_id, "xyz", site.r); - write_dataset(file_id, "uvw", site.u); - write_dataset(file_id, "time", site.time); + site = sample_external_source(&seed); } + write_dataset(file_id, "weight", site.wgt); + write_dataset(file_id, "energy", site.E); + write_dataset(file_id, "xyz", site.r); + write_dataset(file_id, "uvw", site.u); + write_dataset(file_id, "time", site.time); // Close file file_close(file_id); @@ -880,37 +971,6 @@ void Particle::update_neutron_xs( //============================================================================== // Non-method functions //============================================================================== - -std::string particle_type_to_str(ParticleType type) -{ - switch (type) { - case ParticleType::neutron: - return "neutron"; - case ParticleType::photon: - return "photon"; - case ParticleType::electron: - return "electron"; - case ParticleType::positron: - return "positron"; - } - UNREACHABLE(); -} - -ParticleType str_to_particle_type(std::string str) -{ - if (str == "neutron") { - return ParticleType::neutron; - } else if (str == "photon") { - return ParticleType::photon; - } else if (str == "electron") { - return ParticleType::electron; - } else if (str == "positron") { - return ParticleType::positron; - } else { - throw std::invalid_argument {fmt::format("Invalid particle name: {}", str)}; - } -} - void add_surf_source_to_bank(Particle& p, const Surface& surf) { if (simulation::current_batch <= settings::n_inactive || diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index 6b7778211..c226d51ec 100644 --- a/src/particle_restart.cpp +++ b/src/particle_restart.cpp @@ -1,6 +1,7 @@ #include "openmc/particle_restart.h" #include "openmc/array.h" +#include "openmc/bank.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" #include "openmc/mgxs_interface.h" @@ -31,6 +32,16 @@ void read_particle_restart(Particle& p, RunMode& previous_run_mode) hid_t file_id = file_open(settings::path_particle_restart, 'r'); // Read data from file + bool legacy_particle_codes = true; + if (attribute_exists(file_id, "version")) { + array version; + read_attribute(file_id, "version", version); + if (version[0] > VERSION_PARTICLE_RESTART[0] || + (version[0] == VERSION_PARTICLE_RESTART[0] && version[1] >= 1)) { + legacy_particle_codes = false; + } + } + read_dataset(file_id, "current_batch", simulation::current_batch); read_dataset(file_id, "generations_per_batch", settings::gen_per_batch); read_dataset(file_id, "current_generation", simulation::current_gen); @@ -45,7 +56,8 @@ void read_particle_restart(Particle& p, RunMode& previous_run_mode) read_dataset(file_id, "id", p.id()); int type; read_dataset(file_id, "type", type); - p.type() = static_cast(type); + p.type() = legacy_particle_codes ? legacy_particle_index_to_type(type) + : ParticleType {type}; read_dataset(file_id, "weight", p.wgt()); read_dataset(file_id, "energy", p.E()); read_dataset(file_id, "xyz", p.r()); @@ -95,20 +107,16 @@ void run_particle_restart() // Set all tallies to 0 for now (just tracking errors) model::tallies.clear(); - // Compute random number seed - int64_t particle_seed; - switch (previous_run_mode) { - case RunMode::EIGENVALUE: - case RunMode::FIXED_SOURCE: - particle_seed = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - p.id(); - break; - default: - throw std::runtime_error { - "Unexpected run mode: " + - std::to_string(static_cast(previous_run_mode))}; + // Allocate progeny_per_particle if needed for shared secondary mode + // (event_death() writes to this array). Set current_work to 0 since we + // only have one particle being restarted. + if (settings::use_shared_secondary_bank) { + p.current_work() = 0; + simulation::progeny_per_particle.resize(1, 0); } + + // Compute random number seed + int64_t particle_seed = compute_transport_seed(p.id()); init_particle_seeds(particle_seed, p.seeds()); // Force calculation of cross-sections by setting last energy to zero diff --git a/src/particle_type.cpp b/src/particle_type.cpp new file mode 100644 index 000000000..fe8f9fe2a --- /dev/null +++ b/src/particle_type.cpp @@ -0,0 +1,246 @@ +#include "openmc/particle_type.h" + +#include +#include +#include + +#include "openmc/string_utils.h" + +namespace openmc { +namespace { + +constexpr const char* ATOMIC_SYMBOL[] = {"", "H", "He", "Li", "Be", "B", "C", + "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", + "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", + "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", + "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", + "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", + "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", + "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", + "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", + "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"}; + +constexpr int MAX_Z = + static_cast(sizeof(ATOMIC_SYMBOL) / sizeof(ATOMIC_SYMBOL[0])) - 1; + +bool is_integer_string(const std::string& s) +{ + if (s.empty()) + return false; + size_t i = 0; + if (s[0] == '-' || s[0] == '+') { + if (s.size() == 1) + return false; + i = 1; + } + for (; i < s.size(); ++i) { + if (!std::isdigit(static_cast(s[i]))) + return false; + } + return true; +} + +int atomic_number_from_symbol(std::string_view symbol) +{ + for (int z = 1; z <= MAX_Z; ++z) { + if (symbol == ATOMIC_SYMBOL[z]) { + return z; + } + } + return 0; +} + +bool parse_gnds_nuclide(std::string_view name, int& Z, int& A, int& m) +{ + if (name.empty()) + return false; + + size_t pos = 0; + if (!std::isupper(static_cast(name[pos]))) + return false; + + std::string symbol; + symbol += name[pos++]; + if (pos < name.size() && + std::islower(static_cast(name[pos]))) { + symbol += name[pos++]; + } + + if (pos >= name.size() || + !std::isdigit(static_cast(name[pos]))) { + return false; + } + + size_t a_start = pos; + while ( + pos < name.size() && std::isdigit(static_cast(name[pos]))) { + ++pos; + } + A = std::stoi(std::string {name.substr(a_start, pos - a_start)}); + if (A <= 0 || A > 999) + return false; + + m = 0; + if (pos < name.size()) { + if (name[pos] != '_' || pos + 2 >= name.size() || name[pos + 1] != 'm') { + return false; + } + pos += 2; + size_t m_start = pos; + while (pos < name.size() && + std::isdigit(static_cast(name[pos]))) { + ++pos; + } + if (m_start == pos) + return false; + m = std::stoi(std::string {name.substr(m_start, pos - m_start)}); + if (m < 0 || m > 9) + return false; + } + + if (pos != name.size()) + return false; + + Z = atomic_number_from_symbol(symbol); + return Z != 0; +} + +// Helper to convert nuclear PDG number to nuclide name +std::string nuclide_name_from_pdg(int32_t pdg) +{ + int32_t code = pdg; + int m = code % 10; + int A = (code / 10) % 1000; + int Z = (code / 10000) % 1000; + + if (Z <= 0 || Z > MAX_Z || A <= 0 || A > 999) { + throw std::invalid_argument { + "Invalid nuclear PDG number: " + std::to_string(pdg)}; + } + + std::string name = ATOMIC_SYMBOL[Z] + std::to_string(A); + if (m > 0) { + name += "_m" + std::to_string(m); + } + return name; +} + +} // namespace + +//============================================================================== +// ParticleType member function implementations +//============================================================================== + +ParticleType::ParticleType(std::string_view str) +{ + std::string s {str}; + strtrim(s); + if (s.empty()) { + throw std::invalid_argument {"Particle string is empty."}; + } + + std::string lower = s; + to_lower(lower); + + // Check for pdg: prefix + if (starts_with(lower, "pdg:")) { + std::string value_str = lower.substr(4); + if (!is_integer_string(value_str)) { + throw std::invalid_argument {"Invalid PDG number: " + value_str}; + } + pdg_number_ = std::stoi(value_str); + return; + } + + // Check for known particle names + if (lower == "neutron" || lower == "n") { + pdg_number_ = PDG_NEUTRON; + return; + } + if (lower == "photon" || lower == "gamma") { + pdg_number_ = PDG_PHOTON; + return; + } + if (lower == "electron") { + pdg_number_ = PDG_ELECTRON; + return; + } + if (lower == "positron") { + pdg_number_ = PDG_POSITRON; + return; + } + if (lower == "proton" || lower == "p" || lower == "h1") { + pdg_number_ = PDG_PROTON; + return; + } + if (lower == "deuteron" || lower == "d" || lower == "h2") { + pdg_number_ = PDG_DEUTERON; + return; + } + if (lower == "triton" || lower == "t" || lower == "h3") { + pdg_number_ = PDG_TRITON; + return; + } + if (lower == "alpha" || lower == "he4") { + pdg_number_ = PDG_ALPHA; + return; + } + + // Check for integer string + if (is_integer_string(s)) { + pdg_number_ = std::stoi(s); + return; + } + + // Try to parse as GNDS nuclide name + int Z = 0; + int A = 0; + int m = 0; + if (!parse_gnds_nuclide(s, Z, A, m)) { + throw std::invalid_argument {"Invalid nuclide name: " + s}; + } + pdg_number_ = 1000000000 + Z * 10000 + A * 10 + m; +} + +std::string ParticleType::str() const +{ + if (pdg_number_ == PDG_NEUTRON) + return "neutron"; + if (pdg_number_ == PDG_PHOTON) + return "photon"; + if (pdg_number_ == PDG_ELECTRON) + return "electron"; + if (pdg_number_ == PDG_POSITRON) + return "positron"; + if (pdg_number_ == PDG_PROTON) + return "proton"; + + if (is_nucleus()) { + return nuclide_name_from_pdg(pdg_number_); + } + + return "pdg:" + std::to_string(pdg_number_); +} + +//============================================================================== +// Free function implementations +//============================================================================== + +ParticleType legacy_particle_index_to_type(int index) +{ + switch (index) { + case 0: + return ParticleType {PDG_NEUTRON}; + case 1: + return ParticleType {PDG_PHOTON}; + case 2: + return ParticleType {PDG_ELECTRON}; + case 3: + return ParticleType {PDG_POSITRON}; + default: + throw std::invalid_argument { + "Invalid legacy particle index: " + std::to_string(index)}; + } +} + +} // namespace openmc diff --git a/src/photon.cpp b/src/photon.cpp index 4926e3eae..6bbdc928f 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -13,11 +13,7 @@ #include "openmc/search.h" #include "openmc/settings.h" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xoperation.hpp" -#include "xtensor/xslice.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include @@ -33,7 +29,7 @@ constexpr int PhotonInteraction::MAX_STACK_SIZE; namespace data { -xt::xtensor compton_profile_pz; +tensor::Tensor compton_profile_pz; std::unordered_map element_map; vector> elements; @@ -46,8 +42,6 @@ vector> elements; PhotonInteraction::PhotonInteraction(hid_t group) { - using namespace xt::placeholders; - // Set index of element in global vector index_ = data::elements.size(); @@ -96,7 +90,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) read_dataset(rgroup, "xs", pair_production_electron_); close_group(rgroup); } else { - pair_production_electron_ = xt::zeros_like(energy_); + pair_production_electron_ = tensor::zeros_like(energy_); } // Read pair production @@ -105,7 +99,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) read_dataset(rgroup, "xs", pair_production_nuclear_); close_group(rgroup); } else { - pair_production_nuclear_ = xt::zeros_like(energy_); + pair_production_nuclear_ = tensor::zeros_like(energy_); } // Read photoelectric @@ -119,7 +113,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) read_dataset(rgroup, "xs", heating_); close_group(rgroup); } else { - heating_ = xt::zeros_like(energy_); + heating_ = tensor::zeros_like(energy_); } // Read subshell photoionization cross section and atomic relaxation data @@ -133,7 +127,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) } shells_.resize(n_shell); - cross_sections_ = xt::zeros({energy_.size(), n_shell}); + cross_sections_ = tensor::zeros({energy_.size(), n_shell}); // Create mapping from designator to index std::unordered_map shell_map; @@ -160,25 +154,26 @@ PhotonInteraction::PhotonInteraction(hid_t group) hid_t tgroup = open_group(rgroup, designator.c_str()); - // Read binding energy energy and number of electrons if atomic relaxation - // data is present + // Read binding energy if atomic relaxation data is present if (attribute_exists(tgroup, "binding_energy")) { has_atomic_relaxation_ = true; read_attribute(tgroup, "binding_energy", shell.binding_energy); } // Read subshell cross section - xt::xtensor xs; + tensor::Tensor xs; dset = open_dataset(tgroup, "xs"); read_attribute(dset, "threshold_idx", shell.threshold); close_dataset(dset); read_dataset(tgroup, "xs", xs); auto cross_section = - xt::view(cross_sections_, xt::range(shell.threshold, _), i); - cross_section = xt::where(xs > 0, xt::log(xs), 0); + cross_sections_.slice(tensor::range(static_cast(shell.threshold), + cross_sections_.shape(0)), + i); + cross_section = tensor::where(xs > 0, tensor::log(xs), 0); - if (object_exists(tgroup, "transitions")) { + if (settings::atomic_relaxation && object_exists(tgroup, "transitions")) { // Determine dimensions of transitions dset = open_dataset(tgroup, "transitions"); auto dims = object_shape(dset); @@ -186,11 +181,12 @@ PhotonInteraction::PhotonInteraction(hid_t group) int n_transition = dims[0]; if (n_transition > 0) { - xt::xtensor matrix; + tensor::Tensor matrix; read_dataset(tgroup, "transitions", matrix); // Transition probability normalization - double norm = xt::sum(xt::col(matrix, 3))(); + double norm = + tensor::Tensor(matrix.slice(tensor::all, 3)).sum(); shell.transitions.resize(n_transition); for (int j = 0; j < n_transition; ++j) { @@ -209,9 +205,8 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Check the maximum size of the atomic relaxation stack auto max_size = this->calc_max_stack_size(); if (max_size > MAX_STACK_SIZE && mpi::master) { - warning(fmt::format( - "The subshell vacancy stack in atomic relaxation can grow up to {}, but " - "the stack size limit is set to {}.", + warning(fmt::format("The subshell vacancy stack in atomic relaxation can " + "grow up to {}, but the stack size limit is set to {}.", max_size, MAX_STACK_SIZE)); } @@ -220,7 +215,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Read electron shell PDF and binding energies read_dataset(rgroup, "num_electrons", electron_pdf_); - electron_pdf_ /= xt::sum(electron_pdf_); + electron_pdf_ /= electron_pdf_.sum(); read_dataset(rgroup, "binding_energy", binding_energy_); // Read Compton profiles @@ -234,11 +229,11 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Map Compton subshell data to atomic relaxation data by finding the // subshell with the equivalent binding energy - if (has_atomic_relaxation_) { + if (settings::atomic_relaxation && has_atomic_relaxation_) { auto is_close = [](double a, double b) { return std::abs(a - b) / a < FP_REL_PRECISION; }; - subshell_map_ = xt::full_like(binding_energy_, -1); + subshell_map_ = tensor::Tensor(binding_energy_.shape(), -1); for (int i = 0; i < binding_energy_.size(); ++i) { double E_b = binding_energy_[i]; if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) { @@ -257,7 +252,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Create Compton profile CDF auto n_profile = data::compton_profile_pz.size(); auto n_shell_compton = profile_pdf_.shape(0); - profile_cdf_ = xt::empty({n_shell_compton, n_profile}); + profile_cdf_ = tensor::Tensor({n_shell_compton, n_profile}); for (int i = 0; i < n_shell_compton; ++i) { double c = 0.0; profile_cdf_(i, 0) = 0.0; @@ -276,11 +271,11 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Read bremsstrahlung scaled DCS rgroup = open_group(group, "bremsstrahlung"); read_dataset(rgroup, "dcs", dcs_); - auto n_e = dcs_.shape()[0]; - auto n_k = dcs_.shape()[1]; + auto n_e = dcs_.shape(0); + auto n_k = dcs_.shape(1); // Get energy grids used for bremsstrahlung DCS and for stopping powers - xt::xtensor electron_energy; + tensor::Tensor electron_energy; read_dataset(rgroup, "electron_energy", electron_energy); if (data::ttb_k_grid.size() == 0) { read_dataset(rgroup, "photon_energy", data::ttb_k_grid); @@ -293,7 +288,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) close_group(rgroup); // Truncate the bremsstrahlung data at the cutoff energy - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); const auto& E {electron_energy}; double cutoff = settings::energy_cutoff[photon]; if (cutoff > E(0)) { @@ -305,12 +300,12 @@ PhotonInteraction::PhotonInteraction(hid_t group) (std::log(E(i_grid + 1)) - std::log(E(i_grid))); // Interpolate bremsstrahlung DCS at the cutoff energy and truncate - xt::xtensor dcs({n_e - i_grid, n_k}); + tensor::Tensor dcs({n_e - i_grid, n_k}); for (int i = 0; i < n_k; ++i) { double y = std::exp( std::log(dcs_(i_grid, i)) + f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i)))); - auto col_i = xt::view(dcs, xt::all(), i); + tensor::View col_i = dcs.slice(tensor::all, i); col_i(0) = y; for (int j = i_grid + 1; j < n_e; ++j) { col_i(j - i_grid) = dcs_(j, i); @@ -318,9 +313,11 @@ PhotonInteraction::PhotonInteraction(hid_t group) } dcs_ = dcs; - xt::xtensor frst {cutoff}; - electron_energy = xt::concatenate(xt::xtuple( - frst, xt::view(electron_energy, xt::range(i_grid + 1, n_e)))); + tensor::Tensor frst({static_cast(1)}); + frst(0) = cutoff; + tensor::Tensor rest(electron_energy.slice( + tensor::range(i_grid + 1, electron_energy.size()))); + electron_energy = tensor::concatenate(frst, rest); } // Set incident particle energy grid @@ -329,7 +326,8 @@ PhotonInteraction::PhotonInteraction(hid_t group) } // Calculate the radiative stopping power - stopping_power_radiative_ = xt::empty({data::ttb_e_grid.size()}); + stopping_power_radiative_ = + tensor::Tensor({data::ttb_e_grid.size()}); for (int i = 0; i < data::ttb_e_grid.size(); ++i) { // Integrate over reduced photon energy double c = 0.0; @@ -354,14 +352,15 @@ PhotonInteraction::PhotonInteraction(hid_t group) // values below exp(-499) we store the log as -900, for which exp(-900) // evaluates to zero. double limit = std::exp(-499.0); - energy_ = xt::log(energy_); - coherent_ = xt::where(coherent_ > limit, xt::log(coherent_), -900.0); - incoherent_ = xt::where(incoherent_ > limit, xt::log(incoherent_), -900.0); - photoelectric_total_ = xt::where( - photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0); - pair_production_total_ = xt::where( - pair_production_total_ > limit, xt::log(pair_production_total_), -900.0); - heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0); + energy_ = tensor::log(energy_); + coherent_ = tensor::where(coherent_ > limit, tensor::log(coherent_), -900.0); + incoherent_ = + tensor::where(incoherent_ > limit, tensor::log(incoherent_), -900.0); + photoelectric_total_ = tensor::where( + photoelectric_total_ > limit, tensor::log(photoelectric_total_), -900.0); + pair_production_total_ = tensor::where(pair_production_total_ > limit, + tensor::log(pair_production_total_), -900.0); + heating_ = tensor::where(heating_ > limit, tensor::log(heating_), -900.0); } PhotonInteraction::~PhotonInteraction() @@ -512,7 +511,7 @@ void PhotonInteraction::compton_doppler( c = prn(seed) * c_max; // Determine pz corresponding to sampled cdf value - auto cdf_shell = xt::view(profile_cdf_, shell, xt::all()); + tensor::View cdf_shell = profile_cdf_.slice(shell); int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c); double pz_l = data::compton_profile_pz(i); double pz_r = data::compton_profile_pz(i + 1); @@ -608,8 +607,8 @@ void PhotonInteraction::calculate_xs(Particle& p) const // Calculate microscopic photoelectric cross section xs.photoelectric = 0.0; - const auto& xs_lower = xt::row(cross_sections_, i_grid); - const auto& xs_upper = xt::row(cross_sections_, i_grid + 1); + tensor::View xs_lower = cross_sections_.slice(i_grid); + tensor::View xs_upper = cross_sections_.slice(i_grid + 1); for (int i = 0; i < xs_upper.size(); ++i) if (xs_lower(i) != 0) @@ -805,7 +804,7 @@ void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const if (shell.transitions.empty()) { Direction u = isotropic_direction(p.current_seed()); double E = shell.binding_energy; - p.create_secondary(p.wgt(), u, E, ParticleType::photon); + p.create_secondary(p.wgt(), u, E, ParticleType::photon()); continue; } @@ -833,12 +832,13 @@ void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const holes[n_holes++] = transition.secondary_subshell; // Create auger electron - p.create_secondary(p.wgt(), u, transition.energy, ParticleType::electron); + p.create_secondary( + p.wgt(), u, transition.energy, ParticleType::electron()); } else { // Radiative transition -- get X-ray energy // Create fluorescent photon - p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon); + p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon()); } } } diff --git a/src/physics.cpp b/src/physics.cpp index 41509af97..4a8b4d368 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -30,9 +30,9 @@ #include +#include "openmc/tensor.h" #include // for max, min, max_element #include // for sqrt, exp, log, abs, copysign -#include namespace openmc { @@ -44,29 +44,41 @@ void collision(Particle& p) { // Add to collision counter for particle ++(p.n_collision()); + p.secondary_bank_index() = p.local_secondary_bank().size(); // Sample reaction for the material the particle is in - switch (p.type()) { - case ParticleType::neutron: + switch (p.type().pdg_number()) { + case PDG_NEUTRON: sample_neutron_reaction(p); break; - case ParticleType::photon: + case PDG_PHOTON: sample_photon_reaction(p); break; - case ParticleType::electron: + case PDG_ELECTRON: sample_electron_reaction(p); break; - case ParticleType::positron: + case PDG_POSITRON: sample_positron_reaction(p); break; + default: + fatal_error("Unsupported particle PDG for collision sampling."); } - if (settings::weight_window_checkpoint_collision) - apply_weight_windows(p); + if (settings::weight_windows_on) { + auto [ww_found, ww] = search_weight_window(p); + if (!ww_found && p.type() == ParticleType::neutron()) { + // if the weight window is not valid, apply russian roulette for neutrons + // (regardless of weight window collision checkpoint setting) + apply_russian_roulette(p); + } else if (settings::weight_window_checkpoint_collision) { + // if collision checkpointing is on, apply weight window + apply_weight_window(p, ww); + } + } // Kill particle if energy falls below cutoff - int type = static_cast(p.type()); - if (p.E() < settings::energy_cutoff[type]) { + int type = p.type().transport_index(); + if (type != C_NONE && p.E() < settings::energy_cutoff[type]) { p.wgt() = 0.0; } @@ -75,11 +87,11 @@ void collision(Particle& p) std::string msg; if (p.event() == TallyEvent::KILL) { msg = fmt::format(" Killed. Energy = {} eV.", p.E()); - } else if (p.type() == ParticleType::neutron) { + } else if (p.type().is_neutron()) { msg = fmt::format(" {} with {}. Energy = {} eV.", reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_, p.E()); - } else if (p.type() == ParticleType::photon) { + } else if (p.type().is_photon()) { msg = fmt::format(" {} with {}. Energy = {} eV.", reaction_name(p.event_mt()), to_element(data::nuclides[p.event_nuclide()]->name_), p.E()); @@ -115,7 +127,8 @@ void sample_neutron_reaction(Particle& p) // Make sure particle population doesn't grow out of control for // subcritical multiplication problems. - if (p.secondary_bank().size() >= settings::max_secondaries) { + if (p.local_secondary_bank().size() >= settings::max_secondaries && + !settings::use_shared_secondary_bank) { fatal_error( "The secondary particle bank appears to be growing without " "bound. You are likely running a subcritical multiplication problem " @@ -153,18 +166,9 @@ void sample_neutron_reaction(Particle& p) advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE)); } - // Play russian roulette if survival biasing is turned on - if (settings::survival_biasing) { - // if survival normalization is on, use normalized weight cutoff and - // normalized weight survive - if (settings::survival_normalization) { - if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { - russian_roulette(p, settings::weight_survive * p.wgt_born()); - } - } else if (p.wgt() < settings::weight_cutoff) { - russian_roulette(p, settings::weight_survive); - } - } + // Play russian roulette if there are no weight windows + if (!settings::weight_windows_on) + apply_russian_roulette(p); } void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) @@ -208,7 +212,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) // Initialize fission site object with particle data SourceSite site; site.r = p.r(); - site.particle = ParticleType::neutron; + site.particle = ParticleType::neutron(); site.time = p.time(); site.wgt = 1. / weight; site.surf_id = 0; @@ -218,14 +222,14 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) // Reject site if it exceeds time cutoff if (site.delayed_group > 0) { - double t_cutoff = settings::time_cutoff[static_cast(site.particle)]; + double t_cutoff = settings::time_cutoff[site.particle.transport_index()]; if (site.time > t_cutoff) { continue; } } // Set parent and progeny IDs - site.parent_id = p.id(); + site.parent_id = p.current_work(); site.progeny_id = p.n_progeny()++; // Store fission site in bank @@ -250,7 +254,11 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) ifp(p, idx); } } else { - p.secondary_bank().push_back(site); + site.wgt_born = p.wgt_born(); + site.wgt_ww_born = p.wgt_ww_born(); + site.n_split = p.n_split(); + p.local_secondary_bank().push_back(site); + p.n_secondaries()++; } // Increment the number of neutrons born delayed @@ -289,7 +297,7 @@ void sample_photon_reaction(Particle& p) // Kill photon if below energy cutoff -- an extra check is made here because // photons with energy below the cutoff may have been produced by neutrons // reactions or atomic relaxation - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); if (p.E() < settings::energy_cutoff[photon]) { p.E() = 0.0; p.wgt() = 0.0; @@ -339,19 +347,20 @@ void sample_photon_reaction(Particle& p) // Create Compton electron double phi = uniform_distribution(0., 2.0 * PI, p.current_seed()); double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b; - int electron = static_cast(ParticleType::electron); + int electron = ParticleType::electron().transport_index(); if (E_electron >= settings::energy_cutoff[electron]) { double mu_electron = (alpha - alpha_out * p.mu()) / std::sqrt(alpha * alpha + alpha_out * alpha_out - 2.0 * alpha * alpha_out * p.mu()); Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed()); - p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); + p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron()); } // Allow electrons to fill orbital and produce Auger electrons and // fluorescent photons. Since Compton subshell data does not match atomic // relaxation data, use the mapping between the data to find the subshell - if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) { + if (settings::atomic_relaxation && element.has_atomic_relaxation_ && + i_shell >= 0 && element.subshell_map_[i_shell] >= 0) { element.atomic_relaxation(element.subshell_map_[i_shell], p); } @@ -371,8 +380,9 @@ void sample_photon_reaction(Particle& p) // cross sections int i_grid = micro.index_grid; double f = micro.interp_factor; - const auto& xs_lower = xt::row(element.cross_sections_, i_grid); - const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1); + tensor::View xs_lower = element.cross_sections_.slice(i_grid); + tensor::View xs_upper = + element.cross_sections_.slice(i_grid + 1); for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) { const auto& shell {element.shells_[i_shell]}; @@ -418,11 +428,13 @@ void sample_photon_reaction(Particle& p) u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi); // Create secondary electron - p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); + p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron()); // Allow electrons to fill orbital and produce auger electrons // and fluorescent photons - element.atomic_relaxation(i_shell, p); + if (settings::atomic_relaxation) { + element.atomic_relaxation(i_shell, p); + } p.event() = TallyEvent::ABSORB; p.event_mt() = 533 + shell.index_subshell; p.wgt() = 0.0; @@ -443,12 +455,11 @@ void sample_photon_reaction(Particle& p) // Create secondary electron Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed()); - p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); + p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron()); // Create secondary positron u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed()); - p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron); - + p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron()); p.event() = TallyEvent::ABSORB; p.event_mt() = PAIR_PROD; p.wgt() = 0.0; @@ -483,8 +494,8 @@ void sample_positron_reaction(Particle& p) Direction u = isotropic_direction(p.current_seed()); // Create annihilation photon pair traveling in opposite directions - p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon); - p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon); + p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon()); + p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon()); p.E() = 0.0; p.wgt() = 0.0; @@ -609,7 +620,7 @@ void sample_photon_product( continue; for (int j = 0; j < rx->products_.size(); ++j) { - if (rx->products_[j].particle_ == ParticleType::photon) { + if (rx->products_[j].particle_.is_photon()) { // For fission, artificially increase the photon yield to account // for delayed photons double f = 1.0; @@ -1098,7 +1109,7 @@ void sample_fission_neutron( rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed); // resample if energy is greater than maximum neutron energy - constexpr int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); if (site->E < data::energy_max[neutron]) break; @@ -1158,7 +1169,7 @@ void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p) if (std::floor(yield) == yield && yield > 0) { // If yield is integral, create exactly that many secondary particles for (int i = 0; i < static_cast(std::round(yield)) - 1; ++i) { - p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron); + p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron()); } } else { // Otherwise, change weight of particle based on yield @@ -1214,11 +1225,22 @@ void sample_secondary_photons(Particle& p, int i_nuclide) } // Create the secondary photon - bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon); + bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon()); + + // Pre-add photon energy to pht_storage so pht_secondary_particles() + // subtraction results in net zero + if (created_photon && !model::active_pulse_height_tallies.empty()) { + auto it = std::find(model::pulse_height_cells.begin(), + model::pulse_height_cells.end(), p.lowest_coord().cell()); + if (it != model::pulse_height_cells.end()) { + int index = std::distance(model::pulse_height_cells.begin(), it); + p.pht_storage()[index] += E; + } + } // Tag secondary particle with parent nuclide if (created_photon && settings::use_decay_photons) { - p.secondary_bank().back().parent_nuclide = + p.local_secondary_bank().back().parent_nuclide = rx->products_[i_product].parent_nuclide_; } } diff --git a/src/physics_common.cpp b/src/physics_common.cpp index e25ae6b97..10760ce2d 100644 --- a/src/physics_common.cpp +++ b/src/physics_common.cpp @@ -18,4 +18,20 @@ void russian_roulette(Particle& p, double weight_survive) } } +void apply_russian_roulette(Particle& p) +{ + // Exit if survival biasing is turned off + if (!settings::survival_biasing) + return; + + // if survival normalization is on, use normalized weight cutoff and + // normalized weight survive + if (settings::survival_normalization) { + if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { + russian_roulette(p, settings::weight_survive * p.wgt_born()); + } + } else if (p.wgt() < settings::weight_cutoff) { + russian_roulette(p, settings::weight_survive); + } +} } // namespace openmc diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 4c28cb179..212ba765c 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -2,7 +2,7 @@ #include -#include "xtensor/xarray.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" @@ -27,12 +27,22 @@ void collision_mg(Particle& p) { // Add to the collision counter for the particle p.n_collision()++; + p.secondary_bank_index() = p.local_secondary_bank().size(); // Sample the reaction type sample_reaction(p); - if (settings::weight_window_checkpoint_collision) - apply_weight_windows(p); + if (settings::weight_windows_on) { + auto [ww_found, ww] = search_weight_window(p); + if (!ww_found && p.type() == ParticleType::neutron()) { + // if the weight window is not valid, apply russian roulette + // (regardless of weight window collision checkpoint setting) + apply_russian_roulette(p); + } else if (settings::weight_window_checkpoint_collision) { + // if collision checkpointing is on, apply weight window + apply_weight_window(p, ww); + } + } // Display information about collision if ((settings::verbosity >= 10) || p.trace()) { @@ -66,18 +76,9 @@ void sample_reaction(Particle& p) // Sample a scattering event to determine the energy of the exiting neutron scatter(p); - // Play Russian roulette if survival biasing is turned on - if (settings::survival_biasing) { - // if survival normalization is applicable, use normalized weight cutoff and - // normalized weight survive - if (settings::survival_normalization) { - if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { - russian_roulette(p, settings::weight_survive * p.wgt_born()); - } - } else if (p.wgt() < settings::weight_cutoff) { - russian_roulette(p, settings::weight_survive); - } - } + // Play russian roulette if there are no weight windows + if (!settings::weight_windows_on) + apply_russian_roulette(p); } void scatter(Particle& p) @@ -136,7 +137,7 @@ void create_fission_sites(Particle& p) // Initialize fission site object with particle data SourceSite site; site.r = p.r(); - site.particle = ParticleType::neutron; + site.particle = ParticleType::neutron(); site.time = p.time(); site.wgt = 1. / weight; @@ -171,14 +172,14 @@ void create_fission_sites(Particle& p) site.time -= std::log(prn(p.current_seed())) / decay_rate; // Reject site if it exceeds time cutoff - double t_cutoff = settings::time_cutoff[static_cast(site.particle)]; + double t_cutoff = settings::time_cutoff[site.particle.transport_index()]; if (site.time > t_cutoff) { continue; } } // Set parent and progeny ID - site.parent_id = p.id(); + site.parent_id = p.current_work(); site.progeny_id = p.n_progeny()++; // Store fission site in bank @@ -199,7 +200,11 @@ void create_fission_sites(Particle& p) break; } } else { - p.secondary_bank().push_back(site); + site.wgt_born = p.wgt_born(); + site.wgt_ww_born = p.wgt_ww_born(); + site.n_split = p.n_split(); + p.local_secondary_bank().push_back(site); + p.n_secondaries()++; } // Set the delayed group on the particle as well diff --git a/src/plot.cpp b/src/plot.cpp index 2cadc48ce..5c4eb9460 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,20 +1,19 @@ #include "openmc/plot.h" #include -#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include #include #include #include -#include "xtensor/xmanipulation.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include #ifdef USE_LIBPNG #include #endif +#include "openmc/cell.h" #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/dagmc.h" @@ -33,6 +32,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/string_utils.h" +#include "openmc/tallies/filter.h" namespace openmc { @@ -44,10 +44,12 @@ constexpr int PLOT_LEVEL_LOWEST {-1}; //!< lower bound on plot universe level constexpr int32_t NOT_FOUND {-2}; constexpr int32_t OVERLAP {-3}; -IdData::IdData(size_t h_res, size_t v_res) : data_({v_res, h_res, 3}, NOT_FOUND) +IdData::IdData(size_t h_res, size_t v_res, bool /*include_filter*/) + : data_({v_res, h_res, 3}, NOT_FOUND) {} -void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) +void IdData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { // set cell data if (p.n_coord() <= level) { @@ -64,36 +66,101 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) Cell* c = model::cells.at(p.lowest_coord().cell()).get(); if (p.material() == MATERIAL_VOID) { data_(y, x, 2) = MATERIAL_VOID; - return; } else if (c->type_ == Fill::MATERIAL) { Material* m = model::materials.at(p.material()).get(); data_(y, x, 2) = m->id_; } } -void IdData::set_overlap(size_t y, size_t x) +void IdData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) { - xt::view(data_, y, x, xt::all()) = OVERLAP; + for (size_t k = 0; k < data_.shape(2); ++k) + data_(y, x, k) = OVERLAP; } -PropertyData::PropertyData(size_t h_res, size_t v_res) +PropertyData::PropertyData(size_t h_res, size_t v_res, bool /*include_filter*/) : data_({v_res, h_res, 2}, NOT_FOUND) {} -void PropertyData::set_value( - size_t y, size_t x, const GeometryState& p, int level) +void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { Cell* c = model::cells.at(p.lowest_coord().cell()).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; + data_(y, x, 1) = c->density(p.cell_instance()); +} + +void PropertyData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) +{ + data_(y, x) = OVERLAP; +} + +//============================================================================== +// RasterData implementation +//============================================================================== + +RasterData::RasterData(size_t h_res, size_t v_res, bool include_filter) + : id_data_({v_res, h_res, include_filter ? 4u : 3u}, NOT_FOUND), + property_data_({v_res, h_res, 2}, static_cast(NOT_FOUND)), + include_filter_(include_filter) +{} + +void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter, FilterMatch* match) +{ + // set cell data + if (p.n_coord() <= level) { + id_data_(y, x, 0) = NOT_FOUND; + id_data_(y, x, 1) = NOT_FOUND; + } else { + id_data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_; + id_data_(y, x, 1) = level == p.n_coord() - 1 + ? p.cell_instance() + : cell_instance_at_level(p, level); + } + + // set material data + Cell* c = model::cells.at(p.lowest_coord().cell()).get(); + if (p.material() == MATERIAL_VOID) { + id_data_(y, x, 2) = MATERIAL_VOID; + } else if (c->type_ == Fill::MATERIAL) { + Material* m = model::materials.at(p.material()).get(); + id_data_(y, x, 2) = m->id_; + } + + // set filter index (only if filter is being used) + if (include_filter_ && filter) { + filter->get_all_bins(p, TallyEstimator::COLLISION, *match); + if (match->bins_.empty()) { + id_data_(y, x, 3) = -1; + } else { + id_data_(y, x, 3) = match->bins_[0]; + } + match->bins_.clear(); + match->weights_.clear(); + } + + // set temperature (in K) + property_data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; + + // set density (g/cm³) if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { Material* m = model::materials.at(p.material()).get(); - data_(y, x, 1) = m->density_gpcc_; + property_data_(y, x, 1) = c->density(p.cell_instance()); } } -void PropertyData::set_overlap(size_t y, size_t x) +void RasterData::set_overlap(size_t y, size_t x, int overlap_idx) { - data_(y, x) = OVERLAP; + // Set cell, instance, and material to OVERLAP, but preserve filter bin for + // tally plotting. Cell encodes the overlap index as a negative number so that + // it can be used to look up overlap information in the plotter. + id_data_(y, x, 0) = OVERLAP - overlap_idx - 1; + id_data_(y, x, 1) = OVERLAP; + id_data_(y, x, 2) = OVERLAP; + + property_data_(y, x, 0) = OVERLAP; + property_data_(y, x, 1) = OVERLAP; } //============================================================================== @@ -123,11 +190,21 @@ extern "C" int openmc_plot_geometry() return 0; } +void PlottableInterface::write_image(const ImageData& data) const +{ +#ifdef USE_LIBPNG + output_png(path_plot(), data); +#else + output_ppm(path_plot(), data); +#endif +} + void Plot::create_output() const { if (PlotType::slice == type_) { // create 2D image - create_image(); + ImageData image = create_image(); + write_image(image); } else if (PlotType::voxel == type_) { // create voxel file for 3D viewing create_voxel(); @@ -170,9 +247,9 @@ void Plot::print_info() const fmt::print("Basis: YZ\n"); break; } - fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]); + fmt::print("Pixels: {} {}\n", pixels()[0], pixels()[1]); } else if (PlotType::voxel == type_) { - fmt::print("Voxels: {} {} {}\n", pixels_[0], pixels_[1], pixels_[2]); + fmt::print("Voxels: {} {} {}\n", pixels()[0], pixels()[1], pixels()[2]); } } @@ -200,8 +277,11 @@ void read_plots_xml() void read_plots_xml(pugi::xml_node root) { for (auto node : root.children("plot")) { - std::string id_string = get_node_value(node, "id", true); - int id = std::stoi(id_string); + std::string plot_desc = ""; + if (check_for_node(node, "id")) { + plot_desc = get_node_value(node, "id", true); + } + if (check_for_node(node, "type")) { std::string type_str = get_node_value(node, "type", true); if (type_str == "slice") { @@ -216,12 +296,12 @@ void read_plots_xml(pugi::xml_node root) } else if (type_str == "solid_raytrace") { model::plots.emplace_back(std::make_unique(node)); } else { - fatal_error( - fmt::format("Unsupported plot type '{}' in plot {}", type_str, id)); + fatal_error(fmt::format( + "Unsupported plot type '{}' in plot {}", type_str, plot_desc)); } model::plot_map[model::plots.back()->id()] = model::plots.size() - 1; } else { - fatal_error(fmt::format("Must specify plot type in plot {}", id)); + fatal_error(fmt::format("Must specify plot type in plot {}", plot_desc)); } } } @@ -234,11 +314,10 @@ void free_memory_plot() // creates an image based on user input from a plots.xml // specification in the PNG/PPM format -void Plot::create_image() const +ImageData Plot::create_image() const { - - size_t width = pixels_[0]; - size_t height = pixels_[1]; + size_t width = pixels()[0]; + size_t height = pixels()[1]; ImageData data({width, height}, not_found_); @@ -275,30 +354,48 @@ void Plot::create_image() const draw_mesh_lines(data); } -// create image file -#ifdef USE_LIBPNG - output_png(path_plot(), data); -#else - output_ppm(path_plot(), data); -#endif + return data; } void PlottableInterface::set_id(pugi::xml_node plot_node) { - // Copy data into plots + int id {C_NONE}; if (check_for_node(plot_node, "id")) { - id_ = std::stoi(get_node_value(plot_node, "id")); - } else { - fatal_error("Must specify plot id in plots XML file."); + id = std::stoi(get_node_value(plot_node, "id")); } - // Check to make sure 'id' hasn't been used - if (model::plot_map.find(id_) != model::plot_map.end()) { - fatal_error( - fmt::format("Two or more plots use the same unique ID: {}", id_)); + try { + set_id(id); + } catch (const std::runtime_error& e) { + fatal_error(e.what()); } } +void PlottableInterface::set_id(int id) +{ + if (id < 0 && id != C_NONE) { + throw std::runtime_error {fmt::format("Invalid plot ID: {}", id)}; + } + + if (id == C_NONE) { + id = 1; + for (const auto& p : model::plots) { + id = std::max(id, p->id() + 1); + } + } + + if (id_ == id) + return; + + // Check to make sure this ID doesn't already exist + if (model::plot_map.find(id) != model::plot_map.end()) { + throw std::runtime_error { + fmt::format("Two or more plots use the same unique ID: {}", id)}; + } + + id_ = id; +} + // Checks if png or ppm is already present bool file_extension_present( const std::string& filename, const std::string& extension) @@ -348,17 +445,17 @@ void Plot::set_output_path(pugi::xml_node plot_node) vector pxls = get_node_array(plot_node, "pixels"); if (PlotType::slice == type_) { if (pxls.size() == 2) { - pixels_[0] = pxls[0]; - pixels_[1] = pxls[1]; + pixels()[0] = pxls[0]; + pixels()[1] = pxls[1]; } else { fatal_error( fmt::format(" must be length 2 in slice plot {}", id())); } } else if (PlotType::voxel == type_) { if (pxls.size() == 3) { - pixels_[0] = pxls[0]; - pixels_[1] = pxls[1]; - pixels_[2] = pxls[2]; + pixels()[0] = pxls[0]; + pixels()[1] = pxls[1]; + pixels()[2] = pxls[2]; } else { fatal_error( fmt::format(" must be length 3 in voxel plot {}", id())); @@ -419,6 +516,22 @@ void Plot::set_width(pugi::xml_node plot_node) if (pl_width.size() == 2) { width_.x = pl_width[0]; width_.y = pl_width[1]; + switch (basis_) { + case PlotBasis::xy: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, width_.y, 0.0}; + break; + case PlotBasis::xz: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + case PlotBasis::yz: + u_span_ = {0.0, width_.x, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + default: + UNREACHABLE(); + } } else { fatal_error( fmt::format(" must be length 2 in slice plot {}", id())); @@ -447,23 +560,31 @@ void PlottableInterface::set_universe(pugi::xml_node plot_node) } } -void PlottableInterface::set_default_colors(pugi::xml_node plot_node) +void PlottableInterface::set_color_by(pugi::xml_node plot_node) { - // Copy plot color type and initialize all colors randomly + // Copy plot color type std::string pl_color_by = "cell"; if (check_for_node(plot_node, "color_by")) { pl_color_by = get_node_value(plot_node, "color_by", true); } if ("cell" == pl_color_by) { color_by_ = PlotColorBy::cells; - colors_.resize(model::cells.size()); } else if ("material" == pl_color_by) { color_by_ = PlotColorBy::mats; - colors_.resize(model::materials.size()); } else { fatal_error(fmt::format( "Unsupported plot color type '{}' in plot {}", pl_color_by, id())); } +} + +void PlottableInterface::set_default_colors() +{ + // Copy plot color type and initialize all colors randomly + if (PlotColorBy::cells == color_by_) { + colors_.resize(model::cells.size()); + } else if (PlotColorBy::mats == color_by_) { + colors_.resize(model::materials.size()); + } for (auto& c : colors_) { c = random_color(); @@ -710,7 +831,8 @@ PlottableInterface::PlottableInterface(pugi::xml_node plot_node) set_id(plot_node); set_bg_color(plot_node); set_universe(plot_node); - set_default_colors(plot_node); + set_color_by(plot_node); + set_default_colors(); set_user_colors(plot_node); set_mask(plot_node); set_overlap_color(plot_node); @@ -725,7 +847,7 @@ Plot::Plot(pugi::xml_node plot_node, PlotType type) set_width(plot_node); set_meshlines(plot_node); slice_level_ = level_; // Copy level employed in SlicePlotBase::get_map - slice_color_overlaps_ = color_overlaps_; + show_overlaps_ = color_overlaps_; } //============================================================================== @@ -743,14 +865,14 @@ void output_ppm(const std::string& filename, const ImageData& data) // Write header of << "P6\n"; - of << data.shape()[0] << " " << data.shape()[1] << "\n"; + of << data.shape(0) << " " << data.shape(1) << "\n"; of << "255\n"; of.close(); of.open(fname, std::ios::binary | std::ios::app); // Write color for each pixel - for (int y = 0; y < data.shape()[1]; y++) { - for (int x = 0; x < data.shape()[0]; x++) { + for (int y = 0; y < data.shape(1); y++) { + for (int x = 0; x < data.shape(0); x++) { RGBColor rgb = data(x, y); of << rgb.red << rgb.green << rgb.blue; } @@ -782,8 +904,8 @@ void output_png(const std::string& filename, const ImageData& data) png_init_io(png_ptr, fp); // Write header (8 bit colour depth) - int width = data.shape()[0]; - int height = data.shape()[1]; + int width = data.shape(0); + int height = data.shape(1); png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ptr, info_ptr); @@ -822,23 +944,39 @@ void Plot::draw_mesh_lines(ImageData& data) const rgb = meshlines_color_; int ax1, ax2; + Position expected_u {}; + Position expected_v {}; switch (basis_) { case PlotBasis::xy: ax1 = 0; ax2 = 1; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, width_[1], 0.0}; break; case PlotBasis::xz: ax1 = 0; ax2 = 2; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; case PlotBasis::yz: ax1 = 1; ax2 = 2; + expected_u = {0.0, width_[0], 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; default: UNREACHABLE(); } + // Meshlines rely on axis-aligned indexing in global coordinates. + constexpr double rel_tol {1e-12}; + double span_tol = rel_tol * (1.0 + u_span_.norm() + v_span_.norm()); + if ((u_span_ - expected_u).norm() > span_tol || + (v_span_ - expected_v).norm() > span_tol) { + fatal_error("Meshlines are only supported for axis-aligned slice plots."); + } + Position ll_plot {origin_}; Position ur_plot {origin_}; @@ -857,27 +995,27 @@ void Plot::draw_mesh_lines(ImageData& data) const int ax2_min, ax2_max; if (axis_lines.second.size() > 0) { double frac = (axis_lines.second.back() - ll_plot[ax2]) / width[ax2]; - ax2_min = (1.0 - frac) * pixels_[1]; + ax2_min = (1.0 - frac) * pixels()[1]; if (ax2_min < 0) ax2_min = 0; frac = (axis_lines.second.front() - ll_plot[ax2]) / width[ax2]; - ax2_max = (1.0 - frac) * pixels_[1]; - if (ax2_max > pixels_[1]) - ax2_max = pixels_[1]; + ax2_max = (1.0 - frac) * pixels()[1]; + if (ax2_max > pixels()[1]) + ax2_max = pixels()[1]; } else { ax2_min = 0; - ax2_max = pixels_[1]; + ax2_max = pixels()[1]; } // Iterate across the first axis and draw lines. for (auto ax1_val : axis_lines.first) { double frac = (ax1_val - ll_plot[ax1]) / width[ax1]; - int ax1_ind = frac * pixels_[0]; + int ax1_ind = frac * pixels()[0]; for (int ax2_ind = ax2_min; ax2_ind < ax2_max; ++ax2_ind) { for (int plus = 0; plus <= meshlines_width_; plus++) { - if (ax1_ind + plus >= 0 && ax1_ind + plus < pixels_[0]) + if (ax1_ind + plus >= 0 && ax1_ind + plus < pixels()[0]) data(ax1_ind + plus, ax2_ind) = rgb; - if (ax1_ind - plus >= 0 && ax1_ind - plus < pixels_[0]) + if (ax1_ind - plus >= 0 && ax1_ind - plus < pixels()[0]) data(ax1_ind - plus, ax2_ind) = rgb; } } @@ -887,27 +1025,27 @@ void Plot::draw_mesh_lines(ImageData& data) const int ax1_min, ax1_max; if (axis_lines.first.size() > 0) { double frac = (axis_lines.first.front() - ll_plot[ax1]) / width[ax1]; - ax1_min = frac * pixels_[0]; + ax1_min = frac * pixels()[0]; if (ax1_min < 0) ax1_min = 0; frac = (axis_lines.first.back() - ll_plot[ax1]) / width[ax1]; - ax1_max = frac * pixels_[0]; - if (ax1_max > pixels_[0]) - ax1_max = pixels_[0]; + ax1_max = frac * pixels()[0]; + if (ax1_max > pixels()[0]) + ax1_max = pixels()[0]; } else { ax1_min = 0; - ax1_max = pixels_[0]; + ax1_max = pixels()[0]; } // Iterate across the second axis and draw lines. for (auto ax2_val : axis_lines.second) { double frac = (ax2_val - ll_plot[ax2]) / width[ax2]; - int ax2_ind = (1.0 - frac) * pixels_[1]; + int ax2_ind = (1.0 - frac) * pixels()[1]; for (int ax1_ind = ax1_min; ax1_ind < ax1_max; ++ax1_ind) { for (int plus = 0; plus <= meshlines_width_; plus++) { - if (ax2_ind + plus >= 0 && ax2_ind + plus < pixels_[1]) + if (ax2_ind + plus >= 0 && ax2_ind + plus < pixels()[1]) data(ax1_ind, ax2_ind + plus) = rgb; - if (ax2_ind - plus >= 0 && ax2_ind - plus < pixels_[1]) + if (ax2_ind - plus >= 0 && ax2_ind - plus < pixels()[1]) data(ax1_ind, ax2_ind - plus) = rgb; } } @@ -928,9 +1066,9 @@ void Plot::create_voxel() const { // compute voxel widths in each direction array vox; - vox[0] = width_[0] / static_cast(pixels_[0]); - vox[1] = width_[1] / static_cast(pixels_[1]); - vox[2] = width_[2] / static_cast(pixels_[2]); + vox[0] = width_[0] / static_cast(pixels()[0]); + vox[1] = width_[1] / static_cast(pixels()[1]); + vox[2] = width_[2] / static_cast(pixels()[2]); // initial particle position Position ll = origin_ - width_ / 2.; @@ -952,30 +1090,30 @@ void Plot::create_voxel() const // Write current date and time write_attribute(file_id, "date_and_time", time_stamp().c_str()); - array pixels; - std::copy(pixels_.begin(), pixels_.end(), pixels.begin()); - write_attribute(file_id, "num_voxels", pixels); + array h5_pixels; + std::copy(pixels().begin(), pixels().end(), h5_pixels.begin()); + write_attribute(file_id, "num_voxels", h5_pixels); write_attribute(file_id, "voxel_width", vox); write_attribute(file_id, "lower_left", ll); // Create dataset for voxel data -- note that the dimensions are reversed // since we want the order in the file to be z, y, x hsize_t dims[3]; - dims[0] = pixels_[2]; - dims[1] = pixels_[1]; - dims[2] = pixels_[0]; + dims[0] = pixels()[2]; + dims[1] = pixels()[1]; + dims[2] = pixels()[0]; hid_t dspace, dset, memspace; voxel_init(file_id, &(dims[0]), &dspace, &dset, &memspace); SlicePlotBase pltbase; - pltbase.width_ = width_; pltbase.origin_ = origin_; - pltbase.basis_ = PlotBasis::xy; - pltbase.pixels_ = pixels_; - pltbase.slice_color_overlaps_ = color_overlaps_; + pltbase.u_span_ = {width_.x, 0.0, 0.0}; + pltbase.v_span_ = {0.0, width_.y, 0.0}; + pltbase.pixels() = pixels(); + pltbase.show_overlaps_ = color_overlaps_; ProgressBar pb; - for (int z = 0; z < pixels_[2]; z++) { + for (int z = 0; z < pixels()[2]; z++) { // update z coordinate pltbase.origin_.z = ll.z + z * vox[2]; @@ -984,16 +1122,21 @@ void Plot::create_voxel() const // select only cell/material ID data and flip the y-axis int idx = color_by_ == PlotColorBy::cells ? 0 : 2; - xt::xtensor data_slice = - xt::view(ids.data_, xt::all(), xt::all(), idx); - xt::xtensor data_flipped = xt::flip(data_slice, 0); + // Extract 2D slice at index idx from 3D data + size_t rows = ids.data_.shape(0); + size_t cols = ids.data_.shape(1); + tensor::Tensor data_slice({rows, cols}); + for (size_t r = 0; r < rows; ++r) + for (size_t c = 0; c < cols; ++c) + data_slice(r, c) = ids.data_(r, c, idx); + tensor::Tensor data_flipped = data_slice.flip(0); // Write to HDF5 dataset voxel_write_slice(z, dspace, dset, memspace, data_flipped.data()); // update progress bar pb.set_value( - 100. * static_cast(z + 1) / static_cast((pixels_[2]))); + 100. * static_cast(z + 1) / static_cast((pixels()[2]))); } voxel_finalize(dspace, dset, memspace); @@ -1052,7 +1195,10 @@ RayTracePlot::RayTracePlot(pugi::xml_node node) : PlottableInterface(node) check_for_node(node, "field_of_view")) fatal_error("orthographic_width and field_of_view are mutually exclusive " "parameters."); +} +void RayTracePlot::update_view() +{ // Get centerline vector for camera-to-model. We create vectors around this // that form a pixel array, and then trace rays along that. auto up = up_ / up_.norm(); @@ -1079,6 +1225,7 @@ WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node) set_wireframe_thickness(node); set_wireframe_ids(node); set_wireframe_color(node); + update_view(); } void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node) @@ -1180,10 +1327,10 @@ std::pair RayTracePlot::get_pixel_ray( int horiz, int vert) const { // Compute field of view in radians - constexpr double DEGREE_TO_RADIAN = M_PI / 180.0; + constexpr double DEGREE_TO_RADIAN = PI / 180.0; double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN; - double p0 = static_cast(pixels_[0]); - double p1 = static_cast(pixels_[1]); + double p0 = static_cast(pixels()[0]); + double p1 = static_cast(pixels()[1]); double vert_fov_radians = horiz_fov_radians * p1 / p0; // focal_plane_dist can be changed to alter the perspective distortion @@ -1219,16 +1366,17 @@ std::pair RayTracePlot::get_pixel_ray( return result; } -void WireframeRayTracePlot::create_output() const +ImageData WireframeRayTracePlot::create_image() const { - size_t width = pixels_[0]; - size_t height = pixels_[1]; + size_t width = pixels()[0]; + size_t height = pixels()[1]; ImageData data({width, height}, not_found_); // This array marks where the initial wireframe was drawn. We convolve it with // a filter that gets adjusted with the wireframe thickness in order to // thicken the lines. - xt::xtensor wireframe_initial({width, height}, 0); + tensor::Tensor wireframe_initial( + {static_cast(width), static_cast(height)}, 0); /* Holds all of the track segments for the current rendered line of pixels. * old_segments holds a copy of this_line_segments from the previous line. @@ -1245,11 +1393,11 @@ void WireframeRayTracePlot::create_output() const std::vector>> this_line_segments( n_threads); for (int t = 0; t < n_threads; ++t) { - this_line_segments[t].resize(pixels_[0]); + this_line_segments[t].resize(pixels()[0]); } // The last thread writes to this, and the first thread reads from it. - std::vector> old_segments(pixels_[0]); + std::vector> old_segments(pixels()[0]); #pragma omp parallel { @@ -1257,7 +1405,7 @@ void WireframeRayTracePlot::create_output() const const int tid = thread_num(); int vert = tid; - for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) { + for (int iter = 0; iter <= pixels()[1] / n_threads; iter++) { // Save bottom line of current work chunk to compare against later. This // used to be inside the below if block, but it causes a spurious line to @@ -1266,9 +1414,9 @@ void WireframeRayTracePlot::create_output() const if (tid == n_threads - 1) old_segments = this_line_segments[n_threads - 1]; - if (vert < pixels_[1]) { + if (vert < pixels()[1]) { - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { // RayTracePlot implements camera ray generation std::pair ru = get_pixel_ray(horiz, vert); @@ -1330,7 +1478,7 @@ void WireframeRayTracePlot::create_output() const // Now that the horizontal line has finished rendering, we can fill in // wireframe entries that require comparison among all the threads. Hence // the omp barrier being used. It has to be OUTSIDE any if blocks! - if (vert < pixels_[1]) { + if (vert < pixels()[1]) { // Loop over horizontal pixels, checking intersection stack of upper // neighbor @@ -1340,7 +1488,7 @@ void WireframeRayTracePlot::create_output() const else top_cmp = &this_line_segments[tid - 1]; - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { if (!trackstack_equivalent( this_line_segments[tid][horiz], (*top_cmp)[horiz])) { wireframe_initial(horiz, vert) = 1; @@ -1357,8 +1505,8 @@ void WireframeRayTracePlot::create_output() const } // end omp parallel // Now thicken the wireframe lines and apply them to our image - for (int vert = 0; vert < pixels_[1]; ++vert) { - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int vert = 0; vert < pixels()[1]; ++vert) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { if (wireframe_initial(horiz, vert)) { if (wireframe_thickness_ == 1) data(horiz, vert) = wireframe_color_; @@ -1369,19 +1517,21 @@ void WireframeRayTracePlot::create_output() const if (i * i + j * j < wireframe_thickness_ * wireframe_thickness_) { // Check if wireframe pixel is out of bounds - int w_i = std::max(std::min(horiz + i, pixels_[0] - 1), 0); - int w_j = std::max(std::min(vert + j, pixels_[1] - 1), 0); + int w_i = std::max(std::min(horiz + i, pixels()[0] - 1), 0); + int w_j = std::max(std::min(vert + j, pixels()[1] - 1), 0); data(w_i, w_j) = wireframe_color_; } } } } -#ifdef USE_LIBPNG - output_png(path_plot(), data); -#else - output_ppm(path_plot(), data); -#endif + return data; +} + +void WireframeRayTracePlot::create_output() const +{ + ImageData data = create_image(); + write_image(data); } void RayTracePlot::print_info() const @@ -1391,7 +1541,7 @@ void RayTracePlot::print_info() const fmt::print("Look at: {} {} {}\n", look_at_.x, look_at_.y, look_at_.z); fmt::print( "Horizontal field of view: {} degrees\n", horizontal_field_of_view_); - fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]); + fmt::print("Pixels: {} {}\n", pixels()[0], pixels()[1]); } void WireframeRayTracePlot::print_info() const @@ -1473,8 +1623,8 @@ void RayTracePlot::set_pixels(pugi::xml_node node) if (pxls.size() != 2) fatal_error( fmt::format(" must be length 2 in projection plot {}", id())); - pixels_[0] = pxls[0]; - pixels_[1] = pxls[1]; + pixels()[0] = pxls[0]; + pixels()[1] = pxls[1]; } void RayTracePlot::set_camera_position(pugi::xml_node node) @@ -1521,6 +1671,7 @@ SolidRayTracePlot::SolidRayTracePlot(pugi::xml_node node) : RayTracePlot(node) set_opaque_ids(node); set_diffuse_fraction(node); set_light_position(node); + update_view(); } void SolidRayTracePlot::print_info() const @@ -1529,15 +1680,15 @@ void SolidRayTracePlot::print_info() const RayTracePlot::print_info(); } -void SolidRayTracePlot::create_output() const +ImageData SolidRayTracePlot::create_image() const { - size_t width = pixels_[0]; - size_t height = pixels_[1]; + size_t width = pixels()[0]; + size_t height = pixels()[1]; ImageData data({width, height}, not_found_); #pragma omp parallel for schedule(dynamic) collapse(2) - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { - for (int vert = 0; vert < pixels_[1]; ++vert) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { + for (int vert = 0; vert < pixels()[1]; ++vert) { // RayTracePlot implements camera ray generation std::pair ru = get_pixel_ray(horiz, vert); PhongRay ray(ru.first, ru.second, *this); @@ -1546,11 +1697,13 @@ void SolidRayTracePlot::create_output() const } } -#ifdef USE_LIBPNG - output_png(path_plot(), data); -#else - output_ppm(path_plot(), data); -#endif + return data; +} + +void SolidRayTracePlot::create_output() const +{ + ImageData data = create_image(); + write_image(data); } void SolidRayTracePlot::set_opaque_ids(pugi::xml_node node) @@ -1594,144 +1747,6 @@ void SolidRayTracePlot::set_diffuse_fraction(pugi::xml_node node) } } -void Ray::compute_distance() -{ - boundary() = distance_to_boundary(*this); -} - -void Ray::trace() -{ - // To trace the ray from its origin all the way through the model, we have - // to proceed in two phases. In the first, the ray may or may not be found - // inside the model. If the ray is already in the model, phase one can be - // skipped. Otherwise, the ray has to be advanced to the boundary of the - // model where all the cells are defined. Importantly, this is assuming that - // the model is convex, which is a very reasonable assumption for any - // radiation transport model. - // - // After phase one is done, we can starting tracing from cell to cell within - // the model. This step can use neighbor lists to accelerate the ray tracing. - - // Attempt to initialize the particle. We may have to enter a loop to move - // it up to the edge of the model. - bool inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); - - // Advance to the boundary of the model - while (!inside_cell) { - advance_to_boundary_from_void(); - inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); - - // If true this means no surface was intersected. See cell.cpp and search - // for numeric_limits to see where we return it. - if (surface() == std::numeric_limits::max()) { - warning(fmt::format("Lost a ray, r = {}, u = {}", r(), u())); - return; - } - - // Exit this loop and enter into cell-to-cell ray tracing (which uses - // neighbor lists) - if (inside_cell) - break; - - // if there is no intersection with the model, we're done - if (boundary().surface() == SURFACE_NONE) - return; - - event_counter_++; - if (event_counter_ > MAX_INTERSECTIONS) { - warning("Likely infinite loop in ray traced plot"); - return; - } - } - - // Call the specialized logic for this type of ray. This is for the - // intersection for the first intersection if we had one. - if (boundary().surface() != SURFACE_NONE) { - // set the geometry state's surface attribute to be used for - // surface normal computation - surface() = boundary().surface(); - on_intersection(); - if (stop_) - return; - } - - // reset surface attribute to zero after the first intersection so that it - // doesn't perturb surface crossing logic from here on out - surface() = 0; - - // This is the ray tracing loop within the model. It exits after exiting - // the model, which is equivalent to assuming that the model is convex. - // It would be nice to factor out the on_intersection at the end of this - // loop and then do "while (inside_cell)", but we can't guarantee it's - // on a surface in that case. There might be some other way to set it - // up that is perhaps a little more elegant, but this is what works just - // fine. - while (true) { - - compute_distance(); - - // There are no more intersections to process - // if we hit the edge of the model, so stop - // the particle in that case. Also, just exit - // if a negative distance was somehow computed. - if (boundary().distance() == INFTY || boundary().distance() == INFINITY || - boundary().distance() < 0) { - return; - } - - // See below comment where call_on_intersection is checked in an - // if statement for an explanation of this. - bool call_on_intersection {true}; - if (boundary().distance() < 10 * TINY_BIT) { - call_on_intersection = false; - } - - // DAGMC surfaces expect us to go a little bit further than the advance - // distance to properly check cell inclusion. - boundary().distance() += TINY_BIT; - - // Advance particle, prepare for next intersection - for (int lev = 0; lev < n_coord(); ++lev) { - coord(lev).r() += boundary().distance() * coord(lev).u(); - } - surface() = boundary().surface(); - n_coord_last() = n_coord(); - n_coord() = boundary().coord_level(); - if (boundary().lattice_translation()[0] != 0 || - boundary().lattice_translation()[1] != 0 || - boundary().lattice_translation()[2] != 0) { - cross_lattice(*this, boundary(), settings::verbosity >= 10); - } - - // Record how far the ray has traveled - traversal_distance_ += boundary().distance(); - inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10); - - // Call the specialized logic for this type of ray. Note that we do not - // call this if the advance distance is very small. Unfortunately, it seems - // darn near impossible to get the particle advanced to the model boundary - // and through it without sometimes accidentally calling on_intersection - // twice. This incorrectly shades the region as occluded when it might not - // actually be. By screening out intersection distances smaller than a - // threshold 10x larger than the scoot distance used to advance up to the - // model boundary, we can avoid that situation. - if (call_on_intersection) { - on_intersection(); - if (stop_) - return; - } - - if (!inside_cell) - return; - - event_counter_++; - if (event_counter_ > MAX_INTERSECTIONS) { - warning("Likely infinite loop in ray traced plot"); - return; - } - } -} - void ProjectionRay::on_intersection() { // This records a tuple with the following info @@ -1771,7 +1786,10 @@ void PhongRay::on_intersection() // the normal or the diffuse lighting contribution reflected_ = true; result_color_ = plot_.colors_[hit_id]; - Direction to_light = plot_.light_location_ - r(); + // The ray has been advanced slightly past the boundary. Use an + // approximation to the actual hit point for stable normal/lighting. + Position r_hit = r() - TINY_BIT * u(); + Direction to_light = plot_.light_location_ - r_hit; to_light /= to_light.norm(); // TODO @@ -1792,12 +1810,22 @@ void PhongRay::on_intersection() // Get surface pointer const auto& surf = model::surfaces.at(surface_index()); - Direction normal = surf->normal(r_local()); + // The crossed surface may be on a higher coordinate level than the + // innermost local coordinates, so we check the surface's coordinate level + // to find the appropriate coordinate level to use for the normal + // calculation + int surf_level = boundary().coord_level() - 1; + // ensure surface level is within bounds of current coordinate stack + surf_level = std::max(0, std::min(surf_level, n_coord() - 1)); + + Position r_hit_level = + coord(surf_level).r() - TINY_BIT * coord(surf_level).u(); + Direction normal = surf->normal(r_hit_level); normal /= normal.norm(); - // Need to apply translations to find the normal vector in + // Need to apply rotations to find the normal vector in // the base level universe's coordinate system. - for (int lev = n_coord() - 2; lev >= 0; --lev) { + for (int lev = surf_level - 1; lev >= 0; --lev) { if (coord(lev + 1).rotated()) { const Cell& c {*model::cells[coord(lev).cell()]}; normal = normal.inverse_rotate(c.rotation_); @@ -1864,6 +1892,12 @@ void PhongRay::on_intersection() extern "C" int openmc_id_map(const void* plot, int32_t* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_id_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { @@ -1871,7 +1905,7 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1885,14 +1919,20 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) extern "C" int openmc_property_map(const void* plot, double* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_property_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { - set_errmsg("Invalid slice pointer passed to openmc_id_map"); + set_errmsg("Invalid slice pointer passed to openmc_property_map"); return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1904,4 +1944,678 @@ extern "C" int openmc_property_map(const void* plot, double* data_out) return 0; } +extern "C" int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool color_overlaps, + int level, int32_t filter_index, int32_t* geom_data, double* property_data) +{ + // Validate span vectors + Direction u_span_pos {u_span[0], u_span[1], u_span[2]}; + Direction v_span_pos {v_span[0], v_span[1], v_span[2]}; + double u_norm = u_span_pos.norm(); + double v_norm = v_span_pos.norm(); + if (u_norm == 0.0 || v_norm == 0.0) { + set_errmsg("Slice span vectors must be non-zero."); + return OPENMC_E_INVALID_ARGUMENT; + } + + constexpr double ORTHO_REL_TOL = 1e-10; + double dot = u_span_pos.dot(v_span_pos); + if (std::abs(dot) > ORTHO_REL_TOL * u_norm * v_norm) { + set_errmsg("Slice span vectors must be orthogonal."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Validate filter index if provided + if (filter_index >= 0) { + if (int err = verify_filter(filter_index)) + return err; + } + + // Initialize overlap check vector if needed + if (color_overlaps && model::overlap_check_count.size() == 0) { + model::overlap_check_count.resize(model::cells.size()); + } + + try { + // Create a temporary SlicePlotBase object to reuse get_map logic + SlicePlotBase plot_params; + plot_params.origin_ = Position {origin[0], origin[1], origin[2]}; + plot_params.u_span_ = u_span_pos; + plot_params.v_span_ = v_span_pos; + plot_params.pixels_[0] = pixels[0]; + plot_params.pixels_[1] = pixels[1]; + plot_params.show_overlaps_ = color_overlaps; + plot_params.slice_level_ = level; + + // Clear overlap data structures on new slice call + model::overlap_keys.clear(); + model::overlap_key_index.clear(); + + // Use get_map to generate data + auto data = plot_params.get_map(filter_index); + std::copy(data.id_data_.begin(), data.id_data_.end(), geom_data); + + // Copy property data if requested + if (property_data != nullptr) { + std::copy( + data.property_data_.begin(), data.property_data_.end(), property_data); + } + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_UNASSIGNED; + } + + return 0; +} + +// Gets the number of overlaps that we need data for +extern "C" int openmc_slice_data_overlap_count(size_t* count) +{ + if (!count) { + set_errmsg("Null pointer passed for overlap count."); + return OPENMC_E_INVALID_ARGUMENT; + } + *count = model::overlap_keys.size(); + + return 0; +} + +// Plotter pre-allocates array size based on what is returned with +// overlap_count; populates an array of size 3*count +extern "C" int openmc_slice_data_overlap_info( + size_t count, int32_t* overlap_info) +{ + for (size_t i = 0; i < count; ++i) { + overlap_info[i * 3] = model::overlap_keys[i].universe_id; + overlap_info[i * 3 + 1] = model::overlap_keys[i].cell1_id; + overlap_info[i * 3 + 2] = model::overlap_keys[i].cell2_id; + } + + return 0; +} + +extern "C" int openmc_get_plot_index(int32_t id, int32_t* index) +{ + auto it = model::plot_map.find(id); + if (it == model::plot_map.end()) { + set_errmsg("No plot exists with ID=" + std::to_string(id) + "."); + return OPENMC_E_INVALID_ID; + } + + *index = it->second; + return 0; +} + +extern "C" int openmc_plot_get_id(int32_t index, int32_t* id) +{ + if (index < 0 || index >= model::plots.size()) { + set_errmsg("Index in plots array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + *id = model::plots[index]->id(); + return 0; +} + +extern "C" int openmc_plot_set_id(int32_t index, int32_t id) +{ + if (index < 0 || index >= model::plots.size()) { + set_errmsg("Index in plots array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + if (id < 0 && id != C_NONE) { + set_errmsg("Invalid plot ID."); + return OPENMC_E_INVALID_ARGUMENT; + } + + auto* plot = model::plots[index].get(); + int32_t old_id = plot->id(); + if (id == old_id) + return 0; + + model::plot_map.erase(old_id); + try { + plot->set_id(id); + } catch (const std::runtime_error& e) { + model::plot_map[old_id] = index; + set_errmsg(e.what()); + return OPENMC_E_INVALID_ID; + } + model::plot_map[plot->id()] = index; + return 0; +} + +extern "C" size_t openmc_plots_size() +{ + return model::plots.size(); +} + +int map_phong_domain_id( + const SolidRayTracePlot* plot, int32_t id, int32_t* index_out) +{ + if (!plot || !index_out) { + set_errmsg("Invalid plot pointer passed to map_phong_domain_id"); + return OPENMC_E_INVALID_ARGUMENT; + } + + if (plot->color_by_ == PlottableInterface::PlotColorBy::mats) { + auto it = model::material_map.find(id); + if (it == model::material_map.end()) { + set_errmsg("Invalid material ID for SolidRayTracePlot"); + return OPENMC_E_INVALID_ID; + } + *index_out = it->second; + return 0; + } + + if (plot->color_by_ == PlottableInterface::PlotColorBy::cells) { + auto it = model::cell_map.find(id); + if (it == model::cell_map.end()) { + set_errmsg("Invalid cell ID for SolidRayTracePlot"); + return OPENMC_E_INVALID_ID; + } + *index_out = it->second; + return 0; + } + + set_errmsg("Unsupported color_by for SolidRayTracePlot"); + return OPENMC_E_INVALID_TYPE; +} + +int get_solidraytrace_plot_by_index(int32_t index, SolidRayTracePlot** plot) +{ + if (!plot) { + set_errmsg("Null output pointer passed to get_solidraytrace_plot_by_index"); + return OPENMC_E_INVALID_ARGUMENT; + } + + if (index < 0 || index >= model::plots.size()) { + set_errmsg("Index in plots array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + auto* plottable = model::plots[index].get(); + auto* solid_plot = dynamic_cast(plottable); + if (!solid_plot) { + set_errmsg("Plot at index=" + std::to_string(index) + + " is not a solid raytrace plot."); + return OPENMC_E_INVALID_TYPE; + } + + *plot = solid_plot; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_create(int32_t* index) +{ + if (!index) { + set_errmsg( + "Null output pointer passed to openmc_solidraytrace_plot_create"); + return OPENMC_E_INVALID_ARGUMENT; + } + + try { + auto new_plot = std::make_unique(); + new_plot->set_id(); + int32_t new_plot_id = new_plot->id(); +#ifdef USE_LIBPNG + new_plot->path_plot() = fmt::format("plot_{}.png", new_plot_id); +#else + new_plot->path_plot() = fmt::format("plot_{}.ppm", new_plot_id); +#endif + int32_t new_plot_index = model::plots.size(); + model::plots.emplace_back(std::move(new_plot)); + model::plot_map[new_plot_id] = new_plot_index; + *index = new_plot_index; + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_ALLOCATE; + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_pixels( + int32_t index, int32_t* width, int32_t* height) +{ + if (!width || !height) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_pixels"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + *width = plt->pixels()[0]; + *height = plt->pixels()[1]; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_pixels( + int32_t index, int32_t width, int32_t height) +{ + if (width <= 0 || height <= 0) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_set_pixels"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->pixels()[0] = width; + plt->pixels()[1] = height; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_color_by( + int32_t index, int32_t* color_by) +{ + if (!color_by) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_color_by"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (plt->color_by_ == PlottableInterface::PlotColorBy::mats) { + *color_by = 0; + } else if (plt->color_by_ == PlottableInterface::PlotColorBy::cells) { + *color_by = 1; + } else { + set_errmsg("Unsupported color_by for SolidRayTracePlot"); + return OPENMC_E_INVALID_TYPE; + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_color_by( + int32_t index, int32_t color_by) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (color_by == 0) { + plt->color_by_ = PlottableInterface::PlotColorBy::mats; + } else if (color_by == 1) { + plt->color_by_ = PlottableInterface::PlotColorBy::cells; + } else { + set_errmsg("Invalid color_by value for SolidRayTracePlot"); + return OPENMC_E_INVALID_ARGUMENT; + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_default_colors(int32_t index) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->set_default_colors(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_all_opaque(int32_t index) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->opaque_ids().clear(); + if (plt->color_by_ == PlottableInterface::PlotColorBy::mats) { + for (int32_t i = 0; i < model::materials.size(); ++i) { + plt->opaque_ids().insert(i); + } + return 0; + } + + if (plt->color_by_ == PlottableInterface::PlotColorBy::cells) { + for (int32_t i = 0; i < model::cells.size(); ++i) { + plt->opaque_ids().insert(i); + } + return 0; + } + + set_errmsg("Unsupported color_by for SolidRayTracePlot"); + return OPENMC_E_INVALID_TYPE; +} + +extern "C" int openmc_solidraytrace_plot_set_opaque( + int32_t index, int32_t id, bool visible) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + int32_t domain_index = -1; + err = map_phong_domain_id(plt, id, &domain_index); + if (err) + return err; + + if (visible) { + plt->opaque_ids().insert(domain_index); + } else { + plt->opaque_ids().erase(domain_index); + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_color( + int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + int32_t domain_index = -1; + err = map_phong_domain_id(plt, id, &domain_index); + if (err) + return err; + + if (domain_index < 0 || + static_cast(domain_index) >= plt->colors_.size()) { + set_errmsg("Color index out of range for SolidRayTracePlot"); + return OPENMC_E_OUT_OF_BOUNDS; + } + + plt->colors_[domain_index] = RGBColor(r, g, b); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_camera_position( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg("Invalid arguments passed to " + "openmc_solidraytrace_plot_get_camera_position"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& camera_position = plt->camera_position(); + *x = camera_position.x; + *y = camera_position.y; + *z = camera_position.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_camera_position( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->camera_position() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_look_at( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_look_at"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& look_at = plt->look_at(); + *x = look_at.x; + *y = look_at.y; + *z = look_at.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_look_at( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->look_at() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_up( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg("Invalid arguments passed to openmc_solidraytrace_plot_get_up"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& up = plt->up(); + *x = up.x; + *y = up.y; + *z = up.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_up( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->up() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_light_position( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg("Invalid arguments passed to " + "openmc_solidraytrace_plot_get_light_position"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& light_position = plt->light_location(); + *x = light_position.x; + *y = light_position.y; + *z = light_position.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_light_position( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->light_location() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov) +{ + if (!fov) { + set_errmsg("Invalid arguments passed to openmc_solidraytrace_plot_get_fov"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + *fov = plt->horizontal_field_of_view(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_fov(int32_t index, double fov) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->horizontal_field_of_view() = fov; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_update_view(int32_t index) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->update_view(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_create_image( + int32_t index, uint8_t* data_out, int32_t width, int32_t height) +{ + if (!data_out || width <= 0 || height <= 0) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_create_image"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (plt->pixels()[0] != width || plt->pixels()[1] != height) { + set_errmsg( + "Requested image size does not match SolidRayTracePlot pixel settings"); + return OPENMC_E_INVALID_SIZE; + } + + ImageData data = plt->create_image(); + if (static_cast(data.shape()[0]) != width || + static_cast(data.shape()[1]) != height) { + set_errmsg("Unexpected image size from SolidRayTracePlot create_image"); + return OPENMC_E_INVALID_SIZE; + } + + for (int32_t y = 0; y < height; ++y) { + for (int32_t x = 0; x < width; ++x) { + const auto& color = data(x, y); + size_t idx = (static_cast(y) * width + x) * 3; + data_out[idx + 0] = color.red; + data_out[idx + 1] = color.green; + data_out[idx + 2] = color.blue; + } + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_color( + int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b) +{ + if (!r || !g || !b) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_color"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + int32_t domain_index = -1; + err = map_phong_domain_id(plt, id, &domain_index); + if (err) + return err; + + if (domain_index < 0 || + static_cast(domain_index) >= plt->colors_.size()) { + set_errmsg("Color index out of range for SolidRayTracePlot"); + return OPENMC_E_OUT_OF_BOUNDS; + } + + const auto& color = plt->colors_[domain_index]; + *r = color.red; + *g = color.green; + *b = color.blue; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_diffuse_fraction( + int32_t index, double* diffuse_fraction) +{ + if (!diffuse_fraction) { + set_errmsg("Invalid arguments passed to " + "openmc_solidraytrace_plot_get_diffuse_fraction"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + *diffuse_fraction = plt->diffuse_fraction(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_diffuse_fraction( + int32_t index, double diffuse_fraction) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (diffuse_fraction < 0.0 || diffuse_fraction > 1.0) { + set_errmsg("Diffuse fraction must be between 0 and 1"); + return OPENMC_E_INVALID_ARGUMENT; + } + + plt->diffuse_fraction() = diffuse_fraction; + return 0; +} + } // namespace openmc diff --git a/src/random_lcg.cpp b/src/random_lcg.cpp index 29457569b..f2a81fc1c 100644 --- a/src/random_lcg.cpp +++ b/src/random_lcg.cpp @@ -12,6 +12,45 @@ constexpr uint64_t prn_mult {6364136223846793005ULL}; // multiplication constexpr uint64_t prn_add {1442695040888963407ULL}; // additive factor, c uint64_t prn_stride {DEFAULT_STRIDE}; // stride between particles +namespace { + +struct SkipAheadCoefficients { + uint64_t multiplier; + uint64_t increment; +}; + +SkipAheadCoefficients future_seed_coefficients(uint64_t n) +{ + // The algorithm here to determine the parameters used to skip ahead is + // described in F. Brown, "Random Number Generation with Arbitrary Stride," + // Trans. Am. Nucl. Soc. (Nov. 1994). This algorithm is able to skip ahead in + // O(log2(N)) operations instead of O(N). Basically, it computes parameters G + // and C which can then be used to find x_N = G*x_0 + C mod 2^M. + + // Initialize constants + uint64_t g {prn_mult}; + uint64_t c {prn_add}; + uint64_t g_new {1}; + uint64_t c_new {0}; + + while (n > 0) { + // Check if the least significant bit is 1. + if (n & 1) { + g_new *= g; + c_new = c_new * g + c; + } + c *= (g + 1); + g *= g; + + // Move bits right, dropping least significant bit. + n >>= 1; + } + + return {g_new, c_new}; +} + +} // namespace + //============================================================================== // PRN //============================================================================== @@ -69,9 +108,10 @@ uint64_t init_seed(int64_t id, int offset) void init_particle_seeds(int64_t id, uint64_t* seeds) { + auto [multiplier, increment] = + future_seed_coefficients(static_cast(id) * prn_stride); for (int i = 0; i < N_STREAMS; i++) { - seeds[i] = - future_seed(static_cast(id) * prn_stride, master_seed + i); + seeds[i] = multiplier * (master_seed + i) + increment; } } @@ -90,33 +130,9 @@ void advance_prn_seed(int64_t n, uint64_t* seed) uint64_t future_seed(uint64_t n, uint64_t seed) { - // The algorithm here to determine the parameters used to skip ahead is - // described in F. Brown, "Random Number Generation with Arbitrary Stride," - // Trans. Am. Nucl. Soc. (Nov. 1994). This algorithm is able to skip ahead in - // O(log2(N)) operations instead of O(N). Basically, it computes parameters G - // and C which can then be used to find x_N = G*x_0 + C mod 2^M. - - // Initialize constants - uint64_t g {prn_mult}; - uint64_t c {prn_add}; - uint64_t g_new {1}; - uint64_t c_new {0}; - - while (n > 0) { - // Check if the least significant bit is 1. - if (n & 1) { - g_new *= g; - c_new = c_new * g + c; - } - c *= (g + 1); - g *= g; - - // Move bits right, dropping least significant bit. - n >>= 1; - } - // With G and C, we can now find the new seed. - return g_new * seed + c_new; + auto [multiplier, increment] = future_seed_coefficients(n); + return multiplier * seed + increment; } //============================================================================== diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 1bf27e1ed..83128fdaa 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -18,6 +18,7 @@ #include "openmc/weight_windows.h" #include +#include namespace openmc { @@ -29,10 +30,13 @@ namespace openmc { RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; -bool FlatSourceDomain::adjoint_ {false}; +bool FlatSourceDomain::adjoint_requested_ {false}; +RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD}; +bool FlatSourceDomain::fw_cadis_local_ {false}; double FlatSourceDomain::diagonal_stabilization_rho_ {1.0}; std::unordered_map>> FlatSourceDomain::mesh_domain_map_; +std::vector FlatSourceDomain::fw_cadis_local_targets_; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { @@ -63,8 +67,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Create a new 2D tensor with the same size as the first // two dimensions of the 3D tensor - tally_volumes_[i] = - xt::xtensor::from_shape({shape[0], shape[1]}); + tally_volumes_[i] = tensor::Tensor({shape[0], shape[1]}); } } @@ -109,22 +112,29 @@ void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Add scattering + fission source int material = srh.material(); + int temp = srh.temperature_idx(); + double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; + const int material_offset = (material * ntemperature_ + temp) * negroups_; + const int scatter_offset = + (material * ntemperature_ + temp) * negroups_ * negroups_; for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out]; + double sigma_t = sigma_t_[material_offset + g_out] * density_mult; double scatter_source = 0.0; double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { double scalar_flux = srh.scalar_flux_old(g_in); double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; - double chi = chi_[material * negroups_ + g_out]; + sigma_s_[scatter_offset + g_out * negroups_ + g_in] * density_mult; + double nu_sigma_f = nu_sigma_f_[material_offset + g_in] * density_mult; + double chi = chi_[material_offset + g_out]; scatter_source += sigma_s * scalar_flux; - fission_source += nu_sigma_f * scalar_flux * chi; + if (settings::create_fission_neutrons) { + fission_source += nu_sigma_f * scalar_flux * chi; + } } srh.source(g_out) = (scatter_source + fission_source * inverse_k_eff) / sigma_t; @@ -188,6 +198,7 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { source_regions_.scalar_flux_new(sr, g) /= volume; if (settings::run_mode == RunMode::FIXED_SOURCE) { @@ -196,7 +207,9 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( source_regions_.volume_sq(sr); } } else { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } @@ -322,6 +335,7 @@ void FlatSourceDomain::compute_k_eff() } int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { continue; } @@ -330,7 +344,9 @@ void FlatSourceDomain::compute_k_eff() double sr_fission_source_new = 0; for (int g = 0; g < negroups_; g++) { - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g]; + double nu_sigma_f = + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); sr_fission_source_old += nu_sigma_f * source_regions_.scalar_flux_old(sr, g); sr_fission_source_new += @@ -369,6 +385,7 @@ void FlatSourceDomain::compute_k_eff() // Adds entropy value to shared entropy vector in openmc namespace. simulation::entropy.push_back(H); + fission_rate_ = fission_rate_new; k_eff_ = k_eff_new; } @@ -519,18 +536,40 @@ void FlatSourceDomain::reset_tally_volumes() // simulation double FlatSourceDomain::compute_fixed_source_normalization_factor() const { - // If we are not in fixed source mode, then there are no external sources - // so no normalization is needed. - if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) { + // Eigenvalue mode normalization + if (settings::run_mode == RunMode::EIGENVALUE) { + // Normalize fluxes by total number of fission neutrons produced. This + // ensures consistent scaling of the eigenvector such that its magnitude is + // comparable to the eigenvector produced by the Monte Carlo solver. + // Multiplying by the eigenvalue is unintuitive, but it is necessary. + // If the eigenvalue is 1.2, per starting source neutron, you will + // generate 1.2 neutrons. Thus if we normalize to generating only ONE + // neutron in total for the whole domain, then we don't actually have enough + // flux to generate the required 1.2 neutrons. We only know the flux + // required to generate 1 neutron (which would have required less than one + // starting neutron). Thus, you have to scale the flux up by the eigenvalue + // such that 1.2 neutrons are generated, so as to be consistent with the + // bookkeeping in MC which is all done per starting source neutron (not per + // neutron produced). + return k_eff_ / (fission_rate_ * simulation_volume_); + } + + // If we are in adjoint mode of a fixed source problem, the external + // source is already normalized, such that all resulting fluxes are + // also normalized. + if (solve_ == RandomRaySolve::ADJOINT) { return 1.0; } + // Fixed source mode normalization + // Step 1 is to sum over all source regions and energy groups to get the // total external source strength in the simulation. double simulation_external_source_strength = 0.0; #pragma omp parallel for reduction(+ : simulation_external_source_strength) for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { // For non-void regions, we store the external source pre-divided by @@ -538,7 +577,8 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // to get the total source strength in the expected units. double sigma_t = 1.0; if (material != MATERIAL_VOID) { - sigma_t = sigma_t_[material * negroups_ + g]; + sigma_t = sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); } simulation_external_source_strength += source_regions_.external_source(sr, g) * sigma_t * volume; @@ -595,6 +635,8 @@ void FlatSourceDomain::random_ray_tally() double volume = source_regions_.volume(sr) * simulation_volume_; double material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); + double density_mult = source_regions_.density_mult(sr); for (int g = 0; g < negroups_; g++) { double flux = source_regions_.scalar_flux_new(sr, g) * source_normalization_factor; @@ -610,19 +652,28 @@ void FlatSourceDomain::random_ray_tally() case SCORE_TOTAL: if (material != MATERIAL_VOID) { - score = flux * volume * sigma_t_[material * negroups_ + g]; + score = + flux * volume * + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; case SCORE_FISSION: if (material != MATERIAL_VOID) { - score = flux * volume * sigma_f_[material * negroups_ + g]; + score = + flux * volume * + sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; case SCORE_NU_FISSION: if (material != MATERIAL_VOID) { - score = flux * volume * nu_sigma_f_[material * negroups_ + g]; + score = + flux * volume * + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; @@ -630,10 +681,17 @@ void FlatSourceDomain::random_ray_tally() score = 1.0; break; + case SCORE_KAPPA_FISSION: + score = + flux * volume * + kappa_fission_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; + break; + default: fatal_error("Invalid score specified in tallies.xml. Only flux, " - "total, fission, nu-fission, and events are supported in " - "random ray mode."); + "total, fission, nu-fission, kappa-fission, and events " + "are supported in random ray mode."); break; } // Apply score to the appropriate tally bin @@ -667,7 +725,7 @@ void FlatSourceDomain::random_ray_tally() for (int i = 0; i < model::tallies.size(); i++) { Tally& tally {*model::tallies[i]}; #pragma omp parallel for - for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int64_t bin = 0; bin < tally.n_filter_bins(); bin++) { for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { auto score_type = tally.scores_[score_idx]; if (score_type == SCORE_FLUX) { @@ -738,6 +796,12 @@ void FlatSourceDomain::output_to_vtk() const double z_delta = width.z / Nz; std::string filename = openmc_plot->path_plot(); + // Tag plots written during the forward solve of an adjoint run + if (solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) { + auto dot = filename.find_last_of('.'); + filename = filename.substr(0, dot) + ".forward" + filename.substr(dot); + } + // Perform sanity checks on file size uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float); write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)", @@ -788,7 +852,7 @@ void FlatSourceDomain::output_to_vtk() const voxel_positions[z * Ny * Nx + y * Nx + x] = sample; if (variance_reduction::weight_windows.size() == 1) { - WeightWindow ww = + auto [ww_found, ww] = variance_reduction::weight_windows[0]->get_weight_window(p); float weight = ww.lower_weight; weight_windows[z * Ny * Nx + y * Nx + x] = weight; @@ -885,11 +949,14 @@ void FlatSourceDomain::output_to_vtk() const float total_fission = 0.0; if (fsr >= 0) { int mat = source_regions_.material(fsr); + int temp = source_regions_.temperature_idx(fsr); if (mat != MATERIAL_VOID) { for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g]; + double sigma_f = + sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(fsr); total_fission += sigma_f * flux; } } @@ -903,6 +970,7 @@ void FlatSourceDomain::output_to_vtk() const for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; int mat = source_regions_.material(fsr); + int temp = source_regions_.temperature_idx(fsr); float total_external = 0.0f; if (fsr >= 0) { for (int g = 0; g < negroups_; g++) { @@ -910,7 +978,8 @@ void FlatSourceDomain::output_to_vtk() const // multiply it back to get the true external source. double sigma_t = 1.0; if (mat != MATERIAL_VOID) { - sigma_t = sigma_t_[mat * negroups_ + g]; + sigma_t = sigma_t_[(mat * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(fsr); } total_external += source_regions_.external_source(fsr, g) * sigma_t; } @@ -940,7 +1009,10 @@ void FlatSourceDomain::output_to_vtk() const void FlatSourceDomain::apply_external_source_to_source_region( int src_idx, SourceRegionHandle& srh) { - auto s = model::external_sources[src_idx].get(); + auto s = + (solve_ == RandomRaySolve::ADJOINT && !model::adjoint_sources.empty()) + ? model::adjoint_sources[src_idx].get() + : model::external_sources[src_idx].get(); auto is = dynamic_cast(s); auto discrete = dynamic_cast(is->energy()); double strength_factor = is->strength(); @@ -1011,13 +1083,17 @@ void FlatSourceDomain::count_external_source_regions() } } -void FlatSourceDomain::convert_external_sources() +void FlatSourceDomain::convert_external_sources(bool use_adjoint_sources) { + // Determine whether forward or (local) adjoint sources are desired + const auto& sources = + use_adjoint_sources ? model::adjoint_sources : model::external_sources; + // Loop over external sources - for (int es = 0; es < model::external_sources.size(); es++) { + for (int es = 0; es < sources.size(); es++) { // Extract source information - Source* s = model::external_sources[es].get(); + Source* s = sources[es].get(); IndependentSource* is = dynamic_cast(s); Discrete* energy = dynamic_cast(is->energy()); const std::unordered_set& domain_ids = is->domain_ids(); @@ -1089,69 +1165,81 @@ void FlatSourceDomain::flatten_xs() { // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single angle data. - const int t = 0; + // TODO: Currently assumes we are only using single angle data. const int a = 0; n_materials_ = data::mg.macro_xs_.size(); + ntemperature_ = 1; + for (int i = 0; i < n_materials_; i++) { + ntemperature_ = + std::max(ntemperature_, data::mg.macro_xs_[i].n_temperature_points()); + } + for (int i = 0; i < n_materials_; i++) { auto& m = data::mg.macro_xs_[i]; - for (int g_out = 0; g_out < negroups_; g_out++) { - if (m.exists_in_model) { - double sigma_t = - m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); - sigma_t_.push_back(sigma_t); + for (int t = 0; t < ntemperature_; t++) { + for (int g_out = 0; g_out < negroups_; g_out++) { + if (m.exists_in_model && t < m.n_temperature_points()) { + double sigma_t = + m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); + sigma_t_.push_back(sigma_t); - if (sigma_t < MINIMUM_MACRO_XS) { - Material* mat = model::materials[i].get(); - warning(fmt::format( - "Material \"{}\" (id: {}) has a group {} total cross section " - "({:.3e}) below the minimum threshold " - "({:.3e}). Material will be treated as pure void.", - mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS)); - } + if (sigma_t < MINIMUM_MACRO_XS) { + Material* mat = model::materials[i].get(); + warning(fmt::format( + "Material \"{}\" (id: {}) has a group {} total cross section " + "({:.3e}) below the minimum threshold " + "({:.3e}). Material will be treated as pure void.", + mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS)); + } - double nu_sigma_f = - m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); - nu_sigma_f_.push_back(nu_sigma_f); + double nu_sigma_f = + m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); + nu_sigma_f_.push_back(nu_sigma_f); - double sigma_f = - m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); - sigma_f_.push_back(sigma_f); + double sigma_f = + m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); + sigma_f_.push_back(sigma_f); - double chi = - m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); - if (!std::isfinite(chi)) { - // MGXS interface may return NaN in some cases, such as when material - // is fissionable but has very small sigma_f. - chi = 0.0; - } - chi_.push_back(chi); + double chi = + m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); + if (!std::isfinite(chi)) { + // MGXS interface may return NaN in some cases, such as when + // material is fissionable but has very small sigma_f. + chi = 0.0; + } + chi_.push_back(chi); - for (int g_in = 0; g_in < negroups_; g_in++) { - double sigma_s = - m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); - sigma_s_.push_back(sigma_s); - // For transport corrected XS data, diagonal elements may be negative. - // In this case, set a flag to enable transport stabilization for the - // simulation. - if (g_out == g_in && sigma_s < 0.0) - is_transport_stabilization_needed_ = true; - } - } else { - sigma_t_.push_back(0); - nu_sigma_f_.push_back(0); - sigma_f_.push_back(0); - chi_.push_back(0); - for (int g_in = 0; g_in < negroups_; g_in++) { - sigma_s_.push_back(0); + double kappa_fission = + m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a); + kappa_fission_.push_back(kappa_fission); + + for (int g_in = 0; g_in < negroups_; g_in++) { + double sigma_s = + m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); + sigma_s_.push_back(sigma_s); + // For transport corrected XS data, diagonal elements may be + // negative. In this case, set a flag to enable transport + // stabilization for the simulation. + if (g_out == g_in && sigma_s < 0.0) + is_transport_stabilization_needed_ = true; + } + } else { + sigma_t_.push_back(0); + nu_sigma_f_.push_back(0); + sigma_f_.push_back(0); + chi_.push_back(0); + kappa_fission_.push_back(0); + for (int g_in = 0; g_in < negroups_; g_in++) { + sigma_s_.push_back(0); + } } } } } } -void FlatSourceDomain::set_adjoint_sources() +void FlatSourceDomain::set_fw_adjoint_sources() { // Set the adjoint external source to 1/forward_flux. If the forward flux is // negative, zero, or extremely close to zero, set the adjoint source to zero, @@ -1180,6 +1268,10 @@ void FlatSourceDomain::set_adjoint_sources() source_regions_.external_source(sr, g) = 0.0; } else { source_regions_.external_source(sr, g) = 1.0 / flux; + if (!std::isfinite(source_regions_.external_source(sr, g))) { + // If the flux is NaN or Inf, set the adjoint source to zero + source_regions_.external_source(sr, g) = 0.0; + } } if (flux > 0.0) { source_regions_.external_source_present(sr) = 1; @@ -1211,34 +1303,119 @@ void FlatSourceDomain::set_adjoint_sources() source_regions_.external_source_present(sr) = 0; } } + // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { continue; } for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); source_regions_.external_source(sr, g) /= sigma_t; + if (!std::isfinite(source_regions_.external_source(sr, g))) { + // If the flux is NaN or Inf, set the adjoint source to zero + source_regions_.external_source(sr, g) = 0.0; + } } } + + if (fw_cadis_local_) { +// Only external sources that have a non-mesh type tally task should remain +// non-zero. Everything else gets zero'd out. +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + + // If there is already no external source, don't need to do anything + if (source_regions_.external_source_present(sr) == 0) { + continue; + } + + // If there is an adjoint source term here, then we need to check it. + + // We will track if ANY group has a valid local FW-CADIS source term + bool has_any_sources = false; + + // Now, loop over groups + for (int g = 0; g < negroups_; g++) { + + // If there are no tally tasks associated with this source element + // then it is not a local FW-CADIS source, so we continue to the next + // group + if (source_regions_.tally_task(sr, g).empty()) { + source_regions_.external_source(sr, g) = 0.0; + continue; + } + + // If there are tally tasks, we can through them and check if + // any of them are local FW-CADIS targets. + + // We track if ANY of the tasks are local FW-CADIS target tallies + bool local_fw_cadis_target_region = false; + + // Now we loop through + for (const auto& task : source_regions_.tally_task(sr, g)) { + Tally& tally {*model::tallies[task.tally_idx]}; + const auto t_id = tally.id(); + + // Search for target tallies + if (std::find(fw_cadis_local_targets_.begin(), + fw_cadis_local_targets_.end(), + t_id) != fw_cadis_local_targets_.end()) { + local_fw_cadis_target_region = true; + break; + } + } + + // If ANY of the tasks is a local FW-CADIS target, + // Then we keep the source term and set that this + // source region has a valid FW-CADIS source term. + // Otherwise, we zero out the source term. + if (local_fw_cadis_target_region) { + has_any_sources = true; + } else { + source_regions_.external_source(sr, g) = 0.0; + } + } // End loop over groups + + // If there were any valid FW-CADIS source terms for any + // of the groups, then the SR as a whole counts as a source + if (has_any_sources) { + source_regions_.external_source_present(sr) = 1; + } else { + source_regions_.external_source_present(sr) = 0; + } + } // End loop over source regions + } // End local FW-CADIS logic +} + +void FlatSourceDomain::set_local_adjoint_sources() +{ + // Set the external source to user-specified adjoint sources. + convert_external_sources(true); } void FlatSourceDomain::transpose_scattering_matrix() { // Transpose the inner two dimensions for each material +#pragma omp parallel for for (int m = 0; m < n_materials_; ++m) { - int material_offset = m * negroups_ * negroups_; - for (int i = 0; i < negroups_; ++i) { - for (int j = i + 1; j < negroups_; ++j) { - // Calculate indices of the elements to swap - int idx1 = material_offset + i * negroups_ + j; - int idx2 = material_offset + j * negroups_ + i; + for (int t = 0; t < ntemperature_; t++) { + int material_offset = (m * ntemperature_ + t) * negroups_ * negroups_; + for (int i = 0; i < negroups_; ++i) { + for (int j = i + 1; j < negroups_; ++j) { + // Calculate indices of the elements to swap + int idx1 = material_offset + i * negroups_ + j; + int idx2 = material_offset + j * negroups_ + i; - // Swap the elements to transpose the matrix - std::swap(sigma_s_[idx1], sigma_s_[idx2]); + // Swap the elements to transpose the matrix + std::swap(sigma_s_[idx1], sigma_s_[idx2]); + } } } } @@ -1458,18 +1635,28 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( int gs_i_cell = gs.lowest_coord().cell(); Cell& cell = *model::cells[gs_i_cell]; int material = cell.material(gs.cell_instance()); + int temp = 0; // If material total XS is extremely low, just set it to void to avoid // problems with 1/Sigma_t - for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - if (sigma_t < MINIMUM_MACRO_XS) { - material = MATERIAL_VOID; - break; + if (material != MATERIAL_VOID) { + temp = data::mg.macro_xs_[material].get_temperature_index( + cell.sqrtkT(gs.cell_instance())); + for (int g = 0; g < negroups_; g++) { + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g]; + if (sigma_t < MINIMUM_MACRO_XS) { + material = MATERIAL_VOID; + temp = 0; + break; + } } } handle.material() = material; + handle.temperature_idx() = temp; + + handle.density_mult() = cell.density_mult(gs.cell_instance()); // Store the mesh index (if any) assigned to this source region handle.mesh() = mesh_idx; @@ -1499,7 +1686,9 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( // Divide external source term by sigma_t if (material != C_NONE) { for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + handle.density_mult(); handle.external_source(g) /= sigma_t; } } @@ -1574,6 +1763,8 @@ void FlatSourceDomain::apply_transport_stabilization() #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); + double density_mult = source_regions_.density_mult(sr); if (material == MATERIAL_VOID) { continue; } @@ -1581,9 +1772,14 @@ void FlatSourceDomain::apply_transport_stabilization() // Only apply stabilization if the diagonal (in-group) scattering XS is // negative double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g]; + sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) * + negroups_ + + g] * + density_mult; if (sigma_s < 0.0) { - double sigma_t = sigma_t_[material * negroups_ + g]; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; double phi_new = source_regions_.scalar_flux_new(sr, g); double phi_old = source_regions_.scalar_flux_old(sr, g); diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index e1ad68e3d..b4701ed1f 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -43,12 +43,16 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Add scattering + fission source int material = srh.material(); + int temp = srh.temperature_idx(); + double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; MomentMatrix invM = srh.mom_matrix().inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out]; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g_out] * + density_mult; double scatter_flat = 0.0f; double fission_flat = 0.0f; @@ -62,15 +66,23 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Handles for cross sections double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; - double chi = chi_[material * negroups_ + g_out]; + sigma_s_[((material * ntemperature_ + temp) * negroups_ + g_out) * + negroups_ + + g_in] * + density_mult; + double nu_sigma_f = + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g_in] * + density_mult; + double chi = + chi_[(material * ntemperature_ + temp) * negroups_ + g_out]; // Compute source terms for flat and linear components of the flux scatter_flat += sigma_s * flux_flat; - fission_flat += nu_sigma_f * flux_flat * chi; scatter_linear += sigma_s * flux_linear; - fission_linear += nu_sigma_f * flux_linear * chi; + if (settings::create_fission_neutrons) { + fission_flat += nu_sigma_f * flux_flat * chi; + fission_linear += nu_sigma_f * flux_linear * chi; + } } // Compute the flat source term diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index c19d136a4..dde5023e4 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -10,6 +10,8 @@ #include "openmc/settings.h" #include "openmc/simulation.h" +#include + #include "openmc/distribution_spatial.h" #include "openmc/random_dist.h" #include "openmc/source.h" @@ -432,10 +434,13 @@ void RandomRay::attenuate_flux_flat_source( // Get material int material = srh.material(); + int temp = srh.temperature_idx(); // MOC incoming flux attenuation + source contribution/attenuation equation for (int g = 0; g < negroups_; g++) { - float sigma_t = domain_->sigma_t_[material * negroups_ + g]; + float sigma_t = + domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + srh.density_mult(); float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential; @@ -530,6 +535,7 @@ void RandomRay::attenuate_flux_linear_source( n_event()++; int material = srh.material(); + int temp = srh.temperature_idx(); Position& centroid = srh.centroid(); Position midpoint = r + u() * (distance / 2.0); @@ -558,7 +564,9 @@ void RandomRay::attenuate_flux_linear_source( for (int g = 0; g < negroups_; g++) { // Compute tau, the optical thickness of the ray segment - float sigma_t = domain_->sigma_t_[material * negroups_ + g]; + float sigma_t = + domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + srh.density_mult(); float tau = sigma_t * distance; // If tau is very small, set it to zero to avoid numerical issues. @@ -763,6 +771,7 @@ void RandomRay::attenuate_flux_linear_source_void( void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) { domain_ = domain; + ntemperature_ = domain->ntemperature_; // Reset particle event counter n_event() = 0; @@ -783,6 +792,9 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) case RandomRaySampleMethod::HALTON: site = sample_halton(); break; + case RandomRaySampleMethod::S2: + site = sample_s2(); + break; default: fatal_error("Unknown sample method for random ray transport."); } @@ -865,4 +877,27 @@ SourceSite RandomRay::sample_halton() return site; } +SourceSite RandomRay::sample_s2() +{ + // set random number seed + int64_t particle_seed = + (simulation::current_batch - 1) * settings::n_particles + id(); + init_particle_seeds(particle_seed, seeds()); + stream() = STREAM_TRACKING; + + // Get spatial component of the ray_source_ + SpatialDistribution* space = + dynamic_cast(RandomRay::ray_source_.get())->space(); + + SourceSite site; + + // Sample spatial distribution + site.r = space->sample(current_seed()).first; + + // Sample either left or right for S2 (flashlight) transport. + site.u = {prn(current_seed()) < 0.5 ? -1.0 : 1.0, 0.0, 0.0}; + + return site; +} + } // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index d475b2593..00fff99a7 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -1,5 +1,6 @@ #include "openmc/random_ray/random_ray_simulation.h" +#include "openmc/capi.h" #include "openmc/eigenvalue.h" #include "openmc/geometry.h" #include "openmc/message_passing.h" @@ -22,109 +23,6 @@ namespace openmc { // Non-member functions //============================================================================== -void openmc_run_random_ray() -{ - ////////////////////////////////////////////////////////// - // Run forward simulation - ////////////////////////////////////////////////////////// - - // Check if adjoint calculation is needed. If it is, we will run the forward - // calculation first and then the adjoint calculation later. - bool adjoint_needed = FlatSourceDomain::adjoint_; - - // Configure the domain for forward simulation - FlatSourceDomain::adjoint_ = false; - - // If we're going to do an adjoint simulation afterwards, report that this is - // the initial forward flux solve. - if (adjoint_needed && mpi::master) - header("FORWARD FLUX SOLVE", 3); - - // Initialize OpenMC general data structures - openmc_simulation_init(); - - // Validate that inputs meet requirements for random ray mode - if (mpi::master) - validate_random_ray_inputs(); - - // Initialize Random Ray Simulation Object - RandomRaySimulation sim; - - // Initialize fixed sources, if present - sim.apply_fixed_sources_and_mesh_domains(); - - // Begin main simulation timer - simulation::time_total.start(); - - // Execute random ray simulation - sim.simulate(); - - // End main simulation timer - simulation::time_total.stop(); - - // Normalize and save the final forward flux - double source_normalization_factor = - sim.domain()->compute_fixed_source_normalization_factor() / - (settings::n_batches - settings::n_inactive); - -#pragma omp parallel for - for (uint64_t se = 0; se < sim.domain()->n_source_elements(); se++) { - sim.domain()->source_regions_.scalar_flux_final(se) *= - source_normalization_factor; - } - - // Finalize OpenMC - openmc_simulation_finalize(); - - // Output all simulation results - sim.output_simulation_results(); - - ////////////////////////////////////////////////////////// - // Run adjoint simulation (if enabled) - ////////////////////////////////////////////////////////// - - if (!adjoint_needed) { - return; - } - - reset_timers(); - - // Configure the domain for adjoint simulation - FlatSourceDomain::adjoint_ = true; - - if (mpi::master) - header("ADJOINT FLUX SOLVE", 3); - - // Initialize OpenMC general data structures - openmc_simulation_init(); - - sim.domain()->k_eff_ = 1.0; - - // Initialize adjoint fixed sources, if present - sim.prepare_fixed_sources_adjoint(); - - // Transpose scattering matrix - sim.domain()->transpose_scattering_matrix(); - - // Swap nu_sigma_f and chi - sim.domain()->nu_sigma_f_.swap(sim.domain()->chi_); - - // Begin main simulation timer - simulation::time_total.start(); - - // Execute random ray simulation - sim.simulate(); - - // End main simulation timer - simulation::time_total.stop(); - - // Finalize OpenMC - openmc_simulation_finalize(); - - // Output all simulation results - sim.output_simulation_results(); -} - // Enforces restrictions on inputs in random ray mode. While there are // many features that don't make sense in random ray mode, and are therefore // unsupported, we limit our testing/enforcement operations only to inputs @@ -143,11 +41,12 @@ void validate_random_ray_inputs() case SCORE_FISSION: case SCORE_NU_FISSION: case SCORE_EVENTS: + case SCORE_KAPPA_FISSION: break; default: fatal_error( - "Invalid score specified. Only flux, total, fission, nu-fission, and " - "event scores are supported in random ray mode."); + "Invalid score specified. Only flux, total, fission, nu-fission, " + "kappa-fission, and event scores are supported in random ray mode."); } } @@ -180,10 +79,6 @@ void validate_random_ray_inputs() fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets " "supported in random ray mode."); } - if (material.get_xsdata().size() > 1) { - warning("Non-isothermal MGXS detected. Only isothermal XS data sets " - "supported in random ray mode. Using lowest temperature."); - } for (int g = 0; g < data::mg.num_energy_groups_; g++) { if (material.exists_in_model) { // Temperature and angle indices, if using multiple temperature @@ -273,14 +168,12 @@ void validate_random_ray_inputs() "constrained by domain id (cell, material, or universe) in " "random ray mode."); } else if (is->domain_ids().size() > 0 && sp) { - // If both a domain constraint and a non-default point source location - // are specified, notify user that domain constraint takes precedence. - if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) { - warning("Fixed source has both a domain constraint and a point " - "type spatial distribution. The domain constraint takes " - "precedence in random ray mode -- point source coordinate " - "will be ignored."); - } + // If both a domain constraint and a point source location are + // specified, notify user that domain constraint takes precedence. + warning("Fixed source has both a domain constraint and a point " + "type spatial distribution. The domain constraint takes " + "precedence in random ray mode -- point source coordinate " + "will be ignored."); } // Check that a discrete energy distribution was used @@ -294,6 +187,56 @@ void validate_random_ray_inputs() } } + // Validate adjoint sources + /////////////////////////////////////////////////////////////////// + if (FlatSourceDomain::adjoint_requested_ && !model::adjoint_sources.empty()) { + for (int i = 0; i < model::adjoint_sources.size(); i++) { + Source* s = model::adjoint_sources[i].get(); + + // Check for independent source + IndependentSource* is = dynamic_cast(s); + + if (!is) { + fatal_error( + "Only IndependentSource adjoint source types are allowed in " + "random ray mode"); + } + + // Check for isotropic source + UnitSphereDistribution* angle_dist = is->angle(); + Isotropic* id = dynamic_cast(angle_dist); + if (!id) { + fatal_error( + "Invalid source definition -- only isotropic adjoint sources are " + "allowed in random ray mode."); + } + + // Validate that a domain ID was specified OR that it is a point source + auto sp = dynamic_cast(is->space()); + if (is->domain_ids().size() == 0 && !sp) { + fatal_error("Adjoint sources must be point source or spatially " + "constrained by domain id (cell, material, or universe) in " + "random ray mode."); + } else if (is->domain_ids().size() > 0 && sp) { + // If both a domain constraint and a point source location are + // specified, notify user that domain constraint takes precedence. + warning("Adjoint source has both a domain constraint and a point " + "type spatial distribution. The domain constraint takes " + "precedence in random ray mode -- point source coordinate " + "will be ignored."); + } + + // Check that a discrete energy distribution was used + Distribution* d = is->energy(); + Discrete* dd = dynamic_cast(d); + if (!dd) { + fatal_error( + "Only discrete (multigroup) energy distributions are allowed for " + "adjoint sources in random ray mode."); + } + } + } + // Validate plotting files /////////////////////////////////////////////////////////////////// for (int p = 0; p < model::plots.size(); p++) { @@ -337,16 +280,19 @@ void validate_random_ray_inputs() warning( "Linear sources may result in negative fluxes in small source regions " "generated by mesh subdivision. Negative sources may result in low " - "quality FW-CADIS weight windows. We recommend you use flat source mode " - "when generating weight windows with an overlaid mesh tally."); + "quality FW-CADIS weight windows. We recommend you use flat source " + "mode when generating weight windows with an overlaid mesh tally."); } } -void openmc_reset_random_ray() +void openmc_finalize_random_ray() { FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; FlatSourceDomain::volume_normalized_flux_tallies_ = false; - FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::adjoint_requested_ = false; + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; + FlatSourceDomain::fw_cadis_local_ = false; + FlatSourceDomain::fw_cadis_local_targets_.clear(); FlatSourceDomain::mesh_domain_map_.clear(); RandomRay::ray_source_.reset(); RandomRay::source_shape_ = RandomRaySourceShape::FLAT; @@ -390,21 +336,62 @@ void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() domain_->apply_meshes(); if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions - domain_->convert_external_sources(); + domain_->convert_external_sources(false); domain_->count_external_source_regions(); } } -void RandomRaySimulation::prepare_fixed_sources_adjoint() +void RandomRaySimulation::prepare_fw_fixed_sources_adjoint() { + // Prepare adjoint fixed sources using forward flux domain_->source_regions_.adjoint_reset(); if (settings::run_mode == RunMode::FIXED_SOURCE) { - domain_->set_adjoint_sources(); + domain_->set_fw_adjoint_sources(); } } +void RandomRaySimulation::prepare_local_fixed_sources_adjoint() +{ + if (settings::run_mode == RunMode::FIXED_SOURCE) { + domain_->set_local_adjoint_sources(); + } +} + +void RandomRaySimulation::prepare_adjoint_simulation(bool from_forward) +{ + reset_timers(); + + if (mpi::master) + header("ADJOINT FLUX SOLVE", 3); + + if (from_forward) { + // The forward solve has already run. Re-initialize OpenMC's general data + // structures for the adjoint solve and derive the adjoint source from the + // forward flux. + openmc_simulation_init(); + + prepare_fw_fixed_sources_adjoint(); + } else { + // Initialize adjoint fixed sources + domain_->apply_meshes(); + prepare_local_fixed_sources_adjoint(); + domain_->count_external_source_regions(); + } + + domain_->k_eff_ = 1.0; + + // Transpose scattering matrix + domain_->transpose_scattering_matrix(); + + // Swap nu_sigma_f and chi + domain_->nu_sigma_f_.swap(domain_->chi_); +} + void RandomRaySimulation::simulate() { + // Begin main simulation timer + simulation::time_total.start(); + // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { // Initialize the current batch @@ -493,6 +480,26 @@ void RandomRaySimulation::simulate() } // End random ray power iteration loop domain_->count_external_source_regions(); + + // End main simulation timer + simulation::time_total.stop(); + + // Normalize and save the final forward flux + double source_normalization_factor = + domain_->compute_fixed_source_normalization_factor() / + (settings::n_batches - settings::n_inactive); + +#pragma omp parallel for + for (uint64_t se = 0; se < domain_->n_source_elements(); se++) { + domain_->source_regions_.scalar_flux_final(se) *= + source_normalization_factor; + } + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Output all simulation results + output_simulation_results(); } void RandomRaySimulation::output_simulation_results() const @@ -595,7 +602,8 @@ void RandomRaySimulation::print_results_random_ray( } fmt::print(" Volume Estimator Type = {}\n", estimator); - std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF"; + std::string adjoint_true = + (FlatSourceDomain::solve_ == RandomRaySolve::ADJOINT) ? "ON" : "OFF"; fmt::print(" Adjoint Flux Mode = {}\n", adjoint_true); std::string shape; @@ -613,9 +621,18 @@ void RandomRaySimulation::print_results_random_ray( fatal_error("Invalid random ray source shape"); } fmt::print(" Source Shape = {}\n", shape); - std::string sample_method = - (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG" - : "Halton"; + std::string sample_method; + switch (RandomRay::sample_method_) { + case RandomRaySampleMethod::PRNG: + sample_method = "PRNG"; + break; + case RandomRaySampleMethod::HALTON: + sample_method = "Halton"; + break; + case RandomRaySampleMethod::S2: + sample_method = "PRNG S2"; + break; + } fmt::print(" Sample Method = {}\n", sample_method); if (domain_->is_transport_stabilization_needed_) { @@ -651,3 +668,56 @@ void RandomRaySimulation::print_results_random_ray( } } // namespace openmc + +//============================================================================== +// C API functions +//============================================================================== + +void openmc_run_random_ray() +{ + using namespace openmc; + + // Determine which solves to run. If adjoint results are requested and no + // user-defined adjoint source is present, an initial forward solve is needed + // to construct the adjoint source from the forward flux (FW-CADIS). If the + // user has defined an adjoint source, the forward solve is skipped and only + // the adjoint solve is run. + const bool run_adjoint = FlatSourceDomain::adjoint_requested_; + const bool have_adjoint_source = !model::adjoint_sources.empty(); + const bool run_forward = !(run_adjoint && have_adjoint_source); + + // Set the initial solve type + if (!run_forward) { + FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT; + } else if (run_adjoint) { + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD_FOR_ADJOINT; + } else { + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; + } + + // Initialize OpenMC general data structures + openmc_simulation_init(); + + // Validate that inputs meet requirements for random ray mode + if (mpi::master) + validate_random_ray_inputs(); + + // Initialize Random Ray Simulation Object + RandomRaySimulation sim; + + // Run the forward solve + if (run_forward) { + // When an adjoint solve follows, report this as the initial forward solve + if (run_adjoint && mpi::master) + header("FORWARD FLUX SOLVE", 3); + sim.apply_fixed_sources_and_mesh_domains(); + sim.simulate(); + } + + // Run the adjoint solve + if (run_adjoint) { + FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT; + sim.prepare_adjoint_simulation(run_forward); + sim.simulate(); + } +} diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 3b06f0ed0..78543c5ab 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -11,6 +11,7 @@ namespace openmc { //============================================================================== SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), + temperature_idx_(&sr.temperature_idx_), density_mult_(&sr.density_mult_), is_small_(&sr.is_small_), n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), @@ -70,6 +71,8 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) // Scalar fields material_.push_back(sr.material_); + temperature_idx_.push_back(sr.temperature_idx_); + density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); @@ -123,6 +126,8 @@ void SourceRegionContainer::assign( // Clear existing data n_source_regions_ = 0; material_.clear(); + temperature_idx_.clear(); + density_mult_.clear(); is_small_.clear(); n_hits_.clear(); lock_.clear(); @@ -180,6 +185,8 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) SourceRegionHandle handle; handle.negroups_ = negroups(); handle.material_ = &material(sr); + handle.temperature_idx_ = &temperature_idx(sr); + handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); handle.n_hits_ = &n_hits(sr); handle.is_linear_ = is_linear(); diff --git a/src/ray.cpp b/src/ray.cpp new file mode 100644 index 000000000..3d848e3a3 --- /dev/null +++ b/src/ray.cpp @@ -0,0 +1,168 @@ +#include "openmc/ray.h" + +#include "openmc/error.h" +#include "openmc/geometry.h" +#include "openmc/settings.h" + +namespace openmc { + +void Ray::compute_distance() +{ + boundary() = distance_to_boundary(*this); +} + +void Ray::trace() +{ + // To trace the ray from its origin all the way through the model, we have + // to proceed in two phases. In the first, the ray may or may not be found + // inside the model. If the ray is already in the model, phase one can be + // skipped. Otherwise, the ray has to be advanced to the boundary of the + // model where all the cells are defined. Importantly, this is assuming that + // the model is convex, which is a very reasonable assumption for any + // radiation transport model. + // + // After phase one is done, we can starting tracing from cell to cell within + // the model. This step can use neighbor lists to accelerate the ray tracing. + + bool inside_cell; + // Check for location if the particle is already known + if (lowest_coord().cell() == C_NONE) { + // The geometry position of the particle is either unknown or outside of the + // edge of the model. + if (lowest_coord().universe() == C_NONE) { + // Attempt to initialize the particle. We may have to + // enter a loop to move it up to the edge of the model. + inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + } else { + // It has been already calculated that the current position is outside of + // the edge of the model. + inside_cell = false; + } + } else { + // Availability of the cell means that the particle is located inside the + // edge. + inside_cell = true; + } + + // Advance to the boundary of the model + while (!inside_cell) { + advance_to_boundary_from_void(); + inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + + // If true this means no surface was intersected. See cell.cpp and search + // for numeric_limits to see where we return it. + if (surface() == std::numeric_limits::max()) { + warning(fmt::format("Lost a ray, r = {}, u = {}", r(), u())); + return; + } + + // Exit this loop and enter into cell-to-cell ray tracing (which uses + // neighbor lists) + if (inside_cell) + break; + + // if there is no intersection with the model, we're done + if (boundary().surface() == SURFACE_NONE) + return; + + event_counter_++; + if (event_counter_ > MAX_INTERSECTIONS) { + warning("Likely infinite loop in ray traced plot"); + return; + } + } + + // Call the specialized logic for this type of ray. This is for the + // intersection for the first intersection if we had one. + if (boundary().surface() != SURFACE_NONE) { + // set the geometry state's surface attribute to be used for + // surface normal computation + surface() = boundary().surface(); + on_intersection(); + if (stop_) + return; + } + + // reset surface attribute to zero after the first intersection so that it + // doesn't perturb surface crossing logic from here on out + surface() = 0; + + // This is the ray tracing loop within the model. It exits after exiting + // the model, which is equivalent to assuming that the model is convex. + // It would be nice to factor out the on_intersection at the end of this + // loop and then do "while (inside_cell)", but we can't guarantee it's + // on a surface in that case. There might be some other way to set it + // up that is perhaps a little more elegant, but this is what works just + // fine. + while (true) { + + compute_distance(); + + // There are no more intersections to process + // if we hit the edge of the model, so stop + // the particle in that case. Also, just exit + // if a negative distance was somehow computed. + if (boundary().distance() == INFTY || boundary().distance() == INFINITY || + boundary().distance() < 0) { + return; + } + + // See below comment where call_on_intersection is checked in an + // if statement for an explanation of this. + bool call_on_intersection {true}; + if (boundary().distance() < 10 * TINY_BIT) { + call_on_intersection = false; + } + + // DAGMC surfaces expect us to go a little bit further than the advance + // distance to properly check cell inclusion. + boundary().distance() += TINY_BIT; + + // Advance particle, prepare for next intersection + for (int lev = 0; lev < n_coord(); ++lev) { + coord(lev).r() += boundary().distance() * coord(lev).u(); + } + surface() = boundary().surface(); + // Initialize last cells from the current cell, because the cell() variable + // does not contain the data for the case of a single-segment ray + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell(); + } + n_coord_last() = n_coord(); + n_coord() = boundary().coord_level(); + if (boundary().lattice_translation()[0] != 0 || + boundary().lattice_translation()[1] != 0 || + boundary().lattice_translation()[2] != 0) { + cross_lattice(*this, boundary(), settings::verbosity >= 10); + } + + // Record how far the ray has traveled + traversal_distance_ += boundary().distance(); + inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10); + + // Call the specialized logic for this type of ray. Note that we do not + // call this if the advance distance is very small. Unfortunately, it seems + // darn near impossible to get the particle advanced to the model boundary + // and through it without sometimes accidentally calling on_intersection + // twice. This incorrectly shades the region as occluded when it might not + // actually be. By screening out intersection distances smaller than a + // threshold 10x larger than the scoot distance used to advance up to the + // model boundary, we can avoid that situation. + if (call_on_intersection) { + on_intersection(); + if (stop_) + return; + } + + if (!inside_cell) + return; + + event_counter_++; + if (event_counter_ > MAX_INTERSECTIONS) { + warning("Likely infinite loop in ray traced plot"); + return; + } + } +} + +} // namespace openmc diff --git a/src/reaction.cpp b/src/reaction.cpp index d96790c6d..c02e0cc40 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -70,9 +70,8 @@ Reaction::Reaction( if (settings::use_decay_photons) { // Remove photon products for D1S method - products_.erase( - std::remove_if(products_.begin(), products_.end(), - [](const auto& p) { return p.particle_ == ParticleType::photon; }), + products_.erase(std::remove_if(products_.begin(), products_.end(), + [](const auto& p) { return p.particle_.is_photon(); }), products_.end()); // Determine product for D1S method @@ -310,6 +309,7 @@ std::unordered_map REACTION_NAME_MAP { {N_XA, "(n,Xa)"}, {HEATING, "heating"}, {DAMAGE_ENERGY, "damage-energy"}, + {PHOTON_TOTAL, "photon-total"}, {COHERENT, "coherent-scatter"}, {INCOHERENT, "incoherent-scatter"}, {PAIR_PROD_ELEC, "pair-production-electron"}, @@ -347,13 +347,24 @@ void initialize_maps() // Create photoelectric subshells for (int mt = 534; mt <= 572; ++mt) { REACTION_NAME_MAP[mt] = - fmt::format("photoelectric, {} subshell", SUBSHELLS[mt - 534]); + fmt::format("photoelectric-{}", SUBSHELLS[mt - 534]); } // Invert name map to create type map for (const auto& kv : REACTION_NAME_MAP) { REACTION_TYPE_MAP[kv.second] = kv.first; } + + // Alternate names + REACTION_TYPE_MAP["elastic"] = ELASTIC; + REACTION_TYPE_MAP["n2n"] = N_2N; + REACTION_TYPE_MAP["n3n"] = N_3N; + REACTION_TYPE_MAP["n4n"] = N_4N; + REACTION_TYPE_MAP["H1-production"] = N_XP; + REACTION_TYPE_MAP["H2-production"] = N_XD; + REACTION_TYPE_MAP["H3-production"] = N_XT; + REACTION_TYPE_MAP["He3-production"] = N_X3HE; + REACTION_TYPE_MAP["He4-production"] = N_XA; } std::string reaction_name(int mt) @@ -371,62 +382,42 @@ std::string reaction_name(int mt) } } -int reaction_type(std::string name) +int reaction_tally_mt(std::string name) { - // Initialize remainder of name map and all of type map + // All "total" scores should map to the special SCORE_TOTAL + if (name == "total" || name == "(n,total)" || name == "photon-total") + return SCORE_TOTAL; + + // All fission scores should map to the special SCORE_FISSION + if (name == "fission" || name == "(n,fission)") + return SCORE_FISSION; + + // Delegate everything else to reaction_mt() + return reaction_mt(name); +} + +int reaction_mt(const std::string& name) +{ + // Initialize maps if needed if (REACTION_TYPE_MAP.empty()) initialize_maps(); - // (n,total) exists in REACTION_TYPE_MAP for MT=1, but we need this to return - // the special SCORE_TOTAL score - if (name == "(n,total)") - return SCORE_TOTAL; - - // Check if type map has an entry for this reaction name + // Look up directly in type map (no score indirection) auto it = REACTION_TYPE_MAP.find(name); if (it != REACTION_TYPE_MAP.end()) { - return it->second; + int mt = it->second; + return mt; } - // Alternate names for several reactions - if (name == "elastic") { - return ELASTIC; - } else if (name == "n2n") { - return N_2N; - } else if (name == "n3n") { - return N_3N; - } else if (name == "n4n") { - return N_4N; - } else if (name == "H1-production") { - return N_XP; - } else if (name == "H2-production") { - return N_XD; - } else if (name == "H3-production") { - return N_XT; - } else if (name == "He3-production") { - return N_X3HE; - } else if (name == "He4-production") { - return N_XA; - } - - // Assume the given string is a reaction MT number. Make sure it's a natural - // number then return. + // Assume the given string is an MT number int MT = 0; try { MT = std::stoi(name); } catch (const std::invalid_argument& ex) { - throw std::invalid_argument( - "Invalid tally score \"" + name + - "\". See the docs " - "for details: " - "https://docs.openmc.org/en/stable/usersguide/tallies.html#scores"); + throw std::invalid_argument("Unknown reaction name \"" + name + "\"."); } if (MT < 1) - throw std::invalid_argument( - "Invalid tally score \"" + name + - "\". See the docs " - "for details: " - "https://docs.openmc.org/en/stable/usersguide/tallies.html#scores"); + throw std::invalid_argument("Unknown reaction name \"" + name + "\"."); return MT; } diff --git a/src/reaction_product.cpp b/src/reaction_product.cpp index 3ba2c0cfb..a1c937861 100644 --- a/src/reaction_product.cpp +++ b/src/reaction_product.cpp @@ -1,5 +1,6 @@ #include "openmc/reaction_product.h" +#include #include // for string #include @@ -26,7 +27,7 @@ ReactionProduct::ReactionProduct(hid_t group) // Read particle type std::string temp; read_attribute(group, "particle", temp); - particle_ = str_to_particle_type(temp); + particle_ = ParticleType {temp}; // Read emission mode and decay rate read_attribute(group, "emission_mode", temp); @@ -42,7 +43,7 @@ ReactionProduct::ReactionProduct(hid_t group) if (emission_mode_ == EmissionMode::delayed) { if (attribute_exists(group, "decay_rate")) { read_attribute(group, "decay_rate", decay_rate_); - } else if (particle_ == ParticleType::neutron) { + } else if (particle_.is_neutron()) { warning(fmt::format("Decay rate doesn't exist for delayed neutron " "emission ({}).", object_name(group))); @@ -85,7 +86,7 @@ ReactionProduct::ReactionProduct(hid_t group) ReactionProduct::ReactionProduct(const ChainNuclide::Product& product) { - particle_ = ParticleType::photon; + particle_ = ParticleType::photon(); emission_mode_ = EmissionMode::delayed; // Get chain nuclide object for radionuclide @@ -106,9 +107,10 @@ ReactionProduct::ReactionProduct(const ChainNuclide::Product& product) make_unique(chain_nuc->photon_energy())); } -void ReactionProduct::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +AngleEnergy& ReactionProduct::sample_dist(double E_in, uint64_t* seed) const { + assert(!distribution_.empty()); + auto n = applicability_.size(); if (n > 1) { double prob = 0.0; @@ -118,15 +120,24 @@ void ReactionProduct::sample( prob += applicability_[i](E_in); // If i-th distribution is sampled, sample energy from the distribution - if (c <= prob) { - distribution_[i]->sample(E_in, E_out, mu, seed); - break; - } + if (c <= prob) + return *distribution_[i]; } - } else { - // If only one distribution is present, go ahead and sample it - distribution_[0]->sample(E_in, E_out, mu, seed); } + + return *distribution_.back(); +} + +void ReactionProduct::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + sample_dist(E_in, seed).sample(E_in, E_out, mu, seed); +} + +double ReactionProduct::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(E_in, seed).sample_energy_and_pdf(E_in, mu, E_out, seed); } } // namespace openmc diff --git a/src/scattdata.cpp b/src/scattdata.cpp index 21b18cbd9..9aa09956d 100644 --- a/src/scattdata.cpp +++ b/src/scattdata.cpp @@ -4,8 +4,7 @@ #include #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/error.h" @@ -19,8 +18,8 @@ namespace openmc { // ScattData base-class methods //============================================================================== -void ScattData::base_init(int order, const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_energy, +void ScattData::base_init(int order, const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_energy, const double_2dvec& in_mult) { size_t groups = in_energy.size(); @@ -63,23 +62,26 @@ void ScattData::base_init(int order, const xt::xtensor& in_gmin, void ScattData::base_combine(size_t max_order, size_t order_dim, const vector& those_scatts, const vector& scalars, - xt::xtensor& in_gmin, xt::xtensor& in_gmax, + tensor::Tensor& in_gmin, tensor::Tensor& in_gmax, double_2dvec& sparse_mult, double_3dvec& sparse_scatter) { size_t groups = those_scatts[0]->energy.size(); // Now allocate and zero our storage spaces - xt::xtensor this_nuscatt_matrix({groups, groups, order_dim}, 0.); - xt::xtensor this_nuscatt_P0({groups, groups}, 0.); - xt::xtensor this_scatt_P0({groups, groups}, 0.); - xt::xtensor this_mult({groups, groups}, 1.); + tensor::Tensor this_nuscatt_matrix = + tensor::zeros({groups, groups, order_dim}); + tensor::Tensor this_nuscatt_P0 = + tensor::zeros({groups, groups}); + tensor::Tensor this_scatt_P0 = + tensor::zeros({groups, groups}); + tensor::Tensor this_mult = tensor::ones({groups, groups}); // Build the dense scattering and multiplicity matrices for (int i = 0; i < those_scatts.size(); i++) { ScattData* that = those_scatts[i]; // Build the dense matrix for that object - xt::xtensor that_matrix = that->get_matrix(max_order); + tensor::Tensor that_matrix = that->get_matrix(max_order); // Now add that to this for the nu-scatter matrix this_nuscatt_matrix += scalars[i] * that_matrix; @@ -97,7 +99,7 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, // Now we have the dense nuscatt and scatt, we can easily compute the // multiplicity matrix by dividing the two and fixing any nans - this_mult = xt::nan_to_num(this_nuscatt_P0 / this_scatt_P0); + this_mult = tensor::nan_to_num(this_nuscatt_P0 / this_scatt_P0); // We have the data, now we need to convert to a jagged array and then use // the initialize function to store it on the object. @@ -106,7 +108,7 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, int gmin_; for (gmin_ = 0; gmin_ < groups; gmin_++) { bool non_zero = false; - for (int l = 0; l < this_nuscatt_matrix.shape()[2]; l++) { + for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) { if (this_nuscatt_matrix(gin, gmin_, l) != 0.) { non_zero = true; break; @@ -118,7 +120,7 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, int gmax_; for (gmax_ = groups - 1; gmax_ >= 0; gmax_--) { bool non_zero = false; - for (int l = 0; l < this_nuscatt_matrix.shape()[2]; l++) { + for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) { if (this_nuscatt_matrix(gin, gmax_, l) != 0.) { non_zero = true; break; @@ -143,8 +145,8 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, sparse_mult[gin].resize(gmax_ - gmin_ + 1); int i_gout = 0; for (int gout = gmin_; gout <= gmax_; gout++) { - sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape()[2]); - for (int l = 0; l < this_nuscatt_matrix.shape()[2]; l++) { + sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape(2)); + for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) { sparse_scatter[gin][i_gout][l] = this_nuscatt_matrix(gin, gout, l); } sparse_mult[gin][i_gout] = this_mult(gin, gout); @@ -227,8 +229,8 @@ double ScattData::get_xs( // ScattDataLegendre methods //============================================================================== -void ScattDataLegendre::init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, +void ScattDataLegendre::init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) { size_t groups = coeffs.size(); @@ -239,7 +241,7 @@ void ScattDataLegendre::init(const xt::xtensor& in_gmin, // Get the scattering cross section value by summing the un-normalized P0 // coefficient in the variable matrix over all outgoing groups. - scattxs = xt::zeros({groups}); + scattxs = tensor::zeros({groups}); for (int gin = 0; gin < groups; gin++) { int num_groups = in_gmax[gin] - in_gmin[gin] + 1; for (int i_gout = 0; i_gout < num_groups; i_gout++) { @@ -386,8 +388,8 @@ void ScattDataLegendre::combine( size_t groups = those_scatts[0]->energy.size(); - xt::xtensor in_gmin({groups}, 0); - xt::xtensor in_gmax({groups}, 0); + tensor::Tensor in_gmin({groups}, 0); + tensor::Tensor in_gmax({groups}, 0); double_3dvec sparse_scatter(groups); double_2dvec sparse_mult(groups); @@ -404,12 +406,13 @@ void ScattDataLegendre::combine( //============================================================================== -xt::xtensor ScattDataLegendre::get_matrix(size_t max_order) +tensor::Tensor ScattDataLegendre::get_matrix(size_t max_order) { // Get the sizes and initialize the data to 0 size_t groups = energy.size(); size_t order_dim = max_order + 1; - xt::xtensor matrix({groups, groups, order_dim}, 0.); + tensor::Tensor matrix = + tensor::zeros({groups, groups, order_dim}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) { @@ -427,8 +430,8 @@ xt::xtensor ScattDataLegendre::get_matrix(size_t max_order) // ScattDataHistogram methods //============================================================================== -void ScattDataHistogram::init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, +void ScattDataHistogram::init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) { size_t groups = coeffs.size(); @@ -439,7 +442,7 @@ void ScattDataHistogram::init(const xt::xtensor& in_gmin, // Get the scattering cross section value by summing the distribution // over all the histogram bins in angle and outgoing energy groups - scattxs = xt::zeros({groups}); + scattxs = tensor::zeros({groups}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) { scattxs[gin] += std::accumulate( @@ -468,7 +471,7 @@ void ScattDataHistogram::init(const xt::xtensor& in_gmin, ScattData::base_init(order, in_gmin, in_gmax, in_energy, in_mult); // Build the angular distribution mu values - mu = xt::linspace(-1., 1., order + 1); + mu = tensor::linspace(-1., 1., order + 1); dmu = 2. / order; // Calculate f(mu) and integrate it so we can avoid rejection sampling @@ -513,7 +516,7 @@ double ScattDataHistogram::calc_f(int gin, int gout, double mu) int imu; if (mu == 1.) { // use size -2 to have the index one before the end - imu = this->mu.shape()[0] - 2; + imu = this->mu.shape(0) - 2; } else { imu = std::floor((mu + 1.) / dmu + 1.) - 1; } @@ -559,13 +562,13 @@ void ScattDataHistogram::sample( //============================================================================== -xt::xtensor ScattDataHistogram::get_matrix(size_t max_order) +tensor::Tensor ScattDataHistogram::get_matrix(size_t max_order) { // Get the sizes and initialize the data to 0 size_t groups = energy.size(); // We ignore the requested order for Histogram and Tabular representations size_t order_dim = get_order(); - xt::xtensor matrix({groups, groups, order_dim}, 0); + tensor::Tensor matrix({groups, groups, order_dim}, 0); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) { @@ -600,8 +603,8 @@ void ScattDataHistogram::combine( size_t groups = those_scatts[0]->energy.size(); - xt::xtensor in_gmin({groups}, 0); - xt::xtensor in_gmax({groups}, 0); + tensor::Tensor in_gmin({groups}, 0); + tensor::Tensor in_gmax({groups}, 0); double_3dvec sparse_scatter(groups); double_2dvec sparse_mult(groups); @@ -620,8 +623,8 @@ void ScattDataHistogram::combine( // ScattDataTabular methods //============================================================================== -void ScattDataTabular::init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, +void ScattDataTabular::init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) { size_t groups = coeffs.size(); @@ -631,12 +634,12 @@ void ScattDataTabular::init(const xt::xtensor& in_gmin, double_3dvec matrix = coeffs; // Build the angular distribution mu values - mu = xt::linspace(-1., 1., order); + mu = tensor::linspace(-1., 1., order); dmu = 2. / (order - 1); // Get the scattering cross section value by integrating the distribution // over all mu points and then combining over all outgoing groups - scattxs = xt::zeros({groups}); + scattxs = tensor::zeros({groups}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) { for (int imu = 1; imu < order; imu++) { @@ -713,7 +716,7 @@ double ScattDataTabular::calc_f(int gin, int gout, double mu) int imu; if (mu == 1.) { // use size -2 to have the index one before the end - imu = this->mu.shape()[0] - 2; + imu = this->mu.shape(0) - 2; } else { imu = std::floor((mu + 1.) / dmu + 1.) - 1; } @@ -734,7 +737,7 @@ void ScattDataTabular::sample( sample_energy(gin, gout, i_gout, seed); // Determine the outgoing cosine bin - int NP = this->mu.shape()[0]; + int NP = this->mu.shape(0); double xi = prn(seed); double c_k = dist[gin][i_gout][0]; @@ -776,13 +779,14 @@ void ScattDataTabular::sample( //============================================================================== -xt::xtensor ScattDataTabular::get_matrix(size_t max_order) +tensor::Tensor ScattDataTabular::get_matrix(size_t max_order) { // Get the sizes and initialize the data to 0 size_t groups = energy.size(); // We ignore the requested order for Histogram and Tabular representations size_t order_dim = get_order(); - xt::xtensor matrix({groups, groups, order_dim}, 0.); + tensor::Tensor matrix = + tensor::zeros({groups, groups, order_dim}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) { @@ -816,8 +820,8 @@ void ScattDataTabular::combine( size_t groups = those_scatts[0]->energy.size(); - xt::xtensor in_gmin({groups}, 0); - xt::xtensor in_gmax({groups}, 0); + tensor::Tensor in_gmin({groups}, 0); + tensor::Tensor in_gmax({groups}, 0); double_3dvec sparse_scatter(groups); double_2dvec sparse_mult(groups); @@ -854,7 +858,7 @@ void convert_legendre_to_tabular(ScattDataLegendre& leg, ScattDataTabular& tab) tab.scattxs = leg.scattxs; // Build mu and dmu - tab.mu = xt::linspace(-1., 1., n_mu); + tab.mu = tensor::linspace(-1., 1., n_mu); tab.dmu = 2. / (n_mu - 1); // Calculate f(mu) and integrate it so we can avoid rejection sampling diff --git a/src/secondary_correlated.cpp b/src/secondary_correlated.cpp index 0e4891dd3..32701791a 100644 --- a/src/secondary_correlated.cpp +++ b/src/secondary_correlated.cpp @@ -5,11 +5,11 @@ #include // for size_t #include // for back_inserter -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" @@ -25,11 +25,11 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) hid_t dset = open_dataset(group, "energy"); // Get interpolation parameters - xt::xarray temp; + tensor::Tensor temp; read_attribute(dset, "interpolation", temp); - auto temp_b = xt::view(temp, 0); // view of breakpoints - auto temp_i = xt::view(temp, 1); // view of interpolation parameters + tensor::View temp_b = temp.slice(0); // breakpoints + tensor::View temp_i = temp.slice(1); // interpolation parameters std::copy(temp_b.begin(), temp_b.end(), std::back_inserter(breakpoints_)); for (const auto i : temp_i) @@ -50,12 +50,12 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) read_attribute(dset, "interpolation", interp); read_attribute(dset, "n_discrete_lines", n_discrete); - xt::xarray eout; + tensor::Tensor eout; read_dataset(dset, eout); close_dataset(dset); // Read angle distributions - xt::xarray mu; + tensor::Tensor mu; read_dataset(group, "mu", mu); for (int i = 0; i < n_energy; ++i) { @@ -65,7 +65,7 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = eout.shape()[1] - j; + n = eout.shape(1) - j; } // Assign interpolation scheme and number of discrete lines @@ -74,9 +74,9 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) d.n_discrete = n_discrete[i]; // Copy data - d.e_out = xt::view(eout, 0, xt::range(j, j + n)); - d.p = xt::view(eout, 1, xt::range(j, j + n)); - d.c = xt::view(eout, 2, xt::range(j, j + n)); + d.e_out = eout.slice(0, tensor::range(j, j + n)); + d.p = eout.slice(1, tensor::range(j, j + n)); + d.c = eout.slice(2, tensor::range(j, j + n)); // To get answers that match ACE data, for now we still use the tabulated // CDF values that were passed through to the HDF5 library. At a later @@ -118,10 +118,10 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) // Determine offset and size of distribution int offset_mu = std::lround(eout(4, offsets[i] + j)); int m; - if (offsets[i] + j + 1 < eout.shape()[1]) { + if (offsets[i] + j + 1 < eout.shape(1)) { m = std::lround(eout(4, offsets[i] + j + 1)) - offset_mu; } else { - m = mu.shape()[1] - offset_mu; + m = mu.shape(1) - offset_mu; } // For incoherent inelastic thermal scattering, the angle distributions @@ -132,9 +132,12 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) interp_mu = 1; auto interp = int2interp(interp_mu); - auto xs = xt::view(mu, 0, xt::range(offset_mu, offset_mu + m)); - auto ps = xt::view(mu, 1, xt::range(offset_mu, offset_mu + m)); - auto cs = xt::view(mu, 2, xt::range(offset_mu, offset_mu + m)); + tensor::View xs = + mu.slice(0, tensor::range(offset_mu, offset_mu + m)); + tensor::View ps = + mu.slice(1, tensor::range(offset_mu, offset_mu + m)); + tensor::View cs = + mu.slice(2, tensor::range(offset_mu, offset_mu + m)); vector x {xs.begin(), xs.end()}; vector p {ps.begin(), ps.end()}; @@ -152,25 +155,13 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) distribution_.push_back(std::move(d)); } // incoming energies } - -void CorrelatedAngleEnergy::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +Distribution& CorrelatedAngleEnergy::sample_dist( + double E_in, double& E_out, uint64_t* seed) const { - // Find energy bin and calculate interpolation factor -- if the energy is - // outside the range of the tabulated energies, choose the first or last bins - auto n_energy_in = energy_.size(); + // Find energy bin and calculate interpolation factor int i; double r; - if (E_in < energy_[0]) { - i = 0; - r = 0.0; - } else if (E_in > energy_[n_energy_in - 1]) { - i = n_energy_in - 2; - r = 1.0; - } else { - i = lower_bound_index(energy_.begin(), energy_.end(), E_in); - r = (E_in - energy_[i]) / (energy_[i + 1] - energy_[i]); - } + get_energy_index(energy_, E_in, i, r); // Sample between the ith and [i+1]th bin int l = r > prn(seed) ? i + 1 : i; @@ -257,10 +248,22 @@ void CorrelatedAngleEnergy::sample( // Find correlated angular distribution for closest outgoing energy bin if (r1 - c_k < c_k1 - r1 || distribution_[l].interpolation == Interpolation::histogram) { - mu = distribution_[l].angle[k]->sample(seed); + return *distribution_[l].angle[k]; } else { - mu = distribution_[l].angle[k + 1]->sample(seed); + return *distribution_[l].angle[k + 1]; } } +void CorrelatedAngleEnergy::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + mu = sample_dist(E_in, E_out, seed).sample(seed).first; +} + +double CorrelatedAngleEnergy::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(E_in, E_out, seed).evaluate(mu); +} + } // namespace openmc diff --git a/src/secondary_kalbach.cpp b/src/secondary_kalbach.cpp index 4a7878343..018ce1c8a 100644 --- a/src/secondary_kalbach.cpp +++ b/src/secondary_kalbach.cpp @@ -5,10 +5,10 @@ #include // for size_t #include // for back_inserter -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_dist.h" #include "openmc/random_lcg.h" #include "openmc/search.h" @@ -26,11 +26,11 @@ KalbachMann::KalbachMann(hid_t group) hid_t dset = open_dataset(group, "energy"); // Get interpolation parameters - xt::xarray temp; + tensor::Tensor temp; read_attribute(dset, "interpolation", temp); - auto temp_b = xt::view(temp, 0); // view of breakpoints - auto temp_i = xt::view(temp, 1); // view of interpolation parameters + tensor::View temp_b = temp.slice(0); // breakpoints + tensor::View temp_i = temp.slice(1); // interpolation parameters std::copy(temp_b.begin(), temp_b.end(), std::back_inserter(breakpoints_)); for (const auto i : temp_i) @@ -51,7 +51,7 @@ KalbachMann::KalbachMann(hid_t group) read_attribute(dset, "interpolation", interp); read_attribute(dset, "n_discrete_lines", n_discrete); - xt::xarray eout; + tensor::Tensor eout; read_dataset(dset, eout); close_dataset(dset); @@ -62,7 +62,7 @@ KalbachMann::KalbachMann(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = eout.shape()[1] - j; + n = eout.shape(1) - j; } // Assign interpolation scheme and number of discrete lines @@ -71,11 +71,11 @@ KalbachMann::KalbachMann(hid_t group) d.n_discrete = n_discrete[i]; // Copy data - d.e_out = xt::view(eout, 0, xt::range(j, j + n)); - d.p = xt::view(eout, 1, xt::range(j, j + n)); - d.c = xt::view(eout, 2, xt::range(j, j + n)); - d.r = xt::view(eout, 3, xt::range(j, j + n)); - d.a = xt::view(eout, 4, xt::range(j, j + n)); + d.e_out = eout.slice(0, tensor::range(j, j + n)); + d.p = eout.slice(1, tensor::range(j, j + n)); + d.c = eout.slice(2, tensor::range(j, j + n)); + d.r = eout.slice(3, tensor::range(j, j + n)); + d.a = eout.slice(4, tensor::range(j, j + n)); // To get answers that match ACE data, for now we still use the tabulated // CDF values that were passed through to the HDF5 library. At a later @@ -114,24 +114,13 @@ KalbachMann::KalbachMann(hid_t group) } // incoming energies } -void KalbachMann::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +void KalbachMann::sample_params( + double E_in, double& E_out, double& km_a, double& km_r, uint64_t* seed) const { - // Find energy bin and calculate interpolation factor -- if the energy is - // outside the range of the tabulated energies, choose the first or last bins - auto n_energy_in = energy_.size(); + // Find energy bin and calculate interpolation factor int i; double r; - if (E_in < energy_[0]) { - i = 0; - r = 0.0; - } else if (E_in > energy_[n_energy_in - 1]) { - i = n_energy_in - 2; - r = 1.0; - } else { - i = lower_bound_index(energy_.begin(), energy_.end(), E_in); - r = (E_in - energy_[i]) / (energy_[i + 1] - energy_[i]); - } + get_energy_index(energy_, E_in, i, r); // Sample between the ith and [i+1]th bin int l = r > prn(seed) ? i + 1 : i; @@ -181,7 +170,6 @@ void KalbachMann::sample( double E_l_k = distribution_[l].e_out[k]; double p_l_k = distribution_[l].p[k]; - double km_r, km_a; if (distribution_[l].interpolation == Interpolation::histogram) { // Histogram interpolation if (p_l_k > 0.0 && k >= n_discrete) { @@ -227,6 +215,13 @@ void KalbachMann::sample( E_out = E_1 + (E_out - E_i1_1) * (E_K - E_1) / (E_i1_K - E_i1_1); } } +} + +void KalbachMann::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + double km_r, km_a; + sample_params(E_in, E_out, km_a, km_r, seed); // Sampled correlated angle from Kalbach-Mann parameters if (prn(seed) > km_r) { @@ -237,5 +232,15 @@ void KalbachMann::sample( mu = std::log(r1 * std::exp(km_a) + (1.0 - r1) * std::exp(-km_a)) / km_a; } } +double KalbachMann::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + double km_r, km_a; + sample_params(E_in, E_out, km_a, km_r, seed); + + // https://docs.openmc.org/en/latest/methods/neutron_physics.html#equation-KM-pdf-angle + return km_a / (2 * std::sinh(km_a)) * + (std::cosh(km_a * mu) + km_r * std::sinh(km_a * mu)); +} } // namespace openmc diff --git a/src/secondary_nbody.cpp b/src/secondary_nbody.cpp index da0bb81c4..72f0b0b92 100644 --- a/src/secondary_nbody.cpp +++ b/src/secondary_nbody.cpp @@ -22,13 +22,8 @@ NBodyPhaseSpace::NBodyPhaseSpace(hid_t group) read_attribute(group, "q_value", Q_); } -void NBodyPhaseSpace::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +double NBodyPhaseSpace::sample_energy(double E_in, uint64_t* seed) const { - // By definition, the distribution of the angle is isotropic for an N-body - // phase space distribution - mu = uniform_distribution(-1., 1., seed); - // Determine E_max parameter double Ap = mass_ratio_; double E_max = (Ap - 1.0) / Ap * (A_ / (A_ + 1.0) * E_in + Q_); @@ -59,12 +54,29 @@ void NBodyPhaseSpace::sample( std::log(r5) * std::pow(std::cos(PI / 2.0 * r6), 2); break; default: - throw std::runtime_error {"N-body phase space with >5 bodies."}; + fatal_error("N-body phase space with >5 bodies."); } // Now determine v and E_out double v = x / (x + y); - E_out = E_max * v; + return E_max * v; +} + +void NBodyPhaseSpace::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + // By definition, the distribution of the angle is isotropic for an N-body + // phase space distribution + mu = uniform_distribution(-1., 1., seed); + + E_out = sample_energy(E_in, seed); +} + +double NBodyPhaseSpace::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + E_out = sample_energy(E_in, seed); + return 0.5; } } // namespace openmc diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 8b9e8737c..b0f601809 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -1,44 +1,42 @@ #include "openmc/secondary_thermal.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" +#include "openmc/vector.h" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include // for log, exp namespace openmc { -// Helper function to get index on incident energy grid -void get_energy_index( - const vector& energies, double E, int& i, double& f) -{ - // Get index and interpolation factor for elastic grid - i = 0; - f = 0.0; - if (E >= energies.front()) { - i = lower_bound_index(energies.begin(), energies.end(), E); - if (i + 1 < energies.size()) - f = (E - energies[i]) / (energies[i + 1] - energies[i]); - } -} - //============================================================================== // CoherentElasticAE implementation //============================================================================== -CoherentElasticAE::CoherentElasticAE(const CoherentElasticXS& xs) : xs_ {xs} {} +CoherentElasticAE::CoherentElasticAE(const CoherentElasticXS& xs) : xs_ {xs} +{ + const auto& bragg = xs_.bragg_edges(); + auto n = bragg.size(); + bragg_edges_ = tensor::Tensor(bragg.data(), n); + + const auto& factors = xs_.factors(); + factors_diff_ = tensor::zeros({n}); + factors_diff_.slice(0) = factors[0]; + for (int i = 1; i < n; ++i) { + factors_diff_.slice(i) = factors[i] - factors[i - 1]; + } +} void CoherentElasticAE::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1) E_out = E_in; - const auto& energies {xs_.bragg_edges()}; - assert(E_in >= energies.front()); const int i = lower_bound_index(energies.begin(), energies.end(), E_in); @@ -55,6 +53,25 @@ void CoherentElasticAE::sample( mu = 1.0 - 2.0 * energies[k] / E_in; } +double CoherentElasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1) + E_out = E_in; + const auto& factors = xs_.factors(); + + if (E_in < bragg_edges_.front()) + return 0.0; + + const int i = + lower_bound_index(bragg_edges_.begin(), bragg_edges_.end(), E_in); + double E = 0.5 * (1 - mu) * E_in; + double C = 0.5 * E_in / factors[i]; + + return C * get_pdf_discrete(bragg_edges_.slice(tensor::range(i + 1)), + factors_diff_.slice(tensor::range(i + 1)), E, 0.0, E_in); +} + //============================================================================== // IncoherentElasticAE implementation //============================================================================== @@ -67,12 +84,21 @@ IncoherentElasticAE::IncoherentElasticAE(hid_t group) void IncoherentElasticAE::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { + E_out = E_in; + // Sample angle by inverting the distribution in ENDF-102, Eq. 7.4 double c = 2 * E_in * debye_waller_; mu = std::log(1.0 + prn(seed) * (std::exp(2.0 * c) - 1)) / c - 1.0; - - // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7.4) +} +double IncoherentElasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ E_out = E_in; + + // Sample angle by inverting the distribution in ENDF-102, Eq. 7.4 + double c = 2 * E_in * debye_waller_; + double A = c / (1 - std::exp(-2.0 * c)); // normalization factor + return A * std::exp(-c * (1 - mu)); } //============================================================================== @@ -98,7 +124,7 @@ void IncoherentElasticAEDiscrete::sample( // incoming energies. // Sample outgoing cosine bin - int n_mu = mu_out_.shape()[1]; + int n_mu = mu_out_.shape(1); int k = prn(seed) * n_mu; // Rather than use the sampled discrete mu directly, it is smeared over @@ -129,6 +155,20 @@ void IncoherentElasticAEDiscrete::sample( E_out = E_in; } +double IncoherentElasticAEDiscrete::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Get index and interpolation factor for elastic grid + int i; + double f; + get_energy_index(energy_, E_in, i, f); + // Energy doesn't change in elastic scattering + E_out = E_in; + + return get_pdf_discrete_interpolated( + mu_out_.slice(i, tensor::all), mu_out_.slice(i + 1, tensor::all), f, mu); +} + //============================================================================== // IncoherentInelasticAEDiscrete implementation //============================================================================== @@ -142,8 +182,8 @@ IncoherentInelasticAEDiscrete::IncoherentInelasticAEDiscrete( read_dataset(group, "skewed", skewed_); } -void IncoherentInelasticAEDiscrete::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +void IncoherentInelasticAEDiscrete::sample_params( + double E_in, double& E_out, int& j, uint64_t* seed) const { // Get index and interpolation factor for inelastic grid int i; @@ -157,8 +197,7 @@ void IncoherentInelasticAEDiscrete::sample( // for the second and second to last bins, relative to a normal bin // probability of 1). Otherwise, each bin is equally probable. - int j; - int n = energy_out_.shape()[1]; + int n = energy_out_.shape(1); if (!skewed_) { // All bins equally likely j = prn(seed) * n; @@ -189,9 +228,21 @@ void IncoherentInelasticAEDiscrete::sample( // Outgoing energy E_out = (1 - f) * E_ij + f * E_i1j; +} + +void IncoherentInelasticAEDiscrete::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + // Get index and interpolation factor for inelastic grid + int i; + double f; + get_energy_index(energy_, E_in, i, f); + + int j; + sample_params(E_in, E_out, j, seed); // Sample outgoing cosine bin - int m = mu_out_.shape()[2]; + int m = mu_out_.shape(2); int k = prn(seed) * m; // Determine outgoing cosine corresponding to E_in[i] and E_in[i+1] @@ -202,6 +253,20 @@ void IncoherentInelasticAEDiscrete::sample( mu = (1 - f) * mu_ijk + f * mu_i1jk; } +double IncoherentInelasticAEDiscrete::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Get index and interpolation factor for inelastic grid + int i; + double f; + get_energy_index(energy_, E_in, i, f); + int j; + sample_params(E_in, E_out, j, seed); + + return get_pdf_discrete_interpolated(mu_out_.slice(i, j, tensor::all), + mu_out_.slice(i + 1, j, tensor::all), f, mu); +} + //============================================================================== // IncoherentInelasticAE implementation //============================================================================== @@ -231,11 +296,11 @@ IncoherentInelasticAE::IncoherentInelasticAE(hid_t group) // On first pass, allocate space for angles if (j == 0) { auto n_mu = adist->x().size(); - d.mu = xt::empty({d.n_e_out, n_mu}); + d.mu = tensor::Tensor({d.n_e_out, n_mu}); } // Copy outgoing angles - auto mu_j = xt::view(d.mu, j); + tensor::View mu_j = d.mu.slice(j); std::copy(adist->x().begin(), adist->x().end(), mu_j.begin()); } } @@ -244,24 +309,23 @@ IncoherentInelasticAE::IncoherentInelasticAE(hid_t group) } } -void IncoherentInelasticAE::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +void IncoherentInelasticAE::sample_params( + double E_in, double& E_out, double& f, int& l, int& j, uint64_t* seed) const { // Get index and interpolation factor for inelastic grid int i; - double f; - get_energy_index(energy_, E_in, i, f); + double f0; + get_energy_index(energy_, E_in, i, f0); // Pick closer energy based on interpolation factor - int l = f > 0.5 ? i + 1 : i; + l = f0 > 0.5 ? i + 1 : i; // Determine outgoing energy bin // (First reset n_energy_out to the right value) - auto n = distribution_[l].n_e_out; + int n = distribution_[l].n_e_out; double r1 = prn(seed); double c_j = distribution_[l].e_out_cdf[0]; double c_j1; - std::size_t j; for (j = 0; j < n - 1; ++j) { c_j1 = distribution_[l].e_out_cdf[j + 1]; if (r1 < c_j1) @@ -299,15 +363,23 @@ void IncoherentInelasticAE::sample( E_out += E_in - E_l; } + f = (r1 - c_j) / (c_j1 - c_j); +} +void IncoherentInelasticAE::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + double f; + int l, j; + sample_params(E_in, E_out, f, l, j, seed); + // Sample outgoing cosine bin - int n_mu = distribution_[l].mu.shape()[1]; + int n_mu = distribution_[l].mu.shape(1); std::size_t k = prn(seed) * n_mu; // Rather than use the sampled discrete mu directly, it is smeared over // a bin of width 0.5*min(mu[k] - mu[k-1], mu[k+1] - mu[k]) centered on the // discrete mu value itself. const auto& mu_l = distribution_[l].mu; - f = (r1 - c_j) / (c_j1 - c_j); // Interpolate kth mu value between distributions at energies j and j+1 mu = mu_l(j, k) + f * (mu_l(j + 1, k) - mu_l(j, k)); @@ -331,6 +403,19 @@ void IncoherentInelasticAE::sample( mu += std::min(mu - mu_left, mu_right - mu) * (prn(seed) - 0.5); } +double IncoherentInelasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + double f; + int l, j; + sample_params(E_in, E_out, f, l, j, seed); + + const auto& mu_l = distribution_[l].mu; + + return get_pdf_discrete_interpolated( + mu_l.slice(j, tensor::all), mu_l.slice(j + 1, tensor::all), f, mu); +} + //============================================================================== // MixedElasticAE implementation //============================================================================== @@ -353,18 +438,30 @@ MixedElasticAE::MixedElasticAE( close_group(incoherent_group); } -void MixedElasticAE::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +const AngleEnergy& MixedElasticAE::sample_dist( + double E_in, uint64_t* seed) const { // Evaluate coherent and incoherent elastic cross sections double xs_coh = coherent_xs_(E_in); double xs_incoh = incoherent_xs_(E_in); if (prn(seed) * (xs_coh + xs_incoh) < xs_coh) { - coherent_dist_.sample(E_in, E_out, mu, seed); + return coherent_dist_; } else { - incoherent_dist_->sample(E_in, E_out, mu, seed); + return *incoherent_dist_; } } +void MixedElasticAE::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + sample_dist(E_in, seed).sample(E_in, E_out, mu, seed); +} + +double MixedElasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(E_in, seed).sample_energy_and_pdf(E_in, mu, E_out, seed); +} + } // namespace openmc diff --git a/src/secondary_uncorrelated.cpp b/src/secondary_uncorrelated.cpp index 5cbb76fb9..ec2af7102 100644 --- a/src/secondary_uncorrelated.cpp +++ b/src/secondary_uncorrelated.cpp @@ -65,4 +65,22 @@ void UncorrelatedAngleEnergy::sample( E_out = energy_->sample(E_in, seed); } +double UncorrelatedAngleEnergy::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Sample outgoing energy + if (energy_ != nullptr) { + E_out = energy_->sample(E_in, seed); + } else { + E_out = E_in; + } + + if (!angle_.empty()) { + return angle_.evaluate(E_in, mu); + } else { + // no angle distribution given => assume isotropic for all energies + return 0.5; + } +} + } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 9dcf7c8db..15a7b9c27 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -62,6 +62,7 @@ bool output_summary {true}; bool output_tallies {true}; bool particle_restart_run {false}; bool photon_transport {false}; +bool atomic_relaxation {true}; bool reduce_tallies {true}; bool res_scat_on {false}; bool restart_run {false}; @@ -82,6 +83,7 @@ bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; bool use_decay_photons {false}; +bool use_shared_secondary_bank {false}; bool weight_windows_on {false}; bool weight_window_checkpoint_surface {false}; bool weight_window_checkpoint_collision {true}; @@ -96,6 +98,7 @@ std::string path_sourcepoint; std::string path_statepoint; const char* path_statepoint_c {path_statepoint.c_str()}; std::string weight_windows_file; +std::string properties_file; int32_t n_inactive {0}; int32_t max_lost_particles {10}; @@ -136,6 +139,8 @@ int64_t ssw_max_particles; int64_t ssw_max_files; int64_t ssw_cell_id {C_NONE}; SSWCellType ssw_cell_type {SSWCellType::None}; +double surface_grazing_cutoff {0.001}; +double surface_grazing_ratio {0.5}; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; double temperature_default {293.6}; @@ -145,7 +150,7 @@ int trace_gen; int64_t trace_particle; vector> track_identifiers; int trigger_batch_interval {1}; -int verbosity {7}; +int verbosity {-1}; double weight_cutoff {0.25}; double weight_survive {1.0}; @@ -276,8 +281,9 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray inactive distance in settings XML"); } - if (check_for_node(random_ray_node, "source")) { - xml_node source_node = random_ray_node.child("source"); + if (check_for_node(random_ray_node, "ray_source")) { + xml_node ray_source_node = random_ray_node.child("ray_source"); + xml_node source_node = ray_source_node.child("source"); // Get point to list of elements and make sure there is at least // one RandomRay::ray_source_ = Source::create(source_node); @@ -316,7 +322,7 @@ void get_run_parameters(pugi::xml_node node_base) get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); } if (check_for_node(random_ray_node, "adjoint")) { - FlatSourceDomain::adjoint_ = + FlatSourceDomain::adjoint_requested_ = get_node_value_bool(random_ray_node, "adjoint"); } if (check_for_node(random_ray_node, "sample_method")) { @@ -326,6 +332,8 @@ void get_run_parameters(pugi::xml_node node_base) RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } else if (temp_str == "halton") { RandomRay::sample_method_ = RandomRaySampleMethod::HALTON; + } else if (temp_str == "s2") { + RandomRay::sample_method_ = RandomRaySampleMethod::S2; } else { fatal_error("Unrecognized sample method: " + temp_str); } @@ -363,6 +371,13 @@ void get_run_parameters(pugi::xml_node node_base) "between 0 and 1"); } } + if (check_for_node(random_ray_node, "adjoint_source")) { + pugi::xml_node adj_source_node = random_ray_node.child("adjoint_source"); + for (pugi::xml_node source_node : adj_source_node.children("source")) { + // Find any local adjoint sources + model::adjoint_sources.push_back(Source::create(source_node)); + } + } } } @@ -396,8 +411,10 @@ void read_settings_xml() xml_node root = doc.document_element(); // Verbosity - if (check_for_node(root, "verbosity")) { + if (check_for_node(root, "verbosity") && verbosity == -1) { verbosity = std::stoi(get_node_value(root, "verbosity")); + } else if (verbosity == -1) { + verbosity = 7; } // To this point, we haven't displayed any output since we didn't know what @@ -545,6 +562,20 @@ void read_settings_xml(pugi::xml_node root) } else if (rel_max_lost_particles <= 0.0 || rel_max_lost_particles >= 1.0) { fatal_error("Relative max lost particles must be between zero and one."); } + + // Check for user value for the number of generation of the Iterated Fission + // Probability (IFP) method + if (check_for_node(root, "ifp_n_generation")) { + ifp_n_generation = std::stoi(get_node_value(root, "ifp_n_generation")); + if (ifp_n_generation <= 0) { + fatal_error("'ifp_n_generation' must be greater than 0."); + } + // Avoid tallying 0 if IFP logs are not complete when active cycles start + if (ifp_n_generation > n_inactive) { + fatal_error("'ifp_n_generation' must be lower than or equal to the " + "number of inactive cycles."); + } + } } // Copy plotting random number seed if specified @@ -587,6 +618,11 @@ void read_settings_xml(pugi::xml_node root) } } + // Check for atomic relaxation + if (check_for_node(root, "atomic_relaxation")) { + atomic_relaxation = get_node_value_bool(root, "atomic_relaxation"); + } + // Number of bins for logarithmic grid if (check_for_node(root, "log_grid_bins")) { n_log_bins = std::stoi(get_node_value(root, "log_grid_bins")); @@ -660,6 +696,14 @@ void read_settings_xml(pugi::xml_node root) free_gas_threshold = std::stod(get_node_value(root, "free_gas_threshold")); } + // Surface grazing + if (check_for_node(root, "surface_grazing_cutoff")) + surface_grazing_cutoff = + std::stod(get_node_value(root, "surface_grazing_cutoff")); + if (check_for_node(root, "surface_grazing_ratio")) + surface_grazing_ratio = + std::stod(get_node_value(root, "surface_grazing_ratio")); + // Survival biasing if (check_for_node(root, "survival_biasing")) { survival_biasing = get_node_value_bool(root, "survival_biasing"); @@ -717,6 +761,14 @@ void read_settings_xml(pugi::xml_node root) } } + // read properties from file + if (check_for_node(root, "properties_file")) { + properties_file = get_node_value(root, "properties_file"); + if (!file_exists(properties_file)) { + fatal_error(fmt::format("File '{}' does not exist.", properties_file)); + } + } + // Particle trace if (check_for_node(root, "trace")) { auto temp = get_node_array(root, "trace"); @@ -946,7 +998,7 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_ct, "reactions")) { auto temp = get_node_array(node_ct, "reactions"); for (const auto& b : temp) { - int reaction_int = reaction_type(b); + int reaction_int = reaction_mt(b); if (reaction_int > 0) { collision_track_config.mt_numbers.insert(reaction_int); } @@ -1130,20 +1182,6 @@ void read_settings_xml(pugi::xml_node root) temperature_range[1] = range.at(1); } - // Check for user value for the number of generation of the Iterated Fission - // Probability (IFP) method - if (check_for_node(root, "ifp_n_generation")) { - ifp_n_generation = std::stoi(get_node_value(root, "ifp_n_generation")); - if (ifp_n_generation <= 0) { - fatal_error("'ifp_n_generation' must be greater than 0."); - } - // Avoid tallying 0 if IFP logs are not complete when active cycles start - if (ifp_n_generation > n_inactive) { - fatal_error("'ifp_n_generation' must be lower than or equal to the " - "number of inactive cycles."); - } - } - // Check for tabular_legendre options if (check_for_node(root, "tabular_legendre")) { // Get pointer to tabular_legendre node @@ -1209,6 +1247,7 @@ void read_settings_xml(pugi::xml_node root) // read weight windows from file if (check_for_node(root, "weight_windows_file")) { weight_windows_file = get_node_value(root, "weight_windows_file"); + weight_windows_on = true; } // read settings for weight windows value, this will override @@ -1247,6 +1286,16 @@ void read_settings_xml(pugi::xml_node root) break; } } + // If any weight window generators have local FW-CADIS target tallies, + // user-defined adjoint sources cannot be used at the same time. + if (!model::adjoint_sources.empty()) { + for (const auto& wwg : variance_reduction::weight_windows_generators) { + if (!wwg->targets_.empty()) { + fatal_error("Cannot use both user-defined adjoint sources and " + "FW-CADIS target tallies at the same time."); + } + } + } } // Set up weight window checkpoints @@ -1262,10 +1311,38 @@ void read_settings_xml(pugi::xml_node root) } } + if (weight_windows_on) { + if (!weight_window_checkpoint_surface && + !weight_window_checkpoint_collision) + fatal_error( + "Weight Windows are enabled but there are no valid checkpoints."); + } + if (check_for_node(root, "use_decay_photons")) { settings::use_decay_photons = get_node_value_bool(root, "use_decay_photons"); } + + // If weight windows are on, also enable shared secondary bank (unless + // explicitly disabled by user). + if (check_for_node(root, "shared_secondary_bank")) { + bool val = get_node_value_bool(root, "shared_secondary_bank"); + if (val && run_mode == RunMode::EIGENVALUE) { + warning( + "Shared secondary bank is not supported in eigenvalue calculations. " + "Setting will be ignored."); + } else { + settings::use_shared_secondary_bank = val; + } + } else if (settings::weight_windows_on) { + if (run_mode == RunMode::EIGENVALUE) { + warning( + "Shared secondary bank is not supported in eigenvalue calculations. " + "Particle local secondary banks will be used instead."); + } else if (run_mode == RunMode::FIXED_SOURCE) { + settings::use_shared_secondary_bank = true; + } + } } void free_memory_settings() diff --git a/src/simulation.cpp b/src/simulation.cpp index b536ae588..03f40a726 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -12,10 +12,12 @@ #include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/nuclide.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/particle.h" #include "openmc/photon.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/settings.h" #include "openmc/source.h" #include "openmc/state_point.h" @@ -30,7 +32,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #ifdef OPENMC_MPI #include @@ -40,6 +42,7 @@ #include #include +#include #include //============================================================================== @@ -86,7 +89,7 @@ int openmc_simulation_init() } // Determine how much work each process should do - calculate_work(); + calculate_work(settings::n_particles); // Allocate source, fission and surface source banks. allocate_banks(); @@ -122,6 +125,7 @@ int openmc_simulation_init() simulation::ssw_current_file = 1; simulation::k_generation.clear(); simulation::entropy.clear(); + reset_source_rejection_counters(); openmc_reset(); // If this is a restart run, load the state point data and binary source @@ -199,9 +203,12 @@ int openmc_simulation_finalize() if (settings::output_tallies && mpi::master) write_tallies(); - // If weight window generators are present in this simulation, - // write a weight windows file - if (variance_reduction::weight_windows_generators.size() > 0) { + // If weight window generators are present in this simulation, write a + // weight windows file. This is skipped during the forward solve of an + // adjoint (FW-CADIS) run, where only the adjoint-derived weight windows + // are meaningful. + if (variance_reduction::weight_windows_generators.size() > 0 && + FlatSourceDomain::solve_ != RandomRaySolve::FORWARD_FOR_ADJOINT) { openmc_weight_windows_export(); } @@ -213,6 +220,20 @@ int openmc_simulation_finalize() // Stop timers and show timing statistics simulation::time_finalize.stop(); simulation::time_total.stop(); + +#ifdef OPENMC_MPI + // Reduce track count across ranks for correct reporting. In shared secondary + // bank mode, all ranks already have the global count; in non-shared mode, + // each rank only has its own count. + if (settings::weight_windows_on && !settings::use_shared_secondary_bank) { + int64_t total_tracks; + MPI_Reduce(&simulation::simulation_tracks_completed, &total_tracks, 1, + MPI_INT64_T, MPI_SUM, 0, mpi::intracomm); + if (mpi::master) + simulation::simulation_tracks_completed = total_tracks; + } +#endif + if (mpi::master) { if (settings::solver_type != SolverType::RANDOM_RAY) { if (settings::verbosity >= 6) @@ -253,9 +274,17 @@ int openmc_next_batch(int* status) // Transport loop if (settings::event_based) { - transport_event_based(); + if (settings::use_shared_secondary_bank) { + transport_event_based_shared_secondary(); + } else { + transport_event_based(); + } } else { - transport_history_based(); + if (settings::use_shared_secondary_bank) { + transport_history_based_shared_secondary(); + } else { + transport_history_based(); + } } // Accumulate time for transport @@ -323,8 +352,98 @@ const RegularMesh* ufs_mesh {nullptr}; vector k_generation; vector work_index; +int64_t simulation_tracks_completed {0}; + } // namespace simulation +namespace { + +//! Collect thread-local secondary banks into the shared secondary bank in +//! sorted order. +//! +//! \param thread_banks Secondary banks produced by each OpenMP thread +void collect_sorted_history_secondary_banks( + vector>& thread_banks) +{ + // Count the total number of all secondary sites produced + int64_t n_collected = 0; + for (const auto& bank : thread_banks) { + n_collected += bank.size(); + } + + // Count the expected number of progeny from per-parent progeny counts + int64_t n_progeny = 0; + for (int64_t count : simulation::progeny_per_particle) { + n_progeny += count; + } + + if (n_collected != n_progeny) { + fatal_error("Mismatch detected between sum of all particle progeny and " + "secondary bank size during collection."); + } + + // Convert per-parent progeny counts to offsets into the sorted bank + std::exclusive_scan(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), + simulation::progeny_per_particle.begin(), 0); + + // Allocate the shared bank once for the complete generation + simulation::shared_secondary_bank_write.resize(0); + simulation::shared_secondary_bank_write.extend_uninitialized(n_progeny); + + // Place each secondary according to its parent and progeny identifiers + for (const auto& bank : thread_banks) { + for (const auto& site : bank) { + if (site.parent_id < 0 || + site.parent_id >= + static_cast(simulation::progeny_per_particle.size())) { + fatal_error(fmt::format("Invalid parent_id {} for banked site " + "(expected range [0, {})).", + site.parent_id, simulation::progeny_per_particle.size())); + } + int64_t idx = + simulation::progeny_per_particle[site.parent_id] + site.progeny_id; + if (idx < 0 || idx >= n_progeny) { + fatal_error("Mismatch detected between sum of all particle progeny and " + "secondary bank size during collection."); + } + simulation::shared_secondary_bank_write[idx] = site; + } + } +} + +//! Collect particle-local secondary banks into the shared secondary bank. +//! +//! \param n_particles Number of particles in the active event-based buffer +void collect_event_secondary_banks(int64_t n_particles) +{ + // Compute offsets for each particle's local secondary bank. + vector offsets(n_particles); + int64_t total = 0; + for (int64_t i = 0; i < n_particles; ++i) { + offsets[i] = total; + total += simulation::particles[i].local_secondary_bank().size(); + } + + // Extend the shared bank once for all collected secondaries + int64_t bank_offset = + simulation::shared_secondary_bank_write.extend_uninitialized(total); + + // Copy each local bank into its assigned range and clear the local storage +#pragma omp parallel for schedule(static) + for (int64_t i = 0; i < n_particles; ++i) { + auto& local_bank = simulation::particles[i].local_secondary_bank(); + if (!local_bank.empty()) { + std::copy(local_bank.cbegin(), local_bank.cend(), + simulation::shared_secondary_bank_write.data() + bank_offset + + offsets[i]); + local_bank.clear(); + } + } +} + +} // namespace + //============================================================================== // Non-member functions //============================================================================== @@ -413,7 +532,7 @@ void finalize_batch() // Reset global tally results if (simulation::current_batch <= settings::n_inactive) { - xt::view(simulation::global_tallies, xt::all()) = 0.0; + simulation::global_tallies.fill(0.0); simulation::n_realizations = 0; } @@ -546,7 +665,7 @@ void finalize_generation() // If using shared memory, stable sort the fission bank (by parent IDs) // so as to allow for reproducibility regardless of which order particles // are run in. - sort_fission_bank(); + sort_bank(simulation::fission_bank, true); // Distribute fission bank across processors evenly synchronize_bank(); @@ -570,26 +689,35 @@ void finalize_generation() } } -void initialize_history(Particle& p, int64_t index_source) +void sample_source_particle(Particle& p, int64_t index_source) { - // set defaults + // Sample a particle from the source bank if (settings::run_mode == RunMode::EIGENVALUE) { - // set defaults for eigenvalue simulations from primary bank p.from_source(&simulation::source_bank[index_source - 1]); } else if (settings::run_mode == RunMode::FIXED_SOURCE) { // initialize random number seed - int64_t id = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - simulation::work_index[mpi::rank] + index_source; + int64_t id = compute_transport_seed(compute_particle_id(index_source)); uint64_t seed = init_seed(id, STREAM_SOURCE); // sample from external source distribution or custom library then set auto site = sample_external_source(&seed); p.from_source(&site); } - p.current_work() = index_source; +} + +void initialize_particle_track( + Particle& p, int64_t index_source, bool is_secondary) +{ + // Note: index_source is 1-based (first particle = 1), but current_work() is + // stored as 0-based for direct use as an array index into + // progeny_per_particle, source_bank, ifp banks, etc. + if (!is_secondary) { + sample_source_particle(p, index_source); + } + + p.current_work() = index_source - 1; // set identifier for particle - p.id() = simulation::work_index[mpi::rank] + index_source; + p.id() = compute_particle_id(index_source); // set progeny count to zero p.n_progeny() = 0; @@ -597,6 +725,9 @@ void initialize_history(Particle& p, int64_t index_source) // Reset particle event counter p.n_event() = 0; + // Initialize track counter (1 for this primary/secondary track) + p.n_tracks() = 1; + // Reset split counter p.n_split() = 0; @@ -610,9 +741,7 @@ void initialize_history(Particle& p, int64_t index_source) std::fill(p.pht_storage().begin(), p.pht_storage().end(), 0); // set random number seed - int64_t particle_seed = - (simulation::total_gen + overall_generation() - 1) * settings::n_particles + - p.id(); + int64_t particle_seed = compute_transport_seed(p.id()); init_particle_seeds(particle_seed, p.seeds()); // set particle trace @@ -626,17 +755,21 @@ void initialize_history(Particle& p, int64_t index_source) p.write_track() = check_track_criteria(p); // Set the particle's initial weight window value. - p.wgt_ww_born() = -1.0; - apply_weight_windows(p); + if (!is_secondary) { + p.wgt_ww_born() = -1.0; + apply_weight_windows(p); + } // Display message if high verbosity or trace is on if (settings::verbosity >= 9 || p.trace()) { write_message("Simulating Particle {}", p.id()); } -// Add particle's starting weight to count for normalizing tallies later + // Add particle's starting weight to count for normalizing tallies later + if (!is_secondary) { #pragma omp atomic - simulation::total_weight += p.wgt(); + simulation::total_weight += p.wgt(); + } // Force calculation of cross-sections by setting last energy to zero if (settings::run_CE) { @@ -654,13 +787,34 @@ int overall_generation() return settings::gen_per_batch * (current_batch - 1) + current_gen; } -void calculate_work() +int64_t compute_particle_id(int64_t index_source) +{ + if (settings::use_shared_secondary_bank) { + return simulation::work_index[mpi::rank] + index_source + + simulation::simulation_tracks_completed; + } else { + return simulation::work_index[mpi::rank] + index_source; + } +} + +int64_t compute_transport_seed(int64_t particle_id) +{ + if (settings::use_shared_secondary_bank) { + return particle_id; + } else { + return (simulation::total_gen + overall_generation() - 1) * + settings::n_particles + + particle_id; + } +} + +void calculate_work(int64_t n_particles) { // Determine minimum amount of particles to simulate on each processor - int64_t min_work = settings::n_particles / mpi::n_procs; + int64_t min_work = n_particles / mpi::n_procs; // Determine number of processors that have one extra particle - int64_t remainder = settings::n_particles % mpi::n_procs; + int64_t remainder = n_particles % mpi::n_procs; int64_t i_bank = 0; simulation::work_index.resize(mpi::n_procs + 1); @@ -687,7 +841,7 @@ void initialize_data() for (const auto& nuc : data::nuclides) { if (nuc->grid_.size() >= 1) { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); data::energy_min[neutron] = std::max(data::energy_min[neutron], nuc->grid_[0].energy.front()); data::energy_max[neutron] = @@ -698,7 +852,7 @@ void initialize_data() if (settings::photon_transport) { for (const auto& elem : data::elements) { if (elem->energy_.size() >= 1) { - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); int n = elem->energy_.size(); data::energy_min[photon] = std::max(data::energy_min[photon], std::exp(elem->energy_(1))); @@ -711,9 +865,9 @@ void initialize_data() // Determine if minimum/maximum energy for bremsstrahlung is greater/less // than the current minimum/maximum if (data::ttb_e_grid.size() >= 1) { - int photon = static_cast(ParticleType::photon); - int electron = static_cast(ParticleType::electron); - int positron = static_cast(ParticleType::positron); + int photon = ParticleType::photon().transport_index(); + int electron = ParticleType::electron().transport_index(); + int positron = ParticleType::positron().transport_index(); int n_e = data::ttb_e_grid.size(); const std::vector charged = {electron, positron}; @@ -737,7 +891,7 @@ void initialize_data() // grid has not been allocated if (nuc->grid_.size() > 0) { double max_E = nuc->grid_[0].energy.back(); - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); if (max_E == data::energy_max[neutron]) { write_message(7, "Maximum neutron transport energy: {} eV for {}", data::energy_max[neutron], nuc->name_); @@ -754,7 +908,7 @@ void initialize_data() for (auto& nuc : data::nuclides) { nuc->init_grid(); } - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); simulation::log_spacing = std::log(data::energy_max[neutron] / data::energy_min[neutron]) / settings::n_log_bins; @@ -815,21 +969,140 @@ void transport_history_based_single_particle(Particle& p) p.event_collide(); } } - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); } p.event_death(); } void transport_history_based() { -#pragma omp parallel for schedule(runtime) - for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { +#pragma omp parallel + { Particle p; - initialize_history(p, i_work); - transport_history_based_single_particle(p); +#pragma omp for schedule(runtime) + for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { + initialize_particle_track(p, i_work, false); + transport_history_based_single_particle(p); + } } } +// The shared secondary bank transport algorithm works in two phases. In the +// first phase, all primary particles are sampled then transported, and their +// secondary particles are deposited into a shared secondary bank. The second +// phase occurs in a loop, where all secondary tracks in the shared secondary +// bank are transported. Any secondary particles generated during this phase are +// deposited back into the shared secondary bank. The shared secondary bank is +// sorted for consistent ordering and load balanced across MPI ranks. This loop +// continues until there are no more secondary tracks left to transport. +void transport_history_based_shared_secondary() +{ + // Clear shared secondary banks from any prior use + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); + + if (mpi::master) { + write_message(fmt::format(" Primary source particles: {}", + settings::n_particles), + 6); + } + + simulation::progeny_per_particle.resize(simulation::work_per_rank); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + vector> thread_banks(num_threads()); + + // Phase 1: Transport primary particles and deposit first generation of + // secondaries in the shared secondary bank +#pragma omp parallel + { + auto& thread_bank = thread_banks[thread_num()]; + Particle p; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::work_per_rank; i++) { + initialize_particle_track(p, i, false); + transport_history_based_single_particle(p); + for (auto& site : p.local_secondary_bank()) { + thread_bank.push_back(site); + } + p.local_secondary_bank().clear(); + } + } + collect_sorted_history_secondary_banks(thread_banks); + thread_banks.clear(); + + simulation::simulation_tracks_completed += settings::n_particles; + + // Phase 2: Now that the secondary bank has been populated, enter loop over + // all secondary generations + int n_generation_depth = 1; + int64_t alive_secondary = 1; + while (alive_secondary) { + + // Synchronize the shared secondary bank amongst all MPI ranks, such + // that each MPI rank has an approximately equal number of secondary + // tracks. Also reports the total number of secondaries alive across + // all MPI ranks. + alive_secondary = synchronize_global_secondary_bank( + simulation::shared_secondary_bank_write); + + // Recalculate work for each MPI rank based on number of alive secondary + // tracks + calculate_work(alive_secondary); + + // Display the number of secondary tracks in this generation. This + // is useful for user monitoring so as to see if the secondary population is + // exploding and to determine how many generations of secondaries are being + // transported. + if (mpi::master) { + write_message(fmt::format(" Secondary generation {:<2} tracks: {}", + n_generation_depth, alive_secondary), + 6); + } + + simulation::shared_secondary_bank_read = + std::move(simulation::shared_secondary_bank_write); + simulation::shared_secondary_bank_write = SharedArray(); + simulation::progeny_per_particle.resize( + simulation::shared_secondary_bank_read.size()); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + thread_banks.resize(num_threads()); + + // Transport all secondary tracks from the shared secondary bank +#pragma omp parallel + { + auto& thread_bank = thread_banks[thread_num()]; + Particle p; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::shared_secondary_bank_read.size(); + i++) { + initialize_particle_track(p, i, true); + SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; + p.event_revive_from_secondary(site); + transport_history_based_single_particle(p); + for (auto& secondary_site : p.local_secondary_bank()) { + thread_bank.push_back(secondary_site); + } + p.local_secondary_bank().clear(); + } + } // End of transport loop over tracks in shared secondary bank + simulation::shared_secondary_bank_write = + std::move(simulation::shared_secondary_bank_read); + simulation::shared_secondary_bank_read = SharedArray(); + collect_sorted_history_secondary_banks(thread_banks); + thread_banks.clear(); + n_generation_depth++; + simulation::simulation_tracks_completed += alive_secondary; + } // End of loop over secondary generations + + // Reset work so that fission bank etc works correctly + calculate_work(settings::n_particles); +} + void transport_event_based() { int64_t remaining_work = simulation::work_per_rank; @@ -847,33 +1120,7 @@ void transport_event_based() // Initialize all particle histories for this subiteration process_init_events(n_particles, source_offset); - - // Event-based transport loop - while (true) { - // Determine which event kernel has the longest queue - int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), - simulation::calculate_nonfuel_xs_queue.size(), - simulation::advance_particle_queue.size(), - simulation::surface_crossing_queue.size(), - simulation::collision_queue.size()}); - - // Execute event with the longest queue - if (max == 0) { - break; - } else if (max == simulation::calculate_fuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_fuel_xs_queue); - } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); - } else if (max == simulation::advance_particle_queue.size()) { - process_advance_particle_events(); - } else if (max == simulation::surface_crossing_queue.size()) { - process_surface_crossing_events(); - } else if (max == simulation::collision_queue.size()) { - process_collision_events(); - } - } - - // Execute death event for all particles + process_transport_events(); process_death_events(n_particles); // Adjust remaining work and source offset variables @@ -882,4 +1129,110 @@ void transport_event_based() } } +void transport_event_based_shared_secondary() +{ + // Clear shared secondary banks from any prior use + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); + + if (mpi::master) { + write_message(fmt::format(" Primary source particles: {}", + settings::n_particles), + 6); + } + + simulation::progeny_per_particle.resize(simulation::work_per_rank); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Phase 1: Transport primary particles using event-based processing and + // deposit first generation of secondaries in the shared secondary bank + int64_t remaining_work = simulation::work_per_rank; + int64_t source_offset = 0; + + while (remaining_work > 0) { + int64_t n_particles = + std::min(remaining_work, settings::max_particles_in_flight); + + process_init_events(n_particles, source_offset); + process_transport_events(); + process_death_events(n_particles); + + collect_event_secondary_banks(n_particles); + + remaining_work -= n_particles; + source_offset += n_particles; + } + + simulation::simulation_tracks_completed += settings::n_particles; + + // Phase 2: Now that the secondary bank has been populated, enter loop over + // all secondary generations + int n_generation_depth = 1; + int64_t alive_secondary = 1; + while (alive_secondary) { + + // Sort the shared secondary bank by parent ID then progeny ID to + // ensure reproducibility. + sort_bank(simulation::shared_secondary_bank_write, false); + + // Synchronize the shared secondary bank amongst all MPI ranks, such + // that each MPI rank has an approximately equal number of secondary + // tracks. + alive_secondary = synchronize_global_secondary_bank( + simulation::shared_secondary_bank_write); + + // Recalculate work for each MPI rank based on number of alive secondary + // tracks + calculate_work(alive_secondary); + + if (mpi::master) { + write_message(fmt::format(" Secondary generation {:<2} tracks: {}", + n_generation_depth, alive_secondary), + 6); + } + + simulation::shared_secondary_bank_read = + std::move(simulation::shared_secondary_bank_write); + simulation::shared_secondary_bank_write = SharedArray(); + simulation::progeny_per_particle.resize( + simulation::shared_secondary_bank_read.size()); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Ensure particle buffer is large enough for this secondary generation + int64_t sec_buffer_length = std::min( + static_cast(simulation::shared_secondary_bank_read.size()), + settings::max_particles_in_flight); + if (sec_buffer_length > + static_cast(simulation::particles.size())) { + init_event_queues(sec_buffer_length); + } + + // Transport secondary tracks using event-based processing + int64_t sec_remaining = simulation::shared_secondary_bank_read.size(); + int64_t sec_offset = 0; + + while (sec_remaining > 0) { + int64_t n_particles = + std::min(sec_remaining, settings::max_particles_in_flight); + + process_init_secondary_events( + n_particles, sec_offset, simulation::shared_secondary_bank_read); + process_transport_events(); + process_death_events(n_particles); + + collect_event_secondary_banks(n_particles); + + sec_remaining -= n_particles; + sec_offset += n_particles; + } // End of subiteration loop over secondary tracks + n_generation_depth++; + simulation::simulation_tracks_completed += alive_secondary; + } // End of loop over secondary generations + + // Reset work so that fission bank etc works correctly + calculate_work(settings::n_particles); +} + } // namespace openmc diff --git a/src/source.cpp b/src/source.cpp index f6aa665eb..12bbdf84e 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -4,29 +4,34 @@ #define HAS_DYNAMIC_LINKING #endif -#include // for move +#include // for max +#include // for sin, cos, abs +#include // for move #ifdef HAS_DYNAMIC_LINKING #include // for dlopen, dlsym, dlclose, dlerror #endif -#include "xtensor/xadapt.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" #include "openmc/capi.h" #include "openmc/cell.h" +#include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/error.h" #include "openmc/file_utils.h" #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" #include "openmc/material.h" +#include "openmc/math_functions.h" #include "openmc/mcpl_interface.h" #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" +#include "openmc/random_dist.h" #include "openmc/random_lcg.h" #include "openmc/search.h" #include "openmc/settings.h" @@ -37,6 +42,23 @@ namespace openmc { +std::atomic source_n_accept {0}; +std::atomic source_n_reject {0}; + +namespace { + +void validate_particle_type(ParticleType type, const std::string& context) +{ + if (type.is_transportable()) + return; + + fatal_error( + fmt::format("Unsupported source particle type '{}' (PDG {}) in {}.", + type.str(), type.pdg_number(), context)); +} + +} // namespace + //============================================================================== // Global variables //============================================================================== @@ -45,6 +67,8 @@ namespace model { vector> external_sources; +vector> adjoint_sources; + DiscreteIndex external_sources_probability; } // namespace model @@ -81,6 +105,8 @@ unique_ptr Source::create(pugi::xml_node node) return make_unique(node); } else if (source_type == "mesh") { return make_unique(node); + } else if (source_type == "tokamak") { + return make_unique(node); } else { fatal_error(fmt::format("Invalid source type '{}' found.", source_type)); } @@ -177,9 +203,8 @@ void check_rejection_fraction(int64_t n_reject, int64_t n_accept) SourceSite Source::sample_with_constraints(uint64_t* seed) const { bool accepted = false; - static int64_t n_reject = 0; - static int64_t n_accept = 0; - SourceSite site; + int64_t n_local_reject = 0; + SourceSite site {}; while (!accepted) { // Sample a source site without considering constraints yet @@ -193,9 +218,13 @@ SourceSite Source::sample_with_constraints(uint64_t* seed) const satisfies_energy_constraints(site.E) && satisfies_time_constraints(site.time); if (!accepted) { - // Increment number of rejections and check against minimum fraction - ++n_reject; - check_rejection_fraction(n_reject, n_accept); + ++n_local_reject; + + // Check per-particle rejection limit + if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) { + fatal_error("Exceeded maximum number of source rejections per " + "sample. Please check your source definition."); + } // For the "kill" strategy, accept particle but set weight to 0 so that // it is terminated immediately @@ -207,8 +236,13 @@ SourceSite Source::sample_with_constraints(uint64_t* seed) const } } - // Increment number of accepted samples - ++n_accept; + // Flush local rejection count, update accept counter, and check overall + // rejection fraction + if (n_local_reject > 0) { + source_n_reject += n_local_reject; + } + ++source_n_accept; + check_rejection_fraction(source_n_reject, source_n_accept); return site; } @@ -284,22 +318,15 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) { // Check for particle type if (check_for_node(node, "particle")) { - auto temp_str = get_node_value(node, "particle", true, true); - if (temp_str == "neutron") { - particle_ = ParticleType::neutron; - } else if (temp_str == "photon") { - particle_ = ParticleType::photon; + auto temp_str = get_node_value(node, "particle", false, true); + particle_ = ParticleType(temp_str); + if (particle_ == ParticleType::photon() || + particle_ == ParticleType::electron() || + particle_ == ParticleType::positron()) { settings::photon_transport = true; - } else if (temp_str == "electron") { - particle_ = ParticleType::electron; - settings::photon_transport = true; - } else if (temp_str == "positron") { - particle_ = ParticleType::positron; - settings::photon_transport = true; - } else { - fatal_error(std::string("Unknown source particle type: ") + temp_str); } } + validate_particle_type(particle_, "IndependentSource"); // Check for external source file if (check_for_node(node, "file")) { @@ -334,6 +361,19 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) if (check_for_node(node, "energy")) { pugi::xml_node node_dist = node.child("energy"); energy_ = distribution_from_xml(node_dist); + + // For decay photon sources, use the absolute photon emission rate in + // [photons/s] as the source strength + if (dynamic_cast(energy_.get())) { + if (strength_ != 1.0) { + warning(fmt::format( + "Source strength of {} is ignored because the source uses a " + "DecaySpectrum energy distribution. The source strength will be " + "set from the DecaySpectrum emission rate.", + strength_)); + } + strength_ = energy_->integral(); + } } else { // Default to a Watt spectrum with parameters 0.988 MeV and 2.249 MeV^-1 energy_ = UPtrDist {new Watt(0.988e6, 2.249e-6)}; @@ -354,64 +394,93 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) SourceSite IndependentSource::sample(uint64_t* seed) const { - SourceSite site; + SourceSite site {}; site.particle = particle_; + double r_wgt = 1.0; + double E_wgt = 1.0; // Repeat sampling source location until a good site has been accepted bool accepted = false; - static int64_t n_reject = 0; - static int64_t n_accept = 0; + int64_t n_local_reject = 0; while (!accepted) { // Sample spatial distribution - site.r = space_->sample(seed); + auto [r, r_wgt_temp] = space_->sample(seed); + site.r = r; + r_wgt = r_wgt_temp; // Check if sampled position satisfies spatial constraints accepted = satisfies_spatial_constraints(site.r); // Check for rejection if (!accepted) { - ++n_reject; - check_rejection_fraction(n_reject, n_accept); + ++n_local_reject; + if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) { + fatal_error("Exceeded maximum number of source rejections per " + "sample. Please check your source definition."); + } } } // Sample angle - site.u = angle_->sample(seed); + auto [u, u_wgt] = angle_->sample(seed); + site.u = u; + + site.wgt = r_wgt * u_wgt; // Sample energy and time for neutron and photon sources if (settings::solver_type != SolverType::RANDOM_RAY) { // Check for monoenergetic source above maximum particle energy - auto p = static_cast(particle_); + auto p = particle_.transport_index(); auto energy_ptr = dynamic_cast(energy_.get()); + auto decay_spectrum = dynamic_cast(energy_.get()); if (energy_ptr) { - auto energies = xt::adapt(energy_ptr->x()); - if (xt::any(energies > data::energy_max[p])) { + auto energies = + tensor::Tensor(energy_ptr->x().data(), energy_ptr->x().size()); + if ((energies > data::energy_max[p]).any()) { fatal_error("Source energy above range of energies of at least " "one cross section table"); } } while (true) { - // Sample energy spectrum - site.E = energy_->sample(seed); + // Sample energy spectrum. For decay photon sources, also get the parent + // nuclide index to store in the source site for tallying purposes. + if (decay_spectrum) { + auto sample = decay_spectrum->sample_with_parent(seed); + site.E = sample.energy; + E_wgt = sample.weight; + site.parent_nuclide = sample.parent_nuclide; + } else { + auto [E, E_wgt_temp] = energy_->sample(seed); + site.E = E; + E_wgt = E_wgt_temp; + } // Resample if energy falls above maximum particle energy if (site.E < data::energy_max[p] && (satisfies_energy_constraints(site.E))) break; - n_reject++; - check_rejection_fraction(n_reject, n_accept); + ++n_local_reject; + if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) { + fatal_error("Exceeded maximum number of source rejections per " + "sample. Please check your source definition."); + } } // Sample particle creation time - site.time = time_->sample(seed); + auto [time, time_wgt] = time_->sample(seed); + site.time = time; + + site.wgt *= (E_wgt * time_wgt); } - // Increment number of accepted samples - ++n_accept; + // Flush local rejection count into global counter + if (n_local_reject > 0) { + source_n_reject += n_local_reject; + } return site; } @@ -460,6 +529,18 @@ void FileSource::load_sites_from_file(const std::string& path) // Close file file_close(file_id); } + + // Make sure particles in source file have valid types. If any particle is a + // photon, electron, or positron, enable photon transport so that the + // appropriate cross sections are loaded. + for (const auto& site : this->sites_) { + validate_particle_type(site.particle, "FileSource"); + if (site.particle == ParticleType::photon() || + site.particle == ParticleType::electron() || + site.particle == ParticleType::positron()) { + settings::photon_transport = true; + } + } } SourceSite FileSource::sample(uint64_t* seed) const @@ -537,9 +618,9 @@ CompiledSourceWrapper::~CompiledSourceWrapper() // MeshElementSpatial implementation //============================================================================== -Position MeshElementSpatial::sample(uint64_t* seed) const +std::pair MeshElementSpatial::sample(uint64_t* seed) const { - return model::meshes[mesh_index_]->sample_element(elem_index_, seed); + return {model::meshes[mesh_index_]->sample_element(elem_index_, seed), 1.0}; } //============================================================================== @@ -573,6 +654,11 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node) std::make_unique(mesh_idx, elem_index)); } + // Make sure sources use valid particle types + for (const auto& src : sources_) { + validate_particle_type(src->particle_type(), "MeshSource"); + } + // the number of source distributions should either be one or equal to the // number of mesh elements if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) { @@ -594,6 +680,471 @@ SourceSite MeshSource::sample(uint64_t* seed) const return source(element)->sample_with_constraints(seed); } +//============================================================================== +// TokamakSource implementation +//============================================================================== + +TokamakSource::TokamakSource(pugi::xml_node node) : Source(node) +{ + // Read geometry parameters + major_radius_ = std::stod(get_node_value(node, "major_radius")); + minor_radius_ = std::stod(get_node_value(node, "minor_radius")); + elongation_ = std::stod(get_node_value(node, "elongation")); + triangularity_ = std::stod(get_node_value(node, "triangularity")); + shafranov_shift_ = std::stod(get_node_value(node, "shafranov_shift")); + + // Read optional vertical shift + if (check_for_node(node, "vertical_shift")) { + vertical_shift_ = std::stod(get_node_value(node, "vertical_shift")); + } else { + vertical_shift_ = 0.0; + } + + // Read optional toroidal angle bounds + if (check_for_node(node, "phi_start")) { + phi_start_ = std::stod(get_node_value(node, "phi_start")); + } else { + phi_start_ = 0.0; + } + if (check_for_node(node, "phi_extent")) { + phi_extent_ = std::stod(get_node_value(node, "phi_extent")); + } else { + phi_extent_ = 2.0 * PI; + } + if (check_for_node(node, "n_alpha")) { + n_alpha_ = std::stoi(get_node_value(node, "n_alpha")); + } else { + n_alpha_ = 101; // Default + } + + // Read emission profile + r_over_a_ = get_node_array(node, "r_over_a"); + emission_density_ = get_node_array(node, "emission_density"); + + // Read energy distribution(s) + for (auto energy_node : node.children("energy")) { + energy_dists_.push_back(distribution_from_xml(energy_node)); + } + + // Read optional time distribution; default to a delta distribution at t=0 + // for the same behavior as IndependentSource + if (check_for_node(node, "time")) { + time_ = distribution_from_xml(node.child("time")); + } else { + double T[] {0.0}; + double p[] {1.0}; + time_ = UPtrDist {new Discrete {T, p, 1}}; + } + + // Validate inputs + if (emission_density_.size() != r_over_a_.size()) { + fatal_error("TokamakSource: emission_density and r_over_a must have the " + "same length."); + } + if (r_over_a_.size() < 2) { + fatal_error( + "TokamakSource: At least 2 radial points are required for profiles."); + } + if (r_over_a_.front() != 0.0) { + fatal_error("TokamakSource: r_over_a must start at 0."); + } + if (r_over_a_.back() != 1.0) { + fatal_error("TokamakSource: r_over_a must end at 1."); + } + for (size_t i = 1; i < r_over_a_.size(); ++i) { + if (r_over_a_[i] <= r_over_a_[i - 1]) { + fatal_error("TokamakSource: r_over_a must be strictly increasing."); + } + } + for (size_t i = 0; i < emission_density_.size(); ++i) { + if (emission_density_[i] < 0.0) { + fatal_error("TokamakSource: emission_density values cannot be negative."); + } + } + if (major_radius_ <= 0.0) { + fatal_error("TokamakSource: major_radius must be > 0."); + } + if (minor_radius_ <= 0.0) { + fatal_error("TokamakSource: minor_radius must be > 0."); + } + if (minor_radius_ >= major_radius_) { + fatal_error("TokamakSource: minor_radius must be less than major_radius."); + } + if (elongation_ <= 0.0) { + fatal_error("TokamakSource: elongation must be > 0."); + } + if (triangularity_ < -1.0 || triangularity_ > 1.0) { + fatal_error("TokamakSource: triangularity must be in the range [-1, 1]."); + } + if (shafranov_shift_ < 0.0) { + fatal_error("TokamakSource: shafranov_shift must be >= 0."); + } + if (shafranov_shift_ >= 0.5 * minor_radius_) { + fatal_error("TokamakSource: shafranov_shift must be less than half the " + "minor radius."); + } + if (phi_extent_ <= 0.0 || phi_extent_ > 2.0 * PI) { + fatal_error("TokamakSource: phi_extent must be > 0 and <= 2*pi."); + } + if (n_alpha_ <= 2) { + fatal_error("TokamakSource: n_alpha must be > 2."); + } + if (n_alpha_ < 51) { + warning("TokamakSource: n_alpha values below 51 may introduce noticeable " + "discretization bias in source sampling."); + } + if (energy_dists_.empty()) { + fatal_error("TokamakSource: At least one energy distribution is required."); + } + if (energy_dists_.size() != 1 && energy_dists_.size() != r_over_a_.size()) { + fatal_error("TokamakSource: energy distributions must be either 1 (for all " + "r) or match the number of r_over_a points."); + } + + // Compute normalized geometry parameters + epsilon_ = minor_radius_ / major_radius_; + delta_tilde_ = shafranov_shift_ / minor_radius_; + + // Initialize isotropic angular distribution + angle_ = UPtrAngle {new Isotropic()}; + + precompute_sampling_distributions(); +} + +void TokamakSource::precompute_sampling_distributions() +{ + // Use precomputed normalized geometry parameters + double eps = epsilon_; // Inverse aspect ratio (a/R0) + double Dt = delta_tilde_; // Normalized Shafranov shift (Delta/a) + double delta = triangularity_; + + //========================================================================== + // RADIAL CDF (computed first since it's simpler and sampled first) + //========================================================================== + // The marginal radial PDF is obtained by analytically integrating the joint + // distribution f(r_tilde, alpha) over alpha. The result is: + // + // p(r_tilde) ~ S(r_tilde) * [(1 + eps*Dt)*r_tilde + // - (3/8)*c1*eps*r_tilde^2 + // - 2*eps*Dt*r_tilde^3] + // + // where the Bessel function coefficients are: + // c0 = J_0(delta) + J_2(delta) + // c1 = (J_1(2*delta) + J_3(2*delta)) / c0 + // + // For delta -> 0, c0 -> 1 and c1 -> 0, giving the circular cross-section + // limit. + + // Compute Bessel function coefficients. openmc::cyl_bessel_j handles + // negative arguments (negative triangularity) via the parity relation + // J_n(-x) = (-1)^n * J_n(x). + double J0_d = cyl_bessel_j(0, delta); + double J2_d = cyl_bessel_j(2, delta); + double J1_2d = cyl_bessel_j(1, 2.0 * delta); + double J3_2d = cyl_bessel_j(3, 2.0 * delta); + double c0 = J0_d + J2_d; + double c1 = (J1_2d + J3_2d) / c0; + + // Coefficients for the radial polynomial: A*r - B*r^2 - C*r^3 + radial_poly_a_ = 1.0 + eps * Dt; + radial_poly_b_ = 0.375 * c1 * eps; // 3/8 * c1 * eps + radial_poly_c_ = 2.0 * eps * Dt; + + // Build a refined radial grid that retains the user-specified grid points. + // The emission density is interpreted as linear-linear between those points. + constexpr int MIN_SUBINTERVALS = 8; + constexpr double MAX_GRID_SPACING = 1.0e-3; + vector radial_grid {r_over_a_.front()}; + vector radial_emission {emission_density_.front()}; + for (size_t i = 1; i < r_over_a_.size(); ++i) { + double r_lo = r_over_a_[i - 1]; + double r_hi = r_over_a_[i]; + double s_lo = emission_density_[i - 1]; + double s_hi = emission_density_[i]; + int n_subintervals = std::max(MIN_SUBINTERVALS, + static_cast(std::ceil((r_hi - r_lo) / MAX_GRID_SPACING))); + for (int j = 1; j <= n_subintervals; ++j) { + double t = static_cast(j) / n_subintervals; + radial_grid.push_back(r_lo + t * (r_hi - r_lo)); + radial_emission.push_back(s_lo + t * (s_hi - s_lo)); + } + } + + vector radial_pdf(radial_grid.size()); + for (size_t i = 0; i < radial_grid.size(); ++i) { + double r = radial_grid[i]; + // p(r) ~ S(r) * [A*r - B*r^2 - C*r^3] + double geometric_factor = + radial_poly_a_ * r - radial_poly_b_ * r * r - radial_poly_c_ * r * r * r; + radial_pdf[i] = radial_emission[i] * std::max(0.0, geometric_factor); + } + + // Check that the refined profile contains positive probability mass before + // constructing the normalized tabular distribution. + double total = 0.0; + for (size_t i = 1; i < radial_grid.size(); ++i) { + total += 0.5 * (radial_pdf[i - 1] + radial_pdf[i]) * + (radial_grid[i] - radial_grid[i - 1]); + } + if (total <= 0.0) { + fatal_error( + "TokamakSource: Integrated emission density is zero or negative. " + "Check emission_density profile."); + } + radial_dist_ = make_unique(radial_grid.data(), radial_pdf.data(), + radial_grid.size(), Interpolation::lin_lin); + + //========================================================================== + // POLOIDAL CDFs (for conditional sampling of alpha given r) + //========================================================================== + // The conditional distribution P(alpha | r) is a mixture: + // P(alpha | r) ~ sum_k w_k(r) * I_hat_k * p_k(alpha) + // where: + // - w_k(r) are the "dynamic" Bernstein weight functions (depend on r) + // - I_hat_k are the "static" normalized integrals (precomputed constants) + // - p_k(alpha) are the normalized basis distributions (precomputed CDFs) + // + // The static weights I_hat_k = I_k / (2*pi*c0) are: + // I_hat_0 = 1 + eps*Dt + // I_hat_1 = 1 + eps*Dt - (3/16)*c1*eps + // I_hat_2 = 1 - (3/8)*c1*eps + // I_hat_3 = 1 + eps*Dt + // I_hat_4 = 1 + (1/2)*eps*Dt - (3/16)*c1*eps + // I_hat_5 = 1 - eps*Dt - (3/8)*c1*eps + + // Compute static weights analytically + poloidal_integrals_[0] = 1.0 + eps * Dt; + poloidal_integrals_[1] = 1.0 + eps * Dt - 0.1875 * c1 * eps; // 3/16 = 0.1875 + poloidal_integrals_[2] = 1.0 - 0.375 * c1 * eps; // 3/8 = 0.375 + poloidal_integrals_[3] = 1.0 + eps * Dt; + poloidal_integrals_[4] = 1.0 + 0.5 * eps * Dt - 0.1875 * c1 * eps; + poloidal_integrals_[5] = 1.0 - eps * Dt - 0.375 * c1 * eps; + + // Build the alpha grid on [0, pi] (half domain due to up-down symmetry) + int n_alpha = n_alpha_; + vector alpha_grid(n_alpha); + double dalpha = PI / (n_alpha - 1); + for (int i = 0; i < n_alpha; ++i) { + alpha_grid[i] = i * dalpha; + } + + // Compute basis function values g_k(alpha) for tabular distributions + // Using Bernstein form: + // R_tilde = b0*(1-r)^2 + 2*b1*r*(1-r) + b2*r^2 + // J_tilde = b3*(1-r) + b4*r + // with: + // b0(alpha) = 1 + eps*Dt + // b1(alpha) = b0 + (eps/2)*cos(psi), psi = alpha + delta*sin(alpha) + // b2(alpha) = 1 + eps*cos(psi) + // b3(alpha) = cos(delta*sin(alpha)) + // + (delta/4)*(cos(alpha - delta*sin(alpha)) + // - cos(3*alpha + delta*sin(alpha))) + // b4(alpha) = b3(alpha) - 2*Dt*cos(alpha) + + array, N_POLOIDAL_BASIS> basis; + for (int k = 0; k < N_POLOIDAL_BASIS; ++k) { + basis[k].resize(n_alpha); + } + + for (int i = 0; i < n_alpha; ++i) { + double alpha = alpha_grid[i]; + double sin_alpha = std::sin(alpha); + double cos_alpha = std::cos(alpha); + double delta_sin_alpha = delta * sin_alpha; + double psi = alpha + delta_sin_alpha; + double cos_psi = std::cos(psi); + + // Bernstein coefficients b0-b4 + double b0 = 1.0 + eps * Dt; + double b1 = b0 + 0.5 * eps * cos_psi; + double b2 = 1.0 + eps * cos_psi; + double b3 = + std::cos(delta_sin_alpha) + 0.25 * delta * + (std::cos(alpha - delta_sin_alpha) - + std::cos(3.0 * alpha + delta_sin_alpha)); + double b4 = b3 - 2.0 * Dt * cos_alpha; + + // 6 basis functions g_k(alpha) = b_i * b_j + basis[0][i] = b0 * b3; // w0 = (1-r)^3 + basis[1][i] = b1 * b3; // w1 = 2*r*(1-r)^2 + basis[2][i] = b2 * b3; // w2 = r^2*(1-r) + basis[3][i] = b0 * b4; // w3 = r*(1-r)^2 + basis[4][i] = b1 * b4; // w4 = 2*r^2*(1-r) + basis[5][i] = b2 * b4; // w5 = r^3 + } + + // Build a linear-linear distribution for each basis function p_k(alpha) + for (int k = 0; k < N_POLOIDAL_BASIS; ++k) { + poloidal_dists_[k] = make_unique( + alpha_grid.data(), basis[k].data(), n_alpha, Interpolation::lin_lin); + } +} + +double TokamakSource::sample_r_over_a(uint64_t* seed) const +{ + return radial_dist_->sample(seed).first; +} + +double TokamakSource::mixture_weight(int k, double r) const +{ + double s = 1.0 - r; + switch (k) { + case 0: + return s * s * s * poloidal_integrals_[0]; + case 1: + return 2.0 * r * s * s * poloidal_integrals_[1]; + case 2: + return r * r * s * poloidal_integrals_[2]; + case 3: + return r * s * s * poloidal_integrals_[3]; + case 4: + return 2.0 * r * r * s * poloidal_integrals_[4]; + case 5: + return r * r * r * poloidal_integrals_[5]; + default: + UNREACHABLE(); + } +} + +double TokamakSource::sample_poloidal_angle(double r_norm, uint64_t* seed) const +{ + // Sample from the conditional distribution P(alpha | r_tilde) using + // mixture sampling with 6 precomputed basis distributions. + // + // The conditional is: P(alpha | r) ~ sum_k w_k(r) * I_hat_k * p_k(alpha) + // where: + // - w_k(r) are the "dynamic" Bernstein weight functions + // - I_hat_k are the "static" normalized integrals (precomputed in + // poloidal_integrals_) + // - p_k(alpha) are the normalized, precomputed basis distributions + // + // The normalization sum_k w_k(r) * I_hat_k equals the radial geometric + // polynomial evaluated at r, which is known analytically. + // + // Algorithm: + // 1. Compute total from analytical normalization + // 2. Lazily evaluate mixture weights with early exit to select component k + // 3. Sample alpha from the selected basis distribution + + // Analytical normalization: sum_k w_k(r) * I_hat_k + double total = + radial_poly_a_ - radial_poly_b_ * r_norm - radial_poly_c_ * r_norm * r_norm; + double xi = prn(seed) * total; + + // Sample component via lazy evaluation with early exit + // Order optimized for peaked emission profiles: 0, 1, 4, 5, 3, 2 + constexpr int order[] = {0, 1, 4, 5, 3, 2}; + double cumsum = 0.0; + int component = order[N_POLOIDAL_BASIS - 1]; + for (int i = 0; i < N_POLOIDAL_BASIS; ++i) { + cumsum += mixture_weight(order[i], r_norm); + if (xi < cumsum) { + component = order[i]; + break; + } + } + + // Sample alpha from [0, pi] + double alpha = poloidal_dists_[component]->sample(seed).first; + + // Exploit up-down symmetry: randomly flip to [pi, 2*pi] with 50% probability + // This is equivalent to flipping the sign of Z in the final position + if (prn(seed) >= 0.5) { + alpha = 2.0 * PI - alpha; + } + return alpha; +} + +std::pair TokamakSource::sample_energy( + double r_norm, uint64_t* seed) const +{ + if (energy_dists_.size() == 1) { + // Single distribution for all r + return energy_dists_[0]->sample(seed); + } + + // Multiple distributions: stochastic selection between bracketing r points + // Find the interval containing r_norm + size_t i = lower_bound_index(r_over_a_.begin(), r_over_a_.end(), r_norm); + + // Handle boundary cases + if (i >= energy_dists_.size() - 1) { + return energy_dists_.back()->sample(seed); + } + + // Stochastic interpolation: randomly select one of the two bracketing + // distributions based on distance to each + double t = (r_norm - r_over_a_[i]) / (r_over_a_[i + 1] - r_over_a_[i]); + size_t idx = (prn(seed) < t) ? i + 1 : i; + return energy_dists_[idx]->sample(seed); +} + +Position TokamakSource::flux_to_cartesian( + double r, double alpha, double phi) const +{ + // Flux surface parameterization: + // R = R0 + r*cos(alpha + delta*sin(alpha)) + Delta*(1 - (r/a)^2) + // Z = kappa * r * sin(alpha) + // x = R * cos(phi) + // y = R * sin(phi) + // z = Z + + double psi = alpha + triangularity_ * std::sin(alpha); + double r_over_a_sq = (r * r) / (minor_radius_ * minor_radius_); + + double R = + major_radius_ + r * std::cos(psi) + shafranov_shift_ * (1.0 - r_over_a_sq); + double Z = elongation_ * r * std::sin(alpha); + + double x = R * std::cos(phi); + double y = R * std::sin(phi); + double z = Z; + + return {x, y, z}; +} + +SourceSite TokamakSource::sample(uint64_t* seed) const +{ + SourceSite site; + site.particle = ParticleType::neutron(); + site.wgt = 1.0; + site.delayed_group = 0; + + // 1. Sample r/a from radial CDF + double r_norm = sample_r_over_a(seed); + double r = r_norm * minor_radius_; + + // 2. Sample poloidal angle from conditional distribution P(alpha|r) + double alpha = sample_poloidal_angle(r_norm, seed); + + // 3. Sample toroidal angle uniformly in [phi_start, phi_start + phi_extent] + double phi = phi_start_ + phi_extent_ * prn(seed); + + // 4. Convert to Cartesian coordinates + site.r = flux_to_cartesian(r, alpha, phi); + + // 4a. Apply vertical shift if non-zero + if (vertical_shift_ != 0.0) { + site.r.z += vertical_shift_; + } + + // 5. Sample isotropic direction + site.u = angle_->sample(seed).first; + + // 6. Sample energy from distribution(s), applying the importance weight so + // that biased distributions are handled correctly + auto [E, E_wgt] = sample_energy(r_norm, seed); + site.E = E; + + // 7. Sample particle creation time + auto [time, time_wgt] = time_->sample(seed); + site.time = time; + + site.wgt *= E_wgt * time_wgt; + + return site; +} + //============================================================================== // Non-member functions //============================================================================== @@ -662,6 +1213,14 @@ SourceSite sample_external_source(uint64_t* seed) void free_memory_source() { model::external_sources.clear(); + model::adjoint_sources.clear(); + reset_source_rejection_counters(); +} + +void reset_source_rejection_counters() +{ + source_n_accept = 0; + source_n_reject = 0; } //============================================================================== @@ -682,8 +1241,15 @@ extern "C" int openmc_sample_external_source( } auto sites_array = static_cast(sites); + + // Derive independent per-particle seeds from the base seed so that + // each iteration has its own RNG state for thread-safe parallel sampling. + uint64_t base_seed = *seed; + +#pragma omp parallel for schedule(static) for (size_t i = 0; i < n; ++i) { - sites_array[i] = sample_external_source(seed); + uint64_t particle_seed = init_seed(base_seed + i, STREAM_SOURCE); + sites_array[i] = sample_external_source(&particle_seed); } return 0; } diff --git a/src/state_point.cpp b/src/state_point.cpp index 47296da3a..02c68bf60 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -4,8 +4,7 @@ #include // for int64_t #include -#include "xtensor/xbuilder.hpp" // for empty_like -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" @@ -22,6 +21,8 @@ #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" #include "openmc/output.h" +#include "openmc/particle_type.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/tallies/derivative.h" @@ -46,9 +47,15 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // Determine width for zero padding int w = std::to_string(settings::n_max_batches).size(); + // Tag statepoints written during the forward solve of an adjoint run + const char* forward = + (FlatSourceDomain::solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) + ? "forward." + : ""; + // Set filename for state point - filename_ = fmt::format("{0}statepoint.{1:0{2}}.h5", settings::path_output, - simulation::current_batch, w); + filename_ = fmt::format("{0}statepoint.{3}{1:0{2}}.h5", + settings::path_output, simulation::current_batch, w, forward); } // If a file name was specified, ensure it has .h5 file extension @@ -276,8 +283,8 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) std::string name = "tally " + std::to_string(tally->id_); hid_t tally_group = open_group(tallies_group, name.c_str()); auto& results = tally->results_; - write_tally_results(tally_group, results.shape()[0], - results.shape()[1], results.shape()[2], results.data()); + write_tally_results(tally_group, results.shape(0), results.shape(1), + results.shape(2), results.data()); close_group(tally_group); } } else { @@ -516,8 +523,8 @@ extern "C" int openmc_statepoint_load(const char* filename) tally->writable_ = false; } else { auto& results = tally->results_; - read_tally_results(tally_group, results.shape()[0], - results.shape()[1], results.shape()[2], results.data()); + read_tally_results(tally_group, results.shape(0), results.shape(1), + results.shape(2), results.data()); read_dataset(tally_group, "n_realizations", tally->n_realizations_); close_group(tally_group); @@ -554,7 +561,7 @@ extern "C" int openmc_statepoint_load(const char* filename) return 0; } -hid_t h5banktype() +hid_t h5banktype(bool memory) { // Create compound type for position hid_t postype = H5Tcreate(H5T_COMPOUND, sizeof(struct Position)); @@ -569,7 +576,10 @@ hid_t h5banktype() // - openmc/statepoint.py // - docs/source/io_formats/statepoint.rst // - docs/source/io_formats/source.rst - hid_t banktype = H5Tcreate(H5T_COMPOUND, sizeof(struct SourceSite)); + auto n = sizeof(SourceSite); + if (!memory) + n = 2 * sizeof(struct Position) + 3 * sizeof(double) + 3 * sizeof(int); + hid_t banktype = H5Tcreate(H5T_COMPOUND, n); H5Tinsert(banktype, "r", HOFFSET(SourceSite, r), postype); H5Tinsert(banktype, "u", HOFFSET(SourceSite, u), postype); H5Tinsert(banktype, "E", HOFFSET(SourceSite, E), H5T_NATIVE_DOUBLE); @@ -589,8 +599,16 @@ void write_source_point(std::string filename, span source_bank, const vector& bank_index, bool use_mcpl) { std::string ext = use_mcpl ? "mcpl" : "h5"; + + int total_surf_particles = source_bank.size(); +#ifdef OPENMC_MPI + int num_particles = source_bank.size(); + MPI_Allreduce( + &num_particles, &total_surf_particles, 1, MPI_INT, MPI_SUM, mpi::intracomm); +#endif + write_message("Creating source file {}.{} with {} particles ...", filename, - ext, source_bank.size(), 5); + ext, total_surf_particles, 5); // Dispatch to appropriate function based on file type if (use_mcpl) { @@ -629,6 +647,7 @@ void write_h5_source_point(const char* filename, span source_bank, if (mpi::master || parallel) { file_id = file_open(filename_.c_str(), 'w', true); write_attribute(file_id, "filetype", "source"); + write_attribute(file_id, "version", VERSION_STATEPOINT); } // Get pointer to source bank and write to file @@ -641,17 +660,19 @@ void write_h5_source_point(const char* filename, span source_bank, void write_source_bank(hid_t group_id, span source_bank, const vector& bank_index) { - hid_t banktype = h5banktype(); + hid_t membanktype = h5banktype(true); + hid_t filebanktype = h5banktype(false); #ifdef OPENMC_MPI - write_bank_dataset("source_bank", group_id, source_bank, bank_index, banktype, - mpi::source_site); + write_bank_dataset("source_bank", group_id, source_bank, bank_index, + membanktype, filebanktype, mpi::source_site); #else - write_bank_dataset( - "source_bank", group_id, source_bank, bank_index, banktype); + write_bank_dataset("source_bank", group_id, source_bank, bank_index, + membanktype, filebanktype); #endif - H5Tclose(banktype); + H5Tclose(membanktype); + H5Tclose(filebanktype); } // Determine member names of a compound HDF5 datatype @@ -672,7 +693,17 @@ std::string dtype_member_names(hid_t dtype_id) void read_source_bank( hid_t group_id, vector& sites, bool distribute) { - hid_t banktype = h5banktype(); + bool legacy_particle_codes = true; + if (attribute_exists(group_id, "version")) { + array version; + read_attribute(group_id, "version", version); + if (version[0] > VERSION_STATEPOINT[0] || + (version[0] == VERSION_STATEPOINT[0] && version[1] >= 2)) { + legacy_particle_codes = false; + } + } + + hid_t banktype = h5banktype(true); // Open the dataset hid_t dset = H5Dopen(group_id, "source_bank", H5P_DEFAULT); @@ -733,6 +764,12 @@ void read_source_bank( H5Sclose(memspace); H5Dclose(dset); H5Tclose(banktype); + + if (legacy_particle_codes) { + for (auto& site : sites) { + site.particle = legacy_particle_index_to_type(site.particle.pdg_number()); + } + } } void write_unstructured_mesh_results() @@ -804,7 +841,7 @@ void write_unstructured_mesh_results() // construct result vectors vector mean_vec(umesh->n_bins()), std_dev_vec(umesh->n_bins()); - for (int j = 0; j < tally->results_.shape()[0]; j++) { + for (int j = 0; j < tally->results_.shape(0); j++) { // get the volume for this bin double volume = umesh->volume(j); // compute the mean @@ -866,7 +903,7 @@ void write_tally_results_nr(hid_t file_id) #ifdef OPENMC_MPI // Reduce global tallies - xt::xtensor gt_reduced = xt::empty_like(gt); + tensor::Tensor gt_reduced({N_GLOBAL_TALLIES, 3}); MPI_Reduce(gt.data(), gt_reduced.data(), gt.size(), MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); @@ -895,13 +932,18 @@ void write_tally_results_nr(hid_t file_id) write_attribute(file_id, "tallies_present", 1); } - // Get view of accumulated tally values - auto values_view = xt::view(t->results_, xt::all(), xt::all(), - xt::range(static_cast(TallyResult::SUM), - static_cast(TallyResult::SUM_SQ) + 1)); - - // Make copy of tally values in contiguous array - xt::xtensor values = values_view; + // Copy the SUM and SUM_SQ columns from the tally results into a + // contiguous array for MPI reduction + const int r_start = static_cast(TallyResult::SUM); + const int r_end = static_cast(TallyResult::SUM_SQ) + 1; + const size_t r_count = r_end - r_start; + const size_t ni = t->results_.shape(0); + const size_t nj = t->results_.shape(1); + tensor::Tensor values({ni, nj, r_count}); + for (size_t i = 0; i < ni; i++) + for (size_t j = 0; j < nj; j++) + for (size_t r = 0; r < r_count; r++) + values(i, j, r) = t->results_(i, j, r_start + r); if (mpi::master) { // Open group for tally @@ -915,19 +957,22 @@ void write_tally_results_nr(hid_t file_id) MPI_SUM, 0, mpi::intracomm); #endif - // At the end of the simulation, store the results back in the - // regular TallyResults array + // At the end of the simulation, store the reduced results back + // into the tally results array if (simulation::current_batch == settings::n_max_batches || simulation::satisfy_triggers) { - values_view = values; + for (size_t i = 0; i < ni; i++) + for (size_t j = 0; j < nj; j++) + for (size_t r = 0; r < r_count; r++) + t->results_(i, j, r_start + r) = values(i, j, r); } - // Put in temporary tally result - xt::xtensor results_copy = xt::zeros_like(t->results_); - auto copy_view = xt::view(results_copy, xt::all(), xt::all(), - xt::range(static_cast(TallyResult::SUM), - static_cast(TallyResult::SUM_SQ) + 1)); - copy_view = values; + // Put reduced values into a full-sized copy for writing to HDF5 + tensor::Tensor results_copy = tensor::zeros_like(t->results_); + for (size_t i = 0; i < ni; i++) + for (size_t j = 0; j < nj; j++) + for (size_t r = 0; r < r_count; r++) + results_copy(i, j, r_start + r) = values(i, j, r); // Write reduced tally results to file auto shape = results_copy.shape(); diff --git a/src/surface.cpp b/src/surface.cpp index ea19514a3..81b756dea 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -9,6 +9,7 @@ #include #include "openmc/array.h" +#include "openmc/cell.h" #include "openmc/container_util.h" #include "openmc/error.h" #include "openmc/external/quartic_solver.h" @@ -251,9 +252,9 @@ void SurfaceXPlane::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceXPlane::bounding_box(bool pos_side) const { if (pos_side) { - return {x0_, INFTY, -INFTY, INFTY, -INFTY, INFTY}; + return {{x0_, -INFTY, -INFTY}, {INFTY, INFTY, INFTY}}; } else { - return {-INFTY, x0_, -INFTY, INFTY, -INFTY, INFTY}; + return {{-INFTY, -INFTY, -INFTY}, {x0_, INFTY, INFTY}}; } } @@ -291,9 +292,9 @@ void SurfaceYPlane::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const { if (pos_side) { - return {-INFTY, INFTY, y0_, INFTY, -INFTY, INFTY}; + return {{-INFTY, y0_, -INFTY}, {INFTY, INFTY, INFTY}}; } else { - return {-INFTY, INFTY, -INFTY, y0_, -INFTY, INFTY}; + return {{-INFTY, -INFTY, -INFTY}, {INFTY, y0_, INFTY}}; } } @@ -331,9 +332,9 @@ void SurfaceZPlane::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceZPlane::bounding_box(bool pos_side) const { if (pos_side) { - return {-INFTY, INFTY, -INFTY, INFTY, z0_, INFTY}; + return {{-INFTY, -INFTY, z0_}, {INFTY, INFTY, INFTY}}; } else { - return {-INFTY, INFTY, -INFTY, INFTY, -INFTY, z0_}; + return {{-INFTY, -INFTY, -INFTY}, {INFTY, INFTY, z0_}}; } } @@ -492,8 +493,8 @@ void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const { if (!pos_side) { - return {-INFTY, INFTY, y0_ - radius_, y0_ + radius_, z0_ - radius_, - z0_ + radius_}; + return {{-INFTY, y0_ - radius_, z0_ - radius_}, + {INFTY, y0_ + radius_, z0_ + radius_}}; } else { return {}; } @@ -535,8 +536,8 @@ void SurfaceYCylinder::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const { if (!pos_side) { - return {x0_ - radius_, x0_ + radius_, -INFTY, INFTY, z0_ - radius_, - z0_ + radius_}; + return {{x0_ - radius_, -INFTY, z0_ - radius_}, + {x0_ + radius_, INFTY, z0_ + radius_}}; } else { return {}; } @@ -579,8 +580,8 @@ void SurfaceZCylinder::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const { if (!pos_side) { - return {x0_ - radius_, x0_ + radius_, y0_ - radius_, y0_ + radius_, -INFTY, - INFTY}; + return {{x0_ - radius_, y0_ - radius_, -INFTY}, + {x0_ + radius_, y0_ + radius_, INFTY}}; } else { return {}; } @@ -657,8 +658,8 @@ void SurfaceSphere::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceSphere::bounding_box(bool pos_side) const { if (!pos_side) { - return {x0_ - radius_, x0_ + radius_, y0_ - radius_, y0_ + radius_, - z0_ - radius_, z0_ + radius_}; + return {{x0_ - radius_, y0_ - radius_, z0_ - radius_}, + {x0_ + radius_, y0_ + radius_, z0_ + radius_}}; } else { return {}; } @@ -1169,7 +1170,10 @@ Direction SurfaceZTorus::normal(Position r) const //============================================================================== -void read_surfaces(pugi::xml_node node) +void read_surfaces(pugi::xml_node node, + std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map) { // Count the number of surfaces int n_surfaces = 0; @@ -1180,8 +1184,6 @@ void read_surfaces(pugi::xml_node node) // Loop over XML surface elements and populate the array. Keep track of // periodic surfaces and their albedos. model::surfaces.reserve(n_surfaces); - std::set> periodic_pairs; - std::unordered_map albedo_map; { pugi::xml_node surf_node; int i_surf; @@ -1244,6 +1246,7 @@ void read_surfaces(pugi::xml_node node) if (check_for_node(surf_node, "boundary")) { std::string surf_bc = get_node_value(surf_node, "boundary", true, true); if (surf_bc == "periodic") { + periodic_sense_map[model::surfaces.back()->id_] = 0; // Check for surface albedo. Skip sanity check as it is already done // in the Surface class's constructor. if (check_for_node(surf_node, "albedo")) { @@ -1275,6 +1278,28 @@ void read_surfaces(pugi::xml_node node) fmt::format("Two or more surfaces use the same unique ID: {}", id)); } } +} + +void prepare_boundary_conditions(std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map) +{ + // Fill the senses map for periodic surfaces + auto n_periodic = periodic_sense_map.size(); + for (const auto& cell : model::cells) { + if (n_periodic == 0) + break; // Early exit once all periodic surfaces found + + for (auto s : cell->surfaces()) { + auto surf_idx = std::abs(s) - 1; + auto id = model::surfaces[surf_idx]->id_; + + if (periodic_sense_map.count(id)) { + periodic_sense_map[id] = std::copysign(1, s); + --n_periodic; + } + } + } // Resolve unpaired periodic surfaces. A lambda function is used with // std::find_if to identify the unpaired surfaces. @@ -1332,10 +1357,50 @@ void read_surfaces(pugi::xml_node node) // condition. Otherwise, it is a rotational periodic BC. if (std::abs(1.0 - dot_prod) < FP_PRECISION) { surf1.bc_ = make_unique(i_surf, j_surf); - surf2.bc_ = make_unique(i_surf, j_surf); + surf2.bc_ = make_unique(j_surf, i_surf); } else { - surf1.bc_ = make_unique(i_surf, j_surf); - surf2.bc_ = make_unique(i_surf, j_surf); + // check that both normals have at least one 0 component + if (std::abs(norm1.x) > FP_PRECISION && + std::abs(norm1.y) > FP_PRECISION && + std::abs(norm1.z) > FP_PRECISION) { + fatal_error(fmt::format( + "The normal ({}) of the periodic surface ({}) does not contain any " + "component with a zero value. A RotationalPeriodicBC requires one " + "component which is zero for both plane normals.", + norm1, i_surf)); + } + if (std::abs(norm2.x) > FP_PRECISION && + std::abs(norm2.y) > FP_PRECISION && + std::abs(norm2.z) > FP_PRECISION) { + fatal_error(fmt::format( + "The normal ({}) of the periodic surface ({}) does not contain any " + "component with a zero value. A RotationalPeriodicBC requires one " + "component which is zero for both plane normals.", + norm2, j_surf)); + } + // find common zero component, which indicates the periodic axis + RotationalPeriodicBC::PeriodicAxis axis; + if (std::abs(norm1.x) <= FP_PRECISION && + std::abs(norm2.x) <= FP_PRECISION) { + axis = RotationalPeriodicBC::PeriodicAxis::x; + } else if (std::abs(norm1.y) <= FP_PRECISION && + std::abs(norm2.y) <= FP_PRECISION) { + axis = RotationalPeriodicBC::PeriodicAxis::y; + } else if (std::abs(norm1.z) <= FP_PRECISION && + std::abs(norm2.z) <= FP_PRECISION) { + axis = RotationalPeriodicBC::PeriodicAxis::z; + } else { + fatal_error(fmt::format( + "There is no component which is 0.0 in both normal vectors. This " + "indicates that the two planes are not periodic about the X, Y, or Z " + "axis, which is not supported.")); + } + auto i_sign = periodic_sense_map[periodic_pair.first]; + auto j_sign = periodic_sense_map[periodic_pair.second]; + surf1.bc_ = make_unique( + i_sign * (i_surf + 1), j_sign * (j_surf + 1), axis); + surf2.bc_ = make_unique( + j_sign * (j_surf + 1), i_sign * (i_surf + 1), axis); } // If albedo data is present in albedo map, set the boundary albedo. diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 79817981d..badb91077 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -31,7 +31,9 @@ #include "openmc/tallies/filter_musurface.h" #include "openmc/tallies/filter_parent_nuclide.h" #include "openmc/tallies/filter_particle.h" +#include "openmc/tallies/filter_particle_production.h" #include "openmc/tallies/filter_polar.h" +#include "openmc/tallies/filter_reaction.h" #include "openmc/tallies/filter_sph_harm.h" #include "openmc/tallies/filter_sptl_legendre.h" #include "openmc/tallies/filter_surface.h" @@ -146,8 +148,12 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "particle") { return Filter::create(id); + } else if (type == "particleproduction") { + return Filter::create(id); } else if (type == "polar") { return Filter::create(id); + } else if (type == "reaction") { + return Filter::create(id); } else if (type == "surface") { return Filter::create(id); } else if (type == "spatiallegendre") { diff --git a/src/tallies/filter_cell_instance.cpp b/src/tallies/filter_cell_instance.cpp index 316a758d1..928bfb6c5 100644 --- a/src/tallies/filter_cell_instance.cpp +++ b/src/tallies/filter_cell_instance.cpp @@ -9,6 +9,7 @@ #include "openmc/cell.h" #include "openmc/error.h" #include "openmc/geometry.h" +#include "openmc/tensor.h" #include "openmc/xml_interface.h" namespace openmc { @@ -108,7 +109,7 @@ void CellInstanceFilter::to_statepoint(hid_t filter_group) const { Filter::to_statepoint(filter_group); size_t n = cell_instances_.size(); - xt::xtensor data({n, 2}); + tensor::Tensor data({n, 2}); for (int64_t i = 0; i < n; ++i) { const auto& x = cell_instances_[i]; data(i, 0) = model::cells[x.index_cell]->id_; diff --git a/src/tallies/filter_delayedgroup.cpp b/src/tallies/filter_delayedgroup.cpp index 01e39e554..46adf529b 100644 --- a/src/tallies/filter_delayedgroup.cpp +++ b/src/tallies/filter_delayedgroup.cpp @@ -27,7 +27,7 @@ void DelayedGroupFilter::set_groups(span groups) } else if (group > MAX_DELAYED_GROUPS) { throw std::invalid_argument { "Encountered delayedgroup bin with index " + std::to_string(group) + - " which is greater than MAX_DELATED_GROUPS (" + + " which is greater than MAX_DELAYED_GROUPS (" + std::to_string(MAX_DELAYED_GROUPS) + ")"}; } groups_.push_back(group); @@ -39,6 +39,10 @@ void DelayedGroupFilter::set_groups(span groups) void DelayedGroupFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { + // Note that the bin is set to zero here, but bins outside zero are + // tallied to regardless. This is because that logic has to be handled + // in the scoring code instead where looping over the delayed + // group takes place (tally_scoring.cpp). match.bins_.push_back(0); match.weights_.push_back(1.0); } diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 0b954cce3..566b5710a 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -60,13 +60,8 @@ void EnergyFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { if (p.g() != C_NONE && matches_transport_groups_) { - if (estimator == TallyEstimator::TRACKLENGTH) { - match.bins_.push_back(data::mg.num_energy_groups_ - p.g() - 1); - } else { - match.bins_.push_back(data::mg.num_energy_groups_ - p.g_last() - 1); - } + match.bins_.push_back(data::mg.num_energy_groups_ - p.g_last() - 1); match.weights_.push_back(1.0); - } else { // Get the pre-collision energy of the particle. auto E = p.E_last(); diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index 4edfbec4b..a0698992d 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -6,6 +6,7 @@ #include "openmc/constants.h" #include "openmc/error.h" #include "openmc/mesh.h" +#include "openmc/position.h" #include "openmc/xml_interface.h" namespace openmc { @@ -30,6 +31,10 @@ void MeshFilter::from_xml(pugi::xml_node node) if (check_for_node(node, "translation")) { set_translation(get_node_array(node, "translation")); } + // Read the rotation transform. + if (check_for_node(node, "rotation")) { + set_rotation(get_node_array(node, "rotation")); + } } void MeshFilter::get_all_bins( @@ -45,6 +50,12 @@ void MeshFilter::get_all_bins( last_r -= translation(); r -= translation(); } + // apply rotation if present + if (!rotation_.empty()) { + last_r = last_r.rotate(rotation_); + r = r.rotate(rotation_); + u = u.rotate(rotation_); + } if (estimator != TallyEstimator::TRACKLENGTH) { auto bin = model::meshes[mesh_]->get_bin(r); @@ -65,6 +76,9 @@ void MeshFilter::to_statepoint(hid_t filter_group) const if (translated_) { write_dataset(filter_group, "translation", translation_); } + if (rotated_) { + write_dataset(filter_group, "rotation", rotation_); + } } std::string MeshFilter::text_label(int bin) const @@ -93,6 +107,40 @@ void MeshFilter::set_translation(const double translation[3]) this->set_translation({translation[0], translation[1], translation[2]}); } +void MeshFilter::set_rotation(const vector& rot) +{ + rotated_ = true; + + // Compute and store the inverse rotation matrix for the angles given. + rotation_.clear(); + rotation_.reserve(rot.size() == 9 ? 9 : 12); + if (rot.size() == 3) { + double phi = -rot[0] * PI / 180.0; + double theta = -rot[1] * PI / 180.0; + double psi = -rot[2] * PI / 180.0; + rotation_.push_back(std::cos(theta) * std::cos(psi)); + rotation_.push_back(-std::cos(phi) * std::sin(psi) + + std::sin(phi) * std::sin(theta) * std::cos(psi)); + rotation_.push_back(std::sin(phi) * std::sin(psi) + + std::cos(phi) * std::sin(theta) * std::cos(psi)); + rotation_.push_back(std::cos(theta) * std::sin(psi)); + rotation_.push_back(std::cos(phi) * std::cos(psi) + + std::sin(phi) * std::sin(theta) * std::sin(psi)); + rotation_.push_back(-std::sin(phi) * std::cos(psi) + + std::cos(phi) * std::sin(theta) * std::sin(psi)); + rotation_.push_back(-std::sin(theta)); + rotation_.push_back(std::sin(phi) * std::cos(theta)); + rotation_.push_back(std::cos(phi) * std::cos(theta)); + + // When user specifies angles, write them at end of vector + rotation_.push_back(rot[0]); + rotation_.push_back(rot[1]); + rotation_.push_back(rot[2]); + } else { + std::copy(rot.begin(), rot.end(), std::back_inserter(rotation_)); + } +} + //============================================================================== // C-API functions //============================================================================== @@ -201,4 +249,48 @@ extern "C" int openmc_mesh_filter_set_translation( return 0; } +//! Return the rotation matrix of a mesh filter +extern "C" int openmc_mesh_filter_get_rotation( + int32_t index, double rot[], size_t* n) +{ + // Make sure this is a valid index to an allocated filter + if (int err = verify_filter(index)) + return err; + + // Check the filter type + const auto& filter = model::tally_filters[index]; + if (filter->type() != FilterType::MESH) { + set_errmsg("Tried to get a rotation from a non-mesh filter."); + return OPENMC_E_INVALID_TYPE; + } + // Get rotation from the mesh filter and set value + auto mesh_filter = dynamic_cast(filter.get()); + *n = mesh_filter->rotation().size(); + std::memcpy(rot, mesh_filter->rotation().data(), + *n * sizeof(mesh_filter->rotation()[0])); + return 0; +} + +//! Set the flattened rotation matrix of a mesh filter +extern "C" int openmc_mesh_filter_set_rotation( + int32_t index, const double rot[], size_t rot_len) +{ + // Make sure this is a valid index to an allocated filter + if (int err = verify_filter(index)) + return err; + + const auto& filter = model::tally_filters[index]; + // Check the filter type + if (filter->type() != FilterType::MESH) { + set_errmsg("Tried to set a rotation from a non-mesh filter."); + return OPENMC_E_INVALID_TYPE; + } + + // Get a pointer to the filter and downcast + auto mesh_filter = dynamic_cast(filter.get()); + std::vector vec_rot(rot, rot + rot_len); + mesh_filter->set_rotation(vec_rot); + return 0; +} + } // namespace openmc diff --git a/src/tallies/filter_meshmaterial.cpp b/src/tallies/filter_meshmaterial.cpp index 6e1f30380..b45bb4164 100644 --- a/src/tallies/filter_meshmaterial.cpp +++ b/src/tallies/filter_meshmaterial.cpp @@ -1,5 +1,6 @@ #include "openmc/tallies/filter_meshmaterial.h" +#include #include // for move #include @@ -10,6 +11,7 @@ #include "openmc/error.h" #include "openmc/material.h" #include "openmc/mesh.h" +#include "openmc/tensor.h" #include "openmc/xml_interface.h" namespace openmc { @@ -161,7 +163,7 @@ void MeshMaterialFilter::to_statepoint(hid_t filter_group) const write_dataset(filter_group, "mesh", model::meshes[mesh_]->id_); size_t n = bins_.size(); - xt::xtensor data({n, 2}); + tensor::Tensor data({n, 2}); for (int64_t i = 0; i < n; ++i) { const auto& x = bins_[i]; data(i, 0) = x.index_element; diff --git a/src/tallies/filter_particle.cpp b/src/tallies/filter_particle.cpp index eef1d1e63..031068f3a 100644 --- a/src/tallies/filter_particle.cpp +++ b/src/tallies/filter_particle.cpp @@ -13,7 +13,7 @@ void ParticleFilter::from_xml(pugi::xml_node node) // Convert to vector of ParticleType vector types; for (auto& p : particles) { - types.push_back(str_to_particle_type(p)); + types.emplace_back(p); } this->set_particles(types); } @@ -47,7 +47,7 @@ void ParticleFilter::to_statepoint(hid_t filter_group) const Filter::to_statepoint(filter_group); vector particles; for (auto p : particles_) { - particles.push_back(particle_type_to_str(p)); + particles.push_back(p.str()); } write_dataset(filter_group, "bins", particles); } @@ -55,10 +55,10 @@ void ParticleFilter::to_statepoint(hid_t filter_group) const std::string ParticleFilter::text_label(int bin) const { const auto& p = particles_.at(bin); - return fmt::format("Particle: {}", particle_type_to_str(p)); + return fmt::format("Particle: {}", p.str()); } -extern "C" int openmc_particle_filter_get_bins(int32_t idx, int bins[]) +extern "C" int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[]) { if (int err = verify_filter(idx)) return err; @@ -68,7 +68,7 @@ extern "C" int openmc_particle_filter_get_bins(int32_t idx, int bins[]) if (pf) { const auto& particles = pf->particles(); for (int i = 0; i < particles.size(); i++) { - bins[i] = static_cast(particles[i]); + bins[i] = particles[i].pdg_number(); } } else { set_errmsg("The filter at the specified index is not a ParticleFilter"); diff --git a/src/tallies/filter_particle_production.cpp b/src/tallies/filter_particle_production.cpp new file mode 100644 index 000000000..899809679 --- /dev/null +++ b/src/tallies/filter_particle_production.cpp @@ -0,0 +1,108 @@ +#include "openmc/tallies/filter_particle_production.h" + +#include + +#include "openmc/search.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// ParticleProductionFilter implementation +//============================================================================== + +void ParticleProductionFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + int start_idx = p.secondary_bank_index(); + int end_idx = start_idx + p.n_secondaries(); + + // Loop over secondary bank entries + for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) { + const auto& site = p.local_secondary_bank(bank_idx); + + // Find which particle-type slot this secondary belongs to + auto it = type_to_index_.find(site.particle.pdg_number()); + if (it == type_to_index_.end()) + continue; + + int particle_idx = it->second; + if (energy_bins_.empty()) { + // No energy binning, just particle type + match.bins_.push_back(particle_idx); + match.weights_.push_back(site.wgt); + } else { + // Bin the energy + if (site.E >= energy_bins_.front() && site.E <= energy_bins_.back()) { + int n_energies = static_cast(energy_bins_.size()) - 1; + auto energy_idx = + lower_bound_index(energy_bins_.begin(), energy_bins_.end(), site.E); + match.bins_.push_back(particle_idx * n_energies + energy_idx); + match.weights_.push_back(site.wgt); + } + } + } +} + +std::string ParticleProductionFilter::text_label(int bin) const +{ + if (energy_bins_.empty()) { + return fmt::format("Secondary {}", secondary_types_.at(bin).str()); + } else { + int n_energies = static_cast(energy_bins_.size()) - 1; + int particle_idx = bin / n_energies; + int energy_idx = bin % n_energies; + return fmt::format("Secondary {}, Energy [{}, {})", + secondary_types_.at(particle_idx).str(), energy_bins_.at(energy_idx), + energy_bins_.at(energy_idx + 1)); + } +} + +void ParticleProductionFilter::from_xml(pugi::xml_node node) +{ + // Read energy bins if present (optional) + if (check_for_node(node, "energies")) { + auto bins = get_node_array(node, "energies"); + for (int64_t i = 1; i < bins.size(); ++i) { + if (bins[i] <= bins[i - 1]) { + throw std::runtime_error { + "Energy bins must be monotonically increasing."}; + } + } + energy_bins_.assign(bins.begin(), bins.end()); + } + + // Read particle types (required) + auto names = get_node_array(node, "particles"); + for (const auto& name : names) { + int idx = secondary_types_.size(); + secondary_types_.emplace_back(name); + type_to_index_[secondary_types_.back().pdg_number()] = idx; + } + + // Compute total bins + if (energy_bins_.empty()) { + n_bins_ = secondary_types_.size(); + } else { + n_bins_ = secondary_types_.size() * (energy_bins_.size() - 1); + } +} + +void ParticleProductionFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + + // Write energy bins if present + if (!energy_bins_.empty()) { + write_dataset(filter_group, "energies", energy_bins_); + } + + // Write particle types + vector names; + for (const auto& pt : secondary_types_) { + names.push_back(pt.str()); + } + write_dataset(filter_group, "particles", names); +} + +} // namespace openmc diff --git a/src/tallies/filter_reaction.cpp b/src/tallies/filter_reaction.cpp new file mode 100644 index 000000000..8ee9f3ce8 --- /dev/null +++ b/src/tallies/filter_reaction.cpp @@ -0,0 +1,79 @@ +#include "openmc/tallies/filter_reaction.h" + +#include + +#include "openmc/capi.h" +#include "openmc/endf.h" +#include "openmc/error.h" +#include "openmc/reaction.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// ReactionFilter implementation +//============================================================================== + +void ReactionFilter::from_xml(pugi::xml_node node) +{ + // Read bins as reaction name strings + auto bins_str = get_node_array(node, "bins"); + + // Convert reaction names to MT numbers + vector bins_mt; + bins_mt.reserve(bins_str.size()); + for (const auto& name : bins_str) { + bins_mt.push_back(reaction_mt(name)); + } + + this->set_bins(bins_mt); +} + +void ReactionFilter::set_bins(span bins) +{ + // Clear existing bins + bins_.clear(); + bins_.reserve(bins.size()); + + // Copy bins and build lookup map + for (int64_t i = 0; i < bins.size(); ++i) { + bins_.push_back(bins[i]); + } + + n_bins_ = bins_.size(); +} + +void ReactionFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get the event MT number from the particle + int event_mt = p.event_mt(); + + // Check each bin, considering summation rules + for (int64_t i = 0; i < bins_.size(); ++i) { + if (mt_matches(event_mt, bins_[i])) { + match.bins_.push_back(i); + match.weights_.push_back(1.0); + } + } +} + +void ReactionFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + + // Write bins as reaction name strings for human readability + vector names; + names.reserve(bins_.size()); + for (auto mt : bins_) { + names.push_back(reaction_name(mt)); + } + write_dataset(filter_group, "bins", names); +} + +std::string ReactionFilter::text_label(int bin) const +{ + return reaction_name(bins_[bin]); +} + +} // namespace openmc diff --git a/src/tallies/filter_surface.cpp b/src/tallies/filter_surface.cpp index 82f3d7178..4a636f1aa 100644 --- a/src/tallies/filter_surface.cpp +++ b/src/tallies/filter_surface.cpp @@ -52,11 +52,7 @@ void SurfaceFilter::get_all_bins( auto search = map_.find(p.surface_index()); if (search != map_.end()) { match.bins_.push_back(search->second); - if (p.surface() < 0) { - match.weights_.push_back(-1.0); - } else { - match.weights_.push_back(1.0); - } + match.weights_.push_back(1.0); } } diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 9daeb1d69..085633247 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -2,6 +2,7 @@ #include "openmc/array.h" #include "openmc/capi.h" +#include "openmc/cell.h" #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/error.h" @@ -35,9 +36,7 @@ #include "openmc/tallies/filter_time.h" #include "openmc/xml_interface.h" -#include "xtensor/xadapt.hpp" -#include "xtensor/xbuilder.hpp" // for empty_like -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include // for max, set_union @@ -64,12 +63,12 @@ vector active_collision_tallies; vector active_meshsurf_tallies; vector active_surface_tallies; vector active_pulse_height_tallies; -vector pulse_height_cells; +vector pulse_height_cells; vector time_grid; } // namespace model namespace simulation { -xt::xtensor_fixed> global_tallies; +tensor::StaticTensor2D global_tallies; int32_t n_realizations {0}; } // namespace simulation @@ -171,6 +170,12 @@ Tally::Tally(pugi::xml_node node) filt_type == FilterType::ZERNIKE || filt_type == FilterType::ZERNIKE_RADIAL) { estimator_ = TallyEstimator::COLLISION; + } else if (filt_type == FilterType::PARTICLE_PRODUCTION) { + estimator_ = TallyEstimator::ANALOG; + } else if (filt_type == FilterType::REACTION) { + if (estimator_ == TallyEstimator::TRACKLENGTH) { + estimator_ = TallyEstimator::COLLISION; + } } } @@ -215,7 +220,7 @@ Tally::Tally(pugi::xml_node node) "number of inactive cycles."); } settings::ifp_on = true; - } else { + } else if (settings::run_mode == RunMode::FIXED_SOURCE) { fatal_error( "Iterated Fission Probability can only be used in an eigenvalue " "calculation."); @@ -289,12 +294,12 @@ Tally::Tally(pugi::xml_node node) const auto& f = model::tally_filters[particle_filter_index].get(); auto pf = dynamic_cast(f); for (auto p : pf->particles()) { - if (p != ParticleType::neutron) { + if (!p.is_neutron()) { warning(fmt::format( "Particle filter other than NEUTRON used with " "photon transport turned off. All tallies for particle type {}" " will have no scores", - static_cast(p))); + p.str())); } } } @@ -507,7 +512,7 @@ void Tally::set_strides() // longest stride. auto n = filters_.size(); strides_.resize(n, 0); - int stride = 1; + int64_t stride = 1; for (int i = n - 1; i >= 0; --i) { strides_[i] = stride; stride *= model::tally_filters[filters_[i]]->n_bins(); @@ -535,6 +540,8 @@ void Tally::set_scores(const vector& scores) bool legendre_present = false; bool cell_present = false; bool cellfrom_present = false; + bool material_present = false; + bool materialfrom_present = false; bool surface_present = false; bool meshsurface_present = false; bool non_cell_energy_present = false; @@ -551,12 +558,21 @@ void Tally::set_scores(const vector& scores) cellfrom_present = true; } else if (filt->type() == FilterType::CELL) { cell_present = true; + } else if (filt->type() == FilterType::MATERIALFROM) { + materialfrom_present = true; + } else if (filt->type() == FilterType::MATERIAL) { + material_present = true; } else if (filt->type() == FilterType::SURFACE) { surface_present = true; } else if (filt->type() == FilterType::MESH_SURFACE) { meshsurface_present = true; } } + bool surface_types_present = + (surface_present || cellfrom_present || materialfrom_present); + bool non_meshsurface_types_present = + (surface_present || cell_present || cellfrom_present || material_present || + materialfrom_present); // Iterate over the given scores. for (auto score_str : scores) { @@ -569,7 +585,7 @@ void Tally::set_scores(const vector& scores) } // Determine integer code for score - int score = reaction_type(score_str); + int score = reaction_tally_mt(score_str); switch (score) { case SCORE_FLUX: @@ -578,6 +594,12 @@ void Tally::set_scores(const vector& scores) fatal_error("Cannot tally flux for an individual nuclide."); if (energyout_present) fatal_error("Cannot tally flux with an outgoing energy filter."); + if (surface_types_present) { + if (meshsurface_present) + fatal_error("OpenMC does not support mesh surface fluxes yet"); + type_ = TallyType::SURFACE; + estimator_ = TallyEstimator::ANALOG; + } break; case SCORE_TOTAL: @@ -610,47 +632,56 @@ void Tally::set_scores(const vector& scores) case SCORE_CURRENT: // Check which type of current is desired: mesh or surface currents. - if (surface_present || cell_present || cellfrom_present) { - if (meshsurface_present) + if (meshsurface_present) { + if (non_meshsurface_types_present) fatal_error("Cannot tally mesh surface currents in the same tally as " "normal surface currents"); - type_ = TallyType::SURFACE; - estimator_ = TallyEstimator::ANALOG; - } else if (meshsurface_present) { type_ = TallyType::MESH_SURFACE; } else { - fatal_error("Cannot tally currents without surface type filters"); + type_ = TallyType::SURFACE; + estimator_ = TallyEstimator::ANALOG; } break; case HEATING: - if (settings::photon_transport) - estimator_ = TallyEstimator::COLLISION; + if (settings::photon_transport) { + // Photon heating requires a collision estimator (analog energy + // balance). However, if the tally only scores neutrons, we can keep the + // tracklength estimator since neutron heating uses kerma coefficients + // that support tracklength scoring. + bool neutron_only = false; + for (auto i_filt : filters_) { + auto pf = + dynamic_cast(model::tally_filters[i_filt].get()); + if (pf && pf->particles().size() == 1 && + pf->particles()[0].is_neutron()) { + neutron_only = true; + break; + } + } + if (!neutron_only) + estimator_ = TallyEstimator::COLLISION; + } break; - case SCORE_PULSE_HEIGHT: + case SCORE_PULSE_HEIGHT: { if (non_cell_energy_present) { fatal_error("Pulse-height tallies are not compatible with filters " "other than CellFilter and EnergyFilter"); } type_ = TallyType::PULSE_HEIGHT; - - // Collecting indices of all cells covered by the filters in the pulse - // height tally in global variable pulse_height_cells - for (const auto& i_filt : filters_) { - auto cell_filter = - dynamic_cast(model::tally_filters[i_filt].get()); - if (cell_filter) { - const auto& cells = cell_filter->cells(); - for (int i = 0; i < cell_filter->n_bins(); i++) { - int cell_index = cells[i]; - if (!contains(model::pulse_height_cells, cell_index)) { - model::pulse_height_cells.push_back(cell_index); - } - } - } + // Collect all unique cell indices covered by this tally. + // If no CellFilter is present, all cells in the geometry are scored. + const auto* cell_filter_ptr = get_filter(); + int n = cell_filter_ptr ? cell_filter_ptr->n_bins() + : static_cast(model::cells.size()); + for (int i = 0; i < n; ++i) { + int32_t cell_index = cell_filter_ptr ? cell_filter_ptr->cells()[i] : i; + if (!contains(model::pulse_height_cells, cell_index)) + model::pulse_height_cells.push_back(cell_index); } break; + } case SCORE_IFP_TIME_NUM: case SCORE_IFP_BETA_NUM: @@ -681,15 +712,20 @@ void Tally::set_scores(const vector& scores) "in multi-group mode"); } - // Make sure current scores are not mixed in with volumetric scores. - if (type_ == TallyType::SURFACE || type_ == TallyType::MESH_SURFACE) { - if (scores_.size() != 1) - fatal_error("Cannot tally other scores in the same tally as surface " - "currents."); + // Make sure mesh surface tallies contain only current score. + if (meshsurface_present) { + if ((scores_[0] != SCORE_CURRENT) || (scores_.size() > 1)) + fatal_error("Cannot tally score other than 'current' when using a " + "mesh-surface filter."); + } + + // Make sure surface tallies contain only surface type scores score. + if (type_ == TallyType::SURFACE) { + for (auto sc : scores_) + if ((sc != SCORE_CURRENT) && (sc != SCORE_FLUX)) + fatal_error("Cannot tally scores other than 'current' or 'flux' " + "when using surface filters."); } - if ((surface_present || meshsurface_present) && scores_[0] != SCORE_CURRENT) - fatal_error("Cannot tally score other than 'current' when using a surface " - "or mesh-surface filter."); } void Tally::set_nuclides(pugi::xml_node node) @@ -785,7 +821,7 @@ void Tally::init_triggers(pugi::xml_node node) } else { int i_score = 0; for (; i_score < this->scores_.size(); ++i_score) { - if (this->scores_[i_score] == reaction_type(score_str)) + if (this->scores_[i_score] == reaction_tally_mt(score_str)) break; } if (i_score == this->scores_.size()) { @@ -804,9 +840,11 @@ void Tally::init_results() { int n_scores = scores_.size() * nuclides_.size(); if (higher_moments_) { - results_ = xt::empty({n_filter_bins_, n_scores, 5}); + results_ = tensor::Tensor({static_cast(n_filter_bins_), + static_cast(n_scores), size_t {5}}); } else { - results_ = xt::empty({n_filter_bins_, n_scores, 3}); + results_ = tensor::Tensor({static_cast(n_filter_bins_), + static_cast(n_scores), size_t {3}}); } } @@ -814,7 +852,7 @@ void Tally::reset() { n_realizations_ = 0; if (results_.size() != 0) { - xt::view(results_, xt::all()) = 0.0; + results_.fill(0.0); } } @@ -849,9 +887,9 @@ void Tally::accumulate() if (higher_moments_) { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape()[0]; ++i) { + for (int64_t i = 0; i < results_.shape(0); ++i) { // score bins (flux, total reaction rate, fission reaction rate, etc.) - for (int j = 0; j < results_.shape()[1]; ++j) { + for (int j = 0; j < results_.shape(1); ++j) { double val = results_(i, j, TallyResult::VALUE) * norm; double val2 = val * val; results_(i, j, TallyResult::VALUE) = 0.0; @@ -864,9 +902,9 @@ void Tally::accumulate() } else { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape()[0]; ++i) { + for (int64_t i = 0; i < results_.shape(0); ++i) { // score bins (flux, total reaction rate, fission reaction rate, etc.) - for (int j = 0; j < results_.shape()[1]; ++j) { + for (int j = 0; j < results_.shape(1); ++j) { double val = results_(i, j, TallyResult::VALUE) * norm; results_(i, j, TallyResult::VALUE) = 0.0; results_(i, j, TallyResult::SUM) += val; @@ -886,18 +924,18 @@ int Tally::score_index(const std::string& score) const return -1; } -xt::xarray Tally::get_reshaped_data() const +tensor::Tensor Tally::get_reshaped_data() const { - std::vector shape; + vector shape; for (auto f : filters()) { shape.push_back(model::tally_filters[f]->n_bins()); } // add number of scores and nuclides to tally - shape.push_back(results_.shape()[1]); - shape.push_back(results_.shape()[2]); + shape.push_back(results_.shape(1)); + shape.push_back(results_.shape(2)); - xt::xarray reshaped_results = results_; + tensor::Tensor reshaped_results = results_; reshaped_results.reshape(shape); return reshaped_results; } @@ -1002,13 +1040,14 @@ void reduce_tally_results() // Skip any tallies that are not active auto& tally {model::tallies[i_tally]}; - // Get view of accumulated tally values - auto values_view = xt::view(tally->results_, xt::all(), xt::all(), - static_cast(TallyResult::VALUE)); + // Extract 2D view of the VALUE column from the 3D results tensor, + // then copy into a contiguous array for MPI reduction + const int val_idx = static_cast(TallyResult::VALUE); + tensor::View val_view = + tally->results_.slice(tensor::all, tensor::all, val_idx); + tensor::Tensor values(val_view); - // Make copy of tally values in contiguous array - xt::xtensor values = values_view; - xt::xtensor values_reduced = xt::empty_like(values); + tensor::Tensor values_reduced(values.shape()); // Reduce contiguous set of tally results MPI_Reduce(values.data(), values_reduced.data(), values.size(), @@ -1016,9 +1055,9 @@ void reduce_tally_results() // Transfer values on master and reset on other ranks if (mpi::master) { - values_view = values_reduced; + val_view = values_reduced; } else { - values_view = 0.0; + val_view = 0.0; } } } @@ -1026,14 +1065,13 @@ void reduce_tally_results() // Note that global tallies are *always* reduced even when no_reduce option // is on. - // Get view of global tally values + // Get reference to global tallies auto& gt = simulation::global_tallies; - auto gt_values_view = - xt::view(gt, xt::all(), static_cast(TallyResult::VALUE)); + const int val_col = static_cast(TallyResult::VALUE); - // Make copy of values in contiguous array - xt::xtensor gt_values = gt_values_view; - xt::xtensor gt_values_reduced = xt::empty_like(gt_values); + // Copy VALUE column into contiguous array for MPI reduction + tensor::Tensor gt_values(gt.slice(tensor::all, val_col)); + tensor::Tensor gt_values_reduced({size_t {N_GLOBAL_TALLIES}}); // Reduce contiguous data MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES, @@ -1041,9 +1079,9 @@ void reduce_tally_results() // Transfer values on master and reset on other ranks if (mpi::master) { - gt_values_view = gt_values_reduced; + gt.slice(tensor::all, val_col) = gt_values_reduced; } else { - gt_values_view = 0.0; + gt.slice(tensor::all, val_col) = 0.0; } // We also need to determine the total starting weight of particles from the diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 67e851644..241cbf008 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -14,12 +14,14 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/string_utils.h" +#include "openmc/surface.h" #include "openmc/tallies/derivative.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/filter_cell.h" #include "openmc/tallies/filter_delayedgroup.h" #include "openmc/tallies/filter_energy.h" +#include #include namespace openmc { @@ -156,7 +158,7 @@ void score_fission_delayed_dg(int i_tally, int d_bin, double score, dg_match.bins_[i_bin] = d_bin; // Determine the filter scoring index - auto filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto i = 0; i < tally.filters().size(); ++i) { auto i_filt = tally.filters(i); @@ -328,10 +330,10 @@ double score_neutron_heating(const Particle& p, const Tally& tally, double flux, //! Helper function to obtain reaction Q value for photons and charged particles double get_reaction_q_value(const Particle& p) { - if (p.type() == ParticleType::photon && p.event_mt() == PAIR_PROD) { + if (p.type().is_photon() && p.event_mt() == PAIR_PROD) { // pair production return -2 * MASS_ELECTRON_EV; - } else if (p.type() == ParticleType::positron) { + } else if (p.type() == ParticleType::positron()) { // positron annihilation return 2 * MASS_ELECTRON_EV; } else { @@ -344,7 +346,7 @@ double get_reaction_q_value(const Particle& p) double score_particle_heating(const Particle& p, const Tally& tally, double flux, int rxn_bin, int i_nuclide, double atom_density) { - if (p.type() == ParticleType::neutron) + if (p.type().is_neutron()) return score_neutron_heating( p, tally, flux, rxn_bin, i_nuclide, atom_density); if (i_nuclide == -1 || i_nuclide == p.event_nuclide() || @@ -447,7 +449,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) (score_bin == SCORE_PROMPT_NU_FISSION && g == 0)) { // Find the filter scoring index for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.0; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -495,7 +497,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) } else { // Find the filter index and weight for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -576,16 +578,14 @@ double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin) //! collision estimator. void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; // Get the pre-collision energy of the particle. auto E = p.E_last(); - using Type = ParticleType; - for (auto i = 0; i < tally.scores_.size(); ++i) { auto score_bin = tally.scores_[i]; auto score_index = start_index + i; @@ -598,9 +598,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_TOTAL: if (i_nuclide >= 0) { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = p.neutron_xs(i_nuclide).total * atom_density * flux; - } else if (p.type() == Type::photon) { + } else if (p.type().is_photon()) { score = p.photon_xs(i_nuclide).total * atom_density * flux; } } else { @@ -609,7 +609,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_INVERSE_VELOCITY: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Score inverse velocity in units of s/cm. @@ -617,11 +617,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_SCATTER: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; if (i_nuclide >= 0) { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { const auto& micro = p.neutron_xs(i_nuclide); score = (micro.total - micro.absorption) * atom_density * flux; } else { @@ -629,7 +629,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, score = (micro.coherent + micro.incoherent) * atom_density * flux; } } else { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = (p.macro_xs().total - p.macro_xs().absorption) * flux; } else { score = (p.macro_xs().coherent + p.macro_xs().incoherent) * flux; @@ -638,11 +638,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_ABSORPTION: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; if (i_nuclide >= 0) { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = p.neutron_xs(i_nuclide).absorption * atom_density * flux; } else { const auto& xs = p.photon_xs(i_nuclide); @@ -650,7 +650,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, (xs.total - xs.coherent - xs.incoherent) * atom_density * flux; } } else { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = p.macro_xs().absorption * flux; } else { score = @@ -806,7 +806,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // this loop. for (auto d = 1; d < rxn.products_.size(); ++d) { const auto& product = rxn.products_[d]; - if (product.particle_ != Type::neutron) + if (!product.particle_.is_neutron()) continue; auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d); @@ -860,7 +860,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // this loop. for (auto d = 1; d < rxn.products_.size(); ++d) { const auto& product = rxn.products_[d]; - if (product.particle_ != Type::neutron) + if (!product.particle_.is_neutron()) continue; auto yield = @@ -905,13 +905,12 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_EVENTS: -// Simply count the number of scoring events -#pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0; - continue; + // Simply count the number of scoring events + score = 1.0; + break; case ELASTIC: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; if (i_nuclide >= 0) { @@ -943,10 +942,10 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_TIME_NUM: if (settings::ifp_on) { - if ((p.type() == Type::neutron) && (p.fission())) { + if (p.type().is_neutron() && p.fission()) { if (is_generation_time_or_both()) { const auto& lifetimes = - simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_source_lifetime_bank[p.current_work()]; if (lifetimes.size() == settings::ifp_n_generation) { score = lifetimes[0] * p.wgt_last(); } @@ -957,10 +956,10 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_BETA_NUM: if (settings::ifp_on) { - if ((p.type() == Type::neutron) && (p.fission())) { + if (p.type().is_neutron() && p.fission()) { if (is_beta_effective_or_both()) { const auto& delayed_groups = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_source_delayed_group_bank[p.current_work()]; if (delayed_groups.size() == settings::ifp_n_generation) { if (delayed_groups[0] > 0) { score = p.wgt_last(); @@ -982,16 +981,15 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_DENOM: if (settings::ifp_on) { - if ((p.type() == Type::neutron) && (p.fission())) { + if (p.type().is_neutron() && p.fission()) { int ifp_data_size; if (is_beta_effective_or_both()) { ifp_data_size = static_cast( - simulation::ifp_source_delayed_group_bank[p.current_work() - 1] + simulation::ifp_source_delayed_group_bank[p.current_work()] .size()); } else { ifp_data_size = static_cast( - simulation::ifp_source_lifetime_bank[p.current_work() - 1] - .size()); + simulation::ifp_source_lifetime_bank[p.current_work()].size()); } if (ifp_data_size == settings::ifp_n_generation) { score = p.wgt_last(); @@ -1012,7 +1010,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (!simulation::need_depletion_rx) goto default_case; - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; int m; @@ -1045,7 +1043,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case INCOHERENT: case PHOTOELECTRIC: case PAIR_PROD: - if (p.type() != Type::photon) + if (!p.type().is_photon()) continue; if (i_nuclide >= 0) { @@ -1075,7 +1073,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // The default block is really only meant for redundant neutron reactions // (e.g. 444, 901) - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Any other cross section has to be calculated on-the-fly @@ -1114,8 +1112,8 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, //! is not used for analog tallies. void score_general_ce_analog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1129,8 +1127,6 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, p.neutron_xs(p.event_nuclide()).total : 0.0; - using Type = ParticleType; - for (auto i = 0; i < tally.scores_.size(); ++i) { auto score_bin = tally.scores_[i]; auto score_index = start_index + i; @@ -1141,7 +1137,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // All events score to a flux bin. We actually use a collision estimator // in place of an analog one since there is no way to count 'events' // exactly for the flux - if (p.type() == Type::neutron || p.type() == Type::photon) { + if (p.type().is_neutron() || p.type().is_photon()) { score = flux * p.wgt_last() / p.macro_xs().total; } else { score = 0.; @@ -1155,7 +1151,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_INVERSE_VELOCITY: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // All events score to an inverse velocity bin. We actually use a @@ -1165,7 +1161,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_SCATTER: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; // Skip any event where the particle didn't scatter @@ -1177,7 +1173,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_NU_SCATTER: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Only analog estimators are available. @@ -1202,7 +1198,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_ABSORPTION: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; if (settings::survival_biasing) { @@ -1431,7 +1427,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // this loop. for (auto d = 1; d < rxn.products_.size(); ++d) { const auto& product = rxn.products_[d]; - if (product.particle_ != Type::neutron) + if (!product.particle_.is_neutron()) continue; auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d); @@ -1517,13 +1513,12 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_EVENTS: -// Simply count the number of scoring events -#pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0; - continue; + // Simply count the number of scoring events + score = 1.0; + break; case ELASTIC: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Check if event MT matches @@ -1552,7 +1547,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, if (!simulation::need_depletion_rx) goto default_case; - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Check if the event MT matches @@ -1565,7 +1560,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, case INCOHERENT: case PHOTOELECTRIC: case PAIR_PROD: - if (p.type() != Type::photon) + if (!p.type().is_photon()) continue; if (score_bin == PHOTOELECTRIC) { @@ -1592,7 +1587,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // The default block is really only meant for redundant neutron reactions // (e.g. 444, 901) - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Any other score is assumed to be a MT number. Thus, we just need @@ -1620,8 +1615,8 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, //! argument is really just used for filter weights. void score_general_mg(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { auto& tally {*model::tallies[i_tally]}; @@ -2295,10 +2290,9 @@ void score_general_mg(Particle& p, int i_tally, int start_index, break; case SCORE_EVENTS: -// Simply count the number of scoring events -#pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0; - continue; + // Simply count the number of scoring events + score = 1.0; + break; default: continue; @@ -2316,10 +2310,7 @@ void score_analog_tally_ce(Particle& p) // Since electrons/positrons are not transported, we assign a flux of zero. // Note that the heating score does NOT use the flux and will be non-zero for // electrons/positrons. - double flux = - (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) - ? 1.0 - : 0.0; + double flux = (p.type().is_neutron() || p.type().is_photon()) ? 1.0 : 0.0; for (auto i_tally : model::active_analog_tallies) { const Tally& tally {*model::tallies[i_tally]}; @@ -2444,24 +2435,22 @@ void score_tracklength_tally_general( if (p.material() != MATERIAL_VOID) { const auto& mat = model::materials[p.material()]; auto j = mat->mat_nuclide_index_[i_nuclide]; - if (j == C_NONE) { - // Determine log union grid index - if (i_log_union == C_NONE) { - int neutron = static_cast(ParticleType::neutron); - i_log_union = std::log(p.E() / data::energy_min[neutron]) / - simulation::log_spacing; - } - - // Update micro xs cache - if (!tally.multiply_density()) { - p.update_neutron_xs(i_nuclide, i_log_union); - atom_density = 1.0; - } - } else { - atom_density = tally.multiply_density() - ? mat->atom_density(j, p.density_mult()) - : 1.0; + if (j != C_NONE) + atom_density = mat->atom_density(j, p.density_mult()); + } + if (atom_density > 0) { + if (!tally.multiply_density()) + atom_density = 1.0; + } else if (!tally.multiply_density()) { + // Determine log union grid index + if (i_log_union == C_NONE) { + int neutron = ParticleType::neutron().transport_index(); + i_log_union = std::log(p.E() / data::energy_min[neutron]) / + simulation::log_spacing; } + // Update micro xs cache + p.update_neutron_xs(i_nuclide, i_log_union); + atom_density = 1.0; } } @@ -2544,7 +2533,7 @@ void score_collision_tally(Particle& p) { // Determine the collision estimate of the flux double flux = 0.0; - if (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) { + if (p.type().is_neutron() || p.type().is_photon()) { flux = p.wgt_last() / p.macro_xs().total; } @@ -2573,25 +2562,25 @@ void score_collision_tally(Particle& p) double atom_density = 0.; if (i_nuclide >= 0) { - const auto& mat = model::materials[p.material()]; - auto j = mat->mat_nuclide_index_[i_nuclide]; - if (j == C_NONE) { + if (p.material() != MATERIAL_VOID) { + const auto& mat = model::materials[p.material()]; + auto j = mat->mat_nuclide_index_[i_nuclide]; + if (j != C_NONE) + atom_density = mat->atom_density(j, p.density_mult()); + } + if (atom_density > 0) { + if (!tally.multiply_density()) + atom_density = 1.0; + } else if (!tally.multiply_density()) { // Determine log union grid index if (i_log_union == C_NONE) { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); i_log_union = std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing; } - // Update micro xs cache - if (!tally.multiply_density()) { - p.update_neutron_xs(i_nuclide, i_log_union); - atom_density = 1.0; - } - } else { - atom_density = tally.multiply_density() - ? mat->atom_density(j, p.density_mult()) - : 1.0; + p.update_neutron_xs(i_nuclide, i_log_union); + atom_density = 1.0; } } @@ -2619,7 +2608,7 @@ void score_collision_tally(Particle& p) match.bins_present_ = false; } -void score_surface_tally(Particle& p, const vector& tallies) +void score_meshsurface_tally(Particle& p, const vector& tallies) { double current = p.wgt_last(); @@ -2663,6 +2652,69 @@ void score_surface_tally(Particle& p, const vector& tallies) match.bins_present_ = false; } +void score_surface_tally( + Particle& p, const vector& tallies, const Direction& normal) +{ + double wgt = p.wgt_last(); + + double mu = std::clamp(p.u().dot(normal), -1.0, 1.0); + + // Sign for net current: +1 if crossing outward (in direction of normal), + // -1 if crossing inward + double current_sign = std::copysign(1.0, mu); + + // Determine absolute cosine of angle between particle direction and surface + // normal, needed for the surface-crossing flux estimator. + double abs_mu = std::abs(mu); + if (abs_mu < settings::surface_grazing_cutoff) + abs_mu = settings::surface_grazing_ratio * settings::surface_grazing_cutoff; + + for (auto i_tally : tallies) { + auto& tally {*model::tallies[i_tally]}; + + // Initialize an iterator over valid filter bin combinations. If there are + // no valid combinations, use a continue statement to ensure we skip the + // assume_separate break below. + auto filter_iter = FilterBinIter(tally, p); + auto end = FilterBinIter(tally, true, &p.filter_matches()); + if (filter_iter == end) + continue; + + // Loop over filter bins. + for (; filter_iter != end; ++filter_iter) { + auto filter_index = filter_iter.index_; + auto filter_weight = filter_iter.weight_; + + // Loop over scores. + for (auto score_index = 0; score_index < tally.scores_.size(); + ++score_index) { + auto score_bin = tally.scores_[score_index]; + double score; + if (score_bin == SCORE_CURRENT) { + // Net current: weight carries the sign of the crossing direction. + score = wgt * current_sign; + } else { + // SCORE_FLUX: surface-crossing estimator phi_S = sum(w / |mu|). + score = wgt / abs_mu; + } +#pragma omp atomic + tally.results_(filter_index, score_index, TallyResult::VALUE) += + score * filter_weight; + } + } + // If the user has specified that we can assume all tallies are spatially + // separate, this implies that once a tally has been scored to, we needn't + // check the others. This cuts down on overhead when there are many + // tallies specified + if (settings::assume_separate) + break; + } + + // Reset all the filter matches for the next tally event. + for (auto& match : p.filter_matches()) + match.bins_present_ = false; +} + void score_pulse_height_tally(Particle& p, const vector& tallies) { // The pulse height tally in OpenMC hijacks the logic of CellFilter and @@ -2678,64 +2730,59 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) int orig_cell = p.coord(0).cell(); double orig_E_last = p.E_last(); + // Set particle in top level + p.n_coord() = 1; + for (auto i_tally : tallies) { auto& tally {*model::tallies[i_tally]}; - // Determine all CellFilter in the tally - for (const auto& filter : tally.filters()) { - auto cell_filter = - dynamic_cast(model::tally_filters[filter].get()); - if (cell_filter != nullptr) { + // Find CellFilter in the tally (if any) to determine cells to loop over + const auto* cell_filter = tally.get_filter(); + const auto& cells = + cell_filter ? cell_filter->cells() : model::pulse_height_cells; - const auto& cells = cell_filter->cells(); - // Loop over all cells in the CellFilter - for (auto cell_index = 0; cell_index < cells.size(); ++cell_index) { - int cell_id = cells[cell_index]; + for (auto cell_id : cells) { + // Temporarily change cell of particle + p.coord(0).cell() = cell_id; - // Temporarily change cell of particle - p.n_coord() = 1; - p.coord(0).cell() = cell_id; + // Determine index of cell in model::pulse_height_cells + auto it = std::find(model::pulse_height_cells.begin(), + model::pulse_height_cells.end(), cell_id); + int index = std::distance(model::pulse_height_cells.begin(), it); - // Determine index of cell in model::pulse_height_cells - auto it = std::find(model::pulse_height_cells.begin(), - model::pulse_height_cells.end(), cell_id); - int index = std::distance(model::pulse_height_cells.begin(), it); + // Temporarily change energy of particle to pulse-height value + p.E_last() = p.pht_storage()[index]; - // Temporarily change energy of particle to pulse-height value - p.E_last() = p.pht_storage()[index]; + // Initialize an iterator over valid filter bin combinations. If + // there are no valid combinations, use a continue statement to ensure + // we skip the assume_separate break below. + auto filter_iter = FilterBinIter(tally, p); + auto end = FilterBinIter(tally, true, &p.filter_matches()); + if (filter_iter != end) { - // Initialize an iterator over valid filter bin combinations. If - // there are no valid combinations, use a continue statement to ensure - // we skip the assume_separate break below. - auto filter_iter = FilterBinIter(tally, p); - auto end = FilterBinIter(tally, true, &p.filter_matches()); - if (filter_iter == end) - continue; + // Loop over filter bins. + for (; filter_iter != end; ++filter_iter) { + auto filter_index = filter_iter.index_; + auto filter_weight = filter_iter.weight_; - // Loop over filter bins. - for (; filter_iter != end; ++filter_iter) { - auto filter_index = filter_iter.index_; - auto filter_weight = filter_iter.weight_; - - // Loop over scores. - for (auto score_index = 0; score_index < tally.scores_.size(); - ++score_index) { + // Loop over scores. + for (auto score_index = 0; score_index < tally.scores_.size(); + ++score_index) { #pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += - filter_weight; - } + tally.results_(filter_index, score_index, TallyResult::VALUE) += + filter_weight; } - - // Reset all the filter matches for the next tally event. - for (auto& match : p.filter_matches()) - match.bins_present_ = false; } } + + // Reset all the filter matches for the next tally event. + for (auto& match : p.filter_matches()) + match.bins_present_ = false; } - // Restore cell/energy - p.n_coord() = orig_n_coord; - p.coord(0).cell() = orig_cell; - p.E_last() = orig_E_last; } + // Restore cell/energy + p.n_coord() = orig_n_coord; + p.coord(0).cell() = orig_cell; + p.E_last() = orig_E_last; } } // namespace openmc diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index f1f83e298..7515f01a9 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -28,7 +28,7 @@ KTrigger keff_trigger; //============================================================================== std::pair get_tally_uncertainty( - int i_tally, int score_index, int filter_index) + int i_tally, int score_index, int64_t filter_index) { const auto& tally {model::tallies[i_tally]}; @@ -71,7 +71,7 @@ void check_tally_triggers(double& ratio, int& tally_id, int& score) continue; const auto& results = t.results_; - for (auto filter_index = 0; filter_index < results.shape()[0]; + for (int64_t filter_index = 0; filter_index < results.shape(0); ++filter_index) { // Compute the tally uncertainty metrics. auto uncert_pair = diff --git a/src/thermal.cpp b/src/thermal.cpp index cbe0983ed..edfbddf23 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -3,12 +3,7 @@ #include // for sort, move, min, max, find #include // for round, sqrt, abs -#include "xtensor/xarray.hpp" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xsort.hpp" -#include "xtensor/xtensor.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include "openmc/constants.h" @@ -55,7 +50,7 @@ ThermalScattering::ThermalScattering( // Determine temperatures available auto dset_names = dataset_names(kT_group); auto n = dset_names.size(); - auto temps_available = xt::empty({n}); + auto temps_available = tensor::Tensor({n}); for (int i = 0; i < dset_names.size(); ++i) { // Read temperature value double T; @@ -82,7 +77,7 @@ ThermalScattering::ThermalScattering( // Determine actual temperatures to read for (const auto& T : temperature) { - auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; + auto i_closest = tensor::abs(temps_available - T).argmin(); auto temp_actual = temps_available[i_closest]; if (std::abs(temp_actual - T) < settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), @@ -296,16 +291,21 @@ void ThermalData::calculate_xs( *inelastic = (*inelastic_.xs)(E); } -void ThermalData::sample(const NuclideMicroXS& micro_xs, double E, - double* E_out, double* mu, uint64_t* seed) +AngleEnergy& ThermalData::sample_dist( + const NuclideMicroXS& micro_xs, double E, uint64_t* seed) const { // Determine whether inelastic or elastic scattering will occur if (prn(seed) < micro_xs.thermal_elastic / micro_xs.thermal) { - elastic_.distribution->sample(E, *E_out, *mu, seed); + return *elastic_.distribution; } else { - inelastic_.distribution->sample(E, *E_out, *mu, seed); + return *inelastic_.distribution; } +} +void ThermalData::sample(const NuclideMicroXS& micro_xs, double E, + double* E_out, double* mu, uint64_t* seed) const +{ + sample_dist(micro_xs, E, seed).sample(E, *E_out, *mu, seed); // Because of floating-point roundoff, it may be possible for mu to be // outside of the range [-1,1). In these cases, we just set mu to exactly // -1 or 1 @@ -313,6 +313,13 @@ void ThermalData::sample(const NuclideMicroXS& micro_xs, double E, *mu = std::copysign(1.0, *mu); } +double ThermalData::sample_energy_and_pdf(const NuclideMicroXS& micro_xs, + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(micro_xs, E_in, seed) + .sample_energy_and_pdf(E_in, mu, E_out, seed); +} + void free_memory_thermal() { data::thermal_scatt.clear(); diff --git a/src/track_output.cpp b/src/track_output.cpp index f4344d50f..02e092236 100644 --- a/src/track_output.cpp +++ b/src/track_output.cpp @@ -8,7 +8,7 @@ #include "openmc/simulation.h" #include "openmc/vector.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include @@ -122,7 +122,7 @@ void finalize_particle_track(Particle& p) int offset = 0; for (auto& track_i : p.tracks()) { offsets.push_back(offset); - particles.push_back(static_cast(track_i.particle)); + particles.push_back(track_i.particle.pdg_number()); offset += track_i.states.size(); tracks.insert(tracks.end(), track_i.states.begin(), track_i.states.end()); } diff --git a/src/universe.cpp b/src/universe.cpp index 78ddd54d4..e1db80b99 100644 --- a/src/universe.cpp +++ b/src/universe.cpp @@ -61,7 +61,7 @@ bool Universe::find_cell(GeometryState& p) const BoundingBox Universe::bounding_box() const { - BoundingBox bbox = {INFTY, -INFTY, INFTY, -INFTY, INFTY, -INFTY}; + BoundingBox bbox = BoundingBox::inverted(); if (cells_.size() == 0) { return {}; } else { diff --git a/src/urr.cpp b/src/urr.cpp index 02cef2280..b791eecb0 100644 --- a/src/urr.cpp +++ b/src/urr.cpp @@ -25,7 +25,7 @@ UrrData::UrrData(hid_t group_id) // Read URR tables. The HDF5 format is a little // different from how we want it laid out in memory. // This array used to be called "prob_". - xt::xtensor tmp_prob; + tensor::Tensor tmp_prob; read_dataset(group_id, "table", tmp_prob); auto shape = tmp_prob.shape(); @@ -38,7 +38,7 @@ UrrData::UrrData(hid_t group_id) xs_values_.resize({n_energy, n_cdf_values}); // Now fill in the values. Using manual loops here since we might - // not have fancy xtensor slicing code written for GPU tensors. + // not have fancy tensor slicing code written for GPU tensors. // The below enum gives how URR tables are laid out in our HDF5 tables. enum class URRTableParam { CUM_PROB, diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 1deffb804..f675ae78a 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -17,8 +17,7 @@ #include "openmc/timer.h" #include "openmc/xml_interface.h" -#include "xtensor/xadapt.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include // for copy @@ -242,7 +241,8 @@ vector VolumeCalculation::execute() const // non-zero auto n_nuc = settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size(); - xt::xtensor atoms({n_nuc, 2}, 0.0); + auto atoms = + tensor::zeros({static_cast(n_nuc), size_t {2}}); #ifdef OPENMC_MPI if (mpi::master) { @@ -452,9 +452,11 @@ void VolumeCalculation::to_hdf5( } // Create array of total # of atoms with uncertainty for each nuclide - xt::xtensor atom_data({n_nuc, 2}); - xt::view(atom_data, xt::all(), 0) = xt::adapt(result.atoms); - xt::view(atom_data, xt::all(), 1) = xt::adapt(result.uncertainty); + tensor::Tensor atom_data({static_cast(n_nuc), size_t {2}}); + for (size_t k = 0; k < static_cast(n_nuc); ++k) { + atom_data(k, 0) = result.atoms[k]; + atom_data(k, 1) = result.uncertainty[k]; + } // Write results write_dataset(group_id, "nuclides", nucnames); diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 9333800bc..0d268ce17 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -6,12 +6,7 @@ #include #include -#include "xtensor/xdynamic_view.hpp" -#include "xtensor/xindex_view.hpp" -#include "xtensor/xio.hpp" -#include "xtensor/xmasked_view.hpp" -#include "xtensor/xnoalias.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/error.h" #include "openmc/file_utils.h" @@ -49,108 +44,6 @@ openmc::vector> weight_windows_generators; } // namespace variance_reduction -//============================================================================== -// Non-member functions -//============================================================================== - -void apply_weight_windows(Particle& p) -{ - if (!settings::weight_windows_on) - return; - - // WW on photon and neutron only - if (p.type() != ParticleType::neutron && p.type() != ParticleType::photon) - return; - - // skip dead or no energy - if (p.E() <= 0 || !p.alive()) - return; - - bool in_domain = false; - // TODO: this is a linear search - should do something more clever - WeightWindow weight_window; - for (const auto& ww : variance_reduction::weight_windows) { - weight_window = ww->get_weight_window(p); - if (weight_window.is_valid()) - break; - } - - // If particle has not yet had its birth weight window value set, set it to - // the current weight window (or 1.0 if not born in a weight window). - if (p.wgt_ww_born() == -1.0) { - if (weight_window.is_valid()) { - p.wgt_ww_born() = - (weight_window.lower_weight + weight_window.upper_weight) / 2; - } else { - p.wgt_ww_born() = 1.0; - } - } - - // particle is not in any of the ww domains, do nothing - if (!weight_window.is_valid()) - return; - - // Normalize weight windows based on particle's starting weight - // and the value of the weight window the particle was born in. - weight_window.scale(p.wgt_born() / p.wgt_ww_born()); - - // get the paramters - double weight = p.wgt(); - - // first check to see if particle should be killed for weight cutoff - if (p.wgt() < weight_window.weight_cutoff) { - p.wgt() = 0.0; - return; - } - - // check if particle is far above current weight window - // only do this if the factor is not already set on the particle and a - // maximum lower bound ratio is specified - if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 && - p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) { - p.ww_factor() = - p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio); - } - - // move weight window closer to the particle weight if needed - if (p.ww_factor() > 1.0) - weight_window.scale(p.ww_factor()); - - // if particle's weight is above the weight window split until they are within - // the window - if (weight > weight_window.upper_weight) { - // do not further split the particle if above the limit - if (p.n_split() >= settings::max_history_splits) - return; - - double n_split = std::ceil(weight / weight_window.upper_weight); - double max_split = weight_window.max_split; - n_split = std::min(n_split, max_split); - - p.n_split() += n_split; - - // Create secondaries and divide weight among all particles - int i_split = std::round(n_split); - for (int l = 0; l < i_split - 1; l++) { - p.split(weight / n_split); - } - // remaining weight is applied to current particle - p.wgt() = weight / n_split; - - } else if (weight <= weight_window.lower_weight) { - // if the particle weight is below the window, play Russian roulette - double weight_survive = - std::min(weight * weight_window.max_split, weight_window.survival_weight); - russian_roulette(p, weight_survive); - } // else particle is in the window, continue as normal -} - -void free_memory_weight_windows() -{ - variance_reduction::ww_map.clear(); - variance_reduction::weight_windows.clear(); -} - //============================================================================== // WeightWindowSettings implementation //============================================================================== @@ -179,7 +72,7 @@ WeightWindows::WeightWindows(pugi::xml_node node) // get the particle type auto particle_type_str = std::string(get_node_value(node, "particle_type")); - particle_type_ = openmc::str_to_particle_type(particle_type_str); + particle_type_ = ParticleType {particle_type_str}; // Determine associated mesh int32_t mesh_id = std::stoi(get_node_value(node, "mesh")); @@ -252,7 +145,7 @@ WeightWindows* WeightWindows::from_hdf5( std::string particle_type; read_dataset(ww_group, "particle_type", particle_type); - wws->particle_type_ = openmc::str_to_particle_type(particle_type); + wws->particle_type_ = ParticleType {particle_type}; read_dataset(ww_group, "energy_bounds", wws->energy_bounds_); @@ -265,8 +158,12 @@ WeightWindows* WeightWindows::from_hdf5( } wws->set_mesh(model::mesh_map[mesh_id]); - wws->lower_ww_ = xt::empty(wws->bounds_size()); - wws->upper_ww_ = xt::empty(wws->bounds_size()); + wws->lower_ww_ = + tensor::Tensor({static_cast(wws->bounds_size()[0]), + static_cast(wws->bounds_size()[1])}); + wws->upper_ww_ = + tensor::Tensor({static_cast(wws->bounds_size()[0]), + static_cast(wws->bounds_size()[1])}); read_dataset(ww_group, "lower_ww_bounds", wws->lower_ww_); read_dataset(ww_group, "upper_ww_bounds", wws->upper_ww_); @@ -284,7 +181,10 @@ void WeightWindows::set_defaults() { // set energy bounds to the min/max energy supported by the data if (energy_bounds_.size() == 0) { - int p_type = static_cast(particle_type_); + int p_type = particle_type_.transport_index(); + if (p_type == C_NONE) { + fatal_error("Weight windows particle is not supported for transport."); + } energy_bounds_.push_back(data::energy_min[p_type]); energy_bounds_.push_back(data::energy_max[p_type]); } @@ -298,9 +198,11 @@ void WeightWindows::allocate_ww_bounds() "Size of weight window bounds is zero for WeightWindows {}", id()); warning(msg); } - lower_ww_ = xt::empty(shape); + lower_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); lower_ww_.fill(-1); - upper_ww_ = xt::empty(shape); + upper_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); upper_ww_.fill(-1); } @@ -345,10 +247,9 @@ void WeightWindows::set_energy_bounds(span bounds) void WeightWindows::set_particle_type(ParticleType p_type) { - if (p_type != ParticleType::neutron && p_type != ParticleType::photon) - fatal_error( - fmt::format("Particle type '{}' cannot be applied to weight windows.", - particle_type_to_str(p_type))); + if (!p_type.is_neutron() && !p_type.is_photon()) + fatal_error(fmt::format( + "Particle type '{}' cannot be applied to weight windows.", p_type.str())); particle_type_ = p_type; } @@ -372,27 +273,28 @@ void WeightWindows::set_mesh(const Mesh* mesh) set_mesh(model::mesh_map[mesh->id_]); } -WeightWindow WeightWindows::get_weight_window(const Particle& p) const +std::pair WeightWindows::get_weight_window( + const Particle& p) const { // check for particle type if (particle_type_ != p.type()) { - return {}; + return {false, {}}; } + // particle energy + double E = p.E(); + + // check to make sure energy is in range, expects sorted energy values + if (E < energy_bounds_.front() || E > energy_bounds_.back()) + return {false, {}}; + // Get mesh index for particle's position const auto& mesh = this->mesh(); int mesh_bin = mesh->get_bin(p.r()); // particle is outside the weight window mesh if (mesh_bin < 0) - return {}; - - // particle energy - double E = p.E(); - - // check to make sure energy is in range, expects sorted energy values - if (E < energy_bounds_.front() || E > energy_bounds_.back()) - return {}; + return {false, {}}; // get the mesh bin in energy group int energy_bin = @@ -407,7 +309,7 @@ WeightWindow WeightWindows::get_weight_window(const Particle& p) const ww.max_lb_ratio = max_lb_ratio_; ww.max_split = max_split_; ww.weight_cutoff = weight_cutoff_; - return ww; + return {true, ww}; } std::array WeightWindows::bounds_size() const @@ -446,8 +348,8 @@ void WeightWindows::check_bounds(const T& bounds) const } } -void WeightWindows::set_bounds(const xt::xtensor& lower_bounds, - const xt::xtensor& upper_bounds) +void WeightWindows::set_bounds(const tensor::Tensor& lower_bounds, + const tensor::Tensor& upper_bounds) { this->check_bounds(lower_bounds, upper_bounds); @@ -458,7 +360,7 @@ void WeightWindows::set_bounds(const xt::xtensor& lower_bounds, } void WeightWindows::set_bounds( - const xt::xtensor& lower_bounds, double ratio) + const tensor::Tensor& lower_bounds, double ratio) { this->check_bounds(lower_bounds); @@ -473,14 +375,16 @@ void WeightWindows::set_bounds( { check_bounds(lower_bounds, upper_bounds); auto shape = this->bounds_size(); - lower_ww_ = xt::empty(shape); - upper_ww_ = xt::empty(shape); + lower_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); + upper_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); - // set new weight window values - xt::view(lower_ww_, xt::all()) = - xt::adapt(lower_bounds.data(), lower_ww_.shape()); - xt::view(upper_ww_, xt::all()) = - xt::adapt(upper_bounds.data(), upper_ww_.shape()); + // Copy weight window values from input spans into the tensors + std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(), + lower_ww_.data()); + std::copy(upper_bounds.data(), upper_bounds.data() + upper_ww_.size(), + upper_ww_.data()); } void WeightWindows::set_bounds(span lower_bounds, double ratio) @@ -488,14 +392,16 @@ void WeightWindows::set_bounds(span lower_bounds, double ratio) this->check_bounds(lower_bounds); auto shape = this->bounds_size(); - lower_ww_ = xt::empty(shape); - upper_ww_ = xt::empty(shape); + lower_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); + upper_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); - // set new weight window values - xt::view(lower_ww_, xt::all()) = - xt::adapt(lower_bounds.data(), lower_ww_.shape()); - xt::view(upper_ww_, xt::all()) = - xt::adapt(lower_bounds.data(), upper_ww_.shape()); + // Copy lower bounds into both arrays, then scale upper by ratio + std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(), + lower_ww_.data()); + std::copy(lower_bounds.data(), lower_bounds.data() + upper_ww_.size(), + upper_ww_.data()); upper_ww_ *= ratio; } @@ -508,8 +414,8 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, this->check_tally_update_compatibility(tally); // Dimensions of weight window arrays - int e_bins = lower_ww_.shape()[0]; - int64_t mesh_bins = lower_ww_.shape()[1]; + int e_bins = lower_ww_.shape(0); + int64_t mesh_bins = lower_ww_.shape(1); // Initialize weight window arrays to -1.0 by default #pragma omp parallel for collapse(2) schedule(static) @@ -540,16 +446,16 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, /////////////////////////// // Extract tally data // - // At the end of this section, the mean and rel_err array - // is a 2D view of tally data (n_e_groups, n_mesh_bins) + // At the end of this section, mean and rel_err are + // 2D tensors of tally data (n_e_groups, n_mesh_bins) // /////////////////////////// - // build a shape for a view of the tally results, this will always be + // build a shape for the tally results, this will always be // dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension) - // Look for the size of the last dimension of the results array - const auto& results_arr = tally->results(); - const int results_dim = static_cast(results_arr.shape()[2]); + // Look for the size of the last dimension of the results tensor + const auto& results = tally->results(); + const int results_dim = static_cast(results.shape(2)); std::array shape = {1, 1, 1, tally->n_scores(), results_dim}; // set the shape for the filters applied on the tally @@ -586,44 +492,61 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, std::find(filter_types.begin(), filter_types.end(), FilterType::MESH) - filter_types.begin(); - // get a fully reshaped view of the tally according to tally ordering of - // filters - auto tally_values = xt::reshape_view(results_arr, shape); - - // get a that is (particle, energy, mesh, scores, values) - auto transposed_view = xt::transpose(tally_values, transpose); - - // determine the dimension and index of the particle data + // determine the index of the particle within its filter int particle_idx = 0; if (tally->has_filter(FilterType::PARTICLE)) { - // get the particle filter auto pf = tally->get_filter(); const auto& particles = pf->particles(); - // find the index of the particle that matches these weight windows auto p_it = std::find(particles.begin(), particles.end(), this->particle_type_); - // if the particle filter doesn't have particle data for the particle - // used on this weight windows instance, report an error if (p_it == particles.end()) { auto msg = fmt::format("Particle type '{}' not present on Filter {} for " "Tally {} used to update WeightWindows {}", - particle_type_to_str(this->particle_type_), pf->id(), tally->id(), - this->id()); + this->particle_type_.str(), pf->id(), tally->id(), this->id()); fatal_error(msg); } - // use the index of the particle in the filter to down-select data later particle_idx = p_it - particles.begin(); } - // down-select data based on particle and score - auto sum = xt::dynamic_view( - transposed_view, {particle_idx, xt::all(), xt::all(), score_index, - static_cast(TallyResult::SUM)}); - auto sum_sq = xt::dynamic_view( - transposed_view, {particle_idx, xt::all(), xt::all(), score_index, - static_cast(TallyResult::SUM_SQ)}); + // The tally results array is 3D: (n_filter_combos, n_scores, n_result_types). + // The first dimension is a row-major flattening of up to 3 filter dimensions + // (particle, energy, mesh) whose storage order depends on which filters the + // tally has. We need to map our desired indices (particle, energy, mesh) + // into the correct flat filter combination index. + // + // transpose[i] tells us which storage position holds dimension i: + // i=0 -> particle, i=1 -> energy, i=2 -> mesh + // shape[j] gives the number of bins for filter storage position j. + + // Row-major strides for the 3 filter dimensions + const int stride0 = shape[1] * shape[2]; + const int stride1 = shape[2]; + + tensor::Tensor sum( + {static_cast(e_bins), static_cast(mesh_bins)}); + tensor::Tensor sum_sq( + {static_cast(e_bins), static_cast(mesh_bins)}); + + const int i_sum = static_cast(TallyResult::SUM); + const int i_sum_sq = static_cast(TallyResult::SUM_SQ); + + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + // Place particle, energy, and mesh indices into their storage positions + std::array idx = {0, 0, 0}; + idx[transpose[0]] = particle_idx; + idx[transpose[1]] = e; + idx[transpose[2]] = static_cast(m); + + // Compute flat filter combination index (row-major over filter dims) + int flat = idx[0] * stride0 + idx[1] * stride1 + idx[2]; + + sum(e, m) = results(flat, score_index, i_sum); + sum_sq(e, m) = results(flat, score_index, i_sum_sq); + } + } int n = tally->n_realizations_; ////////////////////////////////////////////// @@ -698,7 +621,7 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, } } } else { - // For FW-CADIS, weight windows are inversely proportional to the adjoint + // For (FW-)CADIS, weight windows are inversely proportional to the adjoint // fluxes. We normalize the weight windows across all energy groups. #pragma omp parallel for collapse(2) schedule(static) for (int e = 0; e < e_bins; e++) { @@ -818,8 +741,7 @@ void WeightWindows::to_hdf5(hid_t group) const hid_t ww_group = create_group(group, fmt::format("weight_windows_{}", id())); write_dataset(ww_group, "mesh", this->mesh()->id()); - write_dataset( - ww_group, "particle_type", openmc::particle_type_to_str(particle_type_)); + write_dataset(ww_group, "particle_type", particle_type_.str()); write_dataset(ww_group, "energy_bounds", energy_bounds_); write_dataset(ww_group, "lower_ww_bounds", lower_ww_); write_dataset(ww_group, "upper_ww_bounds", upper_ww_); @@ -846,8 +768,8 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) max_realizations_, active_batches); warning(msg); } - auto tmp_str = get_node_value(node, "particle_type", true, true); - auto particle_type = str_to_particle_type(tmp_str); + auto tmp_str = get_node_value(node, "particle_type", false, true); + auto particle_type = ParticleType {tmp_str}; update_interval_ = std::stoi(get_node_value(node, "update_interval")); on_the_fly_ = get_node_value_bool(node, "on_the_fly"); @@ -856,7 +778,10 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (check_for_node(node, "energy_bounds")) { e_bounds = get_node_array(node, "energy_bounds"); } else { - int p_type = static_cast(particle_type); + int p_type = particle_type.transport_index(); + if (p_type == C_NONE) { + fatal_error("Weight windows particle is not supported for transport."); + } e_bounds.push_back(data::energy_min[p_type]); e_bounds.push_back(data::energy_max[p_type]); } @@ -866,7 +791,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (method_string == "magic") { method_ = WeightWindowUpdateMethod::MAGIC; if (settings::solver_type == SolverType::RANDOM_RAY && - FlatSourceDomain::adjoint_) { + FlatSourceDomain::adjoint_requested_) { fatal_error("Random ray weight window generation with MAGIC cannot be " "done in adjoint mode."); } @@ -875,7 +800,14 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (settings::solver_type != SolverType::RANDOM_RAY) { fatal_error("FW-CADIS can only be run in random ray solver mode."); } - FlatSourceDomain::adjoint_ = true; + FlatSourceDomain::adjoint_requested_ = true; + if (check_for_node(node, "targets")) { + FlatSourceDomain::fw_cadis_local_ = true; + targets_ = get_node_array(node, "targets"); + FlatSourceDomain::fw_cadis_local_targets_.insert( + std::end(FlatSourceDomain::fw_cadis_local_targets_), + std::begin(targets_), std::end(targets_)); + } } else { fatal_error(fmt::format( "Unknown weight window update method '{}' specified", method_string)); @@ -905,7 +837,8 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) ratio_)); if (ratio_ <= 1.0) fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " - "specified for weight window generation")); + "specified for weight window generation", + ratio_)); // create a matching weight windows object auto wws = WeightWindows::create(); @@ -933,7 +866,8 @@ void WeightWindowsGenerator::create_tally() for (const auto& f : model::tally_filters) { if (f->type() == FilterType::MESH) { const auto* mesh_filter = dynamic_cast(f.get()); - if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated()) { + if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated() && + !mesh_filter->rotated()) { ww_tally->add_filter(f.get()); found_mesh_filter = true; break; @@ -996,6 +930,115 @@ void WeightWindowsGenerator::update() const // Non-member functions //============================================================================== +std::pair search_weight_window(const Particle& p) +{ + // TODO: this is a linear search - should do something more clever + for (const auto& ww : variance_reduction::weight_windows) { + auto [ww_found, weight_window] = ww->get_weight_window(p); + if (ww_found) + return {true, weight_window}; + } + return {false, {}}; +} + +void apply_weight_windows(Particle& p) +{ + if (!settings::weight_windows_on) + return; + + // WW on photon and neutron only + if (!p.type().is_neutron() && !p.type().is_photon()) + return; + + // skip dead or no energy + if (p.E() <= 0 || !p.alive()) + return; + + auto [ww_found, ww] = search_weight_window(p); + if (ww_found && ww.is_valid()) { + apply_weight_window(p, ww); + } else { + if (p.wgt_ww_born() == -1.0) + p.wgt_ww_born() = 1.0; + } +} + +void apply_weight_window(Particle& p, WeightWindow weight_window) +{ + if (!weight_window.is_valid()) + return; + + // skip dead or no energy + if (p.E() <= 0 || !p.alive()) + return; + + // If particle has not yet had its birth weight window value set, set it to + // the current weight window. + if (p.wgt_ww_born() == -1.0) + p.wgt_ww_born() = + (weight_window.lower_weight + weight_window.upper_weight) / 2; + + // Normalize weight windows based on particle's starting weight + // and the value of the weight window the particle was born in. + weight_window.scale(p.wgt_born() / p.wgt_ww_born()); + + // get the paramters + double weight = p.wgt(); + + // first check to see if particle should be killed for weight cutoff + if (p.wgt() < weight_window.weight_cutoff) { + p.wgt() = 0.0; + return; + } + + // check if particle is far above current weight window + // only do this if the factor is not already set on the particle and a + // maximum lower bound ratio is specified + if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 && + p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) { + p.ww_factor() = + p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio); + } + + // move weight window closer to the particle weight if needed + if (p.ww_factor() > 1.0) + weight_window.scale(p.ww_factor()); + + // if particle's weight is above the weight window split until they are within + // the window + if (weight > weight_window.upper_weight) { + // do not further split the particle if above the limit + if (p.n_split() >= settings::max_history_splits) + return; + + double n_split = std::ceil(weight / weight_window.upper_weight); + double max_split = weight_window.max_split; + n_split = std::min(n_split, max_split); + + p.n_split() += n_split; + + // Create secondaries and divide weight among all particles + int i_split = std::round(n_split); + for (int l = 0; l < i_split - 1; l++) { + p.split(weight / n_split); + } + // remaining weight is applied to current particle + p.wgt() = weight / n_split; + + } else if (weight <= weight_window.lower_weight) { + // if the particle weight is below the window, play Russian roulette + double weight_survive = + std::min(weight * weight_window.max_split, weight_window.survival_weight); + russian_roulette(p, weight_survive); + } // else particle is in the window, continue as normal +} + +void free_memory_weight_windows() +{ + variance_reduction::ww_map.clear(); + variance_reduction::weight_windows.clear(); +} + void finalize_variance_reduction() { for (const auto& wwg : variance_reduction::weight_windows_generators) { @@ -1109,23 +1152,25 @@ extern "C" int openmc_weight_windows_get_energy_bounds( return 0; } -extern "C" int openmc_weight_windows_set_particle(int32_t index, int particle) +extern "C" int openmc_weight_windows_set_particle( + int32_t index, int32_t particle) { if (int err = verify_ww_index(index)) return err; const auto& wws = variance_reduction::weight_windows.at(index); - wws->set_particle_type(static_cast(particle)); + wws->set_particle_type(ParticleType {particle}); return 0; } -extern "C" int openmc_weight_windows_get_particle(int32_t index, int* particle) +extern "C" int openmc_weight_windows_get_particle( + int32_t index, int32_t* particle) { if (int err = verify_ww_index(index)) return err; const auto& wws = variance_reduction::weight_windows.at(index); - *particle = static_cast(wws->particle_type()); + *particle = wws->particle_type().pdg_number(); return 0; } @@ -1149,7 +1194,8 @@ extern "C" int openmc_weight_windows_set_bounds(int32_t index, return err; const auto& wws = variance_reduction::weight_windows[index]; - wws->set_bounds({lower_bounds, size}, {upper_bounds, size}); + wws->set_bounds(span(lower_bounds, size), + span(upper_bounds, size)); return 0; } diff --git a/src/wmp.cpp b/src/wmp.cpp index e9c4fb5e3..6f72e1ba4 100644 --- a/src/wmp.cpp +++ b/src/wmp.cpp @@ -33,22 +33,22 @@ WindowedMultipole::WindowedMultipole(hid_t group) // Read the "data" array. Use its shape to figure out the number of poles // and residue types in this data. read_dataset(group, "data", data_); - int n_residues = data_.shape()[1] - 1; + int n_residues = data_.shape(1) - 1; // Check to see if this data includes fission residues. fissionable_ = (n_residues == 3); // Read the "windows" array and use its shape to figure out the number of // windows. - xt::xtensor windows; + tensor::Tensor windows; read_dataset(group, "windows", windows); - int n_windows = windows.shape()[0]; + int n_windows = windows.shape(0); windows -= 1; // Adjust to 0-based indices // Read the "broaden_poly" arrays. - xt::xtensor broaden_poly; + tensor::Tensor broaden_poly; read_dataset(group, "broaden_poly", broaden_poly); - if (n_windows != broaden_poly.shape()[0]) { + if (n_windows != broaden_poly.shape(0)) { fatal_error("broaden_poly array shape is not consistent with the windows " "array shape in WMP library for " + name_ + "."); @@ -56,12 +56,12 @@ WindowedMultipole::WindowedMultipole(hid_t group) // Read the "curvefit" array. read_dataset(group, "curvefit", curvefit_); - if (n_windows != curvefit_.shape()[0]) { + if (n_windows != curvefit_.shape(0)) { fatal_error("curvefit array shape is not consistent with the windows " "array shape in WMP library for " + name_ + "."); } - fit_order_ = curvefit_.shape()[1] - 1; + fit_order_ = curvefit_.shape(1) - 1; // Check the code is compiling to work with sufficiently high fit order if (fit_order_ + 1 > MAX_POLY_COEFFICIENTS) { diff --git a/src/xsdata.cpp b/src/xsdata.cpp index 1929f51e6..33d063a7b 100644 --- a/src/xsdata.cpp +++ b/src/xsdata.cpp @@ -5,10 +5,7 @@ #include #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xindex_view.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/error.h" @@ -37,32 +34,32 @@ XsData::XsData(bool fissionable, AngleDistributionType scatter_format, } // allocate all [temperature][angle][in group] quantities vector shape {n_ang, n_g_}; - total = xt::zeros(shape); - absorption = xt::zeros(shape); - inverse_velocity = xt::zeros(shape); + total = tensor::zeros(shape); + absorption = tensor::zeros(shape); + inverse_velocity = tensor::zeros(shape); if (fissionable) { - fission = xt::zeros(shape); - nu_fission = xt::zeros(shape); - prompt_nu_fission = xt::zeros(shape); - kappa_fission = xt::zeros(shape); + fission = tensor::zeros(shape); + nu_fission = tensor::zeros(shape); + prompt_nu_fission = tensor::zeros(shape); + kappa_fission = tensor::zeros(shape); } // allocate decay_rate; [temperature][angle][delayed group] shape[1] = n_dg_; - decay_rate = xt::zeros(shape); + decay_rate = tensor::zeros(shape); if (fissionable) { shape = {n_ang, n_dg_, n_g_}; // allocate delayed_nu_fission; [temperature][angle][delay group][in group] - delayed_nu_fission = xt::zeros(shape); + delayed_nu_fission = tensor::zeros(shape); // chi_prompt; [temperature][angle][in group][out group] shape = {n_ang, n_g_, n_g_}; - chi_prompt = xt::zeros(shape); + chi_prompt = tensor::zeros(shape); // chi_delayed; [temperature][angle][delay group][in group][out group] shape = {n_ang, n_dg_, n_g_, n_g_}; - chi_delayed = xt::zeros(shape); + chi_delayed = tensor::zeros(shape); } for (int a = 0; a < n_ang; a++) { @@ -85,28 +82,30 @@ void XsData::from_hdf5(hid_t xsdata_grp, bool fissionable, { // Reconstruct the dimension information so it doesn't need to be passed size_t n_ang = n_pol * n_azi; - size_t energy_groups = total.shape()[1]; + size_t energy_groups = total.shape(1); // Set the fissionable-specific data if (fissionable) { fission_from_hdf5(xsdata_grp, n_ang, is_isotropic); } // Get the non-fission-specific data - read_nd_vector(xsdata_grp, "decay-rate", decay_rate); - read_nd_vector(xsdata_grp, "absorption", absorption, true); - read_nd_vector(xsdata_grp, "inverse-velocity", inverse_velocity); + read_nd_tensor(xsdata_grp, "decay-rate", decay_rate); + read_nd_tensor(xsdata_grp, "absorption", absorption, true); + read_nd_tensor(xsdata_grp, "inverse-velocity", inverse_velocity); // Get scattering data scatter_from_hdf5( xsdata_grp, n_ang, scatter_format, final_scatter_format, order_data); - // Check absorption to ensure it is not 0 since it is often the - // denominator in tally methods - xt::filtration(absorption, xt::equal(absorption, 0.)) = 1.e-10; + // Replace zero absorption values with a small number to avoid + // division by zero in tally methods + for (size_t i = 0; i < absorption.size(); i++) + if (absorption.data()[i] == 0.0) + absorption.data()[i] = 1.e-10; // Get or calculate the total x/s if (object_exists(xsdata_grp, "total")) { - read_nd_vector(xsdata_grp, "total", total); + read_nd_tensor(xsdata_grp, "total", total); } else { for (size_t a = 0; a < n_ang; a++) { for (size_t gin = 0; gin < energy_groups; gin++) { @@ -115,8 +114,11 @@ void XsData::from_hdf5(hid_t xsdata_grp, bool fissionable, } } - // Fix if total is 0, since it is in the denominator when tallying - xt::filtration(total, xt::equal(total, 0.)) = 1.e-10; + // Replace zero total cross sections with a small number to avoid + // division by zero in tally methods + for (size_t i = 0; i < total.size(); i++) + if (total.data()[i] == 0.0) + total.data()[i] = 1.e-10; } //============================================================================== @@ -127,21 +129,30 @@ void XsData::fission_vector_beta_from_hdf5( // Data is provided as nu-fission and chi with a beta for delayed info // Get chi - xt::xtensor temp_chi({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi", temp_chi, true); + tensor::Tensor temp_chi = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "chi", temp_chi, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi /= xt::view(xt::sum(temp_chi, {1}), xt::all(), xt::newaxis()); + // Normalize chi so it sums to 1 over outgoing groups for each angle + for (size_t a = 0; a < n_ang; a++) { + tensor::View row = temp_chi.slice(a); + row /= row.sum(); + } - // Now every incoming group in prompt_chi and delayed_chi is the normalized - // chi we just made - chi_prompt = xt::view(temp_chi, xt::all(), xt::newaxis(), xt::all()); - chi_delayed = - xt::view(temp_chi, xt::all(), xt::newaxis(), xt::newaxis(), xt::all()); + // Replicate the energy spectrum across all incoming groups — the + // spectrum is independent of the incoming neutron energy + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_prompt.slice(a, gin) = temp_chi.slice(a); + + // Same spectrum for delayed neutrons, replicated across delayed groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_delayed.slice(a, d, gin) = temp_chi.slice(a); // Get nu-fission - xt::xtensor temp_nufiss({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "nu-fission", temp_nufiss, true); + tensor::Tensor temp_nufiss = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "nu-fission", temp_nufiss, true); // Get beta (strategy will depend upon the number of dimensions in beta) hid_t beta_dset = open_dataset(xsdata_grp, "beta"); @@ -151,26 +162,39 @@ void XsData::fission_vector_beta_from_hdf5( if (!is_isotropic) ndim_target += 2; if (beta_ndims == ndim_target) { - xt::xtensor temp_beta({n_ang, n_dg_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = tensor::zeros({n_ang, n_dg_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - // Set prompt_nu_fission = (1. - beta_total)*nu_fission - prompt_nu_fission = temp_nufiss * (1. - xt::sum(temp_beta, {1})); + // prompt_nu_fission = (1 - sum_of_beta) * nu_fission + auto beta_sum = temp_beta.sum(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = temp_nufiss(a, g) * (1.0 - beta_sum(a)); - // Set delayed_nu_fission as beta * nu_fission - delayed_nu_fission = - xt::view(temp_beta, xt::all(), xt::all(), xt::newaxis()) * - xt::view(temp_nufiss, xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission is the outer product of the delayed neutron + // fraction (beta) and the fission production rate (nu-fission) + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = temp_beta(a, d) * temp_nufiss(a, g); } else if (beta_ndims == ndim_target + 1) { - xt::xtensor temp_beta({n_ang, n_dg_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = + tensor::zeros({n_ang, n_dg_, n_g_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - // Set prompt_nu_fission = (1. - beta_total)*nu_fission - prompt_nu_fission = temp_nufiss * (1. - xt::sum(temp_beta, {1})); + // prompt_nu_fission = (1 - sum_of_beta) * nu_fission + // Here beta is energy-dependent, so sum over delayed groups (axis 1) + auto beta_sum = temp_beta.sum(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = temp_nufiss(a, g) * (1.0 - beta_sum(a, g)); - // Set delayed_nu_fission as beta * nu_fission - delayed_nu_fission = - temp_beta * xt::view(temp_nufiss, xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission: beta is already energy-dependent [n_ang, n_dg, n_g], + // so scale each delayed group's beta by the total nu-fission for that group + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = temp_beta(a, d, g) * temp_nufiss(a, g); } } @@ -179,29 +203,42 @@ void XsData::fission_vector_no_beta_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Data is provided separately as prompt + delayed nu-fission and chi // Get chi-prompt - xt::xtensor temp_chi_p({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi-prompt", temp_chi_p, true); + tensor::Tensor temp_chi_p = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "chi-prompt", temp_chi_p, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi_p /= xt::view(xt::sum(temp_chi_p, {1}), xt::all(), xt::newaxis()); + // Normalize prompt chi so it sums to 1 over outgoing groups for each angle + for (size_t a = 0; a < n_ang; a++) { + tensor::View row = temp_chi_p.slice(a); + row /= row.sum(); + } // Get chi-delayed - xt::xtensor temp_chi_d({n_ang, n_dg_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi-delayed", temp_chi_d, true); + tensor::Tensor temp_chi_d = + tensor::zeros({n_ang, n_dg_, n_g_}); + read_nd_tensor(xsdata_grp, "chi-delayed", temp_chi_d, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi_d /= - xt::view(xt::sum(temp_chi_d, {2}), xt::all(), xt::all(), xt::newaxis()); + // Normalize delayed chi so it sums to 1 over outgoing groups for each + // angle and delayed group + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) { + tensor::View row = temp_chi_d.slice(a, d); + row /= row.sum(); + } - // Now assign the prompt and delayed chis by replicating for each incoming - // group - chi_prompt = xt::view(temp_chi_p, xt::all(), xt::newaxis(), xt::all()); - chi_delayed = - xt::view(temp_chi_d, xt::all(), xt::all(), xt::newaxis(), xt::all()); + // Replicate the prompt spectrum across all incoming groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_prompt.slice(a, gin) = temp_chi_p.slice(a); + + // Replicate the delayed spectrum across all incoming groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_delayed.slice(a, d, gin) = temp_chi_d.slice(a, d); // Get prompt and delayed nu-fission directly - read_nd_vector(xsdata_grp, "prompt-nu-fission", prompt_nu_fission, true); - read_nd_vector(xsdata_grp, "delayed-nu-fission", delayed_nu_fission, true); + read_nd_tensor(xsdata_grp, "prompt-nu-fission", prompt_nu_fission, true); + read_nd_tensor(xsdata_grp, "delayed-nu-fission", delayed_nu_fission, true); } void XsData::fission_vector_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) @@ -210,17 +247,22 @@ void XsData::fission_vector_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Therefore, the code only considers the data as prompt. // Get chi - xt::xtensor temp_chi({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi", temp_chi, true); + tensor::Tensor temp_chi = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "chi", temp_chi, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi /= xt::view(xt::sum(temp_chi, {1}), xt::all(), xt::newaxis()); + // Normalize chi so it sums to 1 over outgoing groups for each angle + for (size_t a = 0; a < n_ang; a++) { + tensor::View row = temp_chi.slice(a); + row /= row.sum(); + } - // Now every incoming group in self.chi is the normalized chi we just made - chi_prompt = xt::view(temp_chi, xt::all(), xt::newaxis(), xt::all()); + // Replicate the energy spectrum across all incoming groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_prompt.slice(a, gin) = temp_chi.slice(a); // Get nu-fission directly - read_nd_vector(xsdata_grp, "nu-fission", prompt_nu_fission, true); + read_nd_tensor(xsdata_grp, "nu-fission", prompt_nu_fission, true); } //============================================================================== @@ -231,8 +273,9 @@ void XsData::fission_matrix_beta_from_hdf5( // Data is provided as nu-fission and chi with a beta for delayed info // Get nu-fission matrix - xt::xtensor temp_matrix({n_ang, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "nu-fission", temp_matrix, true); + tensor::Tensor temp_matrix = + tensor::zeros({n_ang, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "nu-fission", temp_matrix, true); // Get beta (strategy will depend upon the number of dimensions in beta) hid_t beta_dset = open_dataset(xsdata_grp, "beta"); @@ -242,65 +285,92 @@ void XsData::fission_matrix_beta_from_hdf5( if (!is_isotropic) ndim_target += 2; if (beta_ndims == ndim_target) { - xt::xtensor temp_beta({n_ang, n_dg_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = tensor::zeros({n_ang, n_dg_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - xt::xtensor temp_beta_sum({n_ang}, 0.); - temp_beta_sum = xt::sum(temp_beta, {1}); + auto beta_sum = temp_beta.sum(1); + auto matrix_gout_sum = temp_matrix.sum(2); - // prompt_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by (1 - beta_sum) - prompt_nu_fission = xt::sum(temp_matrix, {2}) * (1. - temp_beta_sum); + // prompt_nu_fission = sum_gout(matrix) * (1 - beta_total) + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = matrix_gout_sum(a, g) * (1.0 - beta_sum(a)); - // Store chi-prompt - chi_prompt = - xt::view(1.0 - temp_beta_sum, xt::all(), xt::newaxis(), xt::newaxis()) * - temp_matrix; + // chi_prompt = (1 - beta_total) * nu-fission matrix (unnormalized) + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + (1.0 - beta_sum(a)) * temp_matrix(a, gin, gout); - // delayed_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by beta - delayed_nu_fission = - xt::view(temp_beta, xt::all(), xt::all(), xt::newaxis()) * - xt::view(xt::sum(temp_matrix, {2}), xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission is the outer product of the delayed neutron + // fraction (beta) and the total fission rate summed over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = temp_beta(a, d) * matrix_gout_sum(a, g); - // Store chi-delayed - chi_delayed = - xt::view(temp_beta, xt::all(), xt::all(), xt::newaxis(), xt::newaxis()) * - xt::view(temp_matrix, xt::all(), xt::newaxis(), xt::all(), xt::all()); + // chi_delayed = beta * nu-fission matrix, expanded across delayed groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_delayed(a, d, gin, gout) = + temp_beta(a, d) * temp_matrix(a, gin, gout); } else if (beta_ndims == ndim_target + 1) { - xt::xtensor temp_beta({n_ang, n_dg_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = + tensor::zeros({n_ang, n_dg_, n_g_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - xt::xtensor temp_beta_sum({n_ang, n_g_}, 0.); - temp_beta_sum = xt::sum(temp_beta, {1}); + auto beta_sum = temp_beta.sum(1); + auto matrix_gout_sum = temp_matrix.sum(2); - // prompt_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by (1 - beta_sum) - prompt_nu_fission = xt::sum(temp_matrix, {2}) * (1. - temp_beta_sum); + // prompt_nu_fission = sum_gout(matrix) * (1 - beta_total) + // Here beta is energy-dependent, so beta_sum is 2D [n_ang, n_g] + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = + matrix_gout_sum(a, g) * (1.0 - beta_sum(a, g)); - // Store chi-prompt - chi_prompt = - xt::view(1.0 - temp_beta_sum, xt::all(), xt::all(), xt::newaxis()) * - temp_matrix; + // chi_prompt = (1 - beta_sum) * nu-fission matrix (unnormalized) + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + (1.0 - beta_sum(a, gin)) * temp_matrix(a, gin, gout); - // delayed_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by beta - delayed_nu_fission = temp_beta * xt::view(xt::sum(temp_matrix, {2}), - xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission: beta is energy-dependent [n_ang, n_dg, n_g], + // scale by total fission rate summed over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = + temp_beta(a, d, g) * matrix_gout_sum(a, g); - // Store chi-delayed - chi_delayed = - xt::view(temp_beta, xt::all(), xt::all(), xt::all(), xt::newaxis()) * - xt::view(temp_matrix, xt::all(), xt::newaxis(), xt::all(), xt::all()); + // chi_delayed = beta * nu-fission matrix, expanded across delayed groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_delayed(a, d, gin, gout) = + temp_beta(a, d, gin) * temp_matrix(a, gin, gout); } - // Normalize both chis - chi_prompt /= - xt::view(xt::sum(chi_prompt, {2}), xt::all(), xt::all(), xt::newaxis()); + // Normalize chi_prompt so it sums to 1 over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) { + tensor::View row = chi_prompt.slice(a, gin); + row /= row.sum(); + } - chi_delayed /= xt::view( - xt::sum(chi_delayed, {3}), xt::all(), xt::all(), xt::all(), xt::newaxis()); + // Normalize chi_delayed so it sums to 1 over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) { + tensor::View row = chi_delayed.slice(a, d, gin); + row /= row.sum(); + } } void XsData::fission_matrix_no_beta_from_hdf5(hid_t xsdata_grp, size_t n_ang) @@ -308,28 +378,36 @@ void XsData::fission_matrix_no_beta_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Data is provided separately as prompt + delayed nu-fission and chi // Get the prompt nu-fission matrix - xt::xtensor temp_matrix_p({n_ang, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "prompt-nu-fission", temp_matrix_p, true); + tensor::Tensor temp_matrix_p = + tensor::zeros({n_ang, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "prompt-nu-fission", temp_matrix_p, true); // prompt_nu_fission is the sum over outgoing groups - prompt_nu_fission = xt::sum(temp_matrix_p, {2}); + prompt_nu_fission = temp_matrix_p.sum(2); - // chi_prompt is this matrix but normalized over outgoing groups, which we - // have already stored in prompt_nu_fission - chi_prompt = temp_matrix_p / - xt::view(prompt_nu_fission, xt::all(), xt::all(), xt::newaxis()); + // chi_prompt is the nu-fission matrix normalized over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + temp_matrix_p(a, gin, gout) / prompt_nu_fission(a, gin); // Get the delayed nu-fission matrix - xt::xtensor temp_matrix_d({n_ang, n_dg_, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "delayed-nu-fission", temp_matrix_d, true); + tensor::Tensor temp_matrix_d = + tensor::zeros({n_ang, n_dg_, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "delayed-nu-fission", temp_matrix_d, true); // delayed_nu_fission is the sum over outgoing groups - delayed_nu_fission = xt::sum(temp_matrix_d, {3}); + delayed_nu_fission = temp_matrix_d.sum(3); - // chi_prompt is this matrix but normalized over outgoing groups, which we - // have already stored in prompt_nu_fission - chi_delayed = temp_matrix_d / xt::view(delayed_nu_fission, xt::all(), - xt::all(), xt::all(), xt::newaxis()); + // chi_delayed is the delayed nu-fission matrix normalized over outgoing + // groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_delayed(a, d, gin, gout) = + temp_matrix_d(a, d, gin, gout) / delayed_nu_fission(a, d, gin); } void XsData::fission_matrix_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) @@ -338,16 +416,19 @@ void XsData::fission_matrix_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Therefore, the code only considers the data as prompt. // Get nu-fission matrix - xt::xtensor temp_matrix({n_ang, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "nu-fission", temp_matrix, true); + tensor::Tensor temp_matrix = + tensor::zeros({n_ang, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "nu-fission", temp_matrix, true); // prompt_nu_fission is the sum over outgoing groups - prompt_nu_fission = xt::sum(temp_matrix, {2}); + prompt_nu_fission = temp_matrix.sum(2); - // chi_prompt is this matrix but normalized over outgoing groups, which we - // have already stored in prompt_nu_fission - chi_prompt = temp_matrix / - xt::view(prompt_nu_fission, xt::all(), xt::all(), xt::newaxis()); + // chi_prompt is the nu-fission matrix normalized over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + temp_matrix(a, gin, gout) / prompt_nu_fission(a, gin); } //============================================================================== @@ -356,8 +437,8 @@ void XsData::fission_from_hdf5( hid_t xsdata_grp, size_t n_ang, bool is_isotropic) { // Get the fission and kappa_fission data xs; these are optional - read_nd_vector(xsdata_grp, "fission", fission); - read_nd_vector(xsdata_grp, "kappa-fission", kappa_fission); + read_nd_tensor(xsdata_grp, "fission", fission); + read_nd_tensor(xsdata_grp, "kappa-fission", kappa_fission); // Get the data; the strategy for doing so depends on if the data is provided // as a nu-fission matrix or a set of chi and nu-fission vectors @@ -388,7 +469,7 @@ void XsData::fission_from_hdf5( if (n_dg_ == 0) { nu_fission = prompt_nu_fission; } else { - nu_fission = prompt_nu_fission + xt::sum(delayed_nu_fission, {1}); + nu_fission = prompt_nu_fission + delayed_nu_fission.sum(1); } } @@ -404,10 +485,10 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, hid_t scatt_grp = open_group(xsdata_grp, "scatter_data"); // Get the outgoing group boundary indices - xt::xtensor gmin({n_ang, n_g_}, 0.); - read_nd_vector(scatt_grp, "g_min", gmin, true); - xt::xtensor gmax({n_ang, n_g_}, 0.); - read_nd_vector(scatt_grp, "g_max", gmax, true); + tensor::Tensor gmin = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(scatt_grp, "g_min", gmin, true); + tensor::Tensor gmax = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(scatt_grp, "g_max", gmax, true); // Make gmin and gmax start from 0 vice 1 as they do in the library gmin -= 1; @@ -415,11 +496,11 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, // Now use this info to find the length of a vector to hold the flattened // data. - size_t length = order_data * xt::sum(gmax - gmin + 1)(); + size_t length = order_data * (gmax - gmin + 1).sum(); double_4dvec input_scatt(n_ang, double_3dvec(n_g_)); - xt::xtensor temp_arr({length}, 0.); - read_nd_vector(scatt_grp, "scatter_matrix", temp_arr, true); + tensor::Tensor temp_arr = tensor::zeros({length}); + read_nd_tensor(scatt_grp, "scatter_matrix", temp_arr, true); // Compare the number of orders given with the max order of the problem; // strip off the superfluous orders if needed @@ -451,7 +532,7 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, double_3dvec temp_mult(n_ang, double_2dvec(n_g_)); if (object_exists(scatt_grp, "multiplicity_matrix")) { temp_arr.resize({length / order_data}); - read_nd_vector(scatt_grp, "multiplicity_matrix", temp_arr); + read_nd_tensor(scatt_grp, "multiplicity_matrix", temp_arr); // convert the flat temp_arr to a jagged array for passing to scatt data size_t temp_idx = 0; @@ -481,8 +562,8 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, final_scatter_format == AngleDistributionType::TABULAR) { for (size_t a = 0; a < n_ang; a++) { ScattDataLegendre legendre_scatt; - xt::xtensor in_gmin = xt::view(gmin, a, xt::all()); - xt::xtensor in_gmax = xt::view(gmax, a, xt::all()); + tensor::Tensor in_gmin(gmin.slice(a)); + tensor::Tensor in_gmax(gmax.slice(a)); legendre_scatt.init(in_gmin, in_gmax, temp_mult[a], input_scatt[a]); @@ -496,8 +577,8 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, // We are sticking with the current representation // Initialize the ScattData object with this data for (size_t a = 0; a < n_ang; a++) { - xt::xtensor in_gmin = xt::view(gmin, a, xt::all()); - xt::xtensor in_gmax = xt::view(gmax, a, xt::all()); + tensor::Tensor in_gmin(gmin.slice(a)); + tensor::Tensor in_gmax(gmax.slice(a)); scatter[a]->init(in_gmin, in_gmax, temp_mult[a], input_scatt[a]); } } @@ -519,33 +600,67 @@ void XsData::combine( if (i == 0) { inverse_velocity = that->inverse_velocity; } - if (that->prompt_nu_fission.shape()[0] > 0) { + if (!that->prompt_nu_fission.empty()) { nu_fission += scalar * that->nu_fission; prompt_nu_fission += scalar * that->prompt_nu_fission; kappa_fission += scalar * that->kappa_fission; fission += scalar * that->fission; delayed_nu_fission += scalar * that->delayed_nu_fission; - chi_prompt += scalar * - xt::view(xt::sum(that->prompt_nu_fission, {1}), xt::all(), - xt::newaxis(), xt::newaxis()) * - that->chi_prompt; - chi_delayed += scalar * - xt::view(xt::sum(that->delayed_nu_fission, {2}), xt::all(), - xt::all(), xt::newaxis(), xt::newaxis()) * - that->chi_delayed; + // Accumulate chi_prompt weighted by total prompt nu-fission + // (summed over energy groups) for this constituent + { + auto pnf_sum = that->prompt_nu_fission.sum(1); + size_t n_ang = chi_prompt.shape(0); + size_t n_g = chi_prompt.shape(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g; gin++) + for (size_t gout = 0; gout < n_g; gout++) + chi_prompt(a, gin, gout) += + scalar * pnf_sum(a) * that->chi_prompt(a, gin, gout); + } + // Accumulate chi_delayed weighted by total delayed nu-fission + // (summed over energy groups) for this constituent + { + auto dnf_sum = that->delayed_nu_fission.sum(2); + size_t n_ang = chi_delayed.shape(0); + size_t n_dg = chi_delayed.shape(1); + size_t n_g = chi_delayed.shape(2); + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg; d++) + for (size_t gin = 0; gin < n_g; gin++) + for (size_t gout = 0; gout < n_g; gout++) + chi_delayed(a, d, gin, gout) += + scalar * dnf_sum(a, d) * that->chi_delayed(a, d, gin, gout); + } } decay_rate += scalar * that->decay_rate; } - // Ensure the chi_prompt and chi_delayed are normalized to 1 for each - // azimuthal angle and delayed group (for chi_delayed) - chi_prompt /= - xt::view(xt::sum(chi_prompt, {2}), xt::all(), xt::all(), xt::newaxis()); - chi_delayed /= xt::view( - xt::sum(chi_delayed, {3}), xt::all(), xt::all(), xt::all(), xt::newaxis()); + // Normalize chi_prompt so it sums to 1 over outgoing groups + { + size_t n_ang = chi_prompt.shape(0); + size_t n_g = chi_prompt.shape(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g; gin++) { + tensor::View row = chi_prompt.slice(a, gin); + row /= row.sum(); + } + } + // Normalize chi_delayed so it sums to 1 over outgoing groups + { + size_t n_ang = chi_delayed.shape(0); + size_t n_dg = chi_delayed.shape(1); + size_t n_g = chi_delayed.shape(2); + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg; d++) + for (size_t gin = 0; gin < n_g; gin++) { + tensor::View row = chi_delayed.slice(a, d, gin); + row /= row.sum(); + } + } // Allow the ScattData object to combine itself - for (size_t a = 0; a < total.shape()[0]; a++) { + for (size_t a = 0; a < total.shape(0); a++) { // Build vector of the scattering objects to incorporate vector those_scatts(those_xs.size()); for (size_t i = 0; i < those_xs.size(); i++) { diff --git a/tests/conftest.py b/tests/conftest.py index 71dd5ebf5..8dda9f756 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,51 @@ import os +import hashlib import pytest import openmc +import openmc.lib from tests.regression_tests import config as regression_config +# MD5 hash of the official NNDC HDF5 cross_sections.xml file. +# Generated via: md5sum /path/to/nndc_hdf5/cross_sections.xml +_NNDC_XS_MD5 = "2d00773012eda670bc9f95d96a31c989" + +# Collected during pytest_configure, displayed at start and end of session +_environment_warnings = [] + + +def _check_build_environment(): + """Check STRICT_FP and cross section data, collecting any warnings.""" + if not openmc.lib._strict_fp_enabled(): + _environment_warnings.append( + "OpenMC was NOT built with -DOPENMC_ENABLE_STRICT_FP=on. " + "Regression test results may not match reference values due to " + "compiler floating-point optimizations. Rebuild with " + "-DOPENMC_ENABLE_STRICT_FP=on for reproducible results." + ) + + xs_path = os.environ.get("OPENMC_CROSS_SECTIONS") + if not xs_path: + _environment_warnings.append( + "OPENMC_CROSS_SECTIONS environment variable is not set. " + "Regression tests require the NNDC HDF5 cross section data." + ) + elif not os.path.isfile(xs_path): + _environment_warnings.append( + f"OPENMC_CROSS_SECTIONS ({xs_path}) is not a valid file path. " + "Regression tests require the NNDC HDF5 cross section data." + ) + else: + with open(xs_path, "rb") as f: + md5 = hashlib.md5(f.read()).hexdigest() + if md5 != _NNDC_XS_MD5: + _environment_warnings.append( + f"OPENMC_CROSS_SECTIONS ({xs_path}) does not match the " + "official NNDC HDF5 dataset. Regression tests expect the " + "NNDC data; results may differ with other cross section " + "libraries." + ) + def pytest_addoption(parser): parser.addoption('--exe') @@ -21,6 +63,28 @@ def pytest_configure(config): if config.getoption(opt) is not None: regression_config[opt] = config.getoption(opt) + _check_build_environment() + + +def _print_environment_warnings(terminalreporter): + """Print environment warnings as a visible section.""" + if _environment_warnings: + terminalreporter.section("OpenMC Environment Warnings") + for msg in _environment_warnings: + terminalreporter.line(f"WARNING: {msg}", yellow=True) + terminalreporter.line("") + + +def pytest_sessionstart(session): + """Print environment warnings at the start of the test session.""" + _print_environment_warnings(session.config.pluginmanager.get_plugin( + "terminalreporter")) + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + """Reprint environment warnings at the end so they aren't missed.""" + _print_environment_warnings(terminalreporter) + @pytest.fixture def run_in_tmpdir(tmpdir): diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 5f87db9ea..3eb88f023 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -6,6 +6,9 @@ set(TEST_NAMES test_math test_mcpl_stat_sum test_mesh + test_ray + test_region + test_tensor # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_distribution.cpp b/tests/cpp_unit_tests/test_distribution.cpp index e1a212db5..479e7c8a7 100644 --- a/tests/cpp_unit_tests/test_distribution.cpp +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -1,4 +1,6 @@ #include "openmc/distribution.h" +#include "openmc/distribution_spatial.h" +#include "openmc/position.h" #include "openmc/random_lcg.h" #include #include @@ -28,7 +30,7 @@ TEST_CASE("Test alias method sampling of a discrete distribution") int counter = 0; for (size_t i = 0; i < n_samples; i++) { - auto sample = dist.sample(&seed); + auto sample = dist.sample(&seed).first; std += sample * sample / n_samples; dist_mean += sample; @@ -61,7 +63,7 @@ TEST_CASE("Test alias sampling method for pugixml constructor") // Initialize discrete distribution and seed openmc::Discrete dist(energy); uint64_t seed = openmc::init_seed(0, 0); - auto sample = dist.sample(&seed); + auto sample = dist.sample(&seed).first; // Assertions REQUIRE(dist.x().size() == 3); @@ -79,3 +81,139 @@ TEST_CASE("Test alias sampling method for pugixml constructor") REQUIRE(dist.alias()[i] == correct_alias[i]); } } + +TEST_CASE("Test sampling a large linear-linear tabular distribution") +{ + constexpr int n_points = 10001; + constexpr int n_samples = 200000; + openmc::vector x(n_points); + openmc::vector p(n_points); + for (int i = 0; i < n_points; ++i) { + x[i] = static_cast(i) / (n_points - 1); + p[i] = 2.0 * x[i]; + } + + openmc::Tabular dist( + x.data(), p.data(), n_points, openmc::Interpolation::lin_lin); + uint64_t seed = openmc::init_seed(0, 0); + + double mean = 0.0; + for (int i = 0; i < n_samples; ++i) { + mean += dist.sample(&seed).first; + } + mean /= n_samples; + + // The normalized PDF is 2x on [0, 1], which has a mean of 2/3. + REQUIRE_THAT(mean, Catch::Matchers::WithinAbs(2.0 / 3.0, 0.003)); +} + +TEST_CASE("Test construction of SpatialBox with parameters") +{ + openmc::Position ll {-1, -2, -3}; + openmc::Position ur {30, 15, 5}; + openmc::SpatialBox box(ll, ur); + + REQUIRE(box.lower_left() == openmc::Position {-1, -2, -3}); + REQUIRE(box.upper_right() == openmc::Position {30, 15, 5}); + REQUIRE_FALSE(box.only_fissionable()); +} + +TEST_CASE("Test Normal distribution") +{ + // Test untruncated normal distribution + openmc::Normal normal_unbounded(0.0, 1.0); + + // Check PDF at mean (should be 1/sqrt(2*pi) ≈ 0.3989) + REQUIRE_THAT( + normal_unbounded.evaluate(0.0), Catch::Matchers::WithinRel(0.3989, 0.001)); + + // Check that it's not truncated + REQUIRE_FALSE(normal_unbounded.is_truncated()); + + // Check accessors + REQUIRE(normal_unbounded.mean_value() == 0.0); + REQUIRE(normal_unbounded.std_dev() == 1.0); + REQUIRE(normal_unbounded.lower() == -openmc::INFTY); + REQUIRE(normal_unbounded.upper() == openmc::INFTY); +} + +TEST_CASE("Test truncated Normal distribution") +{ + // Create a truncated normal: mean=0, std=1, bounds=[-1, 1] + openmc::Normal normal_truncated(0.0, 1.0, -1.0, 1.0); + + // Check that it's truncated + REQUIRE(normal_truncated.is_truncated()); + + // Check accessors + REQUIRE(normal_truncated.lower() == -1.0); + REQUIRE(normal_truncated.upper() == 1.0); + + // PDF should be zero outside bounds + REQUIRE(normal_truncated.evaluate(-2.0) == 0.0); + REQUIRE(normal_truncated.evaluate(2.0) == 0.0); + + // PDF inside bounds should be higher than untruncated (due to + // renormalization) + openmc::Normal normal_unbounded(0.0, 1.0); + REQUIRE(normal_truncated.evaluate(0.0) > normal_unbounded.evaluate(0.0)); + + // The truncated PDF at mean should be approximately 0.3989 / 0.6827 ≈ 0.584 + // (0.6827 is the probability mass of N(0,1) in [-1,1]) + REQUIRE_THAT( + normal_truncated.evaluate(0.0), Catch::Matchers::WithinRel(0.584, 0.01)); +} + +TEST_CASE("Test truncated Normal sampling") +{ + constexpr int n_samples = 10000; + openmc::Normal normal_truncated(0.0, 1.0, -1.0, 1.0); + uint64_t seed = openmc::init_seed(0, 0); + + // Sample and verify all samples are within bounds + for (int i = 0; i < n_samples; ++i) { + auto [x, w] = normal_truncated.sample(&seed); + REQUIRE(x >= -1.0); + REQUIRE(x <= 1.0); + REQUIRE(w == 1.0); // Unbiased sampling should have weight 1 + } +} + +TEST_CASE("Test one-sided truncated Normal") +{ + // Test lower-bounded only (positive half-normal) + openmc::Normal lower_bounded(0.0, 1.0, 0.0, openmc::INFTY); + REQUIRE(lower_bounded.is_truncated()); + REQUIRE(lower_bounded.evaluate(-1.0) == 0.0); + REQUIRE(lower_bounded.evaluate(1.0) > 0.0); + + // PDF at 0 should be approximately 2 * 0.3989 ≈ 0.798 (half-normal) + REQUIRE_THAT( + lower_bounded.evaluate(0.0), Catch::Matchers::WithinRel(0.798, 0.01)); + + // Test upper-bounded only + openmc::Normal upper_bounded(0.0, 1.0, -openmc::INFTY, 0.0); + REQUIRE(upper_bounded.is_truncated()); + REQUIRE(upper_bounded.evaluate(1.0) == 0.0); + REQUIRE(upper_bounded.evaluate(-1.0) > 0.0); +} + +TEST_CASE("Test Normal XML constructor with truncation") +{ + // XML doc node for truncated Normal + pugi::xml_document doc; + pugi::xml_node energy = doc.append_child("energy"); + energy.append_child("type") + .append_child(pugi::node_pcdata) + .set_value("normal"); + energy.append_child("parameters") + .append_child(pugi::node_pcdata) + .set_value("1.0e6 1.0e5 0.8e6 1.2e6"); + + openmc::Normal dist(energy); + REQUIRE(dist.mean_value() == 1.0e6); + REQUIRE(dist.std_dev() == 1.0e5); + REQUIRE(dist.lower() == 0.8e6); + REQUIRE(dist.upper() == 1.2e6); + REQUIRE(dist.is_truncated()); +} diff --git a/tests/cpp_unit_tests/test_math.cpp b/tests/cpp_unit_tests/test_math.cpp index 1ad7c4b70..7467aa1fe 100644 --- a/tests/cpp_unit_tests/test_math.cpp +++ b/tests/cpp_unit_tests/test_math.cpp @@ -46,6 +46,21 @@ TEST_CASE("Test t_percentile") } } +TEST_CASE("Test cylindrical Bessel functions") +{ + constexpr double x = 2.0; + constexpr double expected[] {0.22389077914123567, 0.57672480775687339, + 0.35283402861563773, 0.12894324947440205}; + + for (int n = 0; n < 4; ++n) { + REQUIRE_THAT(openmc::cyl_bessel_j(n, x), + Catch::Matchers::WithinRel(expected[n], 1.0e-14)); + REQUIRE_THAT(openmc::cyl_bessel_j(n, -x), + Catch::Matchers::WithinRel( + (n % 2 == 0 ? 1.0 : -1.0) * expected[n], 1.0e-14)); + } +} + TEST_CASE("Test calc_pn") { // The reference solutions come from scipy.special.eval_legendre @@ -355,3 +370,24 @@ TEST_CASE("Test broaden_wmp_polynomials") REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); } } + +TEST_CASE("Test isclose") +{ + using openmc::isclose; + + // Identical values are always close, regardless of tolerances. + REQUIRE(isclose(1.0, 1.0, 0.0, 0.0)); + REQUIRE(isclose(0.0, 0.0, 0.0, 0.0)); + + // Absolute tolerance governs comparisons near zero. + REQUIRE(isclose(0.0, 1e-15, 0.0, 1e-14)); + REQUIRE_FALSE(isclose(0.0, 1e-13, 0.0, 1e-14)); + + // Relative tolerance scales with the magnitude of the operands. + REQUIRE(isclose(1.0e12, 1.0e12 + 1.0, 1e-12, 0.0)); + REQUIRE_FALSE(isclose(1.0e12, 1.0e12 + 10.0, 1e-12, 0.0)); + + // The looser of the two tolerances wins. + REQUIRE(isclose(1.0, 1.0 + 1e-13, 0.0, 1e-12)); + REQUIRE(isclose(1.0e6, 1.0e6 + 1e-4, 1e-9, 0.0)); +} diff --git a/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp index 909830e03..d0c26f26c 100644 --- a/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp +++ b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp @@ -25,7 +25,7 @@ TEST_CASE("MCPL stat:sum field") // Initialize test particles for (int i = 0; i < 100; ++i) { - source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].particle = openmc::ParticleType::neutron(); source_bank[i].r = {i * 0.1, i * 0.2, i * 0.3}; source_bank[i].u = {0.0, 0.0, 1.0}; source_bank[i].E = 2.0e6; // 2 MeV @@ -63,7 +63,7 @@ TEST_CASE("MCPL stat:sum field") // Initialize particles for (int i = 0; i < count; ++i) { - source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].particle = openmc::ParticleType::neutron(); source_bank[i].r = {0.0, 0.0, 0.0}; source_bank[i].u = {0.0, 0.0, 1.0}; source_bank[i].E = 1.0e6; diff --git a/tests/cpp_unit_tests/test_ray.cpp b/tests/cpp_unit_tests/test_ray.cpp new file mode 100644 index 000000000..c043c4d25 --- /dev/null +++ b/tests/cpp_unit_tests/test_ray.cpp @@ -0,0 +1,117 @@ +#include +#include + +#include +#include +#include + +#include "openmc/geometry.h" +#include "openmc/geometry_aux.h" +#include "openmc/ray.h" +#include "openmc/settings.h" +#include "openmc/surface.h" + +namespace { + +using Catch::Matchers::WithinAbs; + +constexpr double DISTANCE_TOLERANCE = 1.0e-12; + +class SphericalShellFixture { +public: + SphericalShellFixture() + : run_mode_(openmc::settings::run_mode), + root_universe_(openmc::model::root_universe), + n_coord_levels_(openmc::model::n_coord_levels) + { + openmc::settings::run_mode = openmc::RunMode::PLOTTING; + + constexpr auto geometry = R"( + + + + + + + )"; + + auto result = document_.load_string(geometry); + REQUIRE(result); + openmc::read_geometry_xml(document_.document_element()); + openmc::finalize_geometry(); + openmc::finalize_cell_densities(); + } + + ~SphericalShellFixture() + { + openmc::free_memory_geometry(); + openmc::free_memory_surfaces(); + openmc::model::universe_level_counts.clear(); + openmc::model::root_universe = root_universe_; + openmc::model::n_coord_levels = n_coord_levels_; + openmc::settings::run_mode = run_mode_; + } + +private: + pugi::xml_document document_; + openmc::RunMode run_mode_; + int root_universe_; + int n_coord_levels_; +}; + +class RecordingRay : public openmc::Ray { +public: + using Ray::Ray; + + void on_intersection() override + { + intersections.emplace_back( + traversal_distance_, boundary().surface_index() + 1); + } + + openmc::vector> intersections; +}; + +} // namespace + +TEST_CASE_METHOD(SphericalShellFixture, "Trace a single chord through a sphere") +{ + constexpr double impact_parameter = 1.5; + const double half_chord = + std::sqrt(4.0 - impact_parameter * impact_parameter); + + RecordingRay ray({-3.0, impact_parameter, 0.0}, {1.0, 0.0, 0.0}); + ray.trace(); + + REQUIRE(ray.intersections.size() == 2); + CHECK(ray.intersections[0].second == 2); + CHECK_THAT(ray.intersections[0].first, WithinAbs(0.0, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[1].second == 2); + CHECK_THAT(ray.intersections[1].first, + WithinAbs(2.0 * half_chord, DISTANCE_TOLERANCE)); +} + +TEST_CASE_METHOD( + SphericalShellFixture, "Trace both segments of a spherical shell") +{ + constexpr double impact_parameter = 0.5; + const double outer = std::sqrt(4.0 - impact_parameter * impact_parameter); + const double inner = std::sqrt(1.0 - impact_parameter * impact_parameter); + + RecordingRay ray({-3.0, impact_parameter, 0.0}, {1.0, 0.0, 0.0}); + ray.trace(); + + REQUIRE(ray.intersections.size() == 4); + CHECK(ray.intersections[0].second == 2); + CHECK_THAT(ray.intersections[0].first, WithinAbs(0.0, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[1].second == 1); + CHECK_THAT( + ray.intersections[1].first, WithinAbs(outer - inner, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[2].second == 1); + CHECK_THAT( + ray.intersections[2].first, WithinAbs(outer + inner, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[3].second == 2); + CHECK_THAT( + ray.intersections[3].first, WithinAbs(2.0 * outer, DISTANCE_TOLERANCE)); +} diff --git a/tests/cpp_unit_tests/test_region.cpp b/tests/cpp_unit_tests/test_region.cpp new file mode 100644 index 000000000..b3d9a1427 --- /dev/null +++ b/tests/cpp_unit_tests/test_region.cpp @@ -0,0 +1,101 @@ +#include + +#include "openmc/cell.h" +#include "openmc/surface.h" + +#include + +namespace { + +// Helper class to set up and tear down test surfaces +class SurfaceFixture { +public: + SurfaceFixture() + { + pugi::xml_document doc; + pugi::xml_node surf_node = doc.append_child("surface"); + surf_node.set_name("surface"); + surf_node.append_attribute("id") = "0"; + surf_node.append_attribute("type") = "x-plane"; + surf_node.append_attribute("coeffs") = "1"; + + for (int i = 1; i < 10; ++i) { + surf_node.attribute("id") = i; + openmc::model::surfaces.push_back( + std::make_unique(surf_node)); + openmc::model::surface_map[i] = i - 1; + } + } + + ~SurfaceFixture() + { + openmc::model::surfaces.clear(); + openmc::model::surface_map.clear(); + } +}; + +} // anonymous namespace + +TEST_CASE("Test region simplification") +{ + SurfaceFixture fixture; + + SECTION("Original bug case from issue #3685") + { + // Input: "-1 2 (-3 4) | (-5 6)" was being incorrectly interpreted + auto region = openmc::Region("(-1 2 (-3 4) | (-5 6))", 0); + REQUIRE(region.str() == " ( ( -1 2 ( -3 4 ) ) | ( -5 6 ) )"); + } + + SECTION("Simple union - no extra parentheses needed") + { + auto region = openmc::Region("1 | 2", 0); + REQUIRE(region.str() == " 1 | 2"); + } + + SECTION("Intersection then union") + { + // Intersection should have higher precedence, so (1 2) grouped + auto region = openmc::Region("1 2 | 3", 0); + REQUIRE(region.str() == " ( 1 2 ) | 3"); + } + + SECTION("Union then intersection") + { + // The (2 3) intersection should be grouped + auto region = openmc::Region("1 | 2 3", 0); + REQUIRE(region.str() == " 1 | ( 2 3 )"); + } + + SECTION("Nested parentheses preserved") + { + // These parentheses are meaningful and should be preserved + auto region = openmc::Region("(1 | 2) (3 | 4)", 0); + REQUIRE(region.str() == " ( 1 | 2 ) ( 3 | 4 )"); + } + + SECTION("Deep nesting") + { + auto region = openmc::Region("((1 2) | (3 4)) 5", 0); + REQUIRE(region.str() == " ( ( 1 2 ) | ( 3 4 ) ) 5"); + } + + SECTION("Multiple unions") + { + auto region = openmc::Region("1 | 2 | 3", 0); + REQUIRE(region.str() == " 1 | 2 | 3"); + } + + SECTION("Multiple intersections") + { + auto region = openmc::Region("1 2 3", 0); + // Simple cell - no operators in output + REQUIRE(region.str() == " 1 2 3"); + } + + SECTION("Complex mixed expression") + { + auto region = openmc::Region("1 2 | 3 4 | 5 6", 0); + REQUIRE(region.str() == " ( 1 2 ) | ( 3 4 ) | ( 5 6 )"); + } +} diff --git a/tests/cpp_unit_tests/test_tally.cpp b/tests/cpp_unit_tests/test_tally.cpp index 964d30cc4..03322e764 100644 --- a/tests/cpp_unit_tests/test_tally.cpp +++ b/tests/cpp_unit_tests/test_tally.cpp @@ -1,3 +1,4 @@ +#include "openmc/tallies/filter_energy.h" #include "openmc/tallies/tally.h" #include @@ -46,10 +47,34 @@ TEST_CASE("Test add/set_filter") REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); REQUIRE(model::filter_map[particle_filter->id()] == tally->filters(1)); - // set filters with a duplicate filter, should only add the filter to the tally once + // set filters with a duplicate filter, should only add the filter to the + // tally once filters = {cell_filter, cell_filter}; tally->set_filters(filters); REQUIRE(tally->filters().size() == 1); REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); +} +// Regression test for 64-bit tally filter-bin counts (mesh x groups > 2^31). +TEST_CASE("Tally filter-bin count does not overflow 32 bits") +{ + // Two energy filters whose bin counts multiply to 2.5e9, above INT32_MAX. + constexpr int64_t bins_per_filter = 50000; + + // Only the bin count matters here, so the edge values are an arbitrary ramp. + std::vector edges(bins_per_filter + 1); + for (int64_t i = 0; i < bins_per_filter + 1; ++i) + edges[i] = static_cast(i); + + Tally* tally = Tally::create(); + for (int i = 0; i < 2; ++i) { + Filter* filter = Filter::create("energy"); + dynamic_cast(filter)->set_bins(edges); + tally->add_filter(filter); + } + tally->set_strides(); + + // set_strides() previously accumulated this product in a 32-bit int. + REQUIRE(tally->n_filter_bins() == bins_per_filter * bins_per_filter); + REQUIRE(tally->n_filter_bins() > 2147483647); } \ No newline at end of file diff --git a/tests/cpp_unit_tests/test_tensor.cpp b/tests/cpp_unit_tests/test_tensor.cpp new file mode 100644 index 000000000..0936d866a --- /dev/null +++ b/tests/cpp_unit_tests/test_tensor.cpp @@ -0,0 +1,1008 @@ +#include +#include + +#include +#include + +#include "openmc/tensor.h" + +using namespace openmc; +using namespace openmc::tensor; + +// ============================================================================ +// Tensor constructors +// ============================================================================ + +TEST_CASE("Tensor default constructor") +{ + Tensor t; + REQUIRE(t.size() == 0); + REQUIRE(t.empty()); + REQUIRE(t.shape().empty()); +} + +TEST_CASE("Tensor shape constructor") +{ + Tensor t1({5}); + REQUIRE(t1.size() == 5); + REQUIRE(t1.shape().size() == 1); + REQUIRE(t1.shape(0) == 5); + + Tensor t2({3, 4}); + REQUIRE(t2.size() == 12); + REQUIRE(t2.shape().size() == 2); + REQUIRE(t2.shape(0) == 3); + REQUIRE(t2.shape(1) == 4); + + Tensor t3({2, 3, 4}); + REQUIRE(t3.size() == 24); + REQUIRE(t3.shape().size() == 3); +} + +TEST_CASE("Tensor shape + fill constructor") +{ + Tensor t({2, 3}, 7.0); + REQUIRE(t.size() == 6); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t[i] == 7.0); +} + +TEST_CASE("Tensor pointer constructor") +{ + double vals[] = {1.0, 2.0, 3.0, 4.0}; + Tensor t(vals, 4); + REQUIRE(t.size() == 4); + REQUIRE(t.shape(0) == 4); + REQUIRE(t[0] == 1.0); + REQUIRE(t[1] == 2.0); + REQUIRE(t[2] == 3.0); + REQUIRE(t[3] == 4.0); +} + +TEST_CASE("Tensor copy and move") +{ + Tensor a({2, 3}, 5.0); + Tensor b(a); + REQUIRE(b.size() == 6); + REQUIRE(b(0, 0) == 5.0); + // Modifying copy doesn't affect original + b(0, 0) = 99.0; + REQUIRE(a(0, 0) == 5.0); + + Tensor c(std::move(b)); + REQUIRE(c(0, 0) == 99.0); + REQUIRE(c.size() == 6); +} + +// ============================================================================ +// Tensor indexing +// ============================================================================ + +TEST_CASE("Tensor 1D indexing") +{ + Tensor t({4}, 0); + t[0] = 10; + t[1] = 20; + t[2] = 30; + t[3] = 40; + REQUIRE(t(0) == 10); + REQUIRE(t(1) == 20); + REQUIRE(t(2) == 30); + REQUIRE(t(3) == 40); +} + +TEST_CASE("Tensor 2D indexing (row-major)") +{ + // Layout: [[1, 2, 3], [4, 5, 6]] + Tensor t({2, 3}, 0); + int val = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = val++; + + REQUIRE(t(0, 0) == 1); + REQUIRE(t(0, 2) == 3); + REQUIRE(t(1, 0) == 4); + REQUIRE(t(1, 2) == 6); + // Flat index should match row-major order + REQUIRE(t[0] == 1); + REQUIRE(t[3] == 4); + REQUIRE(t[5] == 6); +} + +TEST_CASE("Tensor 3D indexing") +{ + // 2x3x4 tensor + Tensor t({2, 3, 4}, 0); + t(1, 2, 3) = 42; + // Flat index: 1*12 + 2*4 + 3 = 23 + REQUIRE(t[23] == 42); + REQUIRE(t(1, 2, 3) == 42); +} + +// ============================================================================ +// Tensor assignment +// ============================================================================ + +TEST_CASE("Tensor initializer_list assignment") +{ + Tensor t; + t = {1.0, 2.0, 3.0}; + REQUIRE(t.size() == 3); + REQUIRE(t.shape(0) == 3); + REQUIRE(t[0] == 1.0); + REQUIRE(t[2] == 3.0); +} + +// ============================================================================ +// Tensor mutation +// ============================================================================ + +TEST_CASE("Tensor resize") +{ + Tensor t({2, 3}, 1.0); + REQUIRE(t.size() == 6); + t.resize({4, 5}); + REQUIRE(t.size() == 20); + REQUIRE(t.shape(0) == 4); + REQUIRE(t.shape(1) == 5); +} + +TEST_CASE("Tensor reshape") +{ + Tensor t({12}, 0); + for (size_t i = 0; i < 12; ++i) + t[i] = static_cast(i); + + t.reshape({3, 4}); + REQUIRE(t.shape(0) == 3); + REQUIRE(t.shape(1) == 4); + REQUIRE(t.size() == 12); + // Data unchanged, just reinterpreted + REQUIRE(t(0, 0) == 0); + REQUIRE(t(1, 0) == 4); // row 1, col 0 = flat index 4 + REQUIRE(t(2, 3) == 11); // row 2, col 3 = flat index 11 +} + +TEST_CASE("Tensor fill") +{ + Tensor t({3, 3}, 0.0); + t.fill(42.0); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t[i] == 42.0); +} + +// ============================================================================ +// Tensor iterators +// ============================================================================ + +TEST_CASE("Tensor iterators") +{ + Tensor t({4}, 0); + t = {10, 20, 30, 40}; + int sum = 0; + for (auto val : t) + sum += val; + REQUIRE(sum == 100); +} + +// ============================================================================ +// Tensor reductions +// ============================================================================ + +TEST_CASE("Tensor sum (full)") +{ + Tensor t({3}, 0.0); + t = {1.0, 2.0, 3.0}; + REQUIRE(t.sum() == 6.0); +} + +TEST_CASE("Tensor sum (axis) on 2D") +{ + // [[1, 2, 3], + // [4, 5, 6]] + Tensor t({2, 3}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + // Sum along axis 0 -> [5, 7, 9] + Tensor s0 = t.sum(0); + REQUIRE(s0.size() == 3); + REQUIRE(s0[0] == 5); + REQUIRE(s0[1] == 7); + REQUIRE(s0[2] == 9); + + // Sum along axis 1 -> [6, 15] + Tensor s1 = t.sum(1); + REQUIRE(s1.size() == 2); + REQUIRE(s1[0] == 6); + REQUIRE(s1[1] == 15); +} + +TEST_CASE("Tensor sum (axis) on 3D") +{ + // 2x3x2 tensor filled with sequential values 1..12 + Tensor t({2, 3, 2}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + for (size_t k = 0; k < 2; ++k) + t(i, j, k) = v++; + + // Sum along axis 1 (middle) -> 2x2, each sums 3 values + // [0,0]: t(0,0,0)+t(0,1,0)+t(0,2,0) = 1+3+5 = 9 + // [0,1]: t(0,0,1)+t(0,1,1)+t(0,2,1) = 2+4+6 = 12 + // [1,0]: t(1,0,0)+t(1,1,0)+t(1,2,0) = 7+9+11 = 27 + // [1,1]: t(1,0,1)+t(1,1,1)+t(1,2,1) = 8+10+12 = 30 + Tensor s = t.sum(1); + REQUIRE(s.shape(0) == 2); + REQUIRE(s.shape(1) == 2); + REQUIRE(s(0, 0) == 9); + REQUIRE(s(0, 1) == 12); + REQUIRE(s(1, 0) == 27); + REQUIRE(s(1, 1) == 30); +} + +TEST_CASE("Tensor prod") +{ + Tensor t({4}, 0); + t = {1, 2, 3, 4}; + REQUIRE(t.prod() == 24); +} + +TEST_CASE("Tensor any and all") +{ + Tensor t({4}, false); + REQUIRE(!t.any()); + REQUIRE(!t.all()); + + // Set one element true + t.data()[0] = true; + REQUIRE(t.any()); + REQUIRE(!t.all()); + + // Set all true + for (size_t i = 0; i < t.size(); ++i) + t.data()[i] = true; + REQUIRE(t.any()); + REQUIRE(t.all()); +} + +TEST_CASE("Tensor argmin") +{ + Tensor t({5}, 0.0); + t = {3.0, 1.0, 4.0, 0.5, 2.0}; + REQUIRE(t.argmin() == 3); +} + +TEST_CASE("Tensor flip") +{ + Tensor t({5}, 0); + t = {1, 2, 3, 4, 5}; + Tensor f = t.flip(0); + REQUIRE(f[0] == 5); + REQUIRE(f[1] == 4); + REQUIRE(f[2] == 3); + REQUIRE(f[3] == 2); + REQUIRE(f[4] == 1); +} + +TEST_CASE("Tensor flip 2D") +{ + // [[1, 2], [3, 4], [5, 6]] + Tensor t({3, 2}, 0); + t(0, 0) = 1; + t(0, 1) = 2; + t(1, 0) = 3; + t(1, 1) = 4; + t(2, 0) = 5; + t(2, 1) = 6; + + // Flip axis 0 reverses rows -> [[5,6],[3,4],[1,2]] + Tensor f = t.flip(0); + REQUIRE(f(0, 0) == 5); + REQUIRE(f(0, 1) == 6); + REQUIRE(f(1, 0) == 3); + REQUIRE(f(2, 0) == 1); +} + +// ============================================================================ +// Tensor operators +// ============================================================================ + +TEST_CASE("Tensor scalar compound assignment") +{ + Tensor t({3}, 0.0); + t = {2.0, 4.0, 6.0}; + + t += 1.0; + REQUIRE(t[0] == 3.0); + REQUIRE(t[1] == 5.0); + + t -= 1.0; + REQUIRE(t[0] == 2.0); + + t *= 3.0; + REQUIRE(t[0] == 6.0); + REQUIRE(t[1] == 12.0); + + t /= 2.0; + REQUIRE(t[0] == 3.0); + REQUIRE(t[1] == 6.0); +} + +TEST_CASE("Tensor element-wise arithmetic") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0.0); + a = {1.0, 2.0, 3.0}; + b = {4.0, 5.0, 6.0}; + + Tensor c = a + b; + REQUIRE(c[0] == 5.0); + REQUIRE(c[1] == 7.0); + REQUIRE(c[2] == 9.0); + + c = a - b; + REQUIRE(c[0] == -3.0); + + c = a / b; + REQUIRE(c[0] == 0.25); + + c = a * b; + REQUIRE(c[0] == 4.0); +} + +TEST_CASE("Tensor scalar arithmetic") +{ + Tensor a({3}, 0.0); + a = {1.0, 2.0, 3.0}; + + Tensor b = a + 10.0; + REQUIRE(b[0] == 11.0); + REQUIRE(b[2] == 13.0); + + b = a - 1.0; + REQUIRE(b[0] == 0.0); + + b = a * 2.0; + REQUIRE(b[0] == 2.0); + REQUIRE(b[2] == 6.0); + + // Non-member scalar * tensor (commutativity) + b = 2.0 * a; + REQUIRE(b[0] == 2.0); + REQUIRE(b[2] == 6.0); + + // Non-member scalar + tensor + b = 10.0 + a; + REQUIRE(b[0] == 11.0); +} + +TEST_CASE("Tensor compound arithmetic with tensor") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0.0); + a = {1.0, 2.0, 3.0}; + b = {10.0, 20.0, 30.0}; + a += b; + REQUIRE(a[0] == 11.0); + REQUIRE(a[1] == 22.0); + REQUIRE(a[2] == 33.0); + + a = {1.0, 2.0, 3.0}; + b -= a; + REQUIRE(b[0] == 9.0); + REQUIRE(b[1] == 18.0); + REQUIRE(b[2] == 27.0); + + b = {10.0, 20.0, 30.0}; + a *= b; + REQUIRE(a[0] == 10.0); + REQUIRE(a[1] == 40.0); + REQUIRE(a[2] == 90.0); + + a = {1.0, 2.0, 3.0}; + b /= a; + REQUIRE(b[0] == 10.0); + REQUIRE(b[1] == 10.0); + REQUIRE(b[2] == 10.0); +} + +TEST_CASE("Tensor comparison operators") +{ + Tensor t({4}, 0.0); + t = {1.0, 2.0, 3.0, 4.0}; + + Tensor r = t < 3.0; + REQUIRE(r.data()[0] == true); + REQUIRE(r.data()[1] == true); + REQUIRE(r.data()[2] == false); + REQUIRE(r.data()[3] == false); + + r = t >= 3.0; + REQUIRE(r.data()[0] == false); + REQUIRE(r.data()[2] == true); + REQUIRE(r.data()[3] == true); + + r = t <= 2.0; + REQUIRE(r.data()[0] == true); + REQUIRE(r.data()[1] == true); + REQUIRE(r.data()[2] == false); + + r = t > 3.0; + REQUIRE(r.data()[0] == false); + REQUIRE(r.data()[3] == true); +} + +TEST_CASE("Tensor element-wise comparison") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0.0); + a = {1.0, 5.0, 3.0}; + b = {2.0, 4.0, 3.0}; + + Tensor r = a < b; + REQUIRE(r.data()[0] == true); + REQUIRE(r.data()[1] == false); + REQUIRE(r.data()[2] == false); +} + +TEST_CASE("Tensor mixed-type multiply") +{ + Tensor a({3}, 0); + Tensor b({3}, 0.0); + a = {2, 3, 4}; + b = {1.5, 2.5, 3.5}; + + Tensor c = a * b; + REQUIRE(c[0] == 3.0); + REQUIRE(c[1] == 7.5); + REQUIRE(c[2] == 14.0); +} + +TEST_CASE("Tensor mixed-type divide") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0); + a = {10.0, 20.0, 30.0}; + b = {2, 4, 5}; + + Tensor c = a / b; + REQUIRE(c[0] == 5.0); + REQUIRE(c[1] == 5.0); + REQUIRE(c[2] == 6.0); +} + +// ============================================================================ +// Tensor bool specialization +// ============================================================================ + +TEST_CASE("Tensor storage") +{ + // Tensor uses unsigned char internally to avoid std::vector proxy + Tensor t({4}, false); + t.data()[0] = true; + t.data()[2] = true; + REQUIRE(t.any()); + REQUIRE(!t.all()); + REQUIRE(t.data()[0] == true); + REQUIRE(t.data()[1] == false); +} + +// ============================================================================ +// View (via Tensor accessors) +// ============================================================================ + +TEST_CASE("Tensor slice axis 0 (2D)") +{ + // [[1, 2, 3], [4, 5, 6]] + Tensor t({2, 3}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + auto r0 = t.slice(0); + REQUIRE(r0.size() == 3); + REQUIRE(r0[0] == 1); + REQUIRE(r0[1] == 2); + REQUIRE(r0[2] == 3); + + auto r1 = t.slice(1); + REQUIRE(r1[0] == 4); + REQUIRE(r1[1] == 5); + REQUIRE(r1[2] == 6); + + // Writing through view modifies the tensor + r0[1] = 99; + REQUIRE(t(0, 1) == 99); +} + +TEST_CASE("Tensor slice axis 1 (2D)") +{ + // [[1, 2], [3, 4], [5, 6]] + Tensor t({3, 2}, 0); + t(0, 0) = 1; + t(0, 1) = 2; + t(1, 0) = 3; + t(1, 1) = 4; + t(2, 0) = 5; + t(2, 1) = 6; + + auto c0 = t.slice(all, 0); + REQUIRE(c0.size() == 3); + REQUIRE(c0[0] == 1); + REQUIRE(c0[1] == 3); + REQUIRE(c0[2] == 5); + + auto c1 = t.slice(all, 1); + REQUIRE(c1[0] == 2); + REQUIRE(c1[1] == 4); + REQUIRE(c1[2] == 6); + + // Write through column view + c1[0] = 77; + REQUIRE(t(0, 1) == 77); +} + +TEST_CASE("Tensor slice with range") +{ + Tensor t({6}, 0); + t = {10, 20, 30, 40, 50, 60}; + + // range(start, end) + auto s = t.slice(range(1, 4)); + REQUIRE(s.size() == 3); + REQUIRE(s[0] == 20); + REQUIRE(s[1] == 30); + REQUIRE(s[2] == 40); + + // range(end) from start — range(3) means [0, 3) + auto s2 = t.slice(range(3)); + REQUIRE(s2.size() == 3); + REQUIRE(s2[0] == 10); + REQUIRE(s2[2] == 30); + + // range(start, SIZE_MAX) to end + auto s3 = t.slice(range(3, 6)); + REQUIRE(s3.size() == 3); + REQUIRE(s3[0] == 40); + REQUIRE(s3[2] == 60); + + // Write through slice + s[0] = 99; + REQUIRE(t[1] == 99); +} + +TEST_CASE("Tensor flat view") +{ + Tensor t({2, 3}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + auto f = t.flat(); + REQUIRE(f.size() == 6); + REQUIRE(f[0] == 1); + REQUIRE(f[5] == 6); +} + +TEST_CASE("Tensor slice on 3D") +{ + // 2x3x4 tensor + Tensor t({2, 3, 4}, 0); + int v = 0; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + for (size_t k = 0; k < 4; ++k) + t(i, j, k) = v++; + + // slice(1) -> fix axis 0 at 1 -> 3x4 view + auto s = t.slice(1); + REQUIRE(s.size() == 12); + // t(1,0,0) = 12, t(1,0,1) = 13, ... + REQUIRE(s(0, 0) == 12); + REQUIRE(s(0, 1) == 13); + REQUIRE(s(2, 3) == 23); + + // slice(all, 2) -> fix axis 1 at 2 -> 2x4 view + auto s2 = t.slice(all, 2); + REQUIRE(s2.size() == 8); + // t(0,2,0)=8, t(0,2,1)=9, t(1,2,0)=20 + REQUIRE(s2(0, 0) == 8); + REQUIRE(s2(0, 1) == 9); + REQUIRE(s2(1, 0) == 20); +} + +TEST_CASE("Tensor multi-axis slice") +{ + // 2x3x4 tensor with sequential values + Tensor t({2, 3, 4}, 0); + int v = 0; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + for (size_t k = 0; k < 4; ++k) + t(i, j, k) = v++; + + // slice(1, 2) -> fix axes 0 and 1 -> 1D view of 4 elements + // Equivalent to numpy t[1, 2, :] -> t(1,2,0..3) = [20, 21, 22, 23] + auto s = t.slice(1, 2); + REQUIRE(s.size() == 4); + REQUIRE(s[0] == 20); + REQUIRE(s[1] == 21); + REQUIRE(s[3] == 23); + + // slice(all, 1, range(1, 3)) -> keep axis 0, fix axis 1, range on axis 2 + // Equivalent to numpy t[:, 1, 1:3] + // t(0,1,1)=5, t(0,1,2)=6, t(1,1,1)=17, t(1,1,2)=18 + auto s2 = t.slice(all, 1, range(1, 3)); + REQUIRE(s2.size() == 4); + REQUIRE(s2.ndim() == 2); + REQUIRE(s2(0, 0) == 5); + REQUIRE(s2(0, 1) == 6); + REQUIRE(s2(1, 0) == 17); + REQUIRE(s2(1, 1) == 18); + + // slice(0, range(0, 2)) -> fix axis 0 at 0, range on axis 1 + // Equivalent to numpy t[0, 0:2, :] -> shape (2, 4) + auto s3 = t.slice(0, range(0, 2)); + REQUIRE(s3.ndim() == 2); + REQUIRE(s3.shape(0) == 2); + REQUIRE(s3.shape(1) == 4); + REQUIRE(s3(0, 0) == 0); // t(0,0,0) + REQUIRE(s3(1, 3) == 7); // t(0,1,3) +} + +// ============================================================================ +// View assignment and arithmetic +// ============================================================================ + +TEST_CASE("View scalar assignment (fill)") +{ + Tensor t({2, 3}, 0.0); + auto r = t.slice(0); + r = 7.0; + REQUIRE(t(0, 0) == 7.0); + REQUIRE(t(0, 1) == 7.0); + REQUIRE(t(0, 2) == 7.0); + REQUIRE(t(1, 0) == 0.0); // Other row unchanged +} + +TEST_CASE("View initializer_list assignment") +{ + Tensor t({2, 3}, 0.0); + auto r = t.slice(1); + r = {10.0, 20.0, 30.0}; + REQUIRE(t(1, 0) == 10.0); + REQUIRE(t(1, 1) == 20.0); + REQUIRE(t(1, 2) == 30.0); +} + +TEST_CASE("View copy assignment (deep copy)") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + t.slice(1) = {4.0, 5.0, 6.0}; + + // Copy row 0 into row 1 + t.slice(1) = t.slice(0); + REQUIRE(t(1, 0) == 1.0); + REQUIRE(t(1, 1) == 2.0); + REQUIRE(t(1, 2) == 3.0); +} + +TEST_CASE("View compound operators") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + + t.slice(0) *= 2.0; + REQUIRE(t(0, 0) == 2.0); + REQUIRE(t(0, 1) == 4.0); + + t.slice(0) /= 2.0; + REQUIRE(t(0, 0) == 1.0); + REQUIRE(t(0, 1) == 2.0); +} + +TEST_CASE("View assignment from tensor") +{ + Tensor t({2, 3}, 0.0); + Tensor vals({3}, 0.0); + vals = {7.0, 8.0, 9.0}; + + t.slice(1) = vals; + REQUIRE(t(1, 0) == 7.0); + REQUIRE(t(1, 1) == 8.0); + REQUIRE(t(1, 2) == 9.0); +} + +TEST_CASE("View compound addition from tensor") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + Tensor vals({3}, 0.0); + vals = {10.0, 20.0, 30.0}; + + t.slice(0) += vals; + REQUIRE(t(0, 0) == 11.0); + REQUIRE(t(0, 1) == 22.0); + REQUIRE(t(0, 2) == 33.0); +} + +TEST_CASE("View sum") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + t.slice(1) = {4.0, 5.0, 6.0}; + + REQUIRE(t.slice(0).sum() == 6.0); + REQUIRE(t.slice(1).sum() == 15.0); +} + +TEST_CASE("View iteration") +{ + Tensor t({2, 3}, 0); + t.slice(0) = {1, 2, 3}; + + int sum = 0; + for (auto val : t.slice(0)) + sum += val; + REQUIRE(sum == 6); +} + +TEST_CASE("View sub-slice") +{ + Tensor t({6}, 0); + t = {10, 20, 30, 40, 50, 60}; + + auto s = t.slice(range(1, 5)); // [20, 30, 40, 50] + auto ss = s.slice(range(1, 3)); // [30, 40] + REQUIRE(ss.size() == 2); + REQUIRE(ss[0] == 30); + REQUIRE(ss[1] == 40); +} + +TEST_CASE("Tensor from View") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + + // Construct a new tensor from a view (copies data) + Tensor t2(t.slice(0)); + REQUIRE(t2.size() == 3); + REQUIRE(t2[0] == 1.0); + REQUIRE(t2[2] == 3.0); + + // Modifying the new tensor doesn't affect the original + t2[0] = 99.0; + REQUIRE(t(0, 0) == 1.0); +} + +// ============================================================================ +// Const View +// ============================================================================ + +TEST_CASE("Const tensor produces const views") +{ + Tensor t({2, 3}, 0.0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + const Tensor& ct = t; + auto r = ct.slice(0); // View + REQUIRE(r[0] == 1.0); + REQUIRE(r[2] == 3.0); + + auto c = ct.slice(all, 1); + REQUIRE(c[0] == 2.0); + REQUIRE(c[1] == 5.0); +} + +// ============================================================================ +// StaticTensor2D +// ============================================================================ + +TEST_CASE("StaticTensor2D basics") +{ + StaticTensor2D t; + REQUIRE(t.size() == 12); + REQUIRE(t.shape()[0] == 3); + REQUIRE(t.shape()[1] == 4); + + // Default-initialized to zero + REQUIRE(t(0, 0) == 0.0); + + t(1, 2) = 42.0; + REQUIRE(t(1, 2) == 42.0); + // Flat data: row 1, col 2 = index 1*4 + 2 = 6 + REQUIRE(t.data()[6] == 42.0); +} + +TEST_CASE("StaticTensor2D fill") +{ + StaticTensor2D t; + t.fill(5); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t.data()[i] == 5); +} + +TEST_CASE("StaticTensor2D iteration") +{ + StaticTensor2D t; + t.fill(1); + int sum = 0; + for (auto val : t) + sum += val; + REQUIRE(sum == 6); +} + +TEST_CASE("StaticTensor2D slice") +{ + StaticTensor2D t; + t(0, 0) = 1; + t(0, 1) = 2; + t(1, 0) = 3; + t(1, 1) = 4; + t(2, 0) = 5; + t(2, 1) = 6; + + // slice(1) = row 1 (fix axis 0 at 1) + auto r1 = t.slice(1); + REQUIRE(r1.size() == 2); + REQUIRE(r1[0] == 3); + REQUIRE(r1[1] == 4); + + // slice(all, 0) = column 0 (fix axis 1 at 0) + auto c0 = t.slice(all, 0); + REQUIRE(c0.size() == 3); + REQUIRE(c0[0] == 1); + REQUIRE(c0[1] == 3); + REQUIRE(c0[2] == 5); +} + +TEST_CASE("StaticTensor2D flat view") +{ + StaticTensor2D t; + t(0, 0) = 1.0; + t(0, 1) = 2.0; + t(1, 0) = 3.0; + t(1, 1) = 4.0; + + auto f = t.flat(); + REQUIRE(f.size() == 4); + f = 0.0; + REQUIRE(t(0, 0) == 0.0); + REQUIRE(t(1, 1) == 0.0); +} + +// ============================================================================ +// Non-member functions +// ============================================================================ + +TEST_CASE("zeros") +{ + auto t = zeros({3, 4}); + REQUIRE(t.size() == 12); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t[i] == 0.0); +} + +TEST_CASE("zeros_like") +{ + Tensor a({2, 5}, 7.0); + auto b = zeros_like(a); + REQUIRE(b.size() == 10); + REQUIRE(b.shape(0) == 2); + REQUIRE(b.shape(1) == 5); + for (size_t i = 0; i < b.size(); ++i) + REQUIRE(b[i] == 0.0); +} + +TEST_CASE("full_like") +{ + Tensor a({4}, 0); + auto b = full_like(a, 42); + REQUIRE(b.size() == 4); + for (size_t i = 0; i < b.size(); ++i) + REQUIRE(b[i] == 42); +} + +TEST_CASE("linspace") +{ + auto t = linspace(0.0, 1.0, 5); + REQUIRE(t.size() == 5); + REQUIRE(t[0] == 0.0); + REQUIRE(t[4] == 1.0); + REQUIRE_THAT(t[1], Catch::Matchers::WithinRel(0.25, 1e-12)); + REQUIRE_THAT(t[2], Catch::Matchers::WithinRel(0.5, 1e-12)); + REQUIRE_THAT(t[3], Catch::Matchers::WithinRel(0.75, 1e-12)); +} + +TEST_CASE("concatenate") +{ + Tensor a({3}, 0); + Tensor b({2}, 0); + a = {1, 2, 3}; + b = {4, 5}; + + auto c = concatenate(a, b); + REQUIRE(c.size() == 5); + REQUIRE(c[0] == 1); + REQUIRE(c[2] == 3); + REQUIRE(c[3] == 4); + REQUIRE(c[4] == 5); +} + +TEST_CASE("log") +{ + Tensor t({3}, 0.0); + t = {1.0, std::exp(1.0), std::exp(2.0)}; + + auto r = log(t); + REQUIRE_THAT(r[0], Catch::Matchers::WithinAbs(0.0, 1e-12)); + REQUIRE_THAT(r[1], Catch::Matchers::WithinAbs(1.0, 1e-12)); + REQUIRE_THAT(r[2], Catch::Matchers::WithinAbs(2.0, 1e-12)); +} + +TEST_CASE("abs") +{ + Tensor t({4}, 0.0); + t = {-3.0, -1.0, 0.0, 2.0}; + + auto r = abs(t); + REQUIRE(r[0] == 3.0); + REQUIRE(r[1] == 1.0); + REQUIRE(r[2] == 0.0); + REQUIRE(r[3] == 2.0); +} + +TEST_CASE("where") +{ + Tensor cond({4}, false); + cond.data()[0] = true; + cond.data()[2] = true; + + Tensor vals({4}, 0.0); + vals = {10.0, 20.0, 30.0, 40.0}; + + auto r = where(cond, vals, -1.0); + REQUIRE(r[0] == 10.0); + REQUIRE(r[1] == -1.0); + REQUIRE(r[2] == 30.0); + REQUIRE(r[3] == -1.0); +} + +TEST_CASE("nan_to_num") +{ + Tensor t({4}, 0.0); + t[0] = 1.0; + t[1] = std::nan(""); + t[2] = std::numeric_limits::infinity(); + t[3] = -std::numeric_limits::infinity(); + + auto r = nan_to_num(t); + REQUIRE(r[0] == 1.0); + REQUIRE(r[1] == 0.0); // NaN -> 0 + REQUIRE(r[2] == std::numeric_limits::max()); // +inf -> max + REQUIRE(r[3] == std::numeric_limits::lowest()); // -inf -> lowest +} + +// ============================================================================ +// is_tensor trait +// ============================================================================ + +TEST_CASE("is_tensor trait") +{ + REQUIRE(is_tensor>::value); + REQUIRE(is_tensor>::value); + REQUIRE(is_tensor>::value); + REQUIRE(!is_tensor::value); + REQUIRE(!is_tensor>::value); +} diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py index 1bb00129d..873633525 100644 --- a/tests/dummy_operator.py +++ b/tests/dummy_operator.py @@ -24,7 +24,7 @@ DepletionSolutionTuple = namedtuple( predictor_solution = DepletionSolutionTuple( PredictorIntegrator, np.array([1.0, 2.46847546272295, 4.11525874568034]), - np.array([1.0, 0.986431226850467, -0.0581692232513460])) + np.array([1.0, 0.986431226850467, 0.0])) cecm_solution = DepletionSolutionTuple( @@ -243,4 +243,4 @@ class DummyOperator(TransportOperator): Maps cell name to index in global geometry. """ - return self.volume, self.nuc_list, self.local_mats, self.burnable_mats + return self.volume, self.nuc_list, self.local_mats, self.burnable_mats, {"1": ""} diff --git a/tests/fns_flux_709.npy b/tests/fns_flux_709.npy new file mode 100644 index 000000000..62c612842 Binary files /dev/null and b/tests/fns_flux_709.npy differ diff --git a/tests/regression_tests/atomic_relaxation/__init__.py b/tests/regression_tests/atomic_relaxation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/atomic_relaxation/inputs_true.dat b/tests/regression_tests/atomic_relaxation/inputs_true.dat new file mode 100644 index 000000000..637e04285 --- /dev/null +++ b/tests/regression_tests/atomic_relaxation/inputs_true.dat @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + fixed source + 10000 + 1 + + + 1000000.0 1.0 + + + led + false + true + + + + photon electron + + + 1 + flux heating + + + diff --git a/tests/regression_tests/atomic_relaxation/results_true.dat b/tests/regression_tests/atomic_relaxation/results_true.dat new file mode 100644 index 000000000..6f100dac8 --- /dev/null +++ b/tests/regression_tests/atomic_relaxation/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.956204E+00 +3.826732E+00 +7.918768E+04 +6.270688E+09 +0.000000E+00 +0.000000E+00 +9.208123E+05 +8.478953E+11 diff --git a/tests/regression_tests/atomic_relaxation/test.py b/tests/regression_tests/atomic_relaxation/test.py new file mode 100644 index 000000000..0d2413f59 --- /dev/null +++ b/tests/regression_tests/atomic_relaxation/test.py @@ -0,0 +1,41 @@ +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + mat = openmc.Material() + mat.add_nuclide('Pb208', 1.0) + mat.set_density('g/cm3', 11.35) + + sphere = openmc.Sphere(r=1.0e9, boundary_type='reflective') + inside_sphere = openmc.Cell(fill=mat, region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([inside_sphere]) + + # Isotropic point source of 1 MeV photons at the origin + model.settings.source = openmc.IndependentSource( + particle='photon', + energy=openmc.stats.delta_function(1.0e6) + ) + + # Fixed-source photon transport with atomic relaxation disabled + model.settings.particles = 10000 + model.settings.batches = 1 + model.settings.photon_transport = True + model.settings.electron_treatment = 'led' + model.settings.atomic_relaxation = False + model.settings.run_mode = 'fixed source' + + tally = openmc.Tally() + tally.filters = [openmc.ParticleFilter(['photon', 'electron'])] + tally.scores = ['flux', 'heating'] + model.tallies = [tally] + return model + + +def test_atomic_relaxation(model): + harness = PyAPITestHarness('statepoint.1.h5', model=model) + harness.main() diff --git a/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 b/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 new file mode 100644 index 000000000..c31546213 Binary files /dev/null and b/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat index 7533616c0..005a56020 100644 --- a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ (n,fission) 101 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 new file mode 100644 index 000000000..850bba841 Binary files /dev/null and b/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat index 55fb835de..8d46c3d3b 100644 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 22 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 new file mode 100644 index 000000000..777d91f08 Binary files /dev/null and b/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat index 61890414b..322e74f49 100644 --- a/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 1 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 new file mode 100644 index 000000000..527705297 Binary files /dev/null and b/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat index 8960dde5c..6202bcacc 100644 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ O16 U235 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 new file mode 100644 index 000000000..65419d5ea Binary files /dev/null and b/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat index 8c0d7aa8e..c06061210 100644 --- a/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -52,7 +52,7 @@ 22 77 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 new file mode 100644 index 000000000..507665a60 Binary files /dev/null and b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat index 5173dc35c..434f6b434 100644 --- a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 550000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 new file mode 100644 index 000000000..b7ff67c48 Binary files /dev/null and b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 differ diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat index 005d9feb2..1449c3596 100644 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -56,7 +56,7 @@ 1 11 U238 U235 H1 U234 100000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat b/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat deleted file mode 100644 index 514932c1a..000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eigenvalue - 100 - 5 - 1 - - - -2.0 -2.0 -2.0 2.0 2.0 2.0 - - - true - - - - 200 - - 1 - - diff --git a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat b/tests/regression_tests/collision_track/case_8_2threads/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/test.py b/tests/regression_tests/collision_track/test.py index 00e1e3de4..2f9de9ac0 100644 --- a/tests/regression_tests/collision_track/test.py +++ b/tests/regression_tests/collision_track/test.py @@ -59,28 +59,13 @@ TODO: """ -import os - import openmc -import openmc.lib import pytest from tests.testing_harness import CollisionTrackTestHarness from tests.regression_tests import config -@pytest.fixture(scope="function") -def two_threads(monkeypatch): - """Set the number of OMP threads to 2 for the test.""" - monkeypatch.setenv("OMP_NUM_THREADS", "2") - - -@pytest.fixture(scope="function") -def single_process(monkeypatch): - """Set the number of MPI process to 1 for the test.""" - monkeypatch.setitem(config, "mpi_np", "1") - - @pytest.fixture(scope="module") def model_1(): """Cylindrical core contained in a first box which is contained in a larger box. @@ -181,9 +166,9 @@ def model_1(): # ============================================================================= model.settings = openmc.Settings() - model.settings.particles = 100 + model.settings.particles = 80 model.settings.batches = 5 - model.settings.inactive = 1 + model.settings.inactive = 4 model.settings.seed = 1 bounds = [ @@ -203,19 +188,19 @@ def model_1(): @pytest.mark.parametrize( "folder, model_name, parameter", - [("case_1_Reactions", "model_1", {"max_collisions": 300, "reactions": ["(n,fission)", 101]}), + [("case_1_Reactions", "model_1", {"max_collisions": 100, "reactions": ["(n,fission)", 101]}), ("case_2_Cell_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22]}), + "max_collisions": 100, "cell_ids": [22]}), ("case_3_Material_ID", "model_1", { - "max_collisions": 300, "material_ids": [1]}), + "max_collisions": 100, "material_ids": [1]}), ("case_4_Nuclide_ID", "model_1", { - "max_collisions": 300, "nuclides": ["O16", "U235"]}), + "max_collisions": 100, "nuclides": ["O16", "U235"]}), ("case_5_Universe_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22], "universe_ids": [77]}), + "max_collisions": 100, "cell_ids": [22], "universe_ids": [77]}), ("case_6_deposited_energy_threshold", "model_1", { - "max_collisions": 300, "deposited_E_threshold": 5.5e5}), + "max_collisions": 100, "deposited_E_threshold": 5.5e5}), ("case_7_all_parameters_used_together", "model_1", { - "max_collisions": 300, + "max_collisions": 100, "reactions": ["elastic", 18, "(n,disappear)"], "material_ids": [1, 11], "universe_ids": [77], @@ -235,21 +220,3 @@ def test_collision_track_several_cases( "statepoint.5.h5", model=model, workdir=folder ) harness.main() - - -@pytest.mark.skipif(config["event"], reason="Results from history-based mode.") -def test_collision_track_2threads(model_1, two_threads, single_process): - # This test checks that the `max_collisions` setting is honored: - # no collisions beyond the specified limit should be recorded. - # - # For the result to be reproducible, the number of threads and - # the transport mode (history vs. event) must remain fixed. - assert os.environ["OMP_NUM_THREADS"] == "2" - assert config["mpi_np"] == "1" - model_1.settings.collision_track = { - "max_collisions": 200 - } - harness = CollisionTrackTestHarness( - "statepoint.5.h5", model=model_1, workdir="case_8_2threads" - ) - harness.main() diff --git a/tests/regression_tests/cpp_driver/driver.cpp b/tests/regression_tests/cpp_driver/driver.cpp index a99c97b64..a6c3e6510 100644 --- a/tests/regression_tests/cpp_driver/driver.cpp +++ b/tests/regression_tests/cpp_driver/driver.cpp @@ -2,6 +2,8 @@ #include #endif +#include + #include "openmc/capi.h" #include "openmc/cell.h" #include "openmc/error.h" diff --git a/tests/regression_tests/deplete_with_keff_search_control/__init__.py b/tests/regression_tests/deplete_with_keff_search_control/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_refuel.h5 b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_refuel.h5 new file mode 100644 index 000000000..a335e3b47 Binary files /dev/null and b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_refuel.h5 differ diff --git a/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_rotation.h5 b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_rotation.h5 new file mode 100644 index 000000000..c8b4f67ff Binary files /dev/null and b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_rotation.h5 differ diff --git a/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_translation.h5 b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_translation.h5 new file mode 100644 index 000000000..35285321b Binary files /dev/null and b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_translation.h5 differ diff --git a/tests/regression_tests/deplete_with_keff_search_control/test.py b/tests/regression_tests/deplete_with_keff_search_control/test.py new file mode 100644 index 000000000..82e601280 --- /dev/null +++ b/tests/regression_tests/deplete_with_keff_search_control/test.py @@ -0,0 +1,140 @@ +""" Tests for KeffSearchControl class """ + +from hmac import new +from pathlib import Path +import shutil +import sys + +import pytest +import numpy as np + +import openmc +import openmc.lib +from openmc.deplete import CoupledOperator + +from tests.regression_tests import config + +@pytest.fixture +def model(): + f = openmc.Material(name='f') + f.set_density('g/cm3', 10.29769) + f.add_element('U', 1., enrichment=2.4) + f.add_element('O', 2.) + + h = openmc.Material(name='h') + h.set_density('g/cm3', 0.001598) + h.add_element('He', 2.4044e-4) + + w = openmc.Material(name='w') + w.set_density('g/cm3', 0.740582) + w.add_element('H', 2) + w.add_element('O', 1) + + # Define overall material + materials = openmc.Materials([f, h, w]) + + # Define surfaces + radii = [0.5, 0.8, 1] + height = 80 + surf_in = openmc.ZCylinder(r=radii[0]) + surf_mid = openmc.ZCylinder(r=radii[1]) + surf_out = openmc.ZCylinder(r=radii[2], boundary_type='reflective') + surf_top = openmc.ZPlane(z0=height/2, boundary_type='vacuum') + surf_bot = openmc.ZPlane(z0=-height/2, boundary_type='vacuum') + + surf_trans = openmc.ZPlane(z0=0) + surf_rot1 = openmc.XPlane(x0=0) + surf_rot2 = openmc.YPlane(y0=0) + + # Define cells + cell_f = openmc.Cell(name='fuel_cell', fill=f, + region=-surf_in & -surf_top & +surf_bot) + cell_g = openmc.Cell(fill=h, + region = +surf_in & -surf_mid & -surf_top & +surf_bot & +surf_rot2) + + # Define unbounded cells for rotation universe + cell_w = openmc.Cell(fill=w, region = -surf_rot1) + cell_h = openmc.Cell(fill=h, region = +surf_rot1) + universe_rot = openmc.Universe(cells=(cell_w, cell_h)) + cell_rot = openmc.Cell(name="rot_cell", fill=universe_rot, + region = +surf_in & -surf_mid & -surf_top & +surf_bot & -surf_rot2) + + # Define unbounded cells for translation universe + cell_w = openmc.Cell(fill=w, region=+surf_in & -surf_trans ) + cell_h = openmc.Cell(fill=h, region=+surf_in & +surf_trans) + universe_trans = openmc.Universe(cells=(cell_w, cell_h)) + cell_trans = openmc.Cell(name="trans_cell", fill=universe_trans, + region=+surf_mid & -surf_out & -surf_top & +surf_bot) + + # Define overall geometry + geometry = openmc.Geometry([cell_f, cell_g, cell_rot, cell_trans]) + + # Set material volume for depletion fuel. + f.volume = np.pi * radii[0]**2 * height + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + + +def translate_cell(position): + cell_trans = [cell for cell in openmc.lib.cells.values() if cell.name == 'trans_cell'][0] + openmc.lib.cells[cell_trans.id].translation = [0, 0, position] + + +def rotate_cell(angle): + cell_rot = [cell for cell in openmc.lib.cells.values() if cell.name == 'rot_cell'][0] + openmc.lib.cells[cell_rot.id].rotation = [0, 0, angle] + + +def set_u235_density(u235_density): + fuel = [material for material in openmc.lib.materials.values() + if material.name == 'f'][0] + nuclides = openmc.lib.materials[fuel.id].nuclides + densities = openmc.lib.materials[fuel.id].densities + nuc_idx = nuclides.index('U235') + densities[nuc_idx] = u235_density + openmc.lib.materials[fuel.id].set_densities(nuclides, densities) + + +@pytest.mark.parametrize("function, x0, x1, bracket, ref_result", [ + (translate_cell, -11, -5, (-15, 0), 'depletion_with_translation'), + (rotate_cell, -80, -50, (-90, 0), 'depletion_with_rotation'), + (set_u235_density, 2e-4, 1e-3, (1e-4, 2e-3), 'depletion_with_refuel') +]) +def test_keff_search_control(run_in_tmpdir, model, function, x0, x1, bracket, ref_result): + chain_file = Path(__file__).parents[2] / 'chain_simple.xml' + model.settings.verbosity = 1 + op = CoupledOperator(model, chain_file) + + integrator = openmc.deplete.PredictorIntegrator( + op, [1], 174., timestep_units = 'd') + integrator.add_keff_search_control( + function=function, + x0=x0, + x1=x1, + bracket=bracket, + output=True, + k_tol=0.1, + sigma_final=5e-2) + + integrator.integrate() + + # Get path to test and reference results + path_test = op.output_dir / 'depletion_results.h5' + path_reference = Path(__file__).with_name(f'ref_{ref_result}.h5') + + # If updating results, do so and return + if config['update']: + shutil.copyfile(str(path_test), str(path_reference)) + return + + # Load the reference/test results + res_test = openmc.deplete.Results(path_test) + res_ref = openmc.deplete.Results(path_reference) + + # Use high tolerance here + assert res_test[0].keff_search_root == pytest.approx(res_ref[0].keff_search_root, rel=2) diff --git a/tests/regression_tests/distribmat/inputs_true.dat b/tests/regression_tests/distribmat/inputs_true.dat index 35b3b5b9f..8087d7564 100644 --- a/tests/regression_tests/distribmat/inputs_true.dat +++ b/tests/regression_tests/distribmat/inputs_true.dat @@ -17,7 +17,9 @@ - + + 2 void 3 2 + diff --git a/tests/regression_tests/distribmat/test.py b/tests/regression_tests/distribmat/test.py index 02f7e773e..dd09eec36 100644 --- a/tests/regression_tests/distribmat/test.py +++ b/tests/regression_tests/distribmat/test.py @@ -73,7 +73,7 @@ class DistribmatTestHarness(PyAPITestHarness): # Plots #################### - plot1 = openmc.Plot(plot_id=1) + plot1 = openmc.SlicePlot(plot_id=1) plot1.basis = 'xy' plot1.color_by = 'cell' plot1.filename = 'cellplot' @@ -81,7 +81,7 @@ class DistribmatTestHarness(PyAPITestHarness): plot1.width = (7, 7) plot1.pixels = (400, 400) - plot2 = openmc.Plot(plot_id=2) + plot2 = openmc.SlicePlot(plot_id=2) plot2.basis = 'xy' plot2.color_by = 'material' plot2.filename = 'matplot' diff --git a/tests/regression_tests/filter_musurface/inputs_true.dat b/tests/regression_tests/filter_musurface/inputs_true.dat index 6db8543c2..b457f5028 100644 --- a/tests/regression_tests/filter_musurface/inputs_true.dat +++ b/tests/regression_tests/filter_musurface/inputs_true.dat @@ -31,7 +31,7 @@ 1 2 - current + current flux diff --git a/tests/regression_tests/filter_musurface/results_true.dat b/tests/regression_tests/filter_musurface/results_true.dat index 4cdd7dbf5..657c141a0 100644 --- a/tests/regression_tests/filter_musurface/results_true.dat +++ b/tests/regression_tests/filter_musurface/results_true.dat @@ -5,7 +5,15 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 9.230000E-01 1.791510E-01 +3.294304E+00 +2.314403E+00 3.869000E+00 3.002523E+00 +5.154125E+00 +5.314334E+00 diff --git a/tests/regression_tests/filter_musurface/test.py b/tests/regression_tests/filter_musurface/test.py index f2ec96b49..bfc12a47c 100644 --- a/tests/regression_tests/filter_musurface/test.py +++ b/tests/regression_tests/filter_musurface/test.py @@ -1,6 +1,3 @@ -import numpy as np -from math import pi - import openmc import pytest @@ -32,7 +29,7 @@ def model(): mu_filter = openmc.MuSurfaceFilter([-1.0, -0.5, 0.0, 0.5, 1.0]) tally = openmc.Tally() tally.filters = [surf_filter, mu_filter] - tally.scores = ['current'] + tally.scores = ['current', 'flux'] model.tallies.append(tally) return model diff --git a/tests/regression_tests/filter_reaction/__init__.py b/tests/regression_tests/filter_reaction/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/filter_reaction/inputs_true.dat b/tests/regression_tests/filter_reaction/inputs_true.dat new file mode 100644 index 000000000..3ab9ab3fb --- /dev/null +++ b/tests/regression_tests/filter_reaction/inputs_true.dat @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + eigenvalue + 1000 + 5 + + + + (n,elastic) (n,2n) (n,fission) (n,gamma) (n,total) + + + 1 + flux + + + diff --git a/tests/regression_tests/filter_reaction/results_true.dat b/tests/regression_tests/filter_reaction/results_true.dat new file mode 100644 index 000000000..61ec1c65f --- /dev/null +++ b/tests/regression_tests/filter_reaction/results_true.dat @@ -0,0 +1,13 @@ +k-combined: +2.265297E+00 7.172807E-03 +tally 1: +8.364316E+01 +1.400120E+03 +1.412312E-01 +4.277446E-03 +5.471503E+00 +7.015849E+00 +0.000000E+00 +0.000000E+00 +1.349840E+02 +3.644648E+03 diff --git a/tests/regression_tests/filter_reaction/test.py b/tests/regression_tests/filter_reaction/test.py new file mode 100644 index 000000000..3c60fd517 --- /dev/null +++ b/tests/regression_tests/filter_reaction/test.py @@ -0,0 +1,31 @@ +import openmc + +from tests.testing_harness import PyAPITestHarness + + +def test_filter_reaction(): + model = openmc.Model() + + m = openmc.Material() + m.set_density('g/cm3', 10.0) + m.add_nuclide('U235', 1.0) + model.materials.append(m) + + s = openmc.Sphere(r=100.0, boundary_type='vacuum') + c = openmc.Cell(fill=m, region=-s) + model.geometry = openmc.Geometry([c]) + + # Create a tally with reaction filter + tally = openmc.Tally() + tally.filters = [openmc.ReactionFilter( + ['(n,elastic)', '(n,2n)', '(n,fission)', '(n,gamma)', 'total'] + )] + tally.scores = ['flux'] + model.tallies = openmc.Tallies([tally]) + + # Reduce particles for faster testing + model.settings.particles = 1000 + model.settings.batches = 5 + + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/filter_rotations/__init__.py b/tests/regression_tests/filter_rotations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/filter_rotations/inputs_true.dat b/tests/regression_tests/filter_rotations/inputs_true.dat new file mode 100644 index 000000000..1ad2b9e86 --- /dev/null +++ b/tests/regression_tests/filter_rotations/inputs_true.dat @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 5 + 0 + + + + 3 4 5 + -9 -9 -9 + 9 9 9 + + + 3 4 5 + -9 -9 -9 + 9 9 9 + + + 1 + + + 2 + + + 1 + total + + + 2 + total + + + diff --git a/tests/regression_tests/filter_rotations/results_true.dat b/tests/regression_tests/filter_rotations/results_true.dat new file mode 100644 index 000000000..5c8b83b76 --- /dev/null +++ b/tests/regression_tests/filter_rotations/results_true.dat @@ -0,0 +1,244 @@ +k-combined: +7.729082E-01 3.775399E-02 +tally 1: +5.296804E-02 +5.661701E-04 +8.356446E-02 +1.412139E-03 +5.041335E-02 +5.143568E-04 +1.299348E-01 +3.467618E-03 +3.929702E-01 +3.147038E-02 +1.379707E-01 +3.888484E-03 +1.405034E-01 +4.473799E-03 +3.785796E-01 +2.940585E-02 +1.422010E-01 +4.113723E-03 +5.647073E-02 +6.735251E-04 +7.911154E-02 +1.329137E-03 +5.160755E-02 +5.361448E-04 +6.669424E-02 +9.090832E-04 +1.008621E-01 +2.134534E-03 +6.808932E-02 +9.355993E-04 +1.873006E-01 +7.135961E-03 +6.221575E-01 +7.819842E-02 +1.856653E-01 +6.954762E-03 +2.014929E-01 +8.327845E-03 +5.853251E-01 +6.945708E-02 +1.709645E-01 +5.917124E-03 +7.214913E-02 +1.058962E-03 +1.027720E-01 +2.138475E-03 +6.099853E-02 +7.493941E-04 +6.892071E-02 +9.630680E-04 +1.035459E-01 +2.173883E-03 +6.973870E-02 +9.904237E-04 +2.125703E-01 +9.112659E-03 +9.012205E-01 +2.163546E-01 +2.066426E-01 +8.617414E-03 +2.258950E-01 +1.039607E-02 +9.476792E-01 +2.350708E-01 +2.225585E-01 +1.017898E-02 +7.111503E-02 +1.036847E-03 +1.117012E-01 +2.530040E-03 +6.870474E-02 +9.551035E-04 +5.738897E-02 +6.699030E-04 +9.522335E-02 +1.835769E-03 +6.570917E-02 +8.656870E-04 +1.945592E-01 +7.593336E-03 +5.514753E-01 +6.122981E-02 +2.144202E-01 +9.421739E-03 +1.971631E-01 +7.944046E-03 +6.088996E-01 +7.442954E-02 +1.965447E-01 +7.765628E-03 +7.005494E-02 +1.012891E-03 +1.010633E-01 +2.084095E-03 +6.145926E-02 +7.694351E-04 +4.999479E-02 +5.129164E-04 +7.238243E-02 +1.062921E-03 +4.902309E-02 +4.852193E-04 +1.324655E-01 +3.642431E-03 +3.305312E-01 +2.265726E-02 +1.332993E-01 +3.728385E-03 +1.547469E-01 +4.894837E-03 +3.625944E-01 +2.747313E-02 +1.435761E-01 +4.334405E-03 +5.789603E-02 +7.065383E-04 +7.589559E-02 +1.205386E-03 +5.210018E-02 +5.790843E-04 +tally 2: +4.597932E-02 +4.246849E-04 +8.494738E-02 +1.467238E-03 +5.155072E-02 +5.373129E-04 +1.479395E-01 +4.468164E-03 +3.931843E-01 +3.141943E-02 +1.264081E-01 +3.243689E-03 +1.229742E-01 +3.276501E-03 +3.768427E-01 +2.927971E-02 +1.574908E-01 +5.079304E-03 +5.621044E-02 +6.877866E-04 +8.490616E-02 +1.510673E-03 +4.709600E-02 +4.576632E-04 +5.382674E-02 +5.890600E-04 +1.116560E-01 +2.599817E-03 +6.648634E-02 +8.862677E-04 +1.996861E-01 +8.137198E-03 +6.218616E-01 +7.805532E-02 +1.672379E-01 +5.667797E-03 +1.841275E-01 +6.936896E-03 +5.887874E-01 +7.031665E-02 +1.872574E-01 +7.086584E-03 +7.574698E-02 +1.160992E-03 +1.100173E-01 +2.453972E-03 +5.427866E-02 +5.955556E-04 +5.644318E-02 +6.503634E-04 +1.058628E-01 +2.254338E-03 +8.012794E-02 +1.297032E-03 +2.292757E-01 +1.064490E-02 +9.001874E-01 +2.153097E-01 +1.921874E-01 +7.537343E-03 +2.058642E-01 +8.537136E-03 +9.442653E-01 +2.359241E-01 +2.419900E-01 +1.202201E-02 +7.563876E-02 +1.169874E-03 +1.165428E-01 +2.750405E-03 +5.646354E-02 +6.408683E-04 +4.963879E-02 +4.975915E-04 +1.005478E-01 +2.046400E-03 +7.041191E-02 +1.002590E-03 +1.959123E-01 +7.727902E-03 +5.625596E-01 +6.366248E-02 +1.987313E-01 +8.108832E-03 +1.922194E-01 +7.602752E-03 +6.062527E-01 +7.384124E-02 +2.039506E-01 +8.379010E-03 +7.019905E-02 +1.034239E-03 +1.081383E-01 +2.344040E-03 +5.125142E-02 +5.430397E-04 +4.230495E-02 +3.666730E-04 +7.722275E-02 +1.208856E-03 +4.998937E-02 +5.049355E-04 +1.461632E-01 +4.456332E-03 +3.315459E-01 +2.288314E-02 +1.238273E-01 +3.205844E-03 +1.355317E-01 +3.754923E-03 +3.646520E-01 +2.766067E-02 +1.543013E-01 +5.044487E-03 +6.082173E-02 +7.636985E-04 +8.490569E-02 +1.506309E-03 +4.046126E-02 +3.495832E-04 diff --git a/tests/regression_tests/filter_rotations/test.py b/tests/regression_tests/filter_rotations/test.py new file mode 100644 index 000000000..f5d63d6b4 --- /dev/null +++ b/tests/regression_tests/filter_rotations/test.py @@ -0,0 +1,72 @@ +import numpy as np + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + + model = openmc.model.Model() + + fuel = openmc.Material() + fuel.set_density('g/cm3', 10.0) + fuel.add_nuclide('U235', 1.0) + zr = openmc.Material() + zr.set_density('g/cm3', 1.0) + zr.add_nuclide('Zr90', 1.0) + model.materials.extend([fuel, zr]) + + box1 = openmc.model.RectangularPrism(10.0, 10.0) + box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective') + top = openmc.ZPlane(z0=10.0, boundary_type='vacuum') + bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') + cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top) + cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top) + model.geometry = openmc.Geometry([cell1, cell2]) + + model.settings.batches = 5 + model.settings.inactive = 0 + model.settings.particles = 1000 + + rotation = np.array((0, 0, 10)) + + llc = np.array([-9, -9, -9]) + urc = np.array([9, 9, 9]) + + mesh_dims = (3, 4, 5) + + filters = [] + + # un-rotated meshes + reg_mesh = openmc.RegularMesh() + reg_mesh.dimension = mesh_dims + reg_mesh.lower_left = llc + reg_mesh.upper_right = urc + + filters.append(openmc.MeshFilter(reg_mesh)) + + # rotated meshes + rotated_reg_mesh = openmc.RegularMesh() + rotated_reg_mesh.dimension = mesh_dims + rotated_reg_mesh.lower_left = llc + rotated_reg_mesh.upper_right = urc + + filters.append(openmc.MeshFilter(rotated_reg_mesh)) + filters[-1].rotation = rotation + + # Create tallies + for f in filters: + tally = openmc.Tally() + tally.filters = [f] + tally.scores = ['total'] + model.tallies.append(tally) + + return model + + +def test_filter_mesh_rotations(model): + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/lattice_corner_crossing/__init__.py b/tests/regression_tests/lattice_corner_crossing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/lattice_corner_crossing/inputs_true.dat b/tests/regression_tests/lattice_corner_crossing/inputs_true.dat new file mode 100644 index 000000000..4b49b1403 --- /dev/null +++ b/tests/regression_tests/lattice_corner_crossing/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + 10.0 10.0 + 2 2 + -10.0 -10.0 + +1 2 +2 1 + + + + + + + + + fixed source + 1000 + 10 + + + -0.7071067811865476 -0.7071067811865475 0.0 + + + + + + + 10 10 + -20.0 -20.0 + 20.0 20.0 + + + 1 + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/lattice_corner_crossing/results_true.dat b/tests/regression_tests/lattice_corner_crossing/results_true.dat new file mode 100644 index 000000000..bd81cf535 --- /dev/null +++ b/tests/regression_tests/lattice_corner_crossing/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.648775E-03 +7.016009E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.981847E-03 +8.891414E-06 +3.331677E-03 +1.110007E-05 +6.742237E-03 +4.545776E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.388206E-03 +1.925636E-05 +2.032104E-03 +4.129446E-06 +3.014946E-03 +9.089898E-06 +7.058968E-03 +4.982903E-05 +9.013188E-04 +8.123755E-07 +7.501461E-04 +5.627191E-07 +0.000000E+00 +0.000000E+00 +5.134898E-03 +2.636718E-05 +1.207302E-03 +1.457579E-06 +3.045925E-03 +9.277657E-06 +4.132399E-03 +1.707672E-05 +1.055271E-02 +5.829711E-05 +4.014909E-03 +1.611949E-05 +2.698215E-03 +7.280363E-06 +1.751215E-02 +1.073504E-04 +1.356908E-02 +9.589157E-05 +5.451466E-03 +2.971848E-05 +3.165676E-04 +1.002151E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.724938E-03 +1.387516E-05 +2.812863E-03 +7.278837E-06 +1.000700E+01 +1.001402E+01 +2.345904E-02 +2.127641E-04 +2.451653E-02 +1.750725E-04 +8.695904E-03 +5.553981E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.947565E-03 +2.447840E-05 +1.725721E-02 +8.878803E-05 +5.656444E+01 +3.199538E+02 +2.159961E-02 +1.034364E-04 +3.091063E-02 +5.543317E-04 +4.232209E-03 +1.791159E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.716170E-03 +2.224226E-05 +1.321855E-02 +5.288265E-05 +5.309576E-02 +7.984865E-04 +5.656179E+01 +3.199238E+02 +2.399311E-02 +2.936670E-04 +4.680753E-02 +9.526509E-04 +1.074699E-02 +1.154978E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.526425E-03 +2.048853E-05 +3.983003E-03 +9.553596E-06 +1.726789E-02 +9.579848E-05 +3.355605E-02 +3.331438E-04 +3.578422E-02 +3.560242E-04 +5.649209E+01 +3.191358E+02 +2.591495E-02 +4.169753E-04 +9.484744E-03 +8.065715E-05 +0.000000E+00 +0.000000E+00 +4.232587E-03 +9.113263E-06 +5.787378E-03 +2.194940E-05 +6.193626E-03 +2.357423E-05 +4.552787E-03 +6.987360E-06 +9.641308E-03 +3.506339E-05 +1.303892E-02 +4.378408E-05 +1.831744E-02 +8.836683E-05 +3.024725E+01 +9.148969E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.488204E-03 +2.214752E-06 +5.067487E-03 +1.520987E-05 +7.550927E-03 +2.993991E-05 +3.215559E-03 +1.033982E-05 +4.258525E-03 +1.205064E-05 +2.115487E-03 +4.475286E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/lattice_corner_crossing/test.py b/tests/regression_tests/lattice_corner_crossing/test.py new file mode 100644 index 000000000..4684d144e --- /dev/null +++ b/tests/regression_tests/lattice_corner_crossing/test.py @@ -0,0 +1,83 @@ +""" +This test is designed to ensure that we account for potential corner crossings +in floating point precision. + +""" + +from math import pi, cos, sin + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + + # Length of lattice on each side + lat_size = 20.0 + + # Angle that we're crossing the corner at + phi = pi/4.0 + + air = openmc.Material() + air.set_density('g/cm3', 0.001) + air.add_nuclide('N14', 1.0) + metal = openmc.Material() + metal.set_density('g/cm3', 7.0) + metal.add_nuclide('Fe56', 1.0) + + metal_cell = openmc.Cell(fill=metal) + metal_uni = openmc.Universe(cells=[metal_cell]) + + air_cell = openmc.Cell(fill=air) + air_uni = openmc.Universe(cells=[air_cell]) + + # Define a checkerboard lattice + lattice = openmc.RectLattice() + lattice.lower_left = (-lat_size/2.0, -lat_size/2.0) + lattice.pitch = (lat_size/2, lat_size/2) + lattice.universes = [ + [metal_uni, air_uni], + [air_uni, metal_uni] + ] + + box = openmc.model.RectangularPrism(lat_size, lat_size) + cyl = openmc.ZCylinder(r=lat_size, boundary_type='vacuum') + outside_lattice = openmc.Cell(region=-cyl & +box, fill=air) + inside_lattice = openmc.Cell(region=-box, fill=lattice) + + model.geometry = openmc.Geometry([outside_lattice, inside_lattice]) + + # Set all runtime parameters + model.settings.run_mode = 'fixed source' + model.settings.batches = 10 + model.settings.particles = 1000 + + # Define a source located outside the lattice and pointing straight into its + # corner at 45 degrees + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point((-cos(phi), -sin(phi), 0.0)), + angle=openmc.stats.Monodirectional((cos(phi), sin(phi), 0.0)) + ) + + # Create a mesh tally + mesh = openmc.RegularMesh() + mesh.dimension = (10, 10) + mesh.lower_left = (-lat_size, -lat_size) + mesh.upper_right = (lat_size, lat_size) + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally(tally_id=1) + tally.filters = [mesh_filter] + tally.scores = ['flux'] + tally.estimator = 'tracklength' + model.tallies = [tally] + + return model + + +def test_lattice_corner_crossing(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat index aec3a5400..ab827dfde 100644 --- a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat +++ b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat @@ -47,8 +47,12 @@ - - + + 1 2 3 4 + + + 5 6 7 8 + diff --git a/tests/regression_tests/lattice_distribrho/inputs_true.dat b/tests/regression_tests/lattice_distribrho/inputs_true.dat index 5031bea6e..99994af3d 100644 --- a/tests/regression_tests/lattice_distribrho/inputs_true.dat +++ b/tests/regression_tests/lattice_distribrho/inputs_true.dat @@ -14,7 +14,9 @@ - + + 10.0 20.0 10.0 20.0 + diff --git a/tests/regression_tests/lattice_large_pitch/__init__.py b/tests/regression_tests/lattice_large_pitch/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/lattice_large_pitch/inputs_true.dat b/tests/regression_tests/lattice_large_pitch/inputs_true.dat new file mode 100644 index 000000000..4dd4f0bd7 --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/inputs_true.dat @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + 100.0 100.0 + 2 + 3 3 + -150.0 -150.0 + +1 2 1 +2 1 2 +1 2 1 + + + + + + + + fixed source + 1000 + 10 + + + + 6 6 + -150.0 -150.0 + 150.0 150.0 + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/lattice_large_pitch/results_true.dat b/tests/regression_tests/lattice_large_pitch/results_true.dat new file mode 100644 index 000000000..58c4e306d --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/results_true.dat @@ -0,0 +1,73 @@ +tally 1: +1.749265E+01 +3.275925E+01 +4.159430E+01 +1.799568E+02 +7.027454E+01 +4.967568E+02 +6.789716E+01 +4.652258E+02 +4.272935E+01 +1.861129E+02 +2.073342E+01 +4.374279E+01 +4.028297E+01 +1.641642E+02 +6.734263E+01 +4.627171E+02 +1.037657E+02 +1.078682E+03 +1.108394E+02 +1.240927E+03 +7.236770E+01 +5.302034E+02 +4.356361E+01 +1.921990E+02 +6.731536E+01 +4.615977E+02 +9.850684E+01 +9.810272E+02 +5.164863E+02 +2.670336E+04 +5.097770E+02 +2.604770E+04 +1.064847E+02 +1.137536E+03 +7.052986E+01 +5.003220E+02 +7.065046E+01 +5.036723E+02 +1.044890E+02 +1.103250E+03 +5.121544E+02 +2.632389E+04 +5.065331E+02 +2.570752E+04 +1.044794E+02 +1.101249E+03 +6.569142E+01 +4.361120E+02 +4.739812E+01 +2.313289E+02 +7.402284E+01 +5.591689E+02 +1.066905E+02 +1.149521E+03 +1.038665E+02 +1.086813E+03 +7.160008E+01 +5.217744E+02 +4.219705E+01 +1.816100E+02 +2.089838E+01 +4.464899E+01 +4.154271E+01 +1.745866E+02 +7.081253E+01 +5.049997E+02 +6.673273E+01 +4.509038E+02 +4.184624E+01 +1.771546E+02 +1.853898E+01 +3.538608E+01 diff --git a/tests/regression_tests/lattice_large_pitch/test.py b/tests/regression_tests/lattice_large_pitch/test.py new file mode 100644 index 000000000..0f409badd --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/test.py @@ -0,0 +1,70 @@ +"""Regression test for rectangular lattices with large pitch values. + +Large pitches used to trigger a segmentation fault in ``RectLattice::distance`` +because the boundary-crossing check compared a reconstructed crossing position +against an absolute tolerance that did not scale with the geometry (see #3852). +This test transports particles across a lattice with a large pitch to ensure the +crossing logic remains robust. + +""" + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + + # Large pitch that previously caused floating-point cancellation + pitch = 100.0 + n = 3 + + air = openmc.Material() + air.set_density('g/cm3', 0.001) + air.add_nuclide('N14', 1.0) + metal = openmc.Material() + metal.set_density('g/cm3', 7.0) + metal.add_nuclide('Fe56', 1.0) + + metal_cell = openmc.Cell(fill=metal) + metal_uni = openmc.Universe(cells=[metal_cell]) + air_cell = openmc.Cell(fill=air) + air_uni = openmc.Universe(cells=[air_cell]) + + lattice = openmc.RectLattice() + lattice.lower_left = (-pitch*n/2, -pitch*n/2) + lattice.pitch = (pitch, pitch) + lattice.outer = air_uni + lattice.universes = [ + [metal_uni, air_uni, metal_uni], + [air_uni, metal_uni, air_uni], + [metal_uni, air_uni, metal_uni], + ] + + box = openmc.model.RectangularPrism(pitch*n, pitch*n, boundary_type='vacuum') + root_cell = openmc.Cell(region=-box, fill=lattice) + model.geometry = openmc.Geometry([root_cell]) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 10 + model.settings.particles = 1000 + + mesh = openmc.RegularMesh() + mesh.dimension = (6, 6) + mesh.lower_left = (-pitch*n/2, -pitch*n/2) + mesh.upper_right = (pitch*n/2, pitch*n/2) + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally(tally_id=1) + tally.filters = [mesh_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + return model + + +def test_lattice_large_pitch(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat new file mode 100644 index 000000000..318fdc7b9 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat @@ -0,0 +1,65 @@ + + + + 2g.h5 + + + + + + + + + + + + fixed source + 100 + 2 + 0 + + + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 + + + + false + + multi-group + + false + + true + + 1 + neutron + 0.0 0.625 20000000.0 + 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 + 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 + 3.0 + 10 + 1e-38 + + + 5 1 1 + 0.0 -1000.0 -1000.0 + 929.45 1000.0 1000.0 + + true + 100 + + + + 5 1 1 + 0.0 -1000.0 -1000.0 + 929.45 1000.0 1000.0 + + + 2 + + + 1 + flux + + + diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat new file mode 100644 index 000000000..38ff94417 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat @@ -0,0 +1,11 @@ +tally 1: +1.765369E+04 +3.087787E+08 +1.708316E+04 +2.842002E+08 +9.444106E+03 +7.488341E+07 +2.066528E+03 +2.142445E+06 +8.689619E+02 +5.652099E+05 diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py new file mode 100644 index 000000000..336c8461f --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py @@ -0,0 +1,91 @@ +import os + +import numpy as np +import openmc +from openmc.examples import slab_mg + +from tests.testing_harness import PyAPITestHarness + + +def create_library(): + # Instantiate the energy group data and file object + groups = openmc.mgxs.EnergyGroups([0.0, 0.625, 20.0e6]) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + + # Make the base, isotropic data + nu = [2.50, 2.50] + fiss = np.array([0.002817, 0.097]) + capture = [0.008708, 0.02518] + absorption = np.add(capture, fiss) + scatter = np.array( + [[[0.31980, 0.06694], [0.004555, -0.0003972]], + [[0.00000, 0.00000], [0.424100, 0.05439000]]]) + total = [0.33588, 0.54628] + chi = [1., 0.] + + mat_1 = openmc.XSdata('mat_1', groups) + mat_1.order = 1 + mat_1.set_nu_fission(np.multiply(nu, fiss)) + mat_1.set_absorption(absorption) + mat_1.set_scatter_matrix(scatter) + mat_1.set_total(total) + mat_1.set_chi(chi) + mg_cross_sections_file.add_xsdata(mat_1) + + # Write the file + mg_cross_sections_file.export_to_hdf5('2g.h5') + + +class MGXSTestHarness(PyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = '2g.h5' + if os.path.exists(f): + os.remove(f) + + +def test_mg_fixed_source_ww_fission_shared_secondary(): + create_library() + model = slab_mg() + + # Override settings for fixed-source mode with shared secondary bank + model.settings.run_mode = 'fixed source' + model.settings.inactive = 0 + model.settings.batches = 2 + model.settings.particles = 100 + model.settings.create_fission_neutrons = True + model.settings.shared_secondary_bank = True + model.settings.max_history_splits = 100 + + # Add weight windows on a simple 1D mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.lower_left = (0.0, -1000.0, -1000.0) + ww_mesh.upper_right = (929.45, 1000.0, 1000.0) + ww_mesh.dimension = (5, 1, 1) + + # Uniform lower bounds for 2 energy groups, 5 spatial bins + lower_bounds = np.full((2, 5, 1, 1), 0.5) + ww = openmc.WeightWindows( + ww_mesh, + lower_bounds.flatten(), + None, + 5.0, + [0.0, 0.625, 20.0e6], + 'neutron' + ) + model.settings.weight_windows = [ww] + + # Add a flux tally + mesh = openmc.RegularMesh() + mesh.lower_left = (0.0, -1000.0, -1000.0) + mesh.upper_right = (929.45, 1000.0, 1000.0) + mesh.dimension = (5, 1, 1) + + tally = openmc.Tally() + tally.filters = [openmc.MeshFilter(mesh)] + tally.scores = ['flux'] + model.tallies = [tally] + + harness = MGXSTestHarness('statepoint.2.h5', model) + harness.main() diff --git a/tests/regression_tests/mg_tallies/results_true.dat b/tests/regression_tests/mg_tallies/results_true.dat index 07fe22ce5..c1591cca2 100644 --- a/tests/regression_tests/mg_tallies/results_true.dat +++ b/tests/regression_tests/mg_tallies/results_true.dat @@ -238,8 +238,8 @@ tally 2: 3.994127E-08 4.815195E+06 6.359497E+12 -1.373000E+00 -4.448330E-01 +1.340228E+00 +4.256704E-01 1.788012E-04 8.768717E-09 3.941968E+00 @@ -260,8 +260,8 @@ tally 2: 1.493588E-07 1.064740E+07 2.378109E+13 -2.753000E+00 -1.542179E+00 +2.698926E+00 +1.484990E+00 3.953669E-04 3.279028E-08 8.155605E+00 @@ -282,8 +282,8 @@ tally 2: 1.319523E-07 1.005866E+07 2.100960E+13 -2.829000E+00 -1.702143E+00 +2.795871E+00 +1.662994E+00 3.735055E-04 2.896884E-08 8.365907E+00 @@ -304,8 +304,8 @@ tally 2: 2.314228E-07 1.328300E+07 3.684741E+13 -3.246000E+00 -2.132054E+00 +3.162533E+00 +2.024101E+00 4.932336E-04 5.080661E-08 9.080077E+00 @@ -326,8 +326,8 @@ tally 2: 1.984988E-07 1.206905E+07 3.160522E+13 -3.759000E+00 -2.944479E+00 +3.670108E+00 +2.815133E+00 4.481564E-04 4.357848E-08 1.076370E+01 @@ -348,8 +348,8 @@ tally 2: 1.474939E-07 1.003129E+07 2.348415E+13 -2.577000E+00 -1.509201E+00 +2.523640E+00 +1.451617E+00 3.724888E-04 3.238084E-08 7.654439E+00 @@ -370,8 +370,8 @@ tally 2: 1.993067E-07 1.242911E+07 3.173385E+13 -3.266000E+00 -2.172442E+00 +3.198836E+00 +2.085466E+00 4.615266E-04 4.375584E-08 9.265396E+00 @@ -392,8 +392,8 @@ tally 2: 2.277785E-07 1.329858E+07 3.626717E+13 -3.485000E+00 -2.510947E+00 +3.467276E+00 +2.484377E+00 4.938123E-04 5.000655E-08 9.871966E+00 @@ -414,8 +414,8 @@ tally 2: 1.293793E-09 7.073280E+05 2.059993E+11 -3.000000E-01 -4.514200E-02 +2.978788E-01 +4.498561E-02 2.626500E-05 2.840396E-10 9.197485E-01 @@ -436,8 +436,8 @@ tally 2: 2.205650E-10 2.942995E+05 3.511863E+10 -1.300000E-01 -5.822000E-03 +1.270281E-01 +5.517180E-03 1.092814E-05 4.842290E-11 3.746117E-01 @@ -908,8 +908,8 @@ tally 12: 3.994127E-08 4.815195E+06 6.359497E+12 -1.373000E+00 -4.448330E-01 +1.340228E+00 +4.256704E-01 1.788012E-04 8.768717E-09 2.806910E+00 @@ -928,8 +928,8 @@ tally 12: 1.493588E-07 1.064740E+07 2.378109E+13 -2.753000E+00 -1.542179E+00 +2.698926E+00 +1.484990E+00 3.953669E-04 3.279028E-08 2.869647E+00 @@ -948,8 +948,8 @@ tally 12: 1.319523E-07 1.005866E+07 2.100960E+13 -2.829000E+00 -1.702143E+00 +2.795871E+00 +1.662994E+00 3.735055E-04 2.896884E-08 3.141043E+00 @@ -968,8 +968,8 @@ tally 12: 2.314228E-07 1.328300E+07 3.684741E+13 -3.246000E+00 -2.132054E+00 +3.162533E+00 +2.024101E+00 4.932336E-04 5.080661E-08 3.682383E+00 @@ -988,8 +988,8 @@ tally 12: 1.984988E-07 1.206905E+07 3.160522E+13 -3.759000E+00 -2.944479E+00 +3.670108E+00 +2.815133E+00 4.481564E-04 4.357848E-08 2.634850E+00 @@ -1008,8 +1008,8 @@ tally 12: 1.474939E-07 1.003129E+07 2.348415E+13 -2.577000E+00 -1.509201E+00 +2.523640E+00 +1.451617E+00 3.724888E-04 3.238084E-08 3.192584E+00 @@ -1028,8 +1028,8 @@ tally 12: 1.993067E-07 1.242911E+07 3.173385E+13 -3.266000E+00 -2.172442E+00 +3.198836E+00 +2.085466E+00 4.615266E-04 4.375584E-08 3.402213E+00 @@ -1048,8 +1048,8 @@ tally 12: 2.277785E-07 1.329858E+07 3.626717E+13 -3.485000E+00 -2.510947E+00 +3.467276E+00 +2.484377E+00 4.938123E-04 5.000655E-08 3.110378E-01 @@ -1068,8 +1068,8 @@ tally 12: 1.293793E-09 7.073280E+05 2.059993E+11 -3.000000E-01 -4.514200E-02 +2.978788E-01 +4.498561E-02 2.626500E-05 2.840396E-10 1.267544E-01 @@ -1088,8 +1088,8 @@ tally 12: 2.205650E-10 2.942995E+05 3.511863E+10 -1.300000E-01 -5.822000E-03 +1.270281E-01 +5.517180E-03 1.092814E-05 4.842290E-11 tally 13: diff --git a/tests/regression_tests/mg_temperature/build_2g.py b/tests/regression_tests/mg_temperature/build_2g.py index 1fb723449..bca87364e 100644 --- a/tests/regression_tests/mg_temperature/build_2g.py +++ b/tests/regression_tests/mg_temperature/build_2g.py @@ -227,7 +227,7 @@ def analytical_solution_2g_therm(xsmin, xsmax=None, wgt=1.0): L = np.array([sa[0] + ss12, 0.0, -ss12, sa[1]]).reshape(2, 2) Q = np.array([nsf[0], nsf[1], 0.0, 0.0]).reshape(2, 2) arr = np.linalg.inv(L).dot(Q) - return np.amax(np.linalg.eigvals(arr)) + return np.amax(np.linalg.eigvals(arr).real) def build_inf_model(xsnames, xslibname, temperature, tempmethod='nearest'): diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_multiplicity_matrix.dat similarity index 100% rename from tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat rename to tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_multiplicity_matrix.dat diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat new file mode 100644 index 000000000..6e60f1b19 --- /dev/null +++ b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + + + + 7 + + + 0.0 0.625 20000000.0 + + + 0.0 0.625 20000000.0 + + + 3 + + + 8 + + + 9 + + + 43 44 + total + flux + tracklength + + + 43 44 + total + total + tracklength + + + 43 44 + total + flux + tracklength + + + 43 44 + total + absorption + tracklength + + + 43 44 + total + flux + analog + + + 43 44 49 + total + nu-fission + analog + + + 43 44 + total + flux + analog + + + 43 44 49 53 + total + scatter + analog + + + 54 44 + total + flux + tracklength + + + 54 44 + total + total + tracklength + + + 54 44 + total + flux + tracklength + + + 54 44 + total + absorption + tracklength + + + 54 44 + total + flux + analog + + + 54 44 49 + total + nu-fission + analog + + + 54 44 + total + flux + analog + + + 54 44 49 53 + total + scatter + analog + + + 65 44 + total + flux + tracklength + + + 65 44 + total + total + tracklength + + + 65 44 + total + flux + tracklength + + + 65 44 + total + absorption + tracklength + + + 65 44 + total + flux + analog + + + 65 44 49 + total + nu-fission + analog + + + 65 44 + total + flux + analog + + + 65 44 49 53 + total + scatter + analog + + + diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/test.py b/tests/regression_tests/mgxs_library_ce_to_mg/test.py index 075167f58..b68fbd6c3 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg/test.py @@ -9,7 +9,7 @@ from tests.regression_tests import config class MGXSTestHarness(PyAPITestHarness): - def __init__(self, *args, **kwargs): + def __init__(self, *args, scatter_mgxs_type=None, **kwargs): # Generate inputs using parent class routine super().__init__(*args, **kwargs) @@ -19,8 +19,8 @@ class MGXSTestHarness(PyAPITestHarness): # Initialize MGXS Library for a few cross section types self.mgxs_lib = openmc.mgxs.Library(self._model.geometry) self.mgxs_lib.by_nuclide = False - self.mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission matrix', - 'nu-scatter matrix', 'multiplicity matrix'] + self.mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission matrix'] + self.mgxs_lib.mgxs_types += scatter_mgxs_type self.mgxs_lib.energy_groups = energy_groups self.mgxs_lib.correction = None self.mgxs_lib.legendre_order = 3 @@ -69,9 +69,23 @@ class MGXSTestHarness(PyAPITestHarness): os.remove(f) -def test_mgxs_library_ce_to_mg(): +def test_mgxs_library_ce_to_mg_multiplicity_matrix(): # Set the input set to use the pincell model model = pwr_pin_cell() - harness = MGXSTestHarness('statepoint.10.h5', model) + harness = MGXSTestHarness( + 'statepoint.10.h5', model, + inputs_true='inputs_true_multiplicity_matrix.dat', + scatter_mgxs_type=['nu-scatter matrix', 'multiplicity matrix'] + ) + harness.main() + + +def test_mgxs_library_ce_to_mg_scatter_matrix(): + # Set the input set to use the pincell model + model = pwr_pin_cell() + + harness = MGXSTestHarness('statepoint.10.h5', model, + inputs_true='inputs_true_scatter_matrix.dat', + scatter_mgxs_type=['scatter matrix']) harness.main() diff --git a/tests/regression_tests/mgxs_library_condense/results_true.dat b/tests/regression_tests/mgxs_library_condense/results_true.dat index 98b30932c..023d5afa2 100644 --- a/tests/regression_tests/mgxs_library_condense/results_true.dat +++ b/tests/regression_tests/mgxs_library_condense/results_true.dat @@ -1,189 +1,189 @@ - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.702881 0.026175 -2 1 2 1 1 total 0.706921 0.029169 -1 2 1 1 1 total 0.707809 0.024766 -3 2 2 1 1 total 0.717967 0.024008 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.431023 0.028803 -2 1 2 1 1 total 0.451864 0.030748 -1 2 1 1 1 total 0.456990 0.026359 -3 2 2 1 1 total 0.450621 0.026744 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.431023 0.028803 -2 1 2 1 1 total 0.451864 0.030748 -1 2 1 1 1 total 0.456990 0.026359 -3 2 2 1 1 total 0.450621 0.026744 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.022398 0.001401 -2 1 2 1 1 total 0.022325 0.001371 -1 2 1 1 1 total 0.022942 0.000990 -3 2 2 1 1 total 0.022705 0.001322 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.022394 0.001401 -2 1 2 1 1 total 0.022321 0.001371 -1 2 1 1 1 total 0.022935 0.000990 -3 2 2 1 1 total 0.022699 0.001322 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.011562 0.001544 -2 1 2 1 1 total 0.011852 0.001418 -1 2 1 1 1 total 0.012168 0.000958 -3 2 2 1 1 total 0.011986 0.001418 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.010836 0.000803 -2 1 2 1 1 total 0.010473 0.000591 -1 2 1 1 1 total 0.010774 0.000415 -3 2 2 1 1 total 0.010719 0.000688 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.026602 0.001957 -2 1 2 1 1 total 0.025695 0.001442 -1 2 1 1 1 total 0.026454 0.001015 -3 2 2 1 1 total 0.026310 0.001678 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 2.098256e+06 155243.612264 -2 1 2 1 1 total 2.027699e+06 114334.400924 -1 2 1 1 1 total 2.086255e+06 80325.567787 -3 2 2 1 1 total 2.075596e+06 133128.805680 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.680483 0.025407 -2 1 2 1 1 total 0.684597 0.028126 -1 2 1 1 1 total 0.684867 0.024133 -3 2 2 1 1 total 0.695262 0.023087 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.678017 0.026288 -2 1 2 1 1 total 0.674888 0.033989 -1 2 1 1 1 total 0.681736 0.025618 -3 2 2 1 1 total 0.683701 0.023788 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.678017 0.026290 -1 1 1 1 1 1 P1 total 0.271858 0.011693 -2 1 1 1 1 1 P2 total 0.095219 0.002950 -3 1 1 1 1 1 P3 total 0.012808 0.004686 -8 1 2 1 1 1 P0 total 0.674888 0.033578 -9 1 2 1 1 1 P1 total 0.255058 0.009912 -10 1 2 1 1 1 P2 total 0.098001 0.005459 -11 1 2 1 1 1 P3 total 0.012058 0.005439 -4 2 1 1 1 1 P0 total 0.681736 0.025439 -5 2 1 1 1 1 P1 total 0.250820 0.009035 -6 2 1 1 1 1 P2 total 0.092563 0.006549 -7 2 1 1 1 1 P3 total 0.008511 0.003905 -12 2 2 1 1 1 P0 total 0.683701 0.024254 -13 2 2 1 1 1 P1 total 0.267345 0.011695 -14 2 2 1 1 1 P2 total 0.096222 0.005551 -15 2 2 1 1 1 P3 total 0.011515 0.003236 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.678017 0.026290 -1 1 1 1 1 1 P1 total 0.271858 0.011693 -2 1 1 1 1 1 P2 total 0.095219 0.002950 -3 1 1 1 1 1 P3 total 0.012808 0.004686 -8 1 2 1 1 1 P0 total 0.674888 0.033578 -9 1 2 1 1 1 P1 total 0.255058 0.009912 -10 1 2 1 1 1 P2 total 0.098001 0.005459 -11 1 2 1 1 1 P3 total 0.012058 0.005439 -4 2 1 1 1 1 P0 total 0.681736 0.025439 -5 2 1 1 1 1 P1 total 0.250820 0.009035 -6 2 1 1 1 1 P2 total 0.092563 0.006549 -7 2 1 1 1 1 P3 total 0.008511 0.003905 -12 2 2 1 1 1 P0 total 0.683701 0.024254 -13 2 2 1 1 1 P1 total 0.267345 0.011695 -14 2 2 1 1 1 P2 total 0.096222 0.005551 -15 2 2 1 1 1 P3 total 0.011515 0.003236 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 0.041785 -2 1 2 1 1 1 total 1.0 0.057717 -1 2 1 1 1 1 total 1.0 0.040074 -3 2 2 1 1 1 total 1.0 0.042758 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.028438 0.003513 -2 1 2 1 1 1 total 0.022222 0.001560 -1 2 1 1 1 1 total 0.025698 0.002756 -3 2 2 1 1 1 total 0.026501 0.002315 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 0.041785 -2 1 2 1 1 1 total 1.0 0.057717 -1 2 1 1 1 1 total 1.0 0.040074 -3 2 2 1 1 1 total 1.0 0.042758 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.680483 0.038131 -1 1 1 1 1 1 P1 total 0.272847 0.016111 -2 1 1 1 1 1 P2 total 0.095565 0.004869 -3 1 1 1 1 1 P3 total 0.012854 0.004731 -8 1 2 1 1 1 P0 total 0.684597 0.048501 -9 1 2 1 1 1 P1 total 0.258727 0.016473 -10 1 2 1 1 1 P2 total 0.099411 0.007470 -11 1 2 1 1 1 P3 total 0.012231 0.005552 -4 2 1 1 1 1 P0 total 0.684867 0.036546 -5 2 1 1 1 1 P1 total 0.251972 0.013220 -6 2 1 1 1 1 P2 total 0.092988 0.007474 -7 2 1 1 1 1 P3 total 0.008550 0.003936 -12 2 2 1 1 1 P0 total 0.695262 0.037640 -13 2 2 1 1 1 P1 total 0.271866 0.016280 -14 2 2 1 1 1 P2 total 0.097849 0.006919 -15 2 2 1 1 1 P3 total 0.011710 0.003325 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.680483 0.047566 -1 1 1 1 1 1 P1 total 0.272847 0.019737 -2 1 1 1 1 1 P2 total 0.095565 0.006297 -3 1 1 1 1 1 P3 total 0.012854 0.004762 -8 1 2 1 1 1 P0 total 0.684597 0.062559 -9 1 2 1 1 1 P1 total 0.258727 0.022234 -10 1 2 1 1 1 P2 total 0.099411 0.009420 -11 1 2 1 1 1 P3 total 0.012231 0.005597 -4 2 1 1 1 1 P0 total 0.684867 0.045704 -5 2 1 1 1 1 P1 total 0.251972 0.016635 -6 2 1 1 1 1 P2 total 0.092988 0.008352 -7 2 1 1 1 1 P3 total 0.008550 0.003951 -12 2 2 1 1 1 P0 total 0.695262 0.047964 -13 2 2 1 1 1 P1 total 0.271866 0.020004 -14 2 2 1 1 1 P2 total 0.097849 0.008086 -15 2 2 1 1 1 P3 total 0.011710 0.003363 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.142430 -2 1 2 1 1 total 1.0 0.112715 -1 2 1 1 1 total 1.0 0.192385 -3 2 2 1 1 total 1.0 0.109836 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.143959 -2 1 2 1 1 total 1.0 0.102504 -1 2 1 1 1 total 1.0 0.188381 -3 2 2 1 1 total 1.0 0.116230 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 5.060544e-07 3.651604e-08 -2 1 2 1 1 total 5.101988e-07 4.947639e-08 -1 2 1 1 1 total 5.206450e-07 2.814648e-08 -3 2 2 1 1 total 5.278759e-07 3.171554e-08 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.026414 0.001944 -2 1 2 1 1 total 0.025515 0.001432 -1 2 1 1 1 total 0.026267 0.001008 -3 2 2 1 1 total 0.026125 0.001667 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.028220 0.003510 -2 1 2 1 1 1 total 0.021754 0.001519 -1 2 1 1 1 1 total 0.025487 0.002676 -3 2 2 1 1 1 total 0.026281 0.002263 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.702881 0.026175 +2 1 2 1 total 0.706921 0.029169 +1 2 1 1 total 0.707809 0.024766 +3 2 2 1 total 0.717967 0.024008 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.431023 0.028803 +2 1 2 1 total 0.451864 0.030748 +1 2 1 1 total 0.456990 0.026359 +3 2 2 1 total 0.450621 0.026744 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.431023 0.028803 +2 1 2 1 total 0.451864 0.030748 +1 2 1 1 total 0.456990 0.026359 +3 2 2 1 total 0.450621 0.026744 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.022398 0.001401 +2 1 2 1 total 0.022325 0.001371 +1 2 1 1 total 0.022942 0.000990 +3 2 2 1 total 0.022705 0.001322 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.022394 0.001401 +2 1 2 1 total 0.022321 0.001371 +1 2 1 1 total 0.022935 0.000990 +3 2 2 1 total 0.022699 0.001322 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.011562 0.001544 +2 1 2 1 total 0.011852 0.001418 +1 2 1 1 total 0.012168 0.000958 +3 2 2 1 total 0.011986 0.001418 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.010836 0.000803 +2 1 2 1 total 0.010473 0.000591 +1 2 1 1 total 0.010774 0.000415 +3 2 2 1 total 0.010719 0.000688 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.026602 0.001957 +2 1 2 1 total 0.025695 0.001442 +1 2 1 1 total 0.026454 0.001015 +3 2 2 1 total 0.026310 0.001678 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 2.098256e+06 155243.612264 +2 1 2 1 total 2.027699e+06 114334.400924 +1 2 1 1 total 2.086255e+06 80325.567787 +3 2 2 1 total 2.075596e+06 133128.805680 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.680483 0.025407 +2 1 2 1 total 0.684597 0.028126 +1 2 1 1 total 0.684867 0.024133 +3 2 2 1 total 0.695262 0.023087 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.678017 0.026288 +2 1 2 1 total 0.674888 0.033989 +1 2 1 1 total 0.681736 0.025618 +3 2 2 1 total 0.683701 0.023788 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.678017 0.026290 +1 1 1 1 1 P1 total 0.271858 0.011693 +2 1 1 1 1 P2 total 0.095219 0.002950 +3 1 1 1 1 P3 total 0.012808 0.004686 +8 1 2 1 1 P0 total 0.674888 0.033578 +9 1 2 1 1 P1 total 0.255058 0.009912 +10 1 2 1 1 P2 total 0.098001 0.005459 +11 1 2 1 1 P3 total 0.012058 0.005439 +4 2 1 1 1 P0 total 0.681736 0.025439 +5 2 1 1 1 P1 total 0.250820 0.009035 +6 2 1 1 1 P2 total 0.092563 0.006549 +7 2 1 1 1 P3 total 0.008511 0.003905 +12 2 2 1 1 P0 total 0.683701 0.024254 +13 2 2 1 1 P1 total 0.267345 0.011695 +14 2 2 1 1 P2 total 0.096222 0.005551 +15 2 2 1 1 P3 total 0.011515 0.003236 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.678017 0.026290 +1 1 1 1 1 P1 total 0.271858 0.011693 +2 1 1 1 1 P2 total 0.095219 0.002950 +3 1 1 1 1 P3 total 0.012808 0.004686 +8 1 2 1 1 P0 total 0.674888 0.033578 +9 1 2 1 1 P1 total 0.255058 0.009912 +10 1 2 1 1 P2 total 0.098001 0.005459 +11 1 2 1 1 P3 total 0.012058 0.005439 +4 2 1 1 1 P0 total 0.681736 0.025439 +5 2 1 1 1 P1 total 0.250820 0.009035 +6 2 1 1 1 P2 total 0.092563 0.006549 +7 2 1 1 1 P3 total 0.008511 0.003905 +12 2 2 1 1 P0 total 0.683701 0.024254 +13 2 2 1 1 P1 total 0.267345 0.011695 +14 2 2 1 1 P2 total 0.096222 0.005551 +15 2 2 1 1 P3 total 0.011515 0.003236 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 0.041785 +2 1 2 1 1 total 1.0 0.057717 +1 2 1 1 1 total 1.0 0.040074 +3 2 2 1 1 total 1.0 0.042758 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.028438 0.003513 +2 1 2 1 1 total 0.022222 0.001560 +1 2 1 1 1 total 0.025698 0.002756 +3 2 2 1 1 total 0.026501 0.002315 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 0.041785 +2 1 2 1 1 total 1.0 0.057717 +1 2 1 1 1 total 1.0 0.040074 +3 2 2 1 1 total 1.0 0.042758 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.680483 0.038131 +1 1 1 1 1 P1 total 0.272847 0.016111 +2 1 1 1 1 P2 total 0.095565 0.004869 +3 1 1 1 1 P3 total 0.012854 0.004731 +8 1 2 1 1 P0 total 0.684597 0.048501 +9 1 2 1 1 P1 total 0.258727 0.016473 +10 1 2 1 1 P2 total 0.099411 0.007470 +11 1 2 1 1 P3 total 0.012231 0.005552 +4 2 1 1 1 P0 total 0.684867 0.036546 +5 2 1 1 1 P1 total 0.251972 0.013220 +6 2 1 1 1 P2 total 0.092988 0.007474 +7 2 1 1 1 P3 total 0.008550 0.003936 +12 2 2 1 1 P0 total 0.695262 0.037640 +13 2 2 1 1 P1 total 0.271866 0.016280 +14 2 2 1 1 P2 total 0.097849 0.006919 +15 2 2 1 1 P3 total 0.011710 0.003325 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.680483 0.047566 +1 1 1 1 1 P1 total 0.272847 0.019737 +2 1 1 1 1 P2 total 0.095565 0.006297 +3 1 1 1 1 P3 total 0.012854 0.004762 +8 1 2 1 1 P0 total 0.684597 0.062559 +9 1 2 1 1 P1 total 0.258727 0.022234 +10 1 2 1 1 P2 total 0.099411 0.009420 +11 1 2 1 1 P3 total 0.012231 0.005597 +4 2 1 1 1 P0 total 0.684867 0.045704 +5 2 1 1 1 P1 total 0.251972 0.016635 +6 2 1 1 1 P2 total 0.092988 0.008352 +7 2 1 1 1 P3 total 0.008550 0.003951 +12 2 2 1 1 P0 total 0.695262 0.047964 +13 2 2 1 1 P1 total 0.271866 0.020004 +14 2 2 1 1 P2 total 0.097849 0.008086 +15 2 2 1 1 P3 total 0.011710 0.003363 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.142430 +2 1 2 1 total 1.0 0.112715 +1 2 1 1 total 1.0 0.192385 +3 2 2 1 total 1.0 0.109836 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.143959 +2 1 2 1 total 1.0 0.102504 +1 2 1 1 total 1.0 0.188381 +3 2 2 1 total 1.0 0.116230 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 5.060544e-07 3.651604e-08 +2 1 2 1 total 5.101988e-07 4.947639e-08 +1 2 1 1 total 5.206450e-07 2.814648e-08 +3 2 2 1 total 5.278759e-07 3.171554e-08 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.026414 0.001944 +2 1 2 1 total 0.025515 0.001432 +1 2 1 1 total 0.026267 0.001008 +3 2 2 1 total 0.026125 0.001667 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.028220 0.003510 +2 1 2 1 1 total 0.021754 0.001519 +1 2 1 1 1 total 0.025487 0.002676 +3 2 2 1 1 total 0.026281 0.002263 mesh 1 group in nuclide mean std. dev. x y surf 3 1 1 x-max in 1 total 4.244 0.096333 @@ -218,145 +218,145 @@ 30 2 2 y-max out 1 total 0.000 0.000000 29 2 2 y-min in 1 total 4.416 0.093648 28 2 2 y-min out 1 total 4.280 0.079070 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.943198 0.067915 -2 1 2 1 1 total 0.895741 0.058714 -1 2 1 1 1 total 0.911011 0.068461 -3 2 2 1 1 total 0.927611 0.066426 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.943198 0.067915 -2 1 2 1 1 total 0.895741 0.058714 -1 2 1 1 1 total 0.911011 0.068461 -3 2 2 1 1 total 0.927611 0.066426 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000006 4.453421e-07 -1 1 1 1 2 1 total 0.000032 2.303082e-06 -2 1 1 1 3 1 total 0.000031 2.202789e-06 -3 1 1 1 4 1 total 0.000072 4.961025e-06 -4 1 1 1 5 1 total 0.000032 2.071919e-06 -5 1 1 1 6 1 total 0.000013 8.662934e-07 -12 1 2 1 1 1 total 0.000006 3.282329e-07 -13 1 2 1 2 1 total 0.000031 1.701766e-06 -14 1 2 1 3 1 total 0.000030 1.629615e-06 -15 1 2 1 4 1 total 0.000070 3.675759e-06 -16 1 2 1 5 1 total 0.000031 1.536226e-06 -17 1 2 1 6 1 total 0.000013 6.423838e-07 -6 2 1 1 1 1 total 0.000006 2.308186e-07 -7 2 1 1 2 1 total 0.000032 1.211112e-06 -8 2 1 1 3 1 total 0.000031 1.169911e-06 -9 2 1 1 4 1 total 0.000072 2.685832e-06 -10 2 1 1 5 1 total 0.000032 1.187072e-06 -11 2 1 1 6 1 total 0.000013 4.939053e-07 -18 2 2 1 1 1 total 0.000006 3.819684e-07 -19 2 2 1 2 1 total 0.000032 1.976073e-06 -20 2 2 1 3 1 total 0.000031 1.889748e-06 -21 2 2 1 4 1 total 0.000072 4.252207e-06 -22 2 2 1 5 1 total 0.000032 1.765565e-06 -23 2 2 1 6 1 total 0.000013 7.386890e-07 - mesh 1 delayedgroup group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.0 0.000000 -1 1 1 1 2 1 total 0.0 0.000000 -2 1 1 1 3 1 total 0.0 0.000000 -3 1 1 1 4 1 total 1.0 1.414214 -4 1 1 1 5 1 total 0.0 0.000000 -5 1 1 1 6 1 total 0.0 0.000000 -12 1 2 1 1 1 total 0.0 0.000000 -13 1 2 1 2 1 total 0.0 0.000000 -14 1 2 1 3 1 total 0.0 0.000000 -15 1 2 1 4 1 total 1.0 0.869026 -16 1 2 1 5 1 total 0.0 0.000000 -17 1 2 1 6 1 total 0.0 0.000000 -6 2 1 1 1 1 total 0.0 0.000000 -7 2 1 1 2 1 total 0.0 0.000000 -8 2 1 1 3 1 total 0.0 0.000000 -9 2 1 1 4 1 total 1.0 1.414214 -10 2 1 1 5 1 total 0.0 0.000000 -11 2 1 1 6 1 total 0.0 0.000000 -18 2 2 1 1 1 total 0.0 0.000000 -19 2 2 1 2 1 total 0.0 0.000000 -20 2 2 1 3 1 total 0.0 0.000000 -21 2 2 1 4 1 total 1.0 1.414214 -22 2 2 1 5 1 total 0.0 0.000000 -23 2 2 1 6 1 total 0.0 0.000000 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000227 0.000023 -1 1 1 1 2 1 total 0.001210 0.000119 -2 1 1 1 3 1 total 0.001176 0.000114 -3 1 1 1 4 1 total 0.002722 0.000261 -4 1 1 1 5 1 total 0.001204 0.000112 -5 1 1 1 6 1 total 0.000501 0.000047 -12 1 2 1 1 1 total 0.000227 0.000017 -13 1 2 1 2 1 total 0.001208 0.000087 -14 1 2 1 3 1 total 0.001174 0.000084 -15 1 2 1 4 1 total 0.002710 0.000192 -16 1 2 1 5 1 total 0.001194 0.000082 -17 1 2 1 6 1 total 0.000497 0.000034 -6 2 1 1 1 1 total 0.000227 0.000010 -7 2 1 1 2 1 total 0.001210 0.000053 -8 2 1 1 3 1 total 0.001177 0.000052 -9 2 1 1 4 1 total 0.002726 0.000119 -10 2 1 1 5 1 total 0.001208 0.000053 -11 2 1 1 6 1 total 0.000503 0.000022 -18 2 2 1 1 1 total 0.000227 0.000019 -19 2 2 1 2 1 total 0.001211 0.000102 -20 2 2 1 3 1 total 0.001178 0.000098 -21 2 2 1 4 1 total 0.002726 0.000223 -22 2 2 1 5 1 total 0.001207 0.000096 -23 2 2 1 6 1 total 0.000503 0.000040 - mesh 1 delayedgroup nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013353 0.001345 -1 1 1 1 2 total 0.032619 0.003120 -2 1 1 1 3 total 0.121042 0.011142 -3 1 1 1 4 total 0.305503 0.026418 -4 1 1 1 5 total 0.860433 0.064665 -5 1 1 1 6 total 2.889963 0.219527 -12 1 2 1 1 total 0.013352 0.001049 -13 1 2 1 2 total 0.032626 0.002465 -14 1 2 1 3 total 0.121027 0.008891 -15 1 2 1 4 total 0.305351 0.021426 -16 1 2 1 5 total 0.859866 0.054490 -17 1 2 1 6 total 2.888034 0.184436 -6 2 1 1 1 total 0.013353 0.000612 -7 2 1 1 2 total 0.032616 0.001412 -8 2 1 1 3 total 0.121047 0.005039 -9 2 1 1 4 total 0.305559 0.012002 -10 2 1 1 5 total 0.860640 0.030707 -11 2 1 1 6 total 2.890664 0.103692 -18 2 2 1 1 total 0.013353 0.001132 -19 2 2 1 2 total 0.032617 0.002633 -20 2 2 1 3 total 0.121046 0.009426 -21 2 2 1 4 total 0.305545 0.022417 -22 2 2 1 5 total 0.860588 0.055030 -23 2 2 1 6 total 2.890489 0.186821 - mesh 1 delayedgroup group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 1 total 0.000000 0.000000 -1 1 1 1 2 1 1 total 0.000000 0.000000 -2 1 1 1 3 1 1 total 0.000000 0.000000 -3 1 1 1 4 1 1 total 0.000219 0.000219 -4 1 1 1 5 1 1 total 0.000000 0.000000 -5 1 1 1 6 1 1 total 0.000000 0.000000 -12 1 2 1 1 1 1 total 0.000000 0.000000 -13 1 2 1 2 1 1 total 0.000000 0.000000 -14 1 2 1 3 1 1 total 0.000000 0.000000 -15 1 2 1 4 1 1 total 0.000467 0.000287 -16 1 2 1 5 1 1 total 0.000000 0.000000 -17 1 2 1 6 1 1 total 0.000000 0.000000 -6 2 1 1 1 1 1 total 0.000000 0.000000 -7 2 1 1 2 1 1 total 0.000000 0.000000 -8 2 1 1 3 1 1 total 0.000000 0.000000 -9 2 1 1 4 1 1 total 0.000211 0.000211 -10 2 1 1 5 1 1 total 0.000000 0.000000 -11 2 1 1 6 1 1 total 0.000000 0.000000 -18 2 2 1 1 1 1 total 0.000000 0.000000 -19 2 2 1 2 1 1 total 0.000000 0.000000 -20 2 2 1 3 1 1 total 0.000000 0.000000 -21 2 2 1 4 1 1 total 0.000219 0.000219 -22 2 2 1 5 1 1 total 0.000000 0.000000 -23 2 2 1 6 1 1 total 0.000000 0.000000 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.943198 0.067915 +2 1 2 1 total 0.895741 0.058714 +1 2 1 1 total 0.911011 0.068461 +3 2 2 1 total 0.927611 0.066426 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.943198 0.067915 +2 1 2 1 total 0.895741 0.058714 +1 2 1 1 total 0.911011 0.068461 +3 2 2 1 total 0.927611 0.066426 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000006 4.453421e-07 +1 1 1 2 1 total 0.000032 2.303082e-06 +2 1 1 3 1 total 0.000031 2.202789e-06 +3 1 1 4 1 total 0.000072 4.961025e-06 +4 1 1 5 1 total 0.000032 2.071919e-06 +5 1 1 6 1 total 0.000013 8.662934e-07 +12 1 2 1 1 total 0.000006 3.282329e-07 +13 1 2 2 1 total 0.000031 1.701766e-06 +14 1 2 3 1 total 0.000030 1.629615e-06 +15 1 2 4 1 total 0.000070 3.675759e-06 +16 1 2 5 1 total 0.000031 1.536226e-06 +17 1 2 6 1 total 0.000013 6.423838e-07 +6 2 1 1 1 total 0.000006 2.308186e-07 +7 2 1 2 1 total 0.000032 1.211112e-06 +8 2 1 3 1 total 0.000031 1.169911e-06 +9 2 1 4 1 total 0.000072 2.685832e-06 +10 2 1 5 1 total 0.000032 1.187072e-06 +11 2 1 6 1 total 0.000013 4.939053e-07 +18 2 2 1 1 total 0.000006 3.819684e-07 +19 2 2 2 1 total 0.000032 1.976073e-06 +20 2 2 3 1 total 0.000031 1.889748e-06 +21 2 2 4 1 total 0.000072 4.252207e-06 +22 2 2 5 1 total 0.000032 1.765565e-06 +23 2 2 6 1 total 0.000013 7.386890e-07 + mesh 1 delayedgroup group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.0 0.000000 +1 1 1 2 1 total 0.0 0.000000 +2 1 1 3 1 total 0.0 0.000000 +3 1 1 4 1 total 1.0 1.414214 +4 1 1 5 1 total 0.0 0.000000 +5 1 1 6 1 total 0.0 0.000000 +12 1 2 1 1 total 0.0 0.000000 +13 1 2 2 1 total 0.0 0.000000 +14 1 2 3 1 total 0.0 0.000000 +15 1 2 4 1 total 1.0 0.869026 +16 1 2 5 1 total 0.0 0.000000 +17 1 2 6 1 total 0.0 0.000000 +6 2 1 1 1 total 0.0 0.000000 +7 2 1 2 1 total 0.0 0.000000 +8 2 1 3 1 total 0.0 0.000000 +9 2 1 4 1 total 1.0 1.414214 +10 2 1 5 1 total 0.0 0.000000 +11 2 1 6 1 total 0.0 0.000000 +18 2 2 1 1 total 0.0 0.000000 +19 2 2 2 1 total 0.0 0.000000 +20 2 2 3 1 total 0.0 0.000000 +21 2 2 4 1 total 1.0 1.414214 +22 2 2 5 1 total 0.0 0.000000 +23 2 2 6 1 total 0.0 0.000000 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000227 0.000023 +1 1 1 2 1 total 0.001210 0.000119 +2 1 1 3 1 total 0.001176 0.000114 +3 1 1 4 1 total 0.002722 0.000261 +4 1 1 5 1 total 0.001204 0.000112 +5 1 1 6 1 total 0.000501 0.000047 +12 1 2 1 1 total 0.000227 0.000017 +13 1 2 2 1 total 0.001208 0.000087 +14 1 2 3 1 total 0.001174 0.000084 +15 1 2 4 1 total 0.002710 0.000192 +16 1 2 5 1 total 0.001194 0.000082 +17 1 2 6 1 total 0.000497 0.000034 +6 2 1 1 1 total 0.000227 0.000010 +7 2 1 2 1 total 0.001210 0.000053 +8 2 1 3 1 total 0.001177 0.000052 +9 2 1 4 1 total 0.002726 0.000119 +10 2 1 5 1 total 0.001208 0.000053 +11 2 1 6 1 total 0.000503 0.000022 +18 2 2 1 1 total 0.000227 0.000019 +19 2 2 2 1 total 0.001211 0.000102 +20 2 2 3 1 total 0.001178 0.000098 +21 2 2 4 1 total 0.002726 0.000223 +22 2 2 5 1 total 0.001207 0.000096 +23 2 2 6 1 total 0.000503 0.000040 + mesh 1 delayedgroup nuclide mean std. dev. + x y +0 1 1 1 total 0.013353 0.001345 +1 1 1 2 total 0.032619 0.003120 +2 1 1 3 total 0.121042 0.011142 +3 1 1 4 total 0.305503 0.026418 +4 1 1 5 total 0.860433 0.064665 +5 1 1 6 total 2.889963 0.219527 +12 1 2 1 total 0.013352 0.001049 +13 1 2 2 total 0.032626 0.002465 +14 1 2 3 total 0.121027 0.008891 +15 1 2 4 total 0.305351 0.021426 +16 1 2 5 total 0.859866 0.054490 +17 1 2 6 total 2.888034 0.184436 +6 2 1 1 total 0.013353 0.000612 +7 2 1 2 total 0.032616 0.001412 +8 2 1 3 total 0.121047 0.005039 +9 2 1 4 total 0.305559 0.012002 +10 2 1 5 total 0.860640 0.030707 +11 2 1 6 total 2.890664 0.103692 +18 2 2 1 total 0.013353 0.001132 +19 2 2 2 total 0.032617 0.002633 +20 2 2 3 total 0.121046 0.009426 +21 2 2 4 total 0.305545 0.022417 +22 2 2 5 total 0.860588 0.055030 +23 2 2 6 total 2.890489 0.186821 + mesh 1 delayedgroup group in group out nuclide mean std. dev. + x y +0 1 1 1 1 1 total 0.000000 0.000000 +1 1 1 2 1 1 total 0.000000 0.000000 +2 1 1 3 1 1 total 0.000000 0.000000 +3 1 1 4 1 1 total 0.000219 0.000219 +4 1 1 5 1 1 total 0.000000 0.000000 +5 1 1 6 1 1 total 0.000000 0.000000 +12 1 2 1 1 1 total 0.000000 0.000000 +13 1 2 2 1 1 total 0.000000 0.000000 +14 1 2 3 1 1 total 0.000000 0.000000 +15 1 2 4 1 1 total 0.000467 0.000287 +16 1 2 5 1 1 total 0.000000 0.000000 +17 1 2 6 1 1 total 0.000000 0.000000 +6 2 1 1 1 1 total 0.000000 0.000000 +7 2 1 2 1 1 total 0.000000 0.000000 +8 2 1 3 1 1 total 0.000000 0.000000 +9 2 1 4 1 1 total 0.000211 0.000211 +10 2 1 5 1 1 total 0.000000 0.000000 +11 2 1 6 1 1 total 0.000000 0.000000 +18 2 2 1 1 1 total 0.000000 0.000000 +19 2 2 2 1 1 total 0.000000 0.000000 +20 2 2 3 1 1 total 0.000000 0.000000 +21 2 2 4 1 1 total 0.000219 0.000219 +22 2 2 5 1 1 total 0.000000 0.000000 +23 2 2 6 1 1 total 0.000000 0.000000 diff --git a/tests/regression_tests/mgxs_library_mesh/results_true.dat b/tests/regression_tests/mgxs_library_mesh/results_true.dat index 16b86870d..ec7620c5c 100644 --- a/tests/regression_tests/mgxs_library_mesh/results_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/results_true.dat @@ -1,189 +1,189 @@ - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.103374 0.004981 -2 1 2 1 1 total 0.103852 0.004752 -1 2 1 1 1 total 0.103322 0.003613 -3 2 2 1 1 total 0.102889 0.003387 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.072760 0.005235 -2 1 2 1 1 total 0.074856 0.004972 -1 2 1 1 1 total 0.074583 0.004000 -3 2 2 1 1 total 0.072925 0.003485 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.072768 0.005236 -2 1 2 1 1 total 0.074864 0.004972 -1 2 1 1 1 total 0.074603 0.004002 -3 2 2 1 1 total 0.072881 0.003491 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013309 0.000729 -2 1 2 1 1 total 0.013223 0.000707 -1 2 1 1 1 total 0.013222 0.000596 -3 2 2 1 1 total 0.013282 0.000564 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013241 0.000728 -2 1 2 1 1 total 0.013142 0.000705 -1 2 1 1 1 total 0.013137 0.000596 -3 2 2 1 1 total 0.013221 0.000564 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.001281 0.000874 -2 1 2 1 1 total 0.001297 0.000731 -1 2 1 1 1 total 0.001281 0.000744 -3 2 2 1 1 total 0.001288 0.000719 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.012029 0.000664 -2 1 2 1 1 total 0.011926 0.000639 -1 2 1 1 1 total 0.011941 0.000545 -3 2 2 1 1 total 0.011994 0.000515 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.031361 0.001735 -2 1 2 1 1 total 0.031091 0.001675 -1 2 1 1 1 total 0.031169 0.001418 -3 2 2 1 1 total 0.031255 0.001366 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 2.326447e+06 128504.886535 -2 1 2 1 1 total 2.306518e+06 123638.096130 -1 2 1 1 1 total 2.309435e+06 105395.059055 -3 2 2 1 1 total 2.319667e+06 99692.620001 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.090065 0.004262 -2 1 2 1 1 total 0.090629 0.004086 -1 2 1 1 1 total 0.090100 0.003045 -3 2 2 1 1 total 0.089607 0.002828 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.090188 0.005151 -2 1 2 1 1 total 0.094339 0.004349 -1 2 1 1 1 total 0.088294 0.003953 -3 2 2 1 1 total 0.089633 0.002592 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090122 0.005149 -1 1 1 1 1 1 P1 total 0.030614 0.001612 -2 1 1 1 1 1 P2 total 0.016490 0.000897 -3 1 1 1 1 1 P3 total 0.010163 0.000829 -8 1 2 1 1 1 P0 total 0.094307 0.004337 -9 1 2 1 1 1 P1 total 0.028996 0.001462 -10 1 2 1 1 1 P2 total 0.017486 0.000808 -11 1 2 1 1 1 P3 total 0.009753 0.000742 -4 2 1 1 1 1 P0 total 0.088198 0.003959 -5 2 1 1 1 1 P1 total 0.028739 0.001715 -6 2 1 1 1 1 P2 total 0.015608 0.000680 -7 2 1 1 1 1 P3 total 0.008232 0.000363 -12 2 2 1 1 1 P0 total 0.089536 0.002551 -13 2 2 1 1 1 P1 total 0.029964 0.000821 -14 2 2 1 1 1 P2 total 0.015742 0.001260 -15 2 2 1 1 1 P3 total 0.008944 0.000385 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090188 0.005151 -1 1 1 1 1 1 P1 total 0.030606 0.001614 -2 1 1 1 1 1 P2 total 0.016462 0.000898 -3 1 1 1 1 1 P3 total 0.010173 0.000824 -8 1 2 1 1 1 P0 total 0.094339 0.004349 -9 1 2 1 1 1 P1 total 0.028988 0.001462 -10 1 2 1 1 1 P2 total 0.017473 0.000811 -11 1 2 1 1 1 P3 total 0.009763 0.000749 -4 2 1 1 1 1 P0 total 0.088294 0.003953 -5 2 1 1 1 1 P1 total 0.028719 0.001720 -6 2 1 1 1 1 P2 total 0.015571 0.000691 -7 2 1 1 1 1 P3 total 0.008255 0.000361 -12 2 2 1 1 1 P0 total 0.089633 0.002592 -13 2 2 1 1 1 P1 total 0.030008 0.000847 -14 2 2 1 1 1 P2 total 0.015748 0.001284 -15 2 2 1 1 1 P3 total 0.008951 0.000387 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.000736 0.060260 -2 1 2 1 1 1 total 1.000346 0.042656 -1 2 1 1 1 1 total 1.001091 0.052736 -3 2 2 1 1 1 total 1.001078 0.038212 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.031890 0.002523 -2 1 2 1 1 1 total 0.032714 0.001502 -1 2 1 1 1 1 total 0.030414 0.002369 -3 2 2 1 1 1 total 0.031051 0.001521 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 0.060234 -2 1 2 1 1 1 total 1.0 0.042523 -1 2 1 1 1 1 total 1.0 0.052777 -3 2 2 1 1 1 total 1.0 0.037842 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090065 0.006899 -1 1 1 1 1 1 P1 total 0.030595 0.002243 -2 1 1 1 1 1 P2 total 0.016480 0.001229 -3 1 1 1 1 1 P3 total 0.010157 0.000977 -8 1 2 1 1 1 P0 total 0.090629 0.005616 -9 1 2 1 1 1 P1 total 0.027865 0.001820 -10 1 2 1 1 1 P2 total 0.016804 0.001044 -11 1 2 1 1 1 P3 total 0.009373 0.000813 -4 2 1 1 1 1 P0 total 0.090100 0.005647 -5 2 1 1 1 1 P1 total 0.029359 0.002172 -6 2 1 1 1 1 P2 total 0.015944 0.000984 -7 2 1 1 1 1 P3 total 0.008410 0.000523 -12 2 2 1 1 1 P0 total 0.089607 0.004415 -13 2 2 1 1 1 P1 total 0.029988 0.001459 -14 2 2 1 1 1 P2 total 0.015755 0.001411 -15 2 2 1 1 1 P3 total 0.008951 0.000528 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090131 0.008782 -1 1 1 1 1 1 P1 total 0.030617 0.002905 -2 1 1 1 1 1 P2 total 0.016492 0.001581 -3 1 1 1 1 1 P3 total 0.010164 0.001154 -8 1 2 1 1 1 P0 total 0.090661 0.006820 -9 1 2 1 1 1 P1 total 0.027875 0.002174 -10 1 2 1 1 1 P2 total 0.016810 0.001267 -11 1 2 1 1 1 P3 total 0.009376 0.000906 -4 2 1 1 1 1 P0 total 0.090199 0.007385 -5 2 1 1 1 1 P1 total 0.029391 0.002669 -6 2 1 1 1 1 P2 total 0.015962 0.001295 -7 2 1 1 1 1 P3 total 0.008419 0.000686 -12 2 2 1 1 1 P0 total 0.089704 0.005591 -13 2 2 1 1 1 P1 total 0.030020 0.001856 -14 2 2 1 1 1 P2 total 0.015772 0.001536 -15 2 2 1 1 1 P3 total 0.008961 0.000629 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.098093 -2 1 2 1 1 total 1.0 0.042346 -1 2 1 1 1 total 1.0 0.104352 -3 2 2 1 1 total 1.0 0.067889 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.099401 -2 1 2 1 1 total 1.0 0.042085 -1 2 1 1 1 total 1.0 0.104135 -3 2 2 1 1 total 1.0 0.067307 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 8.547713e-10 3.477664e-11 -2 1 2 1 1 total 9.057083e-10 4.171858e-11 -1 2 1 1 1 total 8.666731e-10 2.371072e-11 -3 2 2 1 1 total 8.532016e-10 1.638929e-11 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.031163 0.001724 -2 1 2 1 1 total 0.030895 0.001664 -1 2 1 1 1 total 0.030973 0.001409 -3 2 2 1 1 total 0.031058 0.001358 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.031781 0.002541 -2 1 2 1 1 1 total 0.032490 0.001487 -1 2 1 1 1 1 total 0.030274 0.002354 -3 2 2 1 1 1 total 0.030882 0.001500 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.103374 0.004981 +2 1 2 1 total 0.103852 0.004752 +1 2 1 1 total 0.103322 0.003613 +3 2 2 1 total 0.102889 0.003387 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.072760 0.005235 +2 1 2 1 total 0.074856 0.004972 +1 2 1 1 total 0.074583 0.004000 +3 2 2 1 total 0.072925 0.003485 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.072768 0.005236 +2 1 2 1 total 0.074864 0.004972 +1 2 1 1 total 0.074603 0.004002 +3 2 2 1 total 0.072881 0.003491 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.013309 0.000729 +2 1 2 1 total 0.013223 0.000707 +1 2 1 1 total 0.013222 0.000596 +3 2 2 1 total 0.013282 0.000564 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.013241 0.000728 +2 1 2 1 total 0.013142 0.000705 +1 2 1 1 total 0.013137 0.000596 +3 2 2 1 total 0.013221 0.000564 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.001281 0.000874 +2 1 2 1 total 0.001297 0.000731 +1 2 1 1 total 0.001281 0.000744 +3 2 2 1 total 0.001288 0.000719 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.012029 0.000664 +2 1 2 1 total 0.011926 0.000639 +1 2 1 1 total 0.011941 0.000545 +3 2 2 1 total 0.011994 0.000515 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.031361 0.001735 +2 1 2 1 total 0.031091 0.001675 +1 2 1 1 total 0.031169 0.001418 +3 2 2 1 total 0.031255 0.001366 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 2.326447e+06 128504.886535 +2 1 2 1 total 2.306518e+06 123638.096130 +1 2 1 1 total 2.309435e+06 105395.059055 +3 2 2 1 total 2.319667e+06 99692.620001 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.090065 0.004262 +2 1 2 1 total 0.090629 0.004086 +1 2 1 1 total 0.090100 0.003045 +3 2 2 1 total 0.089607 0.002828 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.090188 0.005151 +2 1 2 1 total 0.094339 0.004349 +1 2 1 1 total 0.088294 0.003953 +3 2 2 1 total 0.089633 0.002592 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090122 0.005149 +1 1 1 1 1 P1 total 0.030614 0.001612 +2 1 1 1 1 P2 total 0.016490 0.000897 +3 1 1 1 1 P3 total 0.010163 0.000829 +8 1 2 1 1 P0 total 0.094307 0.004337 +9 1 2 1 1 P1 total 0.028996 0.001462 +10 1 2 1 1 P2 total 0.017486 0.000808 +11 1 2 1 1 P3 total 0.009753 0.000742 +4 2 1 1 1 P0 total 0.088198 0.003959 +5 2 1 1 1 P1 total 0.028739 0.001715 +6 2 1 1 1 P2 total 0.015608 0.000680 +7 2 1 1 1 P3 total 0.008232 0.000363 +12 2 2 1 1 P0 total 0.089536 0.002551 +13 2 2 1 1 P1 total 0.029964 0.000821 +14 2 2 1 1 P2 total 0.015742 0.001260 +15 2 2 1 1 P3 total 0.008944 0.000385 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090188 0.005151 +1 1 1 1 1 P1 total 0.030606 0.001614 +2 1 1 1 1 P2 total 0.016462 0.000898 +3 1 1 1 1 P3 total 0.010173 0.000824 +8 1 2 1 1 P0 total 0.094339 0.004349 +9 1 2 1 1 P1 total 0.028988 0.001462 +10 1 2 1 1 P2 total 0.017473 0.000811 +11 1 2 1 1 P3 total 0.009763 0.000749 +4 2 1 1 1 P0 total 0.088294 0.003953 +5 2 1 1 1 P1 total 0.028719 0.001720 +6 2 1 1 1 P2 total 0.015571 0.000691 +7 2 1 1 1 P3 total 0.008255 0.000361 +12 2 2 1 1 P0 total 0.089633 0.002592 +13 2 2 1 1 P1 total 0.030008 0.000847 +14 2 2 1 1 P2 total 0.015748 0.001284 +15 2 2 1 1 P3 total 0.008951 0.000387 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.000736 0.060260 +2 1 2 1 1 total 1.000346 0.042656 +1 2 1 1 1 total 1.001091 0.052736 +3 2 2 1 1 total 1.001078 0.038212 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.031890 0.002523 +2 1 2 1 1 total 0.032714 0.001502 +1 2 1 1 1 total 0.030414 0.002369 +3 2 2 1 1 total 0.031051 0.001521 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 0.060234 +2 1 2 1 1 total 1.0 0.042523 +1 2 1 1 1 total 1.0 0.052777 +3 2 2 1 1 total 1.0 0.037842 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090065 0.006899 +1 1 1 1 1 P1 total 0.030595 0.002243 +2 1 1 1 1 P2 total 0.016480 0.001229 +3 1 1 1 1 P3 total 0.010157 0.000977 +8 1 2 1 1 P0 total 0.090629 0.005616 +9 1 2 1 1 P1 total 0.027865 0.001820 +10 1 2 1 1 P2 total 0.016804 0.001044 +11 1 2 1 1 P3 total 0.009373 0.000813 +4 2 1 1 1 P0 total 0.090100 0.005647 +5 2 1 1 1 P1 total 0.029359 0.002172 +6 2 1 1 1 P2 total 0.015944 0.000984 +7 2 1 1 1 P3 total 0.008410 0.000523 +12 2 2 1 1 P0 total 0.089607 0.004415 +13 2 2 1 1 P1 total 0.029988 0.001459 +14 2 2 1 1 P2 total 0.015755 0.001411 +15 2 2 1 1 P3 total 0.008951 0.000528 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090131 0.008782 +1 1 1 1 1 P1 total 0.030617 0.002905 +2 1 1 1 1 P2 total 0.016492 0.001581 +3 1 1 1 1 P3 total 0.010164 0.001154 +8 1 2 1 1 P0 total 0.090661 0.006820 +9 1 2 1 1 P1 total 0.027875 0.002174 +10 1 2 1 1 P2 total 0.016810 0.001267 +11 1 2 1 1 P3 total 0.009376 0.000906 +4 2 1 1 1 P0 total 0.090199 0.007385 +5 2 1 1 1 P1 total 0.029391 0.002669 +6 2 1 1 1 P2 total 0.015962 0.001295 +7 2 1 1 1 P3 total 0.008419 0.000686 +12 2 2 1 1 P0 total 0.089704 0.005591 +13 2 2 1 1 P1 total 0.030020 0.001856 +14 2 2 1 1 P2 total 0.015772 0.001536 +15 2 2 1 1 P3 total 0.008961 0.000629 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.098093 +2 1 2 1 total 1.0 0.042346 +1 2 1 1 total 1.0 0.104352 +3 2 2 1 total 1.0 0.067889 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.099401 +2 1 2 1 total 1.0 0.042085 +1 2 1 1 total 1.0 0.104135 +3 2 2 1 total 1.0 0.067307 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 8.547713e-10 3.477664e-11 +2 1 2 1 total 9.057083e-10 4.171858e-11 +1 2 1 1 total 8.666731e-10 2.371072e-11 +3 2 2 1 total 8.532016e-10 1.638929e-11 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.031163 0.001724 +2 1 2 1 total 0.030895 0.001664 +1 2 1 1 total 0.030973 0.001409 +3 2 2 1 total 0.031058 0.001358 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.031781 0.002541 +2 1 2 1 1 total 0.032490 0.001487 +1 2 1 1 1 total 0.030274 0.002354 +3 2 2 1 1 total 0.030882 0.001500 mesh 1 group in nuclide mean std. dev. x y surf 3 1 1 x-max in 1 total 0.1888 0.008218 @@ -218,145 +218,145 @@ 30 2 2 y-max out 1 total 0.0000 0.000000 29 2 2 y-min in 1 total 0.1870 0.009597 28 2 2 y-min out 1 total 0.1850 0.011256 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 4.581283 0.329623 -2 1 2 1 1 total 4.452968 0.295762 -1 2 1 1 1 total 4.469295 0.239674 -3 2 2 1 1 total 4.570894 0.218416 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 4.580746 0.329583 -2 1 2 1 1 total 4.452519 0.295716 -1 2 1 1 1 total 4.468066 0.239680 -3 2 2 1 1 total 4.573657 0.219069 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000007 3.812309e-07 -1 1 1 1 2 1 total 0.000036 1.967797e-06 -2 1 1 1 3 1 total 0.000034 1.878630e-06 -3 1 1 1 4 1 total 0.000077 4.212043e-06 -4 1 1 1 5 1 total 0.000031 1.726878e-06 -5 1 1 1 6 1 total 0.000013 7.233832e-07 -12 1 2 1 1 1 total 0.000007 3.663985e-07 -13 1 2 1 2 1 total 0.000035 1.891236e-06 -14 1 2 1 3 1 total 0.000034 1.805539e-06 -15 1 2 1 4 1 total 0.000076 4.048166e-06 -16 1 2 1 5 1 total 0.000031 1.659691e-06 -17 1 2 1 6 1 total 0.000013 6.952388e-07 -6 2 1 1 1 1 total 0.000007 3.123173e-07 -7 2 1 1 2 1 total 0.000035 1.612086e-06 -8 2 1 1 3 1 total 0.000034 1.539037e-06 -9 2 1 1 4 1 total 0.000076 3.450649e-06 -10 2 1 1 5 1 total 0.000031 1.414717e-06 -11 2 1 1 6 1 total 0.000013 5.926201e-07 -18 2 2 1 1 1 total 0.000007 2.946716e-07 -19 2 2 1 2 1 total 0.000036 1.521004e-06 -20 2 2 1 3 1 total 0.000034 1.452083e-06 -21 2 2 1 4 1 total 0.000076 3.255689e-06 -22 2 2 1 5 1 total 0.000031 1.334787e-06 -23 2 2 1 6 1 total 0.000013 5.591374e-07 - mesh 1 delayedgroup group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 1.414214 -1 1 1 1 2 1 total 0.0 0.000000 -2 1 1 1 3 1 total 0.0 0.000000 -3 1 1 1 4 1 total 1.0 0.578922 -4 1 1 1 5 1 total 0.0 0.000000 -5 1 1 1 6 1 total 0.0 0.000000 -12 1 2 1 1 1 total 0.0 0.000000 -13 1 2 1 2 1 total 1.0 0.578922 -14 1 2 1 3 1 total 1.0 1.414214 -15 1 2 1 4 1 total 1.0 1.414214 -16 1 2 1 5 1 total 1.0 0.875472 -17 1 2 1 6 1 total 1.0 1.414214 -6 2 1 1 1 1 total 0.0 0.000000 -7 2 1 1 2 1 total 0.0 0.000000 -8 2 1 1 3 1 total 1.0 1.414214 -9 2 1 1 4 1 total 1.0 0.579392 -10 2 1 1 5 1 total 1.0 1.414214 -11 2 1 1 6 1 total 0.0 0.000000 -18 2 2 1 1 1 total 1.0 0.868163 -19 2 2 1 2 1 total 1.0 1.414214 -20 2 2 1 3 1 total 0.0 0.000000 -21 2 2 1 4 1 total 1.0 0.868969 -22 2 2 1 5 1 total 1.0 1.414214 -23 2 2 1 6 1 total 0.0 0.000000 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000221 0.000015 -1 1 1 1 2 1 total 0.001140 0.000079 -2 1 1 1 3 1 total 0.001088 0.000075 -3 1 1 1 4 1 total 0.002440 0.000169 -4 1 1 1 5 1 total 0.001001 0.000069 -5 1 1 1 6 1 total 0.000419 0.000029 -12 1 2 1 1 1 total 0.000221 0.000013 -13 1 2 1 2 1 total 0.001139 0.000066 -14 1 2 1 3 1 total 0.001088 0.000063 -15 1 2 1 4 1 total 0.002439 0.000142 -16 1 2 1 5 1 total 0.001000 0.000058 -17 1 2 1 6 1 total 0.000419 0.000024 -6 2 1 1 1 1 total 0.000220 0.000013 -7 2 1 1 2 1 total 0.001138 0.000067 -8 2 1 1 3 1 total 0.001086 0.000064 -9 2 1 1 4 1 total 0.002435 0.000144 -10 2 1 1 5 1 total 0.000998 0.000059 -11 2 1 1 6 1 total 0.000418 0.000025 -18 2 2 1 1 1 total 0.000221 0.000013 -19 2 2 1 2 1 total 0.001141 0.000066 -20 2 2 1 3 1 total 0.001090 0.000063 -21 2 2 1 4 1 total 0.002443 0.000141 -22 2 2 1 5 1 total 0.001002 0.000058 -23 2 2 1 6 1 total 0.000420 0.000024 - mesh 1 delayedgroup nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013336 0.000919 -1 1 1 1 2 total 0.032739 0.002257 -2 1 1 1 3 total 0.120780 0.008325 -3 1 1 1 4 total 0.302780 0.020871 -4 1 1 1 5 total 0.849490 0.058555 -5 1 1 1 6 total 2.853000 0.196656 -12 1 2 1 1 total 0.013336 0.000770 -13 1 2 1 2 total 0.032739 0.001891 -14 1 2 1 3 total 0.120780 0.006977 -15 1 2 1 4 total 0.302780 0.017490 -16 1 2 1 5 total 0.849490 0.049071 -17 1 2 1 6 total 2.853000 0.164804 -6 2 1 1 1 total 0.013336 0.000790 -7 2 1 1 2 total 0.032739 0.001940 -8 2 1 1 3 total 0.120780 0.007157 -9 2 1 1 4 total 0.302780 0.017942 -10 2 1 1 5 total 0.849490 0.050338 -11 2 1 1 6 total 2.853000 0.169061 -18 2 2 1 1 total 0.013336 0.000757 -19 2 2 1 2 total 0.032739 0.001858 -20 2 2 1 3 total 0.120780 0.006855 -21 2 2 1 4 total 0.302780 0.017186 -22 2 2 1 5 total 0.849490 0.048217 -23 2 2 1 6 total 2.853000 0.161935 - mesh 1 delayedgroup group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 1 total 0.000026 0.000026 -1 1 1 1 2 1 1 total 0.000000 0.000000 -2 1 1 1 3 1 1 total 0.000000 0.000000 -3 1 1 1 4 1 1 total 0.000083 0.000034 -4 1 1 1 5 1 1 total 0.000000 0.000000 -5 1 1 1 6 1 1 total 0.000000 0.000000 -12 1 2 1 1 1 1 total 0.000000 0.000000 -13 1 2 1 2 1 1 total 0.000081 0.000033 -14 1 2 1 3 1 1 total 0.000026 0.000026 -15 1 2 1 4 1 1 total 0.000026 0.000026 -16 1 2 1 5 1 1 total 0.000059 0.000036 -17 1 2 1 6 1 1 total 0.000033 0.000033 -6 2 1 1 1 1 1 total 0.000000 0.000000 -7 2 1 1 2 1 1 total 0.000000 0.000000 -8 2 1 1 3 1 1 total 0.000032 0.000032 -9 2 1 1 4 1 1 total 0.000080 0.000033 -10 2 1 1 5 1 1 total 0.000028 0.000028 -11 2 1 1 6 1 1 total 0.000000 0.000000 -18 2 2 1 1 1 1 total 0.000054 0.000033 -19 2 2 1 2 1 1 total 0.000029 0.000029 -20 2 2 1 3 1 1 total 0.000000 0.000000 -21 2 2 1 4 1 1 total 0.000054 0.000033 -22 2 2 1 5 1 1 total 0.000032 0.000032 -23 2 2 1 6 1 1 total 0.000000 0.000000 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 4.581283 0.329623 +2 1 2 1 total 4.452968 0.295762 +1 2 1 1 total 4.469295 0.239674 +3 2 2 1 total 4.570894 0.218416 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 4.580746 0.329583 +2 1 2 1 total 4.452519 0.295716 +1 2 1 1 total 4.468066 0.239680 +3 2 2 1 total 4.573657 0.219069 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000007 3.812309e-07 +1 1 1 2 1 total 0.000036 1.967797e-06 +2 1 1 3 1 total 0.000034 1.878630e-06 +3 1 1 4 1 total 0.000077 4.212043e-06 +4 1 1 5 1 total 0.000031 1.726878e-06 +5 1 1 6 1 total 0.000013 7.233832e-07 +12 1 2 1 1 total 0.000007 3.663985e-07 +13 1 2 2 1 total 0.000035 1.891236e-06 +14 1 2 3 1 total 0.000034 1.805539e-06 +15 1 2 4 1 total 0.000076 4.048166e-06 +16 1 2 5 1 total 0.000031 1.659691e-06 +17 1 2 6 1 total 0.000013 6.952388e-07 +6 2 1 1 1 total 0.000007 3.123173e-07 +7 2 1 2 1 total 0.000035 1.612086e-06 +8 2 1 3 1 total 0.000034 1.539037e-06 +9 2 1 4 1 total 0.000076 3.450649e-06 +10 2 1 5 1 total 0.000031 1.414717e-06 +11 2 1 6 1 total 0.000013 5.926201e-07 +18 2 2 1 1 total 0.000007 2.946716e-07 +19 2 2 2 1 total 0.000036 1.521004e-06 +20 2 2 3 1 total 0.000034 1.452083e-06 +21 2 2 4 1 total 0.000076 3.255689e-06 +22 2 2 5 1 total 0.000031 1.334787e-06 +23 2 2 6 1 total 0.000013 5.591374e-07 + mesh 1 delayedgroup group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 1.414214 +1 1 1 2 1 total 0.0 0.000000 +2 1 1 3 1 total 0.0 0.000000 +3 1 1 4 1 total 1.0 0.578922 +4 1 1 5 1 total 0.0 0.000000 +5 1 1 6 1 total 0.0 0.000000 +12 1 2 1 1 total 0.0 0.000000 +13 1 2 2 1 total 1.0 0.578922 +14 1 2 3 1 total 1.0 1.414214 +15 1 2 4 1 total 1.0 1.414214 +16 1 2 5 1 total 1.0 0.875472 +17 1 2 6 1 total 1.0 1.414214 +6 2 1 1 1 total 0.0 0.000000 +7 2 1 2 1 total 0.0 0.000000 +8 2 1 3 1 total 1.0 1.414214 +9 2 1 4 1 total 1.0 0.579392 +10 2 1 5 1 total 1.0 1.414214 +11 2 1 6 1 total 0.0 0.000000 +18 2 2 1 1 total 1.0 0.868163 +19 2 2 2 1 total 1.0 1.414214 +20 2 2 3 1 total 0.0 0.000000 +21 2 2 4 1 total 1.0 0.868969 +22 2 2 5 1 total 1.0 1.414214 +23 2 2 6 1 total 0.0 0.000000 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000221 0.000015 +1 1 1 2 1 total 0.001140 0.000079 +2 1 1 3 1 total 0.001088 0.000075 +3 1 1 4 1 total 0.002440 0.000169 +4 1 1 5 1 total 0.001001 0.000069 +5 1 1 6 1 total 0.000419 0.000029 +12 1 2 1 1 total 0.000221 0.000013 +13 1 2 2 1 total 0.001139 0.000066 +14 1 2 3 1 total 0.001088 0.000063 +15 1 2 4 1 total 0.002439 0.000142 +16 1 2 5 1 total 0.001000 0.000058 +17 1 2 6 1 total 0.000419 0.000024 +6 2 1 1 1 total 0.000220 0.000013 +7 2 1 2 1 total 0.001138 0.000067 +8 2 1 3 1 total 0.001086 0.000064 +9 2 1 4 1 total 0.002435 0.000144 +10 2 1 5 1 total 0.000998 0.000059 +11 2 1 6 1 total 0.000418 0.000025 +18 2 2 1 1 total 0.000221 0.000013 +19 2 2 2 1 total 0.001141 0.000066 +20 2 2 3 1 total 0.001090 0.000063 +21 2 2 4 1 total 0.002443 0.000141 +22 2 2 5 1 total 0.001002 0.000058 +23 2 2 6 1 total 0.000420 0.000024 + mesh 1 delayedgroup nuclide mean std. dev. + x y +0 1 1 1 total 0.013336 0.000919 +1 1 1 2 total 0.032739 0.002257 +2 1 1 3 total 0.120780 0.008325 +3 1 1 4 total 0.302780 0.020871 +4 1 1 5 total 0.849490 0.058555 +5 1 1 6 total 2.853000 0.196656 +12 1 2 1 total 0.013336 0.000770 +13 1 2 2 total 0.032739 0.001891 +14 1 2 3 total 0.120780 0.006977 +15 1 2 4 total 0.302780 0.017490 +16 1 2 5 total 0.849490 0.049071 +17 1 2 6 total 2.853000 0.164804 +6 2 1 1 total 0.013336 0.000790 +7 2 1 2 total 0.032739 0.001940 +8 2 1 3 total 0.120780 0.007157 +9 2 1 4 total 0.302780 0.017942 +10 2 1 5 total 0.849490 0.050338 +11 2 1 6 total 2.853000 0.169061 +18 2 2 1 total 0.013336 0.000757 +19 2 2 2 total 0.032739 0.001858 +20 2 2 3 total 0.120780 0.006855 +21 2 2 4 total 0.302780 0.017186 +22 2 2 5 total 0.849490 0.048217 +23 2 2 6 total 2.853000 0.161935 + mesh 1 delayedgroup group in group out nuclide mean std. dev. + x y +0 1 1 1 1 1 total 0.000026 0.000026 +1 1 1 2 1 1 total 0.000000 0.000000 +2 1 1 3 1 1 total 0.000000 0.000000 +3 1 1 4 1 1 total 0.000083 0.000034 +4 1 1 5 1 1 total 0.000000 0.000000 +5 1 1 6 1 1 total 0.000000 0.000000 +12 1 2 1 1 1 total 0.000000 0.000000 +13 1 2 2 1 1 total 0.000081 0.000033 +14 1 2 3 1 1 total 0.000026 0.000026 +15 1 2 4 1 1 total 0.000026 0.000026 +16 1 2 5 1 1 total 0.000059 0.000036 +17 1 2 6 1 1 total 0.000033 0.000033 +6 2 1 1 1 1 total 0.000000 0.000000 +7 2 1 2 1 1 total 0.000000 0.000000 +8 2 1 3 1 1 total 0.000032 0.000032 +9 2 1 4 1 1 total 0.000080 0.000033 +10 2 1 5 1 1 total 0.000028 0.000028 +11 2 1 6 1 1 total 0.000000 0.000000 +18 2 2 1 1 1 total 0.000054 0.000033 +19 2 2 2 1 1 total 0.000029 0.000029 +20 2 2 3 1 1 total 0.000000 0.000000 +21 2 2 4 1 1 total 0.000054 0.000033 +22 2 2 5 1 1 total 0.000032 0.000032 +23 2 2 6 1 1 total 0.000000 0.000000 diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py index 70833bb39..781be3ef7 100644 --- a/tests/regression_tests/microxs/test.py +++ b/tests/regression_tests/microxs/test.py @@ -34,6 +34,7 @@ def model(): [ ("materials", "direct"), ("materials", "flux"), + ("materials", "hybrid"), ("mesh", "direct"), ("mesh", "flux"), ] @@ -49,12 +50,21 @@ def test_from_model(model, domain_type, rr_mode): domains = mesh nuclides = ['U235', 'O16', 'Xe135'] kwargs = { - 'reaction_rate_mode': rr_mode, 'chain_file': CHAIN_FILE, 'path_statepoint': 'neutron_transport.h5', } if rr_mode == 'flux': + kwargs['reaction_rate_mode'] = 'flux' kwargs['energies'] = 'CASMO-40' + elif rr_mode == 'hybrid': + kwargs['reaction_rate_mode'] = 'flux' + kwargs['energies'] = 'CASMO-40' + kwargs['reaction_rate_opts'] = { + 'nuclides': ['U235'], + 'reactions': ['fission'], + } + else: + kwargs['reaction_rate_mode'] = rr_mode _, test_xs = get_microxs_and_flux(model, domains, nuclides, **kwargs) if config['update']: test_xs[0].to_csv(f'test_reference_{domain_type}_{rr_mode}.csv') diff --git a/tests/regression_tests/microxs/test_reference_materials_hybrid.csv b/tests/regression_tests/microxs/test_reference_materials_hybrid.csv new file mode 100644 index 000000000..6d17a5e2b --- /dev/null +++ b/tests/regression_tests/microxs/test_reference_materials_hybrid.csv @@ -0,0 +1,7 @@ +nuclides,reactions,groups,xs +U235,"(n,gamma)",1,0.1500301670375847 +U235,fission,1,1.2578145727734291 +O16,"(n,gamma)",1,0.00012069778439640312 +O16,fission,1,0.0 +Xe135,"(n,gamma)",1,0.014820264774863558 +Xe135,fission,1,0.0 diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index 07eebaa3e..0ae0079f4 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -41,6 +41,16 @@ neutron photon electron positron + + neutron + + + 0.0 20000000.0 + + + neutron photon + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 + 1 2 current @@ -63,5 +73,10 @@ total heating (n,gamma) analog + + 5 3 4 + events + analog + diff --git a/tests/regression_tests/multipole/inputs_true.dat b/tests/regression_tests/multipole/inputs_true.dat index 22a351240..3b78db77b 100644 --- a/tests/regression_tests/multipole/inputs_true.dat +++ b/tests/regression_tests/multipole/inputs_true.dat @@ -14,7 +14,9 @@ - + + 500 700 0 800 + diff --git a/tests/regression_tests/particle_production_fission/__init__.py b/tests/regression_tests/particle_production_fission/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/particle_production_fission/local/inputs_true.dat b/tests/regression_tests/particle_production_fission/local/inputs_true.dat new file mode 100644 index 000000000..e93dfc36e --- /dev/null +++ b/tests/regression_tests/particle_production_fission/local/inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + fixed source + 100 + 2 + + + 1000000.0 1.0 + + + true + false + + + + neutron + + + neutron + + + 2 1 + events + analog + + + diff --git a/tests/regression_tests/particle_production_fission/local/results_true.dat b/tests/regression_tests/particle_production_fission/local/results_true.dat new file mode 100644 index 000000000..2f5288eba --- /dev/null +++ b/tests/regression_tests/particle_production_fission/local/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +4.570000E+00 +1.047890E+01 diff --git a/tests/regression_tests/particle_production_fission/shared/inputs_true.dat b/tests/regression_tests/particle_production_fission/shared/inputs_true.dat new file mode 100644 index 000000000..b50a39cd0 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/shared/inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + fixed source + 100 + 2 + + + 1000000.0 1.0 + + + true + true + + + + neutron + + + neutron + + + 2 1 + events + analog + + + diff --git a/tests/regression_tests/particle_production_fission/shared/results_true.dat b/tests/regression_tests/particle_production_fission/shared/results_true.dat new file mode 100644 index 000000000..5a4dfc516 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/shared/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +5.180000E+00 +1.355140E+01 diff --git a/tests/regression_tests/particle_production_fission/test.py b/tests/regression_tests/particle_production_fission/test.py new file mode 100644 index 000000000..2ad6ca515 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/test.py @@ -0,0 +1,49 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_particle_production_fission(shared_secondary, subdir): + """Fixed-source model with fissionable material to test that + ParticleProductionFilter correctly counts fission-born neutrons, + with both local and shared secondary bank modes.""" + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() + + mat = openmc.Material() + mat.set_density('g/cm3', 18.0) + mat.add_nuclide('U235', 1.0) + model.materials.append(mat) + + sph = openmc.Sphere(r=5.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sph) + model.geometry = openmc.Geometry([cell]) + + source = openmc.IndependentSource() + source.energy = openmc.stats.delta_function(1.0e6) + + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.batches = 2 + model.settings.source = source + model.settings.create_fission_neutrons = True + model.settings.shared_secondary_bank = shared_secondary + + # ParticleProductionFilter tracking fission neutron production + ppf = openmc.ParticleProductionFilter(['neutron']) + neutron_filter = openmc.ParticleFilter(['neutron']) + tally = openmc.Tally() + tally.filters = [neutron_filter, ppf] + tally.scores = ['events'] + tally.estimator = 'analog' + model.tallies = [tally] + + harness = PyAPITestHarness('statepoint.2.h5', model) + harness.main() diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py b/tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml new file mode 100644 index 000000000..c86e016c6 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml new file mode 100644 index 000000000..f3851d7ef --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat b/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat new file mode 100644 index 000000000..0c84541f9 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat @@ -0,0 +1,16 @@ +current batch: +4.000000E+00 +current generation: +1.000000E+00 +particle id: +3.241000E+03 +run mode: +fixed source +particle weight: +1.000000E+00 +particle energy: +3.896365E+06 +particle xyz: +8.710681E-01 3.698823E+00 -2.286229E+00 +particle uvw: +-5.882735E-01 4.665422E-01 -6.605093E-01 diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml new file mode 100644 index 000000000..3a80fb17b --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml @@ -0,0 +1,15 @@ + + + + fixed source + 12 + 1000 + true + + + + -10 -10 -5 10 10 5 + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py b/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py new file mode 100644 index 000000000..770010900 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py @@ -0,0 +1,6 @@ +from tests.testing_harness import ParticleRestartTestHarness + + +def test_particle_restart_fixed_shared_secondary(): + harness = ParticleRestartTestHarness('particle_4_3241.h5') + harness.main() diff --git a/tests/regression_tests/periodic_6fold/inputs_true.dat b/tests/regression_tests/periodic_6fold/False-False/inputs_true.dat similarity index 100% rename from tests/regression_tests/periodic_6fold/inputs_true.dat rename to tests/regression_tests/periodic_6fold/False-False/inputs_true.dat diff --git a/tests/regression_tests/periodic_6fold/results_true.dat b/tests/regression_tests/periodic_6fold/False-False/results_true.dat similarity index 100% rename from tests/regression_tests/periodic_6fold/results_true.dat rename to tests/regression_tests/periodic_6fold/False-False/results_true.dat diff --git a/tests/regression_tests/periodic_6fold/False-True/inputs_true.dat b/tests/regression_tests/periodic_6fold/False-True/inputs_true.dat new file mode 100644 index 000000000..cbc1e9100 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/False-True/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 5 5 0 + + + + diff --git a/tests/regression_tests/periodic_6fold/False-True/results_true.dat b/tests/regression_tests/periodic_6fold/False-True/results_true.dat new file mode 100644 index 000000000..04aa30878 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/False-True/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_6fold/True-False/inputs_true.dat b/tests/regression_tests/periodic_6fold/True-False/inputs_true.dat new file mode 100644 index 000000000..675a8b300 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-False/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 5 5 0 + + + + diff --git a/tests/regression_tests/periodic_6fold/True-False/results_true.dat b/tests/regression_tests/periodic_6fold/True-False/results_true.dat new file mode 100644 index 000000000..04aa30878 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-False/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_6fold/True-True/inputs_true.dat b/tests/regression_tests/periodic_6fold/True-True/inputs_true.dat new file mode 100644 index 000000000..e5ac03b48 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-True/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 5 5 0 + + + + diff --git a/tests/regression_tests/periodic_6fold/True-True/results_true.dat b/tests/regression_tests/periodic_6fold/True-True/results_true.dat new file mode 100644 index 000000000..04aa30878 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-True/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_6fold/test.py b/tests/regression_tests/periodic_6fold/test.py index 272c65df5..bded1c465 100644 --- a/tests/regression_tests/periodic_6fold/test.py +++ b/tests/regression_tests/periodic_6fold/test.py @@ -1,14 +1,16 @@ from math import sin, cos, pi import openmc +from openmc.utility_funcs import change_directory import pytest from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def model(): - model = openmc.model.Model() +@pytest.mark.parametrize("flip1", [False, True]) +@pytest.mark.parametrize("flip2", [False, True]) +def test_periodic(flip1, flip2): + model = openmc.Model() # Define materials water = openmc.Material() @@ -27,31 +29,52 @@ def model(): # answers. theta1 = (-1/6 + 1/2) * pi theta2 = (1/6 - 1/2) * pi - plane1 = openmc.Plane(a=cos(theta1), b=sin(theta1), boundary_type='periodic') - plane2 = openmc.Plane(a=cos(theta2), b=sin(theta2), boundary_type='periodic') + if flip1: + plane1 = openmc.Plane(a=-cos(theta1), b=-sin(theta1), boundary_type='periodic') + else: + plane1 = openmc.Plane(a=cos(theta1), b=sin(theta1), boundary_type='periodic') + if flip2: + plane2 = openmc.Plane(a=-cos(theta2), b=-sin(theta2), boundary_type='periodic') + else: + plane2 = openmc.Plane(a=cos(theta2), b=sin(theta2), boundary_type='periodic') x_max = openmc.XPlane(5., boundary_type='reflective') z_cyl = openmc.ZCylinder(x0=3*cos(pi/6), y0=3*sin(pi/6), r=2.0) - outside_cyl = openmc.Cell(1, fill=water, region=( - +plane1 & +plane2 & -x_max & +z_cyl)) - inside_cyl = openmc.Cell(2, fill=fuel, region=( - +plane1 & +plane2 & -z_cyl)) + match (flip1, flip2): + case (False, False): + outside_cyl = openmc.Cell(1, fill=water, region=( + +plane1 & +plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + +plane1 & +plane2 & -z_cyl)) + case (False, True): + outside_cyl = openmc.Cell(1, fill=water, region=( + +plane1 & -plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + +plane1 & -plane2 & -z_cyl)) + case (True, False): + outside_cyl = openmc.Cell(1, fill=water, region=( + -plane1 & +plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + -plane1 & +plane2 & -z_cyl)) + case (True, True): + outside_cyl = openmc.Cell(1, fill=water, region=( + -plane1 & -plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + -plane1 & -plane2 & -z_cyl)) root_universe = openmc.Universe(0, cells=(outside_cyl, inside_cyl)) model.geometry = openmc.Geometry(root_universe) # Define settings - model.settings = openmc.Settings() model.settings.particles = 1000 model.settings.batches = 4 model.settings.inactive = 0 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - (0, 0, 0), (5, 5, 0)) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box((0, 0, 0), (5, 5, 0)) ) - return model + with change_directory(f'{flip1}-{flip2}'): + harness = PyAPITestHarness('statepoint.4.h5', model) + harness.main() -def test_periodic(model): - harness = PyAPITestHarness('statepoint.4.h5', model) - harness.main() diff --git a/tests/regression_tests/periodic_cyls/__init__.py b/tests/regression_tests/periodic_cyls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/periodic_cyls/test.py b/tests/regression_tests/periodic_cyls/test.py new file mode 100644 index 000000000..a341e3799 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/test.py @@ -0,0 +1,91 @@ +import openmc +import numpy as np +import pytest +from openmc.utility_funcs import change_directory +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def xcyl_model(): + model = openmc.Model() + # Define materials + fuel = openmc.Material() + fuel.add_nuclide('U235', 0.2) + fuel.add_nuclide('U238', 0.8) + fuel.set_density('g/cc', 19.1) + model.materials = openmc.Materials([fuel]) + + # Define geometry + # finite cylinder + x_min = openmc.XPlane(x0=0.0, boundary_type='reflective') + x_max = openmc.XPlane(x0=20.0, boundary_type='reflective') + x_cyl = openmc.XCylinder(r=20.0,boundary_type='vacuum') + # slice cylinder for periodic BC + periodic_bounding_yplane = openmc.YPlane(y0=0, boundary_type='periodic') + periodic_bounding_plane = openmc.Plane( + a=0.0, b=-np.sqrt(3) / 3, c=1, boundary_type='periodic', + ) + sixth_cyl_cell = openmc.Cell(1, fill=fuel, region = + +x_min &- x_max & -x_cyl & +periodic_bounding_yplane & +periodic_bounding_plane) + periodic_bounding_yplane.periodic_surface = periodic_bounding_plane + periodic_bounding_plane.periodic_surface = periodic_bounding_yplane + + model.geometry = openmc.Geometry([sixth_cyl_cell]) + + + # Define settings + model.settings.particles = 1000 + model.settings.batches = 4 + model.settings.inactive = 0 + model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( + (0, 0, 0), (20, 20, 20)) + ) + return model + +@pytest.fixture +def ycyl_model(): + model = openmc.Model() + # Define materials + fuel = openmc.Material() + fuel.add_nuclide('U235', 0.2) + fuel.add_nuclide('U238', 0.8) + fuel.set_density('g/cc', 19.1) + model.materials = openmc.Materials([fuel]) + + # Define geometry + # finite cylinder + y_min = openmc.YPlane(y0=0.0, boundary_type='reflective') + y_max = openmc.YPlane(y0=20.0, boundary_type='reflective') + y_cyl = openmc.YCylinder(r=20.0,boundary_type='vacuum') + # slice cylinder for periodic BC + periodic_bounding_xplane = openmc.XPlane(x0=0, boundary_type='periodic') + periodic_bounding_plane = openmc.Plane( + a=-np.sqrt(3) / 3, b=0.0, c=1, boundary_type='periodic', + ) + sixth_cyl_cell = openmc.Cell(1, fill=fuel, region = + +y_min &- y_max & -y_cyl & +periodic_bounding_xplane & +periodic_bounding_plane) + periodic_bounding_xplane.periodic_surface = periodic_bounding_plane + periodic_bounding_plane.periodic_surface = periodic_bounding_xplane + model.geometry = openmc.Geometry([sixth_cyl_cell]) + + + # Define settings + model.settings.particles = 1000 + model.settings.batches = 4 + model.settings.inactive = 0 + model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( + (0, 0, 0), (20, 20, 20)) + ) + return model + +def test_xcyl(xcyl_model): + with change_directory("xcyl_model"): + openmc.reset_auto_ids() + harness = PyAPITestHarness('statepoint.4.h5', xcyl_model) + harness.main() + +def test_ycyl(ycyl_model): + with change_directory("ycyl_model"): + openmc.reset_auto_ids() + harness = PyAPITestHarness('statepoint.4.h5', ycyl_model) + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat b/tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat new file mode 100644 index 000000000..7d1ecf426 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 20 20 20 + + + + diff --git a/tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat b/tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat new file mode 100644 index 000000000..c7e3eb670 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.082283E+00 6.676373E-02 diff --git a/tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat b/tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat new file mode 100644 index 000000000..f3ca7e0f4 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 20 20 20 + + + + diff --git a/tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat b/tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat new file mode 100644 index 000000000..562467f2c --- /dev/null +++ b/tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.082652E+00 3.316031E-02 diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index 07eebaa3e..0ae0079f4 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -41,6 +41,16 @@ neutron photon electron positron + + neutron + + + 0.0 20000000.0 + + + neutron photon + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 + 1 2 current @@ -63,5 +73,10 @@ total heating (n,gamma) analog + + 5 3 4 + events + analog + diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index 413f6f0ca..c4f5fafa2 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -138,3 +138,24 @@ tally 4: 2.173936E+08 0.000000E+00 0.000000E+00 +tally 5: +5.000000E-04 +2.500000E-07 +1.300000E-03 +1.690000E-06 +8.000000E-04 +6.400000E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +4.000000E-04 +8.200000E-03 +6.724000E-05 +5.280000E-02 +2.787840E-03 +3.546000E-01 +1.257412E-01 +5.063000E-01 +2.563397E-01 diff --git a/tests/regression_tests/photon_production/test.py b/tests/regression_tests/photon_production/test.py index 150448a12..5ed5c5ecf 100644 --- a/tests/regression_tests/photon_production/test.py +++ b/tests/regression_tests/photon_production/test.py @@ -25,7 +25,8 @@ def model(): inner_cyl_right.region = -cyl & +x_plane_center & -x_plane_right outer_cyl.region = ~(-cyl & +x_plane_left & -x_plane_right) inner_cyl_right.fill = mat - model.geometry = openmc.Geometry([inner_cyl_left, inner_cyl_right, outer_cyl]) + model.geometry = openmc.Geometry( + [inner_cyl_left, inner_cyl_right, outer_cyl]) source = openmc.IndependentSource() source.space = openmc.stats.Point((0, 0, 0)) @@ -38,17 +39,19 @@ def model(): model.settings.batches = 1 model.settings.photon_transport = True model.settings.electron_treatment = 'ttb' - model.settings.cutoff = {'energy_photon' : 1000.0} + model.settings.cutoff = {'energy_photon': 1000.0} model.settings.source = source surface_filter = openmc.SurfaceFilter(cyl) - particle_filter = openmc.ParticleFilter(['neutron', 'photon', 'electron', 'positron']) + particle_filter = openmc.ParticleFilter( + ['neutron', 'photon', 'electron', 'positron']) current_tally = openmc.Tally() current_tally.filters = [surface_filter, particle_filter] current_tally.scores = ['current'] tally_tracklength = openmc.Tally() tally_tracklength.filters = [particle_filter] - tally_tracklength.scores = ['total', '(n,gamma)'] # heating doesn't work with tracklength + # heating doesn't work with tracklength + tally_tracklength.scores = ['total', '(n,gamma)'] tally_tracklength.nuclides = ['Al27', 'total'] tally_tracklength.estimator = 'tracklength' tally_collision = openmc.Tally() @@ -61,8 +64,25 @@ def model(): tally_analog.scores = ['total', 'heating', '(n,gamma)'] tally_analog.nuclides = ['Al27', 'total'] tally_analog.estimator = 'analog' + + # This is an analog tally tracking the energy distribution of photons + # generated by neutrons. The sum of the tally should give the total + # number of photons generated per source neutron. + ene_filter = openmc.EnergyFilter([0.0, 20e6]) # incident neutron energy + + # Track source energies of secondary gammas + ene2_filter = openmc.ParticleProductionFilter( + ['neutron', 'photon'], [0.0, 100e3, 300e3, 500e3, 2e6, 20e6]) + + neutron_only = openmc.ParticleFilter(['neutron']) + tally_gam_ene = openmc.Tally() + tally_gam_ene.filters = [neutron_only, ene_filter, ene2_filter] + tally_gam_ene.scores = ['events'] + tally_gam_ene.estimator = 'analog' + model.tallies.extend([current_tally, tally_tracklength, - tally_collision, tally_analog]) + tally_collision, tally_analog, + tally_gam_ene]) return model diff --git a/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat b/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat new file mode 100644 index 000000000..e25de58b0 --- /dev/null +++ b/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + false + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/local_neutron/results_true.dat b/tests/regression_tests/pulse_height/local_neutron/results_true.dat new file mode 100644 index 000000000..ffa284d92 --- /dev/null +++ b/tests/regression_tests/pulse_height/local_neutron/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.890000E+00 +4.784900E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +6.000000E-02 +1.800000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/pulse_height/inputs_true.dat b/tests/regression_tests/pulse_height/local_photon/inputs_true.dat similarity index 94% rename from tests/regression_tests/pulse_height/inputs_true.dat rename to tests/regression_tests/pulse_height/local_photon/inputs_true.dat index 590928e43..84d033d85 100644 --- a/tests/regression_tests/pulse_height/inputs_true.dat +++ b/tests/regression_tests/pulse_height/local_photon/inputs_true.dat @@ -2,7 +2,7 @@ - + @@ -18,14 +18,12 @@ 100 5 - - 0.0 0.0 0.0 - 1000000.0 1.0 true + false diff --git a/tests/regression_tests/pulse_height/results_true.dat b/tests/regression_tests/pulse_height/local_photon/results_true.dat similarity index 100% rename from tests/regression_tests/pulse_height/results_true.dat rename to tests/regression_tests/pulse_height/local_photon/results_true.dat diff --git a/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat b/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat new file mode 100644 index 000000000..b13c53ef8 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + true + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/shared_neutron/results_true.dat b/tests/regression_tests/pulse_height/shared_neutron/results_true.dat new file mode 100644 index 000000000..ffa284d92 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_neutron/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.890000E+00 +4.784900E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +6.000000E-02 +1.800000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat b/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat new file mode 100644 index 000000000..608410c50 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + true + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/shared_photon/results_true.dat b/tests/regression_tests/pulse_height/shared_photon/results_true.dat new file mode 100644 index 000000000..c57e8ff1c --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_photon/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/pulse_height/test.py b/tests/regression_tests/pulse_height/test.py index 90d960f66..371fcbd0f 100644 --- a/tests/regression_tests/pulse_height/test.py +++ b/tests/regression_tests/pulse_height/test.py @@ -1,53 +1,54 @@ import numpy as np import openmc import pytest +from openmc.utility_funcs import change_directory from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def sphere_model(): - - model = openmc.model.Model() +@pytest.mark.parametrize("shared_secondary,particle", [ + (False, "photon"), + (False, "neutron"), + (True, "photon"), + (True, "neutron") +]) +def test_pulse_height(shared_secondary, particle): + subdir = f"shared_{particle}" if shared_secondary else f"local_{particle}" + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() - # Define materials - NaI = openmc.Material() - NaI.set_density('g/cc', 3.7) - NaI.add_element('Na', 1.0) - NaI.add_element('I', 1.0) + # Define materials + NaI = openmc.Material() + NaI.set_density('g/cm3', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) - model.materials = openmc.Materials([NaI]) + # Define geometry: two spheres in each other + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) - # Define geometry: two spheres in each other - s1 = openmc.Sphere(r=1) - s2 = openmc.Sphere(r=2, boundary_type='vacuum') - inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) - outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) - model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + model.settings.photon_transport = True + model.settings.shared_secondary_bank = shared_secondary + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle=particle + ) - # Define settings - model.settings.run_mode = 'fixed source' - model.settings.batches = 5 - model.settings.particles = 100 - model.settings.photon_transport = True - model.settings.source = openmc.IndependentSource( - space=openmc.stats.Point(), - energy=openmc.stats.Discrete([1e6], [1]), - particle='photon' - ) + # Define tallies + tally = openmc.Tally(name="pht tally") + tally.scores = ['pulse-height'] + cell_filter = openmc.CellFilter(inner_sphere) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1e6, 101)) + tally.filters = [cell_filter, energy_filter] + model.tallies = [tally] - # Define tallies - tally = openmc.Tally(name="pht tally") - tally.scores = ['pulse-height'] - cell_filter = openmc.CellFilter(inner_sphere) - energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) - tally.filters = [cell_filter, energy_filter] - model.tallies = [tally] - - return model - - - -def test_pulse_height(sphere_model): - harness = PyAPITestHarness('statepoint.5.h5', sphere_model) - harness.main() + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat index 0adfc5488..94e270976 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true true naive diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat index 073348c41..755afd6c4 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true true diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat index 657c841b5..dfef53cd2 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.006640E+00 1.812969E-03 tally 1: -6.684129E+00 -8.939821E+00 -2.685967E+00 -1.443592E+00 +1.208044E+00 +2.920182E-01 +4.854426E-01 +4.715453E-02 0.000000E+00 0.000000E+00 -6.358774E+00 -8.091444E+00 -9.687217E-01 -1.878029E-01 +1.149242E+00 +2.643067E-01 +1.750801E-01 +6.134563E-03 0.000000E+00 0.000000E+00 -5.963160E+00 -7.117108E+00 -1.932332E-01 -7.473914E-03 +1.077743E+00 +2.324814E-01 +3.492371E-02 +2.441363E-04 0.000000E+00 0.000000E+00 -5.137593E+00 -5.283310E+00 -1.714616E-01 -5.884834E-03 -1.086218E-06 -2.361752E-13 -4.857253E+00 -4.719856E+00 -5.689580E-02 -6.476286E-04 -2.989356E-03 -1.787808E-06 -4.830516E+00 -4.666801E+00 -7.203015E-03 -1.037676E-05 -3.620020E+00 -2.620927E+00 -5.161382E+00 -5.328124E+00 -6.786255E-02 -9.210763E-04 -5.531943E+00 -6.120553E+00 -5.414034E+00 -5.864661E+00 +9.285362E-01 +1.725808E-01 +3.098889E-02 +1.922297E-04 +1.963161E-07 +7.714727E-15 +8.778641E-01 +1.541719E-01 +1.028293E-02 +2.115448E-05 +5.402741E-04 +5.839789E-08 +8.730274E-01 +1.524358E-01 +1.301813E-03 +3.389450E-07 +6.542525E-01 +8.560964E-02 +9.328247E-01 +1.740366E-01 +1.226491E-02 +3.008584E-05 +9.997969E-01 +1.999204E-01 +9.784958E-01 +1.915688E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.632338E+00 -6.347626E+00 +1.017952E+00 +2.073461E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.682608E+00 -6.462382E+00 +1.027039E+00 +2.110955E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.310716E+00 -5.645180E+00 +9.598240E-01 +1.844004E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.945409E+00 -4.893171E+00 +8.937969E-01 +1.598332E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.842688E+00 -4.690352E+00 +8.752275E-01 +1.532052E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.117198E+00 -5.237280E+00 +9.248400E-01 +1.710699E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -6.938711E+00 -9.633345E+00 -2.835258E+00 -1.608212E+00 +1.254054E+00 +3.146708E-01 +5.124223E-01 +5.253093E-02 0.000000E+00 0.000000E+00 -6.549505E+00 -8.584036E+00 -1.015138E+00 -2.061993E-01 +1.183712E+00 +2.803961E-01 +1.834683E-01 +6.735381E-03 0.000000E+00 0.000000E+00 -6.050651E+00 -7.327711E+00 -1.992816E-01 -7.948424E-03 +1.093555E+00 +2.393604E-01 +3.601678E-02 +2.596341E-04 0.000000E+00 0.000000E+00 -5.113981E+00 -5.234801E+00 -1.732323E-01 -6.006619E-03 -1.097435E-06 -2.410627E-13 -4.837033E+00 -4.680541E+00 -5.760042E-02 -6.637112E-04 -3.026377E-03 -1.832205E-06 -4.827049E+00 -4.660105E+00 -7.319913E-03 -1.071647E-05 -3.678770E+00 -2.706730E+00 -5.175337E+00 -5.356957E+00 -6.923046E-02 -9.586177E-04 -5.643451E+00 -6.370016E+00 -6.693323E+00 -8.964322E+00 -2.753307E+00 -1.516683E+00 +9.242694E-01 +1.709967E-01 +3.130894E-02 +1.962084E-04 +1.983437E-07 +7.874400E-15 +8.742100E-01 +1.528879E-01 +1.041026E-02 +2.167970E-05 +5.469644E-04 +5.984780E-08 +8.724009E-01 +1.522171E-01 +1.322938E-03 +3.500386E-07 +6.648691E-01 +8.841161E-02 +9.353464E-01 +1.749781E-01 +1.251209E-02 +3.131171E-05 +1.019947E+00 +2.080664E-01 +1.209708E+00 +2.928214E-01 +4.976146E-01 +4.954258E-02 0.000000E+00 0.000000E+00 -6.358384E+00 -8.090233E+00 -9.912008E-01 -1.965868E-01 +1.149174E+00 +2.642694E-01 +1.791431E-01 +6.421530E-03 0.000000E+00 0.000000E+00 -5.957484E+00 -7.103246E+00 -1.974033E-01 -7.798286E-03 +1.076718E+00 +2.320295E-01 +3.567737E-02 +2.547314E-04 0.000000E+00 0.000000E+00 -5.130744E+00 -5.268844E+00 -1.749233E-01 -6.123348E-03 -1.108148E-06 -2.457474E-13 -4.857340E+00 -4.720019E+00 -5.816659E-02 -6.768049E-04 -3.056125E-03 -1.868351E-06 -4.830629E+00 -4.667018E+00 -7.366289E-03 -1.085264E-05 -3.702077E+00 -2.741125E+00 -5.164864E+00 -5.335279E+00 -6.947917E-02 -9.655086E-04 -5.663725E+00 -6.415806E+00 +9.272977E-01 +1.721077E-01 +3.161443E-02 +2.000179E-04 +2.002789E-07 +8.027290E-15 +8.778794E-01 +1.541769E-01 +1.051257E-02 +2.210729E-05 +5.523400E-04 +6.102818E-08 +8.730477E-01 +1.524429E-01 +1.331320E-03 +3.544869E-07 +6.690816E-01 +8.953516E-02 +9.334544E-01 +1.742707E-01 +1.255707E-02 +3.153710E-05 +1.023613E+00 +2.095641E-01 diff --git a/tests/regression_tests/random_ray_adjoint_local/__init__.py b/tests/regression_tests/random_ray_adjoint_local/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_adjoint_local/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_local/inputs_true.dat new file mode 100644 index 000000000..9021d1675 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_local/inputs_true.dat @@ -0,0 +1,293 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + + + + + + fixed source + 500 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 800.0 + 100.0 + + + + 0.0 0.0 0.0 35.0 35.0 35.0 + + + + true + + + + + + true + + + + 100.0 1.0 + + + cell + 6 7 + + + + naive + + + 14 14 14 + 0.0 0.0 0.0 + 35.0 35.0 35.0 + + + + + 6 + + + 7 + + + 3 + + + 2 + + + 1 + + + 1 + flux + tracklength + + + 2 + flux + tracklength + + + 3 + flux + tracklength + + + 4 + flux + tracklength + + + 5 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_adjoint_local/results_true.dat b/tests/regression_tests/random_ray_adjoint_local/results_true.dat new file mode 100644 index 000000000..daa948565 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_local/results_true.dat @@ -0,0 +1,15 @@ +tally 1: +2.215273E+01 +9.815738E+01 +tally 2: +1.873933E+01 +7.023420E+01 +tally 3: +4.802282E-01 +4.612707E-02 +tally 4: +2.516720E-01 +1.271063E-02 +tally 5: +1.169938E-02 +3.277334E-05 diff --git a/tests/regression_tests/random_ray_adjoint_local/test.py b/tests/regression_tests/random_ray_adjoint_local/test.py new file mode 100644 index 000000000..c11b8e847 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_local/test.py @@ -0,0 +1,35 @@ +import os +import openmc + +from openmc.examples import random_ray_three_region_cube_with_detectors + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_adjoint_local(): + model = random_ray_three_region_cube_with_detectors() + + detector1_cells = model.geometry.get_cells_by_name("detector 1") + detector2_cells = model.geometry.get_cells_by_name("detector 2") + detector_cells = detector1_cells + detector2_cells + + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + adj_source = openmc.IndependentSource(energy=energy_distribution, constraints={ + 'domains': detector_cells}, strength=3.14) + + model.settings.random_ray['adjoint'] = True + model.settings.random_ray['adjoint_source'] = adj_source + model.settings.random_ray['volume_estimator'] = 'naive' + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat index 464c89a5d..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat index c7584ab64..f984f3718 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.797820E-01 1.054725E-02 +7.479770E-01 1.624548E-02 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 464c89a5d..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat index d544a27df..3bade01e0 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.356667E-01 6.637270E-03 +7.372542E-01 6.967831E-03 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 464c89a5d..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat index 75a10a224..674dee4aa 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.551716E-01 8.117378E-03 +6.413334E-01 2.083132E-02 diff --git a/tests/regression_tests/random_ray_auto_convert/test.py b/tests/regression_tests/random_ray_auto_convert/test.py index fa7f2f17f..99a931dce 100644 --- a/tests/regression_tests/random_ray_auto_convert/test.py +++ b/tests/regression_tests/random_ray_auto_convert/test.py @@ -27,7 +27,7 @@ def test_random_ray_auto_convert(method): # Convert to a multi-group model model.convert_to_multigroup( - method=method, groups='CASMO-2', nparticles=30, + method=method, groups='CASMO-2', nparticles=100, overwrite_mgxs_library=False, mgxs_path="mgxs.h5" ) diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/__init__.py b/tests/regression_tests/random_ray_auto_convert_kappa_fission/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat new file mode 100644 index 000000000..48b0e8256 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat @@ -0,0 +1,75 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + + + 1 + + + 61 + kappa-fission + + + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat new file mode 100644 index 000000000..27b32d787 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat @@ -0,0 +1,5 @@ +k-combined: +7.479770E-01 1.624548E-02 +tally 1: +2.965503E+08 +1.762157E+16 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat new file mode 100644 index 000000000..be738c3e0 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat @@ -0,0 +1,75 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + + + 1 + + + 97 + kappa-fission + + + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat new file mode 100644 index 000000000..5a379db7e --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat @@ -0,0 +1,5 @@ +k-combined: +7.372542E-01 6.967831E-03 +tally 1: +2.909255E+08 +1.693395E+16 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat new file mode 100644 index 000000000..be738c3e0 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat @@ -0,0 +1,75 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + + + 1 + + + 97 + kappa-fission + + + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat new file mode 100644 index 000000000..a60e5aa3c --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat @@ -0,0 +1,5 @@ +k-combined: +6.413334E-01 2.083132E-02 +tally 1: +2.541463E+08 +1.297259E+16 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py b/tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py new file mode 100644 index 000000000..6decf165a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py @@ -0,0 +1,60 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("method", ["material_wise", "stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert(method): + with change_directory(method): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-2', nparticles=100, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5" + ) + + # Convert to a random ray model + model.convert_to_random_ray() + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + # Set a material tally + t = openmc.Tally(name = 'KF Tally') + t.filters = [openmc.MaterialFilter(bins=1)] + t.scores = ['kappa-fission'] + model.tallies.append(t) + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py b/tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat new file mode 100644 index 000000000..d2d8289ff --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat @@ -0,0 +1,63 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + 7000000.0 1.0 + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat new file mode 100644 index 000000000..1fb09fd68 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.657815E-01 2.317564E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat new file mode 100644 index 000000000..81f8c98ca --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat @@ -0,0 +1,66 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat new file mode 100644 index 000000000..073c5c99f --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.827784E-01 2.062954E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat new file mode 100644 index 000000000..d2d8289ff --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat @@ -0,0 +1,63 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + 7000000.0 1.0 + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat new file mode 100644 index 000000000..c5cdf8e29 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.479571E-01 2.398563E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat new file mode 100644 index 000000000..81f8c98ca --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat @@ -0,0 +1,66 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat new file mode 100644 index 000000000..c6cce2e39 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.620306E-01 2.175179E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/test.py b/tests/regression_tests/random_ray_auto_convert_source_energy/test.py new file mode 100644 index 000000000..bb9119d89 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/test.py @@ -0,0 +1,66 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("source_type", ["model", "user"]) +@pytest.mark.parametrize("method", ["stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert_source_energy(method, source_type): + dirname = f"{method}/{source_type}" + with change_directory(dirname): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + # Define the source energy distribution, using different methods + source_energy = None + if source_type == "model": + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(7.0e6) + ) + elif source_type == "user": + source_energy = openmc.stats.delta_function(1.0e4) + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-8', nparticles=100, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5", + source_energy=source_energy + ) + + # Convert to a random ray model + model.convert_to_random_ray() + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/__init__.py b/tests/regression_tests/random_ray_auto_convert_temperature/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat new file mode 100644 index 000000000..c49020d55 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat @@ -0,0 +1,72 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + 395.0 + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + nearest + true + 200.0 400.0 + 200.0 + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat new file mode 100644 index 000000000..fee8bf670 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.499800E-01 1.615317E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat new file mode 100644 index 000000000..c49020d55 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat @@ -0,0 +1,72 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + 395.0 + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + nearest + true + 200.0 400.0 + 200.0 + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat new file mode 100644 index 000000000..ef0a4b87a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.367927E-01 6.850805E-03 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat new file mode 100644 index 000000000..c49020d55 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat @@ -0,0 +1,72 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + 395.0 + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + nearest + true + 200.0 400.0 + 200.0 + + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat new file mode 100644 index 000000000..a702ec851 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +6.431774E-01 2.076589E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/test.py b/tests/regression_tests/random_ray_auto_convert_temperature/test.py new file mode 100644 index 000000000..99c99e614 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/test.py @@ -0,0 +1,73 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("method", ["material_wise", "stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert(method): + with change_directory(method): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + temp_settings = { + 'method' : 'nearest', + 'tolerance' : 200.0, + 'range' : (200.0, 400.0), + 'multipole' : True + } + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-2', nparticles=100, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5", + temperatures=[294.0, 394.0], temperature_settings=temp_settings + ) + + # Convert to a random ray model + model.convert_to_random_ray() + model.settings.temperature = temp_settings + + # Set all material temperatures to room temperature + for mat in model.geometry.get_all_materials().values(): + mat.temperature = 294.0 + + # Set the cell temperature of the fuel such that it moves up to the next + # temperature bin. + for cell in model.geometry.get_all_cells().values(): + if cell.name == "Fuel": + cell.temperature = [395.0] + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_cell_density/__init__.py b/tests/regression_tests/random_ray_cell_density/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat new file mode 100644 index 000000000..eacd54f83 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat @@ -0,0 +1,117 @@ + + + + mgxs.h5 + + + + + + + + + + + + 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 + + + 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 + + + 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + true + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_cell_density/eigen/results_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/results_true.dat new file mode 100644 index 000000000..3e6ce4039 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/eigen/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +7.606488E-01 9.025978E-03 +tally 1: +6.209503E-01 +7.732732E-02 +4.335729E-01 +3.769026E-02 +1.055230E+00 +2.232538E-01 +4.077492E-01 +3.335434E-02 +1.188353E-01 +2.833644E-03 +2.892214E-01 +1.678476E-02 +2.715245E-01 +1.488194E-02 +1.736084E-02 +6.079207E-05 +4.225281E-02 +3.600946E-04 +3.700491E-01 +2.796457E-02 +2.369715E-02 +1.145314E-04 +5.767413E-02 +6.784132E-04 +1.223083E+00 +3.058118E-01 +2.790791E-02 +1.593458E-04 +6.792309E-02 +9.438893E-04 +4.095779E+00 +3.377358E+00 +1.251201E-02 +3.153331E-05 +3.096010E-02 +1.930723E-04 +2.608638E+00 +1.361111E+00 +7.059742E-02 +9.968342E-04 +1.963632E-01 +7.711968E-03 +1.164050E+00 +2.710289E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.325486E-01 +5.675375E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.058117E-01 +1.905099E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.645650E-01 +4.428410E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.350036E+00 +3.718611E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.538916E+00 +2.521670E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.201812E+00 +9.698678E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.776093E-01 +9.211106E-02 +2.527022E-01 +1.280619E-02 +6.150266E-01 +7.585593E-02 +4.194143E-01 +3.528585E-02 +6.303447E-02 +7.972233E-04 +1.534133E-01 +4.722259E-03 +2.743408E-01 +1.520611E-02 +8.961769E-03 +1.621446E-05 +2.181115E-02 +9.604444E-05 +3.871710E-01 +3.063162E-02 +1.288801E-02 +3.392550E-05 +3.136683E-02 +2.009537E-04 +1.259769E+00 +3.241545E-01 +1.478360E-02 +4.466102E-05 +3.598077E-02 +2.645508E-04 +4.032169E+00 +3.273866E+00 +6.215187E-03 +7.776862E-06 +1.537904E-02 +4.761620E-05 +2.583647E+00 +1.335275E+00 +3.533526E-02 +2.498005E-04 +9.828323E-02 +1.932572E-03 +7.851145E-01 +1.234783E-01 +2.966638E-01 +1.763751E-02 +7.220204E-01 +1.044737E-01 +4.505673E-01 +4.067697E-02 +6.823342E-02 +9.332472E-04 +1.660665E-01 +5.527980E-03 +2.832534E-01 +1.626181E-02 +9.297014E-03 +1.749663E-05 +2.262707E-02 +1.036392E-04 +4.056699E-01 +3.370803E-02 +1.358439E-02 +3.774180E-05 +3.306168E-02 +2.235591E-04 +1.279744E+00 +3.345476E-01 +1.512005E-02 +4.668112E-05 +3.679962E-02 +2.765169E-04 +3.917888E+00 +3.090981E+00 +6.082371E-03 +7.449625E-06 +1.505040E-02 +4.561259E-05 +2.520906E+00 +1.271106E+00 +3.477796E-02 +2.419730E-04 +9.673314E-02 +1.872015E-03 diff --git a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat new file mode 100644 index 000000000..f369bae89 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + + true + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_cell_density/fs/results_true.dat b/tests/regression_tests/random_ray_cell_density/fs/results_true.dat new file mode 100644 index 000000000..e12f476e0 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/fs/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +6.203077E-01 +7.706659E-02 +tally 2: +3.203448E-02 +2.058679E-04 +tally 3: +2.091970E-03 +8.765168E-07 diff --git a/tests/regression_tests/random_ray_cell_density/test.py b/tests/regression_tests/random_ray_cell_density/test.py new file mode 100644 index 000000000..48ebe0baa --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/test.py @@ -0,0 +1,43 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice, random_ray_three_region_cube +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("run_mode", ["eigen", "fs"]) +def test_random_ray_basic(run_mode): + with change_directory(run_mode): + if run_mode == "eigen": + openmc.reset_auto_ids() + model = random_ray_lattice() + # Double the densities of the lower-left fuel pin -> cell instances [0, 8). + for id, cell in model.geometry.get_all_cells().items(): + if cell.fill.name == "UO2 fuel": + cell.density = [((i < 8) + 1.0) for i in range(24)] + + # Gold file was generated with manually scaled fuel cross sections. + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() + else: + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + # Increase the density in the source region. + for id, cell in model.geometry.get_all_cells().items(): + if cell.fill.name == "source": + cell.density = 1e3 + + # Gold file was generated with manually scaled source cross sections. + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_cell_temperature/__init__.py b/tests/regression_tests/random_ray_cell_temperature/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat b/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat new file mode 100644 index 000000000..99363ed87 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat @@ -0,0 +1,120 @@ + + + + mgxs.h5 + + + + + + + + + + + + 395.0 395.0 395.0 395.0 395.0 395.0 395.0 395.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 + + + 395.0 395.0 395.0 395.0 395.0 395.0 395.0 395.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 + + + 395.0 395.0 395.0 395.0 395.0 395.0 395.0 395.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + nearest + 200.0 400.0 + 10.0 + + 100.0 + 20.0 + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + true + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_cell_temperature/results_true.dat b/tests/regression_tests/random_ray_cell_temperature/results_true.dat new file mode 100644 index 000000000..50476beb6 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_temperature/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.721099E-01 6.686066E-03 +tally 1: +1.530055E+00 +4.684582E-01 +2.918032E-01 +1.703772E-02 +7.101906E-01 +1.009209E-01 +7.732582E-01 +1.197247E-01 +5.787213E-02 +6.705930E-04 +1.408492E-01 +3.972179E-03 +4.223488E-01 +3.605283E-02 +6.818063E-03 +9.396342E-06 +1.659380E-02 +5.565812E-05 +5.958551E-01 +7.215230E-02 +9.932163E-03 +2.004773E-05 +2.417290E-02 +1.187504E-04 +1.685106E+00 +5.752729E-01 +9.834826E-03 +1.960236E-05 +2.393629E-02 +1.161151E-04 +4.400457E+00 +3.886563E+00 +3.318560E-03 +2.211175E-06 +8.211544E-03 +1.353859E-05 +2.814971E+00 +1.585202E+00 +1.876190E-02 +7.040316E-05 +5.218528E-02 +5.446713E-04 +1.970406E+00 +7.765020E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.643086E-01 +1.494764E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.421562E-01 +3.961223E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.421077E-01 +8.385255E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.717683E+00 +5.974687E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.024715E+00 +3.251629E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.507885E+00 +1.258277E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.237600E+00 +3.066006E-01 +4.632318E-01 +4.294997E-02 +1.127413E+00 +2.544090E-01 +7.085300E-01 +1.005838E-01 +1.066845E-01 +2.280854E-03 +2.596486E-01 +1.351037E-02 +4.080999E-01 +3.359223E-02 +1.334606E-02 +3.590848E-05 +3.248163E-02 +2.126996E-04 +5.587686E-01 +6.339264E-02 +1.868162E-02 +7.084921E-05 +4.546734E-02 +4.196670E-04 +1.647852E+00 +5.503007E-01 +1.940765E-02 +7.635704E-05 +4.723492E-02 +4.523030E-04 +4.672431E+00 +4.381400E+00 +7.228567E-03 +1.048470E-05 +1.788658E-02 +6.419579E-05 +3.054158E+00 +1.866081E+00 +4.202691E-02 +3.534383E-04 +1.168957E-01 +2.734362E-03 +1.313678E+00 +3.454374E-01 +4.964081E-01 +4.934325E-02 +1.208158E+00 +2.922789E-01 +7.298176E-01 +1.067017E-01 +1.106319E-01 +2.452869E-03 +2.692559E-01 +1.452928E-02 +4.129038E-01 +3.441066E-02 +1.357107E-02 +3.713729E-05 +3.302926E-02 +2.199783E-04 +5.677464E-01 +6.543807E-02 +1.908992E-02 +7.390190E-05 +4.646105E-02 +4.377492E-04 +1.651067E+00 +5.522453E-01 +1.956802E-02 +7.754346E-05 +4.762523E-02 +4.593308E-04 +4.583305E+00 +4.217589E+00 +7.135836E-03 +1.022408E-05 +1.765713E-02 +6.260005E-05 +2.988394E+00 +1.786305E+00 +4.141550E-02 +3.431964E-04 +1.151951E-01 +2.655125E-03 diff --git a/tests/regression_tests/random_ray_cell_temperature/test.py b/tests/regression_tests/random_ray_cell_temperature/test.py new file mode 100644 index 000000000..ef36d9238 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_temperature/test.py @@ -0,0 +1,36 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice, random_ray_three_region_cube +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_basic(): + openmc.reset_auto_ids() + model = random_ray_lattice(second_temp=True) + # Set the temperature of the lower-left pin to 395 K -> cell instances [0, 8). + # All other pins are set to 295. + for id, cell in model.geometry.get_all_cells().items(): + if cell.fill.name == "UO2 fuel": + cell.temperature = [(100.0 * (i < 8) + 295.0) for i in range(24)] + + model.settings.temperature = { + 'method' : 'nearest', + 'tolerance' : 10.0, + 'range' : (200.0, 400.0) + } + + # Gold file was generated with manually scaled fuel cross sections. + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 47325ebd7..0ea8c0177 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat index f27ad46b4..034d7f7c6 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.201808E-01 1.506596E-02 +7.134473E-01 1.422763E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py index c7a1c9f7c..8d36e1d25 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/test.py +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -23,7 +23,7 @@ def test_random_ray_diagonal_stabilization(): # MGXS data with some negatives on the diagonal, in order # to trigger diagonal correction. model.convert_to_multigroup( - method='material_wise', groups='CASMO-70', nparticles=30, + method='material_wise', groups='CASMO-70', nparticles=13, overwrite_mgxs_library=True, mgxs_path="mgxs.h5", correction='P0' ) diff --git a/tests/regression_tests/random_ray_entropy/settings.xml b/tests/regression_tests/random_ray_entropy/settings.xml index 81deaa775..0d830417b 100644 --- a/tests/regression_tests/random_ray_entropy/settings.xml +++ b/tests/regression_tests/random_ray_entropy/settings.xml @@ -6,11 +6,13 @@ 5 multi-group - - - 0.0 0.0 0.0 100.0 100.0 100.0 - - + + + + 0.0 0.0 0.0 100.0 100.0 100.0 + + + 40.0 400.0 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat index 9f1987f3a..d650bbaf9 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat index b4f57dbfa..98a51add1 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat index ab91f74e5..20deba664 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index 220fa7db6..2268d82c3 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index f8c443085..fe95baa7b 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear_xy diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat index c84e544fc..a5632ece9 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat index 05c4846e6..9d22603c6 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat index 0c870e100..de941f10f 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + false diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat index ab91f74e5..20deba664 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat index 0c05a71df..943468a10 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -110,11 +110,13 @@ 40.0 40.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + false flat diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat index a67495bf1..650953c4b 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -110,11 +110,13 @@ 40.0 40.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + false linear_xy diff --git a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat index 36d5f6f22..1b86d2dae 100644 --- a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true halton diff --git a/tests/regression_tests/random_ray_halton_samples/results_true.dat b/tests/regression_tests/random_ray_halton_samples/results_true.dat index b62398935..256f8a744 100644 --- a/tests/regression_tests/random_ray_halton_samples/results_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/results_true.dat @@ -1,171 +1,171 @@ k-combined: 8.388051E-01 7.383265E-03 tally 1: -5.033308E+00 -5.072162E+00 -1.917335E+00 -7.360725E-01 -4.666410E+00 -4.360038E+00 -2.851812E+00 -1.629362E+00 -4.365590E-01 -3.818884E-02 -1.062497E+00 -2.262071E-01 -1.697621E+00 -5.829333E-01 -5.639912E-02 -6.427568E-04 -1.372642E-01 -3.807294E-03 -2.376683E+00 -1.151027E+00 -8.060903E-02 -1.323179E-03 -1.961862E-01 -7.837693E-03 -7.145452E+00 -1.037540E+01 -8.551803E-02 -1.486269E-03 -2.081363E-01 -8.803955E-03 -2.053205E+01 -8.469498E+01 -3.235618E-02 -2.102891E-04 -8.006311E-02 -1.287559E-03 -1.326545E+01 -3.519484E+01 -1.867471E-01 -6.975133E-03 -5.194275E-01 -5.396284E-02 -7.558115E+00 -1.142535E+01 +1.065839E+00 +2.274384E-01 +4.060094E-01 +3.300592E-02 +9.881457E-01 +1.955066E-01 +6.038893E-01 +7.306038E-02 +9.244411E-02 +1.712380E-03 +2.249905E-01 +1.014308E-02 +3.594916E-01 +2.614145E-02 +1.194318E-02 +2.882412E-05 +2.906731E-02 +1.707363E-04 +5.032954E-01 +5.161897E-02 +1.707007E-02 +5.933921E-05 +4.154514E-02 +3.514889E-04 +1.513147E+00 +4.652938E-01 +1.810962E-02 +6.665306E-05 +4.407572E-02 +3.948212E-04 +4.347893E+00 +3.798064E+00 +6.851785E-03 +9.430195E-06 +1.695426E-02 +5.773923E-05 +2.809071E+00 +1.578195E+00 +3.954523E-02 +3.127754E-04 +1.099931E-01 +2.419775E-03 +1.600493E+00 +5.123291E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.386211E+00 -2.294414E+00 +7.170555E-01 +1.028835E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.827274E+00 -6.782305E-01 +3.869491E-01 +3.041553E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.702858E+00 -1.489752E+00 +5.723683E-01 +6.680970E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.475537E+00 -1.133971E+01 +1.583046E+00 +5.085372E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.828685E+01 -6.719606E+01 +3.872447E+00 +3.013344E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.143600E+01 -2.615734E+01 +2.421670E+00 +1.172938E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.590713E+00 -4.224107E+00 -1.705847E+00 -5.831967E-01 -4.151691E+00 -3.454497E+00 -2.730853E+00 -1.495413E+00 -4.072633E-01 -3.325944E-02 -9.911973E-01 -1.970084E-01 -1.664732E+00 -5.598668E-01 -5.385645E-02 -5.858353E-04 -1.310758E-01 -3.470126E-03 -2.312239E+00 -1.088485E+00 -7.662778E-02 -1.195422E-03 -1.864967E-01 -7.080943E-03 -7.105766E+00 -1.025960E+01 -8.287512E-02 -1.396474E-03 -2.017039E-01 -8.272053E-03 -2.099024E+01 -8.854251E+01 -3.191885E-02 -2.048368E-04 -7.898095E-02 -1.254175E-03 -1.355820E+01 -3.676862E+01 -1.815102E-01 -6.590727E-03 -5.048614E-01 -5.098890E-02 -5.093659E+00 -5.192360E+00 -1.874632E+00 -7.031793E-01 -4.562478E+00 -4.165199E+00 -2.870214E+00 -1.650126E+00 -4.244069E-01 -3.608745E-02 -1.032921E+00 -2.137598E-01 -1.703400E+00 -5.873029E-01 -5.464557E-02 -6.042855E-04 -1.329964E-01 -3.579413E-03 -2.389118E+00 -1.163674E+00 -7.832237E-02 -1.251254E-03 -1.906210E-01 -7.411654E-03 -7.162707E+00 -1.042515E+01 -8.273831E-02 -1.391799E-03 -2.013709E-01 -8.244359E-03 -2.043145E+01 -8.383557E+01 -3.096158E-02 -1.924116E-04 -7.661226E-02 -1.178098E-03 -1.314148E+01 -3.454143E+01 -1.771732E-01 -6.279887E-03 -4.927984E-01 -4.858410E-02 +9.721128E-01 +1.894086E-01 +3.612242E-01 +2.615053E-02 +8.791475E-01 +1.548996E-01 +5.782742E-01 +6.705354E-02 +8.624040E-02 +1.491336E-03 +2.098919E-01 +8.833753E-03 +3.525265E-01 +2.510689E-02 +1.140473E-02 +2.627142E-05 +2.775683E-02 +1.556157E-04 +4.896481E-01 +4.881407E-02 +1.622698E-02 +5.360976E-05 +3.949322E-02 +3.175511E-04 +1.504743E+00 +4.601003E-01 +1.754995E-02 +6.262618E-05 +4.271358E-02 +3.709679E-04 +4.444922E+00 +3.970609E+00 +6.759184E-03 +9.185746E-06 +1.672513E-02 +5.624252E-05 +2.871068E+00 +1.648778E+00 +3.843643E-02 +2.955428E-04 +1.069090E-01 +2.286455E-03 +1.078620E+00 +2.328291E-01 +3.969671E-01 +3.153110E-02 +9.661384E-01 +1.867708E-01 +6.077864E-01 +7.399164E-02 +8.987086E-02 +1.618157E-03 +2.187277E-01 +9.584966E-03 +3.607158E-01 +2.633746E-02 +1.157185E-02 +2.709895E-05 +2.816358E-02 +1.605174E-04 +5.059289E-01 +5.218618E-02 +1.658585E-02 +5.611375E-05 +4.036663E-02 +3.323832E-04 +1.516801E+00 +4.675245E-01 +1.752096E-02 +6.241641E-05 +4.264304E-02 +3.697253E-04 +4.326588E+00 +3.759517E+00 +6.556454E-03 +8.628460E-06 +1.622349E-02 +5.283037E-05 +2.782815E+00 +1.548888E+00 +3.751781E-02 +2.815972E-04 +1.043539E-01 +2.178566E-03 diff --git a/tests/regression_tests/random_ray_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat index 545bd1d45..72b783344 100644 --- a/tests/regression_tests/random_ray_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true diff --git a/tests/regression_tests/random_ray_k_eff/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat index 37eca77f3..ace18df8c 100644 --- a/tests/regression_tests/random_ray_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.400321E-01 8.023358E-03 +8.400321E-01 8.023357E-03 tally 1: -5.086559E+00 -5.180935E+00 -1.885166E+00 -7.115503E-01 -4.588116E+00 -4.214784E+00 -2.860400E+00 -1.639328E+00 -4.245221E-01 -3.610929E-02 -1.033202E+00 -2.138892E-01 -1.692631E+00 -5.793966E-01 -5.445818E-02 -5.996625E-04 -1.325403E-01 -3.552030E-03 -2.372248E+00 -1.146944E+00 -7.808142E-02 -1.242278E-03 -1.900346E-01 -7.358491E-03 -7.134949E+00 -1.034824E+01 -8.272647E-02 -1.391871E-03 -2.013421E-01 -8.244788E-03 -2.043539E+01 -8.389902E+01 -3.099367E-02 -1.930673E-04 -7.669167E-02 -1.182113E-03 -1.313212E+01 -3.449537E+01 -1.764293E-01 -6.225586E-03 -4.907293E-01 -4.816400E-02 -7.567715E+00 -1.145439E+01 +1.075769E+00 +2.317354E-01 +3.986991E-01 +3.182682E-02 +9.703538E-01 +1.885224E-01 +6.049423E-01 +7.331941E-02 +8.978161E-02 +1.614997E-03 +2.185105E-01 +9.566247E-03 +3.579852E-01 +2.591721E-02 +1.151770E-02 +2.682363E-05 +2.803176E-02 +1.588866E-04 +5.017326E-01 +5.130808E-02 +1.651428E-02 +5.557274E-05 +4.019245E-02 +3.291786E-04 +1.509054E+00 +4.629325E-01 +1.749679E-02 +6.226587E-05 +4.258421E-02 +3.688336E-04 +4.322054E+00 +3.753085E+00 +6.555113E-03 +8.636537E-06 +1.622017E-02 +5.287982E-05 +2.777351E+00 +1.542942E+00 +3.731363E-02 +2.784662E-04 +1.037860E-01 +2.154343E-03 +1.600522E+00 +5.123477E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.383194E+00 -2.290468E+00 +7.155116E-01 +1.024445E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.819672E+00 -6.726158E-01 +3.848568E-01 +3.008769E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.693683E+00 -1.480961E+00 +5.697160E-01 +6.624996E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.453758E+00 -1.128171E+01 +1.576480E+00 +5.046890E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.823561E+01 -6.681652E+01 +3.856827E+00 +2.989000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.137517E+01 -2.588512E+01 +2.405807E+00 +1.157889E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.601916E+00 -4.242624E+00 -1.719723E+00 -5.923465E-01 -4.185462E+00 -3.508695E+00 -2.730305E+00 -1.494324E+00 -4.108214E-01 -3.383938E-02 -9.998572E-01 -2.004436E-01 -1.660852E+00 -5.570432E-01 -5.428709E-02 -5.947848E-04 -1.321239E-01 -3.523137E-03 -2.306069E+00 -1.082855E+00 -7.697031E-02 -1.206036E-03 -1.873303E-01 -7.143815E-03 -7.075194E+00 -1.017519E+01 -8.322052E-02 -1.408294E-03 -2.025446E-01 -8.342071E-03 -2.094832E+01 -8.816715E+01 -3.234739E-02 -2.101889E-04 -8.004135E-02 -1.286945E-03 -1.357413E+01 -3.685983E+01 -1.861680E-01 -6.934827E-03 -5.178169E-01 -5.365102E-02 -5.072149E+00 -5.151429E+00 -1.916643E+00 -7.358710E-01 -4.664726E+00 -4.358845E+00 -2.859464E+00 -1.638250E+00 -4.332944E-01 -3.763170E-02 -1.054552E+00 -2.229070E-01 -1.693008E+00 -5.796671E-01 -5.561096E-02 -6.247543E-04 -1.353459E-01 -3.700658E-03 -2.368860E+00 -1.143296E+00 -7.951819E-02 -1.286690E-03 -1.935314E-01 -7.621557E-03 -7.119587E+00 -1.030023E+01 -8.428175E-02 -1.442931E-03 -2.051274E-01 -8.547243E-03 -2.046758E+01 -8.418768E+01 -3.181946E-02 -2.034766E-04 -7.873502E-02 -1.245847E-03 -1.325834E+01 -3.515919E+01 -1.832838E-01 -6.720555E-03 -5.097947E-01 -5.199331E-02 +9.732717E-01 +1.897670E-01 +3.637108E-01 +2.649544E-02 +8.851992E-01 +1.569426E-01 +5.774286E-01 +6.683395E-02 +8.688408E-02 +1.513473E-03 +2.114585E-01 +8.964883E-03 +3.512640E-01 +2.491729E-02 +1.148149E-02 +2.660532E-05 +2.794365E-02 +1.575935E-04 +4.877362E-01 +4.844138E-02 +1.627932E-02 +5.395212E-05 +3.962062E-02 +3.195790E-04 +1.496416E+00 +4.551920E-01 +1.760130E-02 +6.300084E-05 +4.283856E-02 +3.731872E-04 +4.430519E+00 +3.943947E+00 +6.841363E-03 +9.402132E-06 +1.692847E-02 +5.756741E-05 +2.870816E+00 +1.648662E+00 +3.937267E-02 +3.101707E-04 +1.095131E-01 +2.399624E-03 +1.072714E+00 +2.304093E-01 +4.053505E-01 +3.291277E-02 +9.865420E-01 +1.949549E-01 +6.047420E-01 +7.327008E-02 +9.163611E-02 +1.683034E-03 +2.230240E-01 +9.969255E-03 +3.580639E-01 +2.592902E-02 +1.176143E-02 +2.794541E-05 +2.862497E-02 +1.655313E-04 +5.010143E-01 +5.114429E-02 +1.681803E-02 +5.755790E-05 +4.093173E-02 +3.409375E-04 +1.505803E+00 +4.607828E-01 +1.782566E-02 +6.454911E-05 +4.338462E-02 +3.823584E-04 +4.328879E+00 +3.766055E+00 +6.729800E-03 +9.102372E-06 +1.665242E-02 +5.573204E-05 +2.804070E+00 +1.572690E+00 +3.876396E-02 +3.006259E-04 +1.078200E-01 +2.325780E-03 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat index 98badea18..f6e9c8e3e 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true diff --git a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat index 83209044b..2ae8fad85 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat @@ -1,171 +1,171 @@ k-combined: 8.379203E-01 8.057199E-03 tally 1: -5.080172E+00 -5.167984E+00 -1.880341E+00 -7.079266E-01 -4.576373E+00 -4.193319E+00 -2.859914E+00 -1.638769E+00 -4.243332E-01 -3.607732E-02 -1.032742E+00 -2.136998E-01 -1.692643E+00 -5.794069E-01 -5.445214E-02 -5.995213E-04 -1.325256E-01 -3.551193E-03 -2.372336E+00 -1.147031E+00 -7.807378E-02 -1.242019E-03 -1.900160E-01 -7.356955E-03 -7.135636E+00 -1.035026E+01 -8.273225E-02 -1.392069E-03 -2.013562E-01 -8.245961E-03 -2.044034E+01 -8.394042E+01 -3.100485E-02 -1.932097E-04 -7.671934E-02 -1.182985E-03 -1.313652E+01 -3.451846E+01 -1.764978E-01 -6.230420E-03 -4.909196E-01 -4.820140E-02 -7.585874E+00 -1.150936E+01 +1.073897E+00 +2.309328E-01 +3.974859E-01 +3.163415E-02 +9.674011E-01 +1.873811E-01 +6.045463E-01 +7.322367E-02 +8.969819E-02 +1.612011E-03 +2.183075E-01 +9.548557E-03 +3.578121E-01 +2.589198E-02 +1.151076E-02 +2.679073E-05 +2.801489E-02 +1.586917E-04 +5.015038E-01 +5.126076E-02 +1.650453E-02 +5.550572E-05 +4.016872E-02 +3.287816E-04 +1.508456E+00 +4.625615E-01 +1.748939E-02 +6.221267E-05 +4.256620E-02 +3.685184E-04 +4.320984E+00 +3.751237E+00 +6.554265E-03 +8.634389E-06 +1.621807E-02 +5.286667E-05 +2.776930E+00 +1.542475E+00 +3.730995E-02 +2.784113E-04 +1.037757E-01 +2.153918E-03 +1.603583E+00 +5.143065E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.386790E+00 -2.295327E+00 +7.159242E-01 +1.025623E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.820058E+00 -6.729239E-01 +3.847488E-01 +3.007151E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.694013E+00 -1.481363E+00 +5.695050E-01 +6.620177E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.453818E+00 -1.128202E+01 +1.575716E+00 +5.042003E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.823642E+01 -6.682239E+01 +3.855109E+00 +2.986316E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.137903E+01 -2.590265E+01 +2.405453E+00 +1.157547E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.589529E+00 -4.219985E+00 -1.712446E+00 -5.873675E-01 -4.167750E+00 -3.479202E+00 -2.728285E+00 -1.492132E+00 -4.104063E-01 -3.377132E-02 -9.988468E-01 -2.000405E-01 -1.660506E+00 -5.567967E-01 -5.427391E-02 -5.944708E-04 -1.320918E-01 -3.521277E-03 -2.305889E+00 -1.082691E+00 -7.695793E-02 -1.205644E-03 -1.873002E-01 -7.141490E-03 -7.076637E+00 -1.017946E+01 -8.323139E-02 -1.408687E-03 -2.025710E-01 -8.344398E-03 -2.095897E+01 -8.825741E+01 -3.236513E-02 -2.104220E-04 -8.008525E-02 -1.288373E-03 -1.358006E+01 -3.689205E+01 -1.862562E-01 -6.941428E-03 -5.180621E-01 -5.370209E-02 -5.067386E+00 -5.141748E+00 -1.912704E+00 -7.328484E-01 -4.655140E+00 -4.340941E+00 -2.858992E+00 -1.637705E+00 -4.331050E-01 -3.759875E-02 -1.054091E+00 -2.227118E-01 -1.692974E+00 -5.796396E-01 -5.560487E-02 -6.246120E-04 -1.353311E-01 -3.699815E-03 -2.368737E+00 -1.143184E+00 -7.950086E-02 -1.286135E-03 -1.934892E-01 -7.618269E-03 -7.119767E+00 -1.030095E+01 -8.427628E-02 -1.442764E-03 -2.051141E-01 -8.546249E-03 -2.047651E+01 -8.426275E+01 -3.183786E-02 -2.037156E-04 -7.878057E-02 -1.247311E-03 -1.326415E+01 -3.519010E+01 -1.833688E-01 -6.726832E-03 -5.100312E-01 -5.204188E-02 +9.701818E-01 +1.885724E-01 +3.619962E-01 +2.624740E-02 +8.810264E-01 +1.554734E-01 +5.767221E-01 +6.667165E-02 +8.675429E-02 +1.508976E-03 +2.111426E-01 +8.938242E-03 +3.510185E-01 +2.488161E-02 +1.147307E-02 +2.656498E-05 +2.792316E-02 +1.573545E-04 +4.874578E-01 +4.838573E-02 +1.626869E-02 +5.388082E-05 +3.959473E-02 +3.191567E-04 +1.495984E+00 +4.549294E-01 +1.759493E-02 +6.295564E-05 +4.282306E-02 +3.729194E-04 +4.430601E+00 +3.944093E+00 +6.841763E-03 +9.403281E-06 +1.692946E-02 +5.757445E-05 +2.870670E+00 +1.648496E+00 +3.937215E-02 +3.101637E-04 +1.095116E-01 +2.399569E-03 +1.071187E+00 +2.297540E-01 +4.043214E-01 +3.274592E-02 +9.840375E-01 +1.939666E-01 +6.043491E-01 +7.317500E-02 +9.155169E-02 +1.679939E-03 +2.228185E-01 +9.950920E-03 +3.578809E-01 +2.590208E-02 +1.175437E-02 +2.791136E-05 +2.860779E-02 +1.653296E-04 +5.007414E-01 +5.108824E-02 +1.680608E-02 +5.747571E-05 +4.090264E-02 +3.404506E-04 +1.505100E+00 +4.603562E-01 +1.781573E-02 +6.447733E-05 +4.336044E-02 +3.819332E-04 +4.328647E+00 +3.765700E+00 +6.730396E-03 +9.104085E-06 +1.665389E-02 +5.574253E-05 +2.803934E+00 +1.572540E+00 +3.876306E-02 +3.006136E-04 +1.078175E-01 +2.325685E-03 diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index a43a66e71..269d9892e 100644 --- a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true linear diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat index 4c0e14370..77d41f373 100644 --- a/tests/regression_tests/random_ray_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.095967E+00 1.543581E-02 tally 1: -2.548108E+01 -3.269093E+01 -9.271804E+00 -4.327275E+00 -2.256572E+01 -2.563210E+01 -1.816107E+01 -1.653421E+01 -2.659203E+00 -3.544951E-01 -6.471969E+00 -2.099810E+00 -1.364193E+01 -9.308675E+00 -4.362828E-01 -9.521133E-03 -1.061825E+00 -5.639730E-02 -1.746102E+01 -1.524680E+01 -5.733016E-01 -1.643671E-02 -1.395301E+00 -9.736092E-02 -4.539598E+01 -1.030472E+02 -5.263055E-01 -1.385088E-02 -1.280938E+00 -8.204609E-02 -9.945736E+01 -4.946716E+02 -1.505228E-01 -1.133424E-03 -3.724582E-01 -6.939732E-03 -5.324914E+01 -1.418809E+02 -7.219589E-01 -2.614875E-02 -2.008092E+00 -2.022988E-01 -4.188246E+01 -8.843469E+01 +5.425537E+00 +1.482137E+00 +1.974189E+00 +1.961888E-01 +4.804781E+00 +1.162101E+00 +3.866915E+00 +7.496163E-01 +5.662059E-01 +1.607180E-02 +1.378032E+00 +9.519943E-02 +2.904666E+00 +4.220197E-01 +9.289413E-02 +4.316514E-04 +2.260857E-01 +2.556836E-03 +3.717829E+00 +6.912286E-01 +1.220682E-01 +7.451721E-04 +2.970897E-01 +4.413940E-03 +9.665773E+00 +4.671720E+00 +1.120617E-01 +6.279393E-04 +2.727390E-01 +3.719615E-03 +2.117656E+01 +2.242614E+01 +3.204951E-02 +5.138445E-05 +7.930426E-02 +3.146169E-04 +1.133784E+01 +6.432186E+00 +1.537206E-01 +1.185474E-03 +4.275660E-01 +9.171376E-03 +8.917756E+00 +4.009380E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.224363E+01 -2.481131E+01 +4.736166E+00 +1.124858E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.436097E+01 -1.031496E+01 +3.057755E+00 +4.676344E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.901248E+01 -1.807657E+01 +4.048156E+00 +8.195089E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.559337E+01 -1.039424E+02 +9.707791E+00 +4.712281E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -8.802992E+01 -3.874925E+02 +1.874344E+01 +1.756722E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.506602E+01 -1.016497E+02 +9.595520E+00 +4.608366E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.207833E+01 -2.455068E+01 -8.139772E+00 -3.337974E+00 -1.981058E+01 -1.977210E+01 -1.710407E+01 -1.466648E+01 -2.542287E+00 -3.240467E-01 -6.187418E+00 -1.919453E+00 -1.342690E+01 -9.017908E+00 -4.362263E-01 -9.518695E-03 -1.061688E+00 -5.638286E-02 -1.713924E+01 -1.469065E+01 -5.714028E-01 -1.632839E-02 -1.390680E+00 -9.671931E-02 -4.539193E+01 -1.030326E+02 -5.345254E-01 -1.428719E-02 -1.300944E+00 -8.463057E-02 -1.013270E+02 -5.134168E+02 -1.555517E-01 -1.210145E-03 -3.849017E-01 -7.409480E-03 -5.377836E+01 -1.446537E+02 -7.374053E-01 -2.722908E-02 -2.051056E+00 -2.106567E-01 -2.522726E+01 -3.202007E+01 -9.366659E+00 -4.412278E+00 -2.279657E+01 -2.613561E+01 -1.803368E+01 -1.629756E+01 -2.696490E+00 -3.643066E-01 -6.562716E+00 -2.157928E+00 -1.357447E+01 -9.216150E+00 -4.437305E-01 -9.847287E-03 -1.079951E+00 -5.832923E-02 -1.735848E+01 -1.506775E+01 -5.825173E-01 -1.696803E-02 -1.417731E+00 -1.005081E-01 -4.519297E+01 -1.021280E+02 -5.357712E-01 -1.435322E-02 -1.303976E+00 -8.502167E-02 -9.934508E+01 -4.935314E+02 -1.538761E-01 -1.184229E-03 -3.807556E-01 -7.250804E-03 -5.335839E+01 -1.424221E+02 -7.404823E-01 -2.747396E-02 -2.059614E+00 -2.125512E-01 +4.701002E+00 +1.113069E+00 +1.733152E+00 +1.513360E-01 +4.218145E+00 +8.964208E-01 +3.641849E+00 +6.649342E-01 +5.413112E-01 +1.469131E-02 +1.317443E+00 +8.702223E-02 +2.858878E+00 +4.088359E-01 +9.288202E-02 +4.315389E-04 +2.260562E-01 +2.556170E-03 +3.649312E+00 +6.660131E-01 +1.216639E-01 +7.402611E-04 +2.961057E-01 +4.384849E-03 +9.664912E+00 +4.671059E+00 +1.138118E-01 +6.477188E-04 +2.769985E-01 +3.836780E-03 +2.157464E+01 +2.327600E+01 +3.312018E-02 +5.486221E-05 +8.195357E-02 +3.359106E-04 +1.145052E+01 +6.557893E+00 +1.570084E-01 +1.234420E-03 +4.367109E-01 +9.550040E-03 +5.371474E+00 +1.451702E+00 +1.994382E+00 +2.000414E-01 +4.853928E+00 +1.184921E+00 +3.839778E+00 +7.388780E-01 +5.741437E-01 +1.651648E-02 +1.397351E+00 +9.783345E-02 +2.890296E+00 +4.178220E-01 +9.447974E-02 +4.464346E-04 +2.299448E-01 +2.644403E-03 +3.695989E+00 +6.831063E-01 +1.240303E-01 +7.692555E-04 +3.018649E-01 +4.556594E-03 +9.622545E+00 +4.630042E+00 +1.140770E-01 +6.507108E-04 +2.776440E-01 +3.854503E-03 +2.115266E+01 +2.237450E+01 +3.276343E-02 +5.368742E-05 +8.107083E-02 +3.287176E-04 +1.136109E+01 +6.456700E+00 +1.576636E-01 +1.245524E-03 +4.385334E-01 +9.635950E-03 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 7f76f2fd1..217e95516 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true linear_xy diff --git a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat index abfd03c06..052608b42 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.104727E+00 1.593303E-02 tally 1: -2.566934E+01 -3.317503E+01 -9.417202E+00 -4.465518E+00 -2.291958E+01 -2.645097E+01 -1.823903E+01 -1.667438E+01 -2.679931E+00 -3.600420E-01 -6.522415E+00 -2.132667E+00 -1.365623E+01 -9.327448E+00 -4.370682E-01 -9.554968E-03 -1.063736E+00 -5.659772E-02 -1.750634E+01 -1.532609E+01 -5.762870E-01 -1.660889E-02 -1.402567E+00 -9.838082E-02 -4.543609E+01 -1.032286E+02 -5.271287E-01 -1.389456E-02 -1.282941E+00 -8.230483E-02 -9.881678E+01 -4.882586E+02 -1.487616E-01 -1.106634E-03 -3.681003E-01 -6.775702E-03 -5.260781E+01 -1.384126E+02 -7.018594E-01 -2.464953E-02 -1.952187E+00 -1.907001E-01 -4.184779E+01 -8.826530E+01 +5.465547E+00 +1.504031E+00 +2.005120E+00 +2.024490E-01 +4.880060E+00 +1.199182E+00 +3.883466E+00 +7.559482E-01 +5.706123E-01 +1.632280E-02 +1.388756E+00 +9.668619E-02 +2.907681E+00 +4.228621E-01 +9.306046E-02 +4.331768E-04 +2.264905E-01 +2.565871E-03 +3.727441E+00 +6.948089E-01 +1.227027E-01 +7.529631E-04 +2.986338E-01 +4.460088E-03 +9.674219E+00 +4.679846E+00 +1.122358E-01 +6.299070E-04 +2.731629E-01 +3.731271E-03 +2.103996E+01 +2.213497E+01 +3.167421E-02 +5.016895E-05 +7.837561E-02 +3.071747E-04 +1.120119E+01 +6.274853E+00 +1.494396E-01 +1.117486E-03 +4.156586E-01 +8.645390E-03 +8.910289E+00 +4.001626E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.226932E+01 -2.486554E+01 +4.741600E+00 +1.127303E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.441107E+01 -1.038682E+01 +3.068401E+00 +4.708885E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.909194E+01 -1.822767E+01 +4.065045E+00 +8.263510E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.579319E+01 -1.048559E+02 +9.750251E+00 +4.753623E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -8.833276E+01 -3.901410E+02 +1.880773E+01 +1.768690E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.528911E+01 -1.025740E+02 +9.642921E+00 +4.650165E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.218278E+01 -2.477434E+01 -8.203467E+00 -3.389915E+00 -1.996560E+01 -2.007976E+01 -1.714818E+01 -1.473927E+01 -2.551034E+00 -3.262450E-01 -6.208707E+00 -1.932474E+00 -1.343470E+01 -9.027502E+00 -4.363259E-01 -9.522121E-03 -1.061930E+00 -5.640315E-02 -1.717034E+01 -1.474381E+01 -5.729720E-01 -1.641841E-02 -1.394499E+00 -9.725251E-02 -4.542715E+01 -1.031896E+02 -5.348128E-01 -1.430232E-02 -1.301643E+00 -8.472019E-02 -1.009076E+02 -5.091264E+02 -1.543547E-01 -1.191320E-03 -3.819398E-01 -7.294216E-03 -5.342751E+01 -1.427427E+02 -7.255554E-01 -2.633014E-02 -2.018096E+00 -2.037021E-01 -2.540053E+01 -3.247261E+01 -9.483956E+00 -4.526477E+00 -2.308205E+01 -2.681205E+01 -1.812760E+01 -1.646855E+01 -2.717374E+00 -3.700301E-01 -6.613545E+00 -2.191830E+00 -1.362672E+01 -9.286907E+00 -4.458292E-01 -9.940508E-03 -1.085059E+00 -5.888142E-02 -1.744511E+01 -1.521876E+01 -5.866632E-01 -1.721091E-02 -1.427821E+00 -1.019468E-01 -4.538426E+01 -1.029941E+02 -5.385934E-01 -1.450495E-02 -1.310845E+00 -8.592045E-02 -9.921424E+01 -4.921945E+02 -1.532444E-01 -1.174272E-03 -3.791926E-01 -7.189835E-03 -5.301633E+01 -1.405571E+02 -7.285745E-01 -2.655093E-02 -2.026493E+00 -2.054102E-01 +4.723189E+00 +1.123179E+00 +1.746692E+00 +1.536860E-01 +4.251098E+00 +9.103405E-01 +3.651202E+00 +6.682179E-01 +5.431675E-01 +1.479058E-02 +1.321961E+00 +8.761024E-02 +2.860513E+00 +4.092640E-01 +9.290237E-02 +4.316867E-04 +2.261058E-01 +2.557045E-03 +3.655900E+00 +6.684112E-01 +1.219969E-01 +7.443277E-04 +2.969160E-01 +4.408937E-03 +9.672316E+00 +4.678082E+00 +1.138719E-01 +6.483917E-04 +2.771448E-01 +3.840766E-03 +2.148514E+01 +2.308100E+01 +3.286501E-02 +5.400774E-05 +8.132216E-02 +3.306788E-04 +1.137571E+01 +6.471140E+00 +1.544841E-01 +1.193653E-03 +4.296897E-01 +9.234647E-03 +5.408307E+00 +1.472183E+00 +2.019332E+00 +2.052123E-01 +4.914651E+00 +1.215551E+00 +3.859737E+00 +7.466141E-01 +5.785840E-01 +1.677554E-02 +1.408158E+00 +9.936796E-02 +2.901397E+00 +4.210234E-01 +9.492573E-02 +4.506531E-04 +2.310302E-01 +2.669390E-03 +3.714401E+00 +6.899411E-01 +1.249118E-01 +7.802521E-04 +3.040104E-01 +4.621732E-03 +9.663183E+00 +4.669215E+00 +1.146768E-01 +6.575767E-04 +2.791038E-01 +3.895173E-03 +2.112461E+01 +2.231346E+01 +3.262864E-02 +5.323508E-05 +8.073730E-02 +3.259479E-04 +1.128817E+01 +6.372070E+00 +1.551272E-01 +1.203670E-03 +4.314785E-01 +9.312148E-03 diff --git a/tests/regression_tests/random_ray_low_density/inputs_true.dat b/tests/regression_tests/random_ray_low_density/inputs_true.dat index ab91f74e5..20deba664 100644 --- a/tests/regression_tests/random_ray_low_density/inputs_true.dat +++ b/tests/regression_tests/random_ray_low_density/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat index 088f803bf..b4bd263f5 100644 --- a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat +++ b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat @@ -206,11 +206,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_s2/__init__.py b/tests/regression_tests/random_ray_s2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_s2/inputs_true.dat b/tests/regression_tests/random_ray_s2/inputs_true.dat new file mode 100644 index 000000000..c0dc6292f --- /dev/null +++ b/tests/regression_tests/random_ray_s2/inputs_true.dat @@ -0,0 +1,71 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + fixed source + 100 + 30 + 10 + + + 0.0 -5.0 -5.0 40.0 5.0 5.0 + + + 1000.0 1.0 + + + material + 1 + + + multi-group + + 100.0 + 400.0 + + + + 0.0 -5.0 -5.0 40.0 5.0 5.0 + + + + flat + s2 + + + + + + + + 10 1 1 + 0.0 -5.0 -5.0 + 40.0 5.0 5.0 + + + + + 1 + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_s2/results_true.dat b/tests/regression_tests/random_ray_s2/results_true.dat new file mode 100644 index 000000000..5fb6f7904 --- /dev/null +++ b/tests/regression_tests/random_ray_s2/results_true.dat @@ -0,0 +1,21 @@ +tally 1: +1.153280E+01 +6.650271E+00 +1.413926E+01 +9.995935E+00 +1.579543E+01 +1.247479E+01 +1.676986E+01 +1.406141E+01 +1.722054E+01 +1.482734E+01 +1.722054E+01 +1.482734E+01 +1.676986E+01 +1.406141E+01 +1.579543E+01 +1.247479E+01 +1.413926E+01 +9.995935E+00 +1.153280E+01 +6.650271E+00 diff --git a/tests/regression_tests/random_ray_s2/test.py b/tests/regression_tests/random_ray_s2/test.py new file mode 100644 index 000000000..712d9c124 --- /dev/null +++ b/tests/regression_tests/random_ray_s2/test.py @@ -0,0 +1,82 @@ +import os + +import openmc +import openmc.model +import numpy as np + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_s2(): + NUM_SOURCE_REGIONS = 10 + L = 40.0 + + # Make the simple MGXS library and material. + groups = openmc.mgxs.EnergyGroups(group_edges=[1e-5, 20.0e6]) + + domain_mat_data = openmc.XSdata('domain', groups) + domain_mat_data.order = 0 + domain_mat_data.set_total([0.1]) + domain_mat_data.set_absorption([0.1]) + domain_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[0.0]]]), 0, 3)) + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([domain_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + domain_data = openmc.Macroscopic('domain') + domain_mat = openmc.Material(name='domain') + domain_mat.set_density('macro', 1.0) + domain_mat.add_macroscopic(domain_data) + + container = openmc.model.RectangularPrism(width=10.0, height=10.0, axis='x', + origin=(0.0, 0.0), boundary_type='reflective') + left = openmc.XPlane(x0 = 0.0, boundary_type='vacuum') + right = openmc.XPlane(x0 = L, boundary_type='vacuum') + cell = [openmc.Cell(region = +left & -right & -container, fill = domain_mat)] + + model = openmc.model.Model() + model.geometry = openmc.Geometry(root=openmc.Universe(cells=cell)) + model.materials = openmc.Materials([domain_mat]) + model.materials.cross_sections = './mgxs.h5' + + mesh = openmc.RegularMesh() + mesh.dimension = (NUM_SOURCE_REGIONS, 1, 1) + mesh.lower_left = (0.0, -5.0, -5.0) + mesh.upper_right = (L, 5.0, 5.0) + + tally = openmc.Tally(name="LR") + tally.filters = [openmc.MeshFilter(mesh)] + tally.scores = ['flux'] + tally.estimator = 'tracklength' + model.tallies.append(tally) + + uniform_dist = openmc.stats.Box((0.0, -5.0, -5.0), (L, 5.0, 5.0)) + model.settings.source = [ + openmc.IndependentSource(space=uniform_dist, + energy=openmc.stats.Discrete(x = 1e3, p = 1.0), + constraints={'domains' : [domain_mat]}) + ] + model.settings.energy_mode = "multi-group" + model.settings.batches = 30 + model.settings.inactive = 10 + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.random_ray['distance_inactive'] = 100.0 + model.settings.random_ray['distance_active'] = 400.0 + model.settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + model.settings.random_ray['source_shape'] = 'flat' + model.settings.random_ray['sample_method'] = 's2' + model.settings.random_ray['source_region_meshes'] = [(mesh, [model.geometry.root_universe])] + + model.export_to_model_xml() + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat index aa28e7b68..66390c766 100644 --- a/tests/regression_tests/random_ray_void/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true flat diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat index e4b2f22fa..45228a039 100644 --- a/tests/regression_tests/random_ray_void/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat index 8e8a8ed9b..4d1af46b1 100644 --- a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true hybrid diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat index 1e25b97da..a268d55d0 100644 --- a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true naive diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat index 78c162697..777ccaea5 100644 --- a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true simulation_averaged diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat index 47a8a7182..dd11567f6 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear hybrid diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat index 80a9ada4d..6933fba43 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear naive diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat index 4f032a62a..3ccab1d21 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear simulation_averaged diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 7d03c696d..c671463be 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.080655E-01 4.837707E-03 +2.942254E-01 2.571435E-03 diff --git a/tests/regression_tests/source_dlopen/source_sampling.cpp b/tests/regression_tests/source_dlopen/source_sampling.cpp index fc61ef1fd..678eea4be 100644 --- a/tests/regression_tests/source_dlopen/source_sampling.cpp +++ b/tests/regression_tests/source_dlopen/source_sampling.cpp @@ -10,7 +10,7 @@ class CustomSource : public openmc::Source { { openmc::SourceSite particle; // wgt - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); particle.wgt = 1.0; // position diff --git a/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp b/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp index bf49af4c3..81f3770c6 100644 --- a/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp +++ b/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp @@ -2,36 +2,37 @@ #include "openmc/source.h" class CustomSource : public openmc::Source { - public: - CustomSource(double energy) : energy_(energy) { } +public: + CustomSource(double energy) : energy_(energy) {} - // Samples from an instance of this class. - openmc::SourceSite sample(uint64_t* seed) const - { - openmc::SourceSite particle; - // wgt - particle.particle = openmc::ParticleType::neutron; - particle.wgt = 1.0; - // position - particle.r.x = 0.0; - particle.r.y = 0.0; - particle.r.z = 0.0; - // angle - particle.u = {1.0, 0.0, 0.0}; - particle.E = this->energy_; - particle.delayed_group = 0; + // Samples from an instance of this class. + openmc::SourceSite sample(uint64_t* seed) const + { + openmc::SourceSite particle; + // wgt + particle.particle = openmc::ParticleType::neutron(); + particle.wgt = 1.0; + // position + particle.r.x = 0.0; + particle.r.y = 0.0; + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; + particle.delayed_group = 0; - return particle; - } + return particle; + } - private: - double energy_; +private: + double energy_; }; -// A function to create a unique pointer to an instance of this class when generated -// via a plugin call using dlopen/dlsym. -// You must have external C linkage here otherwise dlopen will not find the file -extern "C" std::unique_ptr openmc_create_source(std::string parameter) +// A function to create a unique pointer to an instance of this class when +// generated via a plugin call using dlopen/dlsym. You must have external C +// linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source( + std::string parameter) { double energy = std::stod(parameter); return std::make_unique(energy); diff --git a/tests/regression_tests/surface_source/surface_source_true.h5 b/tests/regression_tests/surface_source/surface_source_true.h5 index 2c2a19038..02e94c90b 100644 Binary files a/tests/regression_tests/surface_source/surface_source_true.h5 and b/tests/regression_tests/surface_source/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 index a43646158..3a2315acc 100644 Binary files a/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 index 5a73e7ab2..2c2d5bc00 100644 Binary files a/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 index 228f5a7d0..d14771f5a 100644 Binary files a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 index c276af40f..d2acc6991 100644 Binary files a/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 index c276af40f..553090b19 100644 Binary files a/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 index 6946938ad..60a9f4f82 100644 Binary files a/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 index c276af40f..cb3079251 100644 Binary files a/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 index 94cd377d4..ed3aba907 100644 Binary files a/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 index ebce87b36..d62363f97 100644 Binary files a/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 index fded9d987..acaf39cb4 100644 Binary files a/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 index b5b603620..962f95fdd 100644 Binary files a/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 index 6c6925dab..4b830d057 100644 Binary files a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 index 6c6925dab..f53e2c3a2 100644 Binary files a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 index 6c6925dab..ec049fec4 100644 Binary files a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 index 5bdb39b2a..fd5892096 100644 Binary files a/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 index 79ac8e1a6..147ce0358 100644 Binary files a/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 index aadcc3fa4..436063aee 100644 Binary files a/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 index e5a7619c5..436063aee 100644 Binary files a/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 index db8a49dce..eda3e030b 100644 Binary files a/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 index 01d2abf61..083ce90c8 100644 Binary files a/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 index 4e5dcb446..f7e886db4 100644 Binary files a/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 index da36fc505..807211e3a 100644 Binary files a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 and b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 differ diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index f144eb82a..094df1f8b 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -634,7 +634,7 @@ def return_surface_source_data(filepath): wgt = point.wgt delayed_group = point.delayed_group surf_id = point.surf_id - particle = point.particle + particle = point.particle.pdg_number key = ( f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" diff --git a/tests/regression_tests/surface_tally/results_true.dat b/tests/regression_tests/surface_tally/results_true.dat index 70d5cad2c..e50102a88 100644 --- a/tests/regression_tests/surface_tally/results_true.dat +++ b/tests/regression_tests/surface_tally/results_true.dat @@ -15,14 +15,14 @@ mean,std. dev. 5.5000000e-03,1.0979779e-03 1.2500000e-02,1.5438048e-03 4.4700000e-02,1.9035055e-03 -7.3000000e-03,9.8938814e-04 -3.6600000e-02,2.5086517e-03 -4.1600000e-02,2.4864075e-03 -4.2380000e-01,9.6087923e-03 -1.0000000e-04,1.0000000e-04 -1.5000000e-03,4.5338235e-04 -3.0000000e-04,1.5275252e-04 -1.7300000e-02,1.4609738e-03 +-7.3000000e-03,9.8938814e-04 +-3.6600000e-02,2.5086517e-03 +-4.1600000e-02,2.4864075e-03 +-4.2380000e-01,9.6087923e-03 +-1.0000000e-04,1.0000000e-04 +-1.5000000e-03,4.5338235e-04 +-3.0000000e-04,1.5275252e-04 +-1.7300000e-02,1.4609738e-03 -7.3000000e-03,9.8938814e-04 -3.6600000e-02,2.5086517e-03 -4.1600000e-02,2.4864075e-03 diff --git a/tests/regression_tests/surface_tally/test.py b/tests/regression_tests/surface_tally/test.py index e496ac0f6..aa03a8aa2 100644 --- a/tests/regression_tests/surface_tally/test.py +++ b/tests/regression_tests/surface_tally/test.py @@ -169,7 +169,6 @@ class SurfaceTallyTestHarness(PyAPITestHarness): # Extract the relevant data as a CSV string. cols = ('mean', 'std. dev.') return df.to_csv(None, columns=cols, index=False, float_format='%.7e') - return outstr def test_surface_tally(): diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/inputs_true.dat deleted file mode 100644 index eb9393179..000000000 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - fixed source - 200 - 2 - - - 0.001 0.001 0.001 - - - 14000000.0 1.0 - - - true - - 2 - neutron - 0.0 0.5 20000000.0 - -1.0 0.007673145137236567 6.542700644645627e-07 0.0017541380096893788 0.0007245451610090619 7.476545089278482e-06 2.253382683081525e-06 2.1040609012865134e-05 0.08285899874014539 -1.0 1.0262431550731535e-13 3.3164825349503764e-16 0.0064803314120165335 3.5981031766416143e-06 -1.0 1.6205064883026195e-11 1.7770136411255912e-05 0.0001670700632870335 4.323138781337963e-05 -1.0 -1.0 6.922231352729155e-05 0.06550426960512012 -1.0 2.5639656866250453e-06 1.4508262073256659e-13 -1.0 -1.0 0.0009127050763987511 3.8523247550378815e-12 -1.0 0.0008175033526488423 -1.0 0.015148978428271613 2.5157355790201773e-07 2.1612150425221703e-06 1.3181562352445092e-10 2.9282097947816224e-05 2.762955748645507e-05 2.936268747135548e-14 1.6859219614058024e-19 3.937160538176268e-07 2.6011787393916806e-06 0.001570829313580841 -1.0 1.7041669224797384e-09 1.6421491993751715e-11 0.4899999304038166 6.948292975998466e-07 -1.0 -1.0 0.08657573566388353 0.006639379668946672 1.681305446488884e-11 2.2487617828938326e-05 4.109066205029878e-05 0.08288806458368345 -1.0 0.007385384405891666 -1.0 -1.0 1.008394964750947e-06 6.655926357459275e-08 0.0016279794552427574 1.4825219199133353e-12 -1.0 1.93847081892941e-05 1.284759166582202e-05 5.1161740516205715e-05 2.5447281241730366e-07 3.853097455417877e-06 -1.0 8.887234042637684e-05 0.01621821782442112 -1.0 1.8747824667478637e-14 5.97653814110781e-06 -1.0 -1.0 1.2296747260635405e-06 -1.0 -1.0 7.754700849337902e-06 7.091263906557291e-05 3.2799045883372075e-06 0.00020573181450240786 5.6949597066784976e-05 1.271128669411295e-07 0.0010019432401508087 -1.0 -1.0 6.848642863465585e-07 5.783974359937857e-06 2.380955631371226e-06 4.756872959630257e-05 2.1360401784344975e-18 0.00046029617115127556 1.0092905083158804e-11 0.08095280568305474 3.89841667138598e-08 4.354946981329799e-05 -1.0 0.5 0.0015447823995470042 -1.0 -1.0 0.001959411999677113 0.48573842213878143 -1.0 7.962899789920809e-06 5.619835883512353e-14 0.0835648710074336 2.7207433165796702e-11 0.00027464244062887683 4.076762579823379e-17 2.124999182004655e-05 8.037131813528501e-07 8.846493399850663e-06 1.080711295768551e-05 -1.0 -1.0 0.0010737081832502356 2.679340942769232e-06 2.1648918040467363e-05 0.0002668579837809081 1.3455341306162382e-05 2.5629068330446215e-05 1.3228013765525015e-05 -1.0 -1.0 7.002861620919232e-08 1.5172912057312398e-14 -1.0 7.989579285134734e-06 2.639268952045579e-10 -1.0 0.013330619853379314 0.00021534568699157805 2.4469456008493614e-09 8.211982683649991e-09 1.951356547084204e-15 0.00016419031150495913 5.985108617124989e-06 1.28584557092134e-05 1.5476769237571295e-14 5.5379154134462336e-08 0.0016637126543321873 2.499931357628383e-07 8.020767115181574e-07 2.7697233171399416e-13 -1.0 0.007197662162700777 -1.0 0.0833931797381589 2.8226276944532696e-05 3.353295403842037e-05 5.504813693036933e-12 0.007136750029575816 0.08636569925236791 -1.0 -1.0 6.315154082213375e-07 0.4877856021170283 -1.0 3.7051518220083886e-06 -1.0 0.0017950613169359904 7.84289689494925e-06 9.041149893307806e-07 -1.0 8.48176145671274e-10 9.03108551826469e-05 3.634644995026692e-05 2.0295213621716706e-09 1.0305672698745657e-11 1.5550455959178486e-06 0.01664109232689093 8.533030345699353e-17 0.0009252532821720597 -1.0 7.695031155172816e-14 0.0007151459162818186 -1.0 -1.0 5.182269672663266e-10 3.0160758454323657e-07 -1.0 0.06539005235506369 3.452537179033895e-05 -1.0 -1.0 3.855884234344845e-06 0.00017699858357744463 2.594951085377588e-06 1.1333627604823673e-09 -1.0 2.4306375693722205e-06 0.0063790654507599135 2.2060992580622125e-11 -1.0 -1.0 0.0848872967381878 3.932389299316285e-06 1.2911827198500372e-08 1.957092814065688e-05 0.0007213722022489493 0.0018327593437608 1.6699254277734109e-06 0.007188410694198128 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 - -10.0 0.07673145137236567 6.5427006446456266e-06 0.017541380096893787 0.0072454516100906195 7.476545089278483e-05 2.253382683081525e-05 0.00021040609012865135 0.828589987401454 -10.0 1.0262431550731535e-12 3.3164825349503764e-15 0.06480331412016534 3.5981031766416144e-05 -10.0 1.6205064883026195e-10 0.0001777013641125591 0.001670700632870335 0.0004323138781337963 -10.0 -10.0 0.0006922231352729155 0.6550426960512012 -10.0 2.5639656866250452e-05 1.4508262073256658e-12 -10.0 -10.0 0.009127050763987512 3.852324755037882e-11 -10.0 0.008175033526488424 -10.0 0.15148978428271614 2.515735579020177e-06 2.1612150425221703e-05 1.3181562352445092e-09 0.00029282097947816227 0.0002762955748645507 2.9362687471355483e-13 1.6859219614058023e-18 3.937160538176268e-06 2.6011787393916804e-05 0.015708293135808408 -10.0 1.7041669224797384e-08 1.6421491993751715e-10 4.899999304038166 6.948292975998466e-06 -10.0 -10.0 0.8657573566388354 0.06639379668946672 1.681305446488884e-10 0.00022487617828938325 0.0004109066205029878 0.8288806458368345 -10.0 0.07385384405891665 -10.0 -10.0 1.0083949647509469e-05 6.655926357459275e-07 0.016279794552427576 1.4825219199133352e-11 -10.0 0.000193847081892941 0.0001284759166582202 0.0005116174051620571 2.5447281241730365e-06 3.853097455417877e-05 -10.0 0.0008887234042637684 0.16218217824421122 -10.0 1.8747824667478637e-13 5.97653814110781e-05 -10.0 -10.0 1.2296747260635405e-05 -10.0 -10.0 7.754700849337902e-05 0.0007091263906557291 3.2799045883372076e-05 0.0020573181450240785 0.0005694959706678497 1.271128669411295e-06 0.010019432401508087 -10.0 -10.0 6.848642863465585e-06 5.7839743599378564e-05 2.380955631371226e-05 0.0004756872959630257 2.1360401784344976e-17 0.004602961711512756 1.0092905083158804e-10 0.8095280568305474 3.89841667138598e-07 0.00043549469813297995 -10.0 5.0 0.015447823995470043 -10.0 -10.0 0.01959411999677113 4.8573842213878144 -10.0 7.96289978992081e-05 5.619835883512353e-13 0.8356487100743359 2.7207433165796704e-10 0.002746424406288768 4.076762579823379e-16 0.00021249991820046548 8.037131813528501e-06 8.846493399850663e-05 0.0001080711295768551 -10.0 -10.0 0.010737081832502356 2.679340942769232e-05 0.00021648918040467362 0.0026685798378090807 0.00013455341306162382 0.00025629068330446217 0.00013228013765525016 -10.0 -10.0 7.002861620919232e-07 1.51729120573124e-13 -10.0 7.989579285134734e-05 2.639268952045579e-09 -10.0 0.13330619853379314 0.0021534568699157807 2.4469456008493615e-08 8.211982683649991e-08 1.951356547084204e-14 0.0016419031150495913 5.985108617124989e-05 0.000128584557092134 1.5476769237571296e-13 5.537915413446234e-07 0.016637126543321872 2.499931357628383e-06 8.020767115181574e-06 2.7697233171399415e-12 -10.0 0.07197662162700777 -10.0 0.8339317973815891 0.000282262769445327 0.00033532954038420373 5.504813693036933e-11 0.07136750029575815 0.8636569925236791 -10.0 -10.0 6.3151540822133754e-06 4.877856021170283 -10.0 3.7051518220083885e-05 -10.0 0.017950613169359905 7.84289689494925e-05 9.041149893307805e-06 -10.0 8.48176145671274e-09 0.000903108551826469 0.0003634644995026692 2.0295213621716707e-08 1.0305672698745658e-10 1.5550455959178486e-05 0.1664109232689093 8.533030345699353e-16 0.009252532821720597 -10.0 7.695031155172816e-13 0.007151459162818186 -10.0 -10.0 5.182269672663266e-09 3.0160758454323656e-06 -10.0 0.6539005235506369 0.0003452537179033895 -10.0 -10.0 3.855884234344845e-05 0.0017699858357744464 2.594951085377588e-05 1.1333627604823672e-08 -10.0 2.4306375693722206e-05 0.06379065450759913 2.2060992580622125e-10 -10.0 -10.0 0.848872967381878 3.9323892993162844e-05 1.2911827198500372e-07 0.0001957092814065688 0.0072137220224894934 0.018327593437608 1.669925427773411e-05 0.07188410694198127 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 - 3.0 - 1.5 - 10 - 1e-38 - - - 5 6 7 - -240 -240 -240 - 240 240 240 - - - 2 - neutron - 0.0 0.5 20000000.0 - -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.06827568359008944 0.00017329528628152923 0.018541112907970003 0.011113024641218336 8.76041935303527e-05 8.168081568291806e-05 0.00046693784586018913 0.4952432605379829 2.2512327083641865e-10 4.6258592060517866e-07 2.7044817176109556e-05 0.054902816619122045 0.00014126153603265097 8.304845400451923e-08 2.2950716170641463e-05 0.0001335621430311088 0.003432919201293737 0.0005197081842709918 5.726152701849347e-16 -1.0 0.0013332554437480327 0.4346984808472855 7.691527781829036e-17 0.00011042047857307253 3.0571050855707084e-06 -1.0 4.986698316296507e-06 0.011128424196387618 1.3106335518133802e-05 5.447493368067179e-10 0.011440381012864086 4.670673837764874e-08 0.12364069326350118 0.00012558879474108074 3.136829920007009e-05 2.34750482787598e-05 0.001537590300149361 0.0017693279949094218 6.717625881826167e-05 4.587143797469485e-08 2.6211239246796104e-05 0.00030002326388383244 0.01729816946709781 4.5757238372058234e-11 6.92296299242814e-05 3.792209399652396e-06 -1.0 9.256616392487858e-05 8.541606727879512e-07 -1.0 0.49317081643076643 0.05500108683629957 1.298062961617927e-05 0.0006870228272606287 0.0008231482263927564 0.5 3.419181071650816e-06 0.06433819434431046 6.574706543646946e-16 4.0404626147344e-13 0.0003782322077390033 0.00012637534695610975 0.017602971074583987 1.0044390728858277e-05 -1.0 0.0008787411098754915 0.00023686818807101741 0.0009304497896205121 6.949587369646497e-05 0.00012918433677593975 2.0707648529746894e-06 0.0023295401596005313 0.11819165666519223 -1.0 2.1606550732170693e-05 0.00022057970761624768 -1.0 1.0325207105027347e-08 0.0002029396545382813 2.1234980618763222e-13 -1.0 0.00024175998138228048 0.001178616843705956 0.0001399807456616496 0.0038811025614225057 0.0011076796092102186 0.00010814452764860558 0.010955637485319249 -1.0 9.382800796426366e-17 5.098747893974248e-05 0.00027052054298385255 0.00021305826462238593 0.000808100901275472 4.233410073487772e-06 0.0051513292132259495 8.616027294555462e-07 0.4873897581604287 2.9093345975075226e-05 0.000469589568463265 -1.0 -1.0 0.017802624844035688 1.3116807133030035e-15 2.617878055106949e-16 0.01884234453311008 -1.0 2.027381220364608e-16 0.0004299195570308035 1.4628847959717225e-05 0.4899952354672477 2.774067049392004e-07 0.004441328419270369 4.279460329782799e-09 0.0012297303081805393 0.0002792896409558745 0.0006934466049804711 8.370896020975689e-05 3.8270536025799805e-15 -1.0 0.01208261392046878 0.0001497958445994315 0.0015016500045374082 0.005384625561469684 0.00025519632243117644 0.0011028020390507682 0.0002362355168421952 -1.0 -1.0 8.862760180332873e-05 1.3806577785135177e-06 -1.0 0.0001319677450153934 2.6866894852481906e-05 -1.0 0.11510120013926417 0.002672923773363389 1.2257360246712472e-05 8.544159282151438e-05 2.777791741571174e-05 0.0014957033281168012 0.0003257646134837451 0.0008919466185572302 1.2907171695294846e-13 4.6842350882367666e-05 0.01721806295736377 0.00025106741008066317 0.00022234471595443313 2.101790478097517e-09 5.752120832330886e-11 0.06428782790571179 3.2478301680021854e-08 0.4903495510314233 0.001146714816952061 0.0004777967757842694 1.0882011018684852e-06 0.05708498338352203 0.4981497862127628 -1.0 3.2790914744011534e-06 0.00010077009726691265 -1.0 2.210707325798542e-07 4.3166052799897375e-05 -1.0 0.01825795827020865 0.0003182108781955478 3.8561696337086434e-05 1.0995322456277382e-06 9.53380658669839e-06 0.001550036616828061 0.0012648333883989995 5.272021975060514e-06 1.9257248009114363e-05 0.00016757997590048827 0.11891251244906934 5.515522196186723e-06 0.01102706140784316 9.904481358675478e-13 3.520674728719278e-05 0.011294708566804167 7.239087311622819e-08 -1.0 3.5740607788414942e-06 0.00035983547999740665 1.0588257623722672e-07 0.42372713683725016 0.0011273340972061078 8.582011716859219e-15 1.1514579117272292e-10 0.00043130977711323417 0.0027847329307350423 0.00017287873387910357 6.401055165502384e-06 1.722191123879782e-08 0.00020848309220162036 0.05639071613735579 1.8064550990294753e-05 1.289814511028015e-06 1.078687446476881e-14 0.4897735047310072 0.0002981891705231209 0.00016079768514218546 9.332371598523186e-05 0.011724714376848187 0.019040707071165303 0.00014242053195529864 0.06684484191345574 -1.0 - -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.6827568359008944 0.0017329528628152924 0.18541112907970003 0.11113024641218336 0.0008760419353035269 0.0008168081568291806 0.004669378458601891 4.952432605379829 2.2512327083641864e-09 4.625859206051786e-06 0.00027044817176109554 0.5490281661912204 0.0014126153603265096 8.304845400451923e-07 0.00022950716170641464 0.001335621430311088 0.03432919201293737 0.0051970818427099184 5.726152701849347e-15 -10.0 0.013332554437480326 4.346984808472855 7.6915277818290365e-16 0.0011042047857307254 3.0571050855707086e-05 -10.0 4.986698316296507e-05 0.11128424196387618 0.000131063355181338 5.447493368067179e-09 0.11440381012864086 4.670673837764874e-07 1.2364069326350118 0.0012558879474108074 0.0003136829920007009 0.000234750482787598 0.01537590300149361 0.01769327994909422 0.0006717625881826168 4.587143797469485e-07 0.000262112392467961 0.0030002326388383245 0.1729816946709781 4.5757238372058235e-10 0.0006922962992428139 3.792209399652396e-05 -10.0 0.0009256616392487858 8.541606727879512e-06 -10.0 4.931708164307665 0.5500108683629957 0.00012980629616179268 0.006870228272606287 0.008231482263927564 5.0 3.419181071650816e-05 0.6433819434431045 6.574706543646946e-15 4.0404626147344005e-12 0.003782322077390033 0.0012637534695610975 0.17602971074583987 0.00010044390728858278 -10.0 0.008787411098754914 0.002368681880710174 0.00930449789620512 0.0006949587369646498 0.0012918433677593976 2.0707648529746893e-05 0.023295401596005315 1.1819165666519222 -10.0 0.00021606550732170693 0.0022057970761624767 -10.0 1.0325207105027347e-07 0.0020293965453828133 2.123498061876322e-12 -10.0 0.0024175998138228046 0.01178616843705956 0.0013998074566164962 0.03881102561422506 0.011076796092102187 0.0010814452764860557 0.10955637485319249 -10.0 9.382800796426367e-16 0.0005098747893974248 0.0027052054298385255 0.0021305826462238594 0.00808100901275472 4.233410073487772e-05 0.0515132921322595 8.61602729455546e-06 4.873897581604287 0.0002909334597507523 0.0046958956846326495 -10.0 -10.0 0.1780262484403569 1.3116807133030034e-14 2.6178780551069492e-15 0.1884234453311008 -10.0 2.027381220364608e-15 0.004299195570308035 0.00014628847959717226 4.899952354672477 2.774067049392004e-06 0.04441328419270369 4.279460329782799e-08 0.012297303081805393 0.002792896409558745 0.006934466049804711 0.0008370896020975689 3.8270536025799805e-14 -10.0 0.1208261392046878 0.001497958445994315 0.015016500045374082 0.05384625561469684 0.0025519632243117644 0.011028020390507681 0.002362355168421952 -10.0 -10.0 0.0008862760180332873 1.3806577785135176e-05 -10.0 0.001319677450153934 0.00026866894852481903 -10.0 1.1510120013926417 0.026729237733633893 0.00012257360246712473 0.0008544159282151438 0.0002777791741571174 0.014957033281168012 0.0032576461348374514 0.008919466185572301 1.2907171695294846e-12 0.00046842350882367664 0.17218062957363772 0.0025106741008066318 0.0022234471595443312 2.101790478097517e-08 5.752120832330886e-10 0.6428782790571179 3.2478301680021856e-07 4.903495510314233 0.01146714816952061 0.004777967757842694 1.0882011018684853e-05 0.5708498338352204 4.981497862127628 -10.0 3.279091474401153e-05 0.0010077009726691265 -10.0 2.210707325798542e-06 0.00043166052799897375 -10.0 0.1825795827020865 0.0031821087819554777 0.00038561696337086434 1.0995322456277382e-05 9.53380658669839e-05 0.01550036616828061 0.012648333883989995 5.272021975060514e-05 0.00019257248009114364 0.0016757997590048828 1.1891251244906933 5.515522196186723e-05 0.1102706140784316 9.904481358675478e-12 0.0003520674728719278 0.11294708566804167 7.239087311622819e-07 -10.0 3.574060778841494e-05 0.0035983547999740664 1.0588257623722672e-06 4.237271368372502 0.011273340972061077 8.582011716859219e-14 1.151457911727229e-09 0.004313097771132342 0.027847329307350423 0.0017287873387910357 6.401055165502385e-05 1.722191123879782e-07 0.0020848309220162036 0.5639071613735579 0.00018064550990294753 1.289814511028015e-05 1.078687446476881e-13 4.897735047310072 0.002981891705231209 0.0016079768514218546 0.0009332371598523186 0.11724714376848187 0.19040707071165303 0.0014242053195529865 0.6684484191345574 -10.0 - 3.0 - 1.5 - 10 - 1e-38 - - 200 - - - - 5 10 15 - -240 -240 -240 - 240 240 240 - - - 1 - - - 0.0 0.5 20000000.0 - - - neutron photon - - - 1 2 3 - flux - - - diff --git a/tests/regression_tests/weightwindows/local/inputs_true.dat b/tests/regression_tests/weightwindows/local/inputs_true.dat new file mode 100644 index 000000000..57dc42ca4 --- /dev/null +++ b/tests/regression_tests/weightwindows/local/inputs_true.dat @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixed source + 500 + 2 + + + 0.001 0.001 0.001 + + + 14000000.0 1.0 + + + true + + 2 + neutron + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0008758251780046591 -1.0 -1.0 -1.0 -1.0 0.00044494017319042853 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.6620271125067025e-05 0.0006278777700923334 3.3816651814344154e-05 -1.0 -1.0 0.0024363004681119066 0.04404124277352227 0.002389900091734746 -1.0 -1.0 0.001096572213298872 0.04862206884590391 0.0011530054432113332 -1.0 -1.0 -1.0 0.00029204950777272335 2.2332845991385424e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0001298973206651331 0.0028826281529980655 0.00018941326535850932 -1.0 5.4657521927731274e-05 0.014670839729146354 0.49999999999999994 0.011271060592765664 -1.0 -1.0 0.014846491082523524 0.4897211700090108 0.013284874451810723 -1.0 -1.0 5.14657989105535e-05 0.0010115278438204965 0.0008429411802845685 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0009344129479768359 -1.0 -1.0 -1.0 0.0024828273390431962 0.04299740304329489 0.0020539079252555113 -1.0 -1.0 0.003034165905819096 0.04870927636605937 0.00313101668086297 -1.0 -1.0 -1.0 6.339910999436737e-05 7.442176086066386e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.00011276372995589871 0.0002236100157024887 9.56304298913265e-08 -1.0 -1.0 0.0001596955110781239 9.960576598335084e-06 5.3833030623153676e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 4.0453018186074215e-05 4.6013290110121894e-05 2.709738801641897e-05 -1.0 -1.0 -1.0 9.538410811938703e-05 1.992978141244964e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0723231851298364e-05 -1.0 -1.0 -1.0 -1.0 0.0008030100296698905 0.017386220476187222 0.0005027758935317528 -1.0 -1.0 0.00103568411326969 0.015609071124009234 0.0007473731339616588 -1.0 -1.0 -1.0 6.979537549963374e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0014257756927429828 5.9098070539559466e-05 -1.0 5.0168071459366625e-06 0.005588863190166347 0.5 0.003930588100414563 -1.0 -1.0 0.005163345217476071 0.48250750993887326 0.003510432129522241 -1.0 -1.0 3.371977841720992e-05 0.0006640103276501794 9.554899988419713e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.000187872910697387 -1.0 -1.0 -1.0 0.0009134587866163143 0.017081858078684467 0.0002972747357542354 -1.0 -1.0 0.0007973472495507653 0.019300903254809747 0.000902203032280694 -1.0 -1.0 6.170015233787292e-05 1.898984300674969e-05 9.85411583595722e-07 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0173913765490095e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.008758251780046591 -10.0 -10.0 -10.0 -10.0 0.004449401731904285 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00016620271125067024 0.006278777700923334 0.00033816651814344155 -10.0 -10.0 0.024363004681119065 0.4404124277352227 0.02389900091734746 -10.0 -10.0 0.01096572213298872 0.4862206884590391 0.011530054432113333 -10.0 -10.0 -10.0 0.0029204950777272335 0.00022332845991385425 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0012989732066513312 0.028826281529980655 0.0018941326535850931 -10.0 0.0005465752192773128 0.14670839729146354 4.999999999999999 0.11271060592765664 -10.0 -10.0 0.14846491082523525 4.897211700090108 0.13284874451810724 -10.0 -10.0 0.000514657989105535 0.010115278438204964 0.008429411802845685 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00934412947976836 -10.0 -10.0 -10.0 0.024828273390431962 0.4299740304329489 0.020539079252555114 -10.0 -10.0 0.03034165905819096 0.48709276366059373 0.0313101668086297 -10.0 -10.0 -10.0 0.0006339910999436737 0.0007442176086066385 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0011276372995589871 0.002236100157024887 9.56304298913265e-07 -10.0 -10.0 0.001596955110781239 9.960576598335084e-05 0.0005383303062315367 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00040453018186074213 0.00046013290110121896 0.0002709738801641897 -10.0 -10.0 -10.0 0.0009538410811938703 0.00019929781412449641 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010723231851298364 -10.0 -10.0 -10.0 -10.0 0.008030100296698905 0.17386220476187222 0.0050277589353175285 -10.0 -10.0 0.010356841132696899 0.15609071124009233 0.007473731339616587 -10.0 -10.0 -10.0 0.0006979537549963374 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.014257756927429827 0.0005909807053955946 -10.0 5.016807145936663e-05 0.055888631901663474 5.0 0.039305881004145636 -10.0 -10.0 0.05163345217476071 4.825075099388733 0.03510432129522241 -10.0 -10.0 0.00033719778417209923 0.006640103276501793 0.0009554899988419713 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00187872910697387 -10.0 -10.0 -10.0 0.009134587866163143 0.17081858078684467 0.002972747357542354 -10.0 -10.0 0.007973472495507653 0.19300903254809748 0.009022030322806941 -10.0 -10.0 0.0006170015233787292 0.0001898984300674969 9.85411583595722e-06 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010173913765490096 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + + 5 6 7 + -240 -240 -240 + 240 240 240 + + + 2 + photon + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 2.0445691096074744e-05 -1.0 -1.0 -1.0 0.00013281328509288383 0.0006267128082339883 -1.0 -1.0 -1.0 -1.0 0.0004209808495056742 5.340221214300681e-05 -1.0 -1.0 -1.0 1.553693492042344e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 3.7096847855980484e-05 -1.0 -1.0 5.41969614042385e-05 0.0007918090215756442 9.763572147337743e-05 -1.0 -1.0 0.0026398319229115233 0.04039871468258457 0.002806654735003014 -1.0 -1.0 0.0027608366482897244 0.040190978087723005 0.0025084416103979975 -1.0 -1.0 9.774868433298412e-05 0.0006126404916879651 0.00014841730774317252 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.75342408717107e-06 1.7610849405640072e-05 -1.0 -1.0 0.0001160370359598608 0.002822592083222468 0.0003897177301239376 -1.0 -1.0 0.014965179733172532 0.5 0.011723491704290401 -1.0 2.260493623875525e-05 0.015157364795884922 0.49376993953402387 0.013111419473204967 -1.0 -1.0 0.00014919915366377564 0.002197964829133137 0.0003209711829219673 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.4806422867046436e-05 -1.0 -1.0 -1.0 9.207268175745281e-05 0.0007066033561638983 -1.0 -1.0 -1.0 0.003246064903858183 0.03905493028065908 0.0025380574501073605 -1.0 -1.0 0.0032499260211917933 0.04335567738779479 0.0031577214557017056 4.521352809838806e-05 -1.0 0.00018268090756001903 0.0004353654810741932 0.00011273259573092372 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.650194311662424e-06 0.00029403373970835075 0.00013506203419326438 -1.0 -1.0 4.5065302725431816e-06 0.00027725323638744237 3.547290864255937e-05 -1.0 -1.0 -1.0 8.786172732576295e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00020445691096074745 -10.0 -10.0 -10.0 0.0013281328509288383 0.006267128082339883 -10.0 -10.0 -10.0 -10.0 0.004209808495056742 0.0005340221214300681 -10.0 -10.0 -10.0 0.00015536934920423439 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00037096847855980485 -10.0 -10.0 0.000541969614042385 0.007918090215756443 0.0009763572147337743 -10.0 -10.0 0.026398319229115234 0.4039871468258457 0.02806654735003014 -10.0 -10.0 0.027608366482897245 0.40190978087723006 0.025084416103979976 -10.0 -10.0 0.0009774868433298411 0.0061264049168796506 0.0014841730774317252 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.753424087171071e-05 0.00017610849405640073 -10.0 -10.0 0.001160370359598608 0.02822592083222468 0.003897177301239376 -10.0 -10.0 0.14965179733172532 5.0 0.11723491704290401 -10.0 0.0002260493623875525 0.15157364795884923 4.937699395340239 0.13111419473204966 -10.0 -10.0 0.0014919915366377564 0.02197964829133137 0.003209711829219673 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00014806422867046437 -10.0 -10.0 -10.0 0.0009207268175745281 0.007066033561638983 -10.0 -10.0 -10.0 0.03246064903858183 0.39054930280659084 0.025380574501073606 -10.0 -10.0 0.03249926021191793 0.4335567738779479 0.03157721455701706 0.0004521352809838806 -10.0 0.0018268090756001904 0.004353654810741932 0.001127325957309237 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.650194311662424e-05 0.0029403373970835075 0.0013506203419326437 -10.0 -10.0 4.5065302725431816e-05 0.002772532363874424 0.0003547290864255937 -10.0 -10.0 -10.0 0.0008786172732576295 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + false + + true + true + + 200 + + + + 5 10 15 + -240 -240 -240 + 240 240 240 + + + 1 + + + 0.0 0.5 20000000.0 + + + neutron photon + + + 1 2 3 + flux + + + diff --git a/tests/regression_tests/weightwindows/local/results_true.dat b/tests/regression_tests/weightwindows/local/results_true.dat new file mode 100644 index 000000000..d3dfa119d --- /dev/null +++ b/tests/regression_tests/weightwindows/local/results_true.dat @@ -0,0 +1 @@ +a78972fe3c0dadfc256cfb139c5ca8c7b634738e1895ad2d6286ed5e2567c6dc518e499363908e9058432684148a706a179a39110f703d5322491184a1d0c3e4 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat deleted file mode 100644 index 122c5d890..000000000 --- a/tests/regression_tests/weightwindows/results_true.dat +++ /dev/null @@ -1 +0,0 @@ -5df0c08573ccee3fd3495c877c7dc0c4b69c245faf8473b848c89fb837dea7fd437936ffebeb6455c793e66046c46eb0ad6941cc5ef1699fc5fc8a7fcb9b5a0c \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/shared/inputs_true.dat b/tests/regression_tests/weightwindows/shared/inputs_true.dat new file mode 100644 index 000000000..6ec7d12e3 --- /dev/null +++ b/tests/regression_tests/weightwindows/shared/inputs_true.dat @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixed source + 500 + 2 + + + 0.001 0.001 0.001 + + + 14000000.0 1.0 + + + true + + 2 + neutron + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0008758251780046591 -1.0 -1.0 -1.0 -1.0 0.00044494017319042853 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.6620271125067025e-05 0.0006278777700923334 3.3816651814344154e-05 -1.0 -1.0 0.0024363004681119066 0.04404124277352227 0.002389900091734746 -1.0 -1.0 0.001096572213298872 0.04862206884590391 0.0011530054432113332 -1.0 -1.0 -1.0 0.00029204950777272335 2.2332845991385424e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0001298973206651331 0.0028826281529980655 0.00018941326535850932 -1.0 5.4657521927731274e-05 0.014670839729146354 0.49999999999999994 0.011271060592765664 -1.0 -1.0 0.014846491082523524 0.4897211700090108 0.013284874451810723 -1.0 -1.0 5.14657989105535e-05 0.0010115278438204965 0.0008429411802845685 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0009344129479768359 -1.0 -1.0 -1.0 0.0024828273390431962 0.04299740304329489 0.0020539079252555113 -1.0 -1.0 0.003034165905819096 0.04870927636605937 0.00313101668086297 -1.0 -1.0 -1.0 6.339910999436737e-05 7.442176086066386e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.00011276372995589871 0.0002236100157024887 9.56304298913265e-08 -1.0 -1.0 0.0001596955110781239 9.960576598335084e-06 5.3833030623153676e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 4.0453018186074215e-05 4.6013290110121894e-05 2.709738801641897e-05 -1.0 -1.0 -1.0 9.538410811938703e-05 1.992978141244964e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0723231851298364e-05 -1.0 -1.0 -1.0 -1.0 0.0008030100296698905 0.017386220476187222 0.0005027758935317528 -1.0 -1.0 0.00103568411326969 0.015609071124009234 0.0007473731339616588 -1.0 -1.0 -1.0 6.979537549963374e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0014257756927429828 5.9098070539559466e-05 -1.0 5.0168071459366625e-06 0.005588863190166347 0.5 0.003930588100414563 -1.0 -1.0 0.005163345217476071 0.48250750993887326 0.003510432129522241 -1.0 -1.0 3.371977841720992e-05 0.0006640103276501794 9.554899988419713e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.000187872910697387 -1.0 -1.0 -1.0 0.0009134587866163143 0.017081858078684467 0.0002972747357542354 -1.0 -1.0 0.0007973472495507653 0.019300903254809747 0.000902203032280694 -1.0 -1.0 6.170015233787292e-05 1.898984300674969e-05 9.85411583595722e-07 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0173913765490095e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.008758251780046591 -10.0 -10.0 -10.0 -10.0 0.004449401731904285 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00016620271125067024 0.006278777700923334 0.00033816651814344155 -10.0 -10.0 0.024363004681119065 0.4404124277352227 0.02389900091734746 -10.0 -10.0 0.01096572213298872 0.4862206884590391 0.011530054432113333 -10.0 -10.0 -10.0 0.0029204950777272335 0.00022332845991385425 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0012989732066513312 0.028826281529980655 0.0018941326535850931 -10.0 0.0005465752192773128 0.14670839729146354 4.999999999999999 0.11271060592765664 -10.0 -10.0 0.14846491082523525 4.897211700090108 0.13284874451810724 -10.0 -10.0 0.000514657989105535 0.010115278438204964 0.008429411802845685 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00934412947976836 -10.0 -10.0 -10.0 0.024828273390431962 0.4299740304329489 0.020539079252555114 -10.0 -10.0 0.03034165905819096 0.48709276366059373 0.0313101668086297 -10.0 -10.0 -10.0 0.0006339910999436737 0.0007442176086066385 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0011276372995589871 0.002236100157024887 9.56304298913265e-07 -10.0 -10.0 0.001596955110781239 9.960576598335084e-05 0.0005383303062315367 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00040453018186074213 0.00046013290110121896 0.0002709738801641897 -10.0 -10.0 -10.0 0.0009538410811938703 0.00019929781412449641 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010723231851298364 -10.0 -10.0 -10.0 -10.0 0.008030100296698905 0.17386220476187222 0.0050277589353175285 -10.0 -10.0 0.010356841132696899 0.15609071124009233 0.007473731339616587 -10.0 -10.0 -10.0 0.0006979537549963374 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.014257756927429827 0.0005909807053955946 -10.0 5.016807145936663e-05 0.055888631901663474 5.0 0.039305881004145636 -10.0 -10.0 0.05163345217476071 4.825075099388733 0.03510432129522241 -10.0 -10.0 0.00033719778417209923 0.006640103276501793 0.0009554899988419713 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00187872910697387 -10.0 -10.0 -10.0 0.009134587866163143 0.17081858078684467 0.002972747357542354 -10.0 -10.0 0.007973472495507653 0.19300903254809748 0.009022030322806941 -10.0 -10.0 0.0006170015233787292 0.0001898984300674969 9.85411583595722e-06 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010173913765490096 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + + 5 6 7 + -240 -240 -240 + 240 240 240 + + + 2 + photon + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 2.0445691096074744e-05 -1.0 -1.0 -1.0 0.00013281328509288383 0.0006267128082339883 -1.0 -1.0 -1.0 -1.0 0.0004209808495056742 5.340221214300681e-05 -1.0 -1.0 -1.0 1.553693492042344e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 3.7096847855980484e-05 -1.0 -1.0 5.41969614042385e-05 0.0007918090215756442 9.763572147337743e-05 -1.0 -1.0 0.0026398319229115233 0.04039871468258457 0.002806654735003014 -1.0 -1.0 0.0027608366482897244 0.040190978087723005 0.0025084416103979975 -1.0 -1.0 9.774868433298412e-05 0.0006126404916879651 0.00014841730774317252 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.75342408717107e-06 1.7610849405640072e-05 -1.0 -1.0 0.0001160370359598608 0.002822592083222468 0.0003897177301239376 -1.0 -1.0 0.014965179733172532 0.5 0.011723491704290401 -1.0 2.260493623875525e-05 0.015157364795884922 0.49376993953402387 0.013111419473204967 -1.0 -1.0 0.00014919915366377564 0.002197964829133137 0.0003209711829219673 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.4806422867046436e-05 -1.0 -1.0 -1.0 9.207268175745281e-05 0.0007066033561638983 -1.0 -1.0 -1.0 0.003246064903858183 0.03905493028065908 0.0025380574501073605 -1.0 -1.0 0.0032499260211917933 0.04335567738779479 0.0031577214557017056 4.521352809838806e-05 -1.0 0.00018268090756001903 0.0004353654810741932 0.00011273259573092372 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.650194311662424e-06 0.00029403373970835075 0.00013506203419326438 -1.0 -1.0 4.5065302725431816e-06 0.00027725323638744237 3.547290864255937e-05 -1.0 -1.0 -1.0 8.786172732576295e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00020445691096074745 -10.0 -10.0 -10.0 0.0013281328509288383 0.006267128082339883 -10.0 -10.0 -10.0 -10.0 0.004209808495056742 0.0005340221214300681 -10.0 -10.0 -10.0 0.00015536934920423439 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00037096847855980485 -10.0 -10.0 0.000541969614042385 0.007918090215756443 0.0009763572147337743 -10.0 -10.0 0.026398319229115234 0.4039871468258457 0.02806654735003014 -10.0 -10.0 0.027608366482897245 0.40190978087723006 0.025084416103979976 -10.0 -10.0 0.0009774868433298411 0.0061264049168796506 0.0014841730774317252 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.753424087171071e-05 0.00017610849405640073 -10.0 -10.0 0.001160370359598608 0.02822592083222468 0.003897177301239376 -10.0 -10.0 0.14965179733172532 5.0 0.11723491704290401 -10.0 0.0002260493623875525 0.15157364795884923 4.937699395340239 0.13111419473204966 -10.0 -10.0 0.0014919915366377564 0.02197964829133137 0.003209711829219673 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00014806422867046437 -10.0 -10.0 -10.0 0.0009207268175745281 0.007066033561638983 -10.0 -10.0 -10.0 0.03246064903858183 0.39054930280659084 0.025380574501073606 -10.0 -10.0 0.03249926021191793 0.4335567738779479 0.03157721455701706 0.0004521352809838806 -10.0 0.0018268090756001904 0.004353654810741932 0.001127325957309237 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.650194311662424e-05 0.0029403373970835075 0.0013506203419326437 -10.0 -10.0 4.5065302725431816e-05 0.002772532363874424 0.0003547290864255937 -10.0 -10.0 -10.0 0.0008786172732576295 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + true + + true + true + + 200 + + + + 5 10 15 + -240 -240 -240 + 240 240 240 + + + 1 + + + 0.0 0.5 20000000.0 + + + neutron photon + + + 1 2 3 + flux + + + diff --git a/tests/regression_tests/weightwindows/shared/results_true.dat b/tests/regression_tests/weightwindows/shared/results_true.dat new file mode 100644 index 000000000..01381db32 --- /dev/null +++ b/tests/regression_tests/weightwindows/shared/results_true.dat @@ -0,0 +1 @@ +27c2d218ecf46161088003fbb39ccd63495f80d6e0b27dbc3b3e845b49b282dabf0eaba4fb619be15c6116fc963ec5ce1fe4197efbe4084972ec761e02e70eb5 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/__init__.py b/tests/regression_tests/weightwindows/survival_biasing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat new file mode 100644 index 000000000..fe144dc62 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + fixed source + 50 + 5 + + + + 0.01 1.0 + + + + + + + 14100000.0 1.0 + + + true + + 1 + neutron + 0.46135961568957096 0.2874485390202603 0.13256013950494605 0.052765209609807295 0.020958440832685638 0.006317250035749876 0.002627037175682506 0.00030032343592437284 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4611178493390162 0.28624862560842024 0.13999870622621136 0.05508479408959756 0.018281241958254993 0.0055577764872361555 0.0015265432226293397 0.00024298772352636966 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.46294957913063317 0.2857734367546051 0.13365623190336945 0.05034742166227103 0.017525851812913266 0.005096725451851077 0.0026297640951531403 0.00033732424392026144 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45772598750472554 0.281494921471495 0.13604397962762246 0.054792881472295364 0.019802376898755525 0.0073351745579128495 0.002067392946066426 9.529474085926143e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4739762983490318 0.28419061537046764 0.12757509901507888 0.0516920293019733 0.01919167874787235 0.007593035964707848 0.0017488176314107277 9.678267909681988e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47560199098558276 0.27728389146956994 0.12760802572535623 0.05251965811713552 0.022498960644951632 0.01063372459325151 0.004242071054914633 0.00045603112202665183 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47659199471934693 0.28886666944252215 0.1258637573398161 0.05818862375955657 0.020476313720744762 0.007143193244018263 0.0022364083022515273 0.0003953123781408474 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4693487369606823 0.27180862139490125 0.12833455570423483 0.055146743559938344 0.020667403529470333 0.007891508723137146 0.001643551705407358 0.0006501416781353278 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4538724931023337 0.2824491421084296 0.1275341706351566 0.055027501141893705 0.02305114640508087 0.008599303158607198 0.0025082611194161804 0.000518530311231696 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45258444753220123 0.26839102466484255 0.12991633918797083 0.05442263709947767 0.019695895223471486 0.005353204961887518 0.0015074698293840157 0.0001598680898180058 8.872047880811405e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4482467985982804 0.2813276470183359 0.13449860790768647 0.056103278190533436 0.026274320334196962 0.006884071908429303 0.0033099390738735458 0.0005337454397674922 0.0001386380399465575 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4875748950139487 0.2908586705316248 0.14212060658392278 0.06068108009637272 0.019467970468789477 0.00637102427946399 0.0028510425266191687 0.0012184421778115488 0.00029404541567146187 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.5 0.2852101551297824 0.13622307172321643 0.058302519884339946 0.023124734915152625 0.007401527839638796 0.002527635652743992 0.0008509612315162482 0.00018213334574554768 7.815977984795461e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47706397688984176 0.28313436859495966 0.13170624744221976 0.0525453512766549 0.021111753158319105 0.0067451713955609645 0.003204270539718037 0.0010308107242263192 0.0002759210144794204 0.00013727805365387318 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.48641988659284513 0.2816173231471242 0.131541850061199 0.054793200248327956 0.016517701691156576 0.006757591781856257 0.0025425350750908644 0.0006383278085839443 2.279421641064226e-05 0.0003597432472224371 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4307755159245372 0.2652713784080849 0.12753857957535653 0.05470396852881577 0.02049817309420563 0.00566559741247352 0.0007213846765465147 6.848024591311009e-05 -1.0 8.609308814924014e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.43631324024956025 0.2639077825530567 0.13368737962742414 0.05213531603720763 0.020628860469743833 0.008268384844678654 0.0028599221013934375 0.00013059757129982714 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45926287238219 0.2635824148883888 0.13104280996799478 0.055379812186041905 0.019255042561941074 0.007252095690246872 0.0018026593488140996 0.0003215836458542421 9.14840176000331e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4574378998400604 0.28714971179927723 0.1363507911051337 0.05010315954654347 0.017963278989571074 0.0058998176297971605 0.0021393135666168 0.00015677575033083482 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4821442782978812 0.27704132233083506 0.14149074035662307 0.05546042815834048 0.016800649382269998 0.004096804603054233 0.0019161108398578026 0.0005289600253155307 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + 2.306798078447855 1.4372426951013015 0.6628006975247303 0.2638260480490365 0.10479220416342819 0.03158625017874938 0.013135185878412529 0.0015016171796218643 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3055892466950807 1.4312431280421012 0.6999935311310568 0.2754239704479878 0.09140620979127496 0.027788882436180776 0.007632716113146698 0.0012149386176318483 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.314747895653166 1.4288671837730256 0.6682811595168472 0.25173710831135515 0.08762925906456634 0.025483627259255386 0.013148820475765701 0.0016866212196013073 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.288629937523628 1.407474607357475 0.6802198981381123 0.2739644073614768 0.09901188449377762 0.036675872789564246 0.01033696473033213 0.00047647370429630714 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.369881491745159 1.4209530768523382 0.6378754950753944 0.2584601465098665 0.09595839373936176 0.03796517982353924 0.008744088157053638 0.00048391339548409945 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3780099549279137 1.3864194573478497 0.6380401286267812 0.26259829058567763 0.11249480322475816 0.053168622966257545 0.021210355274573163 0.0022801556101332593 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.382959973596735 1.4443333472126108 0.6293187866990806 0.29094311879778284 0.10238156860372381 0.035715966220091315 0.011182041511257637 0.001976561890704237 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3467436848034113 1.3590431069745064 0.6416727785211741 0.2757337177996917 0.10333701764735166 0.03945754361568573 0.00821775852703679 0.0032507083906766388 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2693624655116684 1.412245710542148 0.637670853175783 0.27513750570946854 0.11525573202540434 0.04299651579303599 0.012541305597080901 0.00259265155615848 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.262922237661006 1.3419551233242126 0.6495816959398542 0.2721131854973884 0.09847947611735743 0.02676602480943759 0.007537349146920078 0.000799340449090029 0.0004436023940405703 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2412339929914022 1.4066382350916795 0.6724930395384323 0.28051639095266717 0.1313716016709848 0.03442035954214652 0.016549695369367727 0.0026687271988374613 0.0006931901997327875 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.4378744750697434 1.4542933526581239 0.7106030329196139 0.3034054004818636 0.09733985234394739 0.03185512139731995 0.014255212633095843 0.006092210889057744 0.0014702270783573093 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.5 1.4260507756489118 0.6811153586160822 0.2915125994216997 0.11562367457576313 0.03700763919819398 0.012638178263719959 0.004254806157581241 0.0009106667287277384 3.9079889923977307e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.385319884449209 1.4156718429747983 0.6585312372110987 0.2627267563832745 0.10555876579159552 0.03372585697780482 0.016021352698590185 0.005154053621131596 0.001379605072397102 0.000686390268269366 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.432099432964226 1.4080866157356209 0.657709250305995 0.2739660012416398 0.08258850845578287 0.03378795890928128 0.012712675375454322 0.0031916390429197216 0.0001139710820532113 0.0017987162361121855 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.1538775796226863 1.3263568920404245 0.6376928978767826 0.27351984264407886 0.10249086547102815 0.028327987062367603 0.0036069233827325737 0.0003424012295655504 -5.0 0.0004304654407462007 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.181566201247801 1.3195389127652835 0.6684368981371207 0.2606765801860381 0.10314430234871916 0.041341924223393264 0.014299610506967188 0.0006529878564991358 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.29631436191095 1.317912074441944 0.6552140498399739 0.27689906093020955 0.09627521280970537 0.03626047845123436 0.009013296744070498 0.0016079182292712106 4.574200880001655e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.287189499200302 1.435748558996386 0.6817539555256685 0.25051579773271737 0.08981639494785537 0.029499088148985803 0.010696567833083998 0.0007838787516541741 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.410721391489406 1.3852066116541752 0.7074537017831153 0.2773021407917024 0.08400324691134999 0.020484023015271167 0.009580554199289014 0.0026448001265776534 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 + 3.0 + 10 + 1e-38 + + + 20 20 1 + 0.0 0.0 0.0 + 160.0 160.0 160.0 + + false + + true + true + + + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat new file mode 100644 index 000000000..b457a245c --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat @@ -0,0 +1 @@ +386e507008ed3c72c6e1a101aafc01cfaaff2c0b10555558d1eab6332441f7b17264b55002d721151bac2f3e7c1a8aac4c5ed243f5270533d171850f985206ed \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat new file mode 100644 index 000000000..bafff89c1 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + fixed source + 50 + 5 + + + + 0.01 1.0 + + + + + + + 14100000.0 1.0 + + + true + + 1 + neutron + 0.46135961568957096 0.2874485390202603 0.13256013950494605 0.052765209609807295 0.020958440832685638 0.006317250035749876 0.002627037175682506 0.00030032343592437284 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4611178493390162 0.28624862560842024 0.13999870622621136 0.05508479408959756 0.018281241958254993 0.0055577764872361555 0.0015265432226293397 0.00024298772352636966 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.46294957913063317 0.2857734367546051 0.13365623190336945 0.05034742166227103 0.017525851812913266 0.005096725451851077 0.0026297640951531403 0.00033732424392026144 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45772598750472554 0.281494921471495 0.13604397962762246 0.054792881472295364 0.019802376898755525 0.0073351745579128495 0.002067392946066426 9.529474085926143e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4739762983490318 0.28419061537046764 0.12757509901507888 0.0516920293019733 0.01919167874787235 0.007593035964707848 0.0017488176314107277 9.678267909681988e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47560199098558276 0.27728389146956994 0.12760802572535623 0.05251965811713552 0.022498960644951632 0.01063372459325151 0.004242071054914633 0.00045603112202665183 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47659199471934693 0.28886666944252215 0.1258637573398161 0.05818862375955657 0.020476313720744762 0.007143193244018263 0.0022364083022515273 0.0003953123781408474 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4693487369606823 0.27180862139490125 0.12833455570423483 0.055146743559938344 0.020667403529470333 0.007891508723137146 0.001643551705407358 0.0006501416781353278 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4538724931023337 0.2824491421084296 0.1275341706351566 0.055027501141893705 0.02305114640508087 0.008599303158607198 0.0025082611194161804 0.000518530311231696 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45258444753220123 0.26839102466484255 0.12991633918797083 0.05442263709947767 0.019695895223471486 0.005353204961887518 0.0015074698293840157 0.0001598680898180058 8.872047880811405e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4482467985982804 0.2813276470183359 0.13449860790768647 0.056103278190533436 0.026274320334196962 0.006884071908429303 0.0033099390738735458 0.0005337454397674922 0.0001386380399465575 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4875748950139487 0.2908586705316248 0.14212060658392278 0.06068108009637272 0.019467970468789477 0.00637102427946399 0.0028510425266191687 0.0012184421778115488 0.00029404541567146187 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.5 0.2852101551297824 0.13622307172321643 0.058302519884339946 0.023124734915152625 0.007401527839638796 0.002527635652743992 0.0008509612315162482 0.00018213334574554768 7.815977984795461e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47706397688984176 0.28313436859495966 0.13170624744221976 0.0525453512766549 0.021111753158319105 0.0067451713955609645 0.003204270539718037 0.0010308107242263192 0.0002759210144794204 0.00013727805365387318 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.48641988659284513 0.2816173231471242 0.131541850061199 0.054793200248327956 0.016517701691156576 0.006757591781856257 0.0025425350750908644 0.0006383278085839443 2.279421641064226e-05 0.0003597432472224371 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4307755159245372 0.2652713784080849 0.12753857957535653 0.05470396852881577 0.02049817309420563 0.00566559741247352 0.0007213846765465147 6.848024591311009e-05 -1.0 8.609308814924014e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.43631324024956025 0.2639077825530567 0.13368737962742414 0.05213531603720763 0.020628860469743833 0.008268384844678654 0.0028599221013934375 0.00013059757129982714 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45926287238219 0.2635824148883888 0.13104280996799478 0.055379812186041905 0.019255042561941074 0.007252095690246872 0.0018026593488140996 0.0003215836458542421 9.14840176000331e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4574378998400604 0.28714971179927723 0.1363507911051337 0.05010315954654347 0.017963278989571074 0.0058998176297971605 0.0021393135666168 0.00015677575033083482 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4821442782978812 0.27704132233083506 0.14149074035662307 0.05546042815834048 0.016800649382269998 0.004096804603054233 0.0019161108398578026 0.0005289600253155307 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + 2.306798078447855 1.4372426951013015 0.6628006975247303 0.2638260480490365 0.10479220416342819 0.03158625017874938 0.013135185878412529 0.0015016171796218643 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3055892466950807 1.4312431280421012 0.6999935311310568 0.2754239704479878 0.09140620979127496 0.027788882436180776 0.007632716113146698 0.0012149386176318483 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.314747895653166 1.4288671837730256 0.6682811595168472 0.25173710831135515 0.08762925906456634 0.025483627259255386 0.013148820475765701 0.0016866212196013073 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.288629937523628 1.407474607357475 0.6802198981381123 0.2739644073614768 0.09901188449377762 0.036675872789564246 0.01033696473033213 0.00047647370429630714 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.369881491745159 1.4209530768523382 0.6378754950753944 0.2584601465098665 0.09595839373936176 0.03796517982353924 0.008744088157053638 0.00048391339548409945 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3780099549279137 1.3864194573478497 0.6380401286267812 0.26259829058567763 0.11249480322475816 0.053168622966257545 0.021210355274573163 0.0022801556101332593 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.382959973596735 1.4443333472126108 0.6293187866990806 0.29094311879778284 0.10238156860372381 0.035715966220091315 0.011182041511257637 0.001976561890704237 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3467436848034113 1.3590431069745064 0.6416727785211741 0.2757337177996917 0.10333701764735166 0.03945754361568573 0.00821775852703679 0.0032507083906766388 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2693624655116684 1.412245710542148 0.637670853175783 0.27513750570946854 0.11525573202540434 0.04299651579303599 0.012541305597080901 0.00259265155615848 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.262922237661006 1.3419551233242126 0.6495816959398542 0.2721131854973884 0.09847947611735743 0.02676602480943759 0.007537349146920078 0.000799340449090029 0.0004436023940405703 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2412339929914022 1.4066382350916795 0.6724930395384323 0.28051639095266717 0.1313716016709848 0.03442035954214652 0.016549695369367727 0.0026687271988374613 0.0006931901997327875 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.4378744750697434 1.4542933526581239 0.7106030329196139 0.3034054004818636 0.09733985234394739 0.03185512139731995 0.014255212633095843 0.006092210889057744 0.0014702270783573093 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.5 1.4260507756489118 0.6811153586160822 0.2915125994216997 0.11562367457576313 0.03700763919819398 0.012638178263719959 0.004254806157581241 0.0009106667287277384 3.9079889923977307e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.385319884449209 1.4156718429747983 0.6585312372110987 0.2627267563832745 0.10555876579159552 0.03372585697780482 0.016021352698590185 0.005154053621131596 0.001379605072397102 0.000686390268269366 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.432099432964226 1.4080866157356209 0.657709250305995 0.2739660012416398 0.08258850845578287 0.03378795890928128 0.012712675375454322 0.0031916390429197216 0.0001139710820532113 0.0017987162361121855 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.1538775796226863 1.3263568920404245 0.6376928978767826 0.27351984264407886 0.10249086547102815 0.028327987062367603 0.0036069233827325737 0.0003424012295655504 -5.0 0.0004304654407462007 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.181566201247801 1.3195389127652835 0.6684368981371207 0.2606765801860381 0.10314430234871916 0.041341924223393264 0.014299610506967188 0.0006529878564991358 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.29631436191095 1.317912074441944 0.6552140498399739 0.27689906093020955 0.09627521280970537 0.03626047845123436 0.009013296744070498 0.0016079182292712106 4.574200880001655e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.287189499200302 1.435748558996386 0.6817539555256685 0.25051579773271737 0.08981639494785537 0.029499088148985803 0.010696567833083998 0.0007838787516541741 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.410721391489406 1.3852066116541752 0.7074537017831153 0.2773021407917024 0.08400324691134999 0.020484023015271167 0.009580554199289014 0.0026448001265776534 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 + 3.0 + 10 + 1e-38 + + + 20 20 1 + 0.0 0.0 0.0 + 160.0 160.0 160.0 + + true + + true + true + + + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat new file mode 100644 index 000000000..11e5ffa77 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat @@ -0,0 +1 @@ +d17a437262d3316985fba4b48e21a7fcd5f32ac2d96ef0e66849f567deaeadc1e0f18d5d0bf5e9cab4246dbf823e7f43f249a1f67e928b27ae8c70b89f3cefbf \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/test.py b/tests/regression_tests/weightwindows/survival_biasing/test.py new file mode 100644 index 000000000..c3a3e91a2 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/test.py @@ -0,0 +1,91 @@ +from pathlib import Path + +import pytest +import numpy as np +import openmc +from openmc.utility_funcs import change_directory + +from tests.testing_harness import HashedPyAPITestHarness + + +def build_model(shared_secondary): + openmc.reset_auto_ids() + + # Material + w = openmc.Material(name='Tungsten') + w.add_element('W', 1.0) + w.set_density('g/cm3', 19.25) + + materials = openmc.Materials([w]) + + # Geometry surfaces + x0 = openmc.XPlane(x0=0.0, boundary_type='reflective') + x1 = openmc.XPlane(x0=160.0, boundary_type='vacuum') + y0 = openmc.YPlane(y0=0.0, boundary_type='reflective') + y1 = openmc.YPlane(y0=160.0, boundary_type='reflective') + z0 = openmc.ZPlane(z0=0.0, boundary_type='reflective') + z1 = openmc.ZPlane(z0=160.0, boundary_type='reflective') + + region = +x0 & -x1 & +y0 & -y1 & +z0 & -z1 + cell = openmc.Cell(region=region, fill=w) + root = openmc.Universe(cells=[cell]) + geometry = openmc.Geometry(root) + + # Source: planar on x=0, mono-directional along +x, 14.1 MeV neutrons + space = openmc.stats.CartesianIndependent( + openmc.stats.Discrete([0.01], [1.0]), + openmc.stats.Uniform(0.0, 160.0), + openmc.stats.Uniform(0.0, 160.0), + ) + angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + energy = openmc.stats.Discrete([14.1e6], [1.0]) + + source = openmc.IndependentSource(space=space, angle=angle, energy=energy) + + settings = openmc.Settings() + settings.run_mode = 'fixed source' + settings.batches = 5 + settings.particles = 50 + settings.source = source + settings.shared_secondary_bank = shared_secondary + + model = openmc.Model(geometry=geometry, materials=materials, settings=settings) + + # Mesh tally: 1 cm voxels, flux only + mesh = openmc.RegularMesh() + mesh.dimension = (20, 20, 1) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (160.0, 160.0, 160.0) + + mesh_filter = openmc.MeshFilter(mesh) + flux_tally = openmc.Tally(name='flux') + flux_tally.filters = [mesh_filter] + flux_tally.scores = ['flux'] + tallies = openmc.Tallies([flux_tally]) + model.tallies = tallies + + parent_dir = Path(__file__).parent + lower_ww_bounds = np.loadtxt(parent_dir / 'ww_n.txt') + + weight_windows = openmc.WeightWindows(mesh, + lower_ww_bounds, + upper_bound_ratio=5.0, + particle_type='neutron') + + model.settings.weight_windows = weight_windows + model.settings.weight_window_checkpoints = {'surface': True, + 'collision': True} + model.settings.survival_biasing = True + + return model + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weight_windows_with_survival_biasing(shared_secondary, subdir): + with change_directory(subdir): + model = build_model(shared_secondary) + harness = HashedPyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows/survival_biasing/ww_n.txt b/tests/regression_tests/weightwindows/survival_biasing/ww_n.txt new file mode 100644 index 000000000..aea1ff132 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/ww_n.txt @@ -0,0 +1,400 @@ +4.613596156895709566e-01 +4.611178493390161726e-01 +4.629495791306331709e-01 +4.577259875047255400e-01 +4.739762983490318216e-01 +4.756019909855827565e-01 +4.765919947193469342e-01 +4.693487369606823001e-01 +4.538724931023336850e-01 +4.525844475322012284e-01 +4.482467985982804271e-01 +4.875748950139486837e-01 +5.000000000000000000e-01 +4.770639768898417565e-01 +4.864198865928451299e-01 +4.307755159245372223e-01 +4.363132402495602524e-01 +4.592628723821899905e-01 +4.574378998400603913e-01 +4.821442782978812014e-01 +2.874485390202602964e-01 +2.862486256084202374e-01 +2.857734367546050924e-01 +2.814949214714950187e-01 +2.841906153704676363e-01 +2.772838914695699430e-01 +2.888666694425221504e-01 +2.718086213949012508e-01 +2.824491421084295850e-01 +2.683910246648425479e-01 +2.813276470183359024e-01 +2.908586705316247856e-01 +2.852101551297823723e-01 +2.831343685949596622e-01 +2.816173231471241767e-01 +2.652713784080849013e-01 +2.639077825530566912e-01 +2.635824148883887941e-01 +2.871497117992772297e-01 +2.770413223308350603e-01 +1.325601395049460507e-01 +1.399987062262113557e-01 +1.336562319033694490e-01 +1.360439796276224633e-01 +1.275750990150788799e-01 +1.276080257253562333e-01 +1.258637573398161125e-01 +1.283345557042348262e-01 +1.275341706351565962e-01 +1.299163391879708251e-01 +1.344986079076864738e-01 +1.421206065839227817e-01 +1.362230717232164323e-01 +1.317062474422197593e-01 +1.315418500611990060e-01 +1.275385795753565255e-01 +1.336873796274241355e-01 +1.310428099679947778e-01 +1.363507911051337063e-01 +1.414907403566230681e-01 +5.276520960980729535e-02 +5.508479408959755796e-02 +5.034742166227103299e-02 +5.479288147229536415e-02 +5.169202930197330098e-02 +5.251965811713552035e-02 +5.818862375955657223e-02 +5.514674355993834376e-02 +5.502750114189370462e-02 +5.442263709947767203e-02 +5.610327819053343573e-02 +6.068108009637272066e-02 +5.830251988433994559e-02 +5.254535127665489747e-02 +5.479320024832795566e-02 +5.470396852881576760e-02 +5.213531603720762686e-02 +5.537981218604190459e-02 +5.010315954654347148e-02 +5.546042815834047873e-02 +2.095844083268563751e-02 +1.828124195825499287e-02 +1.752585181291326649e-02 +1.980237689875552487e-02 +1.919167874787235106e-02 +2.249896064495163217e-02 +2.047631372074476194e-02 +2.066740352947033302e-02 +2.305114640508086968e-02 +1.969589522347148583e-02 +2.627432033419696208e-02 +1.946797046878947710e-02 +2.312473491515262478e-02 +2.111175315831910482e-02 +1.651770169115657563e-02 +2.049817309420563088e-02 +2.062886046974383297e-02 +1.925504256194107353e-02 +1.796327898957107358e-02 +1.680064938226999774e-02 +6.317250035749876098e-03 +5.557776487236155451e-03 +5.096725451851077254e-03 +7.335174557912849461e-03 +7.593035964707848043e-03 +1.063372459325150933e-02 +7.143193244018263000e-03 +7.891508723137145853e-03 +8.599303158607197670e-03 +5.353204961887517849e-03 +6.884071908429303423e-03 +6.371024279463989581e-03 +7.401527839638795750e-03 +6.745171395560964518e-03 +6.757591781856257113e-03 +5.665597412473520264e-03 +8.268384844678653561e-03 +7.252095690246871881e-03 +5.899817629797160512e-03 +4.096804603054233183e-03 +2.627037175682505905e-03 +1.526543222629339657e-03 +2.629764095153140288e-03 +2.067392946066426082e-03 +1.748817631410727689e-03 +4.242071054914632773e-03 +2.236408302251527251e-03 +1.643551705407358000e-03 +2.508261119416180414e-03 +1.507469829384015672e-03 +3.309939073873545759e-03 +2.851042526619168658e-03 +2.527635652743991969e-03 +3.204270539718037138e-03 +2.542535075090864363e-03 +7.213846765465147101e-04 +2.859922101393437468e-03 +1.802659348814099642e-03 +2.139313566616799812e-03 +1.916110839857802610e-03 +3.003234359243728432e-04 +2.429877235263696591e-04 +3.373242439202614419e-04 +9.529474085926143009e-05 +9.678267909681988429e-05 +4.560311220266518293e-04 +3.953123781408473961e-04 +6.501416781353277531e-04 +5.185303112316960250e-04 +1.598680898180058108e-04 +5.337454397674922446e-04 +1.218442177811548824e-03 +8.509612315162482432e-04 +1.030810724226319166e-03 +6.383278085839443235e-04 +6.848024591311008757e-05 +1.305975712998271444e-04 +3.215836458542421176e-04 +1.567757503308348204e-04 +5.289600253155306818e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +8.872047880811405188e-05 +1.386380399465574921e-04 +2.940454156714618719e-04 +1.821333457455476831e-04 +2.759210144794204192e-04 +2.279421641064226044e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +9.148401760003310179e-06 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +7.815977984795461318e-06 +1.372780536538731832e-04 +3.597432472224371168e-04 +8.609308814924013556e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index cfb651338..c5b6bd889 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -1,22 +1,23 @@ +from pathlib import Path + import pytest import numpy as np import openmc from openmc.stats import Discrete, Point +from openmc.utility_funcs import change_directory from tests.testing_harness import HashedPyAPITestHarness - -@pytest.fixture -def model(): +def build_model(shared_secondary): + openmc.reset_auto_ids() model = openmc.Model() # materials (M4 steel alloy) m4 = openmc.Material() m4.set_density('g/cc', 2.3) m4.add_nuclide('H1', 0.168018676) - m4.add_nuclide("H2", 1.93244e-05) m4.add_nuclide("O16", 0.561814465) m4.add_nuclide("O17", 0.00021401) m4.add_nuclide("Na23", 0.021365) @@ -26,14 +27,9 @@ def model(): m4.add_nuclide("Si30", 0.006273944) m4.add_nuclide("Ca40", 0.018026179) m4.add_nuclide("Ca42", 0.00012031) - m4.add_nuclide("Ca43", 2.51033e-05) m4.add_nuclide("Ca44", 0.000387892) - m4.add_nuclide("Ca46", 7.438e-07) - m4.add_nuclide("Ca48", 3.47727e-05) m4.add_nuclide("Fe54", 0.000248179) m4.add_nuclide("Fe56", 0.003895875) - m4.add_nuclide("Fe57", 8.99727e-05) - m4.add_nuclide("Fe58", 1.19737e-05) s0 = openmc.Sphere(r=240) s1 = openmc.Sphere(r=250, boundary_type='vacuum') @@ -46,10 +42,13 @@ def model(): # settings settings = model.settings settings.run_mode = 'fixed source' - settings.particles = 200 + settings.particles = 500 settings.batches = 2 settings.max_history_splits = 200 settings.photon_transport = True + settings.shared_secondary_bank = shared_secondary + settings.weight_window_checkpoints = {'surface': True, + 'collision': True} space = Point((0.001, 0.001, 0.001)) energy = Discrete([14E6], [1.0]) @@ -76,10 +75,10 @@ def model(): # weight windows - # load pre-generated weight windows - # (created using the same tally as above) - ww_n_lower_bnds = np.loadtxt('ww_n.txt') - ww_p_lower_bnds = np.loadtxt('ww_p.txt') + # load pre-generated weight windows from parent directory + parent_dir = Path(__file__).parent + ww_n_lower_bnds = np.loadtxt(parent_dir / 'ww_n.txt') + ww_p_lower_bnds = np.loadtxt(parent_dir / 'ww_p.txt') # create a mesh matching the one used # to generate the weight windows @@ -93,6 +92,7 @@ def model(): None, 10.0, e_bnds, + 'neutron', max_lower_bound_ratio=1.5) ww_p = openmc.WeightWindows(ww_mesh, @@ -100,6 +100,7 @@ def model(): None, 10.0, e_bnds, + 'photon', max_lower_bound_ratio=1.5) model.settings.weight_windows = [ww_n, ww_p] @@ -107,9 +108,70 @@ def model(): return model -def test_weightwindows(model): - test = HashedPyAPITestHarness('statepoint.2.h5', model) - test.main() +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weightwindows(shared_secondary, subdir): + with change_directory(subdir): + model = build_model(shared_secondary) + test = HashedPyAPITestHarness('statepoint.2.h5', model) + test.main() + + +def test_zero_bound_windows_play_no_game(tmp_path): + # A weight window lower bound of zero means no weight window information + # exists there (MCNP wwinp files use zero to turn the game off in a cell), + # so transport must proceed as if weight windows were disabled. Previously, + # zero-bound windows demanded a split at every checkpoint (weight/0 -> + # max_split), multiplying the particle population until terminated by the + # split or weight cutoff limits. + model = build_model(False) + for ww in model.settings.weight_windows: + ww.lower_ww_bounds = np.zeros_like(ww.lower_ww_bounds) + ww.upper_ww_bounds = np.zeros_like(ww.upper_ww_bounds) + sp_zero = model.run(cwd=tmp_path / 'zero_windows') + + model.settings.weight_windows_on = False + sp_off = model.run(cwd=tmp_path / 'windows_off') + + with openmc.StatePoint(sp_zero) as sp: + flux_zero = list(sp.tallies.values())[0].mean + with openmc.StatePoint(sp_off) as sp: + flux_off = list(sp.tallies.values())[0].mean + + np.testing.assert_allclose(flux_zero, flux_off, rtol=1e-12) + + +def test_zero_and_negative_bounds_equivalent(tmp_path): + # Zero and negative lower bounds both mean that no weight window + # information exists in a cell (generators mark such cells with -1, and + # MCNP wwinp files use zero), so they must produce identical transport. + # Unlike the all-zero case above, here particles are born under valid + # windows and encounter the no-information region in flight; previously a + # zero lower bound in that situation demanded a split at every checkpoint + # in the cell (weight/0 -> max_split), multiplying the particle population, + # while -1 played no game. + def run_with(bound_value, subdir): + model = build_model(False) + for ww in model.settings.weight_windows: + lb = np.array(ww.lower_ww_bounds, copy=True) + ub = np.array(ww.upper_ww_bounds, copy=True) + lb[3:, :, :, :] = bound_value + ub[3:, :, :, :] = bound_value + ww.lower_ww_bounds = lb + ww.upper_ww_bounds = ub + return model.run(cwd=tmp_path / subdir) + + sp_zero = run_with(0.0, 'zero_region') + sp_negative = run_with(-1.0, 'negative_region') + + with openmc.StatePoint(sp_zero) as sp: + flux_zero = list(sp.tallies.values())[0].mean + with openmc.StatePoint(sp_negative) as sp: + flux_negative = list(sp.tallies.values())[0].mean + + np.testing.assert_allclose(flux_zero, flux_negative, rtol=1e-12) def test_wwinp_cylindrical(): diff --git a/tests/regression_tests/weightwindows/ww_n.txt b/tests/regression_tests/weightwindows/ww_n.txt index dbb49537b..36d7673c6 100644 --- a/tests/regression_tests/weightwindows/ww_n.txt +++ b/tests/regression_tests/weightwindows/ww_n.txt @@ -10,43 +10,30 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -7.695031155172815952e-14 -1.000000000000000000e+00 -7.476545089278482294e-06 -1.000000000000000000e+00 -2.161215042522170282e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.136040178434497494e-18 -1.000000000000000000e+00 -1.345534130616238158e-05 -1.000000000000000000e+00 -3.353295403842037074e-05 -1.000000000000000000e+00 -3.016075845432365699e-07 -1.000000000000000000e+00 -1.026243155073153453e-13 -1.000000000000000000e+00 -1.685921961405802360e-19 -1.000000000000000000e+00 -3.853097455417877125e-06 -1.000000000000000000e+00 -4.354946981329799215e-05 -1.000000000000000000e+00 -7.002861620919231721e-08 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.620506488302619488e-11 -1.000000000000000000e+00 -1.704166922479738429e-09 -1.000000000000000000e+00 -1.874782466747863741e-14 -1.000000000000000000e+00 -1.000000000000000000e+00 +5.465752192773127422e-05 +5.016807145936662541e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -60,301 +47,329 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -5.619835883512352867e-14 -1.000000000000000000e+00 -1.951356547084204088e-15 -1.000000000000000000e+00 -8.481761456712739755e-10 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.450826207325665860e-13 -1.000000000000000000e+00 -4.109066205029878035e-05 -1.000000000000000000e+00 -2.057318145024078612e-04 -1.000000000000000000e+00 -2.124999182004654892e-05 -1.000000000000000000e+00 -5.537915413446233643e-08 -1.000000000000000000e+00 -1.555045595917848555e-06 -1.000000000000000000e+00 -7.213722022489493227e-04 -1.000000000000000000e+00 -7.673145137236566868e-03 -1.000000000000000000e+00 -8.175033526488423340e-04 -1.000000000000000000e+00 -1.008394964750946906e-06 -1.000000000000000000e+00 -6.848642863465584904e-07 -1.000000000000000000e+00 -1.073708183250235556e-03 -1.000000000000000000e+00 -7.197662162700776967e-03 -1.000000000000000000e+00 -7.151459162818185784e-04 -1.000000000000000000e+00 -2.253382683081525085e-06 -1.000000000000000000e+00 -1.318156235244509166e-10 -1.000000000000000000e+00 -1.938470818929409920e-05 -1.000000000000000000e+00 -4.602961711512755646e-04 -1.000000000000000000e+00 -2.562906833044621533e-05 -1.000000000000000000e+00 -5.504813693036932910e-12 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -3.316482534950376351e-16 -1.000000000000000000e+00 -3.937160538176268057e-07 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.517291205731239772e-14 -1.000000000000000000e+00 -6.315154082213375425e-07 -1.000000000000000000e+00 -3.855884234344844889e-06 -1.000000000000000000e+00 -1.777013641125591191e-05 -1.000000000000000000e+00 -1.642149199375171522e-11 -1.000000000000000000e+00 -5.976538141107809983e-06 -1.000000000000000000e+00 -1.959411999677113103e-03 -1.000000000000000000e+00 -1.333061985337931354e-02 -1.000000000000000000e+00 -1.795061316935990395e-03 -1.000000000000000000e+00 -2.430637569372220458e-06 -1.000000000000000000e+00 -6.922231352729155390e-05 -1.000000000000000000e+00 -8.657573566388353237e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.356487100743359431e-02 -1.000000000000000000e+00 -1.641903115049591277e-04 -1.000000000000000000e+00 -9.031085518264690009e-05 -1.000000000000000000e+00 -8.488729673818780352e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.288806458368344621e-02 +1.662027112506702498e-05 +1.072323185129836366e-05 +1.298973206651331117e-04 -1.000000000000000000e+00 -5.694959706678497628e-05 -1.000000000000000000e+00 -8.037131813528500897e-07 -1.000000000000000000e+00 -1.663712654332187308e-03 -1.000000000000000000e+00 -1.664109232689093068e-02 -1.000000000000000000e+00 -1.832759343760799985e-03 -1.000000000000000000e+00 -6.542700644645626997e-07 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -6.655926357459274907e-08 -1.000000000000000000e+00 -5.783974359937856763e-06 +4.045301818607421548e-05 +2.436300468111906627e-03 +8.030100296698905113e-04 +1.467083972914635416e-02 +5.588863190166347417e-03 +2.482827339043196246e-03 +9.134587866163143164e-04 +1.127637299558987091e-04 -1.000000000000000000e+00 -2.679340942769232109e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.104060901286513389e-05 -1.000000000000000000e+00 -2.928209794781622450e-05 +1.096572213298871923e-03 +1.035684113269689892e-03 +1.484649108252352433e-02 +5.163345217476070746e-03 +3.034165905819096072e-03 +7.973472495507652599e-04 +1.596955110781238946e-04 -1.000000000000000000e+00 -1.284759166582201940e-05 -1.000000000000000000e+00 -1.009290508315880392e-11 -1.000000000000000000e+00 -1.322801376552501482e-05 -1.000000000000000000e+00 -7.136750029575815620e-03 -1.000000000000000000e+00 -6.539005235506369085e-02 -1.000000000000000000e+00 -6.480331412016533503e-03 -1.000000000000000000e+00 -2.601178739391680563e-06 -1.000000000000000000e+00 -8.887234042637683843e-05 -1.000000000000000000e+00 +5.146579891055349870e-05 +3.371977841720992210e-05 +-1.000000000000000000e+00 +6.170015233787291603e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +6.278777700923334273e-04 +-1.000000000000000000e+00 +2.882628152998065532e-03 +1.425775692742982772e-03 +9.344129479768359435e-04 +1.878729106973870018e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +8.758251780046590789e-04 +4.601329011012189428e-05 +4.404124277352226835e-02 +1.738622047618722244e-02 +4.999999999999999445e-01 5.000000000000000000e-01 +4.299740304329489199e-02 +1.708185807868446704e-02 +2.236100157024886888e-04 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.877856021170283163e-01 -1.000000000000000000e+00 -1.769985835774446302e-04 -1.000000000000000000e+00 -1.670700632870335029e-04 +4.449401731904285297e-04 +9.538410811938702620e-05 +4.862206884590390688e-02 +1.560907112400923384e-02 +4.897211700090107755e-01 +4.825075099388732580e-01 +4.870927636605937305e-02 +1.930090325480974742e-02 +9.960576598335083693e-06 -1.000000000000000000e+00 -4.899999304038166192e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.857384221387814338e-01 -1.000000000000000000e+00 -2.153456869915780514e-04 -1.000000000000000000e+00 -7.842896894949250204e-06 -1.000000000000000000e+00 -6.379065450759913505e-03 +2.920495077727233452e-04 +6.979537549963373846e-05 +1.011527843820496462e-03 +6.640103276501793748e-04 +6.339910999436736800e-05 +1.898984300674969044e-05 -1.000000000000000000e+00 -6.550426960512012453e-02 -1.000000000000000000e+00 -6.639379668946672301e-03 -1.000000000000000000e+00 -7.754700849337902260e-06 -1.000000000000000000e+00 -2.720743316579670241e-11 -1.000000000000000000e+00 -5.985108617124988932e-06 -1.000000000000000000e+00 -3.634644995026691954e-05 -1.000000000000000000e+00 -3.932389299316284718e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.271128669411294917e-07 -1.000000000000000000e+00 -8.846493399850662704e-06 -1.000000000000000000e+00 -2.499931357628383163e-07 -1.000000000000000000e+00 -8.533030345699353466e-17 -1.000000000000000000e+00 -1.669925427773410862e-06 -1.000000000000000000e+00 -1.754138009689378778e-03 -1.000000000000000000e+00 -1.514897842827161306e-02 -1.000000000000000000e+00 -1.627979455242757438e-03 -1.000000000000000000e+00 -2.380955631371225994e-06 -1.000000000000000000e+00 -2.164891804046736287e-05 -1.000000000000000000e+00 -8.339317973815890683e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.285899874014539257e-02 -1.000000000000000000e+00 -2.762955748645507000e-05 -1.000000000000000000e+00 -5.116174051620571477e-05 -1.000000000000000000e+00 -8.095280568305474045e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.636569925236790846e-02 -1.000000000000000000e+00 -3.452537179033895228e-05 -1.000000000000000000e+00 -3.598103176641614321e-06 -1.000000000000000000e+00 -1.570829313580840913e-03 -1.000000000000000000e+00 -1.621821782442112170e-02 -1.000000000000000000e+00 -1.544782399547004175e-03 +3.381665181434415378e-05 +-1.000000000000000000e+00 +1.894132653585093196e-04 +5.909807053955946611e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.709738801641897098e-05 +2.389900091734746025e-03 +5.027758935317528299e-04 +1.127106059276566409e-02 +3.930588100414563434e-03 +2.053907925255511312e-03 +2.972747357542354098e-04 +9.563042989132649486e-08 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +1.992978141244964075e-05 +1.153005443211333218e-03 +7.473731339616587841e-04 +1.328487445181072285e-02 +3.510432129522240985e-03 +3.131016680862970091e-03 +9.022030322806940360e-04 +5.383303062315367556e-05 +1.017391376549009493e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.233284599138542407e-05 +-1.000000000000000000e+00 +8.429411802845684617e-04 +9.554899988419712775e-05 +7.442176086066385897e-05 +9.854115835957219834e-07 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 -1.000000000000000000e+00 -7.989579285134733983e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.594951085377587857e-06 -1.000000000000000000e+00 -4.323138781337962887e-05 -1.000000000000000000e+00 -6.948292975998465777e-07 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.446945600849361415e-09 -1.000000000000000000e+00 -9.041149893307805599e-07 -1.000000000000000000e+00 -2.206099258062212484e-11 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.681305446488883933e-11 -1.000000000000000000e+00 -7.091263906557290927e-05 -1.000000000000000000e+00 -2.746424406288768276e-04 -1.000000000000000000e+00 -1.285845570921339974e-05 -1.000000000000000000e+00 -2.029521362171670568e-09 -1.000000000000000000e+00 -1.291182719850037242e-08 -1.000000000000000000e+00 -9.127050763987511325e-04 -1.000000000000000000e+00 -7.385384405891665810e-03 -1.000000000000000000e+00 -1.001943240150808675e-03 -1.000000000000000000e+00 -1.080711295768550941e-05 -1.000000000000000000e+00 -8.020767115181573972e-07 -1.000000000000000000e+00 -9.252532821720597109e-04 -1.000000000000000000e+00 -7.188410694198127913e-03 -1.000000000000000000e+00 -7.245451610090619275e-04 -1.000000000000000000e+00 -2.515735579020177281e-07 -1.000000000000000000e+00 -1.482521919913335348e-12 -1.000000000000000000e+00 -4.756872959630257218e-05 -1.000000000000000000e+00 -2.668579837809080925e-04 -1.000000000000000000e+00 -2.822627694453269613e-05 -1.000000000000000000e+00 -5.182269672663266089e-10 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.936268747135548187e-14 -1.000000000000000000e+00 -2.544728124173036570e-07 -1.000000000000000000e+00 -3.898416671385980035e-08 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -370,41 +385,27 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.639268952045578987e-10 -1.000000000000000000e+00 -3.705151822008388643e-06 -1.000000000000000000e+00 -1.133362760482367266e-09 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.229674726063540458e-06 -1.000000000000000000e+00 -7.962899789920809096e-06 -1.000000000000000000e+00 -8.211982683649991481e-09 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.563965686625045276e-06 -1.000000000000000000e+00 -2.248761782893832567e-05 -1.000000000000000000e+00 -3.279904588337207471e-06 -1.000000000000000000e+00 -4.076762579823379031e-17 -1.000000000000000000e+00 -1.547676923757129466e-14 -1.000000000000000000e+00 -1.030567269874565733e-11 -1.000000000000000000e+00 -1.957092814065688094e-05 -1.000000000000000000e+00 -3.852324755037881526e-12 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -412,7 +413,6 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.769723317139941641e-13 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 diff --git a/tests/regression_tests/weightwindows/ww_p.txt b/tests/regression_tests/weightwindows/ww_p.txt index 09ff01ebd..da69b9e13 100644 --- a/tests/regression_tests/weightwindows/ww_p.txt +++ b/tests/regression_tests/weightwindows/ww_p.txt @@ -1,205 +1,241 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -5.447493368067179064e-10 -1.000000000000000000e+00 -4.040462614734400249e-13 -1.000000000000000000e+00 -9.382800796426366364e-17 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -5.752120832330885919e-11 -1.000000000000000000e+00 -3.520674728719278012e-05 -1.000000000000000000e+00 -8.760419353035269667e-05 -1.000000000000000000e+00 -3.136829920007008993e-05 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.233410073487771971e-06 -1.000000000000000000e+00 -2.551963224311764450e-04 -1.000000000000000000e+00 -4.777967757842694024e-04 -1.000000000000000000e+00 -3.598354799974066488e-04 -1.000000000000000000e+00 -4.625859206051786618e-07 -1.000000000000000000e+00 -4.587143797469485258e-08 -1.000000000000000000e+00 -1.291843367759397518e-04 -1.000000000000000000e+00 -4.695895684632649786e-04 -1.000000000000000000e+00 -8.862760180332873163e-05 -1.000000000000000000e+00 -3.279091474401153391e-06 -1.000000000000000000e+00 -1.151457911727229161e-10 -1.000000000000000000e+00 -2.295071617064146262e-05 -1.000000000000000000e+00 -6.922962992428139458e-05 -1.000000000000000000e+00 -2.160655073217069276e-05 -1.000000000000000000e+00 -2.617878055106949035e-16 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.722191123879781983e-08 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.123498061876322157e-13 -1.000000000000000000e+00 -1.462884795971722476e-05 -1.000000000000000000e+00 -2.777791741571173956e-05 -1.000000000000000000e+00 -9.533806586698390583e-06 -1.000000000000000000e+00 -1.078687446476880940e-14 -1.000000000000000000e+00 -3.057105085570708401e-06 -1.000000000000000000e+00 -8.231482263927564300e-04 -1.000000000000000000e+00 -3.881102561422505696e-03 -1.000000000000000000e+00 -1.229730308180539307e-03 -1.000000000000000000e+00 -4.684235088236766580e-05 -1.000000000000000000e+00 -1.675799759004882713e-04 -1.000000000000000000e+00 -1.172471437684818700e-02 +2.260493623875524987e-05 -1.000000000000000000e+00 -6.827568359008943932e-02 -1.000000000000000000e+00 -1.144038101286408600e-02 -1.000000000000000000e+00 -3.782322077390032930e-04 -1.000000000000000000e+00 -5.098747893974247782e-05 -1.000000000000000000e+00 -1.208261392046878005e-02 -1.000000000000000000e+00 -6.428782790571178907e-02 -1.000000000000000000e+00 -1.129470856680416663e-02 -1.000000000000000000e+00 -8.168081568291806404e-05 -1.000000000000000000e+00 -2.347504827875979927e-05 -1.000000000000000000e+00 -8.787411098754914583e-04 -1.000000000000000000e+00 -5.151329213225949548e-03 -1.000000000000000000e+00 -1.102802039050768210e-03 -1.000000000000000000e+00 -1.088201101868485208e-06 -1.000000000000000000e+00 -1.058825762372267240e-07 -1.000000000000000000e+00 -2.704481717610955638e-05 -1.000000000000000000e+00 -2.621123924679610439e-05 -1.000000000000000000e+00 -2.070764852974689408e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.380657778513517658e-06 -1.000000000000000000e+00 -1.007700972669126494e-04 -1.000000000000000000e+00 -4.313097771132341687e-04 -1.000000000000000000e+00 -1.335621430311087867e-04 -1.000000000000000000e+00 -3.792209399652395984e-06 -1.000000000000000000e+00 -2.205797076162476780e-04 -1.000000000000000000e+00 -1.884234453311008084e-02 -1.000000000000000000e+00 -1.151012001392641704e-01 -1.000000000000000000e+00 -1.825795827020864834e-02 -1.000000000000000000e+00 -2.084830922016203648e-04 -1.000000000000000000e+00 -1.333255443748032664e-03 -1.000000000000000000e+00 -4.931708164307664344e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.899952354672477139e-01 -1.000000000000000000e+00 -1.495703328116801227e-03 -1.000000000000000000e+00 -1.550036616828060930e-03 -1.000000000000000000e+00 -4.897735047310072254e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +5.419696140423850144e-05 +-1.000000000000000000e+00 +1.160370359598607949e-04 +-1.000000000000000000e+00 +9.207268175745281452e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +1.328132850928838340e-04 +-1.000000000000000000e+00 +2.639831922911523281e-03 +-1.000000000000000000e+00 +1.496517973317253170e-02 +-1.000000000000000000e+00 +3.246064903858183071e-03 +-1.000000000000000000e+00 +9.650194311662423557e-06 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.760836648289724440e-03 +-1.000000000000000000e+00 +1.515736479588492176e-02 +-1.000000000000000000e+00 +3.249926021191793316e-03 +-1.000000000000000000e+00 +4.506530272543181565e-06 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +9.774868433298411941e-05 +-1.000000000000000000e+00 +1.491991536637756388e-04 +-1.000000000000000000e+00 +1.826809075600190318e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +9.753424087171070478e-06 +-1.000000000000000000e+00 +1.480642286704643632e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.044569109607474437e-05 +-1.000000000000000000e+00 +7.918090215756442467e-04 +-1.000000000000000000e+00 +2.822592083222468049e-03 +-1.000000000000000000e+00 +7.066033561638982710e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +6.267128082339882807e-04 +-1.000000000000000000e+00 +4.039871468258456749e-02 +-1.000000000000000000e+00 5.000000000000000000e-01 -1.000000000000000000e+00 -1.107679609210218599e-03 +3.905493028065908090e-02 -1.000000000000000000e+00 -2.792896409558744781e-04 +2.940337397083507467e-04 -1.000000000000000000e+00 -1.721806295736377085e-02 -1.000000000000000000e+00 -1.189125124490693353e-01 -1.000000000000000000e+00 -1.904070707116530328e-02 -1.000000000000000000e+00 -1.732952862815292309e-04 -1.000000000000000000e+00 -4.670673837764873805e-08 +4.209808495056741907e-04 -1.000000000000000000e+00 -1.263753469561097525e-04 +4.019097808772300467e-02 -1.000000000000000000e+00 -2.705205429838525473e-04 +4.937699395340238717e-01 -1.000000000000000000e+00 -1.497958445994315019e-04 +4.335567738779479152e-02 -1.000000000000000000e+00 -3.247830168002185438e-08 +2.772532363874423726e-04 -1.000000000000000000e+00 -7.239087311622818554e-08 -1.000000000000000000e+00 -4.669378458601891341e-04 -1.000000000000000000e+00 -1.537590300149361015e-03 -1.000000000000000000e+00 -2.368681880710174145e-04 -1.000000000000000000e+00 -8.616027294555461632e-07 +1.553693492042343928e-05 -1.000000000000000000e+00 -2.362355168421952122e-04 +6.126404916879650987e-04 -1.000000000000000000e+00 -5.708498338352203244e-02 +2.197964829133136882e-03 -1.000000000000000000e+00 -4.237271368372501623e-01 +4.353654810741931802e-04 -1.000000000000000000e+00 -5.490281661912204542e-02 +8.786172732576294898e-05 -1.000000000000000000e+00 -3.000232638838324392e-04 -1.000000000000000000e+00 -2.329540159600531311e-03 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -207,9 +243,7 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.784732930735042255e-03 -1.000000000000000000e+00 -3.432919201293736875e-03 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -217,204 +251,170 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.672923773363389179e-03 -1.000000000000000000e+00 -3.182108781955477806e-04 -1.000000000000000000e+00 -5.639071613735578692e-02 -1.000000000000000000e+00 -4.346984808472855177e-01 -1.000000000000000000e+00 -5.500108683629956891e-02 +3.709684785598048377e-05 -1.000000000000000000e+00 -2.417599813822804812e-04 +1.761084940564007186e-05 -1.000000000000000000e+00 -2.774067049392004042e-07 -1.000000000000000000e+00 -3.257646134837451210e-04 -1.000000000000000000e+00 -1.264833388398999524e-03 -1.000000000000000000e+00 -2.981891705231208794e-04 -1.000000000000000000e+00 -4.986698316296506713e-06 -1.000000000000000000e+00 -3.419181071650815928e-06 -1.000000000000000000e+00 -1.081445276486055809e-04 -1.000000000000000000e+00 -6.934466049804711161e-04 -1.000000000000000000e+00 -2.510674100806631662e-04 -1.000000000000000000e+00 -5.515522196186722918e-06 -1.000000000000000000e+00 -1.424205319552986396e-04 +9.763572147337742902e-05 -1.000000000000000000e+00 -1.854111290797000322e-02 +3.897177301239375825e-04 -1.000000000000000000e+00 -1.236406932635011752e-01 -1.000000000000000000e+00 -1.760297107458398680e-02 -1.000000000000000000e+00 -2.130582646223859260e-04 -1.000000000000000000e+00 -1.501650004537408200e-03 -1.000000000000000000e+00 -4.903495510314233030e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.952432605379828989e-01 -1.000000000000000000e+00 -1.769327994909421784e-03 -1.000000000000000000e+00 -9.304497896205120647e-04 -1.000000000000000000e+00 -4.873897581604286766e-01 +2.806654735003014187e-03 -1.000000000000000000e+00 +1.172349170429040112e-02 -1.000000000000000000e+00 +2.538057450107360485e-03 -1.000000000000000000e+00 -4.981497862127627907e-01 +1.350620341932643788e-04 -1.000000000000000000e+00 -1.127334097206107765e-03 -1.000000000000000000e+00 -1.412615360326509664e-04 -1.000000000000000000e+00 -1.729816946709781048e-02 -1.000000000000000000e+00 -1.181916566651922268e-01 -1.000000000000000000e+00 -1.780262484403568810e-02 +5.340221214300680991e-05 -1.000000000000000000e+00 -1.319677450153934042e-04 +2.508441610397997481e-03 -1.000000000000000000e+00 -2.210707325798542070e-07 +1.311141947320496742e-02 -1.000000000000000000e+00 -1.728787338791035720e-04 +3.157721455701705589e-03 -1.000000000000000000e+00 -5.197081842709918220e-04 +3.547290864255936898e-05 -1.000000000000000000e+00 -9.256616392487857544e-05 -1.000000000000000000e+00 -1.032520710502734699e-08 -1.000000000000000000e+00 -2.027381220364608057e-16 -1.000000000000000000e+00 -1.225736024671247218e-05 -1.000000000000000000e+00 -3.856169633708643398e-05 -1.000000000000000000e+00 -1.806455099029475317e-05 -1.000000000000000000e+00 -7.691527781829036254e-17 +1.484173077431725238e-04 -1.000000000000000000e+00 -1.298062961617926916e-05 +3.209711829219673144e-04 -1.000000000000000000e+00 -1.178616843705955963e-03 +1.127325957309237157e-04 -1.000000000000000000e+00 -4.441328419270368887e-03 -1.000000000000000000e+00 -8.919466185572301909e-04 -1.000000000000000000e+00 -5.272021975060514019e-06 -1.000000000000000000e+00 -1.607976851421854571e-04 -1.000000000000000000e+00 -1.112842419638761758e-02 -1.000000000000000000e+00 -6.433819434431045647e-02 -1.000000000000000000e+00 -1.095563748531924904e-02 -1.000000000000000000e+00 -8.370896020975688591e-05 -1.000000000000000000e+00 -2.223447159544331337e-04 -1.000000000000000000e+00 -1.102706140784315975e-02 -1.000000000000000000e+00 -6.684484191345574366e-02 -1.000000000000000000e+00 -1.111302464121833623e-02 -1.000000000000000000e+00 -1.255887947410807431e-04 -1.000000000000000000e+00 -1.004439072885827748e-05 -1.000000000000000000e+00 -8.081009012754719698e-04 -1.000000000000000000e+00 -5.384625561469683769e-03 -1.000000000000000000e+00 -1.146714816952060971e-03 -1.000000000000000000e+00 -3.574060778841494232e-06 -1.000000000000000000e+00 -2.251232708364186507e-10 -1.000000000000000000e+00 -6.717625881826167172e-05 -1.000000000000000000e+00 -6.949587369646497293e-05 -1.000000000000000000e+00 -2.909334597507522605e-05 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.582011716859218622e-15 -1.000000000000000000e+00 -8.304845400451923449e-08 -1.000000000000000000e+00 -4.575723837205823402e-11 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.311680713303003522e-15 -1.000000000000000000e+00 -2.686689485248190559e-05 -1.000000000000000000e+00 -4.316605279989737501e-05 -1.000000000000000000e+00 -6.401055165502384025e-06 -1.000000000000000000e+00 -5.726152701849347207e-16 -1.000000000000000000e+00 -8.541606727879512040e-07 -1.000000000000000000e+00 -2.029396545382813118e-04 -1.000000000000000000e+00 -4.299195570308034968e-04 -1.000000000000000000e+00 -8.544159282151438236e-05 -1.000000000000000000e+00 -1.099532245627738225e-06 -1.000000000000000000e+00 -1.289814511028014912e-06 -1.000000000000000000e+00 -1.104204785730725317e-04 -1.000000000000000000e+00 -6.870228272606287295e-04 -1.000000000000000000e+00 -1.399807456616496116e-04 -1.000000000000000000e+00 -4.279460329782799377e-09 -1.000000000000000000e+00 -1.290717169529484602e-13 -1.000000000000000000e+00 -1.925724800911436302e-05 -1.000000000000000000e+00 -9.332371598523186474e-05 -1.000000000000000000e+00 -1.310633551813380206e-05 -1.000000000000000000e+00 -6.574706543646946099e-16 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -3.827053602579980514e-15 -1.000000000000000000e+00 -2.101790478097516960e-09 -1.000000000000000000e+00 -9.904481358675477728e-13 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +4.521352809838806062e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 diff --git a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat index 5fa6505dd..6bdabfbee 100644 --- a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat @@ -222,11 +222,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true naive diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/__init__.py b/tests/regression_tests/weightwindows_fw_cadis_local/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat new file mode 100644 index 000000000..ffdd977ec --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat @@ -0,0 +1,273 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + + + + + + fixed source + 500 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 2 + neutron + 10 + 1 + true + fw_cadis + 1 2 + + + + 7 7 7 + 0.0 0.0 0.0 + 35.0 35.0 35.0 + + + 800.0 + 100.0 + + + + 0.0 0.0 0.0 35.0 35.0 35.0 + + + + true + + + + + + naive + + + 14 14 14 + 0.0 0.0 0.0 + 35.0 35.0 35.0 + + + + + 6 + + + 7 + + + 1 + flux + tracklength + + + 2 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat new file mode 100644 index 000000000..5991fc621 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat @@ -0,0 +1,696 @@ +RegularMesh + ID = 2 + Name = + Dimensions = 3 + Voxels = [7 7 7] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(35.0), np.float64(35.0), np.float64(35.0)] + Width = [5. 5. 5.] +Lower Bounds +1.52e-01 +1.53e-01 +1.78e-01 +1.97e-01 +2.41e-01 +3.94e-01 +2.26e-03 +1.43e-01 +1.58e-01 +1.75e-01 +1.86e-01 +2.06e-01 +4.83e-02 +2.11e-03 +1.31e-01 +1.37e-01 +1.61e-01 +1.76e-01 +2.04e-01 +1.15e-01 +5.04e-03 +1.05e-01 +1.31e-01 +1.49e-01 +1.72e-01 +1.81e-01 +4.24e-02 +5.31e-03 +6.79e-02 +9.98e-02 +1.56e-01 +1.68e-01 +1.78e-01 +1.39e-02 +5.49e-03 +2.69e-03 +6.66e-03 +2.36e-02 +1.65e-02 +1.71e-02 +1.12e-02 +1.94e-03 +1.12e-04 +5.43e-04 +1.23e-03 +1.51e-03 +1.51e-03 +1.31e-03 +1.21e-03 +1.52e-01 +1.60e-01 +1.81e-01 +2.03e-01 +2.16e-01 +2.58e-02 +2.07e-03 +1.45e-01 +1.59e-01 +1.84e-01 +2.07e-01 +2.14e-01 +8.82e-02 +2.92e-03 +1.36e-01 +1.53e-01 +1.61e-01 +1.92e-01 +2.28e-01 +1.82e-01 +2.70e-03 +1.18e-01 +1.42e-01 +1.52e-01 +1.81e-01 +1.98e-01 +2.87e-02 +6.46e-03 +1.00e-01 +1.41e-01 +1.80e-01 +1.73e-01 +2.05e-01 +1.09e-01 +3.29e-03 +5.37e-03 +1.44e-02 +4.53e-02 +2.16e-02 +7.53e-02 +2.38e-02 +1.38e-03 +4.52e-04 +1.11e-03 +1.39e-03 +1.33e-03 +1.26e-03 +1.22e-03 +9.78e-04 +1.83e-01 +1.77e-01 +1.83e-01 +2.03e-01 +2.27e-01 +7.70e-02 +3.55e-03 +1.63e-01 +1.80e-01 +1.87e-01 +1.86e-01 +2.20e-01 +4.43e-02 +2.00e-03 +1.68e-01 +1.60e-01 +1.63e-01 +1.78e-01 +2.12e-01 +9.86e-02 +2.96e-03 +1.56e-01 +1.59e-01 +1.63e-01 +1.72e-01 +1.81e-01 +7.15e-02 +1.77e-03 +1.58e-01 +1.80e-01 +1.87e-01 +1.66e-01 +1.66e-01 +1.22e-02 +2.65e-03 +2.11e-02 +3.01e-02 +4.85e-02 +1.48e-02 +4.67e-02 +5.38e-03 +1.01e-03 +8.10e-04 +8.55e-04 +1.43e-03 +1.63e-03 +1.05e-03 +1.04e-03 +6.84e-04 +2.01e-01 +2.17e-01 +2.26e-01 +2.37e-01 +2.45e-01 +3.25e-02 +2.68e-02 +2.09e-01 +2.09e-01 +2.07e-01 +2.02e-01 +2.15e-01 +2.42e-02 +2.97e-03 +2.01e-01 +1.94e-01 +1.93e-01 +1.80e-01 +1.72e-01 +1.32e-02 +1.54e-03 +1.84e-01 +1.87e-01 +1.63e-01 +1.49e-01 +1.50e-01 +6.42e-02 +1.91e-03 +1.93e-01 +2.15e-01 +1.93e-01 +1.42e-01 +1.22e-01 +7.21e-03 +9.37e-04 +1.67e-02 +5.33e-02 +5.24e-02 +1.13e-02 +1.23e-02 +4.44e-03 +6.14e-04 +7.25e-04 +6.85e-04 +8.30e-04 +9.75e-04 +6.78e-04 +5.52e-04 +5.29e-04 +2.82e-01 +2.73e-01 +2.85e-01 +2.57e-01 +2.70e-01 +2.24e-02 +2.57e-03 +2.52e-01 +2.49e-01 +2.38e-01 +2.53e-01 +2.37e-01 +1.48e-02 +2.30e-03 +2.18e-01 +2.22e-01 +2.20e-01 +1.96e-01 +1.80e-01 +1.93e-02 +1.53e-03 +2.21e-01 +2.39e-01 +1.99e-01 +1.67e-01 +1.45e-01 +1.11e-02 +8.58e-04 +2.57e-01 +2.32e-01 +1.89e-01 +1.26e-01 +8.31e-02 +8.12e-03 +6.25e-04 +8.53e-02 +7.87e-02 +1.53e-02 +1.22e-02 +7.53e-03 +1.75e-03 +3.39e-04 +1.08e-03 +8.97e-04 +6.11e-04 +5.24e-04 +4.33e-04 +2.49e-04 +2.36e-04 +4.78e-01 +1.38e-01 +5.00e-01 +2.99e-02 +3.12e-02 +1.07e-01 +1.42e-03 +3.01e-01 +4.37e-02 +1.79e-01 +2.39e-01 +7.54e-02 +1.13e-02 +1.17e-03 +3.44e-01 +2.30e-01 +5.55e-02 +5.69e-02 +1.70e-02 +8.02e-03 +8.18e-04 +1.77e-01 +6.57e-02 +6.14e-02 +3.20e-02 +1.32e-02 +6.75e-03 +6.17e-04 +3.49e-02 +1.65e-02 +1.95e-02 +8.71e-03 +9.02e-03 +2.07e-03 +3.41e-04 +7.97e-02 +2.60e-02 +1.65e-02 +6.67e-03 +2.61e-03 +5.74e-04 +1.41e-04 +1.62e-03 +1.49e-03 +8.36e-04 +3.95e-04 +2.37e-04 +1.32e-04 +5.59e-05 +1.07e-03 +9.93e-04 +1.63e-03 +6.02e-03 +1.30e-03 +2.86e-03 +2.98e-03 +1.02e-03 +1.31e-03 +1.39e-03 +2.62e-03 +1.69e-03 +2.40e-03 +3.96e-03 +1.26e-03 +1.78e-03 +1.30e-03 +1.06e-03 +1.79e-03 +1.40e-03 +3.18e-03 +1.93e-03 +1.72e-03 +1.78e-03 +8.41e-04 +8.66e-04 +8.00e-04 +1.01e-03 +1.54e-03 +1.30e-03 +1.11e-03 +9.80e-04 +5.02e-04 +3.11e-04 +3.49e-04 +1.24e-03 +1.66e-03 +8.20e-04 +5.29e-04 +3.62e-04 +1.27e-04 +6.03e-05 +6.28e-04 +6.20e-04 +5.01e-04 +4.28e-04 +2.95e-04 +5.38e-05 +6.24e-06 +Upper Bounds +7.61e-01 +7.65e-01 +8.88e-01 +9.87e-01 +1.21e+00 +1.97e+00 +1.13e-02 +7.17e-01 +7.88e-01 +8.74e-01 +9.32e-01 +1.03e+00 +2.41e-01 +1.06e-02 +6.53e-01 +6.86e-01 +8.03e-01 +8.82e-01 +1.02e+00 +5.77e-01 +2.52e-02 +5.23e-01 +6.53e-01 +7.46e-01 +8.58e-01 +9.06e-01 +2.12e-01 +2.66e-02 +3.40e-01 +4.99e-01 +7.82e-01 +8.41e-01 +8.89e-01 +6.94e-02 +2.75e-02 +1.34e-02 +3.33e-02 +1.18e-01 +8.26e-02 +8.54e-02 +5.62e-02 +9.71e-03 +5.62e-04 +2.72e-03 +6.16e-03 +7.54e-03 +7.55e-03 +6.53e-03 +6.05e-03 +7.58e-01 +7.99e-01 +9.04e-01 +1.01e+00 +1.08e+00 +1.29e-01 +1.03e-02 +7.25e-01 +7.95e-01 +9.18e-01 +1.03e+00 +1.07e+00 +4.41e-01 +1.46e-02 +6.80e-01 +7.67e-01 +8.04e-01 +9.61e-01 +1.14e+00 +9.09e-01 +1.35e-02 +5.92e-01 +7.08e-01 +7.60e-01 +9.05e-01 +9.92e-01 +1.43e-01 +3.23e-02 +5.02e-01 +7.06e-01 +9.01e-01 +8.64e-01 +1.02e+00 +5.44e-01 +1.64e-02 +2.68e-02 +7.22e-02 +2.27e-01 +1.08e-01 +3.77e-01 +1.19e-01 +6.91e-03 +2.26e-03 +5.54e-03 +6.95e-03 +6.66e-03 +6.32e-03 +6.08e-03 +4.89e-03 +9.15e-01 +8.87e-01 +9.13e-01 +1.02e+00 +1.13e+00 +3.85e-01 +1.78e-02 +8.13e-01 +9.00e-01 +9.37e-01 +9.28e-01 +1.10e+00 +2.22e-01 +9.99e-03 +8.40e-01 +8.02e-01 +8.17e-01 +8.90e-01 +1.06e+00 +4.93e-01 +1.48e-02 +7.81e-01 +7.93e-01 +8.16e-01 +8.61e-01 +9.05e-01 +3.58e-01 +8.84e-03 +7.91e-01 +9.00e-01 +9.34e-01 +8.31e-01 +8.31e-01 +6.09e-02 +1.33e-02 +1.06e-01 +1.50e-01 +2.43e-01 +7.38e-02 +2.33e-01 +2.69e-02 +5.07e-03 +4.05e-03 +4.28e-03 +7.13e-03 +8.17e-03 +5.26e-03 +5.20e-03 +3.42e-03 +1.01e+00 +1.09e+00 +1.13e+00 +1.18e+00 +1.22e+00 +1.62e-01 +1.34e-01 +1.05e+00 +1.04e+00 +1.03e+00 +1.01e+00 +1.07e+00 +1.21e-01 +1.49e-02 +1.00e+00 +9.68e-01 +9.64e-01 +9.01e-01 +8.62e-01 +6.59e-02 +7.71e-03 +9.19e-01 +9.37e-01 +8.13e-01 +7.43e-01 +7.51e-01 +3.21e-01 +9.55e-03 +9.63e-01 +1.07e+00 +9.67e-01 +7.12e-01 +6.10e-01 +3.60e-02 +4.68e-03 +8.33e-02 +2.66e-01 +2.62e-01 +5.67e-02 +6.16e-02 +2.22e-02 +3.07e-03 +3.63e-03 +3.42e-03 +4.15e-03 +4.88e-03 +3.39e-03 +2.76e-03 +2.65e-03 +1.41e+00 +1.36e+00 +1.42e+00 +1.28e+00 +1.35e+00 +1.12e-01 +1.28e-02 +1.26e+00 +1.24e+00 +1.19e+00 +1.26e+00 +1.18e+00 +7.42e-02 +1.15e-02 +1.09e+00 +1.11e+00 +1.10e+00 +9.79e-01 +9.01e-01 +9.64e-02 +7.63e-03 +1.11e+00 +1.19e+00 +9.95e-01 +8.35e-01 +7.23e-01 +5.53e-02 +4.29e-03 +1.28e+00 +1.16e+00 +9.47e-01 +6.30e-01 +4.16e-01 +4.06e-02 +3.13e-03 +4.27e-01 +3.94e-01 +7.67e-02 +6.12e-02 +3.77e-02 +8.74e-03 +1.70e-03 +5.41e-03 +4.48e-03 +3.06e-03 +2.62e-03 +2.16e-03 +1.25e-03 +1.18e-03 +2.39e+00 +6.90e-01 +2.50e+00 +1.50e-01 +1.56e-01 +5.36e-01 +7.11e-03 +1.50e+00 +2.18e-01 +8.93e-01 +1.20e+00 +3.77e-01 +5.66e-02 +5.84e-03 +1.72e+00 +1.15e+00 +2.77e-01 +2.84e-01 +8.51e-02 +4.01e-02 +4.09e-03 +8.84e-01 +3.28e-01 +3.07e-01 +1.60e-01 +6.60e-02 +3.37e-02 +3.08e-03 +1.74e-01 +8.27e-02 +9.77e-02 +4.36e-02 +4.51e-02 +1.03e-02 +1.71e-03 +3.98e-01 +1.30e-01 +8.25e-02 +3.33e-02 +1.30e-02 +2.87e-03 +7.05e-04 +8.08e-03 +7.43e-03 +4.18e-03 +1.97e-03 +1.18e-03 +6.58e-04 +2.80e-04 +5.36e-03 +4.96e-03 +8.16e-03 +3.01e-02 +6.48e-03 +1.43e-02 +1.49e-02 +5.08e-03 +6.57e-03 +6.95e-03 +1.31e-02 +8.44e-03 +1.20e-02 +1.98e-02 +6.30e-03 +8.90e-03 +6.49e-03 +5.28e-03 +8.94e-03 +7.00e-03 +1.59e-02 +9.64e-03 +8.62e-03 +8.88e-03 +4.21e-03 +4.33e-03 +4.00e-03 +5.04e-03 +7.71e-03 +6.51e-03 +5.56e-03 +4.90e-03 +2.51e-03 +1.55e-03 +1.75e-03 +6.19e-03 +8.31e-03 +4.10e-03 +2.65e-03 +1.81e-03 +6.34e-04 +3.01e-04 +3.14e-03 +3.10e-03 +2.51e-03 +2.14e-03 +1.47e-03 +2.69e-04 +3.12e-05 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/test.py b/tests/regression_tests/weightwindows_fw_cadis_local/test.py new file mode 100644 index 000000000..abd3f2098 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_local/test.py @@ -0,0 +1,42 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube_with_detectors + +from tests.testing_harness import WeightWindowPyAPITestHarness + + +class MGXSTestHarness(WeightWindowPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_weight_windows_fw_cadis_local(): + model = random_ray_three_region_cube_with_detectors() + + for tally in list(model.tallies): + if tally.name in {"Source Tally", "Absorber Tally", "Cavity Tally"}: + # leave only the tallies of interest + model.tallies.remove(tally) + + ww_mesh = openmc.RegularMesh() + n = 7 + width = 35.0 + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", + targets=model.tallies, + mesh=ww_mesh, + max_realizations=model.settings.batches + ) + model.settings.weight_window_generators = wwg + model.settings.random_ray['volume_estimator'] = 'naive' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat index ceb89e6e3..a0d84257a 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -222,11 +222,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat index c7691e950..62f847858 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -222,11 +222,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/weightwindows_pulse_height/__init__.py b/tests/regression_tests/weightwindows_pulse_height/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat b/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat new file mode 100644 index 000000000..4cd24d529 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + + 1 + photon + 0.0 2000000.0 + 0.01 + 0.05 + 3.0 + 10 + 1e-38 + + + 1 1 1 + -2 -2 -2 + 2 2 2 + + false + + true + true + + 50 + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat b/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat new file mode 100644 index 000000000..c57e8ff1c --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat b/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat new file mode 100644 index 000000000..8bb499a73 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + + 1 + photon + 0.0 2000000.0 + 0.01 + 0.05 + 3.0 + 10 + 1e-38 + + + 1 1 1 + -2 -2 -2 + 2 2 2 + + true + + true + true + + 50 + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat b/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat new file mode 100644 index 000000000..c57e8ff1c --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/weightwindows_pulse_height/test.py b/tests/regression_tests/weightwindows_pulse_height/test.py new file mode 100644 index 000000000..b6b49be26 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/test.py @@ -0,0 +1,73 @@ +import numpy as np +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weightwindows_pulse_height(shared_secondary, subdir): + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() + + # Define materials (NaI scintillator) + NaI = openmc.Material() + NaI.set_density('g/cc', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) + + model.materials = openmc.Materials([NaI]) + + # Define geometry: NaI sphere inside vacuum sphere + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + model.settings.photon_transport = True + model.settings.shared_secondary_bank = shared_secondary + model.settings.max_history_splits = 50 + model.settings.weight_window_checkpoints = { + 'surface': True, + 'collision': True, + } + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle='photon' + ) + + # Define pulse-height tally + tally = openmc.Tally(name="pht tally") + tally.scores = ['pulse-height'] + cell_filter = openmc.CellFilter(inner_sphere) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) + tally.filters = [cell_filter, energy_filter] + model.tallies = [tally] + + # Define weight windows on a simple mesh covering the geometry + ww_mesh = openmc.RegularMesh() + ww_mesh.lower_left = (-2, -2, -2) + ww_mesh.upper_right = (2, 2, 2) + ww_mesh.dimension = (1, 1, 1) + + # Single energy bin for photons + e_bnds = [0.0, 2e6] + + # Uniform weight window bounds (low enough to trigger some splitting) + lower_bounds = np.array([0.01]) + + ww = openmc.WeightWindows(ww_mesh, lower_bounds, None, 5.0, e_bnds, 'photon') + model.settings.weight_windows = [ww] + + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 1ad91b7a8..c1977556d 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -143,7 +143,8 @@ class TestHarness: def _cleanup(self): """Delete statepoints, tally, and test files.""" output = glob.glob('statepoint.*.h5') - output += ['tallies.out', 'results_test.dat', 'summary.h5'] + output += ['tallies.out', 'tallies.forward.out'] + output += ['results_test.dat', 'summary.h5'] output += glob.glob('volume_*.h5') for f in output: if os.path.exists(f): @@ -546,19 +547,19 @@ class CollisionTrackTestHarness(PyAPITestHarness): def _test_output_created(self): """Make sure collision_track.h5 has also been created.""" - super()._test_output_created() if self._model.settings.collision_track: assert os.path.exists( "collision_track.h5" ), "collision_track file has not been created." - def _compare_output(self): + def _compare_results(self): """Compare collision_track.h5 files.""" if self._model.settings.collision_track: collision_track_true = self._return_collision_track_data( "collision_track_true.h5") collision_track_test = self._return_collision_track_data( "collision_track.h5") + assert collision_track_true.shape == collision_track_test.shape np.testing.assert_allclose( collision_track_true, collision_track_test, rtol=1e-07) @@ -582,15 +583,18 @@ class CollisionTrackTestHarness(PyAPITestHarness): def _overwrite_results(self): """Also add the 'collision_track.h5' file during overwriting.""" - super()._overwrite_results() if os.path.exists("collision_track.h5"): shutil.copyfile("collision_track.h5", "collision_track_true.h5") + def _write_results(self, results_string): + # The result file for this test are written by the OpenMC executable itself + pass + @staticmethod def _return_collision_track_data(filepath): """ - Read a collision_track file and return a sorted array composed - of flatten arrays of collision information. + Read a collision_track file and return a sorted array composed of + flattened collision records. Parameters ---------- @@ -600,42 +604,58 @@ class CollisionTrackTestHarness(PyAPITestHarness): Returns ------- data : np.array - Sorted array composed of flatten arrays of collision_track data for - each collision information + Sorted array composed of flattened collision-track records. """ - data = [] - keys = [] - - # Read source file source = openmc.read_collision_track_file(filepath) - for src in source: - r = src['r'] - u = src['u'] - e = src['E'] - de = src['dE'] - time = src['time'] - wgt = src['wgt'] - delayed_group = src['delayed_group'] - cell_id = src['cell_id'] - nuclide_id = src['nuclide_id'] - material_id = src['material_id'] - universe_id = src['universe_id'] - n_collision = src['n_collision'] - event_mt = src['event_mt'] - key = ( - f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" - f"{e:.10e} {de:.10e} {time:.10e} {wgt:.10e} {event_mt} {delayed_group} {cell_id}" - f"{nuclide_id} {material_id} {universe_id} {n_collision} " - ) - keys.append(key) - values = [*r, *u, e, de, time, wgt, event_mt, - delayed_group, cell_id, nuclide_id, material_id, - universe_id, n_collision] - assert len(values) == 17 - data.append(values) + columns = [ + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + source['event_mt'], + source['delayed_group'], + source['cell_id'], + source['nuclide_id'], + source['material_id'], + source['universe_id'], + source['n_collision'], + source['particle'], + source['parent_id'], + source['progeny_id'], + ] + data = np.column_stack(columns) - data = np.array(data) - keys = np.array(keys) - sorted_idx = np.argsort(keys, kind='stable') + # Sort by the complete record, prioritizing stable integer identifiers + # before floating-point fields. This removes dependence on the order in + # which threads append otherwise reproducible collision records. + sort_columns = [ + source['parent_id'], + source['progeny_id'], + source['n_collision'], + source['particle'], + source['cell_id'], + source['material_id'], + source['universe_id'], + source['nuclide_id'], + source['event_mt'], + source['delayed_group'], + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + ] + sorted_idx = np.lexsort(tuple(reversed(sort_columns))) return data[sorted_idx] diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py index e97ab13e2..44b5a5018 100644 --- a/tests/unit_tests/__init__.py +++ b/tests/unit_tests/__init__.py @@ -14,3 +14,12 @@ def assert_unbounded(obj): ll, ur = obj.bounding_box assert ll == pytest.approx((-np.inf, -np.inf, -np.inf)) assert ur == pytest.approx((np.inf, np.inf, np.inf)) + + +def assert_sample_mean(samples, expected_mean): + # Calculate sample standard deviation + std_dev = samples.std() / np.sqrt(samples.size - 1) + + # Means should agree within 4 sigma 99.993% of the time. Note that this is + # expected to fail about 1 out of 16,000 times + assert np.abs(expected_mean - samples.mean()) < 4*std_dev diff --git a/tests/unit_tests/dagmc/test_convert_to_multigroup.py b/tests/unit_tests/dagmc/test_convert_to_multigroup.py new file mode 100644 index 000000000..069dc658e --- /dev/null +++ b/tests/unit_tests/dagmc/test_convert_to_multigroup.py @@ -0,0 +1,53 @@ +"""Test that convert_to_multigroup works with DAGMC models without requiring +particles/batches to be set beforehand. +""" + +from pathlib import Path +import pytest +import openmc +import openmc.lib + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), + reason="DAGMC CAD geometry is not enabled.") + + +def test_convert_to_multigroup_without_particles_batches(run_in_tmpdir): + """Test that convert_to_multigroup works with DAGMC model without + setting particles/batches beforehand.""" + openmc.reset_auto_ids() + + mat = openmc.Material(name="mat") + mat.add_nuclide("Fe56", 1.0) + mat.set_density("g/cm3", 7.0) + + # Use minimal tetrahedral DAGMC file + dagmc_file = Path(__file__).parent / "dagmc_tetrahedral_no_graveyard.h5m" + dagmc_univ = openmc.DAGMCUniverse(dagmc_file, auto_geom_ids=True) + bound_dagmc_univ = dagmc_univ.bounded_universe(padding_distance=1) + + # Create model WITHOUT setting particles or batches + model = openmc.Model() + model.materials = openmc.Materials([mat]) + model.geometry = openmc.Geometry(bound_dagmc_univ) + model.settings = openmc.Settings() # Note: no particles or batches set! + + model.settings.run_mode = 'fixed source' + + # Create a point source + my_source = openmc.IndependentSource() + my_source.space = openmc.stats.Point((0.25, 0.25, 0.25)) + my_source.energy = openmc.stats.delta_function(14e6) + model.settings.source = my_source + + # This should work without requiring particles/batches to be set + # convert_to_multigroup handles initialization internally using non-transport mode + model.convert_to_multigroup( + method='material_wise', + groups='CASMO-2', + nparticles=10, + overwrite_mgxs_library=True + ) + + # Verify the model was converted successfully + assert model.settings.energy_mode == 'multi-group' diff --git a/tests/unit_tests/dagmc/test_lost_particles.py b/tests/unit_tests/dagmc/test_lost_particles.py index 3a4166009..48b6cf165 100644 --- a/tests/unit_tests/dagmc/test_lost_particles.py +++ b/tests/unit_tests/dagmc/test_lost_particles.py @@ -57,11 +57,16 @@ def broken_dagmc_model(request): model.settings.inactive = 2 model.settings.output = {'summary': False} + # Limit max particle events to prevent particles from getting stuck in the + # implicit complement of the non-root DAGMC universe. + model.settings.max_particle_events = 100 + model.export_to_xml() return model +@pytest.mark.skip(reason="Test causes CI to hang intermittently") def test_lost_particles(run_in_tmpdir, broken_dagmc_model): broken_dagmc_model.export_to_xml() # ensure that particles will be lost when cell intersections can't be found diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py index 0de4f6092..5889fa542 100644 --- a/tests/unit_tests/dagmc/test_model.py +++ b/tests/unit_tests/dagmc/test_model.py @@ -31,7 +31,7 @@ def model(request): p = Path(request.fspath).parent / "dagmc.h5m" - daguniv = openmc.DAGMCUniverse(p, auto_geom_ids=True) + daguniv = openmc.DAGMCUniverse(p, name='simple-dagmc', auto_geom_ids=True) lattice = openmc.RectLattice() lattice.dimension = [2, 2] @@ -70,28 +70,20 @@ def model(request): openmc.reset_auto_ids() -def test_dagmc_replace_material_assignment(model): - mats = {} - - mats["foo"] = openmc.Material(name="foo") - mats["foo"].add_nuclide("H1", 2.0) - mats["foo"].add_element("O", 1.0) - mats["foo"].set_density("g/cm3", 1.0) - mats["foo"].add_s_alpha_beta("c_H_in_H2O") - +def test_dagmc_sync_cell_names(model): + dag_univ = None for univ in model.geometry.get_all_universes().values(): - if not isinstance(univ, openmc.DAGMCUniverse): + if isinstance(univ, openmc.DAGMCUniverse): + dag_univ = univ break - cells_with_41 = [] - for cell in univ.cells.values(): - if cell.fill is None: - continue - if cell.fill.name == "41": - cells_with_41.append(cell.id) - univ.replace_material_assignment("41", mats["foo"]) - for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert dag_univ is not None + + for cell_id, cell in dag_univ.cells.items(): + assert cell.name == openmc.lib.cells[cell_id].name + + assert any(cell.name == "implicit complement" + for cell in dag_univ.cells.values()) def test_dagmc_add_material_override_with_id(model): @@ -114,7 +106,7 @@ def test_dagmc_add_material_override_with_id(model): cells_with_41.append(cell.id) univ.add_material_override(cell.id, mats["foo"]) for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert univ.cells[cell_id].fill == mats["foo"] def test_dagmc_add_material_override_with_cell(model): @@ -137,7 +129,7 @@ def test_dagmc_add_material_override_with_cell(model): cells_with_41.append(cell.id) univ.add_material_override(cell, mats["foo"]) for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert univ.cells[cell_id].fill == mats["foo"] def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir): @@ -174,57 +166,20 @@ def test_model_differentiate_with_dagmc(model): assert len(model.materials) == 4*2 + 4 -def test_bad_override_cell_id(model): - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="Cell ID '1' not found in DAGMC universe"): - univ.material_overrides = {1: model.materials[0]} - - -def test_bad_override_type(model): - not_a_dag_cell = openmc.Cell() - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="Unrecognized key type. Must be an integer or openmc.DAGMCCell object"): - univ.material_overrides = {not_a_dag_cell: model.materials[0]} - - -def test_bad_replacement_mat_name(model): - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="No material with name 'not_a_mat' found in the DAGMC universe"): - univ.replace_material_assignment("not_a_mat", model.materials[0]) - - def test_dagmc_xml(model): - # Set the environment - mats = {} - mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") - mats["no-void fuel"].add_nuclide("U235", 0.03) - mats["no-void fuel"].add_nuclide("U238", 0.97) - mats["no-void fuel"].add_nuclide("O16", 2.0) - mats["no-void fuel"].set_density("g/cm3", 10.0) - - mats[5] = openmc.Material(name="41") - mats[5].add_nuclide("H1", 2.0) - mats[5].add_element("O", 1.0) - mats[5].set_density("g/cm3", 1.0) - mats[5].add_s_alpha_beta("c_H_in_H2O") + override_mat = openmc.Material(name="41") + override_mat.add_nuclide("H1", 2.0) + override_mat.add_element("O", 1.0) + override_mat.set_density("g/cm3", 1.0) + override_mat.add_s_alpha_beta("c_H_in_H2O") + model.materials.append(override_mat) for univ in model.geometry.get_all_universes().values(): if isinstance(univ, openmc.DAGMCUniverse): dag_univ = univ break - for k, v in mats.items(): - if isinstance(k, int): - dag_univ.add_material_override(k, v) - model.materials.append(v) - elif isinstance(k, str): - dag_univ.replace_material_assignment(k, v) + dag_univ.add_material_override(5, override_mat) # Tesing the XML subelement generation root = ET.Element('dagmc_universe') @@ -232,15 +187,28 @@ def test_dagmc_xml(model): dagmc_ele = root.find('dagmc_universe') assert dagmc_ele.get('id') == str(dag_univ.id) + assert dagmc_ele.get('name') == str(dag_univ.name) assert dagmc_ele.get('filename') == str(dag_univ.filename) assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower() - override_eles = dagmc_ele.find('material_overrides').findall('cell_override') - assert len(override_eles) == 4 + assert dagmc_ele.find('material_overrides') is None - for i, override_ele in enumerate(override_eles): - cell_id = override_ele.get('id') - assert dag_univ.material_overrides[int(cell_id)][0].id == int(override_ele.find('material_ids').text) + override_elements = dagmc_ele.findall('cell') + assert len(override_elements) == len(dag_univ.cells) + xml_cells = {int(elem.get('id')): elem for elem in override_elements} + for cell_id, cell in dag_univ.cells.items(): + assert cell_id in xml_cells + xml_cell = xml_cells[cell_id] + if cell.fill_type == 'void': + assert xml_cell.get('material') == 'void' + elif cell.fill_type == 'material': + assert xml_cell.get('material') == str(cell.fill.id) + elif cell.fill_type == 'distribmat': + mat_list = xml_cell.find('material').text.split() + expected = ["void" if m is None else str(m.id) for m in cell.fill] + assert mat_list == expected + else: + pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}") model.export_to_model_xml() @@ -251,7 +219,147 @@ def test_dagmc_xml(model): xml_dagmc_univ = univ break - assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.keys() + assert xml_dagmc_univ.cells.keys() == dag_univ.cells.keys() - for xml_mats, model_mats in zip(xml_dagmc_univ._material_overrides.values(), dag_univ._material_overrides.values()): - assert all([xml_mat.id == orig_mat.id for xml_mat, orig_mat in zip(xml_mats, model_mats)]) + for cell_id, cell in dag_univ.cells.items(): + xml_cell = xml_dagmc_univ.cells[cell_id] + assert xml_cell.fill_type == cell.fill_type + if cell.fill_type == 'void': + assert xml_cell.fill is None + elif cell.fill_type == 'material': + assert xml_cell.fill.id == cell.fill.id + elif cell.fill_type == 'distribmat': + xml_ids = [m.id if m is not None else None for m in xml_cell.fill] + model_ids = [m.id if m is not None else None for m in cell.fill] + assert xml_ids == model_ids + else: + pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}") + + +def test_dagmc_xml_reject_fill_override(): + mats = {'1': openmc.Material(1), 'void': None} + elem = ET.fromstring( + '' + '' + '' + ) + with pytest.raises(ValueError, match="cannot specify 'fill'"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_reject_region_override(): + mats = {'1': openmc.Material(1), 'void': None} + elem = ET.fromstring( + '' + '' + '' + ) + with pytest.raises(ValueError, match="cannot specify 'region'"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def _legacy_xml(cell_overrides): + """Helper to build a with old-format .""" + inner = ''.join( + f'{mids}' + for cid, mids in cell_overrides.items() + ) + return ET.fromstring( + f'' + f'{inner}' + f'' + ) + + +def test_dagmc_xml_legacy_single_material_compat(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning, match="deprecated"): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert 3 in univ.cells + assert univ.cells[3].fill is mat + + +def test_dagmc_xml_legacy_distribmat_compat(): + mat1, mat2 = openmc.Material(2), openmc.Material(3) + mats = {'2': mat1, '3': mat2, 'void': None} + elem = _legacy_xml({5: '2 3'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert univ.cells[5].fill_type == 'distribmat' + assert list(univ.cells[5].fill) == [mat1, mat2] + + +def test_dagmc_xml_legacy_void_compat(): + mats = {'void': None} + elem = _legacy_xml({7: 'void'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert univ.cells[7].fill_type == 'void' + + +def test_dagmc_xml_legacy_both_raises(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = ET.fromstring( + '' + '' + '1' + '' + '' + '' + ) + with pytest.raises(ValueError, match="both"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_legacy_deprecation_warning(): + mats = {'1': openmc.Material(1), 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_legacy_roundtrip(): + """Old-format XML loads correctly and re-exports using the new format.""" + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + + root = ET.Element('geometry') + univ.create_xml_subelement(root) + dagmc_elem = root.find('dagmc_universe') + + assert dagmc_elem.find('material_overrides') is None + cell_elems = dagmc_elem.findall('cell') + assert len(cell_elems) == 1 + assert int(cell_elems[0].get('id')) == 3 + assert cell_elems[0].get('material') == '1' + + +def test_dagmc_xml_temperature_roundtrip(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + + elem = ET.fromstring( + '' + '' + '' + ) + + dag_univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert dag_univ.cells[7].fill.id == 1 + assert dag_univ.cells[7].temperature == pytest.approx(825.0) + + root = ET.Element('geometry') + dag_univ.create_xml_subelement(root) + dagmc_elem = root.find('dagmc_universe') + xml_cell = dagmc_elem.find('cell') + assert xml_cell.get('temperature') == '825.0' + + dag_univ_roundtrip = openmc.DAGMCUniverse.from_xml_element(dagmc_elem, mats) + assert dag_univ_roundtrip.cells[7].fill.id == 1 + assert dag_univ_roundtrip.cells[7].temperature == pytest.approx(825.0) diff --git a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py index 8166ba68c..366ce18ef 100644 --- a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py +++ b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py @@ -131,7 +131,7 @@ def test_write_data_to_vtk_size_mismatch(mesh): mesh : openmc.StructuredMesh The mesh to test """ - right_size = mesh.num_mesh_cells + right_size = mesh.n_elements data = np.random.random(right_size + 1) # Error message has \ in to escape characters that are otherwise recognized @@ -139,7 +139,7 @@ def test_write_data_to_vtk_size_mismatch(mesh): # string when using the match argument as that uses regular expression expected_error_msg = ( fr"The size of the dataset 'label' \({len(data)}\) should be equal to " - fr"the number of mesh cells \({mesh.num_mesh_cells}\)" + fr"the number of mesh cells \({mesh.n_elements}\)" ) with pytest.raises(ValueError, match=expected_error_msg): mesh.write_data_to_vtk(filename="out.vtk", datasets={"label": data}) diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 60b205818..8d39c071b 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -20,6 +20,12 @@ def test_contains(): assert (10.0, -4., 2.0) in c +def test_id(): + openmc.Cell(cell_id=0) + with pytest.raises(ValueError): + openmc.Cell(cell_id=-1) + + def test_repr(cell_with_lattice): cells, mats, univ, lattice = cell_with_lattice repr(cells[0]) # cell with distributed materials @@ -368,6 +374,49 @@ def test_rotation_from_xml(rotation): np.testing.assert_allclose(new_cell.rotation, cell.rotation) +def test_dagmccell_from_xml_element(): + """DAGMCCell.from_xml_element parses material, temperature, density, + and volume; rejects unsupported attributes.""" + mat = openmc.Material(1) + mat.add_nuclide('U235', 1.0) + mats = {'1': mat} + # In practice, from_xml_element is always called from DAGMCUniverse + # during XML parsing. A placeholder universe is used here so the test + # can exercise the method directly without a real DAGMC model file. + placeholder_univ = openmc.DAGMCUniverse('model.h5m') + + # material + temperature + density round-trip + xml = '' + cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + assert cell.id == 5 + assert cell.name == 'fuel' + assert cell.fill is mat + assert cell.density == 10.5 + assert cell.temperature == 900.0 + + # volume round-trip + xml = '' + cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + assert cell.volume == 42.0 + + # forbidden: region, fill, universe + for tag, val in [('region', '-1'), ('fill', '2'), ('universe', '0')]: + xml = f'' + with pytest.raises(ValueError, match=tag): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + # forbidden: translation, rotation + for tag, val in [('translation', '1 0 0'), ('rotation', '0 0 90')]: + xml = f'' + with pytest.raises(ValueError, match=tag): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + # missing material raises + xml = '' + with pytest.raises(ValueError, match='material'): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + def test_plot(run_in_tmpdir): zcyl = openmc.ZCylinder() c = openmc.Cell(region=-zcyl) diff --git a/tests/unit_tests/test_collision_track.py b/tests/unit_tests/test_collision_track.py index 9bc6a8c15..96676e2cd 100644 --- a/tests/unit_tests/test_collision_track.py +++ b/tests/unit_tests/test_collision_track.py @@ -5,6 +5,7 @@ import openmc import pytest import h5py import numpy as np +import shutil from tests.testing_harness import CollisionTrackTestHarness as ctt @@ -33,6 +34,7 @@ def geometry(): {"max_collisions": 200, "mcpl": True} ], + ids=str ) def test_xml_serialization(parameter, run_in_tmpdir): """Check that the different use cases can be written and read in XML.""" @@ -44,7 +46,7 @@ def test_xml_serialization(parameter, run_in_tmpdir): assert read_settings.collision_track == parameter -@pytest.fixture(scope="module") +@pytest.fixture def model(): """Simple hydrogen sphere divided in two hemispheres by a z-plane to form 2 cells.""" @@ -109,6 +111,7 @@ def test_particle_location(run_in_tmpdir, model): assert False +@pytest.mark.skipif(shutil.which("mcpl-config") is None, reason="MCPL is not available.") def test_format_similarity(run_in_tmpdir, model): model.settings.collision_track = {"max_collisions": 200, "reactions": ['elastic'], "cell_ids": [1, 2], "mcpl": False} @@ -125,3 +128,43 @@ def test_format_similarity(run_in_tmpdir, model): np.testing.assert_allclose(data_h5, data_mcpl, rtol=1e-05) # tolerance not that low due to the strings that is saved in MCPL, # not enough precision! + + +def test_photon_particles(run_in_tmpdir, model): + """Test that the collision track can be used to track photon particles.""" + model.settings.collision_track = {"max_collisions": 200, "cell_ids": [1, 2]} + + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box(*model.geometry.bounding_box), + energy=openmc.stats.delta_function(1e5), + particle='photon' + ) + model.run() + + with h5py.File("collision_track.h5", "r") as f: + source = f["collision_track_bank"] + + assert len(source) < 200 + + allowed_particles = (openmc.ParticleType.PHOTON, openmc.ParticleType.ELECTRON) + + for point in source: + particle_type = openmc.ParticleType(point['particle']) + assert particle_type in allowed_particles + + if particle_type == openmc.ParticleType.ELECTRON: + assert point['nuclide_id'] == 0 + + +def test_collision_track_two_threads(model, run_in_tmpdir): + # This test checks that the `max_collisions` setting is honored: + # no collisions beyond the specified limit should be recorded. + # + # The exact set of events in the capped bank is not reproducible with + # multiple threads because the bank stores whichever thread appends first + # until capacity is reached. + model.settings.collision_track = {"max_collisions": 200} + model.run(threads=2, particles=500) + + collision_track = openmc.read_collision_track_hdf5("collision_track.h5") + assert len(collision_track) == 200 diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py index 8f3b62f40..49c1b3049 100644 --- a/tests/unit_tests/test_d1s.py +++ b/tests/unit_tests/test_d1s.py @@ -150,3 +150,7 @@ def test_apply_time_correction(run_in_tmpdir): result_summed.get_reshaped_data() result.get_pandas_dataframe() result_summed.get_pandas_dataframe() + + # The summed tally is derived, so sum/sum_sq are None + assert result_summed.sum is None + assert result_summed.sum_sq is None diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index de8d90a43..f81f857e0 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -100,6 +100,28 @@ def test_fpy(u235_yields): ufloat_close(thermal['I135'], ufloat(0.0292737, 0.000819663)) +def test_decay_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'decay', 'dec-041_Nb_090.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.Decay.from_endf(material) + + assert data.nuclide['name'] == 'Nb90' + assert not data.nuclide['stable'] + assert len(data.modes) == 2 + + +def test_fpy_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'nfy', 'nfy-092_U_235.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.FissionProductYields.from_endf(material) + + assert data.nuclide['name'] == 'U235' + assert data.energies == pytest.approx([0.0253, 500.e3, 1.4e7]) + assert 'I135' in data.cumulative[0] + + def test_sources(ba137m, nb90): # Running .sources twice should give same objects sources = ba137m.sources @@ -133,16 +155,16 @@ def test_decay_photon_energy(): with pytest.raises(DataError): openmc.data.decay_photon_energy('I135') - # Set chain file to simple chain - openmc.config['chain_file'] = Path(__file__).parents[1] / "chain_simple.xml" + # Temporarily Set chain file to simple chain + with openmc.config.patch('chain_file', Path(__file__).parents[1] / 'chain_simple.xml'): - # Check strength of I135 source and presence of specific spectral line - src = openmc.data.decay_photon_energy('I135') - assert isinstance(src, openmc.stats.Discrete) - assert src.integral() == pytest.approx(3.920996223799345e-05) - assert 1260409. in src.x + # Check strength of I135 source and presence of specific spectral line + src = openmc.data.decay_photon_energy('I135') + assert isinstance(src, openmc.stats.Discrete) + assert src.integral() == pytest.approx(3.920996223799345e-05) + assert 1260409. in src.x - # Check Xe135 source, which should be tabular - src = openmc.data.decay_photon_energy('Xe135') - assert isinstance(src, openmc.stats.Tabular) - assert src.integral() == pytest.approx(2.076506258964966e-05) + # Check Xe135 source, which should be tabular + src = openmc.data.decay_photon_energy('Xe135') + assert isinstance(src, openmc.stats.Tabular) + assert src.integral() == pytest.approx(2.076506258964966e-05) diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 4f1880014..32be5eb89 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -34,6 +34,20 @@ def test_dose_coefficients(): assert energy[-1] == approx(20.0e6) assert dose[-1] == approx(338.0) + energy, dose = dose_coefficients( + 'neutron', data_source='icrp74', dose_quantity='ambient') + assert energy[0] == approx(1e-3) + assert dose[0] == approx(6.60) + assert energy[-1] == approx(20.0e6) + assert dose[-1] == approx(600) + + energy, dose = dose_coefficients( + 'photon', data_source='icrp74', dose_quantity='ambient') + assert energy[0] == approx(0.01e6) + assert dose[0] == approx(0.061) + assert energy[-1] == approx(10e6) + assert dose[-1] == approx(25.6) + # Invalid particle/geometry should raise an exception with raises(ValueError): dose_coefficients('slime', 'LAT') @@ -41,6 +55,14 @@ def test_dose_coefficients(): dose_coefficients('neutron', 'ZZ') with raises(ValueError): dose_coefficients('neutron', data_source='icrp7000') + with raises(ValueError): + dose_coefficients('neutron', dose_quantity='banana') + with raises(ValueError): + dose_coefficients( + 'neutron', data_source='icrp116', dose_quantity='ambient') + with raises(ValueError): + dose_coefficients( + 'neutron', 'ISO', data_source='icrp74', dose_quantity='ambient') with raises(ValueError) as excinfo: dose_coefficients("photons", data_source="icrp116") expected_particles = [ @@ -57,7 +79,8 @@ def test_dose_coefficients(): "proton", ] expected_msg = ( - "'photons' has no dose data in data source icrp116. " - f"Available particles for icrp116 are: {expected_particles}" + "'photons' has no effective dose data in data source icrp116. " + "Available particles for icrp116 with dose quantity effective are: " + f"{expected_particles}" ) assert str(excinfo.value) == expected_msg diff --git a/tests/unit_tests/test_data_mass_attenuation.py b/tests/unit_tests/test_data_mass_attenuation.py new file mode 100644 index 000000000..0fe12a7db --- /dev/null +++ b/tests/unit_tests/test_data_mass_attenuation.py @@ -0,0 +1,53 @@ +from pytest import approx, raises + +from openmc.data import mass_energy_absorption_coefficient, mass_attenuation_coefficient +from openmc.data.function import Tabulated1D + + +def test_mass_attenuation_type(): + mu = mass_attenuation_coefficient(26) # Fe + assert isinstance(mu, Tabulated1D) + + +def test_mass_attenuation_spot_values(): + # Spot checks for Fe (Z=26) against NIST data: first/last tabulated points + # and a mid-range value at 1 MeV + mu = mass_attenuation_coefficient(26) + assert mu(1e3) == approx(9085.0) + assert mu(1e6) == approx(0.05995) + assert mu(2e7) == approx(0.03224) + + +def test_mass_attenuation_caching(): + # Repeated calls with the same Z should return the identical object + mu1 = mass_attenuation_coefficient(26) + mu2 = mass_attenuation_coefficient('Fe') + assert mu1 is mu2 + + +def test_mass_attenuation_invalid_z(): + with raises(ValueError, match="Z=0"): + mass_attenuation_coefficient(0) + with raises(ValueError, match="Z=200"): + mass_attenuation_coefficient(200) + + +def test_mass_energy_absorption_type(): + # Spot checks on values from NIST tables + mu_en = mass_energy_absorption_coefficient("air") + assert isinstance(mu_en, Tabulated1D) + + +def test_mass_energy_absorption_spot_values(): + mu_en = mass_energy_absorption_coefficient("air") + assert mu_en(1e3) == approx(3.599e3) + assert mu_en(10.e3) == approx(4.742) + assert mu_en(2e7) == approx(1.311e-2) + + +def test_mass_energy_absorption_invalid(): + # Invalid material/data_source should raise an exception + with raises(ValueError): + mass_energy_absorption_coefficient("pasta") + with raises(ValueError): + mass_energy_absorption_coefficient("air", data_source="nist000") diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index 14db68913..320d09eda 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -7,6 +7,7 @@ from pathlib import Path import numpy as np import pytest import openmc.data +from openmc.deplete import Chain, Nuclide def test_data_library(tmpdir): @@ -134,7 +135,8 @@ def test_zam(): with pytest.raises(ValueError): openmc.data.zam('Am242-m1') -def test_half_life(): + +def test_half_life(tmp_path): assert openmc.data.half_life('H2') is None assert openmc.data.half_life('U235') == pytest.approx(2.22102e16) assert openmc.data.half_life('Am242') == pytest.approx(57672.0) @@ -143,3 +145,32 @@ def test_half_life(): assert openmc.data.decay_constant('U235') == pytest.approx(log(2.0)/2.22102e16) assert openmc.data.decay_constant('Am242') == pytest.approx(log(2.0)/57672.0) assert openmc.data.decay_constant('Am242_m1') == pytest.approx(log(2.0)/4449622000.0) + + # Create minimal chain with H3 and Am242 to test half-life and decay + # constant retrieval from chain file + chain = Chain() + h3 = Nuclide("H3") + h3.half_life = 1.0 + chain.add_nuclide(h3) + am242 = Nuclide("Am242") + chain.add_nuclide(am242) + + assert openmc.data.half_life('H3', chain_file=chain) == 1.0 + assert openmc.data.decay_constant('H3', chain_file=chain) == pytest.approx(log(2.0)) + + # Nuclides that are present but stable in the chain should not fall back to + # ENDF/B-VIII.0 data. + assert openmc.data.half_life('Am242', chain_file=chain) is None + assert openmc.data.decay_constant('Am242', chain_file=chain) == 0.0 + + # Nuclides missing from the chain fall back to ENDF/B-VIII.0 data. + assert openmc.data.half_life('U235', chain_file=chain) == pytest.approx(2.22102e16) + + chain_path = tmp_path / "chain.xml" + chain.export_to_xml(chain_path) + assert openmc.data.half_life('H3', chain_file=chain_path) == 1.0 + + endf_h3 = openmc.data.half_life('H3') + with openmc.config.patch('chain_file', chain_path): + assert openmc.data.half_life('H3', chain_file=None) == 1.0 + assert openmc.data.half_life('H3', chain_file=False) == endf_h3 diff --git a/tests/unit_tests/test_data_multipole.py b/tests/unit_tests/test_data_multipole.py index 105099bb9..a58ab4865 100644 --- a/tests/unit_tests/test_data_multipole.py +++ b/tests/unit_tests/test_data_multipole.py @@ -49,14 +49,40 @@ def test_export_to_hdf5(tmpdir, u235): def test_from_endf(endf_data): - pytest.importorskip('vectfit') endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') assert openmc.data.WindowedMultipole.from_endf( - endf_file, log=True, wmp_options={"n_win": 400, "n_cf": 3}) + endf_file, + log=True, + # Keep the test lightweight + vf_options={ + "njoy_error": 5e-3, + "vf_pieces": 1, + "rtol": 5e-2, + "atol": 1e-3, + "orders": [8, 12], + "n_vf_iter": 6, + }, + wmp_options={"n_win": 50, "n_cf": 3, "rtol": 5e-2, "atol": 1e-3}, + ) def test_from_endf_search(endf_data): - pytest.importorskip('vectfit') - endf_file = os.path.join(endf_data, 'neutrons', 'n-095_Am_244.endf') + endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') assert openmc.data.WindowedMultipole.from_endf( - endf_file, log=True, wmp_options={"search": True, 'rtol':1e-2}) + endf_file, + log=True, + vf_options={ + "njoy_error": 5e-3, + "vf_pieces": 1, + "rtol": 5e-2, + "atol": 1e-3, + "orders": [8, 12], + "n_vf_iter": 6, + }, + wmp_options={ + "search": True, + "rtol": 5e-2, + "search_n_win": 3, + "search_cf_orders": [5, 3], + }, + ) diff --git a/tests/unit_tests/test_data_neutron.py b/tests/unit_tests/test_data_neutron.py index d43d93ae5..c767f0171 100644 --- a/tests/unit_tests/test_data_neutron.py +++ b/tests/unit_tests/test_data_neutron.py @@ -133,6 +133,28 @@ def test_attributes(pu239): assert pu239.atomic_weight_ratio == pytest.approx(236.9986) +def test_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.IncidentNeutron.from_endf(material) + + assert data.name == 'H1' + assert data.atomic_number == 1 + assert data.mass_number == 1 + assert 2 in data.reactions + + +def test_fission_energy_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'neutrons', 'n-092_U_235.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + neutron = openmc.data.IncidentNeutron.from_endf(material) + + data = openmc.data.FissionEnergyRelease.from_endf(material, neutron) + + assert data.fragments(0.0) > 0.0 + + def test_fission_energy(pu239): fer = pu239.fission_energy assert isinstance(fer, openmc.data.FissionEnergyRelease) diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index 98b180f52..aceb82c3a 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -149,3 +149,27 @@ def test_photodat_only(run_in_tmpdir, endf_data): photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' data = openmc.data.IncidentPhoton.from_endf(photoatomic_file) data.export_to_hdf5('tmp.h5', 'w') + + +def test_from_endf_material(endf_data): + endf_dir = Path(endf_data) + photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' + relaxation_file = endf_dir / 'atomic_relax' / 'atom-001_H_000.endf' + photoatomic = openmc.data.endf.get_evaluations(photoatomic_file)[0] + relaxation = openmc.data.endf.get_evaluations(relaxation_file)[0] + + data = openmc.data.IncidentPhoton.from_endf(photoatomic, relaxation) + + assert data.atomic_number == 1 + assert 502 in data.reactions + assert data.atomic_relaxation.binding_energy['K'] == pytest.approx(13.61) + + +def test_atomic_relaxation_from_endf_material(endf_data): + filename = Path(endf_data) / 'atomic_relax' / 'atom-001_H_000.endf' + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.AtomicRelaxation.from_endf(material) + + assert data.binding_energy['K'] == pytest.approx(13.61) + assert data.num_electrons['K'] == pytest.approx(1.0) diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index c444d0c58..81ed42b41 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -6,6 +6,8 @@ import random import numpy as np import pytest import openmc.data +from openmc.data.thermal import _THERMAL_NAMES +from openmc.data.njoy import _THERMAL_DATA from . import needs_njoy @@ -146,6 +148,18 @@ def test_h2o_endf(endf_data): '600K', '650K', '800K'] +def test_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + h2o = openmc.data.ThermalScattering.from_endf( + material, divide_incoherent_elastic=True) + + assert not h2o.elastic + assert h2o.atomic_weight_ratio == pytest.approx(0.99917) + assert h2o.temperatures[0] == '294K' + + def test_hzrh_attributes(hzrh): assert hzrh.atomic_weight_ratio == pytest.approx(0.99917) assert hzrh.energy_max == pytest.approx(1.9734) @@ -258,6 +272,27 @@ def test_get_thermal_name(): assert f('boogie_monster') == 'c_boogie_monster' +def test_thermal_names_data_consistency(): + # Check that keys in _THERMAL_NAMES are also in _THERMAL_DATA + names_only = set(_THERMAL_NAMES.keys()) - set(_THERMAL_DATA.keys()) + assert not names_only, f"Keys in _THERMAL_NAMES but not in _THERMAL_DATA: {names_only}" + + # Check that keys in _THERMAL_DATA are also in _THERMAL_NAMES + data_only = set(_THERMAL_DATA.keys()) - set(_THERMAL_NAMES.keys()) + assert not data_only, f"Keys in _THERMAL_DATA but not in _THERMAL_NAMES: {data_only}" + + # Check that the name from each ThermalTuple in _THERMAL_DATA appears as + # a recognized alias in _THERMAL_NAMES for the same key + missing_aliases = [] + for key, thermal_tuple in _THERMAL_DATA.items(): + name = thermal_tuple.name + if name not in _THERMAL_NAMES[key]: + missing_aliases.append((key, name, _THERMAL_NAMES[key])) + assert not missing_aliases, ( + f"ThermalTuple names not in _THERMAL_NAMES aliases: {missing_aliases}" + ) + + @pytest.fixture def fake_mixed_elastic(): fake_tsl = openmc.data.ThermalScattering("c_D_in_7LiD", 1.9968, 4.9, [0.0253]) diff --git a/tests/unit_tests/test_deplete_activation.py b/tests/unit_tests/test_deplete_activation.py index eace1976e..2f1713880 100644 --- a/tests/unit_tests/test_deplete_activation.py +++ b/tests/unit_tests/test_deplete_activation.py @@ -113,6 +113,11 @@ def test_activation(run_in_tmpdir, model, reaction_rate_mode, reaction_rate_opts assert atoms[0] == pytest.approx(n0) assert atoms[1] / atoms[0] == pytest.approx(0.5, rel=tolerance) + # Check that material name is preserved in depletion results + step_result = results[0] + mat_from_results = step_result.get_material(f"{w.id}") + assert mat_from_results.name == 'tungsten' + def test_decay(run_in_tmpdir): """Test decay-only timesteps where no transport solve is performed""" diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index e90b61022..ef8cb9ef2 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -246,6 +246,29 @@ def test_form_matrix(simple_chain): assert new_mat[r, c] == mat[r, c] +def test_decay_matrix(simple_chain): + """Test that decay_matrix contains only radioactive decay terms.""" + # Nuclide order: H1(0), A(1), B(2), C(3) + decay_A = log(2) / 2.36520E+04 + decay_B = log(2) / 3.29040E+04 + + expected = np.zeros((4, 4)) + expected[1, 1] = -decay_A # Loss: A decays + expected[2, 1] = decay_A * 0.6 # A -> B (branching ratio 0.6) + expected[3, 1] = decay_A * 0.4 # A -> C (branching ratio 0.4) + expected[1, 2] = decay_B # B -> A (branching ratio 1.0) + expected[2, 2] = -decay_B # Loss: B decays + + assert np.allclose(expected, simple_chain.decay_matrix.toarray()) + + +def test_decay_matrix_cached(simple_chain): + """Test that decay_matrix is lazily computed and returns the same object.""" + m1 = simple_chain.decay_matrix + m2 = simple_chain.decay_matrix + assert m1 is m2 + + def test_getitem(): """Test nuc_by_ind converter function.""" chain = Chain() diff --git a/tests/unit_tests/test_deplete_cram.py b/tests/unit_tests/test_deplete_cram.py index 8987fbd7a..64cff3a8b 100644 --- a/tests/unit_tests/test_deplete_cram.py +++ b/tests/unit_tests/test_deplete_cram.py @@ -1,12 +1,15 @@ -""" Tests for cram.py +"""Tests for cram.py. Compares a few Mathematica matrix exponentials to CRAM16/CRAM48. +Tests substep accuracy against self-converged reference solutions. """ -from pytest import approx import numpy as np +import pytest import scipy.sparse as sp -from openmc.deplete.cram import CRAM16, CRAM48 +from pytest import approx +from openmc.deplete.cram import (CRAM16, CRAM48, Cram16Solver, Cram48Solver, + IPFCramSolver) def test_CRAM16(): @@ -35,3 +38,63 @@ def test_CRAM48(): z0 = np.array((0.904837418035960, 0.576799023327476)) assert z == approx(z0) + + +def test_substeps1_matches_original(): + """substeps=1 must be bitwise identical to original spsolve path.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 0.1 + + z_orig = CRAM48(mat, x, dt) + z_sub1 = CRAM48(mat, x, dt, substeps=1) + + np.testing.assert_array_equal(z_sub1, z_orig) + + +def test_substeps2_matches_two_half_steps(): + """substeps=2 must match two independent CRAM calls with dt/2.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 1.0 + + # Two manual half-steps using original spsolve path + z_half = CRAM48(mat, x, dt / 2) + z_two = CRAM48(mat, z_half, dt / 2) + + # Single call with substeps=2 + z_sub2 = CRAM48(mat, x, dt, substeps=2) + + assert z_sub2 == approx(z_two, rel=1e-12) + + +@pytest.mark.parametrize("substeps", [0, -1]) +def test_invalid_substeps(substeps): + """substeps must be a positive integer at call time.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + + with pytest.raises(ValueError, match="substeps"): + CRAM48(mat, x, 0.1, substeps=substeps) + + +def test_substeps_self_convergence(): + """Increasing substeps converges toward reference solution. + + Uses CRAM16 (alpha0 ~ 2e-16) where substep convergence is visible. + CRAM48 (alpha0 ~ 2e-47) is already near machine precision for small + systems; its correctness is verified by the other substep tests. + """ + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + x = np.array([1.0, 1.0]) + dt = 50 # lambda*dt = 50 and 150, stresses CRAM16 + + n_ref = CRAM16(mat, x, dt, substeps=128) + + prev_err = np.inf + for s in [1, 2, 4, 8, 16]: + n_s = CRAM16(mat, x, dt, substeps=s) + err = np.linalg.norm(n_s - n_ref) / np.linalg.norm(n_ref) + assert err < prev_err, \ + f"substeps={s} error {err:.2e} not less than previous {prev_err:.2e}" + prev_err = err diff --git a/tests/unit_tests/test_deplete_decay.py b/tests/unit_tests/test_deplete_decay.py index 6e7b0b101..db96fcbe2 100644 --- a/tests/unit_tests/test_deplete_decay.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -82,3 +82,8 @@ def test_deplete_decay_step_fissionable(run_in_tmpdir): _, u238 = results.get_atoms(f"{mat.id}", "U238") assert u238[1] == pytest.approx(original_atoms) + + # Check that material name is preserved in depletion results + step_result = results[0] + mat_from_results = step_result.get_material(f"{mat.id}") + assert mat_from_results.name == 'I do not decay.' diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 6463eaa20..ec886e707 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -19,7 +19,7 @@ from openmc.mpi import comm from openmc.deplete import ( ReactionRates, StepResult, Results, OperatorResult, PredictorIntegrator, CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator, - LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram) + LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram, pool) from tests import dummy_operator @@ -59,8 +59,9 @@ def test_results_save(run_in_tmpdir): burn_list = full_burn_list[2*comm.rank: 2*comm.rank + 2] nuc_list = ["na", "nb"] + name_list = {mat: "" for mat in full_burn_list} op.get_results_info.return_value = ( - vol_dict, nuc_list, burn_list, full_burn_list) + vol_dict, nuc_list, burn_list, full_burn_list, name_list) # Construct end-of-step concentrations x1 = [rng.random(2), rng.random(2)] @@ -130,7 +131,8 @@ def test_results_save_without_rates(run_in_tmpdir): vol_dict = {"0": 1.0} nuc_list = ["na"] burn_list = ["0"] - op.get_results_info.return_value = (vol_dict, nuc_list, burn_list, burn_list) + name_list = {mat: "" for mat in burn_list} + op.get_results_info.return_value = (vol_dict, nuc_list, burn_list, burn_list, name_list) x = [np.array([1.0])] rates = ReactionRates(burn_list, nuc_list, ["ra"]) @@ -181,18 +183,42 @@ def test_bad_integrator_inputs(): with pytest.raises(TypeError, match=".*callable.*NoneType"): PredictorIntegrator(op, timesteps, power=1, solver=None) - with pytest.raises(ValueError, match=".*arguments"): + with pytest.raises(ValueError, match="four arguments"): PredictorIntegrator(op, timesteps, power=1, solver=mock_bad_solver_nargs) + with pytest.raises(ValueError, match="default to 1"): + PredictorIntegrator(op, timesteps, power=1, + solver=mock_bad_solver_fourth_required) -def mock_good_solver(A, n, t): - pass + with pytest.raises(ValueError, match="substeps"): + PredictorIntegrator(op, timesteps, power=1, substeps=0) + + with pytest.raises(ValueError, match="substeps"): + PredictorIntegrator(op, timesteps, power=1, substeps=-1) + + +def mock_good_solver(A, n, t, substeps=1): + return n.copy() + + +def mock_good_solver_substeps(A, n, t, substeps=1): + return n + substeps + + +def mock_unsupported_substeps_solver(A, n, t, substeps=1): + if substeps > 1: + raise NotImplementedError("substeps > 1 not supported") + return n.copy() def mock_bad_solver_nargs(A, n): pass +def mock_bad_solver_fourth_required(A, n, t, substeps): + pass + + @pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) def test_integrator(run_in_tmpdir, scheme): """Test the integrators against their expected values""" @@ -224,13 +250,71 @@ def test_integrator(run_in_tmpdir, scheme): integrator = bundle.solver(operator, [0.75], 1, solver="cram16") assert integrator.solver is cram.CRAM16 + integrator = bundle.solver(operator, [0.75], 1, solver=cram.Cram48Solver, + substeps=2) + assert integrator.solver is cram.Cram48Solver + assert integrator.substeps == 2 + integrator.solver = mock_good_solver assert integrator.solver is mock_good_solver - lfunc = lambda A, n, t: mock_good_solver(A, n, t) + lfunc = lambda A, n, t, substeps=1: mock_good_solver(A, n, t, substeps) integrator.solver = lfunc assert integrator.solver is lfunc + integrator.solver = mock_good_solver_substeps + assert integrator.solver is mock_good_solver_substeps + + +def test_custom_solver_with_default_substeps(monkeypatch): + operator = dummy_operator.DummyOperator() + n = operator.initial_condition() + rates = operator(n, 1.0).rates + integrator = PredictorIntegrator( + operator, [0.75], power=1.0, solver=mock_good_solver) + monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False) + + _, result = integrator._timed_deplete(n, rates, 0.75) + + np.testing.assert_array_equal(result[0], n[0]) + + +def test_substep_aware_custom_solver_receives_substeps(monkeypatch): + operator = dummy_operator.DummyOperator() + n = operator.initial_condition() + rates = operator(n, 1.0).rates + integrator = PredictorIntegrator( + operator, [0.75], power=1.0, solver=mock_good_solver_substeps, + substeps=3) + monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False) + + _, result = integrator._timed_deplete(n, rates, 0.75) + + np.testing.assert_array_equal(result[0], n[0] + 3) + + +def test_custom_solver_propagates_substeps_error(monkeypatch): + operator = dummy_operator.DummyOperator() + n = operator.initial_condition() + rates = operator(n, 1.0).rates + integrator = PredictorIntegrator( + operator, [0.75], power=1.0, + solver=mock_unsupported_substeps_solver, substeps=2) + monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False) + + with pytest.raises(NotImplementedError, match="not supported"): + integrator._timed_deplete(n, rates, 0.75) + + +def test_custom_solver_requires_four_args(): + op = MagicMock() + op.prev_res = None + op.chain = None + op.heavy_metal = 1.0 + + with pytest.raises(ValueError, match="four arguments"): + PredictorIntegrator(op, [1], power=1, solver=mock_bad_solver_nargs) + @pytest.mark.parametrize("integrator", INTEGRATORS) def test_timesteps(integrator): diff --git a/tests/unit_tests/test_deplete_keff_search_control.py b/tests/unit_tests/test_deplete_keff_search_control.py new file mode 100644 index 000000000..425b8f840 --- /dev/null +++ b/tests/unit_tests/test_deplete_keff_search_control.py @@ -0,0 +1,116 @@ +""" Tests for KeffSearchControl class """ + +from pathlib import Path + +import pytest +import numpy as np + +import openmc +import openmc.lib +from openmc.deplete import CoupledOperator + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" + + +def make_model(): + f = openmc.Material(name="fuel") + f.add_element("U", 1, percent_type="ao", enrichment=4.25) + f.add_element("O", 2) + f.set_density("g/cc", 10.4) + f.temperature = 293.15 + + w = openmc.Material(name="water") + w.add_element("O", 1) + w.add_element("H", 2) + w.set_density("g/cc", 1.0) + w.temperature = 293.15 + w.depletable = True + + h = openmc.Material(name='helium') + h.add_element('He', 1) + h.set_density('g/cm3', 0.001598) + + radii = [0.42, 0.45] + height = 0.5 + + f.volume = np.pi * radii[0] ** 2 * height + w.volume = np.pi * (radii[1]**2 - radii[0]**2) * height/2 + + materials = openmc.Materials([f, w, h]) + + surf_interface = openmc.ZPlane(z0=0) + surf_top = openmc.ZPlane(z0=height/2) + surf_bot = openmc.ZPlane(z0=-height/2) + surf_in = openmc.Sphere(r=radii[0]) + surf_out = openmc.Sphere(r=radii[1], boundary_type='vacuum') + + cell_water = openmc.Cell(fill=w, region=-surf_interface) + cell_helium = openmc.Cell(fill=h, region=+surf_interface) + universe = openmc.Universe(cells=(cell_water, cell_helium)) + cell_fuel = openmc.Cell(name='fuel_cell', fill=f, + region=-surf_in & -surf_top & +surf_bot) + cell_universe = openmc.Cell(name='universe_cell',fill=universe, + region=+surf_in & -surf_out & -surf_top & +surf_bot) + geometry = openmc.Geometry([cell_fuel, cell_universe]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + + +def translate_cell(position): + """Helper function to translate a cell""" + cell = [c for c in openmc.lib.cells.values() if c.name == 'universe_cell'][0] + openmc.lib.cells[cell.id].translation = [0, 0, position] + return position + + +def rotate_cell(angle): + """Helper function to rotate a cell""" + cell = [c for c in openmc.lib.cells.values() if c.name == 'universe_cell'][0] + openmc.lib.cells[cell.id].rotation = [0, 0, angle] + return angle + + +def set_u235_density(u235_density): + """Helper function to set the U235 density directly""" + fuel = [m for m in openmc.lib.materials.values() if m.name == 'fuel'][0] + nuclides = openmc.lib.materials[fuel.id].nuclides + densities = openmc.lib.materials[fuel.id].densities + u235_idx = nuclides.index('U235') + densities[u235_idx] = u235_density + openmc.lib.materials[fuel.id].set_densities(nuclides, densities) + return u235_density + + +@pytest.mark.parametrize("function, x0, x1, bracket", [ + (translate_cell, -1.0, 1.0, (-5.0, 5.0)), + (rotate_cell, -45.0, 45.0, (-90.0, 90.0)), + (set_u235_density, 0.8, 1.2, (0.5, 1.5)) +]) +def test_integrator_add_keff_search_control(run_in_tmpdir, function, x0, x1, bracket): + """Test adding add_keff_search_control to integrator""" + model = make_model() + operator = CoupledOperator(model, CHAIN_PATH) + integrator = openmc.deplete.PredictorIntegrator( + operator, [1, 1], 0.0, timestep_units='d') + + integrator.add_keff_search_control( + function=function, + x0=x0, + x1=x1, + bracket=bracket, + k_tol=0.1, + output=False, + ) + + assert integrator._keff_search_control.x0 == x0 + assert integrator._keff_search_control.x1 == x1 + assert integrator._keff_search_control.function == function + assert integrator._keff_search_control.search_kwargs['x_min'] == bracket[0] + assert integrator._keff_search_control.search_kwargs['x_max'] == bracket[1] + assert integrator._keff_search_control.search_kwargs['k_tol'] == 0.1 + assert not integrator._keff_search_control.search_kwargs['output'] diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 5762a8511..0b1937fac 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -6,12 +6,15 @@ to a custom file with new depletion_chain node from os import remove from pathlib import Path +from unittest.mock import patch import pytest -from openmc.deplete import MicroXS +import openmc +from openmc.deplete import MicroXS, get_microxs_and_flux import numpy as np ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" +CHAIN_FILE = Path(__file__).parents[1] / "chain_simple.xml" def test_from_array(): @@ -124,3 +127,187 @@ def test_microxs_zero_flux(): # All microscopic cross sections should be zero assert np.all(microxs.data == 0.0) + + +def test_hybrid_tally_setup(): + """In hybrid mode a 1-group RR tally is added alongside the flux tally.""" + # Create a simple model with one material and a few nuclides for testing + model = openmc.Model() + mat = openmc.Material(components={'U235': 1.0, 'O16': 2.0}) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model.geometry = openmc.Geometry([cell]) + model.settings.batches = 2 + model.settings.particles = 10 + + # Define 2-group energy structure for the test + energies = [0., 0.625, 2.0e7] + + # Function to replace Model.run and capture the tallies that were created + captured = {} + def capture_run(**kwargs): + captured['tallies'] = list(model.tallies) + raise StopIteration + + # Call get_microxs_and_flux but replace Model.run with a function that + # captures the tallies and raises StopIteration to exit early + with patch.object(model, 'run', side_effect=capture_run): + with pytest.raises(StopIteration): + get_microxs_and_flux( + model, [mat], + nuclides=['U235', 'O16'], + reactions=['fission', '(n,gamma)'], + energies=energies, + reaction_rate_mode='flux', + reaction_rate_opts={'nuclides': ['U235'], 'reactions': ['fission']}, + chain_file=CHAIN_FILE, + ) + + # Check that both tallies were created with the expected properties + tally_names = [t.name for t in captured['tallies']] + assert 'MicroXS flux 0' in tally_names + assert 'MicroXS RR 0' in tally_names + + # Check that the RR tally has the expected nuclides and reactions + rr = next(t for t in captured['tallies'] if t.name == 'MicroXS RR 0') + assert rr.nuclides == ['U235'] + assert rr.scores == ['fission'] + + # RR tally must use a 1-group energy filter spanning the full energy range + ef = next(f for f in rr.filters if isinstance(f, openmc.EnergyFilter)) + assert len(ef.values) == 2 + assert ef.values[0] == pytest.approx(energies[0]) + assert ef.values[-1] == pytest.approx(energies[-1]) + + +def _simple_model(): + model = openmc.Model() + mat = openmc.Material(components={'H1': 1.0, 'H2': 1.0}, + density=5.0, density_units='g/cm3') + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.run_mode = 'fixed source' + return model, mat + + +def test_hybrid_tally_defaults_to_all_nuclides(run_in_tmpdir): + energies = [0., 0.625, 2.0e7] + kwargs = { + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)', '(n,gamma)'], + 'energies': energies, + 'reaction_rate_mode': 'flux', + 'chain_file': CHAIN_FILE, + } + + model, mat = _simple_model() + default_fluxes, default_micros = get_microxs_and_flux( + model, [mat], reaction_rate_opts={'reactions': ['(n,2n)']}, **kwargs + ) + + model, mat = _simple_model() + explicit_fluxes, explicit_micros = get_microxs_and_flux( + model, [mat], + reaction_rate_opts={ + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)'] + }, + **kwargs + ) + + np.testing.assert_allclose(default_fluxes[0], explicit_fluxes[0]) + np.testing.assert_allclose(default_micros[0].data, explicit_micros[0].data) + assert default_micros[0].nuclides == explicit_micros[0].nuclides + assert default_micros[0].reactions == explicit_micros[0].reactions + + +def test_flux_mode_returns_one_group_flux(run_in_tmpdir): + model, mat = _simple_model() + fluxes, micros = get_microxs_and_flux( + model, [mat], + nuclides=['H1'], + reactions=['(n,2n)'], + energies=[0., 0.625, 2.0e7], + reaction_rate_mode='flux', + chain_file=CHAIN_FILE, + ) + + assert fluxes[0].shape == (1,) + assert micros[0].data.shape == (1, 1, 1) + assert fluxes[0][0] > 0.0 + +# --------------------------------------------------------------------------- +# Tests for MicroXS.merge() +# --------------------------------------------------------------------------- + +def _make_microxs(nuclides, reactions, values, groups=1): + """Helper: build a MicroXS from a flat list of values (nuclide-major order).""" + data = np.array(values, dtype=float).reshape( + len(nuclides), len(reactions), groups) + return MicroXS(data, nuclides, reactions) + + +def test_merge_disjoint(): + """Merging two MicroXS with no overlapping nuclides or reactions.""" + m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'], + [1., 2., 3., 4.]) + m2 = _make_microxs(['Pu239'], ['(n,2n)'], [5.]) + + merged = m1.merge(m2) + + assert merged.nuclides == ['U235', 'U238', 'Pu239'] + assert merged.reactions == ['fission', '(n,gamma)', '(n,2n)'] + assert merged.data.shape == (3, 3, 1) + + # Self data preserved + assert merged['U235', 'fission'] == pytest.approx([1.]) + assert merged['U238', '(n,gamma)'] == pytest.approx([4.]) + # New data from other + assert merged['Pu239', '(n,2n)'] == pytest.approx([5.]) + # Cross-terms that had no data should be zero + assert merged['U235', '(n,2n)'] == pytest.approx([0.]) + assert merged['Pu239', 'fission'] == pytest.approx([0.]) + + +def test_merge_prefer_other(): + """prefer='other': other overwrites shared entries, adds new ones.""" + # m1: U235 and U238, reactions fission and (n,gamma) + m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'], + [1., 2., 3., 4.]) + # m2: only U235, reactions fission (overlap) and (n,2n) (new) + m2 = _make_microxs(['U235'], ['fission', '(n,2n)'], [9., 5.]) + + merged = m1.merge(m2) + + # Nuclide/reaction sets + assert set(merged.nuclides) == {'U235', 'U238'} + assert set(merged.reactions) == {'fission', '(n,gamma)', '(n,2n)'} + + # U235/fission overwritten by m2 + assert merged['U235', 'fission'] == pytest.approx([9.]) + # U235/(n,gamma) untouched + assert merged['U235', '(n,gamma)'] == pytest.approx([2.]) + # New reaction added from m2 + assert merged['U235', '(n,2n)'] == pytest.approx([5.]) + # U238 data preserved + assert merged['U238', 'fission'] == pytest.approx([3.]) + assert merged['U238', '(n,gamma)'] == pytest.approx([4.]) + # U238/(n,2n) not in either → zero + assert merged['U238', '(n,2n)'] == pytest.approx([0.]) + + +def test_merge_prefer_self(): + """prefer='self': shared pairs keep self's value; new entries use other's value.""" + m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'], + [1., 2., 3., 4.]) + m2 = _make_microxs(['U235'], ['fission', '(n,2n)'], [9., 5.]) + + merged = m1.merge(m2, prefer='self') + + # U235/fission: self wins + assert merged['U235', 'fission'] == pytest.approx([1.]) + # U235/(n,2n): other used + assert merged['U235', '(n,2n)'] == pytest.approx([5.]) diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 9a4699a4f..bc1a078b0 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -1,10 +1,12 @@ """Tests the Results class""" -from pathlib import Path from math import inf +from pathlib import Path import numpy as np import pytest + +import openmc import openmc.deplete @@ -37,6 +39,36 @@ def test_get_activity(res): np.testing.assert_allclose(a_xe135, a_xe135_ref) +def test_get_activity_chain_file(res, tmp_path): + """Tests evaluating activity with chain half-life data""" + _, a_endf = res.get_activity("1", by_nuclide=True, chain_file=False) + xe135_endf = np.array([a["Xe135"] for a in a_endf]) + + chain = openmc.deplete.Chain() + xe135 = openmc.deplete.Nuclide("Xe135") + xe135.half_life = openmc.data.half_life("Xe135") / 2.0 + chain.add_nuclide(xe135) + + t_chain, a_chain = res.get_activity("1", by_nuclide=True, chain_file=chain) + xe135_chain = np.array([a["Xe135"] for a in a_chain]) + + t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0]) + np.testing.assert_allclose(t_chain, t_ref) + np.testing.assert_allclose(xe135_chain, 2.0 * xe135_endf) + + chain_path = tmp_path / "chain.xml" + chain.export_to_xml(chain_path) + with openmc.config.patch('chain_file', chain_path): + _, a_config = res.get_activity("1", by_nuclide=True) + xe135_config = np.array([a["Xe135"] for a in a_config]) + np.testing.assert_allclose(xe135_config, xe135_chain) + + stable_chain = openmc.deplete.Chain() + stable_chain.add_nuclide(openmc.deplete.Nuclide("Xe135")) + _, a_stable = res.get_activity("1", by_nuclide=True, chain_file=stable_chain) + assert all(a["Xe135"] == 0.0 for a in a_stable) + + def test_get_atoms(res): """Tests evaluating single nuclide concentration.""" t, n = res.get_atoms("1", "Xe135") @@ -221,3 +253,11 @@ def test_stepresult_get_material(res): densities = mat1.get_nuclide_atom_densities() assert densities['Xe135'] == pytest.approx(1e-14) assert densities['U234'] == pytest.approx(1.00506e-05) + + +def test_stepresult_get_material_mat_id_as_int(res): + # Get material at first timestep using int mat_id + step_result = res[0] + mat1 = step_result.get_material(1) + assert mat1.id == 1 + assert mat1.volume == step_result.volume["1"] diff --git a/tests/unit_tests/test_element.py b/tests/unit_tests/test_element.py index d91cfaf6e..82e526ec3 100644 --- a/tests/unit_tests/test_element.py +++ b/tests/unit_tests/test_element.py @@ -45,11 +45,11 @@ def test_expand_no_isotopes(): def test_expand_ta(): - ref = {'Ta180': 0.01201, 'Ta181': 99.98799} + ref = {'Ta181': 100.0} element = openmc.Element('Ta') for isotope in element.expand(100.0, 'ao'): assert isotope[1] == approx(ref[isotope[0]]) - + def test_expand_exceptions(): """ Test that correct exceptions are raised for invalid input """ diff --git a/tests/unit_tests/test_endf.py b/tests/unit_tests/test_endf.py index 1d4982054..cdd21d7a6 100644 --- a/tests/unit_tests/test_endf.py +++ b/tests/unit_tests/test_endf.py @@ -2,6 +2,17 @@ from openmc.data import endf from pytest import approx +def test_evaluation_from_material(endf_data): + filename = f'{endf_data}/neutrons/n-001_H_001.endf' + material = endf.get_evaluations(filename)[0] + evaluation = endf.Evaluation(material) + + assert evaluation.material == material.MAT + assert evaluation.gnds_name == 'H1' + assert evaluation.section == material.section_text + assert evaluation.reaction_list == material[1, 451]['section_list'] + + def test_float_endf(): assert endf.float_endf('+3.2146') == approx(3.2146) assert endf.float_endf('.12345') == approx(0.12345) diff --git a/tests/unit_tests/test_filter_distribcell.py b/tests/unit_tests/test_filter_distribcell.py new file mode 100644 index 000000000..d5734a2c0 --- /dev/null +++ b/tests/unit_tests/test_filter_distribcell.py @@ -0,0 +1,53 @@ +import openmc +import pandas as pd + + +def test_distribcell_filter_apply_tally_results(run_in_tmpdir): + # Reset IDs to ensure consistent paths + openmc.reset_auto_ids() + + mat = openmc.Material() + mat.add_nuclide("U235", 1.0) + mat.set_density("g/cm3", 1.0) + + # Define 2x2 lattice with a cylinder in each universe + cyl = openmc.ZCylinder(r=1.0) + cell1 = openmc.Cell(fill=mat, region=-cyl) + cell2 = openmc.Cell(fill=None, region=+cyl) + univ = openmc.Universe(cells=[cell1, cell2]) + lattice = openmc.RectLattice() + lattice.lower_left = (-3.0, -3.0) + lattice.pitch = (3.0, 3.0) + lattice.universes = [[univ, univ], [univ, univ]] + box = openmc.model.RectangularPrism(6., 6., boundary_type='reflective') + root_cell = openmc.Cell(region=-box, fill=lattice) + geometry = openmc.Geometry([root_cell]) + + # Create model and add tally with distribcell filter + model = openmc.Model(geometry) + model.settings.batches = 10 + model.settings.particles = 1000 + tally = openmc.Tally() + distribcell_filter = openmc.DistribcellFilter(cell1) + tally.filters = [distribcell_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + # Run OpenMC and apply tally results + model.run(apply_tally_results=True) + + # Check that mean and standard deviation are available on tally + assert tally.mean.shape == (4, 1, 1) + assert tally.std_dev.shape == (4, 1, 1) + + # Make sure paths attribute on filter is correct + assert distribcell_filter.paths == [ + 'u3->c3->l2(0,0)->u1->c1', + 'u3->c3->l2(1,0)->u1->c1', + 'u3->c3->l2(0,1)->u1->c1', + 'u3->c3->l2(1,1)->u1->c1', + ] + + # Check that we can get a DataFrame from the tally + df = tally.get_pandas_dataframe() + assert isinstance(df, pd.DataFrame) diff --git a/tests/unit_tests/test_filter_mesh.py b/tests/unit_tests/test_filter_mesh.py index a8bd4996d..e26bea337 100644 --- a/tests/unit_tests/test_filter_mesh.py +++ b/tests/unit_tests/test_filter_mesh.py @@ -259,3 +259,154 @@ def test_get_reshaped_data(run_in_tmpdir): assert data1.shape == (2, 19*3*2, 1, 1) assert data2.shape == (2, 19, 3, 2, 1, 1) + +def test_mesh_filter_rotation_roundtrip(run_in_tmpdir): + """Test that MeshFilter rotation works as expected""" + + + mesh = openmc.RegularMesh() + mesh.lower_left = [-10, -10, -10] + mesh.upper_right = [10, 10, 10] + mesh.dimension = [2, 3, 4] + + # check that rotatoin is round-tripped correctly for a set of angles + mesh_filter = openmc.MeshFilter(mesh) + mesh_filter.rotation = [0, 0, 90] # Rotate around z-axis by 90 degrees + + elem = mesh_filter.to_xml_element() + mesh_filter_xml = openmc.MeshFilter.from_xml_element(elem, meshes={mesh.id: mesh}) + assert all(mesh_filter_xml.rotation == mesh_filter.rotation) + + # check that rotation matrix is round-tripped correctly for a rotation matrix + mesh_filter.rotation = np.array([[0.7071, 0, 0.7071], + [0, 1, 0], + [-0.7071, 0, 0.7071]]) + + elem = mesh_filter.to_xml_element() + mesh_filter_xml = openmc.MeshFilter.from_xml_element(elem, meshes={mesh.id: mesh}) + assert np.allclose(mesh_filter_xml.rotation, mesh_filter.rotation) + + +def test_mesh_filter_dataframe_regular_3d(): + """Test MeshFilter.get_pandas_dataframe with a 3D RegularMesh.""" + mesh = openmc.RegularMesh() + mesh.lower_left = [0, 0, 0] + mesh.upper_right = [3, 4, 5] + mesh.dimension = [3, 4, 5] + + f = openmc.MeshFilter(mesh) + data_size = 3 * 4 * 5 + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') in df.columns + assert (mesh_key, 'z') in df.columns + assert len(df) == data_size + # x varies fastest (stride=1), then y (stride=3), then z (stride=12) + assert list(df[(mesh_key, 'x')][:3]) == [1, 2, 3] + assert df[(mesh_key, 'y')].iloc[0] == 1 + assert df[(mesh_key, 'y')].iloc[3] == 2 + + +def test_mesh_filter_dataframe_regular_2d(): + """Test MeshFilter.get_pandas_dataframe with a 2D RegularMesh.""" + mesh = openmc.RegularMesh() + mesh.lower_left = [0, 0] + mesh.upper_right = [2, 3] + mesh.dimension = [2, 3] + + f = openmc.MeshFilter(mesh) + data_size = 2 * 3 + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') in df.columns + # Should NOT have a z column for a 2D mesh + assert (mesh_key, 'z') not in df.columns + assert len(df) == data_size + + +def test_mesh_filter_dataframe_regular_1d(): + """Test MeshFilter.get_pandas_dataframe with a 1D RegularMesh.""" + mesh = openmc.RegularMesh() + mesh.lower_left = [0] + mesh.upper_right = [5] + mesh.dimension = [5] + + f = openmc.MeshFilter(mesh) + data_size = 5 + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') not in df.columns + assert (mesh_key, 'z') not in df.columns + assert len(df) == data_size + assert list(df[(mesh_key, 'x')]) == [1, 2, 3, 4, 5] + + +def test_mesh_filter_dataframe_cylindrical(): + """Test MeshFilter.get_pandas_dataframe with a CylindricalMesh.""" + mesh = openmc.CylindricalMesh( + r_grid=[0.0, 1.0, 2.0], + phi_grid=[0.0, math.pi], + z_grid=[0.0, 5.0, 10.0] + ) + + f = openmc.MeshFilter(mesh) + nr, nphi, nz = mesh.dimension # 2, 1, 2 + data_size = nr * nphi * nz + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'r') in df.columns + assert (mesh_key, 'phi') in df.columns + assert (mesh_key, 'z') in df.columns + # Should NOT have x, y columns + assert (mesh_key, 'x') not in df.columns + assert (mesh_key, 'y') not in df.columns + assert len(df) == data_size + # r varies fastest + assert list(df[(mesh_key, 'r')][:nr]) == [1, 2] + + +def test_mesh_filter_dataframe_spherical(): + """Test MeshFilter.get_pandas_dataframe with a SphericalMesh.""" + mesh = openmc.SphericalMesh( + r_grid=[0.0, 1.0, 2.0, 3.0], + theta_grid=[0, math.pi], + phi_grid=[0, 2 * math.pi] + ) + + f = openmc.MeshFilter(mesh) + nr, ntheta, nphi = mesh.dimension # 3, 1, 1 + data_size = nr * ntheta * nphi + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'r') in df.columns + assert (mesh_key, 'theta') in df.columns + assert (mesh_key, 'phi') in df.columns + assert (mesh_key, 'x') not in df.columns + assert len(df) == data_size + + +def test_mesh_filter_dataframe_rectilinear(): + """Test MeshFilter.get_pandas_dataframe with a RectilinearMesh.""" + mesh = openmc.RectilinearMesh() + mesh.x_grid = [0.0, 1.0, 2.0] + mesh.y_grid = [0.0, 1.0, 2.0, 3.0] + mesh.z_grid = [0.0, 5.0] + + f = openmc.MeshFilter(mesh) + nx, ny, nz = mesh.dimension # 2, 3, 1 + data_size = nx * ny * nz + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') in df.columns + assert (mesh_key, 'z') in df.columns + assert len(df) == data_size diff --git a/tests/unit_tests/test_filter_reaction.py b/tests/unit_tests/test_filter_reaction.py new file mode 100644 index 000000000..df8fc9d7b --- /dev/null +++ b/tests/unit_tests/test_filter_reaction.py @@ -0,0 +1,87 @@ +import pytest +import openmc + + +def test_reaction_filter_construction_with_strings(): + f = openmc.ReactionFilter(['(n,elastic)', '(n,gamma)']) + assert len(f.bins) == 2 + assert f.bins[0] == '(n,elastic)' + assert f.bins[1] == '(n,gamma)' + + +def test_reaction_filter_construction_with_mt(): + f = openmc.ReactionFilter([2, 102]) + assert len(f.bins) == 2 + assert f.bins[0] == '(n,elastic)' + assert f.bins[1] == '(n,gamma)' + + +def test_reaction_filter_mixed(): + f = openmc.ReactionFilter([2, '(n,gamma)']) + assert f.bins[0] == '(n,elastic)' + assert f.bins[1] == '(n,gamma)' + + +def test_reaction_filter_single_bin_string(): + f = openmc.ReactionFilter('(n,elastic)') + assert len(f.bins) == 1 + assert f.bins[0] == '(n,elastic)' + + +def test_reaction_filter_single_bin_mt(): + f = openmc.ReactionFilter(2) + assert len(f.bins) == 1 + assert f.bins[0] == '(n,elastic)' + + +def test_reaction_filter_single_bin_naming(): + f = openmc.ReactionFilter('total') + assert len(f.bins) == 1 + assert f.bins[0] == '(n,total)' + + +def test_reaction_filter_invalid_mt(): + with pytest.raises(ValueError, match="No known reaction"): + openmc.ReactionFilter([999999]) + + +def test_reaction_filter_invalid_string(): + with pytest.raises(ValueError, match="Unknown reaction name"): + openmc.ReactionFilter(['not-a-reaction']) + + +def test_reaction_filter_invalid_type(): + with pytest.raises(TypeError, match="Expected str or int"): + openmc.ReactionFilter([3.14]) + + +def test_reaction_filter_xml_roundtrip(): + f = openmc.ReactionFilter([2, 102], filter_id=42) + elem = f.to_xml_element() + f2 = openmc.ReactionFilter.from_xml_element(elem) + assert f2.id == 42 + assert len(f2.bins) == 2 + assert f2.bins[0] == '(n,elastic)' + assert f2.bins[1] == '(n,gamma)' + + +def test_reaction_filter_num_bins(): + f = openmc.ReactionFilter(['(n,elastic)', '(n,fission)', '(n,gamma)']) + assert f.num_bins == 3 + + +def test_reaction_filter_repr(): + f = openmc.ReactionFilter([2, 102]) + r = repr(f) + assert 'ReactionFilter' in r + + +def test_reaction_filter_short_name(): + assert openmc.ReactionFilter.short_name == 'Reaction' + + +def test_reaction_filter_total_warning(): + """Test that using 'total' emits a warning about ambiguity.""" + with pytest.warns(UserWarning, match="ambiguous"): + f = openmc.ReactionFilter(['total']) + assert f.bins[0] == '(n,total)' diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 8c56a310e..2e7481c87 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -17,14 +17,16 @@ def box_model(): model.settings.particles = 100 model.settings.batches = 10 model.settings.inactive = 0 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Point()) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point()) return model def test_cell_instance(): c1 = openmc.Cell() c2 = openmc.Cell() - f = openmc.CellInstanceFilter([(c1, 0), (c1, 1), (c1, 2), (c2, 0), (c2, 1)]) + f = openmc.CellInstanceFilter( + [(c1, 0), (c1, 1), (c1, 2), (c2, 0), (c2, 1)]) # Make sure __repr__ works repr(f) @@ -234,7 +236,8 @@ def test_first_moment(run_in_tmpdir, box_model): flux, scatter = sp.tallies[plain_tally.id].mean.ravel() # Check that first moment matches - first_score = lambda t: sp.tallies[t.id].mean.ravel()[0] + def first_score(t): + return sp.tallies[t.id].mean.ravel()[0] assert first_score(leg_tally) == scatter assert first_score(leg_sptl_tally) == scatter assert first_score(sph_scat_tally) == scatter @@ -258,7 +261,8 @@ def test_lethargy_bin_width(): assert len(f.lethargy_bin_width) == 175 energy_bins = openmc.mgxs.GROUP_STRUCTURES['VITAMIN-J-175'] assert f.lethargy_bin_width[0] == np.log10(energy_bins[1]/energy_bins[0]) - assert f.lethargy_bin_width[-1] == np.log10(energy_bins[-1]/energy_bins[-2]) + assert f.lethargy_bin_width[-1] == np.log10( + energy_bins[-1]/energy_bins[-2]) def test_energyfunc(): @@ -292,7 +296,8 @@ def test_tabular_from_energyfilter(): # 'histogram' is the default assert tab.interpolation == 'histogram' - tab = efilter.get_tabular(values=np.array([10, 10, 5]), interpolation='linear-linear') + tab = efilter.get_tabular(values=np.array( + [10, 10, 5]), interpolation='linear-linear') assert tab.interpolation == 'linear-linear' @@ -314,6 +319,100 @@ def test_energy_filter(): openmc.EnergyFilter([-1.2, 0.25, 0.5]) +def test_particle_production_filter(): + energy_bins = [1e3, 1e4, 1e5, 1e6] + + # --- Single particle with energy bins --- + f = openmc.ParticleProductionFilter('photon', energy_bins) + + # particles getter always returns a list + assert isinstance(f.particles, list) + assert len(f.particles) == 1 + assert f.particles[0] == openmc.ParticleType.PHOTON + + assert f.num_energy_bins == 3 + assert f.num_bins == 3 + assert f.shape == (1, 3) + assert len(f.bins) == 3 + # Each bin is (particle_name, e_low, e_high) + assert f.bins[0] == ('photon', 1e3, 1e4) + assert f.bins[2] == ('photon', 1e5, 1e6) + + # __repr__ check + repr(f) + + # to_xml_element() + elem = f.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'particleproduction' + assert elem.find('particles').text == 'photon' + assert elem.find('energies').text.split()[0] == str(energy_bins[0]) + + # from_xml_element() + new_f = openmc.Filter.from_xml_element(elem) + assert new_f.id == f.id + assert new_f.particles == f.particles + assert np.allclose(new_f.energies, f.energies) + + # pandas output (with energy bins -> 3 MultiIndex columns) + df = f.get_pandas_dataframe(data_size=3, stride=1) + assert df.shape[0] == 3 + assert ('particleproduction', 'particle') in df.columns + assert ('particleproduction', 'energy low [eV]') in df.columns + assert ('particleproduction', 'energy high [eV]') in df.columns + + # --- Multiple particles with energy bins --- + f2 = openmc.ParticleProductionFilter(['photon', 'neutron'], energy_bins) + assert len(f2.particles) == 2 + assert f2.num_bins == 6 # 2 particles * 3 energy bins + assert f2.shape == (2, 3) + assert len(f2.bins) == 6 + # First 3 bins are photon, next 3 are neutron + assert f2.bins[0] == ('photon', 1e3, 1e4) + assert f2.bins[3] == ('neutron', 1e3, 1e4) + + df2 = f2.get_pandas_dataframe(data_size=6, stride=1) + assert df2.shape[0] == 6 + assert list(df2[('particleproduction', 'particle')]) == \ + ['photon'] * 3 + ['neutron'] * 3 + + # XML round-trip + elem2 = f2.to_xml_element() + new_f2 = openmc.Filter.from_xml_element(elem2) + assert len(new_f2.particles) == 2 + assert np.allclose(new_f2.energies, energy_bins) + + # --- Multiple particles without energy bins --- + f3 = openmc.ParticleProductionFilter(['photon', 'neutron', 'electron']) + assert f3.energies is None + assert f3.num_bins == 3 + assert f3.num_energy_bins == 1 + assert f3.shape == (3, 1) + assert f3.bins == ['photon', 'neutron', 'electron'] + + repr(f3) + + df3 = f3.get_pandas_dataframe(data_size=3, stride=1) + assert df3.shape[0] == 3 + assert ('particleproduction', 'particle') in df3.columns + # Should not have energy columns + assert ('particleproduction', 'energy low [eV]') not in df3.columns + + # XML round-trip without energies + elem3 = f3.to_xml_element() + assert elem3.find('energies') is None + new_f3 = openmc.Filter.from_xml_element(elem3) + assert new_f3.energies is None + assert len(new_f3.particles) == 3 + + # --- Energies from group structure name --- + f4 = openmc.ParticleProductionFilter('photon', energies='CCFE-709') + expected = openmc.mgxs.GROUP_STRUCTURES['CCFE-709'] + assert np.allclose(f4.energies, expected) + assert f4.num_energy_bins == len(expected) - 1 + assert f4.num_bins == len(expected) - 1 + + def test_weight(): f = openmc.WeightFilter([0.01, 0.1, 1.0, 10.0]) expected_bins = [[0.01, 0.1], [0.1, 1.0], [1.0, 10.0]] diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index eb4dc3dce..4cfae0df2 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -5,6 +5,7 @@ import os import numpy as np import pytest import openmc +from openmc.examples import random_ray_pin_cell import openmc.exceptions as exc import openmc.lib @@ -83,6 +84,19 @@ def uo2_trigger_model(): yield +@pytest.fixture(scope='module') +def random_ray_pincell_model(): + """Set up a random ray model to test with and delete files when done""" + openmc.reset_auto_ids() + # Write XML and MGXS files in tmpdir + with cdtemp(): + model = random_ray_pin_cell() + model.settings.batches = 200 + model.settings.inactive = 50 + model.settings.particles = 50 + model.export_to_xml() + yield + @pytest.fixture(scope='module') def lib_init(pincell_model, mpi_intracomm): openmc.lib.init(intracomm=mpi_intracomm) @@ -237,9 +251,9 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" - assert m.depletable == False + assert not m.depletable m.depletable = True - assert m.depletable == True + assert m.depletable def test_properties_density(lib_init): @@ -608,6 +622,13 @@ def test_regular_mesh(lib_init): assert isinstance(mesh, openmc.lib.RegularMesh) assert mesh_id == mesh.id + rotation = (180.0, 0.0, 0.0) + + mf = openmc.lib.MeshFilter(mesh) + assert mf.mesh == mesh + mf.rotation = rotation + assert np.allclose(mf.rotation, rotation) + translation = (1.0, 2.0, 3.0) mf = openmc.lib.MeshFilter(mesh) @@ -874,22 +895,23 @@ def test_load_nuclide(lib_init): openmc.lib.load_nuclide('Pu3') +class LegacySlicePlot: + origin = (0.0, 0.0, 0.0) + width = 1.26 + height = 1.26 + basis = 'xy' + h_res = 3 + v_res = 3 + level = -1 + + def test_id_map(lib_init): expected_ids = np.array([[(3, 0, 3), (2, 0, 2), (3, 0, 3)], [(2, 0, 2), (1, 0, 1), (2, 0, 2)], [(3, 0, 3), (2, 0, 2), (3, 0, 3)]], dtype='int32') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - ids = openmc.lib.plot.id_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + ids = openmc.lib.id_map(LegacySlicePlot()) assert np.array_equal(expected_ids, ids) @@ -899,20 +921,81 @@ def test_property_map(lib_init): [ (293.6, 6.55), (293.6, 10.29769), (293.6, 6.55)], [(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)]], dtype='float') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - properties = openmc.lib.plot.property_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + properties = openmc.lib.property_map(LegacySlicePlot()) assert np.allclose(expected_properties, properties, atol=1e-04) +def test_slice_data(lib_init): + expected_properties = np.array( + [[(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)], + [ (293.6, 6.55), (293.6, 10.29769), (293.6, 6.55)], + [(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)]], dtype='float') + origin = (0.0, 0.0, 0.0) + _, properties = openmc.lib.slice_data( + origin, + width=(1.26, 1.26), + basis='xy', + pixels=(3, 3), + include_properties=True + ) + assert np.allclose(expected_properties, properties, atol=1e-04) + + +def test_solid_raytrace_plot(lib_init, pincell_model): + # Ensure plot mapping can be accessed and grows after allocation + n0 = len(openmc.lib.plots) + plot = openmc.lib.SolidRayTracePlot() + assert len(openmc.lib.plots) == n0 + 1 + assert plot.id in openmc.lib.plots + assert openmc.lib.plots[plot.id] is plot + + # Exercise plot property getters/setters + plot.pixels = (8, 6) + assert plot.pixels == (8, 6) + + plot.color_by = openmc.lib.SolidRayTracePlot.COLOR_BY_MATERIAL + assert plot.color_by == openmc.lib.SolidRayTracePlot.COLOR_BY_MATERIAL + + plot.camera_position = (2.0, 0.0, 1.0) + plot.look_at = (0.0, 0.0, 0.0) + plot.up = (0.0, 0.0, 1.0) + plot.light_position = (3.0, 2.0, 4.0) + plot.fov = 60.0 + plot.diffuse_fraction = 0.4 + assert plot.camera_position == pytest.approx((2.0, 0.0, 1.0)) + assert plot.look_at == pytest.approx((0.0, 0.0, 0.0)) + assert plot.up == pytest.approx((0.0, 0.0, 1.0)) + assert plot.light_position == pytest.approx((3.0, 2.0, 4.0)) + assert plot.fov == pytest.approx(60.0) + assert plot.diffuse_fraction == pytest.approx(0.4) + + # Exercise color/visibility CAPI wrappers + plot.set_default_colors() + plot.set_color(1, (12, 34, 56)) + assert plot.get_color(1) == (12, 34, 56) + plot.set_visibility(1, False) + plot.set_visibility(1, True) + + # Confirm image creation path works and dimensions match pixels + plot.update_view() + image = plot.create_image() + assert image.shape == (6, 8, 3) + assert image.dtype == np.uint8 + + # Change some properties and confirm image changes + plot.set_color(1, (255, 0, 0)) + plot.update_view() + image2 = plot.create_image() + assert not np.array_equal(image, image2) + + # Solid raytrace uses Phong/diffuse shading, so rendered RGB values are + # generally modulated and need not exactly match the assigned palette. + changed = np.any(image != image2, axis=2) + assert np.any(changed) + assert np.mean(image2[..., 0][changed]) > np.mean(image[..., 0][changed]) + + def test_position(lib_init): pos = openmc.lib.plot._Position(1.0, 2.0, 3.0) @@ -1039,9 +1122,29 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): assert p1.time == p2.time assert p1.wgt == p2.wgt + # as_array should return a numpy structured array with matching values + arr = openmc.lib.sample_external_source(10, prn_seed=3, as_array=True) + assert isinstance(arr, np.ndarray) + assert len(arr) == 10 + for p, row in zip(particles, arr): + assert p.r == pytest.approx(row['r']) + assert p.E == pytest.approx(row['E']) + openmc.lib.finalize() # Make sure sampling works in volume calculation mode openmc.lib.init(["-c"]) openmc.lib.sample_external_source(100) openmc.lib.finalize() + + +def test_random_ray(random_ray_pincell_model, mpi_intracomm): + openmc.lib.finalize() + openmc.lib.init(intracomm=mpi_intracomm) + openmc.lib.simulation_init() + openmc.lib.run_random_ray() + keff = openmc.lib.keff() + + assert keff[0]==pytest.approx(1.3236826574065745) + + openmc.lib.finalize() diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 2e3724272..12d363376 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -7,7 +7,7 @@ import numpy as np import openmc from openmc.data import decay_photon_energy -from openmc.deplete import Chain +from openmc.deplete import Chain, Nuclide import openmc.examples import openmc.model import openmc.stats @@ -77,6 +77,13 @@ def test_add_components(): with pytest.raises(ValueError): m.add_components({'H1': 1.0}, percent_type = 'oa') + +def test_id(): + openmc.Material(material_id=0) + with pytest.raises(ValueError): + openmc.Material(material_id=-1) + + def test_nuclides_to_ignore(run_in_tmpdir): """Test nuclides_to_ignore when exporting a material to XML""" m = openmc.Material() @@ -481,6 +488,7 @@ def test_borated_water(): # Test the density override m = openmc.model.borated_water(975, 566.5, 15.51, density=0.9) assert m.density == pytest.approx(0.9, 1e-3) + assert m.temperature == pytest.approx(566.5) def test_from_xml(run_in_tmpdir): @@ -553,6 +561,10 @@ def test_get_activity(): m1.add_element("Fe", 0.7) m1.add_element("Li", 0.3) m1.set_density('g/cm3', 1.5) + with pytest.raises(ValueError, match="Volume must be set"): + m1.get_activity(units='Bq') + with pytest.raises(ValueError, match="Volume must be set"): + m1.get_activity(units='Ci') # activity in Bq/cc and Bq/g should not require volume setting assert m1.get_activity(units='Bq/cm3') == 0 assert m1.get_activity(units='Bq/g') == 0 @@ -586,6 +598,8 @@ def test_get_activity(): assert pytest.approx(m4.get_activity(units='Bq/g', by_nuclide=True)["H3"]) == 355978108155965.94 # [Bq/g] assert pytest.approx(m4.get_activity(units='Bq/cm3')) == 355978108155965.94*3/2 # [Bq/cc] assert pytest.approx(m4.get_activity(units='Bq/cm3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2 # [Bq/cc] + assert pytest.approx(m4.get_activity(units='Bq/m3')) == 355978108155965.94*3/2*1e6 # [Bq/m3] + assert pytest.approx(m4.get_activity(units='Bq/m3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2*1e6 # [Bq/m3] # volume is required to calculate total activity m4.volume = 10. assert pytest.approx(m4.get_activity(units='Bq')) == 355978108155965.94*3/2*10 # [Bq] @@ -600,6 +614,35 @@ def test_get_activity(): assert m4.get_activity(units='Ci/m3') == pytest.approx(ci/m3) +def test_get_activity_chain_file(tmp_path): + m = openmc.Material() + m.add_nuclide("H3", 1.0) + m.set_density('g/cm3', 1.0) + + chain = Chain() + h3 = Nuclide("H3") + h3.half_life = 1.0 + chain.add_nuclide(h3) + + atoms_per_bcm = m.get_nuclide_atom_densities()["H3"] + expected = np.log(2.0) * 1e24 * atoms_per_bcm + + assert m.get_activity(chain_file=chain) == pytest.approx(expected) + + chain_path = tmp_path / "chain.xml" + chain.export_to_xml(chain_path) + assert m.get_activity(chain_file=chain_path) == pytest.approx(expected) + + endf_activity = m.get_activity(chain_file=False) + with openmc.config.patch('chain_file', chain_path): + assert m.get_activity() == pytest.approx(expected) + assert m.get_activity(chain_file=False) == pytest.approx(endf_activity) + + stable_chain = Chain() + stable_chain.add_nuclide(Nuclide("H3")) + assert m.get_activity(chain_file=stable_chain) == 0.0 + + def test_get_decay_heat(): # Set chain file for testing openmc.config['chain_file'] = Path(__file__).parents[1] / 'chain_simple.xml' @@ -609,6 +652,8 @@ def test_get_decay_heat(): m1.add_nuclide("U235", 0.2) m1.add_nuclide("U238", 0.8) m1.set_density('g/cm3', 10.5) + with pytest.raises(ValueError, match="Volume must be set"): + m1.get_decay_heat(units='W') # decay heat in W/cc and W/g should not require volume setting assert m1.get_decay_heat(units='W/cm3') == 0 assert m1.get_decay_heat(units='W/g') == 0 @@ -642,6 +687,8 @@ def test_get_decay_heat(): assert pytest.approx(m4.get_decay_heat(units='W/g', by_nuclide=True)["I135"]) == 40175.15720273193 # [W/g] assert pytest.approx(m4.get_decay_heat(units='W/cm3')) == 40175.15720273193*3/2 # [W/cc] assert pytest.approx(m4.get_decay_heat(units='W/cm3', by_nuclide=True)["I135"]) == 40175.15720273193*3/2 #[W/cc] + assert pytest.approx(m4.get_decay_heat(units='W/m3')) == 40175.15720273193*3/2*1e6 # [W/m3] + assert pytest.approx(m4.get_decay_heat(units='W/m3', by_nuclide=True)["I135"]) == 40175.15720273193*3/2*1e6 # [W/m3] # volume is required to calculate total decay heat m4.volume = 10. assert pytest.approx(m4.get_decay_heat(units='W')) == 40175.15720273193*3/2*10 # [W] @@ -672,6 +719,8 @@ def test_decay_photon_energy(): src_per_bqg = m.get_decay_photon_energy(units='Bq/g') src_per_bqkg = m.get_decay_photon_energy(units='Bq/kg') assert pytest.approx(src_per_bqg.integral()) == src_per_bqkg.integral() / 1000. + src_per_bqm3 = m.get_decay_photon_energy(units='Bq/m3') + assert pytest.approx(src_per_bqm3.integral()) == src_per_cm3.integral() * 1e6 # If we add Xe135 (which has a tabular distribution), the photon source # should be a mixture distribution @@ -767,3 +816,109 @@ def test_mean_free_path(): mat2.add_nuclide('Pb208', 1.0) mat2.set_density('g/cm3', 11.34) assert mat2.mean_free_path(energy=14e6) == pytest.approx(5.65, abs=1e-2) + + +def test_material_from_constructor(): + # Test that components and percent_type work in the constructor + components = { + 'Li': {'percent': 0.5, 'enrichment': 60.0, 'enrichment_target': 'Li7'}, + 'O16': 1.0, + 'Be': 0.5 + } + mat = openmc.Material( + material_id=123, + name="test-mat", + components=components, + percent_type="ao" + ) + # Check that nuclides were added + nuclide_names = [nuc.name for nuc in mat.nuclides] + assert 'O16' in nuclide_names + assert 'Be9' in nuclide_names + assert 'Li7' in nuclide_names + assert 'Li6' in nuclide_names + assert mat.id == 123 + assert mat.name == "test-mat" + + mat1 = openmc.Material( + **{ + "material_id": 1, + "name": "neutron_star", + "density": 1e17, + "density_units": "kg/m3", + } + ) + assert mat1.id == 1 + assert mat1.name == "neutron_star" + assert mat1._density == 1e17 + assert mat1._density_units == "kg/m3" + assert mat1.nuclides == [] + + mat2 = openmc.Material( + material_id=42, + name="plasma", + temperature=None, + density=1e-7, + density_units="g/cm3", + ) + assert mat2.id == 42 + assert mat2.name == "plasma" + assert mat2.temperature is None + assert mat2.density == 1e-7 + assert mat2.density_units == "g/cm3" + assert mat2.nuclides == [] + + +def test_get_photon_contact_dose_rate(): + # Set chain file for testing + openmc.config['chain_file'] = Path(__file__).parents[1] / 'chain_simple.xml' + + # A purely stable material (Fe) should give zero dose + m_stable = openmc.Material() + m_stable.add_element('Fe', 1.0) + m_stable.set_density('g/cm3', 7.87) + assert m_stable.get_photon_contact_dose_rate('absorbed-air') == 0.0 + assert m_stable.get_photon_contact_dose_rate('effective') == 0.0 + + # I135 has a Discrete photon source (lines) + m_i135 = openmc.Material() + m_i135.add_nuclide('I135', 1.0) + m_i135.set_density('atom/b-cm', 1.0) + + cdr_abs = m_i135.get_photon_contact_dose_rate('absorbed-air') + cdr_eff = m_i135.get_photon_contact_dose_rate('effective') + assert cdr_abs == pytest.approx(6.091547e10, rel=1e-4) # [Gy/h] + assert cdr_eff == pytest.approx(6.102167e10, rel=1e-4) # [Sv/h] + + # Xe135 has a Tabular photon source (continuous distribution) + m_xe135 = openmc.Material() + m_xe135.add_nuclide('Xe135', 1.0) + m_xe135.set_density('atom/b-cm', 1.0) + + cdr_xe_abs = m_xe135.get_photon_contact_dose_rate('absorbed-air') + cdr_xe_eff = m_xe135.get_photon_contact_dose_rate('effective') + assert cdr_xe_abs == pytest.approx(7.886077e8, rel=1e-4) # [Gy/h] + assert cdr_xe_eff == pytest.approx(9.488298e8, rel=1e-4) # [Sv/h] + + # by_nuclide=True should return a dict whose values sum to the total + cdr_by_nuc = m_i135.get_photon_contact_dose_rate('absorbed-air', by_nuclide=True) + assert isinstance(cdr_by_nuc, dict) + assert 'I135' in cdr_by_nuc + assert sum(cdr_by_nuc.values()) == pytest.approx(cdr_abs) + + # For a mixed material the sum over nuclides must equal the total + m_mix = openmc.Material() + m_mix.add_nuclide('I135', 0.5) + m_mix.add_nuclide('Xe135', 0.5) + m_mix.set_density('atom/b-cm', 1.0) + cdr_mix_total = m_mix.get_photon_contact_dose_rate('absorbed-air') + cdr_mix_nuc = m_mix.get_photon_contact_dose_rate('absorbed-air', by_nuclide=True) + assert sum(cdr_mix_nuc.values()) == pytest.approx(cdr_mix_total) + + # Input validation + with pytest.raises(ValueError): + m_i135.get_photon_contact_dose_rate('invalid-quantity') + with pytest.raises(TypeError): + m_i135.get_photon_contact_dose_rate('absorbed-air', build_up='two') + with pytest.raises(ValueError): + m_i135.get_photon_contact_dose_rate('absorbed-air', build_up=-1.0) diff --git a/tests/unit_tests/test_materials.py b/tests/unit_tests/test_materials.py index 5a382b777..6620402fd 100644 --- a/tests/unit_tests/test_materials.py +++ b/tests/unit_tests/test_materials.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + import openmc from openmc.deplete import Chain @@ -78,3 +80,50 @@ def test_export_duplicate_materials_to_xml(run_in_tmpdir): materials_in = openmc.Materials.from_xml("materials.xml") assert len(materials_in) == 2 + + +def test_materials_deplete_length_mismatch(): + mats = openmc.Materials([openmc.Material()]) + + with pytest.raises(ValueError, match="multigroup_fluxes length"): + mats.deplete( + multigroup_fluxes=[], + energy_group_structures=["VITAMIN-J-42"], + timesteps=[1.0], + source_rates=1.0, + ) + + with pytest.raises(ValueError, match="energy_group_structures length"): + mats.deplete( + multigroup_fluxes=[[1.0]], + energy_group_structures=[], + timesteps=[1.0], + source_rates=1.0, + ) + + +def test_materials_deplete_missing_volume(monkeypatch): + mat = openmc.Material() + mat.add_nuclide("Ni58", 1.0) + mat.set_density("g/cm3", 7.87) + + mats = openmc.Materials([mat]) + + class DummySession: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + monkeypatch.setattr(openmc.lib, "TemporarySession", DummySession) + + chain = Path(__file__).parents[1] / "chain_ni.xml" + with pytest.raises(ValueError, match="has no volume"): + mats.deplete( + multigroup_fluxes=[[1.0]], + energy_group_structures=["VITAMIN-J-42"], + timesteps=[1.0], + source_rates=1.0, + chain_file=chain, + ) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 9aca8b596..9b1469fc5 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,6 +1,8 @@ -from math import pi +from math import pi, sqrt from tempfile import TemporaryDirectory from pathlib import Path +import itertools +import random import h5py import numpy as np @@ -488,13 +490,28 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type): simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename) -@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.") -def test_write_vtkhdf(request, run_in_tmpdir): +vtkhdf_tests = [ + ( + Path("test_mesh_dagmc_tets.vtk"), + "moab" + ), + ( + Path("test_mesh_hexes.exo"), + "libmesh" + ) +] +@pytest.mark.parametrize('mesh_file, mesh_library', vtkhdf_tests) +def test_write_vtkhdf(mesh_file, mesh_library, request, run_in_tmpdir): """Performs a minimal UnstructuredMesh simulation, reads in the resulting statepoint file and writes the mesh data to vtk and vtkhdf files. It is necessary to read in the unstructured mesh from a statepoint file to ensure it has all the required attributes """ + if mesh_library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip("DAGMC not enabled.") + if mesh_library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip("LibMesh not enabled.") + model = openmc.Model() surf1 = openmc.Sphere(r=1000.0, boundary_type="vacuum") @@ -502,8 +519,8 @@ def test_write_vtkhdf(request, run_in_tmpdir): model.geometry = openmc.Geometry([cell1]) umesh = openmc.UnstructuredMesh( - request.path.parent / "test_mesh_dagmc_tets.vtk", - "moab", + request.path.parent / mesh_file, + mesh_library, mesh_id = 1 ) mesh_filter = openmc.MeshFilter(umesh) @@ -512,6 +529,8 @@ def test_write_vtkhdf(request, run_in_tmpdir): mesh_tally = openmc.Tally(name="test_tally") mesh_tally.filters = [mesh_filter] mesh_tally.scores = ["flux"] + if mesh_library == "libmesh": + mesh_tally.estimator = "collision" model.tallies = [mesh_tally] @@ -554,6 +573,26 @@ def test_write_vtkhdf(request, run_in_tmpdir): with h5py.File("test_mesh.vtkhdf", "r"): ... + import vtk + reader = vtk.vtkHDFReader() + reader.SetFileName("test_mesh.vtkhdf") + reader.Update() + + # Get mean from file and make sure it matches original data + num_elements = reader.GetOutput().GetNumberOfCells() + assert num_elements == umesh_from_sp.n_elements + + num_vertices = reader.GetOutput().GetNumberOfPoints() + assert num_vertices == umesh_from_sp.vertices.shape[0] + + arr = reader.GetOutput().GetCellData().GetArray("mean") + mean = np.array([arr.GetTuple1(i) for i in range(my_tally.mean.size)]) + np.testing.assert_almost_equal(mean, my_tally.mean.flatten()/umesh_from_sp.volumes) + + arr = reader.GetOutput().GetCellData().GetArray("std_dev") + std_dev = np.array([arr.GetTuple1(i) for i in range(my_tally.std_dev.size)]) + np.testing.assert_almost_equal(std_dev, my_tally.std_dev.flatten()/umesh_from_sp.volumes) + def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" @@ -610,6 +649,7 @@ def test_mesh_get_homogenized_materials(): @pytest.fixture def sphere_model(): + openmc.reset_auto_ids() # Model with three materials separated by planes x=0 and z=0 mats = [] for i in range(3): @@ -691,6 +731,49 @@ def test_mesh_material_volumes_serialize(): assert new_volumes.by_element(3) == [(2, 1.0)] +def test_mesh_material_volumes_serialize_with_bboxes(): + materials = np.array([ + [1, -1, -2], + [-1, -2, -2], + [2, 1, -2], + [2, -2, -2] + ]) + volumes = np.array([ + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0], + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0] + ]) + + # (xmin, ymin, zmin, xmax, ymax, zmax) + bboxes = np.empty((4, 3, 6)) + bboxes[..., 0:3] = np.inf + bboxes[..., 3:6] = -np.inf + bboxes[0, 0] = [-1.0, -2.0, -3.0, 1.0, 2.0, 3.0] # material 1 + bboxes[0, 1] = [-5.0, -6.0, -7.0, 5.0, 6.0, 7.0] # void + bboxes[1, 0] = [0.0, 0.0, 0.0, 10.0, 1.0, 2.0] # void + bboxes[2, 0] = [-1.0, -1.0, -1.0, 0.0, 0.0, 0.0] # material 2 + bboxes[2, 1] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0] # material 1 + bboxes[3, 0] = [-2.0, -2.0, -2.0, 2.0, 2.0, 2.0] # material 2 + + mmv = openmc.MeshMaterialVolumes(materials, volumes, bboxes) + with TemporaryDirectory() as tmpdir: + path = f'{tmpdir}/volumes_bboxes.npz' + mmv.save(path) + loaded = openmc.MeshMaterialVolumes.from_npz(path) + + assert loaded.has_bounding_boxes + first = loaded.by_element(0, include_bboxes=True)[0][2] + assert isinstance(first, openmc.BoundingBox) + np.testing.assert_array_equal(first.lower_left, (-1.0, -2.0, -3.0)) + np.testing.assert_array_equal(first.upper_right, (1.0, 2.0, 3.0)) + + second = loaded.by_element(0, include_bboxes=True)[1][2] + assert isinstance(second, openmc.BoundingBox) + np.testing.assert_array_equal(second.lower_left, (-5.0, -6.0, -7.0)) + np.testing.assert_array_equal(second.upper_right, (5.0, 6.0, 7.0)) + + def test_mesh_material_volumes_boundary_conditions(sphere_model): """Test the material volumes method using a regular mesh that overlaps with a vacuum boundary condition.""" @@ -718,6 +801,53 @@ def test_mesh_material_volumes_boundary_conditions(sphere_model): assert evaluated[1] == pytest.approx(expected[1], rel=1e-2) +def test_mesh_material_volumes_bounding_boxes(): + # Create a model with 8 spherical cells at known locations with random radii + box = openmc.model.RectangularParallelepiped( + -10, 10, -10, 10, -10, 10, boundary_type='vacuum') + + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + + sph_cells = [] + for x, y, z in itertools.product((-5., 5.), repeat=3): + mat_i = mat.clone() + sph = openmc.Sphere(x, y, z, r=random.uniform(0.5, 1.5)) + sph_cells.append(openmc.Cell(region=-sph, fill=mat_i)) + background = openmc.Cell(region=-box & openmc.Intersection([~c.region for c in sph_cells])) + + model = openmc.Model() + model.geometry = openmc.Geometry(sph_cells + [background]) + model.settings.particles = 1000 + model.settings.batches = 10 + + # Create a one-element mesh that encompasses the entire geometry + mesh = openmc.RegularMesh() + mesh.lower_left = (-10., -10., -10.) + mesh.upper_right = (10., 10., 10.) + mesh.dimension = (1, 1, 1) + + # Run material volume calculation with bounding boxes + n_samples = (400, 400, 400) + mmv = mesh.material_volumes(model, n_samples, max_materials=10, bounding_boxes=True) + assert mmv.has_bounding_boxes + + # Create a mapping of material ID to bounding box + bbox_by_mat = { + mat_id: bbox + for mat_id, vol, bbox in mmv.by_element(0, include_bboxes=True) + if mat_id is not None and vol > 0.0 + } + + # Match the mesh ray spacing used for the bounding box estimator. + tol = 0.5 * mesh.bounding_box.width[0] / n_samples[0] + for cell in sph_cells: + bbox = bbox_by_mat[cell.fill.id] + cell_bbox = cell.bounding_box + np.testing.assert_allclose(bbox.lower_left, cell_bbox.lower_left, atol=tol) + np.testing.assert_allclose(bbox.upper_right, cell_bbox.upper_right, atol=tol) + + def test_raytrace_mesh_infinite_loop(run_in_tmpdir): # Create a model with one large spherical cell sphere = openmc.Sphere(r=100, boundary_type='vacuum') @@ -828,3 +958,199 @@ def test_filter_time_mesh(run_in_tmpdir): f"Collision vs tracklength tallies disagree: chi2={chi2_stat:.2f} " f">= {crit=:.2f} ({dof=}, {alpha=})" ) + + +def test_regular_mesh_get_indices_at_coords(): + """Test get_indices_at_coords method for RegularMesh""" + # Create a 10x10x10 mesh from (0,0,0) to (1,1,1) + # Each voxel is 0.1 x 0.1 x 0.1 + mesh = openmc.RegularMesh() + mesh.lower_left = (0, 0, 0) + mesh.upper_right = (1, 1, 1) + mesh.dimension = [10, 10, 10] + + # Test lower-left corner maps to first voxel (0, 0, 0) + assert mesh.get_indices_at_coords([0.0, 0.0, 0.0]) == (0, 0, 0) + + # Test centroid of first voxel + # Voxel 0 spans [0.0, 0.1], so centroid is at 0.05 + assert mesh.get_indices_at_coords([0.05, 0.05, 0.05]) == (0, 0, 0) + + # Test centroid of last voxel maps correctly + # Voxel 9 spans [0.9, 1.0], so centroid is at 0.95 + assert mesh.get_indices_at_coords([0.95, 0.95, 0.95]) == (9, 9, 9) + + # Test a middle voxel + # Voxel 4 spans [0.4, 0.5], so 0.45 should map to it + assert mesh.get_indices_at_coords([0.45, 0.45, 0.45]) == (4, 4, 4) + + # Test mixed indices + assert mesh.get_indices_at_coords([0.05, 0.45, 0.95]) == (0, 4, 9) + assert mesh.get_indices_at_coords([0.95, 0.05, 0.45]) == (9, 0, 4) + + # Test coordinates outside mesh bounds raise ValueError + with pytest.raises(ValueError): + mesh.get_indices_at_coords([-0.5, 0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([1.5, 0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, -0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, 1.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, 0.5, -0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, 0.5, 1.5]) + + # Test that results match expected dimensionality (3D mesh returns 3-tuple) + result = mesh.get_indices_at_coords([0.5, 0.5, 0.5]) + assert isinstance(result, tuple) + assert len(result) == 3 + + # Test that indices can be used directly with centroids array + idx = mesh.get_indices_at_coords([0.95, 0.95, 0.95]) + centroid = mesh.centroids[idx] + np.testing.assert_array_almost_equal(centroid, [0.95, 0.95, 0.95]) + + # Test with a 2D mesh + mesh_2d = openmc.RegularMesh() + mesh_2d.lower_left = (0, 0) + mesh_2d.upper_right = (1, 1) + mesh_2d.dimension = [10, 10] + result_2d = mesh_2d.get_indices_at_coords([0.5, 0.5, 999.0]) + assert isinstance(result_2d, tuple) + assert len(result_2d) == 2 + assert result_2d == (5, 5) + + # Test with a 1D mesh + mesh_1d = openmc.RegularMesh() + mesh_1d.lower_left = [0] + mesh_1d.upper_right = [1] + mesh_1d.dimension = [10] + result_1d = mesh_1d.get_indices_at_coords([0.5, 999.0, 999.0]) + assert isinstance(result_1d, tuple) + assert len(result_1d) == 1 + assert result_1d == (5,) + + +def test_rectilinear_mesh_get_indices_at_coords(): + """Test get_indices_at_coords method for RectilinearMesh""" + # Create a 3x2x2 rectilinear mesh with non-uniform spacing + mesh = openmc.RectilinearMesh() + mesh.x_grid = [0., 1., 5., 10.] + mesh.y_grid = [-10., -5., 0.] + mesh.z_grid = [-100., 0., 100.] + + # Test lower-left corner maps to first voxel (0, 0, 0) + assert mesh.get_indices_at_coords([0.0, -10., -100.]) == (0, 0, 0) + + # Test centroid of first voxel + assert mesh.get_indices_at_coords([0.5, -7.5, -50.]) == (0, 0, 0) + + # Test centroid of last voxel maps correctly + assert mesh.get_indices_at_coords([7.5, -2.5, 50.]) == (2, 1, 1) + + # Test upper_right corner maps to last voxel + assert mesh.get_indices_at_coords([10., 0., 100.]) == (2, 1, 1) + + # Test a middle voxel + assert mesh.get_indices_at_coords([2., -5., 0.]) == (1, 1, 1) + + # Test coordinates outside mesh bounds raise ValueError + with pytest.raises(ValueError): + mesh.get_indices_at_coords([-0.5, 0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([1.5, 0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, -0.5, 110.]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, -20., 110.]) + + +def test_SphericalMesh_get_indices_at_coords(): + """Test get_indices_at_coords method for SphericalMesh""" + + # Basic mesh with default phi and theta grids (single angular bin) + mesh = openmc.SphericalMesh(r_grid=(0, 5, 10)) + + assert mesh.get_indices_at_coords([3, 0, 0]) == (0, 0, 0) + assert mesh.get_indices_at_coords([0, 0, 3]) == (0, 0, 0) + assert mesh.get_indices_at_coords([0, 0, -3]) == (0, 0, 0) + assert mesh.get_indices_at_coords([7, 0, 0]) == (1, 0, 0) + assert mesh.get_indices_at_coords([10, 0, 0]) == (1, 0, 0) + + # Out-of-bounds r + with pytest.raises(ValueError): + mesh.get_indices_at_coords([11, 0, 0]) + + mesh2 = openmc.SphericalMesh(r_grid=(2, 5, 10)) + with pytest.raises(ValueError): + mesh2.get_indices_at_coords([1, 0, 0]) + + # Multi-bin angular grids: use points clearly inside bins + mesh3 = openmc.SphericalMesh( + r_grid=(0, 5, 10), + theta_grid=(0, pi/4, pi/2, pi), + phi_grid=(0, pi/2, pi, 3*pi/2, 2*pi) + ) + + # Near z-axis: theta~0 -> bin 0 + assert mesh3.get_indices_at_coords([0.01, 0, 3]) == (0, 0, 0) + + # theta in (0, pi/4) -> bin 0: [1, 0, 2] theta=arccos(2/sqrt(5))~0.46 + assert mesh3.get_indices_at_coords([1, 0, 2]) == (0, 0, 0) + + # theta in (pi/4, pi/2) -> bin 1: [2, 0, 1] theta=arccos(1/sqrt(5))~1.107 + assert mesh3.get_indices_at_coords([2, 0, 1]) == (0, 1, 0) + + # theta in (pi/2, pi) -> bin 2: [1, 0, -2] theta=arccos(-2/sqrt(5))~2.034 + assert mesh3.get_indices_at_coords([1, 0, -2]) == (0, 2, 0) + + # phi in (pi/2, pi) -> bin 1: [-1, 1, 0.5] + assert mesh3.get_indices_at_coords([-1, 1, 0.5]) == (0, 1, 1) + + # phi in (pi, 3*pi/2) -> bin 2: [-1, -1, 0.5] + assert mesh3.get_indices_at_coords([-1, -1, 0.5]) == (0, 1, 2) + + # phi in (3*pi/2, 2*pi) -> bin 3: [1, -1, 0.5] + assert mesh3.get_indices_at_coords([1, -1, 0.5]) == (0, 1, 3) + + # Non-default origin + mesh4 = openmc.SphericalMesh( + r_grid=(0, 5, 10), + origin=(100, 200, 300) + ) + + assert mesh4.get_indices_at_coords([103, 200, 300]) == (0, 0, 0) + assert mesh4.get_indices_at_coords([100, 200, 307]) == (1, 0, 0) + + with pytest.raises(ValueError): + mesh4.get_indices_at_coords([111, 200, 300]) + + # Degenerate case: point at origin with r_grid starting at 0 + mesh5 = openmc.SphericalMesh(r_grid=(0, 5)) + assert mesh5.get_indices_at_coords([0, 0, 0]) == (0, 0, 0) + + # Out-of-bounds theta: restricted theta grid + mesh6 = openmc.SphericalMesh( + r_grid=(0, 10), + theta_grid=(0, pi/4) + ) + with pytest.raises(ValueError): + mesh6.get_indices_at_coords([5, 0, 0]) # theta=pi/2 > pi/4 + + # Out-of-bounds phi: restricted phi grid + mesh7 = openmc.SphericalMesh( + r_grid=(0, 10), + phi_grid=(0, pi/2) + ) + with pytest.raises(ValueError): + mesh7.get_indices_at_coords([-5, 0, 0]) # phi=pi > pi/2 + + # Diagonal point: verify r, theta, phi all computed correctly + r = 6.0 + val = r / sqrt(3) + result = mesh3.get_indices_at_coords([val, val, val]) + assert result[0] == 1 # r=6 in second bin [5, 10] + assert result[1] == 1 # theta=arccos(1/sqrt(3))~0.955, in (pi/4, pi/2) + assert result[2] == 0 # phi=pi/4, in [0, pi/2) diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py index 5b1173126..46d22f0d2 100644 --- a/tests/unit_tests/test_mesh_from_domain.py +++ b/tests/unit_tests/test_mesh_from_domain.py @@ -16,6 +16,31 @@ def test_reg_mesh_from_cell(): assert np.array_equal(mesh.upper_right, cell.bounding_box[1]) +def test_reg_mesh_from_bounding_box(): + """Tests a RegularMesh can be made from a BoundingBox directly.""" + bb = openmc.BoundingBox([-8, -7, -5], [12, 13, 15]) + + mesh = openmc.RegularMesh.from_domain(domain=bb, dimension=[7, 11, 13]) + assert isinstance(mesh, openmc.RegularMesh) + assert np.array_equal(mesh.dimension, (7, 11, 13)) + assert np.array_equal(mesh.lower_left, bb[0]) + assert np.array_equal(mesh.upper_right, bb[1]) + + +def test_rectilinear_mesh_from_bounding_box(): + """Tests a RectilinearMesh can be made from a BoundingBox directly.""" + bb = openmc.BoundingBox([-8, -7, -5], [12, 13, 15]) + + mesh = openmc.RectilinearMesh.from_bounding_box(bb, dimension=[2, 4, 5]) + assert isinstance(mesh, openmc.RectilinearMesh) + assert np.array_equal(mesh.dimension, (2, 4, 5)) + assert np.array_equal(mesh.lower_left, bb[0]) + assert np.array_equal(mesh.upper_right, bb[1]) + assert np.array_equal(mesh.x_grid, [-8., 2., 12.]) + assert np.array_equal(mesh.y_grid, [-7., -2., 3., 8., 13.]) + assert np.array_equal(mesh.z_grid, [-5., -1., 3., 7., 11., 15.]) + + def test_cylindrical_mesh_from_cell(): """Tests a CylindricalMesh can be made from a Cell and the specified dimensions are propagated through.""" diff --git a/tests/unit_tests/test_mesh_hexes.exo b/tests/unit_tests/test_mesh_hexes.exo new file mode 120000 index 000000000..272da92a4 --- /dev/null +++ b/tests/unit_tests/test_mesh_hexes.exo @@ -0,0 +1 @@ +../regression_tests/unstructured_mesh/test_mesh_hexes.exo \ No newline at end of file diff --git a/tests/unit_tests/test_mg_inverse_velocity.py b/tests/unit_tests/test_mg_inverse_velocity.py new file mode 100644 index 000000000..b520e48d1 --- /dev/null +++ b/tests/unit_tests/test_mg_inverse_velocity.py @@ -0,0 +1,59 @@ +import openmc +import numpy as np +import pytest + +@pytest.fixture +def one_group_lib(): + groups = openmc.mgxs.EnergyGroups([0.0, 20.0e6]) + xsdata = openmc.XSdata('slab_mat', groups) + xsdata.order = 0 + xsdata.set_total([0.0]) + xsdata.set_absorption([0.0]) + xsdata.set_scatter_matrix([[[0.0]]]) + + mg_library = openmc.MGXSLibrary(groups) + mg_library.add_xsdata(xsdata) + name = 'mgxs.h5' + mg_library.export_to_hdf5(name) + yield name + +@pytest.fixture +def slab_model(one_group_lib): + model = openmc.Model() + mat = openmc.Material(name='slab_material') + mat.set_density('macro', 1.0) + mat.add_macroscopic('slab_mat') + + model.materials = openmc.Materials([mat]) + model.materials.cross_sections = one_group_lib + + x_min = openmc.XPlane(x0=0.0, boundary_type='vacuum') + x_max = openmc.XPlane(x0=10.0, boundary_type='vacuum') + + y_min = openmc.YPlane(y0=-10.0, boundary_type='vacuum') + y_max = openmc.YPlane(y0=10.0, boundary_type='vacuum') + z_min = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') + z_max = openmc.ZPlane(z0=19.0, boundary_type='vacuum') + + cell = openmc.Cell(fill=mat, region=+z_min & -x_max & +y_min & -y_max & +z_min & -z_max) + model.geometry = openmc.Geometry([cell]) + + model.settings = openmc.Settings() + model.settings.energy_mode = 'multi-group' + model.settings.run_mode = 'fixed source' + model.settings.batches = 3 + model.settings.particles = 10 + + source = openmc.IndependentSource() + source.space = openmc.stats.Point((5.0, 0.0, 0.0)) + model.settings.source = source + return model + +def test_inverse_velocity(run_in_tmpdir, slab_model): + tally = openmc.Tally() + tally.scores = ['flux','inverse-velocity'] + slab_model.tallies = [tally] + slab_model.run(apply_tally_results=True) + inverse_velocity = tally.mean.squeeze()[1]/tally.mean.squeeze()[0] + + assert inverse_velocity == pytest.approx(1.6144e-5, rel=1e-4) diff --git a/tests/unit_tests/test_mgxs_convert_flux.py b/tests/unit_tests/test_mgxs_convert_flux.py new file mode 100644 index 000000000..e235f88e0 --- /dev/null +++ b/tests/unit_tests/test_mgxs_convert_flux.py @@ -0,0 +1,62 @@ +"""Tests for openmc.mgxs.convert_flux_groups function.""" + +import numpy as np +import pytest +from pytest import approx + +import openmc.mgxs + + +def test_coarse_to_fine(): + """Test coarse to fine conversion with flux conservation.""" + source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) + flux_source = np.array([1e8, 2e8]) + + flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target) + + # Check conservation + assert np.sum(flux_target) == approx(np.sum(flux_source)) + assert len(flux_target) == 4 + assert np.all(flux_target >= 0) + + +def test_fine_to_coarse(): + """Test fine to coarse conversion (reverse direction).""" + source = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) + target = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + flux_source = np.array([1e7, 2e7, 3e7, 4e7]) + + flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target) + + assert np.sum(flux_target) == approx(np.sum(flux_source)) + assert len(flux_target) == 2 + + +def test_lethargy_distribution(): + """Test that flux is distributed by lethargy, not linear energy.""" + # Single group from 1 to 100 eV + source = openmc.mgxs.EnergyGroups([1.0, 100.0]) + # Split into two groups: 1-10 eV and 10-100 eV + target = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + flux_source = np.array([1e8]) + + flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target) + + # Each target group spans one decade (ln(10) lethargy each) + # So flux should be split 50/50 by lethargy + assert flux_target[0] == approx(5e7) + assert flux_target[1] == approx(5e7) + + +def test_fns_ccfe709_to_ukaea1102(): + """Test CCFE-709 to UKAEA-1102 conversion with real FNS flux spectrum.""" + from pathlib import Path + flux_file = Path(__file__).parent.parent / 'fns_flux_709.npy' + fns_flux_709 = np.load(flux_file) + + flux_1102 = openmc.mgxs.convert_flux_groups(fns_flux_709, 'CCFE-709', 'UKAEA-1102') + + assert len(flux_1102) == 1102 + assert np.sum(flux_1102) == approx(np.sum(fns_flux_709), rel=1e-10) + assert np.all(flux_1102 >= 0) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index f4f94a47c..f60c458b6 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -7,6 +7,7 @@ import pytest import openmc import openmc.lib +from openmc.plots import id_map_to_rgb @pytest.fixture(scope='function') @@ -73,13 +74,13 @@ def pin_model_attributes(): tal.scores = ['flux', 'fission'] tals.append(tal) - plot1 = openmc.Plot(plot_id=1) + plot1 = openmc.SlicePlot(plot_id=1) plot1.origin = (0., 0., 0.) plot1.width = (pitch, pitch) plot1.pixels = (300, 300) plot1.color_by = 'material' plot1.filename = 'test' - plot2 = openmc.Plot(plot_id=2) + plot2 = openmc.SlicePlot(plot_id=2) plot2.origin = (0., 0., 0.) plot2.width = (pitch, pitch) plot2.pixels = (300, 300) @@ -264,8 +265,8 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Check to see that values are assigned to the C and python representations # First python cell = model.geometry.get_all_cells()[1] - assert cell.temperature == [600.0] - assert cell.density == [pytest.approx(10.0, 1e-5)] + assert cell.temperature == 600.0 + assert cell.density == pytest.approx(10.0, 1e-5) assert cell.fill.get_mass_density() == pytest.approx(5.0) # Now C assert openmc.lib.cells[1].get_temperature() == 600. @@ -285,8 +286,8 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): 'with_properties/settings.xml' ) cell = model_with_properties.geometry.get_all_cells()[1] - assert cell.temperature == [600.0] - assert cell.density == [pytest.approx(10.0, 1e-5)] + assert cell.temperature == 600.0 + assert cell.density == pytest.approx(10.0, 1e-5) assert cell.fill.get_mass_density() == pytest.approx(5.0) @@ -659,6 +660,29 @@ def test_model_plot(): plt.close('all') +def test_model_plot_invalid_inputs(): + surface = openmc.Sphere(r=10.0, boundary_type="vacuum") + cell = openmc.Cell(region=-surface) + model = openmc.Model(openmc.Geometry([cell])) + + with pytest.raises(ValueError): + model.plot(n_samples=-1) + with pytest.raises(TypeError): + model.plot(n_samples=1.5) + with pytest.raises(ValueError): + model.plot(plane_tolerance=0.0) + with pytest.raises(TypeError): + model.plot(plane_tolerance='1') + with pytest.raises(ValueError): + model.plot(pixels=-1) + with pytest.raises(ValueError): + model.plot(pixels=(0, 100)) + with pytest.raises(ValueError): + model.plot(pixels=(100,)) + with pytest.raises(ValueError): + model.slice_data(u_span=(2, 0, 0), v_span=(0, 2, 0), pixels=-1) + + def test_model_id_map_initialization(run_in_tmpdir): model = openmc.examples.pwr_assembly() model.init_lib(output=False) @@ -902,6 +926,32 @@ def test_id_map_aligned_model(): assert tr_material == 5, f"Expected material ID 5 at top-right corner, got {tr_material}" +def test_id_map_model_with_overlaps(): + """Test id_map with a model that has overlaps and color_overlaps option""" + surface1 = openmc.Sphere(r=50, boundary_type="vacuum") + surface2 = openmc.Sphere(r=30) + cell1 = openmc.Cell(region=-surface1) + cell2 = openmc.Cell(region=-surface2) + geometry = openmc.Geometry([cell1, cell2]) + settings = openmc.Settings() + model = openmc.Model(geometry=geometry, settings=settings) + id_slice = model.id_map( + pixels=(10, 10), + basis='xy', + origin=(0, 0, 0), + width=(100, 100), + ) + assert -3 not in id_slice # -3 indicates overlap region + id_slice = model.id_map( + pixels=(10, 10), + basis='xy', + origin=(0, 0, 0), + width=(100, 100), + color_overlaps=True, # enables id_map to return -3 for overlaps + ) + assert -3 in id_slice + + def test_setter_from_list(): mat = openmc.Material() model = openmc.Model(materials=[mat]) @@ -911,7 +961,7 @@ def test_setter_from_list(): model = openmc.Model(tallies=[tally]) assert isinstance(model.tallies, openmc.Tallies) - plot = openmc.Plot() + plot = openmc.SlicePlot() model = openmc.Model(plots=[plot]) assert isinstance(model.plots, openmc.Plots) @@ -970,3 +1020,73 @@ def test_keff_search(run_in_tmpdir): # Check that total_batches property works assert result.total_batches == sum(result.batches) assert result.total_batches > 0 + + +def test_id_map_to_rgb(): + """Test conversion of ID map to RGB image array.""" + # Create a simple model + mat = openmc.Material() + mat.set_density('g/cm3', 1.0) + mat.add_nuclide('Li7', 1.0) + + sphere = openmc.Sphere(r=5.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + geometry = openmc.Geometry([cell]) + settings = openmc.Settings( + batches=10, particles=100, run_mode='fixed source' + ) + model = openmc.Model(geometry, settings=settings) + + id_data = np.zeros((10, 10, 3), dtype=np.int32) + id_data[:, :, 0] = cell.id # Cell IDs + id_data[:, :, 2] = mat.id # Material IDs + + # Test color_by with default colors + for color_by in ['cell', 'material']: + rgb = id_map_to_rgb(id_data, color_by=color_by) + assert rgb.shape == (10, 10, 3) + assert rgb.dtype == float + assert np.all((rgb >= 0) & (rgb <= 1)) # RGB values in [0, 1] + + # Test with custom colors + colors = {cell.id: (255, 0, 0)} # Red + rgb_custom = id_map_to_rgb(id_data, color_by='cell', colors=colors) + assert np.allclose(rgb_custom, [1.0, 0.0, 0.0]) # All pixels should be red + + # Test with overlaps + id_data_overlap = id_data.copy() + id_data_overlap[5:, 5:, 0] = -3 # Mark some pixels as overlaps + rgb_overlap = id_map_to_rgb( + id_data_overlap, overlap_color=(0, 255, 0) + ) + # Check that overlap region is green + assert np.allclose(rgb_overlap[5:, 5:], [0.0, 1.0, 0.0]) + + +def test_convert_to_multigroup_preserves_material_names(run_in_tmpdir): + """convert_to_multigroup leaves the user's material names unchanged and keys + the MGXS library by a unique sanitised name + id, so distinct materials that + share a name do not collapse to a single cross section.""" + a = openmc.Material(name="Steel Plate #1") + a.add_element("Fe", 1.0) + a.set_density("g/cm3", 7.9) + b = openmc.Material(name="Steel Plate #1") # same name, distinct material + b.add_element("Fe", 1.0) + b.set_density("g/cm3", 7.9) + + s1 = openmc.Sphere(r=1.0) + s2 = openmc.Sphere(r=2.0, boundary_type="vacuum") + c1 = openmc.Cell(fill=a, region=-s1) + c2 = openmc.Cell(fill=b, region=+s1 & -s2) + model = openmc.Model(openmc.Geometry([c1, c2]), openmc.Materials([a, b])) + + # Pre-create the library so MGXS generation (and transport) is skipped. + Path("mgxs.h5").touch() + model.convert_to_multigroup(method="material_wise", mgxs_path="mgxs.h5") + + # User names are preserved, not sanitised or de-duplicated. + assert [m.name for m in model.materials] == ["Steel Plate #1", "Steel Plate #1"] + # Each material reads a unique, sanitised library entry (name + id). + macro = [m._macroscopic for m in model.materials] + assert macro == [f"Steel_Plate__1_{a.id}", f"Steel_Plate__1_{b.id}"] + assert len(set(macro)) == 2 diff --git a/tests/unit_tests/test_particle_type.py b/tests/unit_tests/test_particle_type.py new file mode 100644 index 000000000..67e385ea6 --- /dev/null +++ b/tests/unit_tests/test_particle_type.py @@ -0,0 +1,298 @@ +"""Unit tests for ParticleType class.""" + +import pytest +from openmc import ParticleType + + +# Tests for creating ParticleType instances + +def test_create_from_int(): + """Test creation from PDG number.""" + p = ParticleType(2112) + assert p.pdg_number == 2112 + assert int(p) == 2112 + + +def test_create_from_string_name(): + """Test creation from particle name.""" + p = ParticleType('neutron') + assert p.pdg_number == 2112 + + p = ParticleType('photon') + assert p.pdg_number == 22 + + +def test_create_from_string_aliases(): + """Test creation from particle aliases.""" + assert ParticleType('n').pdg_number == 2112 + assert ParticleType('gamma').pdg_number == 22 + assert ParticleType('p').pdg_number == 2212 + assert ParticleType('proton').pdg_number == 2212 + assert ParticleType('d').pdg_number == 1000010020 + assert ParticleType('t').pdg_number == 1000010030 + + +def test_create_from_string_nuclide(): + """Test creation from GNDS nuclide name.""" + p = ParticleType('He4') + assert p.pdg_number == 1000020040 + + p = ParticleType('U235') + assert p.pdg_number == 1000922350 + + p = ParticleType('Am242_m1') + assert p.pdg_number == 1000952421 + + +def test_create_from_string_pdg_prefix(): + """Test creation with pdg: prefix.""" + p = ParticleType('pdg:2112') + assert p.pdg_number == 2112 + + p = ParticleType('PDG:22') + assert p.pdg_number == 22 + + +def test_create_from_particle_type(): + """Test creation from existing ParticleType.""" + p1 = ParticleType(2112) + p2 = ParticleType(p1) + assert p1 == p2 + assert p1 is not p2 # Different instances + + +def test_legacy_particle_indices(): + """Test backward compatibility with legacy indices 0-3.""" + assert ParticleType(0) == ParticleType.NEUTRON + assert ParticleType(1) == ParticleType.PHOTON + assert ParticleType(2) == ParticleType.ELECTRON + assert ParticleType(3) == ParticleType.POSITRON + + +def test_create_invalid_type(): + """Test creation with invalid type raises TypeError.""" + with pytest.raises(TypeError): + ParticleType([2112]) + + with pytest.raises(TypeError): + ParticleType({'pdg': 2112}) + + +def test_create_invalid_string(): + """Test creation with invalid string raises ValueError.""" + with pytest.raises(ValueError): + ParticleType('') + + with pytest.raises(ValueError): + ParticleType('pdg:invalid') + + +def test_create_case_insensitive(): + """Test that string parsing is case insensitive for aliases.""" + assert ParticleType('NEUTRON').pdg_number == 2112 + assert ParticleType('Neutron').pdg_number == 2112 + assert ParticleType('PHOTON').pdg_number == 22 + + +# Tests for equality and comparison + +def test_equality_same_pdg(): + """Test that instances with same PDG are equal.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + assert p1 == p2 + + +def test_equality_int(): + """Test equality comparison with int.""" + p = ParticleType(2112) + assert p == 2112 + assert 2112 == p + + +def test_equality_string(): + """Test equality comparison with string.""" + p = ParticleType(2112) + assert p == 'neutron' + assert p == 'pdg:2112' + assert p == 'n' + + +def test_inequality(): + """Test inequality comparisons.""" + p1 = ParticleType(2112) + p2 = ParticleType(22) + assert p1 != p2 + assert p1 != 22 + assert p1 != 'photon' + + +def test_equality_with_constants(): + """Test equality with class constants.""" + p = ParticleType(2112) + assert p == ParticleType.NEUTRON + assert ParticleType.NEUTRON == p + + +def test_equality_invalid_string(): + """Test equality with invalid string returns False.""" + p = ParticleType(2112) + assert not (p == 'invalid_particle') + assert p != 'invalid_particle' + + +# Tests for hashing behavior + +def test_hash_consistency(): + """Test that equal instances hash to the same value.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + assert hash(p1) == hash(p2) + + +def test_set_deduplication(): + """Test that equal instances deduplicate in sets.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + s = {p1, p2} + assert len(s) == 1 + + +def test_dict_key(): + """Test use as dictionary key.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + d = {p1: 'neutron'} + assert d[p2] == 'neutron' + + +def test_hash_different_particles(): + """Test that different particles have different hashes (usually).""" + p1 = ParticleType(2112) + p2 = ParticleType(22) + # Different PDG numbers should (almost always) have different hashes + assert hash(p1) != hash(p2) + + +# Tests for properties and computed attributes + +def test_pdg_number_property(): + """Test pdg_number property.""" + p = ParticleType(2112) + assert p.pdg_number == 2112 + assert isinstance(p.pdg_number, int) + + +def test_int_conversion(): + """Test __int__ conversion.""" + p = ParticleType(1000020040) + assert int(p) == 1000020040 + + +def test_zam_elementary(): + """Test zam property for elementary particles.""" + assert ParticleType.NEUTRON.zam is None + assert ParticleType.PHOTON.zam is None + assert ParticleType.ELECTRON.zam is None + assert ParticleType.POSITRON.zam is None + + +def test_zam_nucleus(): + """Test zam property for nuclear particles.""" + he4 = ParticleType('He4') + assert he4.zam == (2, 4, 0) + + u235 = ParticleType('U235') + Z, A, m = u235.zam + assert Z == 92 + assert A == 235 + assert m == 0 + + +def test_zam_metastable(): + """Test zam property for metastable nuclei.""" + # Am242m has m=1 + am242m = ParticleType('Am242_m1') + Z, A, m = am242m.zam + assert Z == 95 + assert A == 242 + assert m == 1 + + +def test_is_nucleus_false(): + """Test is_nucleus for elementary particles.""" + assert not ParticleType.NEUTRON.is_nucleus + assert not ParticleType.PHOTON.is_nucleus + assert not ParticleType.ELECTRON.is_nucleus + assert not ParticleType.POSITRON.is_nucleus + + +def test_is_nucleus_true(): + """Test is_nucleus for nuclear particles.""" + assert ParticleType.ALPHA.is_nucleus + assert ParticleType('He4').is_nucleus + assert ParticleType('U235').is_nucleus + assert ParticleType.DEUTERON.is_nucleus + assert ParticleType.TRITON.is_nucleus + + +# Tests for __str__ and __repr__ + +def test_str_elementary(): + """Test string representation of elementary particles.""" + assert str(ParticleType.NEUTRON) == 'neutron' + assert str(ParticleType.PHOTON) == 'photon' + assert str(ParticleType.ELECTRON) == 'electron' + assert str(ParticleType.POSITRON) == 'positron' + assert str(ParticleType.PROTON) == 'H1' + + +def test_str_nucleus(): + """Test string representation of nuclei.""" + assert str(ParticleType.ALPHA) == 'He4' + assert str(ParticleType('U235')) == 'U235' + assert str(ParticleType.DEUTERON) == 'H2' + assert str(ParticleType.TRITON) == 'H3' + + +def test_str_arbitrary_pdg(): + """Test string representation of arbitrary PDG number.""" + # PDG number that doesn't match any known particle + p = ParticleType(12345) + assert str(p) == 'pdg:12345' + + +def test_repr(): + """Test repr includes PDG number.""" + p = ParticleType.NEUTRON + repr_str = repr(p) + assert 'ParticleType' in repr_str + assert 'PDG=2112' in repr_str + assert 'neutron' in repr_str + + +def test_repr_nucleus(): + """Test repr for nuclear particles.""" + p = ParticleType.ALPHA + repr_str = repr(p) + assert 'ParticleType' in repr_str + assert 'PDG=1000020040' in repr_str + assert 'He4' in repr_str + + +def test_create_from_numpy_int(): + """Test creation from numpy integer types.""" + import numpy as np + p = ParticleType(np.int32(2112)) + assert p.pdg_number == 2112 + + p = ParticleType(np.int64(22)) + assert p.pdg_number == 22 + + +def test_equality_numpy_int(): + """Test equality comparison with numpy integer types.""" + import numpy as np + p = ParticleType(2112) + assert p == np.int32(2112) + assert p == np.int64(2112) diff --git a/tests/unit_tests/test_periodic_bc.py b/tests/unit_tests/test_periodic_bc.py new file mode 100644 index 000000000..4cc126abc --- /dev/null +++ b/tests/unit_tests/test_periodic_bc.py @@ -0,0 +1,47 @@ +from math import cos, sin, radians +import random + +import openmc +import pytest + + +@pytest.mark.parametrize("angle", [30., 45., 60., 90., 120.]) +def test_rotational_periodic_bc(angle): + # Pick random starting angle + start = random.uniform(0., 360.) + degrees = angle + ang1 = radians(start) + ang2 = radians(start + degrees) + + # Define three points on each plane and then randomly shuffle them + p1_points = [(0., 0., 0.), (cos(ang1), sin(ang1), 0.), (0., 0., 1.)] + p2_points = [(0., 0., 0.), (cos(ang2), sin(ang2), 0.), (0., 0., 1.)] + random.shuffle(p1_points) + random.shuffle(p2_points) + + # Create periodic planes and a cylinder + p1 = openmc.Plane.from_points(*p1_points, boundary_type='periodic') + p2 = openmc.Plane.from_points(*p2_points, boundary_type='periodic') + p1.periodic_surface = p2 + zcyl = openmc.ZCylinder(r=5., boundary_type='vacuum') + + # Figure out which side of planes to use based on a point in the middle + ang_mid = radians(start + degrees/2.) + mid_point = (cos(ang_mid), sin(ang_mid), 0.) + r1 = -p1 if mid_point in -p1 else +p1 + r2 = -p2 if mid_point in -p2 else +p2 + + # Create one cell bounded by the two planes and the cylinder + mat = openmc.Material(density=1.0, density_units='g/cm3', components={'U235': 1.0}) + cell = openmc.Cell(fill=mat, region=r1 & r2 & -zcyl) + + # Make the model complete + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.source = openmc.IndependentSource(space=openmc.stats.Point(mid_point)) + model.settings.particles = 1000 + model.settings.batches = 10 + model.settings.inactive = 5 + + # Run the model + model.run() diff --git a/tests/unit_tests/test_pin.py b/tests/unit_tests/test_pin.py index 5496a3b73..53665b1a6 100644 --- a/tests/unit_tests/test_pin.py +++ b/tests/unit_tests/test_pin.py @@ -46,11 +46,6 @@ def test_failure(pin_mats, good_radii): with pytest.raises(ValueError, match="length"): pin(good_surfaces[:len(pin_mats) - 2], pin_mats) - # Non-positive radii - rad = [openmc.ZCylinder(r=-0.1)] + good_surfaces[1:] - with pytest.raises(ValueError, match="index 0"): - pin(rad, pin_mats) - # Non-increasing radii surfs = tuple(reversed(good_surfaces)) with pytest.raises(ValueError, match="index 1"): diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index fad574ee6..98a93e44b 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -9,12 +9,11 @@ from openmc.plots import _SVG_COLORS @pytest.fixture(scope='module') def myplot(): - plot = openmc.Plot(name='myplot') + plot = openmc.SlicePlot(name='myplot') plot.width = (100., 100.) plot.origin = (2., 3., -10.) plot.pixels = (500, 500) plot.filename = './not-a-dir/myplot' - plot.type = 'slice' plot.basis = 'yz' plot.background = 'black' plot.background = (0, 0, 0) @@ -80,8 +79,7 @@ def test_voxel_plot(run_in_tmpdir): geometry.export_to_xml() materials = openmc.Materials() materials.export_to_xml() - vox_plot = openmc.Plot() - vox_plot.type = 'voxel' + vox_plot = openmc.VoxelPlot() vox_plot.id = 12 vox_plot.width = (1500., 1500., 1500.) vox_plot.pixels = (200, 200, 200) @@ -97,8 +95,9 @@ def test_voxel_plot(run_in_tmpdir): assert Path('h5_voxel_plot.h5').is_file() assert Path('another_test_voxel_plot.vti').is_file() - slice_plot = openmc.Plot() - with pytest.raises(ValueError): + # SlicePlot should not have to_vtk method + slice_plot = openmc.SlicePlot() + with pytest.raises(AttributeError): slice_plot.to_vtk('shimmy.vti') @@ -153,14 +152,14 @@ def test_from_geometry(): geom = openmc.Geometry(univ) for basis in ('xy', 'yz', 'xz'): - plot = openmc.Plot.from_geometry(geom, basis) + plot = openmc.SlicePlot.from_geometry(geom, basis) assert plot.origin == pytest.approx((0., 0., 0.)) assert plot.width == pytest.approx((width, width)) assert plot.basis == basis def test_highlight_domains(): - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.color_by = 'material' plots = openmc.Plots([plot]) @@ -179,8 +178,8 @@ def test_xml_element(myplot): assert elem.find('pixels') is not None assert elem.find('background').text == '0 0 0' - newplot = openmc.Plot.from_xml_element(elem) - attributes = ('id', 'color_by', 'filename', 'type', 'basis', 'level', + newplot = openmc.SlicePlot.from_xml_element(elem) + attributes = ('id', 'color_by', 'filename', 'basis', 'level', 'meshlines', 'show_overlaps', 'origin', 'width', 'pixels', 'background', 'mask_background') for attr in attributes: @@ -200,11 +199,11 @@ def test_to_xml_element_proj(myprojectionplot): def test_plots(run_in_tmpdir): - p1 = openmc.Plot(name='plot1') + p1 = openmc.SlicePlot(name='plot1') p1.origin = (5., 5., 5.) p1.colors = {10: (255, 100, 0)} p1.mask_components = [2, 4, 6] - p2 = openmc.Plot(name='plot2') + p2 = openmc.SlicePlot(name='plot2') p2.origin = (-3., -3., -3.) plots = openmc.Plots([p1, p2]) assert len(plots) == 2 @@ -213,7 +212,7 @@ def test_plots(run_in_tmpdir): plots = openmc.Plots([p1, p2, p3]) assert len(plots) == 3 - p4 = openmc.Plot(name='plot4') + p4 = openmc.VoxelPlot(name='plot4') plots.append(p4) assert len(plots) == 4 @@ -230,8 +229,7 @@ def test_plots(run_in_tmpdir): def test_voxel_plot_roundtrip(): # Define a voxel plot and create XML element - plot = openmc.Plot(name='my voxel plot') - plot.type = 'voxel' + plot = openmc.VoxelPlot(name='my voxel plot') plot.filename = 'voxel1' plot.pixels = (50, 50, 50) plot.origin = (0., 0., 0.) @@ -243,7 +241,6 @@ def test_voxel_plot_roundtrip(): new_plot = plot.from_xml_element(elem) assert new_plot.name == plot.name assert new_plot.filename == plot.filename - assert new_plot.type == plot.type assert new_plot.pixels == plot.pixels assert new_plot.origin == plot.origin assert new_plot.width == plot.width @@ -288,10 +285,9 @@ def test_phong_plot_roundtrip(): def test_plot_directory(run_in_tmpdir): pwr_pin = openmc.examples.pwr_pin_cell() - # create a standard plot, expected to work - plot = openmc.Plot() + # create a standard slice plot, expected to work + plot = openmc.SlicePlot() plot.filename = 'plot_1' - plot.type = 'slice' plot.pixels = (10, 10) plot.color_by = 'material' plot.width = (100., 100.) diff --git a/tests/unit_tests/test_pulse_height.py b/tests/unit_tests/test_pulse_height.py new file mode 100644 index 000000000..1f27cc6f2 --- /dev/null +++ b/tests/unit_tests/test_pulse_height.py @@ -0,0 +1,59 @@ +import numpy as np +import pytest +import openmc + + +@pytest.fixture +def model(): + openmc.reset_auto_ids() + model = openmc.Model() + + # Define materials + NaI = openmc.Material() + NaI.set_density('g/cm3', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) + + # Define geometry: two spheres in each other + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', fill=NaI, region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 1 + model.settings.particles = 10000 + model.settings.photon_transport = True + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle='photon' + ) + + # Define tallies + energy_filter = openmc.EnergyFilter([1e3, 1e7]) + + tally1 = openmc.Tally() + tally1.scores = ['pulse-height'] + cell_filter1 = openmc.CellFilter([inner_sphere, outer_sphere]) + tally1.filters = [cell_filter1, energy_filter] + + tally2 = openmc.Tally() + tally2.scores = ['pulse-height'] + cell_filter2 = openmc.CellFilter([outer_sphere, inner_sphere]) + tally2.filters = [cell_filter2, energy_filter] + + model.tallies = [tally1, tally2] + return model + + +def test_pulse_height(model, run_in_tmpdir): + sp_path = model.run() + sp = openmc.StatePoint(sp_path) + t1 = sp.tallies[1].mean.squeeze() + t2 = sp.tallies[2].mean.squeeze() + + np.testing.assert_array_equal(t1, t2[::-1]) + + diff --git a/tests/unit_tests/test_r2s.py b/tests/unit_tests/test_r2s.py index a94f85c8c..5e617919d 100644 --- a/tests/unit_tests/test_r2s.py +++ b/tests/unit_tests/test_r2s.py @@ -6,7 +6,7 @@ from openmc.deplete import Chain, R2SManager @pytest.fixture -def simple_model_and_mesh(tmp_path): +def simple_model_and_mesh(): # Define two materials: water and Ni h2o = openmc.Material() h2o.add_nuclide("H1", 2.0) @@ -33,8 +33,8 @@ def simple_model_and_mesh(tmp_path): # Simple settings with a point source settings = openmc.Settings() - settings.batches = 10 - settings.particles = 1000 + settings.batches = 2 + settings.particles = 250 settings.run_mode = 'fixed source' settings.source = openmc.IndependentSource() model = openmc.Model(geometry, settings=settings) @@ -46,6 +46,16 @@ def simple_model_and_mesh(tmp_path): return model, (c1, c2), mesh +@pytest.fixture +def source_stage_manager(simple_model_and_mesh): + model, (c1, c2), _ = simple_model_and_mesh + r2s = R2SManager(model, [c1, c2]) + r2s.results['depletion_results'] = [None, None] + r2s.results['activation_materials'] = [c1.fill, c2.fill] + bounding_boxes = {c1.id: c1.bounding_box, c2.id: c2.bounding_box} + return r2s, bounding_boxes + + def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): model, (c1, c2), mesh = simple_model_and_mesh @@ -59,28 +69,32 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): outdir = r2s.run( timesteps=[(1.0, 'd')], source_rates=[1.0], - photon_time_indices=[1], output_dir=tmp_path, chain_file=chain, + micro_kwargs={'nuclides': ['Ni58'], 'reactions': ['(n,p)']}, ) # Check directories and files exist nt = Path(outdir) / 'neutron_transport' assert (nt / 'fluxes.npy').exists() assert (nt / 'micros.h5').exists() - assert (nt / 'mesh_material_volumes.npz').exists() + assert (nt / 'mesh_material_volumes_0.npz').exists() act = Path(outdir) / 'activation' assert (act / 'depletion_results.h5').exists() pt = Path(outdir) / 'photon_transport' assert (pt / 'tally_ids.json').exists() - assert (pt / 'time_1' / 'statepoint.10.h5').exists() + assert not (pt / 'time_0').exists() + assert (pt / 'time_1' / 'statepoint.2.h5').exists() # Basic results structure checks assert len(r2s.results['fluxes']) == 2 assert len(r2s.results['micros']) == 2 - assert len(r2s.results['mesh_material_volumes']) == 2 + assert len(r2s.results['mesh_material_volumes']) == 1 + assert len(r2s.results['mesh_material_volumes'][0]) == 2 assert len(r2s.results['activation_materials']) == 2 assert len(r2s.results['depletion_results']) == 2 + assert list(r2s.results['photon_sources']) == [1] + assert r2s.results['photon_sources'][1] # Check activation materials amats = r2s.results['activation_materials'] @@ -93,13 +107,83 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): r2s_loaded.load_results(outdir) assert len(r2s_loaded.results['fluxes']) == 2 assert len(r2s_loaded.results['micros']) == 2 - assert len(r2s_loaded.results['mesh_material_volumes']) == 2 + assert len(r2s_loaded.results['mesh_material_volumes']) == 1 + assert len(r2s_loaded.results['mesh_material_volumes'][0]) == 2 assert len(r2s_loaded.results['activation_materials']) == 2 assert len(r2s_loaded.results['depletion_results']) == 2 +def test_r2s_multi_mesh(simple_model_and_mesh, tmp_path): + model, _, _ = simple_model_and_mesh + + # Two 1x1x1 meshes that together cover the full domain, split along y. + # Each mesh element spans the full x range [-10, 10], crossing the x=0 + # material boundary, so both meshes contain both materials within their + # single element. + mesh1 = openmc.RegularMesh() + mesh1.lower_left = (-10.0, -10.0, -10.0) + mesh1.upper_right = (10.0, 0.0, 10.0) + mesh1.dimension = (1, 1, 1) + mesh2 = openmc.RegularMesh() + mesh2.lower_left = (-10.0, 0.0, -10.0) + mesh2.upper_right = (10.0, 10.0, 10.0) + mesh2.dimension = (1, 1, 1) + + r2s = R2SManager(model, [mesh1, mesh2]) + chain = Chain.from_xml(Path(__file__).parents[1] / "chain_ni.xml") + + outdir = r2s.run( + timesteps=[(1.0, 'd')], + source_rates=[1.0], + photon_time_indices=[1], + output_dir=tmp_path, + chain_file=chain, + micro_kwargs={'nuclides': ['Ni58'], 'reactions': ['(n,p)']}, + ) + + # Check that per-mesh MMV files were written + nt = Path(outdir) / 'neutron_transport' + assert (nt / 'fluxes.npy').exists() + assert (nt / 'micros.h5').exists() + assert (nt / 'mesh_material_volumes_0.npz').exists() + assert (nt / 'mesh_material_volumes_1.npz').exists() + act = Path(outdir) / 'activation' + assert (act / 'depletion_results.h5').exists() + pt = Path(outdir) / 'photon_transport' + assert (pt / 'tally_ids.json').exists() + assert (pt / 'time_1' / 'statepoint.2.h5').exists() + + # Two meshes, each with 1 element containing both materials → + # 2 element-material combinations per mesh, 4 total + assert len(r2s.results['mesh_material_volumes']) == 2 + assert len(r2s.results['mesh_material_volumes'][0]) == 2 + assert len(r2s.results['mesh_material_volumes'][1]) == 2 + assert len(r2s.results['fluxes']) == 4 + assert len(r2s.results['micros']) == 4 + assert len(r2s.results['activation_materials']) == 4 + assert len(r2s.results['depletion_results']) == 2 + + # Activation material names encode mesh index + amats = r2s.results['activation_materials'] + assert all(m.depletable for m in amats) + assert any('Mesh 0' in m.name for m in amats) + assert any('Mesh 1' in m.name for m in amats) + + # Check loading results + r2s_loaded = R2SManager(model, [mesh1, mesh2]) + r2s_loaded.load_results(outdir) + assert len(r2s_loaded.results['mesh_material_volumes']) == 2 + assert len(r2s_loaded.results['mesh_material_volumes'][0]) == 2 + assert len(r2s_loaded.results['mesh_material_volumes'][1]) == 2 + assert len(r2s_loaded.results['activation_materials']) == 4 + assert len(r2s_loaded.results['depletion_results']) == 2 + + def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): model, (c1, c2), _ = simple_model_and_mesh + tally = openmc.Tally() + tally.scores = ['flux'] + model.tallies = [tally] # Use cell-based domains r2s = R2SManager(model, [c1, c2]) @@ -113,9 +197,11 @@ def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): timesteps=[(1.0, 'd')], source_rates=[1.0], photon_time_indices=[1], + by_parent_nuclide=True, output_dir=tmp_path, bounding_boxes=bounding_boxes, - chain_file=chain + chain_file=chain, + micro_kwargs={'nuclides': ['Ni58'], 'reactions': ['(n,p)']}, ) # Check directories and files exist @@ -126,13 +212,15 @@ def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): assert (act / 'depletion_results.h5').exists() pt = Path(outdir) / 'photon_transport' assert (pt / 'tally_ids.json').exists() - assert (pt / 'time_1' / 'statepoint.10.h5').exists() + assert (pt / 'time_1' / 'statepoint.2.h5').exists() # Basic results structure checks assert len(r2s.results['fluxes']) == 2 assert len(r2s.results['micros']) == 2 assert len(r2s.results['activation_materials']) == 2 assert len(r2s.results['depletion_results']) == 2 + assert r2s.photon_model.tallies[0].contains_filter( + openmc.ParentNuclideFilter) # Check activation materials amats = r2s.results['activation_materials'] @@ -150,3 +238,88 @@ def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): assert len(r2s_loaded.results['micros']) == 2 assert len(r2s_loaded.results['activation_materials']) == 2 assert len(r2s_loaded.results['depletion_results']) == 2 + + +def test_step4_requires_photon_sources(simple_model_and_mesh, tmp_path): + model, (c1, c2), _ = simple_model_and_mesh + r2s = R2SManager(model, [c1, c2]) + output_dir = tmp_path / 'photon' + + with pytest.raises(RuntimeError, match='step3_photon_source'): + r2s.step4_photon_transport(output_dir) + + r2s.results['photon_sources'] = {} + with pytest.raises(RuntimeError, match='No decay photon sources'): + r2s.step4_photon_transport(output_dir) + + assert not output_dir.exists() + + +def test_default_photon_times_skip_empty_sources( + source_stage_manager, tmp_path, monkeypatch +): + r2s, bounding_boxes = source_stage_manager + source = object() + sources_by_time = {0: [], 1: [source]} + monkeypatch.setattr( + r2s, '_create_photon_sources', + lambda time_index, work_items: sources_by_time[time_index]) + + r2s.step3_photon_source( + bounding_boxes=bounding_boxes, output_dir=tmp_path) + + assert r2s.results['photon_sources'] == {1: [source]} + + +def test_explicit_empty_photon_source_fails( + source_stage_manager, tmp_path, monkeypatch +): + r2s, bounding_boxes = source_stage_manager + source = object() + sources_by_time = {0: [], 1: [source]} + monkeypatch.setattr( + r2s, '_create_photon_sources', + lambda time_index, work_items: sources_by_time[time_index]) + r2s.results['photon_sources'] = {99: [source]} + + with pytest.raises(RuntimeError, match='requested time indices: 0'): + r2s.step3_photon_source( + [0, 1], bounding_boxes, output_dir=tmp_path) + + assert 'photon_sources' not in r2s.results + + +def test_default_photon_times_require_a_source( + source_stage_manager, tmp_path, monkeypatch +): + r2s, bounding_boxes = source_stage_manager + monkeypatch.setattr( + r2s, '_create_photon_sources', + lambda time_index, work_items: []) + + with pytest.raises(RuntimeError, match='at any depletion time'): + r2s.step3_photon_source( + bounding_boxes=bounding_boxes, output_dir=tmp_path) + + assert 'photon_sources' not in r2s.results + + +@pytest.mark.parametrize( + ('time_indices', 'exception'), + [ + ([], ValueError), + ([2], IndexError), + ([-3], IndexError), + ([1.0], TypeError), + ], +) +def test_photon_time_index_validation( + source_stage_manager, tmp_path, time_indices, exception +): + r2s, bounding_boxes = source_stage_manager + + with pytest.raises(exception): + r2s.step3_photon_source( + time_indices, bounding_boxes, output_dir=tmp_path) + + assert 'photon_sources' not in r2s.results diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index fe618fd2d..bdb3ea8fe 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -1,8 +1,17 @@ +from pathlib import Path + +import h5py +import pytest + import openmc +import openmc.lib import openmc.stats def test_export_to_xml(run_in_tmpdir): + + tmp_properties_file = 'properties_test.h5' + s = openmc.Settings(run_mode='fixed source', batches=1000, seed=17) s.generations_per_batch = 10 s.inactive = 100 @@ -21,6 +30,8 @@ def test_export_to_xml(run_in_tmpdir): s.statepoint = {'batches': [50, 150, 500, 1000]} s.surf_source_read = {'path': 'surface_source_1.h5'} s.surf_source_write = {'surface_ids': [2], 'max_particles': 200} + s.surface_grazing_ratio = 0.7 + s.surface_grazing_cutoff = 0.1 s.confidence_intervals = True s.ptables = True s.plot_seed = 100 @@ -43,6 +54,7 @@ def test_export_to_xml(run_in_tmpdir): s.tabular_legendre = {'enable': True, 'num_points': 50} s.temperature = {'default': 293.6, 'method': 'interpolation', 'multipole': True, 'range': (200., 1000.)} + s.properties_file = tmp_properties_file s.trace = (10, 1, 20) s.track = [(1, 1, 1), (2, 1, 1)] s.ufs_mesh = mesh @@ -57,6 +69,7 @@ def test_export_to_xml(run_in_tmpdir): s.log_grid_bins = 2000 s.photon_transport = False s.electron_treatment = 'led' + s.atomic_relaxation = False s.write_initial_source = True s.weight_window_checkpoints = {'surface': True, 'collision': False} source_region_mesh = openmc.RegularMesh() @@ -85,6 +98,7 @@ def test_export_to_xml(run_in_tmpdir): # Make sure exporting XML works s.export_to_xml() + # Generate settings from XML s = openmc.Settings.from_xml() assert s.run_mode == 'fixed source' @@ -107,6 +121,8 @@ def test_export_to_xml(run_in_tmpdir): assert s.statepoint == {'batches': [50, 150, 500, 1000]} assert s.surf_source_read['path'].name == 'surface_source_1.h5' assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200} + assert s.surface_grazing_ratio == 0.7 + assert s.surface_grazing_cutoff == 0.1 assert s.confidence_intervals assert s.ptables assert s.plot_seed == 100 @@ -129,6 +145,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.tabular_legendre == {'enable': True, 'num_points': 50} assert s.temperature == {'default': 293.6, 'method': 'interpolation', 'multipole': True, 'range': [200., 1000.]} + assert s.properties_file == Path(tmp_properties_file) assert s.trace == [10, 1, 20] assert s.track == [(1, 1, 1), (2, 1, 1)] assert isinstance(s.ufs_mesh, openmc.RegularMesh) @@ -143,6 +160,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.log_grid_bins == 2000 assert not s.photon_transport assert s.electron_treatment == 'led' + assert not s.atomic_relaxation assert s.write_initial_source assert len(s.volume_calculations) == 1 vol = s.volume_calculations[0] @@ -172,3 +190,59 @@ def test_export_to_xml(run_in_tmpdir): assert s.max_secondaries == 1_000_000 assert s.source_rejection_fraction == 0.01 assert s.free_gas_threshold == 800.0 + + +def test_properties_file_load(tmp_path, mpi_intracomm): + model = openmc.examples.pwr_assembly() + + # Session 1: export a structurally valid properties file via the C++ API, + # then collect the cell/material structure so we can patch it with h5py. + cell_instances = {} # {cell_id: n_instances} — material cells only + mat_densities = {} # {mat_id: original atom/b-cm density} + + props_path = tmp_path / 'properties.h5' + with openmc.lib.TemporarySession(model, intracomm=mpi_intracomm): + openmc.lib.export_properties(str(props_path)) + for cell_id, cell in openmc.lib.cells.items(): + try: + cell.fill # raises NotImplementedError for non-material cells + cell_instances[cell_id] = cell.num_instances + except NotImplementedError: + pass + for mat_id, mat in openmc.lib.materials.items(): + mat_densities[mat_id] = mat.get_density('atom/b-cm') + + assert any(n > 1 for n in cell_instances.values()) + + # Patch the exported properties file overwriting temperatures + # with per-instance values and scale material atom densities. + density_factor = 0.75 + with h5py.File(props_path, 'r+') as f: + cells_grp = f['geometry/cells'] + for cell_id, n in cell_instances.items(): + cell_grp = cells_grp[f'cell {cell_id}'] + del cell_grp['temperature'] + cell_grp.create_dataset( + 'temperature', data=[500.0 + 5.0 * i for i in range(n)] + ) + + for mat_id, orig_density in mat_densities.items(): + f['materials'][f'material {mat_id}'].attrs['atom_density'] = \ + orig_density * density_factor + + # now apply the newly patched properties file using the settings + # and load the model again, checking that the new temperature and + # density values match those in the new file + model.settings.properties_file = props_path + + with openmc.lib.TemporarySession(model, intracomm=mpi_intracomm): + for cell_id, n in cell_instances.items(): + cell = openmc.lib.cells[cell_id] + for i in range(n): + assert cell.get_temperature(i) == pytest.approx(500.0 + 5.0 * i) + + for mat_id, orig_density in mat_densities.items(): + mat = openmc.lib.materials[mat_id] + assert mat.get_density('atom/b-cm') == pytest.approx( + orig_density * density_factor, rel=1e-5 + ) diff --git a/tests/unit_tests/test_slice_data.py b/tests/unit_tests/test_slice_data.py new file mode 100644 index 000000000..cc5fb0474 --- /dev/null +++ b/tests/unit_tests/test_slice_data.py @@ -0,0 +1,168 @@ +import numpy as np +import openmc +from openmc.examples import pwr_pin_cell + + +def test_slice_data_basic(run_in_tmpdir): + """Test basic slice_data functionality.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(100, 100), + basis='xy' + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (100, 100, 3) + assert geom_data.dtype == np.int32 + assert prop_data.shape == (100, 100, 2) + assert prop_data.dtype == np.float64 + + # Check we have valid geometry + assert np.any(geom_data[:, :, 0] >= 0) # Valid cell IDs + assert np.any(prop_data[:, :, 0] > 0) # Valid temperatures + + +def test_slice_data_no_properties(run_in_tmpdir): + """Test slice_data without property data.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + assert prop_data is None + + +def test_slice_data_with_filter(run_in_tmpdir): + """Test slice_data with a cell filter.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + include_properties=False + ) + + # With filter, should have 4 fields + assert geom_data.shape == (50, 50, 4) + + # Filter bin index should be populated where cells exist + filter_bins = geom_data[:, :, 3] + valid_cells = geom_data[:, :, 0] >= 0 + assert np.any(filter_bins[valid_cells] >= 0) + + +def test_slice_data_overlaps(run_in_tmpdir): + """Test slice_data with overlap detection.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + show_overlaps=True, + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + # Check for overlap markers (-3) if any exist + # Note: This test may pass without finding overlaps if geometry is correct + + +def test_slice_data_overlaps_with_filter(run_in_tmpdir): + """Test that overlaps don't overwrite filter bin data.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + show_overlaps=True, + include_properties=False + ) + + assert geom_data.shape == (50, 50, 4) + + # If any overlaps exist, verify filter bin is still valid (not -3) + overlap_pixels = geom_data[:, :, 0] == -3 + if np.any(overlap_pixels): + # Filter bins at overlap locations should NOT be -3 + filter_bins_at_overlaps = geom_data[overlap_pixels, 3] + assert not np.all(filter_bins_at_overlaps == -3), \ + "Filter bins should be preserved even where overlaps are detected" + + +def test_slice_data_different_bases(run_in_tmpdir): + """Test slice_data with different basis planes.""" + model = pwr_pin_cell() + + for basis in ['xy', 'xz', 'yz']: + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(25, 25), + basis=basis + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_oriented_spans(run_in_tmpdir): + """Test slice_data with oriented span vectors.""" + model = pwr_pin_cell() + + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + u_span=(1.0, 0.0, 0.0), + v_span=(0.0, 0.0, 1.0), + pixels=(25, 25) + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_level(run_in_tmpdir): + """Test slice_data with specific universe level.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + level=0, # Root universe only + include_properties=False + ) + + assert geom_data.shape == (50, 50, 3) + + +def test_id_map_reverted(run_in_tmpdir): + """Test that id_map returns 3D array without filter support.""" + model = pwr_pin_cell() + id_data = model.id_map( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + basis='xy' + ) + + # Should have 3 fields (cell_id, cell_instance, material_id) + assert id_data.shape == (50, 50, 3) + assert id_data.dtype == np.int32 + + # Check valid data + assert np.any(id_data[:, :, 0] >= 0) # Valid cell IDs diff --git a/tests/unit_tests/test_slice_data_overlap_info.py b/tests/unit_tests/test_slice_data_overlap_info.py new file mode 100644 index 000000000..e5f08245d --- /dev/null +++ b/tests/unit_tests/test_slice_data_overlap_info.py @@ -0,0 +1,89 @@ +import pytest +import numpy as np +import openmc +import openmc.lib + +# Sentinel value matching _OVERLAP in plotmodel.py and OVERLAP in plot.cpp +_OVERLAP = -3 + + +@pytest.fixture(scope='module') +def overlap_model(): + openmc.reset_auto_ids() + + # Three cylinders: cyl1 and cyl2 overlap near x=0, cyl2 and cyl3 overlap + # near x=4. This gives us two spatially distinct overlap regions in one model. + mat1 = openmc.Material(components={'H1': 1.0}) + mat2 = openmc.Material(components={'H1': 1.0}) + mat3 = openmc.Material(components={'H1': 1.0}) + + # cyl1 and cyl2 overlap on the left, cyl2 and cyl3 overlap on the right + cyl1 = openmc.ZCylinder(x0=-2.0, r=2.5) + cyl2 = openmc.ZCylinder(x0=0.0, r=2.5) + cyl3 = openmc.ZCylinder(x0=2.0, r=2.5) + boundary = openmc.Sphere(r=20.0, boundary_type='vacuum') + cell1 = openmc.Cell(region=-cyl1, fill=mat1) + cell2 = openmc.Cell(region=-cyl2, fill=mat2) + cell3 = openmc.Cell(region=-cyl3, fill=mat3) + cell_outside = openmc.Cell(region=+cyl1 & +cyl2 & +cyl3 & -boundary) + geometry = openmc.Geometry([cell1, cell2, cell3, cell_outside]) + + settings = openmc.Settings() + settings.run_mode = 'fixed source' + settings.particles = 100 + settings.batches = 1 + model = openmc.Model(geometry=geometry, settings=settings) + + with openmc.lib.TemporarySession(model, args=['-s', '1']): + yield + + +def run_slice(origin=(0.0, 0.0, 0.0), width=(10.0, 6.0), show_overlaps=True): + # Helper that runs a slice over a region covering both overlap zones + geom_data, _ = openmc.lib.slice_data( + origin=origin, + width=width, + basis='xy', + pixels=(100, 60), + show_overlaps=show_overlaps, + include_properties=False, + ) + return geom_data + + +def test_overlaps_enabled(overlap_model): + # Run a single slice with overlap detection enabled and check all + # expected properties in one pass. + geom_data = run_slice() + overlap_info = openmc.lib.slice_data_overlap_info() + n = overlap_info.shape[0] + cell_ids = geom_data[:, :, 0] + + # cell_ids should contain values more negative than _OVERLAP; RasterData + # encodes each unique overlap as OVERLAP - overlap_idx - 1 into slot 2. + assert np.any(cell_ids < _OVERLAP) + + # overlap_keys should have 2 entries for the two distinct overlapping + # cylinder pairs in this model. + assert n == 2, f"Expected exactly 2 overlap entries, got {n}" + + # Each entry is a (universe_id, cell1_id, cell2_id) triple; verify values. + for i in range(n): + universe_id = int(overlap_info[i, 0]) + cell1_id = int(overlap_info[i, 1]) + cell2_id = int(overlap_info[i, 2]) + assert universe_id == 1 + assert cell1_id in {1, 2, 3} + assert cell2_id in {1, 2, 3} + assert cell1_id != cell2_id + + +def test_overlaps_disabled(overlap_model): + # With show_overlaps=False, set_overlap is never called and overlap_keys + # is never written to, so the image and map should both be clean. + geom_data = run_slice(show_overlaps=False) + overlap_info = openmc.lib.slice_data_overlap_info() + n = overlap_info.shape[0] + + assert not np.any(geom_data[:, :, 2] < _OVERLAP) + assert n == 0 diff --git a/tests/unit_tests/test_slice_voxel_plots.py b/tests/unit_tests/test_slice_voxel_plots.py new file mode 100644 index 000000000..48ca31b7a --- /dev/null +++ b/tests/unit_tests/test_slice_voxel_plots.py @@ -0,0 +1,273 @@ +"""Tests for SlicePlot and VoxelPlot classes + +This module tests the functionality of the new SlicePlot and VoxelPlot +classes that replace the legacy Plot class. +""" +import warnings + +import pytest +import openmc + + +def test_slice_plot_initialization(): + """Test SlicePlot initialization with defaults""" + plot = openmc.SlicePlot() + assert plot.width == [4.0, 4.0] + assert plot.pixels == [400, 400] + assert plot.basis == 'xy' + assert plot.origin == [0., 0., 0.] + + +def test_slice_plot_width_validation(): + """Test that SlicePlot only accepts 2 values for width""" + plot = openmc.SlicePlot() + + # Should accept 2 values + plot.width = [10.0, 20.0] + assert plot.width == [10.0, 20.0] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "2"'): + plot.width = [10.0] + + # Should reject 3 values + with pytest.raises(ValueError, match='must be of length "2"'): + plot.width = [10.0, 20.0, 30.0] + + +def test_slice_plot_pixels_validation(): + """Test that SlicePlot only accepts 2 values for pixels""" + plot = openmc.SlicePlot() + + # Should accept 2 values + plot.pixels = [100, 200] + assert plot.pixels == [100, 200] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "2"'): + plot.pixels = [100] + + # Should reject 3 values + with pytest.raises(ValueError, match='must be of length "2"'): + plot.pixels = [100, 200, 300] + + +def test_slice_plot_basis(): + """Test that SlicePlot has basis attribute""" + plot = openmc.SlicePlot() + + # Test all valid basis values + for basis in ['xy', 'xz', 'yz']: + plot.basis = basis + assert plot.basis == basis + + # Test invalid basis + with pytest.raises(ValueError): + plot.basis = 'invalid' + + +def test_slice_plot_meshlines(): + """Test that SlicePlot has meshlines attribute""" + plot = openmc.SlicePlot() + + meshlines = { + 'type': 'tally', + 'id': 1, + 'linewidth': 2, + 'color': (255, 0, 0) + } + plot.meshlines = meshlines + assert plot.meshlines == meshlines + + +def test_slice_plot_xml_roundtrip(): + """Test SlicePlot XML serialization and deserialization""" + plot = openmc.SlicePlot(name='test_slice') + plot.width = [15.0, 25.0] + plot.pixels = [150, 250] + plot.basis = 'xz' + plot.origin = [1.0, 2.0, 3.0] + plot.color_by = 'material' + plot.filename = 'test_plot' + + # Convert to XML and back + elem = plot.to_xml_element() + new_plot = openmc.SlicePlot.from_xml_element(elem) + + # Check all attributes preserved + assert new_plot.name == plot.name + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.basis == plot.basis + assert new_plot.origin == pytest.approx(plot.origin) + assert new_plot.color_by == plot.color_by + assert new_plot.filename == plot.filename + + +def test_slice_plot_from_geometry(): + """Test creating SlicePlot from geometry""" + # Create simple geometry + s = openmc.Sphere(r=10.0, boundary_type='vacuum') + c = openmc.Cell(region=-s) + univ = openmc.Universe(cells=[c]) + geom = openmc.Geometry(univ) + + # Test all basis options + for basis in ['xy', 'xz', 'yz']: + plot = openmc.SlicePlot.from_geometry(geom, basis=basis) + assert plot.basis == basis + assert plot.width == pytest.approx([20.0, 20.0]) + assert plot.origin == pytest.approx([0.0, 0.0, 0.0]) + + +def test_voxel_plot_initialization(): + """Test VoxelPlot initialization with defaults""" + plot = openmc.VoxelPlot() + assert plot.width == [4.0, 4.0, 4.0] + assert plot.pixels == [400, 400, 400] + assert plot.origin == [0., 0., 0.] + + +def test_voxel_plot_width_validation(): + """Test that VoxelPlot only accepts 3 values for width""" + plot = openmc.VoxelPlot() + + # Should accept 3 values + plot.width = [10.0, 20.0, 30.0] + assert plot.width == [10.0, 20.0, 30.0] + + # Should reject 2 values + with pytest.raises(ValueError, match='must be of length "3"'): + plot.width = [10.0, 20.0] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "3"'): + plot.width = [10.0] + + +def test_voxel_plot_pixels_validation(): + """Test that VoxelPlot only accepts 3 values for pixels""" + plot = openmc.VoxelPlot() + + # Should accept 3 values + plot.pixels = [100, 200, 300] + assert plot.pixels == [100, 200, 300] + + # Should reject 2 values + with pytest.raises(ValueError, match='must be of length "3"'): + plot.pixels = [100, 200] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "3"'): + plot.pixels = [100] + + +def test_voxel_plot_xml_roundtrip(): + """Test VoxelPlot XML serialization and deserialization""" + plot = openmc.VoxelPlot(name='test_voxel') + plot.width = [10.0, 20.0, 30.0] + plot.pixels = [100, 200, 300] + plot.origin = [1.0, 2.0, 3.0] + plot.color_by = 'cell' + plot.filename = 'voxel_plot' + + # Convert to XML and back + elem = plot.to_xml_element() + new_plot = openmc.VoxelPlot.from_xml_element(elem) + + # Check all attributes preserved + assert new_plot.name == plot.name + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.origin == pytest.approx(plot.origin) + assert new_plot.color_by == plot.color_by + assert new_plot.filename == plot.filename + + +def test_plot_deprecation_warning(): + """Test that Plot class raises deprecation warning""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + openmc.Plot() + + assert len(w) == 1 + assert issubclass(w[0].category, FutureWarning) + assert "deprecated" in str(w[0].message).lower() + + +def test_plot_returns_slice_plot(): + """Test that Plot() returns a SlicePlot instance""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + # Should be an actual SlicePlot instance + assert isinstance(plot, openmc.SlicePlot) + + +def test_plot_type_setter_raises_error(): + """Test that setting plot.type raises a helpful error""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + with pytest.raises(TypeError, match="no longer supported"): + plot.type = 'voxel' + + with pytest.raises(TypeError, match="no longer supported"): + plot.type = 'slice' + + +def test_plot_type_getter_warns(): + """Test that getting plot.type raises a deprecation warning""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + plot_type = plot.type + + assert plot_type == 'slice' + assert len(w) == 1 + assert issubclass(w[0].category, FutureWarning) + assert "deprecated" in str(w[0].message).lower() + + +def test_plots_collection_mixed_types(): + """Test Plots collection with different plot types""" + slice_plot = openmc.SlicePlot(name='slice') + voxel_plot = openmc.VoxelPlot(name='voxel') + wireframe_plot = openmc.WireframeRayTracePlot(name='wireframe') + + plots = openmc.Plots([slice_plot, voxel_plot, wireframe_plot]) + + assert len(plots) == 3 + assert isinstance(plots[0], openmc.SlicePlot) + assert isinstance(plots[1], openmc.VoxelPlot) + assert isinstance(plots[2], openmc.WireframeRayTracePlot) + + +def test_plots_collection_xml_roundtrip(run_in_tmpdir): + """Test XML export and import with new plot types""" + s1 = openmc.SlicePlot(name='slice1') + s1.width = [10.0, 20.0] + s1.basis = 'xz' + + v1 = openmc.VoxelPlot(name='voxel1') + v1.width = [10.0, 20.0, 30.0] + + plots = openmc.Plots([s1, v1]) + plots.export_to_xml() + + # Read back + new_plots = openmc.Plots.from_xml() + + assert len(new_plots) == 2 + assert isinstance(new_plots[0], openmc.SlicePlot) + assert isinstance(new_plots[1], openmc.VoxelPlot) + assert new_plots[0].name == 'slice1' + assert new_plots[1].name == 'voxel1' + assert new_plots[0].basis == 'xz' + assert new_plots[0].width == pytest.approx([10.0, 20.0]) + assert new_plots[1].width == pytest.approx([10.0, 20.0, 30.0]) diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index bb8a1b785..394c09e73 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -1,5 +1,6 @@ from collections import Counter from math import pi +from pathlib import Path import openmc import openmc.lib @@ -35,21 +36,6 @@ def test_source(): assert src.strength == 1.0 -def test_spherical_uniform(): - r_outer = 2.0 - r_inner = 1.0 - thetas = (0.0, pi/2) - phis = (0.0, pi) - origin = (0.0, 1.0, 2.0) - - sph_indep_function = openmc.stats.spherical_uniform(r_outer, - r_inner, - thetas, - phis, - origin) - - assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent) - def test_point_cloud(): positions = [(1, 0, 2), (0, 1, 0), (0, 0, 3), (4, 9, 2)] strengths = [1, 2, 3, 4] @@ -106,6 +92,74 @@ def test_point_cloud_strengths(run_in_tmpdir, sphere_box_model): assert sampled_strength == expected_strength, f'Strength incorrect for {positions[i]}' +def test_decay_spectrum_parent_nuclide(run_in_tmpdir): + chain_file = Path('chain_decay_spectrum_parent.xml') + chain_file.write_text(""" + + + + 1000000.0 1.0 + + + + + 2000000.0 1.0 + + + +""") + + inner_sphere = openmc.Sphere(r=10.0) + outer_sphere = openmc.Sphere(r=20.0, boundary_type='vacuum') + + shell_mat = openmc.Material() + shell_mat.add_nuclide('H1', 1.0) + shell_mat.set_density('atom/b-cm', 1.0e-12) + + void_cell = openmc.Cell(region=-inner_sphere) + shell_cell = openmc.Cell(fill=shell_mat, region=+inner_sphere & -outer_sphere) + + model = openmc.Model() + model.geometry = openmc.Geometry([void_cell, shell_cell]) + model.materials = [shell_mat] + model.settings.run_mode = 'fixed source' + model.settings.photon_transport = True + model.settings.particles = 1000 + model.settings.batches = 5 + model.settings.source = openmc.IndependentSource( + particle='photon', + space=openmc.stats.Point((0.0, 0.0, 0.0)), + energy=openmc.stats.DecaySpectrum( + {'ParentA': 1.0, 'ParentB': 1.0}, + volume=1.0 + ) + ) + + tally = openmc.Tally() + tally.filters = [ + openmc.CellFilter([void_cell]), + openmc.ParticleFilter(['photon']), + openmc.EnergyFilter([0.0, 1.5e6, 2.5e6]), + openmc.ParentNuclideFilter(['ParentA', 'ParentB']) + ] + tally.scores = ['flux'] + model.tallies = [tally] + + with openmc.config.patch('chain_file', chain_file): + sp_filename = model.run() + + with openmc.StatePoint(sp_filename) as sp: + tally_out = sp.tallies[tally.id] + mean = tally_out.get_reshaped_data('mean').squeeze() + + assert mean.shape == (2, 2) + assert mean[0, 0] > 0.0 + assert mean[1, 1] > 0.0 + assert mean[0, 1] == 0.0 + assert mean[1, 0] == 0.0 + assert np.count_nonzero(mean) == 2 + + def test_source_file(): filename = 'source.h5' src = openmc.FileSource(path=filename) diff --git a/tests/unit_tests/test_source_biasing.py b/tests/unit_tests/test_source_biasing.py new file mode 100644 index 000000000..12588594c --- /dev/null +++ b/tests/unit_tests/test_source_biasing.py @@ -0,0 +1,276 @@ +"""Tests for source biasing using C++ sampling routines via openmc.lib + +This test module validates that the C++ distribution sampling implementations +correctly handle both unbiased and biased sampling when used in source +definitions. Each test: + +1. Creates a minimal model with a source using a specific energy distribution +2. Uses model.sample_external_source() to generate samples via openmc.lib +3. Extracts energies from the returned particle list +4. Validates that: + - Unbiased sampling produces the expected mean + - Biased sampling with importance weighting produces the expected mean + - Weights are correctly applied (non-unity for biased case) + +These tests complement the Python-level tests in test_stats.py by exercising +the full C++ sampling codepath that is used during actual simulations. +""" + +import numpy as np +import pytest +import openmc + +from tests.unit_tests import assert_sample_mean + + +@pytest.fixture +def model(): + """Create a minimal model for source sampling tests.""" + sphere = openmc.Sphere(r=100.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + geometry = openmc.Geometry([cell]) + settings = openmc.Settings(particles=100, batches=1) + space = openmc.stats.Point() + angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + settings.source = openmc.IndependentSource(space=space, angle=angle) + return openmc.Model(geometry=geometry, settings=settings) + + +@pytest.mark.flaky(reruns=1) +def test_discrete(run_in_tmpdir, model): + """Test Discrete distribution sampling via C++ routines.""" + vals = np.array([1.0, 2.0, 3.0]) + probs = np.array([0.1, 0.7, 0.2]) + exp_mean = (vals * probs).sum() + + # Create source with discrete energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Discrete(vals, probs) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = np.array([0.2, 0.1, 0.7]) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_uniform(run_in_tmpdir, model): + """Test Uniform distribution sampling via C++ routines.""" + a, b = 5.0, 10.0 + exp_mean = 0.5 * (a + b) + + # Create source with uniform energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Uniform(a, b) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.PowerLaw(a, b, 2) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_powerlaw(run_in_tmpdir, model): + """Test PowerLaw distribution sampling via C++ routines.""" + a, b, n = 1.0, 20.0, 2.0 + + # Determine mean of distribution + exp_mean = (n+1)*(b**(n+2) - a**(n+2))/((n+2)*(b**(n+1) - a**(n+1))) + + # Create source with powerlaw energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.PowerLaw(a, b, n) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Uniform(a, b) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_maxwell(run_in_tmpdir, model): + """Test Maxwell distribution sampling via C++ routines.""" + theta = 1.2895e6 + exp_mean = 3/2 * theta + + # Create source with Maxwell energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Maxwell(theta) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Maxwell(theta * 1.1) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_watt(run_in_tmpdir, model): + """Test Watt distribution sampling via C++ routines.""" + a, b = 0.965e6, 2.29e-6 + exp_mean = 3/2 * a + a**2 * b / 4 + + # Create source with Watt energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Watt(a, b) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Watt(a*1.05, b) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_tabular(run_in_tmpdir, model): + """Test Tabular distribution sampling via C++ routines.""" + # Test linear-linear sampling + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0, 6.0]) + + # Create tabular distribution and normalize to get expected mean + model.settings.source[0].energy = energy_dist = openmc.stats.Tabular(x, p, 'linear-linear') + energy_dist.normalize() + exp_mean = energy_dist.mean() + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Uniform(x[0], x[-1]) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_mixture(run_in_tmpdir, model): + """Test Mixture distribution sampling via C++ routines.""" + d1 = openmc.stats.Uniform(0, 5) + d2 = openmc.stats.Uniform(3, 7) + p = [0.5, 0.5] + + # Create mixture energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Mixture(p, [d1, d2]) + exp_mean = (2.5 + 5.0) / 2 + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample using biased sub-distribution + energy_dist.distribution[0].bias = openmc.stats.PowerLaw(0, 5, 2) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_normal(run_in_tmpdir, model): + """Test Normal distribution sampling via C++ routines.""" + mean_val = 25.0 + std_dev = 2.0 + + # Create source with normal energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Normal(mean_val, std_dev) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, mean_val) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Normal(mean_val * 1.1, std_dev) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, mean_val) + assert np.any(weights != 1.0) diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index 41906c80f..f19ea74a6 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -42,7 +42,7 @@ def test_source_file(run_in_tmpdir): assert np.all(arr['E'] == n - np.arange(n)) assert np.all(arr['wgt'] == 1.0) assert np.all(arr['delayed_group'] == 0) - assert np.all(arr['particle'] == 0) + assert np.all(arr['particle'] == 2112) # PDG number for neutron # Ensure sites read in are consistent sites = openmc.ParticleList.from_hdf5('test_source.h5') @@ -64,7 +64,7 @@ def test_source_file(run_in_tmpdir): dgs = np.array([s.delayed_group for s in sites]) assert np.all(dgs == 0) p_types = np.array([s.particle for s in sites]) - assert np.all(p_types == 0) + assert np.all(p_types == 2112) # PDG number for neutron # Ensure a ParticleList item is a SourceParticle site = sites[0] @@ -145,3 +145,27 @@ def test_source_file_transport(run_in_tmpdir): # Try running OpenMC model.run() + + +def test_source_file_photon_transport(run_in_tmpdir): + # Create a source file containing a photon. Note that photon_transport is + # not explicitly enabled in the settings -- it should be turned on + # automatically because the source file contains a photon. + particle = openmc.SourceParticle(E=1.0e6, particle='photon') + openmc.write_source_file([particle], 'photon_source.h5') + + # Create simple model to use the photon source file + model = openmc.Model() + al = openmc.Material() + al.add_element('Al', 1.0) + al.set_density('g/cm3', 2.7) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=al, region=-sph) + model.geometry = openmc.Geometry([cell]) + model.settings.source = openmc.FileSource(path='photon_source.h5') + model.settings.particles = 10 + model.settings.batches = 3 + model.settings.run_mode = 'fixed source' + + # Running OpenMC should succeed + model.run() diff --git a/tests/unit_tests/test_source_tokamak.py b/tests/unit_tests/test_source_tokamak.py new file mode 100644 index 000000000..f926c2007 --- /dev/null +++ b/tests/unit_tests/test_source_tokamak.py @@ -0,0 +1,263 @@ +import numpy as np +import pytest + +import openmc +import openmc.stats + +from tests.unit_tests import assert_sample_mean + + +def make_source(**kwargs): + """Build a valid TokamakSource, overriding defaults via kwargs.""" + r_over_a = np.linspace(0.0, 1.0, 10) + params = dict( + major_radius=620.0, + minor_radius=200.0, + elongation=1.8, + triangularity=0.45, + shafranov_shift=10.0, + r_over_a=r_over_a, + emission_density=(1.0 - r_over_a**2), + energy=openmc.stats.muir(e0=14.08e6, m_rat=5.0, kt=2.0e4), + ) + params.update(kwargs) + return openmc.TokamakSource(**params) + + +def test_tokamak_source_roundtrip(): + src = make_source( + phi_start=0.1, phi_extent=np.pi, n_alpha=51, vertical_shift=5.0, + strength=2.0, time=openmc.stats.Uniform(0.0, 1e-6)) + + elem = src.to_xml_element() + assert elem.get('type') == 'tokamak' + + new = openmc.SourceBase.from_xml_element(elem) + assert isinstance(new, openmc.TokamakSource) + assert new.major_radius == src.major_radius + assert new.minor_radius == src.minor_radius + assert new.elongation == src.elongation + assert new.triangularity == src.triangularity + assert new.shafranov_shift == src.shafranov_shift + assert new.phi_start == src.phi_start + assert new.phi_extent == src.phi_extent + assert new.n_alpha == src.n_alpha + assert new.vertical_shift == src.vertical_shift + assert new.strength == src.strength + np.testing.assert_allclose(new.r_over_a, src.r_over_a) + np.testing.assert_allclose(new.emission_density, src.emission_density) + assert len(new.energy) == 1 + assert isinstance(new.time, openmc.stats.Uniform) + assert new.time.a == src.time.a + assert new.time.b == src.time.b + + +def test_tokamak_source_default_time(): + src = make_source() + assert src.time is None + + new = openmc.SourceBase.from_xml_element(src.to_xml_element()) + assert new.time is None + + with pytest.raises(TypeError): + make_source(time=1.0) + + +def test_tokamak_source_multiple_energies(): + r_over_a = np.linspace(0.0, 1.0, 5) + energies = [openmc.stats.muir(e0=14.08e6, m_rat=5.0, kt=kt) + for kt in (1.0e4, 1.5e4, 2.0e4, 2.5e4, 3.0e4)] + src = make_source(r_over_a=r_over_a, + emission_density=np.ones_like(r_over_a), + energy=energies) + assert len(src.energy) == len(r_over_a) + + new = openmc.SourceBase.from_xml_element(src.to_xml_element()) + assert len(new.energy) == len(r_over_a) + + +@pytest.mark.parametrize("kwargs, match", [ + (dict(minor_radius=700.0), "smaller than major_radius"), + (dict(shafranov_shift=150.0), "half the minor_radius"), + (dict(emission_density=np.ones(5)), "same length as r_over_a"), + (dict(energy=[openmc.stats.muir(14.08e6, 5.0, 2.0e4)] * 2), + "Number of energy distributions"), + (dict(r_over_a=np.linspace(0.1, 1.0, 10)), "must start at 0"), + (dict(r_over_a=np.linspace(0.0, 0.9, 10)), "must end at 1"), + (dict(emission_density=-np.linspace(0.0, 1.0, 10)), "cannot be negative"), + (dict(emission_density=np.zeros(10)), "must contain a positive value"), +]) +def test_tokamak_source_invalid(kwargs, match): + with pytest.raises(ValueError, match=match): + make_source(**kwargs) + + +@pytest.mark.parametrize("value", [-1.5, 1.5]) +def test_tokamak_source_invalid_triangularity(value): + with pytest.raises(ValueError): + make_source(triangularity=value) + + +def test_tokamak_source_invalid_n_alpha(): + with pytest.raises(ValueError): + make_source(n_alpha=2) + + +@pytest.mark.parametrize(("attribute", "value", "match"), [ + ("minor_radius", 700.0, "smaller than major_radius"), + ("shafranov_shift", 150.0, "half the minor_radius"), + ("emission_density", np.ones(5), "same length as r_over_a"), + ("energy", [openmc.stats.delta_function(1.0)] * 2, + "Number of energy distributions"), +]) +def test_tokamak_source_mutation_validation(attribute, value, match): + src = make_source() + setattr(src, attribute, value) + with pytest.raises(ValueError, match=match): + src.to_xml_element() + + +@pytest.mark.parametrize(("emission_density", "expected_mean"), [ + ([1.0, 1.0], 2.0 / 3.0), + ([1.0, 0.0], 0.5), +]) +def test_tokamak_source_radial_sampling( + run_in_tmpdir, emission_density, expected_mean +): + """Check radial sampling for profiles on the coarsest valid grid.""" + major_radius = 620.0 + minor_radius = 200.0 + src = make_source( + major_radius=major_radius, + minor_radius=minor_radius, + elongation=1.0, + triangularity=0.0, + shafranov_shift=0.0, + r_over_a=[0.0, 1.0], + emission_density=emission_density, + energy=openmc.stats.delta_function(14.07e6), + ) + + sphere = openmc.Sphere(r=2000.0, boundary_type='vacuum') + model = openmc.Model( + geometry=openmc.Geometry([openmc.Cell(region=-sphere)]), + settings=openmc.Settings( + particles=100, batches=1, run_mode='fixed source', source=src), + ) + + sites = model.sample_external_source(20_000) + xyz = np.array([site.r for site in sites]) + major_r = np.hypot(xyz[:, 0], xyz[:, 1]) + r_over_a = np.hypot(major_r - major_radius, xyz[:, 2]) / minor_radius + assert_sample_mean(r_over_a, expected_mean) + + +def test_tokamak_source_poloidal_sampling(run_in_tmpdir): + """Check linear-linear poloidal sampling on a coarse internal grid.""" + major_radius = 620.0 + minor_radius = 200.0 + with pytest.warns(UserWarning, match="below 51"): + src = make_source( + major_radius=major_radius, + minor_radius=minor_radius, + elongation=1.0, + triangularity=0.0, + shafranov_shift=0.0, + emission_density=np.ones(10), + n_alpha=3, + energy=openmc.stats.delta_function(14.07e6), + ) + + sphere = openmc.Sphere(r=2000.0, boundary_type='vacuum') + model = openmc.Model( + geometry=openmc.Geometry([openmc.Cell(region=-sphere)]), + settings=openmc.Settings( + particles=100, batches=1, run_mode='fixed source', source=src), + ) + + sites = model.sample_external_source(100_000) + xyz = np.array([site.r for site in sites]) + major_r = np.hypot(xyz[:, 0], xyz[:, 1]) + + # With three alpha points, linear interpolation of cos(alpha) on [0, pi] + # gives 1 - 2*alpha/pi. Integrating the resulting density gives this mean. + expected_R = ( + major_radius + + 2.0 * minor_radius**2 / (np.pi**2 * major_radius) + ) + assert_sample_mean(major_r, expected_R) + + +@pytest.mark.flaky(reruns=1) +def test_tokamak_source_sampling(run_in_tmpdir): + """Exercise the compiled C++ sampling path and check invariants. + + Sampled moments are compared against direct numerical quadrature of the + exact source density S(r)*R*|J|, where the Jacobian J of the flux-surface + map is computed from analytic partial derivatives. This check is + independent of the Bernstein-mixture factorization used by the + implementation. + """ + R0, a, kappa, delta = 620.0, 200.0, 1.8, -0.5 + shafranov, zshift = 40.0, 25.0 + phi_start, phi_extent = 0.5, np.pi / 2 + + # Fine grids to make discretization error negligible relative to + # statistical uncertainty + r_over_a = np.linspace(0.0, 1.0, 200) + src = make_source( + major_radius=R0, minor_radius=a, elongation=kappa, + triangularity=delta, shafranov_shift=shafranov, vertical_shift=zshift, + r_over_a=r_over_a, emission_density=1.0 - r_over_a**2, + phi_start=phi_start, phi_extent=phi_extent, n_alpha=201, + energy=openmc.stats.delta_function(14.07e6)) + + sphere = openmc.Sphere(r=2000.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + settings = openmc.Settings( + particles=100, batches=1, run_mode='fixed source', source=src) + model = openmc.Model(geometry=openmc.Geometry([cell]), settings=settings) + + n_samples = 20_000 + sites = model.sample_external_source(n_samples) + + xyz = np.array([s.r for s in sites]) + R = np.hypot(xyz[:, 0], xyz[:, 1]) + z = xyz[:, 2] + + # Energy, weight, and time invariants + assert np.all([s.E == 14.07e6 for s in sites]) + assert np.all([s.wgt == 1.0 for s in sites]) + assert np.all([s.time == 0.0 for s in sites]) + + # Toroidal angle within the requested sector + phi = np.arctan2(xyz[:, 1], xyz[:, 0]) + assert phi.min() >= phi_start + assert phi.max() <= phi_start + phi_extent + + # Positions bounded by the last closed flux surface + assert R.min() >= R0 - a + assert R.max() <= R0 + a + shafranov + assert np.abs(z - zshift).max() <= kappa * a + + # Up-down symmetry about the vertical shift + assert_sample_mean(z, zshift) + + # Reference moments by 2D quadrature of the exact density + r = np.linspace(0.0, 1.0, 1001)[:, np.newaxis] # r/a + alpha = np.linspace(0.0, 2 * np.pi, 2001)[np.newaxis, :] + psi = alpha + delta * np.sin(alpha) + R_map = R0 + a * r * np.cos(psi) + shafranov * (1.0 - r**2) + Z_map = kappa * a * r * np.sin(alpha) + dR_dr = a * np.cos(psi) - 2.0 * shafranov * r + dR_da = -a * r * np.sin(psi) * (1.0 + delta * np.cos(alpha)) + dZ_dr = kappa * a * np.sin(alpha) * np.ones_like(psi) + dZ_da = kappa * a * r * np.cos(alpha) + jac = np.abs(dR_dr * dZ_da - dR_da * dZ_dr) + dens = (1.0 - r**2) * R_map * jac + norm = dens.sum() + expected_R = (R_map * dens).sum() / norm + expected_z2 = (Z_map**2 * dens).sum() / norm + + assert_sample_mean(R, expected_R) + assert_sample_mean((z - zshift)**2, expected_z2) diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 998d4b984..2754cf5d0 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -1,19 +1,14 @@ from math import pi +from pathlib import Path import numpy as np import pytest import openmc import openmc.stats +from openmc.stats.univariate import _INTERPOLATION_SCHEMES, DecaySpectrum from scipy.integrate import trapezoid - -def assert_sample_mean(samples, expected_mean): - # Calculate sample standard deviation - std_dev = samples.std() / np.sqrt(samples.size - 1) - - # Means should agree within 4 sigma 99.993% of the time. Note that this is - # expected to fail about 1 out of 16,000 times - assert np.abs(expected_mean - samples.mean()) < 4*std_dev +from tests.unit_tests import assert_sample_mean @pytest.mark.flaky(reruns=1) @@ -47,8 +42,19 @@ def test_discrete(): # sample discrete distribution and check that the mean of the samples is # within 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d3.sample(n_samples) + samples, weights = d3.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d3.bias = np.array([0.2, 0.1, 0.7]) + bias_elem = d3.to_xml_element('distribution') + d4 = openmc.stats.Univariate.from_xml_element(bias_elem) + np.testing.assert_array_equal(d4.bias, [0.2, 0.1, 0.7]) + samples, weights = d4.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) def test_delta_function(): @@ -83,6 +89,28 @@ def test_merge_discrete(): assert triple.integral() == pytest.approx(6.0) +def test_merge_discrete_with_bias(): + # Two discrete distributions with different biases + d1 = openmc.stats.Discrete([1.0, 2.0], [0.5, 0.5]) + d2 = openmc.stats.Discrete([2.0, 3.0], [0.3, 0.7], bias=[0.1, 0.9]) + + merged = openmc.stats.Discrete.merge([d1, d2], [0.6, 0.4]) + exp_mean = 0.6 * d1.mean() + 0.4 * d2.mean() + + # Verify merged distribution has correct x values + assert set(merged.x) == {1.0, 2.0, 3.0} + + # Bias should not be changed in original distributions + assert d1.bias is None + assert np.all(d2.bias == [0.1, 0.9]) + + # Sample and verify bias is applied correctly + samples, weights = merged.sample(10_000) + + # Verify weighted mean matches expected unbiased mean + assert_sample_mean(samples*weights, exp_mean) + + def test_clip_discrete(): # Create discrete distribution with two points that are not important, one # because the x value is very small, and one because the p value is very @@ -125,8 +153,19 @@ def test_uniform(): # std. dev. of the expected mean exp_mean = 0.5 * (a + b) n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d.bias = openmc.stats.PowerLaw(a, b, 2) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.PowerLaw) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -147,8 +186,19 @@ def test_powerlaw(): # sample power law distribution and check that the mean of the samples is # within 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d.bias = openmc.stats.Uniform(a, b) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Uniform) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -166,13 +216,25 @@ def test_maxwell(): # sample maxwell distribution and check that the mean of the samples is # within 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) # A second sample starting from a different seed - samples_2 = d.sample(n_samples) + samples_2, weights_2 = d.sample(n_samples) assert_sample_mean(samples_2, exp_mean) assert samples_2.mean() != samples.mean() + assert np.all(weights_2 == 1) + + # Test biased distribution + d.bias = openmc.stats.Maxwell((theta * 1.1)) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Maxwell) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -195,29 +257,51 @@ def test_watt(): # sample Watt distribution and check that the mean of the samples is within # 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution with 5 percent higher T_e + d.bias = openmc.stats.Watt(a*1.05, b) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Watt) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) def test_tabular(): # test linear-linear sampling - x = np.array([0.0, 5.0, 7.0, 10.0]) + x = np.array([0.001, 5.0, 7.0, 10.0]) p = np.array([10.0, 20.0, 5.0, 6.0]) d = openmc.stats.Tabular(x, p, 'linear-linear') n_samples = 100_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, d.mean()) + assert np.all(weights == 1.0) - # test linear-linear normalization - d.normalize() - assert d.integral() == pytest.approx(1.0) + for scheme in _INTERPOLATION_SCHEMES: + # test sampling + d = openmc.stats.Tabular(x, p, scheme) + n_samples = 100_000 + samples = d.sample(n_samples)[0] + assert_sample_mean(samples, d.mean()) # test histogram sampling d = openmc.stats.Tabular(x, p, interpolation='histogram') - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, d.mean()) + assert np.all(weights == 1.0) + # Multiplying the probabilities should preserve the mean but change the integral + d2 = openmc.stats.Tabular(x, p*2, interpolation='histogram') + assert d2.mean() == pytest.approx(d.mean()) + assert d2.integral() == pytest.approx(2.0*d.integral()) + + # Normalizing should result in an integral of 1 d.normalize() assert d.integral() == pytest.approx(1.0) @@ -226,7 +310,7 @@ def test_tabular(): d = openmc.stats.Tabular(x, p[:-1], interpolation='histogram') d.cdf() d.mean() - assert_sample_mean(d.sample(n_samples), d.mean()) + assert_sample_mean(d.sample(n_samples)[0], d.mean()) # passing a shorter probability set should raise an error for linear-linear with pytest.raises(ValueError): @@ -238,6 +322,16 @@ def test_tabular(): d = openmc.stats.Tabular(x, p, interpolation='linear-linear') d.cdf() + # Test biased distribution + d.bias = openmc.stats.Uniform(x[0], x[-1]) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Uniform) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, d2.mean()) + assert np.all(weights != 1.0) + def test_tabular_from_xml(): x = np.array([0.0, 5.0, 7.0, 10.0]) @@ -288,8 +382,9 @@ def test_mixture(): # Sample and make sure sample mean is close to expected mean n_samples = 1_000_000 - samples = mix.sample(n_samples) + samples, weights = mix.sample(n_samples) assert_sample_mean(samples, (2.5 + 5.0)/2) + assert np.all(weights == 1.0) elem = mix.to_xml_element('distribution') @@ -298,6 +393,26 @@ def test_mixture(): assert d.distribution == [d1, d2] assert len(d) == 4 + # Test biased sub-distribution + d.distribution[0].bias = openmc.stats.PowerLaw(0, 5, 2) + bias_elem = d.to_xml_element('distribution') + d3 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d3.distribution[0].bias, openmc.stats.PowerLaw) + samples, weights = d3.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, (2.5 + 5.0)/2) + + # Test biased meta-probability + d.distribution[0].bias = None + d.bias = [0.25, 0.75] + bias_elem_2 = d.to_xml_element('distribution') + d4 = openmc.stats.Univariate.from_xml_element(bias_elem_2) + assert isinstance (d4.bias, np.ndarray) + samples, weights = d4.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, (2.5 + 5.0)/2) + assert np.all(weights != 1.0) + def test_mixture_clip(): # Create mixture distribution containing a discrete distribution with two @@ -330,6 +445,13 @@ def test_mixture_clip(): with pytest.warns(UserWarning): mix_clip = mix.clip(1e-6) + # Make sure warning is raised if a biased Discrete is clipped + d3 = openmc.stats.Discrete([1.0, 1.001], [1.0, 0.7e-8]) + d3.bias = [0.9, 0.1] + mix = openmc.stats.Mixture([1.0, 1.0], [d3, d2]) + with pytest.raises(RuntimeError): + mix_clip = mix.clip(1e-6) + def test_polar_azimuthal(): # default polar-azimuthal should be uniform in mu and phi @@ -365,12 +487,17 @@ def test_polar_azimuthal(): def test_isotropic(): d = openmc.stats.Isotropic() + mu = openmc.stats.Uniform(-1.0, 1.0) + phi = openmc.stats.PowerLaw(0., 2*np.pi, 2) + d2 = openmc.stats.PolarAzimuthal(mu, phi) + d.bias = d2 elem = d.to_xml_element() assert elem.tag == 'angle' assert elem.attrib['type'] == 'isotropic' d = openmc.stats.Isotropic.from_xml_element(elem) assert isinstance(d, openmc.stats.Isotropic) + assert isinstance(d.bias, openmc.stats.PolarAzimuthal) def test_monodirectional(): @@ -387,6 +514,7 @@ def test_cartesian(): x = openmc.stats.Uniform(-10., 10.) y = openmc.stats.Uniform(-10., 10.) z = openmc.stats.Uniform(0., 20.) + z.bias = openmc.stats.PowerLaw(0., 20., 3) d = openmc.stats.CartesianIndependent(x, y, z) elem = d.to_xml_element() @@ -402,6 +530,7 @@ def test_cartesian(): d = openmc.stats.Spatial.from_xml_element(elem) assert isinstance(d, openmc.stats.CartesianIndependent) + assert isinstance (d.z.bias, openmc.stats.PowerLaw) def test_box(): @@ -432,6 +561,113 @@ def test_point(): assert d.xyz == pytest.approx(p) +def test_spherical_uniform(): + r_outer = 2.0 + r_inner = 1.0 + thetas = (0.0, pi/2) + phis = (0.0, pi) + origin = (0.0, 1.0, 2.0) + + sph_indep_function = openmc.stats.spherical_uniform(r_outer, + r_inner, + thetas, + phis, + origin) + + assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent) + + +def test_cylindrical_uniform(): + r_outer = 2.0 + r_inner = 1.0 + height = 1.0 + phis = (0.0, pi) + origin = (0.0, 1.0, 2.0) + + dist = openmc.stats.cylindrical_uniform(r_outer, height, r_inner, phis, + origin=origin) + + assert isinstance(dist, openmc.stats.CylindricalIndependent) + + # Check r distribution (PowerLaw with exponent 1 for uniform area sampling) + assert isinstance(dist.r, openmc.stats.PowerLaw) + assert dist.r.a == pytest.approx(r_inner) + assert dist.r.b == pytest.approx(r_outer) + assert dist.r.n == pytest.approx(1.0) + + # Check phi distribution + assert isinstance(dist.phi, openmc.stats.Uniform) + assert dist.phi.a == pytest.approx(phis[0]) + assert dist.phi.b == pytest.approx(phis[1]) + + # Check z distribution (centered on origin along z_dir) + assert isinstance(dist.z, openmc.stats.Uniform) + assert dist.z.a == pytest.approx(-height / 2) + assert dist.z.b == pytest.approx(height / 2) + + # Check origin and default directions + np.testing.assert_allclose(dist.origin, origin) + np.testing.assert_allclose(dist.r_dir, [1., 0., 0.]) + np.testing.assert_allclose(dist.z_dir, [0., 0., 1.]) + + # XML round-trip preserves all parameters + elem = dist.to_xml_element() + dist2 = openmc.stats.CylindricalIndependent.from_xml_element(elem) + np.testing.assert_allclose(dist2.origin, origin) + np.testing.assert_allclose(dist2.r_dir, dist.r_dir) + np.testing.assert_allclose(dist2.z_dir, dist.z_dir) + + +def test_cylindrical_uniform_tilted(): + # Test with non-default axis orientation (y-axis as cylinder axis) + dist = openmc.stats.cylindrical_uniform( + r_outer=3.0, height=2.0, r_dir=(1., 0., 0.), z_dir=(0., 1., 0.) + ) + np.testing.assert_allclose(dist.z_dir, [0., 1., 0.]) + np.testing.assert_allclose(dist.r_dir, [1., 0., 0.]) + + # XML round-trip preserves tilted directions + elem = dist.to_xml_element() + dist2 = openmc.stats.CylindricalIndependent.from_xml_element(elem) + np.testing.assert_allclose(dist2.z_dir, dist.z_dir) + np.testing.assert_allclose(dist2.r_dir, dist.r_dir) + + +def test_cylindrical_uniform_ring(): + # height=0 should produce a flat ring (delta function at z=0) + r_outer = 2.0 + r_inner = 1.0 + phis = (0.0, pi) + origin = (0.0, 1.0, 2.0) + + dist = openmc.stats.cylindrical_uniform(r_outer, 0.0, r_inner, phis, + origin=origin) + + assert isinstance(dist, openmc.stats.CylindricalIndependent) + + # Check r distribution + assert isinstance(dist.r, openmc.stats.PowerLaw) + assert dist.r.a == pytest.approx(r_inner) + assert dist.r.b == pytest.approx(r_outer) + assert dist.r.n == pytest.approx(1.0) + + # Check phi distribution + assert isinstance(dist.phi, openmc.stats.Uniform) + assert dist.phi.a == pytest.approx(phis[0]) + assert dist.phi.b == pytest.approx(phis[1]) + + # z distribution must be a delta function at 0.0 (local frame) + assert isinstance(dist.z, openmc.stats.Discrete) + assert dist.z.x[0] == pytest.approx(0.0) + + # XML round-trip + elem = dist.to_xml_element() + dist2 = openmc.stats.CylindricalIndependent.from_xml_element(elem) + np.testing.assert_allclose(dist2.origin, origin) + np.testing.assert_allclose(dist2.r_dir, dist.r_dir) + np.testing.assert_allclose(dist2.z_dir, dist.z_dir) + + @pytest.mark.flaky(reruns=1) def test_normal(): mean = 10.0 @@ -448,8 +684,105 @@ def test_normal(): # sample normal distribution n_samples = 100_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d.bias = openmc.stats.Normal(10.0, 4.0) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Normal) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, mean) + assert np.all(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_normal_truncated(): + mean = 10.0 + std_dev = 2.0 + lower = 6.0 + upper = 14.0 + + d = openmc.stats.Normal(mean, std_dev, lower, upper) + + # Check attributes + assert d.mean_value == pytest.approx(mean) + assert d.std_dev == pytest.approx(std_dev) + assert d.lower == pytest.approx(lower) + assert d.upper == pytest.approx(upper) + assert len(d) == 4 + assert d.support == (lower, upper) + + # Test XML round-trip + elem = d.to_xml_element('distribution') + assert elem.attrib['type'] == 'normal' + params = elem.attrib['parameters'].split() + assert len(params) == 4 + + d2 = openmc.stats.Normal.from_xml_element(elem) + assert d2.mean_value == pytest.approx(mean) + assert d2.std_dev == pytest.approx(std_dev) + assert d2.lower == pytest.approx(lower) + assert d2.upper == pytest.approx(upper) + + # Test PDF evaluation + # PDF should be zero outside bounds + assert d.evaluate(lower - 1.0) == 0.0 + assert d.evaluate(upper + 1.0) == 0.0 + + # PDF should be positive inside bounds + assert d.evaluate(mean) > 0.0 + + # PDF should be higher than untruncated at the mean (due to renormalization) + d_unbounded = openmc.stats.Normal(mean, std_dev) + assert d.evaluate(mean) > d_unbounded.evaluate(mean) + + # Verify that PDF integrates to approximately 1 + x = np.linspace(lower, upper, 1000) + integral = trapezoid(d.evaluate(x), x) + assert integral == pytest.approx(1.0, rel=0.01) + + # Sample truncated distribution + n_samples = 10_000 + samples, weights = d.sample(n_samples) + + # All samples should be within bounds + assert np.all(samples >= lower) + assert np.all(samples <= upper) + + # Weights should all be 1 (no biasing) + assert np.all(weights == 1.0) + + +def test_normal_truncated_one_sided(): + # Test lower-bounded only (positive half-normal centered at 0) + d_lower = openmc.stats.Normal(0.0, 1.0, lower=0.0) + assert d_lower.lower == 0.0 + assert d_lower.upper == np.inf + assert d_lower.evaluate(-1.0) == 0.0 + assert d_lower.evaluate(1.0) > 0.0 + + # PDF at 0 should be approximately 2 * 0.3989 ≈ 0.798 (half-normal) + assert d_lower.evaluate(0.0) == pytest.approx(0.798, rel=0.01) + + # Test upper-bounded only + d_upper = openmc.stats.Normal(0.0, 1.0, upper=0.0) + assert d_upper.lower == -np.inf + assert d_upper.upper == 0.0 + assert d_upper.evaluate(1.0) == 0.0 + assert d_upper.evaluate(-1.0) > 0.0 + + +def test_normal_truncated_errors(): + # Invalid bounds (lower >= upper) + with pytest.raises(ValueError): + openmc.stats.Normal(0.0, 1.0, lower=1.0, upper=0.0) + + with pytest.raises(ValueError): + openmc.stats.Normal(0.0, 1.0, lower=1.0, upper=1.0) @pytest.mark.flaky(reruns=1) @@ -468,8 +801,9 @@ def test_muir(): # sample muir distribution n_samples = 100_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, mean) + assert np.all(weights == 1.0) @pytest.mark.flaky(reruns=1) @@ -502,20 +836,71 @@ def test_combine_distributions(): assert isinstance(mixed, openmc.stats.Mixture) assert len(mixed.distribution) == 2 assert len(mixed.probability) == 2 + assert mixed == openmc.stats.combine_distributions([mixed], [1.0]) + + # Mixture combined with another distribution: probabilities should be + # correctly scaled when the Mixture is flattened + d_a = openmc.stats.delta_function(1.0) + d_b = openmc.stats.delta_function(2.0) + m = openmc.stats.Mixture([0.3, 0.7], [d_a, d_b]) + extra = openmc.stats.delta_function(3.0) + result = openmc.stats.combine_distributions([m, extra], [0.5, 0.5]) + assert isinstance(result, openmc.stats.Discrete) + assert result.x == pytest.approx([1.0, 2.0, 3.0]) + assert result.p == pytest.approx([0.5*0.3, 0.5*0.7, 0.5]) + + # Passing a Mixture with a bias should warn that the bias is dropped + biased_m = openmc.stats.Mixture([0.5, 0.5], [d_a, d_b], bias=[0.8, 0.2]) + with pytest.warns(UserWarning, match='bias'): + openmc.stats.combine_distributions([biased_m], [1.0]) + + # Single tabular returns a tabular distribution with scaled probabilities + t_single = openmc.stats.Tabular([0.0, 1.0], [2.0, 0.0]) + scaled = openmc.stats.combine_distributions([t_single], [0.25]) + assert isinstance(scaled, openmc.stats.Tabular) + assert scaled.p == pytest.approx([0.5, 0.0]) + + # Mixture with biased tabular should preserve unbiased mean via weights + bias = openmc.stats.Tabular([0.0, 1.0], [2.0, 0.0]) + t_biased = openmc.stats.Tabular([0.0, 1.0], [1.0, 1.0], bias=bias) + d1 = openmc.stats.delta_function(0.0) + mixed = openmc.stats.combine_distributions([t_biased, d1], [0.5, 0.5]) + assert isinstance(mixed, openmc.stats.Mixture) + samples, weights = mixed.sample(10_000) + assert_sample_mean(samples*weights, 0.25) # Combine 1 discrete and 2 tabular -- the tabular distributions should # combine to produce a uniform distribution with mean 0.5. The combined # distribution should have a mean of 0.25. t1 = openmc.stats.Tabular([0., 1.], [2.0, 0.0]) t2 = openmc.stats.Tabular([0., 1.], [0.0, 2.0]) - d1 = openmc.stats.Discrete([0.0], [1.0]) + d1 = openmc.stats.delta_function(0.0) combined = openmc.stats.combine_distributions([t1, t2, d1], [0.25, 0.25, 0.5]) assert combined.integral() == pytest.approx(1.0) # Sample the combined distribution and make sure the sample mean is within # uncertainty of the expected value - samples = combined.sample(10_000) + samples, weights = combined.sample(10_000) assert_sample_mean(samples, 0.25) + assert np.all(weights == 1.0) + + # If biased/unbiased Discrete distributions are combined, unbiased probability + # should be conserved and points from both original distributions should be + # assigned bias probabilities. + x1 = [0.0, 1.0, 10.0] + p1 = [0.3, 0.2, 0.5] + b1 = [0.2, 0.5, 0.3] + d1 = openmc.stats.Discrete(x1, p1, b1) + x2 = [0.5, 1.0, 5.0] + p2 = [0.4, 0.5, 0.1] + d2 = openmc.stats.Discrete(x2, p2) + combined = openmc.stats.combine_distributions([d1, d2, t1], [0.25, 0.25, 0.5]) + + p3 = [0.075, 0.1, 0.175, 0.025, 0.125] + b3 = [0.05, 0.1, 0.25, 0.025, 0.075] + assert all(combined.distribution[-1].p == p3) + assert all(combined.distribution[-1].bias == b3) + def test_reference_vwu_projection(): """When a non-orthogonal vector is provided, the setter should project out @@ -546,3 +931,218 @@ def test_reference_vwu_normalization(): # reference_v should be unit length assert np.isclose(np.linalg.norm(reference_v), 1.0, atol=1e-12) + + +def test_fusion_spectrum_dd(): + d = openmc.stats.fusion_neutron_spectrum(10e3, 'DD') + assert isinstance(d, openmc.stats.Normal) + + # E_0 for D(d,n)3He is ~2.45 MeV; thermal shift at 10 keV should be + # several tens of keV, so mean should be noticeably above E_0 + assert d.mean_value > 2.45e6 + assert d.mean_value < 2.6e6 + + # Standard deviation should be positive and on order of ~50-100 keV + assert d.std_dev > 30e3 + assert d.std_dev < 200e3 + + +def test_fusion_spectrum_dt(): + d = openmc.stats.fusion_neutron_spectrum(10e3, 'DT') + assert isinstance(d, openmc.stats.Normal) + + # E_0 for T(d,n)alpha is ~14.02 MeV; with thermal shift mean should be + # above E_0 by several tens of keV + assert d.mean_value > 14.02e6 + assert d.mean_value < 14.2e6 + + # Standard deviation should be on order of ~200-400 keV + assert d.std_dev > 100e3 + assert d.std_dev < 500e3 + + +def test_fusion_spectrum_temp_continuity(): + # Verify the low-T and high-T formulas produce nearly identical results + # at the 40 keV switchover point + d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DT') + d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DT') + + assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3) + assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3) + + # Same check for DD + d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DD') + d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DD') + + assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3) + assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3) + + +def test_fusion_spectrum_high_temp(): + # At T_i = 80 keV (high-T regime), ensure the function still produces + # reasonable results using Table IV formulas + for reactants in ('DD', 'DT'): + d = openmc.stats.fusion_neutron_spectrum(80e3, reactants) + assert isinstance(d, openmc.stats.Normal) + assert d.mean_value > 0 + assert d.std_dev > 0 + + # DT mean at 80 keV should be higher than at 10 keV + d_10 = openmc.stats.fusion_neutron_spectrum(10e3, 'DT') + d_80 = openmc.stats.fusion_neutron_spectrum(80e3, 'DT') + assert d_80.mean_value > d_10.mean_value + assert d_80.std_dev > d_10.std_dev + + +def test_fusion_spectrum_zero_temp(): + # At very low temperature, mean should approach E_0 and width should + # approach zero + d = openmc.stats.fusion_neutron_spectrum(1.0, 'DT') + assert d.mean_value == pytest.approx(14.049e6, rel=1e-3) + assert d.std_dev < 5e3 # width approaches zero at low temperature + + +def test_fusion_spectrum_invalid(): + # Invalid reactant string should raise an error + with pytest.raises(ValueError): + openmc.stats.fusion_neutron_spectrum(10e3, '🐔🧇') + + # Negative temperature should raise an error + with pytest.raises(ValueError): + openmc.stats.fusion_neutron_spectrum(-10e3, 'DT') + + # Temperature above 100 keV should raise an error + with pytest.raises(ValueError): + openmc.stats.fusion_neutron_spectrum(101e3, 'DT') + + +@pytest.fixture(autouse=False) +def decay_spectrum_chain(): + """Set chain_file for the duration of a test and clear the _photon_integral + cache so results from a different chain don't bleed across tests.""" + CHAIN_FILE = (Path(__file__).parents[1] / 'chain_simple.xml').resolve() + DecaySpectrum._photon_integral.cache_clear() + with openmc.config.patch('chain_file', CHAIN_FILE): + yield + DecaySpectrum._photon_integral.cache_clear() + + +def test_decay_spectrum_construction(): + nuclides = {'I135': 1.5e-3, 'Xe135': 8.2e-4} + d = openmc.stats.DecaySpectrum(nuclides, volume=100.0) + assert d.nuclides == nuclides + assert d.volume == pytest.approx(100.0) + assert len(d) == 2 + + +def test_decay_spectrum_validation(): + # nuclides must be a dict + with pytest.raises(TypeError): + openmc.stats.DecaySpectrum(['I135'], volume=1.0) + + # densities must be > 0 + with pytest.raises(ValueError): + openmc.stats.DecaySpectrum({'I135': -1.0}, volume=1.0) + + # volume must be > 0 + with pytest.raises(ValueError): + openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=-1.0) + + with pytest.raises(ValueError): + openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=0.0) + + +def test_decay_spectrum_xml_roundtrip(): + nuclides = {'I135': 1.5e-3, 'Xe135': 8.2e-4} + d = openmc.stats.DecaySpectrum(nuclides, volume=100.0) + + elem = d.to_xml_element('energy') + assert elem.get('type') == 'decay_spectrum' + assert float(elem.get('volume')) == pytest.approx(100.0) + assert elem.findtext('nuclides').split() == list(nuclides) + assert [float(x) for x in elem.findtext('parameters').split()] == pytest.approx( + list(nuclides.values())) + + # Round-trip via DecaySpectrum.from_xml_element + d2 = openmc.stats.DecaySpectrum.from_xml_element(elem) + assert d2.nuclides == nuclides + assert d2.volume == pytest.approx(100.0) + + # Round-trip via the Univariate dispatcher + d3 = openmc.stats.Univariate.from_xml_element(elem) + assert isinstance(d3, openmc.stats.DecaySpectrum) + assert d3 == d + + +def test_decay_spectrum_to_distribution(decay_spectrum_chain): + # Single emitting nuclide -> concrete distribution, not None + d = openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=10.0) + dist = d.to_distribution() + assert dist is not None + + # Result is cached on second call + dist2 = d.to_distribution() + assert dist2 is dist + + # Nuclide with no photon source -> None + d_stable = openmc.stats.DecaySpectrum({'Xe136': 1e-3}, volume=10.0) + assert d_stable.to_distribution() is None + + # Mixture of emitters -> non-None combined distribution + d_mix = openmc.stats.DecaySpectrum( + {'I135': 1e-3, 'Xe135': 5e-4}, volume=10.0 + ) + dist_mix = d_mix.to_distribution() + assert dist_mix is not None + + +def test_decay_spectrum_integral(decay_spectrum_chain): + # For an emitting nuclide, integral should be > 0 + d = openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=10.0) + assert d.integral() > 0.0 + + # Proportional to density: doubling density doubles integral + d2 = openmc.stats.DecaySpectrum({'I135': 2e-3}, volume=10.0) + assert d2.integral() == pytest.approx(2.0 * d.integral()) + + # Proportional to volume + d3 = openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=20.0) + assert d3.integral() == pytest.approx(2.0 * d.integral()) + + # Pure non-emitter -> 0.0 + d_stable = openmc.stats.DecaySpectrum({'Xe136': 1e-3}, volume=10.0) + assert d_stable.integral() == pytest.approx(0.0) + + +def test_decay_spectrum_clip(decay_spectrum_chain): + # Stable / non-emitting nuclides are removed unconditionally + d = openmc.stats.DecaySpectrum( + {'I135': 1e-3, 'Xe135': 5e-4, 'Xe136': 1.0, 'Cs135': 1.0}, + volume=10.0, + ) + d_clip = d.clip() + assert 'Xe136' not in d_clip.nuclides + assert 'Cs135' not in d_clip.nuclides + assert 'I135' in d_clip.nuclides + assert 'Xe135' in d_clip.nuclides + # Original is unchanged + assert 'Xe136' in d.nuclides + + # inplace=True modifies and returns the same object + d_same = d.clip(inplace=True) + assert d_same is d + assert 'Xe136' not in d.nuclides + + # A nuclide with negligible emission rate is removed by tolerance clipping. + # U235 has a very small integral (~4e-17 Bq/atom) compared with I135 (~4e-5) + d_tight = openmc.stats.DecaySpectrum( + {'I135': 1e-3, 'U235': 1e-3}, volume=10.0 + ) + d_tight_clip = d_tight.clip(tolerance=1e-9) + assert 'U235' not in d_tight_clip.nuclides + assert 'I135' in d_tight_clip.nuclides + + # All non-emitters -> empty nuclides dict + d_empty = openmc.stats.DecaySpectrum({'Xe136': 1e-3}, volume=10.0) + d_empty.clip(inplace=True) + assert d_empty.nuclides == {} diff --git a/tests/unit_tests/test_surface.py b/tests/unit_tests/test_surface.py index e9560223d..f5c0b8f8b 100644 --- a/tests/unit_tests/test_surface.py +++ b/tests/unit_tests/test_surface.py @@ -7,6 +7,12 @@ import openmc import pytest +def test_id(): + for i in range(-10, 1): + with pytest.raises(ValueError): + openmc.Plane(a=1, b=2, c=-1, d=3, surface_id=i) + + def assert_infinite_bb(s): ll, ur = (-s).bounding_box assert np.all(np.isinf(ll)) @@ -189,6 +195,12 @@ def test_cylinder(): assert s.dy == -1 assert s.dz == 1 assert s.r == 2 + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.Cylinder(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r=0.0) + with pytest.raises(ValueError): + openmc.Cylinder(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r=-1.0) # Check bounding box assert_infinite_bb(s) @@ -238,6 +250,12 @@ def test_xcylinder(): assert s.y0 == y assert s.z0 == z assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.XCylinder(y0=y, z0=z, r=0.0) + with pytest.raises(ValueError): + openmc.XCylinder(y0=y, z0=z, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -284,6 +302,12 @@ def test_ycylinder(): assert s.x0 == x assert s.z0 == z assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.YCylinder(x0=x, z0=z, r=0.0) + with pytest.raises(ValueError): + openmc.YCylinder(x0=x, z0=z, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -321,6 +345,12 @@ def test_zcylinder(): assert s.x0 == x assert s.y0 == y assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.ZCylinder(x0=x, y0=y, r=0.0) + with pytest.raises(ValueError): + openmc.ZCylinder(x0=x, y0=y, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -359,6 +389,12 @@ def test_sphere(): assert s.y0 == y assert s.z0 == z assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.Sphere(x0=x, y0=y, z0=z, r=0.0) + with pytest.raises(ValueError): + openmc.Sphere(x0=x, y0=y, z0=z, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -398,6 +434,12 @@ def cone_common(apex, r2, cls): assert s.y0 == y assert s.z0 == z assert s.r2 == r2 + + # Check radius must be positive + with pytest.raises(ValueError): + cls(x0=x, y0=y, z0=z, r2=0.0) + with pytest.raises(ValueError): + cls(x0=x, y0=y, z0=z, r2=-1.0) # Check bounding box assert_infinite_bb(s) @@ -436,6 +478,12 @@ def test_cone(): assert s.dy == -1 assert s.dz == 1 assert s.r2 == 4 + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.Cone(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r2=0.0) + with pytest.raises(ValueError): + openmc.Cone(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r2=-1.0) # Check bounding box assert_infinite_bb(s) @@ -616,6 +664,13 @@ def torus_common(center, R, r1, r2, cls): assert s.a == R assert s.b == r1 assert s.c == r2 + + # Check radius must be positive + params = [(0.0, r1, r2), (R, 0.0, r2), (R, r1, 0.0), + (-1.0, r1, r2), (R, -1.0, r2), (R, r1, -1.0)] + for a,b,c in params: + with pytest.raises(ValueError): + cls(x0=x, y0=y, z0=z, a=a, b=b, c=c) # evaluate method assert s.evaluate((x, y, z)) > 0.0 diff --git a/tests/unit_tests/test_surface_flux.py b/tests/unit_tests/test_surface_flux.py new file mode 100644 index 000000000..e4419252e --- /dev/null +++ b/tests/unit_tests/test_surface_flux.py @@ -0,0 +1,164 @@ +"""Tests for surface flux tallying via flux score + SurfaceFilter.""" + +import math +import pytest + +import openmc + + +@pytest.fixture +def two_cell_model(): + """Simple two-cell slab model with a monodirectional fixed source. + + Cell1 occupies x in [-10, 0], cell2 x in [0, 10]. The source fires all + particles from (-5, 0, 0) in the +x direction with weight 1. Every + particle therefore crosses the surface at x=0 from cell1 into cell2 at + mu = 1 (normal incidence). + """ + openmc.reset_auto_ids() + model = openmc.Model() + + xmin = openmc.XPlane(-10.0, boundary_type="vacuum") + xmid = openmc.XPlane(0.0) + xmax = openmc.XPlane(10.0, boundary_type="vacuum") + ymin = openmc.YPlane(-10.0, boundary_type="vacuum") + ymax = openmc.YPlane(10.0, boundary_type="vacuum") + zmin = openmc.ZPlane(-10.0, boundary_type="vacuum") + zmax = openmc.ZPlane(10.0, boundary_type="vacuum") + + cell1 = openmc.Cell(region=+xmin & -xmid & +ymin & -ymax & +zmin & -zmax) + cell2 = openmc.Cell(region=+xmid & -xmax & +ymin & -ymax & +zmin & -zmax) + model.geometry = openmc.Geometry([cell1, cell2]) + + src = openmc.IndependentSource() + src.space = openmc.stats.Point((-5.0, 0.0, 0.0)) + src.angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 1 + model.settings.particles = 100 + model.settings.source = src + + return model, xmid, cell1, cell2 + + +def test_surface_filter_flux_normal_incidence(two_cell_model, run_in_tmpdir): + """SurfaceFilter + flux at mu=1 gives w/|mu| = 1.0 per source particle.""" + model, xmid, *_ = two_cell_model + + surf_filter = openmc.SurfaceFilter([xmid]) + flux_tally = openmc.Tally() + flux_tally.filters = [surf_filter] + flux_tally.scores = ['flux'] + model.tallies = [flux_tally] + + model.run(apply_tally_results=True) + flux_mean = flux_tally.mean.flat[0] + + # Every particle crosses at mu=1 with weight 1, so flux = 1.0 + assert flux_mean == pytest.approx(1.0, rel=1e-8) + + +def test_surface_filter_current_outward(two_cell_model, run_in_tmpdir): + """SurfaceFilter + current gives +1.0 for purely outward crossings.""" + model, xmid, *_ = two_cell_model + + surf_filter = openmc.SurfaceFilter([xmid]) + current_tally = openmc.Tally() + current_tally.filters = [surf_filter] + current_tally.scores = ['current'] + model.tallies = [current_tally] + + model.run(apply_tally_results=True) + current_mean = current_tally.mean.flat[0] + + # All crossings are outward → net current = +1.0 + assert current_mean == pytest.approx(1.0) + + +def test_surface_filter_flux_angled(two_cell_model, run_in_tmpdir): + """Surface flux at 60-degree incidence gives w/|mu| = 2.0.""" + model, xmid, *_ = two_cell_model + + # Modify source to use 60-degree angle from normal: mu = cos(60°) = 0.5 + mu = ux = 0.5 + uy = math.sqrt(1.0 - ux**2) + model.settings.source[0].angle = openmc.stats.Monodirectional((ux, uy, 0.0)) + + surf_filter = openmc.SurfaceFilter([xmid]) + flux_tally = openmc.Tally() + flux_tally.filters = [surf_filter] + flux_tally.scores = ['flux'] + model.tallies = [flux_tally] + + model.run(apply_tally_results=True) + flux_mean = flux_tally.mean.flat[0] + + # flux = w/|mu| = 1/0.5 = 2.0 + assert flux_mean == pytest.approx(1.0 / mu) + + +def test_surface_tally_during_lattice_crossing(run_in_tmpdir): + openmc.reset_auto_ids() + model = openmc.Model() + + xmin = openmc.XPlane(-1.0, boundary_type="vacuum") + xmax = openmc.XPlane(1.0, boundary_type="vacuum") + ymin = openmc.YPlane(-1.0, boundary_type="vacuum") + ymax = openmc.YPlane(1.0, boundary_type="vacuum") + zmin = openmc.ZPlane(-1.0, boundary_type="vacuum") + zmax = openmc.ZPlane(1.0, boundary_type="vacuum") + + inner_cell = openmc.Cell() + inner_univ = openmc.Universe(cells=[inner_cell]) + + tile_cell = openmc.Cell(fill=inner_univ) + tile_univ = openmc.Universe(cells=[tile_cell]) + + lattice = openmc.RectLattice() + lattice.lower_left = (-1.0, -1.0) + lattice.pitch = (1.0, 2.0) + lattice.universes = [[tile_univ, tile_univ]] + + root_cell = openmc.Cell( + fill=lattice, region=+xmin & -xmax & +ymin & -ymax & +zmin & -zmax) + model.geometry = openmc.Geometry([root_cell]) + + src = openmc.IndependentSource() + src.space = openmc.stats.Point((-0.5, 0.0, 0.0)) + src.angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 1 + model.settings.particles = 5 + model.settings.source = src + + current_tally = openmc.Tally() + current_tally.filters = [openmc.SurfaceFilter(xmax)] + current_tally.scores = ['current'] + model.tallies = [current_tally] + + model.run(apply_tally_results=True) + assert current_tally.mean.flat[0] == pytest.approx(1.0) + + +def test_cellfrom_filter_flux_directional(two_cell_model, run_in_tmpdir): + """SurfaceFilter + CellFromFilter + flux scores only the correct direction.""" + model, xmid, cell1, cell2 = two_cell_model + + surf_filter = openmc.SurfaceFilter([xmid]) + cellfrom_filter = openmc.CellFromFilter([cell1, cell2]) + + tally = openmc.Tally() + tally.filters = [surf_filter, cellfrom_filter] + tally.scores = ['flux'] + + model.tallies = [tally] + model.run(apply_tally_results=True) + mean_from1 = tally.mean.flat[0] + mean_from2 = tally.mean.flat[1] + + # All particles cross xmid from cell1 at mu=1 → flux = 1.0 + assert mean_from1 == pytest.approx(1.0) + # No particles cross xmid from cell2 → flux = 0 + assert mean_from2 == pytest.approx(0.0) diff --git a/tests/unit_tests/test_tally.py b/tests/unit_tests/test_tally.py new file mode 100644 index 000000000..7136a621b --- /dev/null +++ b/tests/unit_tests/test_tally.py @@ -0,0 +1,63 @@ +import openmc + + +def test_tally_init_args(): + """Test that Tally constructor kwargs are applied correctly.""" + filter = openmc.EnergyFilter([0.0, 1.0, 20.0e6]) + tally = openmc.Tally( + name='my tally', + scores=['flux', 'fission'], + filters=[filter], + nuclides=['U235'], + estimator='tracklength', + ) + + assert tally.name == 'my tally' + assert tally.scores == ['flux', 'fission'] + assert tally.filters == [filter] + assert tally.nuclides == ['U235'] + assert tally.estimator == 'tracklength' + + +def test_heating_estimator_with_photon_transport(run_in_tmpdir): + """Test that a neutron-only heating tally uses the tracklength estimator + even when photon transport is enabled. + + Without a neutron particle filter, photon heating requires the collision + estimator (analog energy balance). But neutron heating has kerma + coefficients that support tracklength scoring, so a neutron-only tally + should keep the tracklength estimator. + """ + mat = openmc.Material() + mat.add_nuclide('Si28', 1.0) + mat.set_density('g/cm3', 2.3) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sph) + + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.run_mode = 'fixed source' + model.settings.particles = 100 + model.settings.batches = 1 + model.settings.photon_transport = True + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point() + ) + + neutron_filter = openmc.ParticleFilter(['neutron']) + + # Neutron-only heating — should get tracklength estimator + t_neutron = openmc.Tally(name='heating_neutron') + t_neutron.filters = [neutron_filter] + t_neutron.scores = ['heating'] + + # No particle filter — should get collision estimator + t_all = openmc.Tally(name='heating_all') + t_all.scores = ['heating'] + + model.tallies = [t_neutron, t_all] + + sp_filename = model.run() + with openmc.StatePoint(sp_filename) as sp: + assert sp.tallies[t_neutron.id].estimator == 'tracklength' + assert sp.tallies[t_all.id].estimator == 'collision' diff --git a/tests/unit_tests/test_tracks.py b/tests/unit_tests/test_tracks.py index fa8664159..74da83552 100644 --- a/tests/unit_tests/test_tracks.py +++ b/tests/unit_tests/test_tracks.py @@ -70,7 +70,7 @@ def test_tracks(sphere_model, particle, run_in_tmpdir): particle_track = track.particle_tracks[0] assert isinstance(particle_track, openmc.ParticleTrack) - assert particle_track.particle.name.lower() == particle + assert str(particle_track.particle).lower() == particle assert isinstance(particle_track.states, np.ndarray) # Sanity checks on actual data @@ -140,8 +140,10 @@ def test_filter(sphere_model, run_in_tmpdir): assert matches == tracks matches = tracks.filter(state_filter=lambda s: s['E'] > 0.0) assert matches == tracks - matches = tracks.filter(particle='bunnytron') + matches = tracks.filter(particle='proton') assert matches == [] + with pytest.raises(ValueError): + tracks.filter(particle='bunnytron') def test_write_to_vtk(sphere_model): diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index efe8552a6..0955347f7 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -1,3 +1,5 @@ +from pathlib import Path + import lxml.etree as ET import numpy as np import openmc @@ -47,6 +49,12 @@ def test_bounding_box(): assert_unbounded(u) +def test_id(): + openmc.Universe(universe_id=0) + with pytest.raises(ValueError): + openmc.Universe(universe_id=-1) + + def test_plot(run_in_tmpdir, sphere_model): # model with -inf and inf in the bounding box @@ -104,6 +112,43 @@ def test_plot(run_in_tmpdir, sphere_model): plt.close('all') +def test_mg_plot(run_in_tmpdir): + # Create a simple universe with macroscopic data + h2o_data = openmc.Macroscopic('LWTR') + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic(h2o_data) + sph = openmc.Sphere(r=10, boundary_type="vacuum") + cell = openmc.Cell(region=-sph, fill=water) + univ = openmc.Universe(cells=[cell]) + + # Create MGXS library and export to HDF5 + groups = openmc.mgxs.EnergyGroups([1e-5, 20.0e6]) + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([1.0]) + h2o_xsdata.set_absorption([0.5]) + scatter_matrix = np.array([[[0.5]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + mg_library = openmc.MGXSLibrary(groups) + mg_library.add_xsdatas([h2o_xsdata]) + mgxs_path = Path.cwd() / 'mgxs.h5' + mg_library.export_to_hdf5(mgxs_path) + + # Set MG cross sections in config and plot + with openmc.config.patch('mg_cross_sections', mgxs_path): + univ.plot(width=(200, 200), basis='yz', color_by='cell') + univ.plot(width=(200, 200), basis='yz', color_by='material') + + with pytest.raises(RuntimeError): + univ.plot(width=(200, 200), basis='yz', color_by='cell') + + # Close plots to avoid warning + import matplotlib.pyplot as plt + plt.close('all') + + def test_get_nuclides(uo2): c = openmc.Cell(fill=uo2) univ = openmc.Universe(cells=[c]) diff --git a/tests/unit_tests/test_vectfit.py b/tests/unit_tests/test_vectfit.py new file mode 100644 index 000000000..f7b2e905d --- /dev/null +++ b/tests/unit_tests/test_vectfit.py @@ -0,0 +1,224 @@ +""" +Initially from Jingang Liang: https://github.com/mit-crpg/vectfit.git +""" + +import numpy as np +import pytest +from openmc.data.vectfit import evaluate, vectfit + + +@pytest.fixture +def ref_poles(): + """Reference poles for real-pole test.""" + return np.array( + [ + 9.709261771920490e02 + 0.0j, + -1.120960794075339e03 + 0.0j, + 1.923889557426567e00 + 7.543700246109742e01j, + 1.923889557426567e00 - 7.543700246109742e01j, + 1.159741300380281e02 + 3.595650922556496e-02j, + 1.159741300380281e02 - 3.595650922556496e-02j, + 1.546932165729394e02 + 8.728391144940301e-02j, + 1.546932165729394e02 - 8.728391144940301e-02j, + 2.280349190818197e02 + 2.814037559718684e-01j, + 2.280349190818197e02 - 2.814037559718684e-01j, + 2.313004772627853e02 + 3.004628477692201e-01j, + 2.313004772627853e02 - 3.004628477692201e-01j, + 2.787470098364861e02 + 3.414179169920170e-01j, + 2.787470098364861e02 - 3.414179169920170e-01j, + 3.570711338764254e02 + 4.485587371149193e-01j, + 3.570711338764254e02 - 4.485587371149193e-01j, + 4.701059001346060e02 + 6.598089307174224e-01j, + 4.701059001346060e02 - 6.598089307174224e-01j, + 7.275819506342254e02 + 1.189678974845038e03j, + 7.275819506342254e02 - 1.189678974845038e03j, + ] + ) + + +@pytest.fixture +def ref_residues(): + """Reference residues for real-pole test.""" + return np.array( + [ + [ + -3.269879776751686e07 + 0.0j, + 1.131087935798761e09 + 0.0j, + 1.634151281869857e04 + 2.251103589277891e05j, + 1.634151281869857e04 - 2.251103589277891e05j, + 3.281792303833561e03 - 1.756079516325274e04j, + 3.281792303833561e03 + 1.756079516325274e04j, + 1.110800880243503e04 - 4.324813594540043e04j, + 1.110800880243503e04 + 4.324813594540043e04j, + 8.812700704117636e04 - 2.256520243571103e05j, + 8.812700704117636e04 + 2.256520243571103e05j, + 5.842090495551535e04 - 1.442159380741478e05j, + 5.842090495551535e04 + 1.442159380741478e05j, + 1.339410514130921e05 - 2.640767909713812e05j, + 1.339410514130921e05 + 2.640767909713812e05j, + 2.211245633333130e05 - 3.222447758311512e05j, + 2.211245633333130e05 + 3.222447758311512e05j, + 4.124430059785149e05 - 4.076023108323907e05j, + 4.124430059785149e05 + 4.076023108323907e05j, + 1.607378314999252e09 - 1.401163320110452e08j, + 1.607378314999252e09 + 1.401163320110452e08j, + ] + ] + ) + + +@pytest.fixture +def vector_test_data(): + """Simple 2-signal test with known poles and residues.""" + Ns = 101 + s = np.linspace(3.0, 7.0, Ns) + poles = [5.0 + 0.1j, 5.0 - 0.1j] + residues = [[0.5 - 11.0j, 0.5 + 11.0j], [1.5 - 20.0j, 1.5 + 20.0j]] + f = np.zeros((2, Ns)) + for i in range(2): + f[i, :] = np.real( + residues[i][0] / (s - poles[0]) + residues[i][1] / (s - poles[1]) + ) + weight = 1.0 / f + init_poles = [3.5 + 0.035j, 3.5 - 0.035j] + return s, poles, residues, f, weight, init_poles + + +@pytest.fixture +def poly_test_data(): + """Test data with rational function plus polynomial terms.""" + Ns = 201 + s = np.linspace(0.0, 5.0, Ns) + poles = [-20.0 + 30.0j, -20.0 - 30.0j] + residues = [[5.0 + 10.0j, 5.0 - 10.0j]] + polys = [[1.0, 2.0, 0.3]] + f = evaluate(s, poles, residues, polys) + weight = 1.0 / f + init_poles = [2.5 + 0.025j, 2.5 - 0.025j] + return s, poles, residues, polys, f, weight, init_poles + + +@pytest.fixture +def real_poles_data(ref_poles, ref_residues): + """Large-scale signal using complex and real poles.""" + Ns = 2000 + s = np.linspace(1.0e-2, 5.0e3, Ns) + f = np.zeros((1, Ns)) + for p, r in zip(ref_poles, ref_residues[0]): + f[0] += (r / (s - p)).real + weight = 1.0 / f + poles = np.linspace(1.1e-2, 4.8e3, 10) + poles = poles + poles * 0.01j + poles = np.sort(np.append(poles, np.conj(poles))) + return s, f, weight, poles + + +@pytest.fixture +def large_test_data(): + """Stress test data with thousands of poles and samples.""" + Ns = 3000 + N = 200 + s = np.linspace(1.0e-2, 5.0e3, Ns) + poles = np.linspace(1.1e-2, 4.8e3, N // 2) + 0.01j * np.linspace( + 1.1e-2, 4.8e3, N // 2 + ) + poles = np.sort(np.append(poles, np.conj(poles))) + residues = np.linspace(1e2, 1e6, N // 2) + 0.5j * np.linspace(1e2, 1e6, N // 2) + residues = np.sort(np.append(residues, np.conj(residues))).reshape((1, N)) + f = np.zeros((1, Ns)) + for p, r in zip(poles, residues[0]): + f[0] += (r / (s - p)).real + weight = 1.0 / f + init_poles = np.linspace(1.2e-2, 4.7e3, N // 2) + 0.01j * np.linspace( + 1.2e-2, 4.7e3, N // 2 + ) + init_poles = np.sort(np.append(init_poles, np.conj(init_poles))) + return s, f, weight, init_poles + + +@pytest.fixture +def eval_test_data(): + """Reference data for evaluating rational + polynomial models.""" + Ns = 101 + s = np.linspace(-5.0, 5.0, Ns) + poles = [-2.0 + 30.0j, -2.0 - 30.0j] + residues = [5.0 + 10.0j, 5.0 - 10.0j] + polys = [1.0, 2.0, 0.3] + return s, poles, residues, polys + + +def test_vector(vector_test_data): + """Test vectfit with vector samples and simple poles. + It is expected to get exact results with one iteration. + """ + s, expected_poles, expected_residues, f, weight, init_poles = vector_test_data + poles, residues, _, fit, _ = vectfit(f, s, init_poles, weight) + assert np.allclose( + np.sort_complex(poles), np.sort_complex(expected_poles), rtol=1e-7 + ) + assert np.allclose(f, evaluate(s, poles, residues), rtol=1e-7) + assert np.allclose(f, fit, rtol=1e-5) + + +def test_poly(poly_test_data): + """Test vectfit with polynomials.""" + s, expected_poles, expected_residues, expected_polys, f, weight, init_poles = ( + poly_test_data + ) + poles, residues, cf, fit, _ = vectfit(f, s, init_poles, weight, n_polys=3) + poles, residues, cf, fit, _ = vectfit(f, s, poles, weight, n_polys=3) + assert np.allclose( + np.sort_complex(poles), np.sort_complex(expected_poles), rtol=1e-5 + ) + assert np.allclose(f, evaluate(s, poles, residues, cf), rtol=1e-5) + assert np.allclose(cf, expected_polys, rtol=1e-5) + assert np.allclose(f, fit, rtol=1e-4) + + +def test_real_poles(real_poles_data, ref_poles, ref_residues): + """Test vectfit with more poles including real poles""" + s, f, weight, poles = real_poles_data + for _ in range(6): + poles, residues, _, fit, _ = vectfit(f, s, poles, weight) + assert np.allclose( + np.sort_complex(poles), np.sort_complex(ref_poles), rtol=1e-5, atol=1e-8 + ) + assert np.allclose(f, evaluate(s, poles, residues), rtol=1e-4) + assert np.allclose(f, fit, rtol=1e-3) + + +def test_large(large_test_data): + """Test vectfit with a large set of poles and samples""" + s, f, weight, init_poles = large_test_data + poles_fit, residues_fit, _, f_fit, _ = vectfit(f, s, init_poles, weight) + assert np.allclose(f, f_fit, rtol=1e-3) + + +def test_evaluate(eval_test_data): + """Test evaluate function""" + s, poles, residues, polys = eval_test_data + + # Single signal, no polynomial + f_ref = np.real(residues[0] / (s - poles[0]) + residues[1] / (s - poles[1])) + f = evaluate(s, poles, residues) + assert np.allclose(f[0], f_ref) + + # Single signal, with polynomial + for n, c in enumerate(polys): + f_ref += c * np.power(s, n) + f = evaluate(s, poles, residues, polys) + assert np.allclose(f[0], f_ref) + + # Multi-signal, multi-residue, multi-poly + poles = [5.0 + 0.1j, 5.0 - 0.1j] + residues = [[0.5 - 11.0j, 0.5 + 11.0j], [1.5 - 20.0j, 1.5 + 20.0j]] + polys = [[1.0, 2.0, 0.3], [4.0, -2.0, -10.0]] + f_ref = np.zeros((2, len(s))) + for i in range(2): + f_ref[i, :] = np.real( + residues[i][0] / (s - poles[0]) + residues[i][1] / (s - poles[1]) + ) + for n, c in enumerate(polys[i]): + f_ref[i, :] += c * np.power(s, n) + f = evaluate(s, poles, residues, polys) + assert np.allclose(f, f_ref) diff --git a/tests/unit_tests/test_void.py b/tests/unit_tests/test_void.py new file mode 100644 index 000000000..8713d4b0b --- /dev/null +++ b/tests/unit_tests/test_void.py @@ -0,0 +1,42 @@ +import numpy as np +import openmc +import pytest + + +@pytest.fixture +def empty_sphere(): + openmc.reset_auto_ids() + model = openmc.Model() + surf = openmc.Sphere(r=10, boundary_type='vacuum') + cell = openmc.Cell(region=-surf) + model.geometry = openmc.Geometry([cell]) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 3 + model.settings.particles = 1000 + + tally = openmc.Tally() + tally.scores = ['total', 'elastic'] + tally.nuclides = ['U235'] + tally.multiply_density = False + model.tallies.append(tally) + + return model + + +def test_equivalent_microxs(empty_sphere, run_in_tmpdir): + sp_file = empty_sphere.run() + with openmc.StatePoint(sp_file) as sp: + tally1 = sp.tallies[1] + + mat = openmc.Material() + mat.add_nuclide('H1', 1e-16) + + empty_sphere.geometry.get_all_cells()[1].fill = mat + + sp_file = empty_sphere.run() + with openmc.StatePoint(sp_file) as sp: + tally2 = sp.tallies[1] + + assert np.isclose(tally1.mean.sum(), tally2.mean.sum(), rtol=1e-10, atol=0) + assert tally1.mean.sum() > 0 diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index d6e509522..efa203b51 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -122,7 +122,8 @@ def model(): return model -def test_weightwindows(model, wws): +@pytest.mark.parametrize("shared_secondary", [False, True]) +def test_weightwindows(model, wws, shared_secondary): ww_files = ('ww_n.txt', 'ww_p.txt') cwd = Path(__file__).parent.absolute() @@ -131,6 +132,7 @@ def test_weightwindows(model, wws): with cdtemp(filepaths): # run once with variance reduction off model.settings.weight_windows_on = False + model.settings.shared_secondary_bank = shared_secondary analog_sp = model.run() os.rename(analog_sp, 'statepoint.analog.h5') @@ -223,7 +225,8 @@ def test_lower_ww_bounds_shape(): assert ww.lower_ww_bounds.shape == (2, 3, 4, 1) -def test_photon_heating(run_in_tmpdir): +@pytest.mark.parametrize("shared_secondary", [False, True]) +def test_photon_heating(run_in_tmpdir, shared_secondary): water = openmc.Material() water.add_nuclide('H1', 1.0) water.add_nuclide('O16', 2.0) @@ -246,7 +249,8 @@ def test_photon_heating(run_in_tmpdir): model.settings.run_mode = 'fixed source' model.settings.batches = 5 - model.settings.particles = 100 + model.settings.particles = 101 + model.settings.shared_secondary_bank = shared_secondary tally = openmc.Tally() tally.scores = ['heating'] @@ -260,7 +264,11 @@ def test_photon_heating(run_in_tmpdir): with openmc.StatePoint(sp_file) as sp: tally_mean = sp.tallies[tally.id].mean - # these values should be nearly identical + # Note: Our current physics model actually does allow this tally to + # occasionally go slightly negative. However, larger bugs can + # make this more common. We have selected a particle count for + # this test that happens to produce no negative tallies for both + # the shared and non-shared secondary PRNG streams. assert np.all(tally_mean >= 0) diff --git a/tests/unit_tests/weightwindows/test_wwinp_reader.py b/tests/unit_tests/weightwindows/test_wwinp_reader.py index 637463bb2..601517d47 100644 --- a/tests/unit_tests/weightwindows/test_wwinp_reader.py +++ b/tests/unit_tests/weightwindows/test_wwinp_reader.py @@ -52,7 +52,7 @@ n_mesh.z_grid = np.array([-100.0, n_e_bounds = (np.array([0.0, 100000.0, 146780.0]),) -n_particles = ('neutron',) +n_particles = [openmc.ParticleType.NEUTRON] # expected results - neutron and photon data np_mesh = openmc.RectilinearMesh() @@ -63,7 +63,7 @@ np_mesh.z_grid = n_mesh.z_grid np_e_bounds = (np.array([0.0, 100000.0, 146780.0, 215440.0]), np.array([0.0, 1.0E8])) -np_particles = ('neutron', 'photon') +np_particles = [openmc.ParticleType.NEUTRON, openmc.ParticleType.PHOTON] # expected results - photon data only p_mesh = openmc.RectilinearMesh() @@ -74,7 +74,7 @@ p_mesh.y_grid = np_mesh.y_grid p_mesh.z_grid = np.array([-50.0, 50.0]) p_e_bounds = (np.array([0.0, 100000.0, 146780.0, 215440.0, 316230.0]),) -p_particles = ('photon',) +p_particles = [openmc.ParticleType.PHOTON] expected_results = [('wwinp_n', n_mesh, n_particles, n_e_bounds), ('wwinp_np', np_mesh, np_particles, np_e_bounds), diff --git a/tools/ci/gha-install-njoy.sh b/tools/ci/gha-install-njoy.sh index 8255ffea8..168fcd4a7 100755 --- a/tools/ci/gha-install-njoy.sh +++ b/tools/ci/gha-install-njoy.sh @@ -1,7 +1,7 @@ #!/bin/bash set -ex cd $HOME -git clone https://github.com/njoy/NJOY2016 +git clone -b 2016.78 https://github.com/njoy/NJOY2016 cd NJOY2016 mkdir build && cd build cmake -Dstatic=on .. && make 2>/dev/null && sudo make install diff --git a/tools/ci/gha-install-vectfit.sh b/tools/ci/gha-install-vectfit.sh deleted file mode 100755 index bd38e1ea8..000000000 --- a/tools/ci/gha-install-vectfit.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -ex - -PYBIND_BRANCH='master' -PYBIND_REPO='https://github.com/pybind/pybind11' - -XTL_BRANCH='0.6.13' -XTL_REPO='https://github.com/xtensor-stack/xtl' - -XTENSOR_BRANCH='0.21.3' -XTENSOR_REPO='https://github.com/xtensor-stack/xtensor' - -XTENSOR_PYTHON_BRANCH='0.24.1' -XTENSOR_PYTHON_REPO='https://github.com/xtensor-stack/xtensor-python' - -XTENSOR_BLAS_BRANCH='0.17.1' -XTENSOR_BLAS_REPO='https://github.com/xtensor-stack/xtensor-blas' - -cd $HOME -git clone -b $PYBIND_BRANCH $PYBIND_REPO -cd pybind11 && mkdir build && cd build && cmake .. && sudo make install -pip install $HOME/pybind11 - -cd $HOME -git clone -b $XTL_BRANCH $XTL_REPO -cd xtl && mkdir build && cd build && cmake .. && sudo make install - -cd $HOME -git clone -b $XTENSOR_BRANCH $XTENSOR_REPO -cd xtensor && mkdir build && cd build && cmake .. && sudo make install - -cd $HOME -git clone -b $XTENSOR_PYTHON_BRANCH $XTENSOR_PYTHON_REPO -cd xtensor-python && mkdir build && cd build && cmake .. && sudo make install - -cd $HOME -git clone -b $XTENSOR_BLAS_BRANCH $XTENSOR_BLAS_REPO -cd xtensor-blas && mkdir build && cd build && cmake .. && sudo make install - -# Install wheel (remove when vectfit supports installation with build isolation) -pip install wheel - -# Install vectfit -cd $HOME -git clone https://github.com/liangjg/vectfit.git -pip install --no-build-isolation ./vectfit diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 1cc792f8d..5488b9547 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -9,8 +9,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False): os.mkdir('build') os.chdir('build') - # Build in debug mode by default with support for MCPL - cmake_cmd = ['cmake', '-DCMAKE_BUILD_TYPE=Debug', '-DOPENMC_USE_MCPL=on'] + # Build in RelWithDebInfo mode by default with support for MCPL + cmake_cmd = ['cmake', '-DCMAKE_BUILD_TYPE=RelWithDebInfo', '-DOPENMC_USE_MCPL=on'] # Turn off OpenMP if specified if not omp: @@ -43,6 +43,9 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False): # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') + # Enable strict FP for cross-platform reproducibility in CI + cmake_cmd.append('-DOPENMC_ENABLE_STRICT_FP=on') + # Build and install cmake_cmd.append('..') print(' '.join(cmake_cmd)) diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 74c3947f1..4cf62afbb 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -18,11 +18,6 @@ fi pip install 'ncrystal>=4.1.0' nctool --test -# Install vectfit for WMP generation if needed -if [[ $VECTFIT = 'y' ]]; then - ./tools/ci/gha-install-vectfit.sh -fi - # Install libMesh if needed if [[ $LIBMESH = 'y' ]]; then ./tools/ci/gha-install-libmesh.sh @@ -39,7 +34,9 @@ if [[ $MPI == 'y' ]]; then export CC=mpicc export HDF5_MPI=ON export HDF5_DIR=/usr/lib/x86_64-linux-gnu/hdf5/mpich - pip install --no-binary=h5py h5py + # Install h5py without build isolation to pick up already installed mpi4py + pip install setuptools Cython pkgconfig + pip install --no-build-isolation --no-binary=h5py h5py fi # Build and install OpenMC executable diff --git a/vendor/xtensor b/vendor/xtensor deleted file mode 160000 index 3634f2ded..000000000 --- a/vendor/xtensor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3634f2ded19e0cf38208c8b86cea9e1d7c8e397d diff --git a/vendor/xtl b/vendor/xtl deleted file mode 160000 index a7c1c5444..000000000 --- a/vendor/xtl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a7c1c5444dfc57f76620391af4c94785ff82c8d6