diff --git a/.claude/skills/reviewing-openmc-code/SKILL.md b/.claude/skills/reviewing-openmc-code/SKILL.md new file mode 100644 index 0000000000..28b4c189b5 --- /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 0000000000..37917abc18 --- /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 0000000000..b28ddb0f8a --- /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 0000000000..1fe85b50d9 --- /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 0000000000..34613092bb --- /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 0000000000..4125ee9660 --- /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 0000000000..bd5d38d6c5 --- /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 0000000000..c111dd73e8 --- /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 0000000000..39b2085f4f --- /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 0000000000..e1f71b83f7 --- /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 68a6e692a4..958cc21fdd 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 580d409c5d..b8c14279b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,10 @@ jobs: source_changed: ${{ steps.filter.outputs.source_changed }} steps: - name: Check out the repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Examine changed files id: filter - uses: dorny/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # latest master commit, not released yet + uses: dorny/paths-filter@v4 with: filters: | source_changed: @@ -43,45 +43,42 @@ jobs: 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 }} @@ -89,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 @@ -106,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 }} @@ -150,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: | @@ -167,7 +158,7 @@ jobs: openmc -v - name: cache-xs - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/nndc_hdf5 @@ -222,6 +213,7 @@ jobs: parallel: true flag-name: C++ and Python path-to-lcov: coverage.lcov + fail-on-error: false coverage: needs: [filter-changes, main] @@ -234,6 +226,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true + fail-on-error: false ci-pass: needs: [filter-changes, main, coverage] diff --git a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml index 813596953b..e3e4761509 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 6757f77271..317cfc0e83 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 7a81363a78..7c78c6c998 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 a219f2a91d..33a3a3b060 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 a901b8d3f0..1d6051a707 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 22e9aa68fb..2c53636bd1 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 843ce0f6fd..12fbfa579a 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 db62bb53e6..26de24e594 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 de95937828..4a6c5ff261 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 e8ea98aebd..f3194833f3 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 fab030192b..ef9faf00ca 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 fd51a9fa73..a5c8f711b5 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 cef14ca2c8..7d7d46ed9b 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 780059f307..dd8dfb14a9 100644 --- a/.gitignore +++ b/.gitignore @@ -104,5 +104,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 f84d09bb1f..2bc1238943 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 0000000000..bdfaa538e1 --- /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 3578144b25..61301bdb49 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,6 +10,9 @@ build: sphinx: configuration: docs/source/conf.py +formats: + - pdf + python: install: - method: pip diff --git a/AGENTS.md b/AGENTS.md index 44962d1ade..19abba7d97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,57 @@ OpenMC uses a git flow branching model with two primary branches: ### Instructions for Code Review -When analyzing code changes on a feature or bugfix branch (e.g., when a user asks "what do you think of these changes?"), **compare the branch changes against `develop`, not `master`**. Pull requests are submitted to merge into `develop`, so differences relative to `develop` represent the actual proposed changes. Comparing against `master` will include unrelated changes from other features that have already been merged to `develop`. +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 @@ -184,7 +234,7 @@ def test_my_feature(): 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 a debug build (`-DCMAKE_BUILD_TYPE=Debug`) +**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. @@ -229,14 +279,14 @@ When modifying C++ public APIs, update corresponding ctypes signatures in `openm - **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 15) before committing; install via `tools/dev/install-commit-hooks.sh` +- **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, setuptools, endf). Other packages must be optional +- **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) @@ -295,4 +345,4 @@ Check for optional features: 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 15; other versions may produce different formatting +5. **Clang-format version**: CI uses version 18; other versions may produce different formatting diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..9538b5ddc4 --- /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 d3119fb875..9fe133a22e 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 @@ -388,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 @@ -426,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 @@ -496,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) @@ -553,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 diff --git a/Dockerfile b/Dockerfile index a163a28102..688e5a370f 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 8a60b66bf9..b5dfd1748e 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 837a39c783..cc837bba71 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -5,8 +5,6 @@ get_filename_component(_OPENMC_PREFIX "${OpenMC_CMAKE_DIR}/../../.." ABSOLUTE) 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@) endif() diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 2583d51dff..1777e795ad 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -580,6 +580,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 +874,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 826c20022a..3a2e7fb93b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,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 diff --git a/docs/source/devguide/agentic-tools.rst b/docs/source/devguide/agentic-tools.rst new file mode 100644 index 0000000000..fed377cc07 --- /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/index.rst b/docs/source/devguide/index.rst index 2e131e0949..53b9f58538 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/styleguide.rst b/docs/source/devguide/styleguide.rst index 2c882b0341..a5599566e2 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 f2e39441a8..8627cbde23 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/depletion_results.rst b/docs/source/io_formats/depletion_results.rst index 7fb088268c..856993db67 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,6 +29,8 @@ 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//** diff --git a/docs/source/io_formats/mgxs_library.rst b/docs/source/io_formats/mgxs_library.rst index f7f5387a48..8a26311d1e 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/settings.rst b/docs/source/io_formats/settings.rst index ed7c6273aa..1a436a75ed 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 --------------------- @@ -542,6 +555,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 --------------------- @@ -572,7 +597,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 @@ -580,6 +605,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". @@ -687,14 +741,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: @@ -814,6 +870,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. @@ -845,6 +902,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 @@ -857,6 +918,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. @@ -995,17 +1060,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 @@ -1023,6 +1090,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`. @@ -1052,6 +1123,21 @@ variable and whose sub-elements/attributes are as follows: 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 @@ -1072,23 +1158,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 -------------------------- @@ -1138,6 +1207,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 ------------------------------ @@ -1225,6 +1320,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 ------------------------------ @@ -1645,6 +1757,14 @@ mesh-based weight windows. The ratio of the lower to upper weight window bounds. *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 diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 0ba0e061fd..dc57e57751 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 @@ -321,6 +321,32 @@ should be set to: 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/license.rst b/docs/source/license.rst index 0a90a74413..1ec9fc04a6 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 a66abb3ed4..764c4c6280 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 5e17316aa1..8bc2a0a1bf 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 27a3f873ab..f2c22ab225 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 cdda5ea929..7778e0714c 100644 --- a/docs/source/methods/variance_reduction.rst +++ b/docs/source/methods/variance_reduction.rst @@ -82,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:: @@ -135,6 +135,18 @@ aware of this. \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: -------------- diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index dea8c4427c..b3911c8b3b 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -132,6 +132,7 @@ Constructing Tallies openmc.MeshSurfaceFilter openmc.EnergyFilter openmc.EnergyoutFilter + openmc.ParticleProductionFilter openmc.MuFilter openmc.MuSurfaceFilter openmc.PolarFilter @@ -148,6 +149,7 @@ Constructing Tallies openmc.ZernikeRadialFilter openmc.ParentNuclideFilter openmc.ParticleFilter + openmc.ReactionFilter openmc.MeshMaterialVolumes openmc.Trigger openmc.TallyDerivative diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 67eca00947..dab45481dd 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 1eaf90c972..9d47430f75 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/stats.rst b/docs/source/pythonapi/stats.rst index c8318ba862..2f2e2835a2 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 0f887940ea..6cb774b5b0 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 398680e746..21981fdaa8 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -190,6 +190,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 ================================ diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 3224d5fff5..8f83b9b084 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -248,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: --------- diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 64d4801799..2a0d301d4b 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/random_ray.rst b/docs/source/usersguide/random_ray.rst index 64ef7de6d5..d35aff83b7 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -646,7 +646,9 @@ model to use these multigroup cross sections. An example is given below:: overwrite_mgxs_library=False, mgxs_path="mgxs.h5", correction=None, - source_energy=None + source_energy=None, + temperatures=None, + temperature_settings=None ) The most important parameter to set is the ``method`` parameter, which can be @@ -733,6 +735,20 @@ 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 @@ -928,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 --------------------------------- @@ -1057,22 +1075,47 @@ 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. 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. .. 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 diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 5a04fedd70..8ac07f3c89 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -604,6 +604,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 diff --git a/docs/source/usersguide/tallies.rst b/docs/source/usersguide/tallies.rst index e3b4e508bc..20a89cea7c 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 8d41807e1d..d551195f5c 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -4,26 +4,27 @@ Variance Reduction ================== -Global variance reduction in OpenMC is accomplished by weight windowing -or source biasing techniques, the latter of which additionally provides a -local variance reduction capability. 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 +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` @@ -71,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:: @@ -90,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:: @@ -148,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 diff --git a/examples/parameterized_custom_source/parameterized_source_ring.cpp b/examples/parameterized_custom_source/parameterized_source_ring.cpp index a983414f1c..9756e1b977 100644 --- a/examples/parameterized_custom_source/parameterized_source_ring.cpp +++ b/examples/parameterized_custom_source/parameterized_source_ring.cpp @@ -1,5 +1,5 @@ -#include // for M_PI -#include // for unique_ptr +#include +#include #include #include "openmc/particle.h" diff --git a/include/openmc/angle_energy.h b/include/openmc/angle_energy.h index ac931b1b53..55deb5d415 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 0000000000..b4d515d2e9 --- /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 c4e940bc87..6abcdd7f18 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/bremsstrahlung.h b/include/openmc/bremsstrahlung.h index 2f7e41bf08..d3b5868317 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 019a418a12..911654d318 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -125,6 +125,49 @@ int openmc_nuclide_name(int index, const char** name); int openmc_plot_geometry(); int openmc_id_map(const void* slice, int32_t* data_out); int openmc_property_map(const void* slice, double* data_out); +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 +183,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); @@ -218,6 +262,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); diff --git a/include/openmc/chain.h b/include/openmc/chain.h index a3bc6f3a36..e01f2a01c1 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 980cff447e..0b425a673d 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" @@ -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,7 @@ 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 }; //============================================================================== // Geometry Constants diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 80fe70baed..f319dd19df 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -265,23 +265,29 @@ private: }; //============================================================================== -//! 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); //! Evaluate probability density, f(x), at a point //! \param x Point to evaluate f(x) - //! \return 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 @@ -290,8 +296,15 @@ protected: 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(); }; //============================================================================== @@ -394,6 +407,71 @@ private: 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 #endif // OPENMC_DISTRIBUTION_H diff --git a/include/openmc/distribution_angle.h b/include/openmc/distribution_angle.h index efd4e58425..78de70c422 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 9b08ed039d..efacb91319 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 7b9c2abf8c..a72780737a 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 { @@ -29,6 +30,14 @@ public: //! \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 }; @@ -52,6 +61,11 @@ public: //! \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(); } @@ -87,6 +101,11 @@ public: //! \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) { diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 3e31b17a94..663cf5568e 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -67,12 +67,18 @@ public: 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 }; //============================================================================== diff --git a/include/openmc/eigenvalue.h b/include/openmc/eigenvalue.h index b456fee21e..4387476760 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 4a737eb881..34fee9758b 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/event.h b/include/openmc/event.h index 2d215a10e4..48a009f2b4 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/hdf5_interface.h b/include/openmc/hdf5_interface.h index 28b0d2b113..93e4bfc820 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/lattice.h b/include/openmc/lattice.h index f87d28b21b..ca40bbc2a3 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 c10f25551e..3967c36878 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" @@ -189,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 0d960c33db..dcc0e21fe2 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -201,6 +201,18 @@ 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); + //! Helper function to get index and interpolation function on an incident //! energy grid //! @@ -211,5 +223,15 @@ std::complex w_derivative(std::complex z, int order); 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); + } // namespace openmc #endif // OPENMC_MATH_FUNCTIONS_H diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5dc327fe35..0d8189caa1 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -8,8 +8,8 @@ #include #include "hdf5.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #include "openmc/bounding_box.h" #include "openmc/error.h" @@ -284,8 +284,8 @@ public: 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 @@ -348,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 @@ -419,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 { @@ -515,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 @@ -526,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 { diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index 9b1602f299..5d8ff58c23 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 da074f825e..117ac503d4 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 58d8339396..7a8b2acadd 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -96,7 +96,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_; diff --git a/include/openmc/output.h b/include/openmc/output.h index 940ea78ceb..0ad8b2fe50 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 2f6e6196bf..8db9721baa 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 diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 75166b36af..f72948f6eb 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -50,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 { @@ -533,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_; @@ -558,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}; @@ -688,8 +698,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_; } @@ -729,13 +754,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 index fed3c92846..0fd896c57d 100644 --- a/include/openmc/particle_type.h +++ b/include/openmc/particle_type.h @@ -12,6 +12,7 @@ #include #include "openmc/constants.h" +#include "openmc/error.h" namespace openmc { @@ -61,6 +62,17 @@ public: //---------------------------------------------------------------------------- // 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; diff --git a/include/openmc/photon.h b/include/openmc/photon.h index f6f28a4df1..93c6dba53f 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 e38a3c7f88..b0e395c1ea 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 7e27679eab..6c00fc2f6b 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -6,8 +6,8 @@ #include #include +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xarray.hpp" #include "hdf5.h" #include "openmc/cell.h" @@ -17,6 +17,7 @@ #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/random_lcg.h" +#include "openmc/ray.h" #include "openmc/xml_interface.h" namespace openmc { @@ -83,19 +84,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,30 +116,36 @@ 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); @@ -140,7 +155,7 @@ struct IdData { void set_overlap(size_t y, size_t x); // Members - xt::xtensor data_; //!< 2D array of cell & material ids + tensor::Tensor data_; //!< 2D array of cell & material ids }; struct PropertyData { @@ -152,7 +167,7 @@ struct PropertyData { void set_overlap(size_t y, size_t x); // Members - xt::xtensor data_; //!< 2D array of temperature & density data + tensor::Tensor data_; //!< 2D array of temperature & density data }; //=============================================================================== @@ -166,6 +181,11 @@ public: 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 @@ -270,11 +290,11 @@ 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) int meshlines_width_; //!< Width of lines added to the plot @@ -294,17 +314,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 +365,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 +374,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 +410,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 +468,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 +498,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 +505,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 +528,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 0d4086966d..6f51af34de 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; @@ -76,6 +77,7 @@ public: // Static Data members static bool volume_normalized_flux_tallies_; static bool adjoint_; // If the user wants outputs based on the adjoint flux + static bool fw_cadis_local_; static double diagonal_stabilization_rho_; // Adjusts strength of diagonal stabilization // for transport corrected MGXS data @@ -84,6 +86,8 @@ public: static std::unordered_map>> mesh_domain_map_; + static std::vector fw_cadis_local_targets_; + //---------------------------------------------------------------------------- // Static data members static RandomRayVolumeEstimator volume_estimator_; @@ -100,17 +104,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 @@ -177,10 +182,10 @@ protected: // 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 40c67ef954..b61d2d67aa 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 3db6510697..ccd2cbe476 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -20,8 +20,9 @@ public: //---------------------------------------------------------------------------- // Methods void apply_fixed_sources_and_mesh_domains(); - void prepare_fixed_sources_adjoint(); - void prepare_adjoint_simulation(); + void prepare_fw_fixed_sources_adjoint(); + void prepare_local_fixed_sources_adjoint(); + void prepare_adjoint_simulation(bool fw_adjoint); void simulate(); void output_simulation_results() const; void instability_check( @@ -34,15 +35,9 @@ public: // Accessors FlatSourceDomain* domain() const { return domain_.get(); } - //---------------------------------------------------------------------------- - // Public data members - - // Flag for adjoint simulation; - bool adjoint_needed_; - private: //---------------------------------------------------------------------------- - // Private data members + // Data members // Contains all flat source region data unique_ptr domain_; @@ -57,19 +52,14 @@ private: // Number of energy groups int negroups_; - // Toggle for first simulation - bool is_first_simulation_; - }; // class RandomRaySimulation //============================================================================ //! Non-member functions //============================================================================ -void openmc_run_random_ray(); void validate_random_ray_inputs(); -void openmc_reset_random_ray(); -void print_adjoint_header(); +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 c20d46abe9..65f2e6bc41 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -146,6 +146,7 @@ public: // Scalar fields int* material_; + int* temperature_idx_; double* density_mult_; int* is_small_; int* n_hits_; @@ -199,6 +200,9 @@ public: 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_; } @@ -319,8 +323,9 @@ public: //--------------------------------------- // Scalar fields - - int material_ {0}; //!< Index in openmc::model::materials array + 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_; @@ -400,6 +405,9 @@ 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]; } @@ -634,6 +642,7 @@ 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_; diff --git a/include/openmc/ray.h b/include/openmc/ray.h new file mode 100644 index 0000000000..62e86b0d90 --- /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 3314d18666..e95db1665d 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 9a8eab7d91..79a6160e25 100644 --- a/include/openmc/reaction_product.h +++ b/include/openmc/reaction_product.h @@ -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 a75ef09d97..bea881402a 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 6905c38e36..69b22981af 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 83806d3524..b25352be9f 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 efb4fd75ba..9d033a6b86 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 5b18902afb..45d2c5260f 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 3afa3d9ceb..f895ae77f6 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 259ba6dd1a..8203b21ab5 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -68,6 +68,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? @@ -88,7 +89,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 @@ -105,6 +107,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 @@ -170,6 +174,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 7e9ef28c58..b309ca3f1c 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,26 @@ 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; + } + //! 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 ebda84c1b0..c6db41afd5 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -51,6 +51,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 //============================================================================== @@ -61,7 +64,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(); @@ -72,8 +75,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 //! @@ -94,16 +98,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 2f32aa2a05..e307b1ed21 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -4,6 +4,7 @@ #ifndef OPENMC_SOURCE_H #define OPENMC_SOURCE_H +#include #include #include @@ -25,15 +26,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; @@ -265,6 +275,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/tallies/filter.h b/include/openmc/tallies/filter.h index 65098597a5..77b0d9f420 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 cf8a8aa0f5..3e410d9fdc 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_particle_production.h b/include/openmc/tallies/filter_particle_production.h new file mode 100644 index 0000000000..00fac9f7cc --- /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 0000000000..015729eefb --- /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 e877fdca9e..3895d74f66 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_; } @@ -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; @@ -163,7 +162,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}; @@ -216,15 +215,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; @@ -260,12 +258,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 c3ab779e6a..d1aed28318 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -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 0000000000..9e443e72f9 --- /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 de0767d0af..c06a2ee0dd 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 1e60371584..d40c2aff7e 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 fa8d3d65ec..ef75ec0653 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 0d7435ca2a..a5d404133c 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.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 //============================================================================== @@ -137,16 +126,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,11 +171,11 @@ 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_; } @@ -197,9 +186,9 @@ private: 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 @@ -234,8 +223,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 6a4abd8671..5cc04e5959 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 f49613ecde..17a34e5c77 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 feafde68dd..c9dbde986b 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 30e8b2ce47..3205b4340c 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -66,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/cell.py b/openmc/cell.py index 82a034b1c8..499cf95042 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/data/__init__.py b/openmc/data/__init__.py index c2d35565a8..9b38d758ec 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 6ccb76c922..d3de6a1925 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/data.py b/openmc/data/data.py index 5ecadd37be..c22e54e7dc 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -6,6 +6,12 @@ from pathlib import Path from math import sqrt, log from warnings import warn +from endf.data import (ATOMIC_NUMBER, ATOMIC_SYMBOL, ELEMENT_SYMBOL, + EV_PER_MEV, K_BOLTZMANN, gnds_name, zam) + +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 +118,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 +208,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 +220,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) @@ -523,33 +456,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()) @@ -592,28 +498,3 @@ def isotopes(element: str) -> list[tuple[str, float]]: 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 7cd4bf43d4..ce20a252c3 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,7 +12,7 @@ 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_NUMBER, gnds_name +from .data import gnds_name, zam from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -241,9 +240,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: @@ -253,6 +250,9 @@ class DecayMode(EqualityMixin): delta_A, delta_Z = changes A += delta_A Z += delta_Z + break + else: + return None return gnds_name(Z, A, self._daughter_state) 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/effective_dose/dose.py b/openmc/data/dose/dose.py similarity index 100% rename from openmc/data/effective_dose/dose.py rename to openmc/data/dose/dose.py 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/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/mass_attenuation.h5 b/openmc/data/dose/mass_attenuation.h5 new file mode 100644 index 0000000000..f62785140c 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 0000000000..c4260480bf --- /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/endf.py b/openmc/data/endf.py index eca3744693..c50431dc7f 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -10,350 +10,33 @@ 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 _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: @@ -516,24 +199,3 @@ class Evaluation: self.target['mass_number'], 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 diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index dd14e0d194..d843b59712 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 71927cbed6..492cdd7f3b 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -805,9 +805,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/reaction.py b/openmc/data/reaction.py index 65b59582cf..ab1285b721 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -21,6 +21,7 @@ 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) diff --git a/openmc/data/vectfit.py b/openmc/data/vectfit.py new file mode 100644 index 0000000000..c5a783a3c0 --- /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 056f7c2737..66bd7148d3 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 @@ -210,7 +211,7 @@ class TransportOperator(ABC): full_burn_list : list of int All burnable materials in the geometry. name_list : list of str - Material names corresponding to materials in burn_list + Material names corresponding to materials in full_burn_list """ def finalize(self): @@ -540,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` @@ -562,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] @@ -572,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 @@ -600,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_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, 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 @@ -631,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: @@ -652,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 @@ -683,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 @@ -719,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 @@ -839,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, @@ -877,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) @@ -895,6 +952,7 @@ class Integrator(ABC): self._i_res + i, proc_time, write_rates=write_rates, + keff_search_root=keff_search_root, path=path ) @@ -908,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, @@ -918,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) @@ -1050,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 @@ -1069,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` @@ -1091,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] @@ -1104,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 @@ -1134,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_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, 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 @@ -1158,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): @@ -1294,7 +1464,7 @@ 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 @@ -1307,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 a835face72..42d4ab07ea 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -18,7 +18,7 @@ from typing import List import lxml.etree as ET -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 @@ -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 @@ -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,152 @@ class Chain: out[nuc.name] = dict(yield_obj) return out + @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 + ---------- + 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 reaction-rate terms. + + See Also + -------- + :attr:`decay_matrix`, :meth:`form_matrix` + """ + reactions = set() + n = len(self) + + # 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) + vals.append(val) + + 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): + if nuc.name not in index_nuc: + continue + + nuc_ind = index_nuc[nuc.name] + nuc_rates = rates[nuc_ind, :] + + for r_type, target, _, br in nuc.reactions: + r_id = 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 + 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) + + reactions.clear() + + return csc_array((vals, (rows, cols)), shape=(n, n)) + def form_matrix(self, rates, fission_yields=None): - """Forms depletion matrix. + """Form the full transmutation matrix (decay + reactions). Parameters ---------- @@ -624,96 +771,10 @@ class Chain: See Also -------- + :attr:`decay_matrix`, :meth:`form_rxn_matrix`, :meth:`get_default_fission_yields` """ - 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. - rows, cols, vals = [], [], [] - def setval(i, j, val): - rows.append(i) - cols.append(j) - vals.append(val) - - if fission_yields is None: - fission_yields = self.get_default_fission_yields() - - 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: - 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) - - 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, :] - - 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 - 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) - - # Clear set of reactions - reactions.clear() - - # Return CSC representation instead of DOK - return csc_array((vals, (rows, cols)), shape=(n, n)) + 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 @@ -808,35 +869,27 @@ class Chain: n = len(self) 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() @@ -866,14 +919,13 @@ class Chain: # Use DOK as intermediate representation n = len(self) 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 34bb28b491..a21d57d460 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 cecc388f4c..3594ffbe84 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -3,12 +3,13 @@ Implements two different forms of CRAM for use in openmc.deplete. """ +from functools import partial import numbers import numpy as np -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 @@ -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,7 +62,7 @@ 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 @@ -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 * csc_array(A, dtype=np.float64) - y = n0.copy() + 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') - for alpha, theta in zip(self.alpha, self.theta): - y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) - return y * self.alpha0 + + 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() + 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/independent_operator.py b/openmc/deplete/independent_operator.py index c192907cf2..c12863956b 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -384,7 +384,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 0000000000..49f7cc4dff --- /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 879a2d4ee9..687cf646f2 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. @@ -80,10 +86,12 @@ def get_microxs_and_flux( Energy group boundaries in [eV] or the name of the group structure. If left as None energies will default to [0.0, 100e6] 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 +107,10 @@ 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 + (per energy group). Supported keys: "nuclides", "reactions". Returns ------- @@ -135,31 +147,66 @@ def get_microxs_and_flux( 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_nuclides = list(opts.get('nuclides', [])) + rr_reactions = list(opts.get('reactions', [])) + # 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': + 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, 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, 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() @@ -196,37 +243,45 @@ 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() # (domains, groups, 1, 1) + flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + 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() # (domains, groups, nuclides, reactions) + reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) - 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( + # If using flux mode, compute flux-collapsed microscopic XS + if reaction_rate_mode == 'flux': + flux_micros = [MicroXS.from_multigroup_flux( energies=energies, multigroup_flux=flux_i, chain_file=chain_file, @@ -234,6 +289,14 @@ def get_microxs_and_flux( reactions=reactions ) for flux_i 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 +547,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/pool.py b/openmc/deplete/pool.py index 58f90894b6..19ad0ada50 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -42,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 @@ -74,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 @@ -164,7 +167,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, # 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]) @@ -198,7 +201,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, 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 57bbe437ff..6dbb3adb2c 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -1,5 +1,6 @@ from __future__ import annotations from collections.abc import Sequence +from contextlib import nullcontext import copy from datetime import datetime import json @@ -8,16 +9,17 @@ 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 +33,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 +72,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 +87,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 +108,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 +116,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 +133,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,7 +151,7 @@ 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, @@ -177,9 +189,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 @@ -206,6 +219,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 +231,35 @@ 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_transport( + photon_time_indices, bounding_boxes, output_dir / 'photon_transport', + mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs + ) return output_dir @@ -243,11 +271,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,19 +296,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 = {} mat_vol_kwargs.setdefault('bounding_boxes', True) - self.results['mesh_material_volumes'] = mmv = comm.bcast( - self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs)) - # 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 @@ -357,8 +396,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() @@ -468,12 +508,20 @@ 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 if comm.rank == 0: tally_ids = [tally.id for tally in self.photon_model.tallies] @@ -486,45 +534,30 @@ class R2SManager: if different_photon_model: photon_cells = self.photon_model.geometry.get_all_cells() - 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 \ + # 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 + continue + work_items.append((cell, original_mat, bounding_boxes[cell.id])) - # 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 + # Ensure photon transport is enabled in settings + self.photon_model.settings.photon_transport = True + for time_index in time_indices: # Convert time_index (which may be negative) to a normal index if time_index < 0: - time_index = len(self.results['depletion_results']) + time_index + time_index += len(self.results['depletion_results']) + + # Build decay photon sources and assign to the photon model + sources = self._create_photon_sources(time_index, work_items) + self.photon_model.settings.source = sources # Run photon transport calculation photon_dir = Path(output_dir) / f'time_{time_index}' @@ -537,90 +570,107 @@ 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.IndependentSource]: - """Create decay photon source for a mesh-based calculation. + def _get_mesh_work_items(self): + """Enumerate mesh-based work items across all meshes. - For each mesh element-material combination, an - :class:`~openmc.IndependentSource` is created with a - :class:`~openmc.stats.Box` spatial distribution based on the bounding - box of the material within the mesh element. A material constraint is - also applied so that sampled source sites are limited to the correct - region. + 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. - 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. + Returns + ------- + list of tuple + Each tuple is (index_mat, mat_id, bbox). + """ + mmv_list = self.results['mesh_material_volumes'] + photon_mmv_list = self.results.get('mesh_material_volumes_photon') + + 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 + + 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 + } + + 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 + + return work_items + + 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, optional - Time index for the decay photon source. Default is -1 (last time). + 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 - A list of IndependentSource objects for the decay photons, one for - each mesh element-material combination with non-zero source strength. - + Photon sources for each activated region. """ - mat_dict = self.neutron_model._get_all_materials() - - # List to hold all sources - sources = [] - - # Index in the overall list of activated materials - index_mat = 0 - - # Get various results from previous steps - mat_vols = self.results['mesh_material_volumes'] + step_result = self.results['depletion_results'][time_index] materials = self.results['activation_materials'] - results = self.results['depletion_results'] - photon_mat_vols = self.results.get('mesh_material_volumes_photon') + mesh_based = self.method == 'mesh-based' + if mesh_based: + mat_dict = self.neutron_model._get_all_materials() - # 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 mat_id, _, bbox in mat_vols.by_element(index_elem, include_bboxes=True): - # 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: - index_mat += 1 - continue - - # Get activated material composition + 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 - energy = activated_mat.get_decay_photon_energy() - if energy is not None: - strength = energy.integral() - space = openmc.stats.Box(*bbox) - sources.append(openmc.IndependentSource( - space=space, - 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} + + 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 @@ -638,10 +688,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)) @@ -665,10 +718,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 e1fcb26b6d..adb0d3dbc0 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -113,7 +113,7 @@ 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]. by_nuclide : bool @@ -231,7 +231,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 27420246f6..9f638a0585 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): @@ -73,6 +76,7 @@ class StepResult: self.name_list = None self.data = None + self.keff_search_root = None def __repr__(self): t = self.time[0] @@ -153,14 +157,14 @@ class StepResult: full_burn_list : list of str List of all burnable material IDs name_list : list of str, optional - Material names corresponding to materials in burn_list + 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(burn_list, name_list)) if name_list is not None else {} + 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)) @@ -196,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 ------- @@ -217,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)) @@ -364,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. @@ -396,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) @@ -429,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 @@ -448,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): @@ -496,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]) @@ -550,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 @@ -574,6 +596,8 @@ 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'. @@ -601,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/examples.py b/openmc/examples.py index 350a4d24d5..90a0bffe7b 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -656,10 +656,19 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5') -> openmc.Model return model -def _generate_c5g7_materials() -> openmc.Materials: +def _generate_c5g7_materials(second_temp = False) -> openmc.Materials: """Generate materials utilizing multi-group cross sections based on the the C5G7 Benchmark. + 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 ------- materials : openmc.Materials @@ -672,9 +681,45 @@ def _generate_c5g7_materials() -> openmc.Materials: assembly transport calculations without spatial homogenization" """ # 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 @@ -707,29 +752,33 @@ def _generate_c5g7_materials() -> openmc.Materials: 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]) @@ -823,10 +872,19 @@ def _generate_subdivided_pin_cell(uo2, water) -> openmc.Universe: return pincell -def random_ray_pin_cell() -> openmc.Model: +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 @@ -837,7 +895,7 @@ def random_ray_pin_cell() -> openmc.Model: ########################################################################### # Create Materials for the problem - materials = _generate_c5g7_materials() + materials = _generate_c5g7_materials(second_temp) uo2 = materials[0] water = materials[1] @@ -897,13 +955,22 @@ def random_ray_pin_cell() -> openmc.Model: return model -def random_ray_lattice() -> openmc.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 @@ -914,7 +981,7 @@ def random_ray_lattice() -> openmc.Model: ########################################################################### # Create Materials for the problem - materials = _generate_c5g7_materials() + materials = _generate_c5g7_materials(second_temp) uo2 = materials[0] water = materials[1] @@ -1243,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 \ No newline at end of file diff --git a/openmc/executor.py b/openmc/executor.py index 9cd2993454..75094e7388 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) diff --git a/openmc/filter.py b/openmc/filter.py index 53ec93e21d..87aeb70c3a 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 = ( @@ -126,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 @@ -275,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. @@ -426,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) @@ -629,6 +630,7 @@ class CellInstanceFilter(Filter): DistribcellFilter """ + def __init__(self, bins, filter_id=None): self.bins = bins self.id = filter_id @@ -749,6 +751,7 @@ class ParticleFilter(Filter): The number of filter bins """ + def __eq__(self, other): if type(self) is not type(other): return False @@ -969,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. @@ -1103,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 @@ -1412,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 @@ -1437,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 @@ -1712,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()]) @@ -1742,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. @@ -1909,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. @@ -2062,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: @@ -2101,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) @@ -2266,6 +2550,7 @@ class DelayedGroupFilter(Filter): The number of filter bins """ + def check_bins(self, bins): # Check the bin values. for g in bins: @@ -2334,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 @@ -2346,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 @@ -2359,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 @@ -2452,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/lib/__init__.py b/openmc/lib/__init__.py index d2a794eb15..9b135370fe 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 79310bb37f..0ee8c3ff06 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -31,8 +31,10 @@ class _SourceSite(Structure): ('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 dc81c3487b..574a37443a 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 @@ -607,10 +608,18 @@ class ParticleFilter(Filter): 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' @@ -711,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/plot.py b/openmc/lib/plot.py index 68f61821c5..90af80d5b7 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -1,7 +1,11 @@ +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 @@ -258,3 +262,383 @@ def property_map(plot): 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 + +_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/material.py b/openmc/material.py index 735a057432..86cc3d7939 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -2,6 +2,7 @@ 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 @@ -22,8 +23,10 @@ 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 # Units for density supported by OpenMC @@ -346,7 +349,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 @@ -364,7 +367,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") @@ -375,6 +378,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': @@ -409,6 +414,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 @@ -1196,16 +1385,16 @@ class Material(IDManagerMixin): 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. + """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. @@ -1223,16 +1412,21 @@ class Material(IDManagerMixin): 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': @@ -1251,16 +1445,15 @@ class Material(IDManagerMixin): 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 @@ -1279,13 +1472,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': diff --git a/openmc/mesh.py b/openmc/mesh.py index efe1c20a16..4129913d85 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -236,12 +236,12 @@ class MeshBase(IDManagerMixin, ABC): self._name = name else: self._name = '' - + @property @abstractmethod def lower_left(self): pass - + @property @abstractmethod def upper_right(self): @@ -255,7 +255,7 @@ class MeshBase(IDManagerMixin, ABC): @abstractmethod def indices(self): pass - + @property @abstractmethod def n_elements(self): @@ -268,10 +268,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): @@ -537,11 +538,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 @@ -636,7 +646,7 @@ class StructuredMesh(MeshBase): s0 = (slice(0, -1),)*ndim + (slice(None),) s1 = (slice(1, None),)*ndim + (slice(None),) return (vertices[s0] + vertices[s1]) / 2 - + @property def n_elements(self): return np.prod(self.dimension) @@ -927,6 +937,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.""" @@ -995,6 +1086,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 @@ -1177,55 +1272,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): @@ -1416,6 +1503,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 " @@ -1475,6 +1603,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 @@ -1623,6 +1755,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 @@ -1709,6 +1925,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 @@ -1811,14 +2031,14 @@ class CylindricalMesh(StructuredMesh): self, coords: Sequence[float] ) -> tuple[int, int, int]: - """Finds the index of the mesh voxel at the specified x,y,z coordinates. + """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 ------- @@ -1885,33 +2105,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*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. @@ -1922,38 +2140,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, @@ -1962,8 +2170,6 @@ class CylindricalMesh(StructuredMesh): origin=origin ) - return mesh - def to_xml_element(self): """Return XML representation of the mesh @@ -2156,6 +2362,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 @@ -2267,38 +2477,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, + name: str = '', 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. + 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. @@ -2309,16 +2517,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( @@ -2331,8 +2533,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) @@ -2444,6 +2645,11 @@ class SphericalMesh(StructuredMesh): arr[..., 2] = z + origin[2] return arr + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + raise NotImplementedError( + "get_indices_at_coords is not yet implemented for SphericalMesh" + ) + def require_statepoint_data(func): @wraps(func) @@ -2671,6 +2877,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): diff --git a/openmc/mgxs/groups.py b/openmc/mgxs/groups.py index 8910c7d423..aea7a6d299 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) diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index b476de9020..78ab3ca198 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,15 @@ 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, + 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 +1324,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 c12c1a9abe..57cc955ba2 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 12b8f6a756..4c13c75087 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -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 c1a15998e1..ab9b58b7a3 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -515,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. diff --git a/openmc/mixin.py b/openmc/mixin.py index 0bc4128b0b..28c97bfdd1 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/model.py b/openmc/model/model.py index 67da2f73a3..c9a24b8b38 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -265,6 +265,46 @@ class Model: denom_tally = openmc.Tally(name='IFP denominator') 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 == 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( @@ -576,6 +616,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) @@ -634,6 +675,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) @@ -1294,8 +1338,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 @@ -1307,13 +1352,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 @@ -1324,7 +1373,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): @@ -1695,8 +1744,8 @@ class Model: self.geometry.get_all_materials().values() ) + @staticmethod def _auto_generate_mgxs_lib( - self, model: openmc.model.model, groups: openmc.mgxs.EnergyGroups, correction: str | none, @@ -1861,6 +1910,85 @@ class Model: 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 != 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 != 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, @@ -1869,13 +1997,17 @@ class Model: 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. + 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 @@ -1907,50 +2039,79 @@ class Model: 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. """ - mgxs_sets = [] - for material in self.materials: - model = openmc.Model() - # Set materials on the model - model.materials = [material] + src = self._create_mgxs_sources( + groups, + spatial_dist=openmc.stats.Point(), + source_energy=source_energy + ) - # Settings - model.settings.batches = 100 - model.settings.particles = nparticles + temp_settings = {} + if temperature_settings == None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings - model.settings.source = self._create_mgxs_sources( - groups, - spatial_dist=openmc.stats.Point(), - source_energy=source_energy - ) + if temperatures == 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) - model.settings.run_mode = 'fixed source' - model.settings.create_fission_neutrons = False + # 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) - model.settings.output = {'summary': True, 'tallies': False} + # 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]) - # 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 - - # Add MGXS Tallies - mgxs_lib = self._auto_generate_mgxs_lib( - model, groups, correction, directory) - - # 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) - - # 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) + # 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( @@ -2026,6 +2187,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 != 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 != 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, @@ -2034,6 +2278,8 @@ class Model: 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 @@ -2043,7 +2289,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 ---------- @@ -2075,41 +2323,152 @@ class Model: 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 == None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings + + if temperatures == 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 != 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 - # Stochastic slab geometry - model.geometry, spatial_distribution = Model._create_stochastic_slab_geometry( - model.materials) - - # Define the sources - model.settings.source = self._create_mgxs_sources( - groups, - spatial_dist=spatial_distribution, - source_energy=source_energy - ) - - model.settings.run_mode = 'fixed source' - model.settings.create_fission_neutrons = False - - model.settings.output = {'summary': True, 'tallies': False} - - # Add MGXS Tallies - mgxs_lib = self._auto_generate_mgxs_lib( + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( model, groups, correction, directory) - 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 != 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, @@ -2118,6 +2477,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 @@ -2142,36 +2503,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 == 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 == None: + mgxs_sets = Model._isothermal_materialwise_mgxs( + self, + groups, + nparticles, + correction, + directory, + temp_settings + ).values() - # Add MGXS Tallies - mgxs_lib = self._auto_generate_mgxs_lib( - model, groups, correction, directory) + # 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 + ) - names = [mat.name for mat in mgxs_lib.domains] + # 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]) - # 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. @@ -2182,9 +2582,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 @@ -2212,8 +2616,16 @@ class Model: '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 @@ -2225,9 +2637,16 @@ 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 @@ -2241,13 +2660,16 @@ class Model: 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, source_energy) + 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, source_energy) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy, + temperatures, temperature_settings) else: raise ValueError( f'MGXS generation method "{method}" not recognized') diff --git a/openmc/plots.py b/openmc/plots.py index 8b67d5cacb..aeece7acf6 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -197,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 diff --git a/openmc/settings.py b/openmc/settings.py index 3cf662e311..8120eb073e 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -41,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 @@ -63,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 @@ -179,6 +187,9 @@ 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: @@ -207,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, @@ -225,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 @@ -242,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 @@ -263,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: @@ -285,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 @@ -351,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 @@ -390,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 @@ -456,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 @@ -649,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 @@ -706,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 @@ -1018,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 @@ -1238,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 @@ -1351,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') @@ -1596,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") @@ -1621,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") @@ -1708,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") @@ -1827,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 @@ -1887,11 +2008,12 @@ 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() if source_element.find('bias') is not None: raise RuntimeError( "Ray source distributions should not be biased.") - element.append(source_element) + subelement.append(source_element) elif key == 'source_region_meshes': subelement = ET.SubElement(element, 'source_region_meshes') @@ -1909,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() @@ -2084,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: @@ -2124,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: @@ -2205,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: @@ -2300,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: @@ -2337,8 +2496,9 @@ 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.") @@ -2355,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': @@ -2423,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) @@ -2431,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) @@ -2440,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) @@ -2456,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) @@ -2537,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) @@ -2545,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) @@ -2554,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) @@ -2569,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/stats/multivariate.py b/openmc/stats/multivariate.py index 2057f4a499..d486970377 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): @@ -610,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 ---------- @@ -623,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): @@ -669,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 @@ -683,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 @@ -704,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): @@ -1219,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 94258c833c..1b18bcb174 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -2,17 +2,21 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable, Sequence -from copy import deepcopy -from math import sqrt, pi, exp +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 @@ -25,6 +29,16 @@ _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. @@ -113,6 +127,8 @@ 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_unbiased(self, n_samples: int = 1, seed: int | None = None): @@ -1038,18 +1054,27 @@ class Watt(Univariate): 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. @@ -1059,6 +1084,10 @@ 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 @@ -1066,12 +1095,18 @@ class Normal(Univariate): Distribution for biased sampling """ - def __init__(self, mean_value, std_dev, bias: Univariate | None = None): + 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 @@ -1093,16 +1128,69 @@ class Normal(Univariate): cv.check_greater_than('Normal std_dev', std_dev, 0.0) self._std_dev = std_dev + @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 (-np.inf, np.inf) + 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): - return scipy.stats.norm.pdf(x, self.mean_value, self.std_dev) + """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 @@ -1115,12 +1203,16 @@ 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 @@ -1141,7 +1233,10 @@ class Normal(Univariate): """ params = get_elem_list(elem, "parameters", float) bias_dist = cls._read_bias_from_xml(elem) - return cls(*map(float, params), bias=bias_dist) + 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, bias: Univariate | None = None): @@ -1173,7 +1268,7 @@ def muir(e0: float, m_rat: float, kt: float, bias: Univariate | None = None): """ # https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-05411-MS std_dev = sqrt(2 * e0 * kt / m_rat) - return Normal(e0, std_dev, bias) + return Normal(e0, std_dev, bias=bias) # Retain deprecated name for the time being @@ -1187,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. @@ -1299,44 +1526,64 @@ 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): @@ -1395,11 +1642,56 @@ 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 @@ -1497,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): @@ -1896,32 +2201,353 @@ 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[Discrete | Tabular], + 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 : sequence of openmc.stats.Discrete or openmc.stats.Tabular + dists : sequence of openmc.stats.Discrete, openmc.stats.Tabular, or openmc.stats.Mixture Distributions to combine probs : sequence of float Probability (or intensity) of each distribution """ + new_probs = [] + new_dists = [] for i, dist in enumerate(dists): - cv.check_type(f'dists[{i}]', dist, (Discrete, Tabular)) + 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(dists) if isinstance(d, Discrete)] diff --git a/openmc/surface.py b/openmc/surface.py index 1fe5fabdf7..c2afeb613a 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 @@ -151,6 +156,7 @@ class Surface(IDManagerMixin, ABC): """ + min_id = 1 next_id = 1 used_ids = set() _atol = 1.e-12 @@ -1260,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') @@ -1426,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.) @@ -1524,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.) @@ -1622,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.) @@ -1722,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 @@ -1848,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') @@ -1984,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.) @@ -2086,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.) @@ -2188,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.) @@ -2291,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 74d4722af4..ceced42553 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -16,6 +16,23 @@ 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 @@ -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 @@ -394,7 +438,7 @@ class Tally(IDManagerMixin): self._num_realizations = int(group['n_realizations'][()]) for filt in self.filters: - if isinstance(filt, openmc.DistribcellFilter): + if isinstance(filt, DistribcellFilter): filter_group = f[f'tallies/filters/filter {filt.id}'] filt._num_bins = int(filter_group['n_bins'][()]) @@ -1062,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 @@ -1575,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 @@ -1677,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 @@ -1760,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' @@ -1957,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)' @@ -2059,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 @@ -2246,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) @@ -2257,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) @@ -2268,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': @@ -2479,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 @@ -2621,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) @@ -3269,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 @@ -3335,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 @@ -3373,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) @@ -3396,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 @@ -3414,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 @@ -3487,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 @@ -3526,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) @@ -3550,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 @@ -3569,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 @@ -3759,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: @@ -3854,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/weight_windows.py b/openmc/weight_windows.py index 7797986df0..63af2596ef 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -11,6 +11,7 @@ import h5py import openmc 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 @@ -499,6 +500,8 @@ class WeightWindowGenerator: 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. @@ -518,6 +521,8 @@ class WeightWindowGenerator: 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. @@ -529,7 +534,7 @@ 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, @@ -537,6 +542,7 @@ class WeightWindowGenerator: energy_bounds: Sequence[float] | None = None, 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 @@ -549,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 @@ -611,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: @@ -638,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): @@ -681,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: @@ -707,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) @@ -733,8 +770,8 @@ 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) @@ -743,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 2d67e83401..098487c06e 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,7 +34,6 @@ dependencies = [ "pandas", "lxml", "uncertainties", - "setuptools", "endf", ] @@ -70,7 +69,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 0000000000..0e63c96030 --- /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 6d830e153b..5b12b48fd0 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,36 +106,198 @@ 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(); } - allocate_temporary_vector_ifp( - sorted_ifp_delayed_group_bank, sorted_ifp_lifetime_bank); + 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; - copy_ifp_data_from_fission_banks( - i, idx, sorted_ifp_delayed_group_bank, sorted_ifp_lifetime_bank); + 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()); - copy_ifp_data_to_fission_banks( - sorted_ifp_delayed_group_bank.data(), sorted_ifp_lifetime_bank.data()); + 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 } //============================================================================== diff --git a/src/bremsstrahlung.cpp b/src/bremsstrahlung.cpp index d77066fb0e..ec1088101f 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 diff --git a/src/chain.cpp b/src/chain.cpp index a60ef12cd6..3915c016c2 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}); } @@ -74,6 +74,13 @@ void DecayPhotonAngleEnergy::sample( 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; +} + //============================================================================== // Global variables //============================================================================== @@ -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 943042f67e..714a5bf3ac 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/cross_sections.cpp b/src/cross_sections.cpp index b1bfde03d1..ec9da5b8ab 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/distribution.cpp b/src/distribution.cpp index 537a56171d..7f5b498add 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -7,7 +7,9 @@ #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" @@ -15,6 +17,10 @@ #include "openmc/random_lcg.h" #include "openmc/xml_interface.h" +namespace { +std::unordered_set decay_spectrum_missing_chain_nuclides; +} + namespace openmc { //============================================================================== @@ -363,30 +369,92 @@ double Watt::evaluate(double x) const //============================================================================== // 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); } +void Normal::compute_normalization() +{ + // 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 { - return normal_variate(mean_value_, std_dev_, seed); + 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 (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))); + // 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_; } //============================================================================== @@ -401,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); @@ -437,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_)); @@ -455,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(); } } } @@ -495,7 +572,7 @@ double Tabular::sample_unbiased(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]; @@ -508,6 +585,24 @@ double Tabular::sample_unbiased(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(); } } @@ -669,6 +764,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() " @@ -679,4 +776,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 50f1aca112..ecb5961f63 100644 --- a/src/distribution_angle.cpp +++ b/src/distribution_angle.cpp @@ -2,8 +2,7 @@ #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" @@ -30,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); @@ -41,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()}; @@ -83,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 a4a5ce9e1b..2f8e6cf1a9 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 857e1c30b4..47785649b5 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" @@ -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")) { @@ -116,6 +118,22 @@ std::pair PolarAzimuthal::sample_impl( weight}; } +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 //============================================================================== @@ -157,6 +175,11 @@ std::pair Isotropic::sample(uint64_t* seed) const } } +double Isotropic::evaluate(Direction u) const +{ + return 1.0 / (4.0 * PI); +} + //============================================================================== // Monodirectional implementation //============================================================================== diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index e25e08d748..ba8658f10e 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -141,6 +141,41 @@ 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; } std::pair CylindricalIndependent::sample(uint64_t* seed) const @@ -148,10 +183,8 @@ std::pair CylindricalIndependent::sample(uint64_t* seed) const auto [r, r_wgt] = r_->sample(seed); auto [phi, phi_wgt] = phi_->sample(seed); auto [z, z_wgt] = z_->sample(seed); - double x = r * cos(phi) + origin_.x; - double y = r * sin(phi) + origin_.y; - z += origin_.z; - Position xi {x, y, z}; + Position xi = + r * (cos(phi) * r_dir_ + sin(phi) * phi_dir_) + z * z_dir_ + origin_; return {xi, r_wgt * phi_wgt * z_wgt}; } diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index e7265d1354..b73c1aba6c 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 @@ -188,9 +185,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; @@ -280,7 +277,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 @@ -434,7 +431,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; @@ -573,7 +570,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); @@ -585,7 +582,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; @@ -609,7 +606,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 @@ -631,7 +628,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 c0c1d2e7e8..8a9c5e48a8 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 f33e132d0a..2d436bb9dc 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/finalize.cpp b/src/finalize.cpp index 344eaa1a0a..fd891d9dd8 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,12 +142,15 @@ 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::use_decay_photons = false; + settings::use_shared_secondary_bank = false; settings::verbosity = -1; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; @@ -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/hdf5_interface.cpp b/src/hdf5_interface.cpp index c56d485e28..00c6a4399c 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 68d9b34b73..2c7e03a675 100644 --- a/src/ifp.cpp +++ b/src/ifp.cpp @@ -14,13 +14,13 @@ void ifp(const Particle& p, int64_t idx) { if (simulation::ifp_delayed_on) { 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 (simulation::ifp_lifetime_on) { 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); } } diff --git a/src/initialize.cpp b/src/initialize.cpp index a2269ed1ea..78b414f786 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; @@ -296,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 @@ -357,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; @@ -401,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 @@ -416,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( @@ -450,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(); @@ -471,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(); @@ -494,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 92d451f61f..e799a340e6 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -340,6 +340,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 + @@ -986,6 +1006,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 072e6decad..21b11b8b9c 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); } } @@ -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 9473f928b2..81f08fa04e 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -95,6 +95,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,6 +926,24 @@ 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; + } +} + // Helper function to get index and interpolation function on an incident energy // grid void get_energy_index( diff --git a/src/mesh.cpp b/src/mesh.cpp index a72b1441f7..5ab7ac3988 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -6,6 +6,7 @@ #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t +#include // for accumulate #include #ifdef _MSC_VER @@ -16,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" @@ -772,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( @@ -961,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); @@ -972,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); @@ -981,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; } } @@ -1096,7 +1089,7 @@ int StructuredMesh::n_surface_bins() const return 4 * n_dimension_ * n_bins(); } -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 @@ -1104,7 +1097,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++) { @@ -1123,31 +1116,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; } @@ -1340,10 +1327,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; @@ -1365,13 +1352,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) { @@ -1383,7 +1370,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."); @@ -1391,11 +1378,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++) { @@ -1411,7 +1398,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."); @@ -1421,7 +1408,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."); } @@ -1432,11 +1419,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."); @@ -1454,7 +1441,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) { @@ -1569,13 +1556,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 @@ -1583,7 +1570,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++) { @@ -1602,31 +1589,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; } @@ -1893,18 +1874,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) @@ -2178,15 +2160,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) @@ -2696,7 +2679,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; } @@ -2723,17 +2706,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; @@ -2743,7 +2726,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]; @@ -2792,7 +2775,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; } @@ -3669,8 +3652,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 @@ -3684,7 +3674,12 @@ Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const 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 diff --git a/src/mgxs.cpp b/src/mgxs.cpp index a2c479f215..1dc090ab96 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 34f87d1798..865c565808 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); } diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 69e603a7c6..17d6e952c3 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,9 +373,8 @@ 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_.is_photon()) { for (int k = 0; k < n; ++k) { @@ -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) { @@ -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 diff --git a/src/output.cpp b/src/output.cpp index ae2daaffc1..725e8d0693 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" : "", @@ -295,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"); } @@ -315,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; @@ -330,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 @@ -345,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) @@ -359,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); } } @@ -495,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); + } } //============================================================================== diff --git a/src/particle.cpp b/src/particle.cpp index a1176abc79..779ae18da9 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,30 +48,33 @@ double Particle::speed() const { if (settings::run_CE) { // Determine mass in eV/c^2 - double mass; - switch (this->type().pdg_number()) { - case PDG_NEUTRON: - mass = MASS_NEUTRON_EV; - break; - case PDG_PHOTON: - mass = 0.0; - break; - case PDG_ELECTRON: - case PDG_POSITRON: - mass = MASS_ELECTRON_EV; - break; - default: - fatal_error("Unsupported particle for speed calculation."); - } + 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; } } @@ -87,7 +91,10 @@ bool Particle::create_secondary( 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(); @@ -95,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(); @@ -115,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) @@ -161,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() @@ -311,31 +341,50 @@ void Particle::event_cross_surface() 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[lowest_coord().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().is_neutron()) { keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total; @@ -346,7 +395,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; @@ -383,6 +432,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 @@ -418,61 +472,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().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); + // 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(); } } @@ -484,6 +549,7 @@ void Particle::event_death() // Finish particle track output. if (write_track()) { + write_particle_track(*this); finalize_particle_track(*this); } @@ -507,11 +573,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(); } } @@ -643,7 +715,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 @@ -675,13 +747,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; } @@ -731,7 +805,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; } @@ -830,28 +904,27 @@ void Particle::write_restart() const write_dataset(file_id, "id", id()); 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); diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index f02fcb94b5..c226d51ec2 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" @@ -106,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/photon.cpp b/src/photon.cpp index 951acb9fbd..6bbdc928f2 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); @@ -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) diff --git a/src/physics.cpp b/src/physics.cpp index 6bdd3f2857..c9f718b8d8 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,6 +44,7 @@ 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().pdg_number()) { @@ -63,8 +64,17 @@ void collision(Particle& p) 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 = p.type().transport_index(); @@ -117,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 " @@ -155,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) @@ -227,7 +229,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } // 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 +252,11 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) // Iterated Fission Probability (IFP) method 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 @@ -351,7 +357,8 @@ void sample_photon_reaction(Particle& p) // 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 && i_shell >= 0 && + element.subshell_map_[i_shell] >= 0) { element.atomic_relaxation(element.subshell_map_[i_shell], p); } @@ -371,8 +378,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]}; @@ -422,7 +430,9 @@ void sample_photon_reaction(Particle& p) // 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; @@ -1217,7 +1227,7 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // 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 e25ae6b97a..10760ce2da 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 866d4d728e..212ba765ca 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) @@ -178,7 +179,7 @@ void create_fission_sites(Particle& p) } // 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 2cadc48cef..707d53dc2c 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -7,14 +7,14 @@ #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" @@ -73,7 +73,8 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) void IdData::set_overlap(size_t y, size_t x) { - 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) @@ -123,11 +124,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 +181,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 +211,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 +230,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 +248,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 +288,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 +379,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())); @@ -447,23 +478,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 +749,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); @@ -743,14 +783,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 +822,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); @@ -857,27 +897,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 +927,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 +968,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,18 +992,18 @@ 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); @@ -971,11 +1011,11 @@ void Plot::create_voxel() const pltbase.width_ = width_; pltbase.origin_ = origin_; pltbase.basis_ = PlotBasis::xy; - pltbase.pixels_ = pixels_; + pltbase.pixels() = pixels(); pltbase.slice_color_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 +1024,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 +1097,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 +1127,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) @@ -1182,8 +1231,8 @@ std::pair RayTracePlot::get_pixel_ray( // Compute field of view in radians constexpr double DEGREE_TO_RADIAN = M_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 +1268,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 +1295,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 +1307,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 +1316,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 +1380,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 +1390,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 +1407,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 +1419,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 +1443,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 +1525,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 +1573,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 +1582,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 +1599,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 +1649,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 +1688,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 +1712,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_); @@ -1904,4 +1834,588 @@ extern "C" int openmc_property_map(const void* plot, double* data_out) 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_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index c2effaa5d4..06c6ef14d7 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 { @@ -30,9 +31,11 @@ RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_ {false}; +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 +66,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 +111,24 @@ 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] * density_mult; + 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] * - density_mult; - double nu_sigma_f = - nu_sigma_f_[material * negroups_ + g_in] * density_mult; - double chi = chi_[material * negroups_ + g_out]; + double sigma_s = + 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; if (settings::create_fission_neutrons) { @@ -193,6 +197,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) { @@ -201,8 +206,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] * - source_regions_.density_mult(sr); + 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); } @@ -328,6 +334,7 @@ void FlatSourceDomain::compute_k_eff() } int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { continue; } @@ -336,8 +343,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] * - source_regions_.density_mult(sr); + 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 += @@ -560,6 +568,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const #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 @@ -567,8 +576,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] * source_regions_.density_mult(sr); + 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; @@ -624,9 +633,9 @@ void FlatSourceDomain::random_ray_tally() // source strength. double volume = source_regions_.volume(sr) * simulation_volume_; - int material = source_regions_.material(sr); + 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; @@ -643,21 +652,27 @@ void FlatSourceDomain::random_ray_tally() case SCORE_TOTAL: if (material != MATERIAL_VOID) { score = - flux * volume * sigma_t_[material * negroups_ + g] * density_mult; + 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] * density_mult; + 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] * - density_mult; + score = + flux * volume * + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; @@ -666,8 +681,10 @@ void FlatSourceDomain::random_ray_tally() break; case SCORE_KAPPA_FISSION: - score = flux * volume * kappa_fission_[material * negroups_ + g] * - density_mult; + score = + flux * volume * + kappa_fission_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; break; default: @@ -828,7 +845,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; @@ -925,12 +942,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] * - source_regions_.density_mult(fsr); + double sigma_f = + sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(fsr); total_fission += sigma_f * flux; } } @@ -944,6 +963,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++) { @@ -951,7 +971,7 @@ 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; @@ -982,7 +1002,9 @@ 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 = (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(); @@ -1053,13 +1075,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(); @@ -1131,74 +1157,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); - double kappa_fission = - m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a); - kappa_fission_.push_back(kappa_fission); + 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); + 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, @@ -1227,6 +1260,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; @@ -1258,35 +1295,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] * source_regions_.density_mult(sr); + 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]); + } } } } @@ -1506,18 +1627,26 @@ 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()); @@ -1550,7 +1679,8 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( if (material != C_NONE) { for (int g = 0; g < negroups_; g++) { double sigma_t = - sigma_t_[material * negroups_ + g] * handle.density_mult(); + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + handle.density_mult(); handle.external_source(g) /= sigma_t; } } @@ -1625,6 +1755,7 @@ 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; @@ -1633,10 +1764,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] * density_mult; + 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 02f4c9e235..b4701ed1fa 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -43,13 +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] * density_mult; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g_out] * + density_mult; double scatter_flat = 0.0f; double fission_flat = 0.0f; @@ -62,12 +65,16 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) MomentArray flux_linear = srh.flux_moments_old(g_in); // Handles for cross sections - double sigma_s = sigma_s_[material * negroups_ * negroups_ + - g_out * negroups_ + g_in] * - density_mult; + double sigma_s = + sigma_s_[((material * ntemperature_ + temp) * negroups_ + g_out) * + negroups_ + + g_in] * + density_mult; double nu_sigma_f = - nu_sigma_f_[material * negroups_ + g_in] * density_mult; - double chi = chi_[material * negroups_ + g_out]; + 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; diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 1b61d8c207..dde5023e44 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,11 +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] * srh.density_mult(); + 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; @@ -531,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); @@ -560,7 +565,8 @@ void RandomRay::attenuate_flux_linear_source( // Compute tau, the optical thickness of the ray segment float sigma_t = - domain_->sigma_t_[material * negroups_ + g] * srh.density_mult(); + 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. @@ -765,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; @@ -785,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."); } @@ -867,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 7bab3a9b1b..24650afb8d 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,49 +23,6 @@ namespace openmc { // Non-member functions //============================================================================== -void openmc_run_random_ray() -{ - ////////////////////////////////////////////////////////// - // Run forward simulation - ////////////////////////////////////////////////////////// - - if (mpi::master) { - if (FlatSourceDomain::adjoint_) { - FlatSourceDomain::adjoint_ = false; - openmc::print_adjoint_header(); - FlatSourceDomain::adjoint_ = true; - } - } - - // 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(); - - // Run initial random ray simulation - sim.simulate(); - - ////////////////////////////////////////////////////////// - // Run adjoint simulation (if enabled) - ////////////////////////////////////////////////////////// - - if (sim.adjoint_needed_) { - // Setup for adjoint simulation - sim.prepare_adjoint_simulation(); - - // Run adjoint simulation - sim.simulate(); - } -} - // 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 @@ -121,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 @@ -214,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 @@ -235,6 +187,56 @@ void validate_random_ray_inputs() } } + // Validate adjoint sources + /////////////////////////////////////////////////////////////////// + if (FlatSourceDomain::adjoint_ && !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++) { @@ -278,33 +280,24 @@ 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::fw_cadis_local_ = false; + FlatSourceDomain::fw_cadis_local_targets_.clear(); FlatSourceDomain::mesh_domain_map_.clear(); RandomRay::ray_source_.reset(); RandomRay::source_shape_ = RandomRaySourceShape::FLAT; RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } -void print_adjoint_header() -{ - if (!FlatSourceDomain::adjoint_) - // If we're going to do an adjoint simulation afterwards, report that this - // is the initial forward flux solve. - header("FORWARD FLUX SOLVE", 3); - else - // Otherwise report that we are doing the adjoint simulation - header("ADJOINT FLUX SOLVE", 3); -} - //============================================================================== // RandomRaySimulation implementation //============================================================================== @@ -335,16 +328,6 @@ RandomRaySimulation::RandomRaySimulation() // Convert OpenMC native MGXS into a more efficient format // internal to the random ray solver domain_->flatten_xs(); - - // Check if adjoint calculation is needed. If it is, we will run the forward - // calculation first and then the adjoint calculation later. - adjoint_needed_ = FlatSourceDomain::adjoint_; - - // Adjoint is always false for the forward calculation - FlatSourceDomain::adjoint_ = false; - - // The first simulation is run after initialization - is_first_simulation_ = true; } void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() @@ -352,30 +335,52 @@ 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_adjoint_simulation() +void RandomRaySimulation::prepare_local_fixed_sources_adjoint() { - // Configure the domain for adjoint simulation - FlatSourceDomain::adjoint_ = true; + if (settings::run_mode == RunMode::FIXED_SOURCE) { + domain_->set_local_adjoint_sources(); + } +} + +void RandomRaySimulation::prepare_adjoint_simulation(bool fw_adjoint) +{ + reset_timers(); + + if (mpi::master) + header("ADJOINT FLUX SOLVE", 3); + + if (fw_adjoint) { + // Forward simulation has already been run; + // Configure the domain for adjoint simulation and + // re-initialize OpenMC general data structures + FlatSourceDomain::adjoint_ = true; + + 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(); + } - // Reset k-eff domain_->k_eff_ = 1.0; - // Initialize adjoint fixed sources, if present - prepare_fixed_sources_adjoint(); - // Transpose scattering matrix domain_->transpose_scattering_matrix(); @@ -385,18 +390,6 @@ void RandomRaySimulation::prepare_adjoint_simulation() void RandomRaySimulation::simulate() { - if (!is_first_simulation_) { - if (mpi::master && adjoint_needed_) - openmc::print_adjoint_header(); - - // Reset the timers and reinitialize the general OpenMC datastructures if - // this is after the first simulation - reset_timers(); - - // Initialize OpenMC general data structures - openmc_simulation_init(); - } - // Begin main simulation timer simulation::time_total.start(); @@ -492,7 +485,7 @@ void RandomRaySimulation::simulate() // End main simulation timer simulation::time_total.stop(); - // Normalize and save the final flux + // Normalize and save the final forward flux double source_normalization_factor = domain_->compute_fixed_source_normalization_factor() / (settings::n_batches - settings::n_inactive); @@ -508,11 +501,6 @@ void RandomRaySimulation::simulate() // Output all simulation results output_simulation_results(); - - // Toggle that the simulation object has been initialized after the first - // simulation - if (is_first_simulation_) - is_first_simulation_ = false; } void RandomRaySimulation::output_simulation_results() const @@ -633,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_) { @@ -671,3 +668,67 @@ void RandomRaySimulation::print_results_random_ray( } } // namespace openmc + +//============================================================================== +// C API functions +//============================================================================== + +void openmc_run_random_ray() +{ + ////////////////////////////////////////////////////////// + // Run forward simulation + ////////////////////////////////////////////////////////// + + // Check if adjoint calculation is needed, and if local adjoint source(s) + // are present. If an adjoint calculation is needed and no sources are + // specified, we will run a forward calculation first to calculate adjoint + // sources for global variance reduction, then perform an adjoint + // calculation later. + bool adjoint_needed = openmc::FlatSourceDomain::adjoint_; + bool fw_adjoint = openmc::model::adjoint_sources.empty() && adjoint_needed; + + // If we're going to do an adjoint simulation with forward-weighted adjoint + // sources afterwards, report that this is the initial forward flux solve. + if (!adjoint_needed || fw_adjoint) { + // Configure the domain for forward simulation + openmc::FlatSourceDomain::adjoint_ = false; + + if (adjoint_needed && openmc::mpi::master) + openmc::header("FORWARD FLUX SOLVE", 3); + } else { + // Configure domain for adjoint simulation (later) + openmc::FlatSourceDomain::adjoint_ = true; + } + + // Initialize OpenMC general data structures + openmc_simulation_init(); + + // Validate that inputs meet requirements for random ray mode + if (openmc::mpi::master) + openmc::validate_random_ray_inputs(); + + // Initialize Random Ray Simulation Object + openmc::RandomRaySimulation sim; + + if (!adjoint_needed || fw_adjoint) { + // Initialize fixed sources, if present + sim.apply_fixed_sources_and_mesh_domains(); + + // Execute random ray simulation + sim.simulate(); + } + + ////////////////////////////////////////////////////////// + // Run adjoint simulation (if enabled) + ////////////////////////////////////////////////////////// + + if (!adjoint_needed) { + return; + } + + // Setup for adjoint simulation + sim.prepare_adjoint_simulation(fw_adjoint); + + // Execute random ray simulation + sim.simulate(); +} diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 15c65221aa..78543c5ab5 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -11,11 +11,11 @@ namespace openmc { //============================================================================== SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), - 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_), volume_sq_t_(&sr.volume_sq_t_), - volume_naive_(&sr.volume_naive_), + 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_), + volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), position_recorded_(&sr.position_recorded_), external_source_present_(&sr.external_source_present_), position_(&sr.position_), centroid_(&sr.centroid_), @@ -71,6 +71,7 @@ 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_); @@ -125,6 +126,7 @@ void SourceRegionContainer::assign( // Clear existing data n_source_regions_ = 0; material_.clear(); + temperature_idx_.clear(); density_mult_.clear(); is_small_.clear(); n_hits_.clear(); @@ -183,6 +185,7 @@ 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); diff --git a/src/ray.cpp b/src/ray.cpp new file mode 100644 index 0000000000..3d848e3a3a --- /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 9ac5a1f528..c02e0cc407 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -309,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"}, @@ -346,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) @@ -370,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 ee560d6077..a1c9378616 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 @@ -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 21b18cbd92..9aa09956d5 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 e820419b48..32701791ad 100644 --- a/src/secondary_correlated.cpp +++ b/src/secondary_correlated.cpp @@ -5,8 +5,7 @@ #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" @@ -26,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) @@ -51,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) { @@ -66,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 @@ -75,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 @@ -119,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 @@ -133,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()}; @@ -153,9 +155,8 @@ 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 int i; @@ -247,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).first; + return *distribution_[l].angle[k]; } else { - mu = distribution_[l].angle[k + 1]->sample(seed).first; + 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 6ac91e665b..018ce1c8a9 100644 --- a/src/secondary_kalbach.cpp +++ b/src/secondary_kalbach.cpp @@ -5,8 +5,7 @@ #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" @@ -27,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) @@ -52,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); @@ -63,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 @@ -72,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 @@ -115,8 +114,8 @@ 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 int i; @@ -171,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) { @@ -217,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) { @@ -227,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 da0bb81c47..72f0b0b92d 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 030d398aab..b0f601809d 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -4,8 +4,9 @@ #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 @@ -16,16 +17,26 @@ namespace openmc { // 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); @@ -42,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 //============================================================================== @@ -54,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)); } //============================================================================== @@ -85,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 @@ -116,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 //============================================================================== @@ -129,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; @@ -144,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; @@ -176,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] @@ -189,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 //============================================================================== @@ -218,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()); } } @@ -231,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) @@ -286,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)); @@ -318,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 //============================================================================== @@ -340,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 5cbb76fb9b..ec2af71025 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 3157541e91..4d6faa9948 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -61,6 +61,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}; @@ -81,6 +82,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}; @@ -95,6 +97,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}; @@ -134,6 +137,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}; @@ -274,8 +279,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); @@ -324,6 +330,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); } @@ -361,6 +369,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)); + } + } } } @@ -595,6 +610,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")); @@ -668,6 +688,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"); @@ -725,6 +753,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"); @@ -954,7 +990,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); } @@ -1241,6 +1277,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 @@ -1256,10 +1302,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 5eb3a21dce..2af5da7f4f 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -30,7 +30,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #ifdef OPENMC_MPI #include @@ -86,7 +86,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 +122,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 @@ -213,6 +214,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 +268,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 @@ -325,6 +348,8 @@ const RegularMesh* ufs_mesh {nullptr}; vector k_generation; vector work_index; +int64_t simulation_tracks_completed {0}; + } // namespace simulation //============================================================================== @@ -427,7 +452,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; } @@ -560,7 +585,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(); @@ -584,26 +609,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; @@ -611,6 +645,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; @@ -624,9 +661,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 @@ -640,17 +675,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) { @@ -668,13 +707,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); @@ -829,7 +889,7 @@ 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(); } @@ -839,11 +899,137 @@ void transport_history_based() #pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; - initialize_history(p, 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); + + // Phase 1: Transport primary particles and deposit first generation of + // secondaries in the shared secondary bank +#pragma omp parallel + { + vector thread_bank; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::work_per_rank; i++) { + Particle p; + initialize_particle_track(p, i, false); + transport_history_based_single_particle(p); + for (auto& site : p.local_secondary_bank()) { + thread_bank.push_back(site); + } + } + + // Drain thread-local bank into the shared secondary bank (once per thread) +#pragma omp critical(SharedSecondaryBank) + { + for (auto& site : thread_bank) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + } + } + + 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. 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); + + // Transport all secondary tracks from the shared secondary bank +#pragma omp parallel + { + vector thread_bank; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::shared_secondary_bank_read.size(); + i++) { + Particle p; + 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); + } + } + + // Drain thread-local bank into the shared secondary bank (once per + // thread) +#pragma omp critical(SharedSecondaryBank) + { + for (auto& secondary_site : thread_bank) { + simulation::shared_secondary_bank_write.thread_unsafe_append( + secondary_site); + } + } + } // End of transport loop over tracks in shared secondary bank + 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; @@ -861,33 +1047,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 @@ -896,4 +1056,122 @@ 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 secondaries from all particle buffers into shared bank + for (int64_t i = 0; i < n_particles; i++) { + for (auto& site : simulation::particles[i].local_secondary_bank()) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + simulation::particles[i].local_secondary_bank().clear(); + } + + 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 secondaries from all particle buffers into shared bank + for (int64_t i = 0; i < n_particles; i++) { + for (auto& site : simulation::particles[i].local_secondary_bank()) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + simulation::particles[i].local_secondary_bank().clear(); + } + + 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 8bd9c78935..bee20d5c30 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -10,7 +10,7 @@ #include // for dlopen, dlsym, dlclose, dlerror #endif -#include "xtensor/xadapt.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" @@ -37,6 +37,9 @@ 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) @@ -59,6 +62,8 @@ namespace model { vector> external_sources; +vector> adjoint_sources; + DiscreteIndex external_sources_probability; } // namespace model @@ -191,9 +196,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 @@ -207,9 +211,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 @@ -221,8 +229,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; } @@ -341,6 +354,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)}; @@ -361,15 +387,14 @@ 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) { @@ -383,8 +408,11 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // 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."); + } } } @@ -399,27 +427,40 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Check for monoenergetic source above maximum particle energy 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 - auto [E, E_wgt_temp] = energy_->sample(seed); - site.E = E; - E_wgt = E_wgt_temp; + // 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 @@ -429,8 +470,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const 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; } @@ -691,6 +734,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; } //============================================================================== @@ -711,8 +762,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 455299529b..da1c141a23 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" @@ -277,8 +276,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 { @@ -517,8 +516,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); @@ -593,8 +592,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) { @@ -827,7 +834,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 @@ -889,7 +896,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); @@ -918,13 +925,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 @@ -938,19 +950,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/tallies/filter.cpp b/src/tallies/filter.cpp index 79817981db..badb910773 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 316a758d11..928bfb6c5e 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 01e39e554a..46adf529b9 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 0b954cce3a..566b5710ad 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_meshmaterial.cpp b/src/tallies/filter_meshmaterial.cpp index 6e1f30380f..b45bb4164e 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_production.cpp b/src/tallies/filter_particle_production.cpp new file mode 100644 index 0000000000..899809679a --- /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 0000000000..8ee9f3ce87 --- /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 82f3d71789..4a636f1aa3 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 9d37a6ac74..357b552c5f 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; + } } } @@ -477,6 +482,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; @@ -493,12 +500,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) { @@ -511,7 +527,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: @@ -520,6 +536,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: @@ -552,16 +574,14 @@ 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; @@ -570,29 +590,24 @@ void Tally::set_scores(const vector& scores) 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: @@ -639,15 +654,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) @@ -743,7 +763,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()) { @@ -762,9 +782,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}}); } } @@ -772,7 +794,7 @@ void Tally::reset() { n_realizations_ = 0; if (results_.size() != 0) { - xt::view(results_, xt::all()) = 0.0; + results_.fill(0.0); } } @@ -807,9 +829,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 (int 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; @@ -822,9 +844,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 (int 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; @@ -844,18 +866,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; } @@ -960,13 +982,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(), @@ -974,9 +997,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; } } } @@ -984,14 +1007,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, @@ -999,9 +1021,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 7b1e5a9c4f..c9500cfb22 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 { @@ -903,10 +905,9 @@ 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().is_neutron()) @@ -942,7 +943,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_TIME_NUM: if (p.type().is_neutron() && p.fission()) { const auto& lifetime = - simulation::ifp_source_lifetime_bank[p.current_work() - 1][0]; + simulation::ifp_source_lifetime_bank[p.current_work()][0]; score = lifetime * p.wgt_last(); } break; @@ -950,7 +951,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_BETA_NUM: if (p.type().is_neutron() && p.fission()) { const auto& delayed_group = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1][0]; + simulation::ifp_source_delayed_group_bank[p.current_work()][0]; if (delayed_group > 0) { score = p.wgt_last(); if (tally.delayedgroup_filter_ != C_NONE) { @@ -1485,10 +1486,9 @@ 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().is_neutron()) @@ -2263,10 +2263,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; @@ -2409,24 +2408,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 = 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; + 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; } } @@ -2538,25 +2535,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 = 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; } } @@ -2584,7 +2581,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(); @@ -2628,6 +2625,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 @@ -2646,56 +2706,49 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) 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.n_coord() = 1; + 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) + continue; - // 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; - } - } - - // Reset all the filter matches for the next tally event. - for (auto& match : p.filter_matches()) - match.bins_present_ = false; + 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; } // Restore cell/energy p.n_coord() = orig_n_coord; diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index f1f83e2982..6c54edd5e1 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -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 (auto 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 cbe0983ed6..edfbddf23e 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 e86f774f04..02e0922364 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 diff --git a/src/urr.cpp b/src/urr.cpp index 02cef22809..b791eecb0d 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 1deffb8048..f675ae78a2 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 5ca4addbbf..0614110cd3 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().is_neutron() && !p.type().is_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 //============================================================================== @@ -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_); @@ -301,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); } @@ -374,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 = @@ -409,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 @@ -448,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); @@ -460,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); @@ -475,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) @@ -490,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; } @@ -510,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) @@ -542,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 @@ -588,25 +492,14 @@ 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 {}", @@ -614,17 +507,46 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, 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_; ////////////////////////////////////////////// @@ -699,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++) { @@ -879,6 +801,13 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) fatal_error("FW-CADIS can only be run in random ray solver mode."); } FlatSourceDomain::adjoint_ = 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)); @@ -1000,6 +929,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) { @@ -1155,7 +1193,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 e9c4fb5e3e..6f72e1ba4c 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 1929f51e6f..33d063a7b9 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 71dd5ebf5d..8dda9f7565 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 20341b420f..ce7e539ea5 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -7,6 +7,7 @@ set(TEST_NAMES test_mcpl_stat_sum test_mesh 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 f7c480c8d1..e46088a630 100644 --- a/tests/cpp_unit_tests/test_distribution.cpp +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -92,3 +92,103 @@ TEST_CASE("Test construction of SpatialBox with parameters") 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_tensor.cpp b/tests/cpp_unit_tests/test_tensor.cpp new file mode 100644 index 0000000000..0936d866a9 --- /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 9595765d73..873633525f 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( diff --git a/tests/regression_tests/atomic_relaxation/__init__.py b/tests/regression_tests/atomic_relaxation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..637e042858 --- /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 0000000000..6f100dac8f --- /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 0000000000..0d2413f590 --- /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/cpp_driver/driver.cpp b/tests/regression_tests/cpp_driver/driver.cpp index a99c97b64e..a6c3e65103 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 0000000000..e69de29bb2 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 0000000000..a335e3b478 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 0000000000..c8b4f67ff2 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 0000000000..35285321bc 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 0000000000..82e6012809 --- /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 35b3b5b9fb..8087d75649 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/filter_musurface/inputs_true.dat b/tests/regression_tests/filter_musurface/inputs_true.dat index 6db8543c2d..b457f5028e 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 4cdd7dbf50..657c141a05 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 f2ec96b495..bfc12a47cd 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 0000000000..e69de29bb2 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 0000000000..3ab9ab3fb6 --- /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 0000000000..61ec1c65fa --- /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 0000000000..3c60fd5173 --- /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/lattice_distribmat/True/inputs_true.dat b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat index aec3a54008..ab827dfdee 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 5031bea6e2..99994af3d6 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/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 0000000000..e69de29bb2 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 0000000000..318fdc7b90 --- /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 0000000000..38ff944173 --- /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 0000000000..336c8461f1 --- /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 07fe22ce50..c1591cca28 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/mgxs_library_condense/results_true.dat b/tests/regression_tests/mgxs_library_condense/results_true.dat index 98b30932c3..023d5afa2a 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 16b86870d7..ec7620c5c5 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 70833bb39c..781be3ef7f 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 0000000000..6d17a5e2be --- /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 07eebaa3e6..0ae0079f41 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 22a351240c..3b78db77b7 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 0000000000..e69de29bb2 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 0000000000..e93dfc36e4 --- /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 0000000000..2f5288eba1 --- /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 0000000000..b50a39cd08 --- /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 0000000000..5a4dfc516c --- /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 0000000000..2ad6ca5155 --- /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 0000000000..e69de29bb2 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 0000000000..c86e016c6e --- /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 0000000000..f3851d7ef1 --- /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 0000000000..0c84541f90 --- /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 0000000000..3a80fb17b5 --- /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 0000000000..770010900e --- /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/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index 07eebaa3e6..0ae0079f41 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 413f6f0ca4..c4f5fafa25 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 150448a12f..5ed5c5ecf0 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/inputs_true.dat b/tests/regression_tests/pulse_height/local/inputs_true.dat similarity index 95% rename from tests/regression_tests/pulse_height/inputs_true.dat rename to tests/regression_tests/pulse_height/local/inputs_true.dat index 590928e435..50e026bfb7 100644 --- a/tests/regression_tests/pulse_height/inputs_true.dat +++ b/tests/regression_tests/pulse_height/local/inputs_true.dat @@ -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/results_true.dat similarity index 100% rename from tests/regression_tests/pulse_height/results_true.dat rename to tests/regression_tests/pulse_height/local/results_true.dat diff --git a/tests/regression_tests/pulse_height/shared/inputs_true.dat b/tests/regression_tests/pulse_height/shared/inputs_true.dat new file mode 100644 index 0000000000..c2aef829c5 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared/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/results_true.dat b/tests/regression_tests/pulse_height/shared/results_true.dat new file mode 100644 index 0000000000..c57e8ff1c8 --- /dev/null +++ b/tests/regression_tests/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/pulse_height/test.py b/tests/regression_tests/pulse_height/test.py index 90d960f664..2cbb9152eb 100644 --- a/tests/regression_tests/pulse_height/test.py +++ b/tests/regression_tests/pulse_height/test.py @@ -1,53 +1,53 @@ 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,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_pulse_height(shared_secondary, subdir): + 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/cc', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) - model.materials = openmc.Materials([NaI]) + 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.source = openmc.IndependentSource( - space=openmc.stats.Point(), - energy=openmc.stats.Discrete([1e6], [1]), - particle='photon' - ) + # 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='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, 1_000_000, 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 0adfc54884..94e2709768 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 073348c41e..755afd6c43 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_local/__init__.py b/tests/regression_tests/random_ray_adjoint_local/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..9021d1675d --- /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 0000000000..daa9485655 --- /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 0000000000..c11b8e8470 --- /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 464c89a5df..86d5ec4abd 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 @@ -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/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 464c89a5df..86d5ec4abd 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 @@ -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/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 464c89a5df..86d5ec4abd 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 @@ -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_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat index 9f3a827f68..b00935ef38 100644 --- 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 @@ -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_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat index edf68f7e25..472406fa88 100644 --- 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 @@ -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_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat index edf68f7e25..472406fa88 100644 --- 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 @@ -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_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat index 80a166c678..15981f7fa5 100644 --- 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 @@ -38,11 +38,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_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat index 464c89a5df..86d5ec4abd 100644 --- 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 @@ -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_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat index 80a166c678..15981f7fa5 100644 --- 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 @@ -38,11 +38,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_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat index 464c89a5df..86d5ec4abd 100644 --- 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 @@ -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_temperature/__init__.py b/tests/regression_tests/random_ray_auto_convert_temperature/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..c60e6a0419 --- /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 0000000000..fee8bf6708 --- /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 0000000000..c60e6a0419 --- /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 0000000000..ef0a4b87ae --- /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 0000000000..c60e6a0419 --- /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 0000000000..a702ec851b --- /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 0000000000..99c99e6147 --- /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/eigen/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat index 0dd354cf02..eacd54f835 100644 --- a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat @@ -12,9 +12,15 @@ - - - + + 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 + @@ -80,11 +86,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_cell_density/fs/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat index e90f25973e..f369bae89f 100644 --- a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_density/fs/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_cell_density/test.py b/tests/regression_tests/random_ray_cell_density/test.py index cb6062cdc1..48ebe0baaa 100644 --- a/tests/regression_tests/random_ray_cell_density/test.py +++ b/tests/regression_tests/random_ray_cell_density/test.py @@ -22,7 +22,7 @@ def test_random_ray_basic(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, 9). + # 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)] 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 0000000000..e69de29bb2 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 0000000000..99363ed876 --- /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 0000000000..50476beb61 --- /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 0000000000..ef36d9238a --- /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 47325ebd7d..11100e88e1 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -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_entropy/settings.xml b/tests/regression_tests/random_ray_entropy/settings.xml index 81deaa7751..0d830417b6 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 9f1987f3ac..d650bbaf95 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 b4f57dbfa8..98a51add1f 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 ab91f74e50..20deba664b 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 220fa7db64..2268d82c39 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 f8c4430852..fe95baa7bb 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 c84e544fcc..a5632ece96 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 05c4846e6b..9d22603c63 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 0c870e1006..de941f10fb 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 ab91f74e50..20deba664b 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 0c05a71df3..943468a109 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 a67495bf16..650953c4b0 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 36d5f6f227..1b86d2daee 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_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat index 545bd1d457..72b783344f 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_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat index 98badea18d..f6e9c8e3e7 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_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index a43a66e71c..269d9892eb 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_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 7f76f2fd1c..217e955161 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_low_density/inputs_true.dat b/tests/regression_tests/random_ray_low_density/inputs_true.dat index ab91f74e50..20deba664b 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 088f803bfa..b4bd263f5a 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 0000000000..e69de29bb2 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 0000000000..c0dc6292f7 --- /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 0000000000..5fb6f79041 --- /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 0000000000..712d9c1241 --- /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 aa28e7b68b..66390c7666 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 e4b2f22fa2..45228a0395 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 8e8a8ed9b8..4d1af46b12 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 1e25b97da6..a268d55d04 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 78c1626976..777ccaea51 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 47a8a71824..dd11567f69 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 80a9ada4d5..6933fba435 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 4f032a62a8..3ccab1d21b 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/surface_tally/results_true.dat b/tests/regression_tests/surface_tally/results_true.dat index 70d5cad2c6..e50102a887 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 e496ac0f65..aa03a8aa2d 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 eb9393179f..0000000000 --- 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 0000000000..57dc42ca4c --- /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 0000000000..d3dfa119d8 --- /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 122c5d8909..0000000000 --- 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 0000000000..6ec7d12e33 --- /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 0000000000..01381db32c --- /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 0000000000..e69de29bb2 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 0000000000..fe144dc62e --- /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 0000000000..b457a245c7 --- /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 0000000000..bafff89c17 --- /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 0000000000..11e5ffa77f --- /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 0000000000..c3a3e91a22 --- /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 0000000000..aea1ff1328 --- /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 cfb651338c..1d3b063bd4 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,15 @@ 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_wwinp_cylindrical(): diff --git a/tests/regression_tests/weightwindows/ww_n.txt b/tests/regression_tests/weightwindows/ww_n.txt index dbb49537bd..36d7673c6f 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 09ff01ebd1..da69b9e13f 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 5fa6505ddf..6bdabfbee3 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 0000000000..e69de29bb2 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 0000000000..ffdd977ec0 --- /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 0000000000..5991fc621c --- /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 0000000000..abd3f20986 --- /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 ceb89e6e34..a0d84257a8 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 c7691e950c..62f8478586 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 0000000000..e69de29bb2 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 0000000000..4cd24d5291 --- /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 0000000000..c57e8ff1c8 --- /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 0000000000..8bb499a73b --- /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 0000000000..c57e8ff1c8 --- /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 0000000000..b6b49be260 --- /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/unit_tests/dagmc/test_convert_to_multigroup.py b/tests/unit_tests/dagmc/test_convert_to_multigroup.py new file mode 100644 index 0000000000..069dc658e5 --- /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 3a4166009d..48b6cf1657 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/test_cell.py b/tests/unit_tests/test_cell.py index 60b2058186..2baa59f1bf 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 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 0000000000..0fe12a7db2 --- /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_multipole.py b/tests/unit_tests/test_data_multipole.py index 105099bb94..a58ab48652 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_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index e90b610224..ef8cb9ef2a 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 8987fbd7aa..64cff3a8b7 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_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 558cb434be..ec886e7079 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 @@ -183,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""" @@ -226,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 0000000000..425b8f8405 --- /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 5762a8511b..26529e6ce9 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,127 @@ 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]) + +# --------------------------------------------------------------------------- +# 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 9a4699a4fd..39c532c549 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -1,10 +1,11 @@ """Tests the Results class""" -from pathlib import Path from math import inf +from pathlib import Path import numpy as np import pytest + import openmc.deplete @@ -221,3 +222,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_filter_mesh.py b/tests/unit_tests/test_filter_mesh.py index faa43af474..e26bea337b 100644 --- a/tests/unit_tests/test_filter_mesh.py +++ b/tests/unit_tests/test_filter_mesh.py @@ -285,3 +285,128 @@ def test_mesh_filter_rotation_roundtrip(run_in_tmpdir): 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 0000000000..df8fc9d7b4 --- /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 8c56a310e1..2e7481c876 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 e5a4d198e8..51e648dcf9 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) @@ -920,6 +934,60 @@ def test_property_map(lib_init): 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) @@ -1046,9 +1114,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 764c98d41a..89dfc03ddd 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -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() @@ -554,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 @@ -587,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] @@ -610,6 +623,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 @@ -643,6 +658,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] @@ -673,6 +690,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 @@ -819,3 +838,58 @@ def test_material_from_constructor(): 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_mesh.py b/tests/unit_tests/test_mesh.py index c5855a7b05..0b28bdfbe4 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -612,6 +612,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): @@ -920,3 +921,110 @@ 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.]) diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py index 5b1173126f..46d22f0d2f 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_mg_inverse_velocity.py b/tests/unit_tests/test_mg_inverse_velocity.py new file mode 100644 index 0000000000..b520e48d19 --- /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_model.py b/tests/unit_tests/test_model.py index 3846ba4fb8..9234b2d272 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -265,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. @@ -286,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) diff --git a/tests/unit_tests/test_pin.py b/tests/unit_tests/test_pin.py index 5496a3b73a..53665b1a60 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_r2s.py b/tests/unit_tests/test_r2s.py index a94f85c8c0..266bd49763 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) @@ -68,7 +68,7 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): 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' @@ -78,7 +78,8 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): # 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 @@ -93,11 +94,77 @@ 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, + ) + + # 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.10.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 diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index fe618fd2d6..bdb3ea8fe9 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_source.py b/tests/unit_tests/test_source.py index bb8a1b7852..394c09e739 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_stats.py b/tests/unit_tests/test_stats.py index 507e85743d..2754cf5d09 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -1,9 +1,11 @@ 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 from tests.unit_tests import assert_sample_mean @@ -273,7 +275,7 @@ def test_watt(): @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 @@ -281,9 +283,12 @@ def test_tabular(): 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') @@ -291,6 +296,12 @@ def test_tabular(): 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) @@ -550,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 @@ -581,6 +699,92 @@ def test_normal(): 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) def test_muir(): mean = 10.0 @@ -632,6 +836,23 @@ 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]) @@ -710,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 e9560223d1..f5c0b8f8b6 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 0000000000..4e067ffe23 --- /dev/null +++ b/tests/unit_tests/test_surface_flux.py @@ -0,0 +1,120 @@ +"""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_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 0000000000..52647a36c7 --- /dev/null +++ b/tests/unit_tests/test_tally.py @@ -0,0 +1,19 @@ +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' diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 6fbf1cc383..0955347f7f 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -49,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 diff --git a/tests/unit_tests/test_vectfit.py b/tests/unit_tests/test_vectfit.py new file mode 100644 index 0000000000..f7b2e905db --- /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 0000000000..8713d4b0bd --- /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 d6e509522f..efa203b510 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/tools/ci/gha-install-njoy.sh b/tools/ci/gha-install-njoy.sh index 8255ffea83..168fcd4a7b 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 bd38e1ea8c..0000000000 --- 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 1cc792f8d7..5488b95476 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 74c3947f18..4cf62afbb7 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 3634f2ded1..0000000000 --- a/vendor/xtensor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3634f2ded19e0cf38208c8b86cea9e1d7c8e397d diff --git a/vendor/xtl b/vendor/xtl deleted file mode 160000 index a7c1c5444d..0000000000 --- a/vendor/xtl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a7c1c5444dfc57f76620391af4c94785ff82c8d6