├── .gitignore ├── README.md ├── benchmarking ├── __init__.py ├── benchmark.py ├── plot.py ├── report.py ├── results.py └── uncertainties.csv ├── setup.py └── validation ├── __init__.py ├── neutron_physics.py ├── photon_physics.py ├── photon_production.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules 2 | *.pyc 3 | 4 | # setuptools build and distribution folder 5 | build/ 6 | dist/ 7 | 8 | # Python egg metadata 9 | validation.egg-info 10 | 11 | # emacs and vim backups 12 | *~ 13 | *.swp 14 | 15 | # Validation 16 | openmc/ 17 | mcnp/ 18 | serpent/ 19 | plots/ 20 | 21 | # Benchmarking 22 | results/ 23 | benchmarks/ 24 | 25 | # macOS 26 | *.DS_Store 27 | 28 | # tex outputs 29 | *.tex 30 | 31 | # Jupyter Notebooks 32 | *.ipynb* 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | This repository contains a collection of validation scripts, notebooks, results, etc. that have been used in the preparation of reports and articles. The following scripts are currently available: 4 | 5 | - `openmc-run-benchmarks` -- Run a collection of ICSBEP benchmarks with OpenMC or MCNP6 6 | - `openmc-plot-benchmarks` -- Plot results from `openmc-run-benchmarks` 7 | - `openmc-report-benchmarks` -- Create .tex file comparing results from `openmc-run-benchmarks` to experimental values 8 | - `openmc-validate-neutron-physics` -- Used for validating neutron transport in OpenMC. The script generates an infinite medium problem of a single nuclide with a point source at a single energy and compares the resulting neutron energy spectra from OpenMC and either MCNP6 or Serpent 2. This script requires that you have MCNP6 or Serpent 2 installed (`mcnp6` or `sss2` executable available). 9 | - `openmc-validate-photon-physics` -- Used for validating photon transport in OpenMC. The script generates an infinite medium problem of a single element with a point source at a single energy and compares the resulting photon energy spectra from OpenMC and either MCNP6 or Serpent 2. This script requires that you have MCNP6 or Serpent 2 installed (`mcnp6` or `sss2` executable available). 10 | - `openmc-validate-photon-production` -- Used for validating photon production in OpenMC. This script generates a broomstick problem where monoenergetic neutrons are shot down a thin, infinitely long cylinder. The energy spectra of photons produced from neutron reactions is tallied along the surface of the cylinder. Comparisons are made between OpenMC and either MCNP6 or Serpent 2. Again, this script requires that MCNP6 or Serpent 2 is properly installed. 11 | 12 | ## Installation 13 | 14 | The validation package can be installed via: 15 | 16 | ```bash 17 | python setup.py install 18 | ``` 19 | 20 | ## Prerequisites 21 | 22 | OpenMC, NumPy, Matplotlib, h5py 23 | -------------------------------------------------------------------------------- /benchmarking/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/validation/50599804d0cce8d5f2345ef094611d00211b8c4d/benchmarking/__init__.py -------------------------------------------------------------------------------- /benchmarking/benchmark.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from pathlib import Path 4 | import re 5 | import shutil 6 | import subprocess 7 | import time 8 | 9 | import openmc 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('-l', '--benchmarks', type=Path, 15 | default=Path('benchmarks/lists/pst-short'), 16 | help='List of benchmarks to run.') 17 | parser.add_argument('-c', '--code', choices=['openmc', 'mcnp'], 18 | default='openmc', 19 | help='Code used to run benchmarks.') 20 | parser.add_argument('-x', '--cross-sections', type=str, 21 | default=os.getenv('OPENMC_CROSS_SECTIONS'), 22 | help='OpenMC cross sections XML file.') 23 | parser.add_argument('-s', '--suffix', type=str, default='80c', 24 | help='MCNP cross section suffix') 25 | parser.add_argument('--suffix-thermal', type=str, default='20t', 26 | help='MCNP thermal scattering suffix') 27 | parser.add_argument('-p', '--particles', type=int, default=10000, 28 | help='Number of source particles.') 29 | parser.add_argument('-b', '--batches', type=int, default=150, 30 | help='Number of batches.') 31 | parser.add_argument('-i', '--inactive', type=int, default=50, 32 | help='Number of inactive batches.') 33 | parser.add_argument('-m', '--max-batches', type=int, default=10000, 34 | help='Maximum number of batches.') 35 | parser.add_argument('--threads', type=int, default=None, 36 | help='Number of OpenMP threads') 37 | parser.add_argument('-t', '--threshold', type=float, default=0.0001, 38 | help='Value of the standard deviation trigger on eigenvalue.') 39 | parser.add_argument('--mpi-args', default="", 40 | help="MPI execute command and any additional MPI arguments") 41 | args = parser.parse_args() 42 | 43 | # Create timestamp 44 | timestamp = time.strftime("%Y-%m-%d-%H%M%S") 45 | 46 | # Check that executable exists 47 | executable = 'mcnp6' if args.code == 'mcnp' else 'openmc' 48 | if not shutil.which(executable, os.X_OK): 49 | msg = f'Unable to locate executable {executable} in path.' 50 | raise IOError(msg) 51 | mpi_args = args.mpi_args.split() 52 | 53 | # Create directory and set filename for results 54 | results_dir = Path('results') 55 | results_dir.mkdir(exist_ok=True) 56 | outfile = results_dir / f'{timestamp}.csv' 57 | 58 | # Get a copy of the benchmarks repository 59 | if not Path('benchmarks').is_dir(): 60 | repo = 'https://github.com/mit-crpg/benchmarks.git' 61 | subprocess.run(['git', 'clone', repo], check=True) 62 | 63 | # Get the list of benchmarks to run 64 | if not args.benchmarks.is_file(): 65 | msg = f'Unable to locate benchmark list {args.benchmarks}.' 66 | raise IOError(msg) 67 | with open(args.benchmarks) as f: 68 | benchmarks = [Path(line) for line in f.read().split()] 69 | 70 | # Set cross sections 71 | if args.cross_sections is not None: 72 | os.environ["OPENMC_CROSS_SECTIONS"] = args.cross_sections 73 | 74 | # Prepare and run benchmarks 75 | for i, benchmark in enumerate(benchmarks): 76 | print(f"{i + 1} {benchmark} ", end="", flush=True) 77 | 78 | path = 'benchmarks' / benchmark 79 | 80 | if args.code == 'openmc': 81 | openmc.reset_auto_ids() 82 | 83 | # Remove old statepoint files 84 | for f in path.glob('statepoint.*.h5'): 85 | os.remove(f) 86 | 87 | # Modify settings 88 | settings = openmc.Settings.from_xml(path / 'settings.xml') 89 | settings.particles = args.particles 90 | settings.inactive = args.inactive 91 | settings.batches = args.batches 92 | settings.keff_trigger = {'type': 'std_dev', 93 | 'threshold': args.threshold} 94 | settings.trigger_active = True 95 | settings.trigger_max_batches = args.max_batches 96 | settings.output = {'tallies': False} 97 | settings.export_to_xml(path) 98 | 99 | # Re-generate materials if Python script is present 100 | genmat_script = path / "generate_materials.py" 101 | if genmat_script.is_file(): 102 | subprocess.run(["python", "generate_materials.py"], cwd=path) 103 | 104 | # Run benchmark 105 | arg_list = mpi_args + ['openmc'] 106 | if args.threads is not None: 107 | arg_list.extend(['-s', f'{args.threads}']) 108 | proc = subprocess.run( 109 | arg_list, 110 | cwd=path, 111 | stdout=subprocess.PIPE, 112 | stderr=subprocess.STDOUT, 113 | universal_newlines=True, 114 | ) 115 | 116 | # Determine last statepoint 117 | t_last = 0 118 | last_statepoint = None 119 | for sp in path.glob('statepoint.*.h5'): 120 | mtime = sp.stat().st_mtime 121 | if mtime >= t_last: 122 | t_last = mtime 123 | last_statepoint = sp 124 | 125 | # Read k-effective mean and standard deviation from statepoint 126 | if last_statepoint is not None: 127 | with openmc.StatePoint(last_statepoint) as sp: 128 | mean = sp.keff.nominal_value 129 | stdev = sp.keff.std_dev 130 | 131 | else: 132 | # Read input file 133 | with open(path / 'input', 'r') as f: 134 | lines = f.readlines() 135 | 136 | # Update criticality source card 137 | line = f'kcode {args.particles} 1 {args.inactive} {args.batches}\n' 138 | for i in range(len(lines)): 139 | if lines[i].strip().startswith('kcode'): 140 | lines[i] = line 141 | break 142 | 143 | # Update cross section suffix 144 | match = '(7[0-4]c)|(8[0-6]c)' 145 | if not re.match(match, args.suffix): 146 | msg = f'Unsupported cross section suffix {args.suffix}.' 147 | raise ValueError(msg) 148 | lines = [re.sub(match, args.suffix, x) for x in lines] 149 | 150 | # Update thermal cross section suffix 151 | match = r'\.[1-9][0-9]t' 152 | lines = [re.sub(match, f'.{args.suffix_thermal}', x) for x in lines] 153 | 154 | # Write new input file 155 | with open(path / 'input', 'w') as f: 156 | f.write(''.join(lines)) 157 | 158 | # Remove old MCNP output files 159 | for f in ('outp', 'runtpe', 'srctp'): 160 | try: 161 | os.remove(path / f) 162 | except OSError: 163 | pass 164 | 165 | # Run benchmark and capture and print output 166 | arg_list = mpi_args + [executable, 'inp=input'] 167 | if args.threads is not None: 168 | arg_list.extend(['tasks', f'{args.threads}']) 169 | proc = subprocess.run( 170 | arg_list, 171 | cwd=path, 172 | stdout=subprocess.PIPE, 173 | stderr=subprocess.STDOUT, 174 | universal_newlines=True 175 | ) 176 | 177 | # Read k-effective mean and standard deviation from output 178 | with open(path / 'outp', 'r') as f: 179 | for line in f: 180 | if line.strip().startswith('col/abs/trk len'): 181 | words = line.split() 182 | mean = float(words[2]) 183 | stdev = float(words[3]) 184 | break 185 | else: 186 | mean = stdev = "" 187 | 188 | # Write output to file 189 | with open(path / f"output_{timestamp}", "w") as fh: 190 | fh.write(proc.stdout) 191 | 192 | if proc.returncode != 0: 193 | mean = stdev = "" 194 | print() 195 | else: 196 | # Display k-effective 197 | print(f"{mean:.5f} ± {stdev:.5f}" if mean else "") 198 | 199 | # Write results 200 | words = str(benchmark).split('/') 201 | name = words[1] 202 | case = '/' + words[3] if len(words) > 3 else '' 203 | line = f'{name}{case},{mean},{stdev}\n' 204 | with open(outfile, 'a') as f: 205 | f.write(line) 206 | -------------------------------------------------------------------------------- /benchmarking/plot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from fnmatch import fnmatch 3 | from math import sqrt 4 | import os 5 | from pathlib import Path 6 | 7 | import matplotlib.pyplot as plt 8 | from matplotlib.patches import Polygon 9 | import numpy as np 10 | 11 | from .results import get_result_dataframe, get_icsbep_dataframe, abbreviated_name 12 | 13 | 14 | def plot(files, labels=None, plot_type='keff', match=None, show_mean=True, 15 | show_shaded=True, show_uncertainties=True): 16 | """For all benchmark cases, produce a plot comparing the k-effective mean 17 | from the calculation to the experimental value along with uncertainties. 18 | 19 | Parameters 20 | ---------- 21 | files : iterable of str 22 | Name of a results file produced by the benchmarking script. 23 | labels: iterable of str 24 | Labels for each dataset to use in legend 25 | plot_type : {'keff', 'diff'} 26 | Type of plot to produce. A 'keff' plot shows the ratio of the 27 | calculation k-effective mean to the experimental value (C/E). A 'diff' 28 | plot shows the difference between C/E values for different 29 | calculations. Default is 'keff'. 30 | match : str 31 | Pattern to match benchmark names to 32 | show_mean : bool 33 | Whether to show bar/line indicating mean value 34 | show_shaded : bool 35 | Whether to show shaded region indicating uncertainty of mean 36 | show_uncertainties : bool 37 | Whether to show uncertainties for individual cases 38 | 39 | Returns 40 | ------- 41 | matplotlib.axes.Axes 42 | A matplotlib.axes.Axes object 43 | 44 | """ 45 | if labels is None: 46 | labels = [Path(f).name for f in files] 47 | 48 | # Read data from spreadsheets 49 | dataframes = {} 50 | for csvfile, label in zip(files, labels): 51 | dataframes[label] = get_result_dataframe(csvfile).dropna() 52 | 53 | # Get model keff and uncertainty from ICSBEP 54 | icsbep = get_icsbep_dataframe() 55 | 56 | # Determine common benchmarks 57 | base = labels[0] 58 | index = dataframes[base].index 59 | for df in dataframes.values(): 60 | index = index.intersection(df.index) 61 | 62 | # Applying matching as needed 63 | if match is not None: 64 | cond = index.map(lambda x: fnmatch(x, match)) 65 | index = index[cond] 66 | 67 | # Setup x values (integers) and corresponding tick labels 68 | n = index.size 69 | x = np.arange(1, n + 1) 70 | xticklabels = index.map(abbreviated_name) 71 | 72 | fig, ax = plt.subplots(figsize=(17, 6)) 73 | 74 | if plot_type == 'diff': 75 | # Check that two results files are specified 76 | if len(files) < 2: 77 | raise ValueError('Must provide two or more files to create a "diff" plot') 78 | 79 | kwargs = {'mec': 'black', 'mew': 0.15, 'fmt': 'o'} 80 | 81 | keff0 = dataframes[base]['keff'].loc[index] 82 | stdev0 = 1.96*dataframes[base]['stdev'].loc[index] 83 | for i, label in enumerate(labels[1:]): 84 | df = dataframes[label] 85 | keff_i = df['keff'].loc[index] 86 | stdev_i = 1.96*df['stdev'].loc[index] 87 | 88 | diff = (keff_i - keff0) * 1e5 89 | err = np.sqrt(stdev_i**2 + stdev0**2) * 1e5 90 | kwargs['label'] = labels[i + 1] + ' - ' + labels[0] 91 | if show_uncertainties: 92 | ax.errorbar(x, diff, yerr=err, color=f'C{i}', **kwargs) 93 | else: 94 | ax.plot(x, diff, color=f'C{i}', **kwargs) 95 | 96 | # Plot mean difference 97 | if show_mean: 98 | mu = diff.mean() 99 | if show_shaded: 100 | sigma = diff.std() / sqrt(n) 101 | verts = [(0, mu - sigma), (0, mu + sigma), (n+1, mu + sigma), (n+1, mu - sigma)] 102 | poly = Polygon(verts, facecolor=f'C{i}', alpha=0.5) 103 | ax.add_patch(poly) 104 | else: 105 | ax.plot([-1, n], [mu, mu], '-', color=f'C{i}', lw=1.5) 106 | 107 | # Define y-axis label 108 | ylabel = r'$\Delta k_\mathrm{eff}$ [pcm]' 109 | 110 | else: 111 | for i, (label, df) in enumerate(dataframes.items()): 112 | # Calculate keff C/E and its standard deviation 113 | coe = (df['keff'] / icsbep['keff']).loc[index] 114 | stdev = 1.96 * df['stdev'].loc[index] 115 | 116 | # Plot keff C/E 117 | kwargs = {'color': f'C{i}', 'mec': 'black', 'mew': 0.15, 'label': label} 118 | if show_uncertainties: 119 | ax.errorbar(x, coe, yerr=stdev, fmt='o', **kwargs) 120 | else: 121 | ax.plot(x, coe, 'o', **kwargs) 122 | 123 | # Plot mean C/E 124 | if show_mean: 125 | mu = coe.mean() 126 | sigma = coe.std() / sqrt(n) 127 | if show_shaded: 128 | verts = [(0, mu - sigma), (0, mu + sigma), (n+1, mu + sigma), (n+1, mu - sigma)] 129 | poly = Polygon(verts, facecolor=f'C{i}', alpha=0.5) 130 | ax.add_patch(poly) 131 | else: 132 | ax.plot([-1, n], [mu, mu], '-', color=f'C{i}', lw=1.5) 133 | 134 | # Show shaded region of benchmark model uncertainties 135 | unc = icsbep['stdev'].loc[index] 136 | vert = np.block([[x, x[::-1]], [1 + unc, 1 - unc[::-1]]]).T 137 | poly = Polygon(vert, facecolor='gray', edgecolor=None, alpha=0.2) 138 | ax.add_patch(poly) 139 | 140 | # Define axes labels and title 141 | ylabel = r'$k_\mathrm{eff}$ C/E' 142 | 143 | # Configure plot 144 | ax.set_axisbelow(True) 145 | ax.set_xlim((0, n+1)) 146 | ax.set_xticks(x) 147 | ax.set_xticklabels(xticklabels, rotation='vertical') 148 | ax.tick_params(axis='x', which='major', labelsize=10) 149 | ax.tick_params(axis='y', which='major', labelsize=14) 150 | ax.set_xlabel('Benchmark case', fontsize=18) 151 | ax.set_ylabel(ylabel, fontsize=18) 152 | ax.grid(True, which='both', color='lightgray', ls='-', alpha=0.7) 153 | ax.legend(numpoints=1) 154 | return ax 155 | 156 | 157 | def main(): 158 | """Produce plot of benchmark results""" 159 | parser = ArgumentParser() 160 | parser.add_argument('files', nargs='+', help='Result CSV files') 161 | parser.add_argument('--labels', help='Comma-separated list of dataset labels') 162 | parser.add_argument('--plot-type', choices=['keff', 'diff'], default='keff') 163 | parser.add_argument('--match', help='Pattern to match benchmark names to') 164 | parser.add_argument('--show-mean', action='store_true', help='Show line/bar indicating mean') 165 | parser.add_argument('--no-show-mean', dest='show_mean', action='store_false', 166 | help='Do not show line/bar indicating mean') 167 | parser.add_argument('--show-uncertainties', action='store_true', 168 | help='Show uncertainty bars on individual cases') 169 | parser.add_argument('--no-show-uncertainties', dest='show_uncertainties', action='store_false', 170 | help='Do not show uncertainty bars on individual cases') 171 | parser.add_argument('--show-shaded', action='store_true', 172 | help='Show shaded region indicating uncertainty of mean C/E') 173 | parser.add_argument('--no-show-shaded', dest='show_shaded', action='store_false', 174 | help='Do not show shaded region indicating uncertainty of mean C/E') 175 | parser.add_argument('-o', '--output', help='Filename to save to') 176 | parser.set_defaults(show_uncertainties=True, show_shaded=True, show_mean=True) 177 | args = parser.parse_args() 178 | 179 | if args.labels is not None: 180 | args.labels = args.labels.split(',') 181 | 182 | ax = plot( 183 | args.files, 184 | labels=args.labels, 185 | plot_type=args.plot_type, 186 | match=args.match, 187 | show_mean=args.show_mean, 188 | show_shaded=args.show_shaded, 189 | show_uncertainties=args.show_uncertainties, 190 | ) 191 | if args.output is not None: 192 | plt.savefig(args.output, bbox_inches='tight', transparent=True) 193 | else: 194 | plt.show() 195 | -------------------------------------------------------------------------------- /benchmarking/report.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from fnmatch import fnmatch 3 | from pathlib import Path 4 | 5 | from openmc import __version__ 6 | 7 | from .results import get_result_dataframe, get_icsbep_dataframe 8 | 9 | def write_document(result, output, match=None): 10 | """Write LaTeX document section with preamble, run info, and table 11 | entries for all benchmark data comparing the calculated and 12 | experimental values along with uncertainties. 13 | 14 | Parameters 15 | ---------- 16 | result : str 17 | Name of a result csv file produced by the benchmarking script. 18 | output : str 19 | Name of the file to be written, ideally a .tex file 20 | match : str 21 | Pattern to match benchmark names to 22 | 23 | """ 24 | # Define document preamble 25 | preamble = [ 26 | r'\documentclass[12pt]{article}', 27 | r'\usepackage[letterpaper, margin=1in]{geometry}', 28 | r'\usepackage{dcolumn}', 29 | r'\usepackage{tabularx}', 30 | r'\usepackage{booktabs}', 31 | r'\usepackage{longtable}', 32 | r'\usepackage{fancyhdr}', 33 | r'\usepackage{siunitx}', 34 | r'\setlength\LTcapwidth{5.55in}', 35 | r'\setlength\LTleft{0.5in}', 36 | r'\setlength\LTright{0.5in}' 37 | ] 38 | 39 | # Define document start and end snippets 40 | doc_start = [r'\begin{document}', r'\part*{Benchmark Results}'] 41 | doc_end = [r'\end{document}'] 42 | 43 | # Convert from list to string 44 | result = result[0] 45 | 46 | label = Path(result).name 47 | 48 | # Read data from spreadsheet 49 | dataframes = {} 50 | dataframes[label] = get_result_dataframe(result).dropna() 51 | 52 | # Get model keff and uncertainty from ICSBEP 53 | icsbep = get_icsbep_dataframe() 54 | 55 | # Determine ICSBEP case names 56 | base = label 57 | index = dataframes[base].index 58 | 59 | df = dataframes[label] 60 | 61 | # Applying matching as needed 62 | if match is not None: 63 | cond = index.map(lambda x: fnmatch(x, match)) 64 | index = index[cond] 65 | 66 | # Custom Table Description and Caption 67 | desc = (r'Table \ref{tab:1} uses (nuclear data info here) and openmc ' 68 | f'version {__version__} to evaluate ICSBEP benchmarks.') 69 | caption = r'\caption{\label{tab:1} Criticality (' + label + r') Benchmark Results}\\' 70 | 71 | # Define Table Entry 72 | table = [ 73 | desc, 74 | r'\begin{longtable}{lcccc}', 75 | caption, 76 | r'\endfirsthead', 77 | r'\midrule', 78 | r'\multicolumn{5}{r}{Continued on Next Page}\\', 79 | r'\midrule', 80 | r'\endfoot', 81 | r'\bottomrule', 82 | r'\endlastfoot', 83 | r'\toprule', 84 | r'& Exp. $k_{\textrm{eff}}$&Exp. unc.& Calc. $k_{\textrm{eff}}$&Calc. unc.\\', 85 | r'\midrule', 86 | '% DATA', 87 | r'\end{longtable}' 88 | ] 89 | 90 | for case in index: 91 | # Obtain and format calculated values 92 | keff = '{:.6f}'.format(df['keff'].loc[case]) 93 | keff = r'\num{' + keff + '}' 94 | 95 | stdev = '{:.6f}'.format(df['stdev'].loc[case]) 96 | stdev = r'\num{' + stdev + '}' 97 | 98 | # Obtain and format experimental values 99 | icsbep_keff = '{:.4f}'.format(icsbep['keff'].loc[case]) 100 | icsbep_stdev = '{:.4f}'.format(icsbep['stdev'].loc[case]) 101 | 102 | # Insert data values into table as separate entries 103 | table.insert(-1, rf'{case}&{icsbep_keff}&{icsbep_stdev}&{keff}&{stdev}\\') 104 | 105 | # Write all accumulated lines 106 | with open(output, 'w') as tex: 107 | tex.writelines('\n'.join(preamble + doc_start + table + doc_end)) 108 | 109 | 110 | def main(): 111 | """Produce LaTeX document with tabulated benchmark results""" 112 | 113 | parser = ArgumentParser() 114 | parser.add_argument('result', nargs='+', help='Result CSV file') 115 | parser.add_argument('--match', help='Pattern to match benchmark names to') 116 | parser.add_argument('-o', '--output', default='report.tex', help='Filename to save to') 117 | args = parser.parse_args() 118 | 119 | write_document(args.result, args.output, args.match) 120 | -------------------------------------------------------------------------------- /benchmarking/results.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | 4 | import pandas as pd 5 | 6 | 7 | def abbreviated_name(name): 8 | """Return short name for ICSBEP benchmark cases 9 | 10 | Parameters 11 | ---------- 12 | name : str 13 | ICSBEP benchmark name, e.g. "pu-met-fast-021/case-2" 14 | 15 | Returns 16 | ------- 17 | str 18 | Abbreviated name, e.g. "pmf21-2" 19 | 20 | """ 21 | model, *case = name.split('/') 22 | volume, form, spectrum, number = model.split('-') 23 | abbreviation = volume[0] + form[0] + spectrum[0] 24 | if case: 25 | casenum = case[0].replace('case', '') 26 | else: 27 | casenum = '' 28 | return f'{abbreviation}{int(number)}{casenum}' 29 | 30 | 31 | def get_result_dataframe(filename): 32 | """Read the data from a file produced by the benchmarking script. 33 | 34 | Parameters 35 | ---------- 36 | filename : str 37 | Name of a results file produced by the benchmarking script. 38 | 39 | Returns 40 | ------- 41 | pandas.DataFrame 42 | Dataframe with 'keff' and 'stdev' columns. The benchmark name is used as 43 | the index in the dataframe. 44 | 45 | """ 46 | return pd.read_csv( 47 | filename, 48 | header=None, 49 | names=['name', 'keff', 'stdev'], 50 | usecols=[0, 1, 2], 51 | index_col="name", 52 | ) 53 | 54 | 55 | def get_icsbep_dataframe(): 56 | """Read the benchmark model k-effective means and uncertainties. 57 | 58 | Returns 59 | ------- 60 | pandas.DataFrame 61 | Dataframe with 'keff' and 'stdev' columns. The benchmark name is used as 62 | the index in the dataframe. 63 | 64 | """ 65 | cwd = Path(__file__).parent 66 | index = [] 67 | keff = [] 68 | stdev = [] 69 | with open(cwd / 'uncertainties.csv', 'r') as csvfile: 70 | reader = csv.reader(csvfile, skipinitialspace=True) 71 | for benchmark, case, mean, uncertainty in reader: 72 | index.append(f'{benchmark}/{case}' if case else benchmark) 73 | keff.append(float(mean)) 74 | stdev.append(float(uncertainty)) 75 | return pd.DataFrame({'keff': keff, 'stdev': stdev}, index=index) 76 | -------------------------------------------------------------------------------- /benchmarking/uncertainties.csv: -------------------------------------------------------------------------------- 1 | heu-comp-inter-003, case-1, 1.0, 0.0057 2 | heu-comp-inter-003, case-2, 1.0, 0.0061 3 | heu-comp-inter-003, case-3, 1.0, 0.0056 4 | heu-comp-inter-003, case-4, 1.0, 0.0055 5 | heu-comp-inter-003, case-5, 1.0, 0.0047 6 | heu-comp-inter-003, case-6, 1.0, 0.0047 7 | heu-comp-inter-003, case-7, 1.0, 0.0050 8 | heu-met-fast-001, case-1, 1.0, 0.001 9 | heu-met-fast-001, case-2, 1.0, 0.001 10 | heu-met-fast-002, case-1, 1.0, 0.003 11 | heu-met-fast-002, case-2, 1.0, 0.003 12 | heu-met-fast-002, case-3, 1.0, 0.003 13 | heu-met-fast-002, case-4, 1.0, 0.003 14 | heu-met-fast-002, case-5, 1.0, 0.003 15 | heu-met-fast-002, case-6, 1.0, 0.003 16 | heu-met-fast-003, case-1, 1.0, 0.005 17 | heu-met-fast-003, case-2, 1.0, 0.005 18 | heu-met-fast-003, case-3, 1.0, 0.005 19 | heu-met-fast-003, case-4, 1.0, 0.003 20 | heu-met-fast-003, case-5, 1.0, 0.003 21 | heu-met-fast-003, case-6, 1.0, 0.003 22 | heu-met-fast-003, case-7, 1.0, 0.003 23 | heu-met-fast-003, case-8, 1.0, 0.005 24 | heu-met-fast-003, case-9, 1.0, 0.005 25 | heu-met-fast-003, case-10, 1.0, 0.005 26 | heu-met-fast-003, case-11, 1.0, 0.005 27 | heu-met-fast-003, case-12, 1.0, 0.003 28 | heu-met-fast-004, case-1, 1.0020, 0.0 29 | heu-met-fast-004, case-2, 0.9985, 0.0 30 | heu-met-fast-008, , 0.9989, 0.0016 31 | heu-met-fast-009, case-1, 0.9992, 0.0015 32 | heu-met-fast-009, case-2, 0.9992, 0.0015 33 | heu-met-fast-011, , 0.9989, 0.0015 34 | heu-met-fast-012, , 0.9992, 0.0018 35 | heu-met-fast-013, , 0.9990, 0.0015 36 | heu-met-fast-014, , 0.9989, 0.0017 37 | heu-met-fast-015, , 0.9996, 0.0017 38 | heu-met-fast-018, case-1, 1.0, 0.0014 39 | heu-met-fast-018, case-2, 1.0, 0.0014 40 | heu-met-fast-019, case-1, 1.0, 0.0028 41 | heu-met-fast-019, case-2, 1.0, 0.0028 42 | heu-met-fast-020, case-1, 1.0, 0.0028 43 | heu-met-fast-020, case-2, 1.0, 0.0028 44 | heu-met-fast-021, case-1, 1.0, 0.0024 45 | heu-met-fast-021, case-2, 1.0, 0.0024 46 | heu-met-fast-022, case-1, 1.0, 0.0019 47 | heu-met-fast-022, case-2, 1.0, 0.0019 48 | heu-met-fast-026, b-1, 0.9982, 0.0042 49 | heu-met-fast-026, b-2, 0.9982, 0.0042 50 | heu-met-fast-026, b-3, 1.0, 0.0038 51 | heu-met-fast-026, b-4, 1.0, 0.0038 52 | heu-met-fast-026, b-5, 1.0, 0.0038 53 | heu-met-fast-026, b-6, 0.9982, 0.0042 54 | heu-met-fast-026, b-7, 0.9982, 0.0042 55 | heu-met-fast-026, b-8, 1.0, 0.0038 56 | heu-met-fast-026, b-9, 1.0, 0.0038 57 | heu-met-fast-026, b-10, 1.0, 0.0038 58 | heu-met-fast-026, c-1, 0.9982, 0.0042 59 | heu-met-fast-026, c-2, 0.9982, 0.0042 60 | heu-met-fast-026, c-3, 0.9982, 0.0042 61 | heu-met-fast-026, c-4, 1.0, 0.0038 62 | heu-met-fast-026, c-5, 1.0, 0.0038 63 | heu-met-fast-026, c-6, 1.0, 0.0038 64 | heu-met-fast-026, c-7, 1.0, 0.0038 65 | heu-met-fast-026, c-8, 0.9982, 0.0042 66 | heu-met-fast-026, c-9, 0.9982, 0.0042 67 | heu-met-fast-026, c-10, 1.0, 0.0038 68 | heu-met-fast-026, c-11, 1.0, 0.0038 69 | heu-met-fast-026, c-12, 1.0, 0.0038 70 | heu-met-fast-026, d-1, 0.9982, 0.0042 71 | heu-met-fast-026, d-2, 0.9982, 0.0042 72 | heu-met-fast-026, d-3, 1.0, 0.0038 73 | heu-met-fast-026, d-4, 1.0, 0.0038 74 | heu-met-fast-026, d-5, 1.0, 0.0038 75 | heu-met-fast-026, d-6, 0.9982, 0.0042 76 | heu-met-fast-026, d-7, 0.9982, 0.0042 77 | heu-met-fast-026, d-8, 1.0, 0.0038 78 | heu-met-fast-026, d-9, 1.0, 0.0038 79 | heu-met-fast-026, d-10, 1.0, 0.0038 80 | heu-met-fast-027, , 1.0, 0.0025 81 | heu-met-fast-028, , 1.0, 0.0030 82 | heu-met-fast-029, , 1.0, 0.0020 83 | heu-met-fast-032, case-1, 1.0, 0.0016 84 | heu-met-fast-032, case-2, 1.0, 0.0027 85 | heu-met-fast-032, case-3, 1.0, 0.0017 86 | heu-met-fast-032, case-4, 1.0, 0.0017 87 | heu-met-fast-034, case-1, 0.9990, 0.0012 88 | heu-met-fast-034, case-2, 0.9990, 0.0012 89 | heu-met-fast-034, case-3, 0.9990, 0.0012 90 | heu-met-fast-041, case-1, 1.0013, 0.0030 91 | heu-met-fast-041, case-2, 1.0022, 0.0043 92 | heu-met-fast-041, case-3, 1.0006, 0.0029 93 | heu-met-fast-041, case-4, 1.0006, 0.0025 94 | heu-met-fast-041, case-5, 1.0006, 0.0031 95 | heu-met-fast-041, case-6, 1.0006, 0.0045 96 | heu-met-fast-058, case-1, 1.0, 0.0025 97 | heu-met-fast-058, case-2, 1.0, 0.0035 98 | heu-met-fast-058, case-3, 1.0, 0.0027 99 | heu-met-fast-058, case-4, 1.0, 0.0021 100 | heu-met-fast-058, case-5, 1.0, 0.0033 101 | heu-met-fast-066, case-1, 1.0030, 0.0033 102 | heu-met-fast-066, case-2, 1.0023, 0.0029 103 | heu-met-fast-066, case-3, 1.0023, 0.0026 104 | heu-met-fast-066, case-4, 1.0043, 0.0043 105 | heu-met-fast-066, case-5, 1.0030, 0.0033 106 | heu-met-fast-066, case-6, 1.0028, 0.0030 107 | heu-met-fast-066, case-7, 1.0048, 0.0039 108 | heu-met-fast-066, case-8, 1.0039, 0.0040 109 | heu-met-fast-066, case-9, 1.0027, 0.0036 110 | heu-met-fast-069, case-1, 0.9998, 0.0004 111 | heu-met-fast-069, case-2, 0.9994, 0.0004 112 | heu-met-inter-006, case-1, 0.9977, 0.0008 113 | heu-met-inter-006, case-2, 1.0001, 0.0008 114 | heu-met-inter-006, case-3, 1.0015, 0.0009 115 | heu-met-inter-006, case-4, 1.0016, 0.0008 116 | heu-met-therm-001, case-1, 1.0010, 0.0060 117 | heu-met-therm-001, case-2, 1.0010, 0.0060 118 | heu-met-therm-014, case-1, 0.9931, 0.0015 119 | heu-met-therm-014, case-2, 0.9939, 0.0015 120 | heu-sol-therm-001, case-1, 1.0004, 0.0060 121 | heu-sol-therm-001, case-2, 1.0021, 0.0072 122 | heu-sol-therm-001, case-3, 1.0003, 0.0035 123 | heu-sol-therm-001, case-4, 1.0008, 0.0053 124 | heu-sol-therm-001, case-5, 1.0001, 0.0049 125 | heu-sol-therm-001, case-6, 1.0002, 0.0046 126 | heu-sol-therm-001, case-7, 1.0008, 0.0040 127 | heu-sol-therm-001, case-8, 0.9998, 0.0038 128 | heu-sol-therm-001, case-9, 1.0008, 0.0054 129 | heu-sol-therm-001, case-10, 0.9993, 0.0054 130 | heu-sol-therm-004, case-1, 1.0, 0.00325 131 | heu-sol-therm-004, case-2, 1.0, 0.00355 132 | heu-sol-therm-004, case-3, 1.0, 0.0039 133 | heu-sol-therm-004, case-4, 1.0, 0.00455 134 | heu-sol-therm-004, case-5, 1.0, 0.0052 135 | heu-sol-therm-004, case-6, 1.0, 0.00585 136 | heu-sol-therm-009, case-1, 0.9990, 0.0043 137 | heu-sol-therm-009, case-2, 1.0, 0.0039 138 | heu-sol-therm-009, case-3, 1.0, 0.0036 139 | heu-sol-therm-009, case-4, 0.9986, 0.0035 140 | heu-sol-therm-010, case-1, 1.0, 0.0029 141 | heu-sol-therm-010, case-2, 1.0, 0.0029 142 | heu-sol-therm-010, case-3, 1.0, 0.0029 143 | heu-sol-therm-010, case-4, 0.9992, 0.0029 144 | heu-sol-therm-011, case-1, 1.0, 0.0023 145 | heu-sol-therm-011, case-2, 1.0, 0.0023 146 | heu-sol-therm-012, , 0.9999, 0.0058 147 | heu-sol-therm-013, case-1, 1.0012, 0.0026 148 | heu-sol-therm-013, case-2, 1.0007, 0.0036 149 | heu-sol-therm-013, case-3, 1.0009, 0.0036 150 | heu-sol-therm-013, case-4, 1.0003, 0.0036 151 | heu-sol-therm-020, case-1, 0.9966, 0.0116 152 | heu-sol-therm-020, case-2, 0.9956, 0.0093 153 | heu-sol-therm-020, case-3, 0.9957, 0.0079 154 | heu-sol-therm-020, case-4, 0.9955, 0.0078 155 | heu-sol-therm-020, case-5, 0.9959, 0.0077 156 | heu-sol-therm-032, , 1.0015, 0.0026 157 | heu-sol-therm-042, case-1, 0.9957, 0.0039 158 | heu-sol-therm-042, case-2, 0.9965, 0.0036 159 | heu-sol-therm-042, case-3, 0.9994, 0.0028 160 | heu-sol-therm-042, case-4, 1.0, 0.0034 161 | heu-sol-therm-042, case-5, 1.0, 0.0034 162 | heu-sol-therm-042, case-6, 1.0, 0.0037 163 | heu-sol-therm-042, case-7, 1.0, 0.0036 164 | heu-sol-therm-042, case-8, 1.0, 0.0035 165 | heu-sol-therm-043, case-1, 0.9986, 0.0031 166 | heu-sol-therm-043, case-2, 0.9995, 0.0026 167 | heu-sol-therm-043, case-3, 0.9990, 0.0025 168 | ieu-met-fast-001, case-1, 0.9988, 0.0009 169 | ieu-met-fast-001, case-2, 0.9997, 0.0009 170 | ieu-met-fast-001, case-3, 0.9993, 0.0003 171 | ieu-met-fast-001, case-4, 1.0002, 0.0003 172 | ieu-met-fast-002, , 1.0, 0.0030 173 | ieu-met-fast-003, case-1, 1.0, 0.0017 174 | ieu-met-fast-003, case-2, 1.0, 0.0017 175 | ieu-met-fast-004, case-1, 1.0, 0.0030 176 | ieu-met-fast-004, case-2, 1.0, 0.0030 177 | ieu-met-fast-005, case-1, 1.0, 0.0021 178 | ieu-met-fast-005, case-2, 1.0, 0.0021 179 | ieu-met-fast-006, case-1, 1.0, 0.0023 180 | ieu-met-fast-006, case-2, 1.0, 0.0023 181 | ieu-met-fast-007, case-1, 1.0045, 0.0007 182 | ieu-met-fast-007, case-2, 1.0049, 0.0008 183 | ieu-met-fast-007, case-4, 1.0049, 0.0008 184 | ieu-met-fast-009, , 1.0, 0.0053 185 | leu-comp-therm-008, case-1, 1.0007, 0.0012 186 | leu-comp-therm-008, case-2, 1.0007, 0.0012 187 | leu-comp-therm-008, case-3, 1.0007, 0.0012 188 | leu-comp-therm-008, case-4, 1.0007, 0.0012 189 | leu-comp-therm-008, case-5, 1.0007, 0.0012 190 | leu-comp-therm-008, case-6, 1.0007, 0.0012 191 | leu-comp-therm-008, case-7, 1.0007, 0.0012 192 | leu-comp-therm-008, case-8, 1.0007, 0.0012 193 | leu-comp-therm-008, case-9, 1.0007, 0.0012 194 | leu-comp-therm-008, case-10, 1.0007, 0.0012 195 | leu-comp-therm-008, case-11, 1.0007, 0.0012 196 | leu-comp-therm-008, case-12, 1.0007, 0.0012 197 | leu-comp-therm-008, case-13, 1.0007, 0.0012 198 | leu-comp-therm-008, case-14, 1.0007, 0.0012 199 | leu-comp-therm-008, case-15, 1.0007, 0.0012 200 | leu-comp-therm-008, case-16, 1.0007, 0.0012 201 | leu-comp-therm-008, case-17, 1.0007, 0.0012 202 | leu-sol-therm-001, , 0.9991, 0.0029 203 | leu-sol-therm-002, case-1, 1.0038, 0.0040 204 | leu-sol-therm-002, case-2, 1.0024, 0.0037 205 | leu-sol-therm-002, case-3, 1.0024, 0.0044 206 | leu-sol-therm-003, case-1, 0.9997, 0.0039 207 | leu-sol-therm-003, case-2, 0.9993, 0.0042 208 | leu-sol-therm-003, case-3, 0.9995, 0.0042 209 | leu-sol-therm-003, case-4, 0.9995, 0.0042 210 | leu-sol-therm-003, case-5, 0.9997, 0.0048 211 | leu-sol-therm-003, case-6, 0.9999, 0.0049 212 | leu-sol-therm-003, case-7, 0.9994, 0.0049 213 | leu-sol-therm-003, case-8, 0.9993, 0.0052 214 | leu-sol-therm-003, case-9, 0.9996, 0.0052 215 | leu-sol-therm-004, case-1, 0.9994, 0.0008 216 | leu-sol-therm-004, case-29, 0.9999, 0.0009 217 | leu-sol-therm-004, case-33, 0.9999, 0.0009 218 | leu-sol-therm-004, case-34, 0.9999, 0.0010 219 | leu-sol-therm-004, case-46, 0.9999, 0.0010 220 | leu-sol-therm-004, case-51, 0.9994, 0.0011 221 | leu-sol-therm-004, case-54, 0.9996, 0.0011 222 | leu-sol-therm-007, case-14, 0.9961, 0.0009 223 | leu-sol-therm-007, case-30, 0.9973, 0.0009 224 | leu-sol-therm-007, case-32, 0.9985, 0.0010 225 | leu-sol-therm-007, case-36, 0.9988, 0.0011 226 | leu-sol-therm-007, case-49, 0.9983, 0.0011 227 | leu-sol-therm-016, case-105, 0.9996, 0.0013 228 | leu-sol-therm-016, case-113, 0.9999, 0.0013 229 | leu-sol-therm-016, case-125, 0.9994, 0.0014 230 | leu-sol-therm-016, case-129, 0.9996, 0.0014 231 | leu-sol-therm-016, case-131, 0.9995, 0.0014 232 | leu-sol-therm-016, case-140, 0.9992, 0.0015 233 | leu-sol-therm-016, case-196, 0.9994, 0.0015 234 | mix-comp-therm-002, pnl-30d, 1.0010, 0.0059 235 | mix-comp-therm-002, pnl-31d, 1.0009, 0.0045 236 | mix-comp-therm-002, pnl-32d, 1.0024, 0.0029 237 | mix-comp-therm-002, pnl-33d, 1.0024, 0.0021 238 | mix-comp-therm-002, pnl-34d, 1.0038, 0.0022 239 | mix-comp-therm-002, pnl-35d, 1.0029, 0.0024 240 | mix-comp-therm-002, pnl-30, 1.0024, 0.0060 241 | mix-comp-therm-002, pnl-31, 1.0009, 0.0047 242 | mix-comp-therm-002, pnl-32, 1.0042, 0.0031 243 | mix-comp-therm-002, pnl-33, 1.0024, 0.0024 244 | mix-comp-therm-002, pnl-34, 1.0038, 0.0025 245 | mix-comp-therm-002, pnl-35, 1.0029, 0.0027 246 | mix-comp-therm-004, case-1, 1.0, 0.0046 247 | mix-comp-therm-004, case-2, 1.0, 0.0046 248 | mix-comp-therm-004, case-3, 1.0, 0.0046 249 | mix-comp-therm-004, case-4, 1.0, 0.0039 250 | mix-comp-therm-004, case-5, 1.0, 0.0039 251 | mix-comp-therm-004, case-6, 1.0, 0.0039 252 | mix-comp-therm-004, case-7, 1.0, 0.0040 253 | mix-comp-therm-004, case-8, 1.0, 0.0040 254 | mix-comp-therm-004, case-9, 1.0, 0.0040 255 | mix-comp-therm-004, case-10, 1.0, 0.0051 256 | mix-comp-therm-004, case-11, 1.0, 0.0051 257 | mix-met-fast-001, , 1.0, 0.0016 258 | mix-met-fast-003, , 0.9993, 0.0016 259 | mix-met-fast-007, case-1, 1.0, 0.0045 260 | mix-met-fast-007, case-2, 1.0, 0.0023 261 | mix-met-fast-007, case-3, 1.0, 0.0028 262 | mix-met-fast-007, case-4, 1.0, 0.0028 263 | mix-met-fast-007, case-5, 1.0, 0.0032 264 | mix-met-fast-007, case-6, 1.0, 0.0035 265 | mix-met-fast-007, case-7, 1.0, 0.0032 266 | mix-met-fast-007, case-8, 1.0, 0.0030 267 | mix-met-fast-007, case-9, 1.0, 0.0028 268 | mix-met-fast-007, case-10, 1.0, 0.0027 269 | mix-met-fast-007, case-11, 1.0, 0.0026 270 | mix-met-fast-007, case-12, 1.0, 0.0030 271 | mix-met-fast-007, case-13, 1.0, 0.0033 272 | mix-met-fast-007, case-14, 1.0, 0.0032 273 | mix-met-fast-007, case-15, 1.0, 0.0032 274 | mix-met-fast-007, case-16, 1.0, 0.0028 275 | mix-met-fast-007, case-17, 1.0, 0.0028 276 | mix-met-fast-007, case-18, 1.0, 0.0030 277 | mix-met-fast-007, case-19, 1.0, 0.0034 278 | mix-met-fast-007, case-20, 1.0, 0.0030 279 | mix-met-fast-007, case-21, 1.0, 0.0031 280 | mix-met-fast-007, case-22, 1.0, 0.0030 281 | mix-met-fast-007, case-23, 1.0, 0.0028 282 | mix-met-fast-008, 8a-2, 0.9992, 0.0063 283 | mix-met-fast-008, 8b, 1.0010, 0.0023 284 | mix-met-fast-008, 8c-2, 0.9860, 0.0044 285 | mix-met-fast-008, 8d, 0.9730, 0.0045 286 | mix-met-fast-008, 8e, 1.0060, 0.0069 287 | mix-met-fast-008, 8f-2, 0.9710, 0.0042 288 | mix-met-fast-008, 8h, 1.0300, 0.0025 289 | pu-comp-inter-001, , 1.0, 0.0110 290 | pu-met-fast-001, , 1.0, 0.0020 291 | pu-met-fast-002, , 1.0, 0.0020 292 | pu-met-fast-003, case-101, 1.0, 0.0030 293 | pu-met-fast-003, case-102, 1.0, 0.0030 294 | pu-met-fast-003, case-103, 1.0, 0.0030 295 | pu-met-fast-003, case-104, 1.0, 0.0030 296 | pu-met-fast-003, case-105, 1.0, 0.0030 297 | pu-met-fast-005, , 1.0, 0.0013 298 | pu-met-fast-006, , 1.0, 0.0030 299 | pu-met-fast-008, case-1, 1.0, 0.0006 300 | pu-met-fast-008, case-2, 1.0, 0.0006 301 | pu-met-fast-009, , 1.0, 0.0027 302 | pu-met-fast-010, , 1.0, 0.0018 303 | pu-met-fast-011, , 1.0, 0.0010 304 | pu-met-fast-012, case-1, 1.0009, 0.0021 305 | pu-met-fast-015, case-1, 1.0041, 0.0026 306 | pu-met-fast-018, , 1.0, 0.0030 307 | pu-met-fast-019, , 0.9992, 0.0015 308 | pu-met-fast-020, , 0.9993, 0.0017 309 | pu-met-fast-021, case-1, 1.0, 0.0026 310 | pu-met-fast-021, case-2, 1.0, 0.0026 311 | pu-met-fast-022, case-1, 1.0, 0.0021 312 | pu-met-fast-022, case-2, 1.0, 0.0021 313 | pu-met-fast-023, case-1, 1.0, 0.0020 314 | pu-met-fast-023, case-2, 1.0, 0.0020 315 | pu-met-fast-024, case-1, 1.0, 0.0020 316 | pu-met-fast-024, case-2, 1.0, 0.0020 317 | pu-met-fast-025, case-1, 1.0, 0.0020 318 | pu-met-fast-025, case-2, 1.0, 0.0020 319 | pu-met-fast-026, case-1, 1.0, 0.0024 320 | pu-met-fast-026, case-2, 1.0, 0.0024 321 | pu-met-fast-029, case-1, 1.0, 0.0020 322 | pu-met-fast-029, case-2, 1.0, 0.0020 323 | pu-met-fast-032, case-1, 1.0, 0.0020 324 | pu-met-fast-032, case-2, 1.0, 0.0020 325 | pu-met-fast-035, case-1, 1.0, 0.0016 326 | pu-met-fast-036, case-1, 1.0, 0.0031 327 | pu-met-fast-039, case-1, 1.0, 0.0022 328 | pu-met-fast-040, case-1, 1.0, 0.0038 329 | pu-met-fast-044, case-1, 0.9977, 0.0021 330 | pu-met-fast-044, case-2, 0.9980, 0.0022 331 | pu-met-fast-044, case-3, 0.9977, 0.0021 332 | pu-met-fast-044, case-4, 0.9978, 0.0026 333 | pu-met-fast-044, case-5, 0.9977, 0.0024 334 | pu-sol-therm-001, case-1, 1.0, 0.0050 335 | pu-sol-therm-001, case-2, 1.0, 0.0050 336 | pu-sol-therm-001, case-3, 1.0, 0.0050 337 | pu-sol-therm-001, case-4, 1.0, 0.0050 338 | pu-sol-therm-001, case-5, 1.0, 0.0050 339 | pu-sol-therm-001, case-6, 1.0, 0.0050 340 | pu-sol-therm-002, case-1, 1.0, 0.0047 341 | pu-sol-therm-002, case-2, 1.0, 0.0047 342 | pu-sol-therm-002, case-3, 1.0, 0.0047 343 | pu-sol-therm-002, case-4, 1.0, 0.0047 344 | pu-sol-therm-002, case-5, 1.0, 0.0047 345 | pu-sol-therm-002, case-6, 1.0, 0.0047 346 | pu-sol-therm-002, case-7, 1.0, 0.0047 347 | pu-sol-therm-003, case-1, 1.0, 0.0047 348 | pu-sol-therm-003, case-2, 1.0, 0.0047 349 | pu-sol-therm-003, case-3, 1.0, 0.0047 350 | pu-sol-therm-003, case-4, 1.0, 0.0047 351 | pu-sol-therm-003, case-5, 1.0, 0.0047 352 | pu-sol-therm-003, case-6, 1.0, 0.0047 353 | pu-sol-therm-003, case-7, 1.0, 0.0047 354 | pu-sol-therm-003, case-8, 1.0, 0.0047 355 | pu-sol-therm-004, case-1, 1.0, 0.0047 356 | pu-sol-therm-004, case-2, 1.0, 0.0047 357 | pu-sol-therm-004, case-3, 1.0, 0.0047 358 | pu-sol-therm-004, case-4, 1.0, 0.0047 359 | pu-sol-therm-004, case-5, 1.0, 0.0047 360 | pu-sol-therm-004, case-6, 1.0, 0.0047 361 | pu-sol-therm-004, case-7, 1.0, 0.0047 362 | pu-sol-therm-004, case-8, 1.0, 0.0047 363 | pu-sol-therm-004, case-9, 1.0, 0.0047 364 | pu-sol-therm-004, case-10, 1.0, 0.0047 365 | pu-sol-therm-004, case-11, 1.0, 0.0047 366 | pu-sol-therm-004, case-12, 1.0, 0.0047 367 | pu-sol-therm-004, case-13, 1.0, 0.0047 368 | pu-sol-therm-005, case-1, 1.0, 0.0047 369 | pu-sol-therm-005, case-2, 1.0, 0.0047 370 | pu-sol-therm-005, case-3, 1.0, 0.0047 371 | pu-sol-therm-005, case-4, 1.0, 0.0047 372 | pu-sol-therm-005, case-5, 1.0, 0.0047 373 | pu-sol-therm-005, case-6, 1.0, 0.0047 374 | pu-sol-therm-005, case-7, 1.0, 0.0047 375 | pu-sol-therm-005, case-8, 1.0, 0.0047 376 | pu-sol-therm-005, case-9, 1.0, 0.0047 377 | pu-sol-therm-006, case-1, 1.0, 0.0035 378 | pu-sol-therm-006, case-2, 1.0, 0.0035 379 | pu-sol-therm-006, case-3, 1.0, 0.0035 380 | pu-sol-therm-007, case-2, 1.0, 0.0047 381 | pu-sol-therm-007, case-3, 1.0, 0.0047 382 | pu-sol-therm-007, case-5, 1.0, 0.0047 383 | pu-sol-therm-007, case-6, 1.0, 0.0047 384 | pu-sol-therm-007, case-7, 1.0, 0.0047 385 | pu-sol-therm-007, case-8, 1.0, 0.0047 386 | pu-sol-therm-007, case-9, 1.0, 0.0047 387 | pu-sol-therm-007, case-10, 1.0, 0.0047 388 | pu-sol-therm-009, case-1, 1.0000, 0.0033 389 | pu-sol-therm-009, case-1a, 1.0003, 0.0033 390 | pu-sol-therm-009, case-2, 1.0000, 0.0033 391 | pu-sol-therm-009, case-2a, 1.0003, 0.0033 392 | pu-sol-therm-009, case-3, 1.0000, 0.0033 393 | pu-sol-therm-009, case-3a, 1.0003, 0.0033 394 | pu-sol-therm-011, case-16-1, 1.0, 0.0052 395 | pu-sol-therm-011, case-16-2, 1.0, 0.0052 396 | pu-sol-therm-011, case-16-3, 1.0, 0.0052 397 | pu-sol-therm-011, case-16-4, 1.0, 0.0052 398 | pu-sol-therm-011, case-16-5, 1.0, 0.0052 399 | pu-sol-therm-011, case-18-1, 1.0, 0.0052 400 | pu-sol-therm-011, case-18-2, 1.0, 0.0052 401 | pu-sol-therm-011, case-18-3, 1.0, 0.0052 402 | pu-sol-therm-011, case-18-4, 1.0, 0.0052 403 | pu-sol-therm-011, case-18-5, 1.0, 0.0052 404 | pu-sol-therm-011, case-18-6, 1.0, 0.0052 405 | pu-sol-therm-011, case-18-7, 1.0, 0.0052 406 | pu-sol-therm-012, case-1, 1.0, 0.0043 407 | pu-sol-therm-012, case-2, 1.0, 0.0043 408 | pu-sol-therm-012, case-3, 1.0, 0.0058 409 | pu-sol-therm-012, case-4, 1.0, 0.0058 410 | pu-sol-therm-012, case-5, 1.0, 0.0058 411 | pu-sol-therm-012, case-6, 1.0, 0.0007 412 | pu-sol-therm-012, case-7, 1.0, 0.0013 413 | pu-sol-therm-012, case-8, 1.0, 0.0013 414 | pu-sol-therm-012, case-9, 1.0, 0.0043 415 | pu-sol-therm-012, case-10, 1.0, 0.0043 416 | pu-sol-therm-012, case-11, 1.0, 0.0043 417 | pu-sol-therm-012, case-12, 1.0, 0.0043 418 | pu-sol-therm-012, case-13, 1.0, 0.0058 419 | pu-sol-therm-012, case-14, 1.0, 0.0013 420 | pu-sol-therm-012, case-15, 1.0, 0.0043 421 | pu-sol-therm-012, case-16, 1.0, 0.0043 422 | pu-sol-therm-012, case-17, 1.0, 0.0043 423 | pu-sol-therm-012, case-18, 1.0, 0.0043 424 | pu-sol-therm-012, case-19, 1.0, 0.0043 425 | pu-sol-therm-012, case-20, 1.0, 0.0058 426 | pu-sol-therm-012, case-21, 1.0, 0.0058 427 | pu-sol-therm-012, case-22, 1.0, 0.0058 428 | pu-sol-therm-012, case-23, 1.0, 0.0058 429 | pu-sol-therm-018, case-1, 1.0, 0.0034 430 | pu-sol-therm-018, case-2, 1.0, 0.0034 431 | pu-sol-therm-018, case-3, 1.0, 0.0032 432 | pu-sol-therm-018, case-4, 1.0, 0.0030 433 | pu-sol-therm-018, case-5, 1.0, 0.0030 434 | pu-sol-therm-018, case-6, 1.0, 0.0031 435 | pu-sol-therm-018, case-7, 1.0, 0.0032 436 | pu-sol-therm-018, case-8, 1.0, 0.0033 437 | pu-sol-therm-018, case-9, 1.0, 0.0034 438 | pu-sol-therm-021, case-1, 1.0, 0.0032 439 | pu-sol-therm-021, case-2, 1.0, 0.0032 440 | pu-sol-therm-021, case-3, 1.0, 0.0065 441 | pu-sol-therm-021, case-4, 1.0, 0.0025 442 | pu-sol-therm-021, case-5, 1.0, 0.0025 443 | pu-sol-therm-021, case-6, 1.0, 0.0044 444 | pu-sol-therm-021, case-7, 1.0, 0.0032 445 | pu-sol-therm-021, case-8, 1.0, 0.0065 446 | pu-sol-therm-021, case-9, 1.0, 0.0032 447 | pu-sol-therm-021, case-10, 1.0, 0.0025 448 | pu-sol-therm-034, case-1, 1.0, 0.0062 449 | pu-sol-therm-034, case-2, 1.0, 0.0044 450 | pu-sol-therm-034, case-3, 1.0, 0.0040 451 | pu-sol-therm-034, case-4, 1.0, 0.0039 452 | pu-sol-therm-034, case-5, 1.0, 0.0040 453 | pu-sol-therm-034, case-6, 1.0, 0.0042 454 | pu-sol-therm-034, case-7, 1.0, 0.0057 455 | pu-sol-therm-034, case-8, 1.0, 0.0055 456 | pu-sol-therm-034, case-9, 1.0, 0.0052 457 | pu-sol-therm-034, case-10, 1.0, 0.0052 458 | pu-sol-therm-034, case-11, 1.0, 0.0048 459 | pu-sol-therm-034, case-12, 1.0, 0.0042 460 | pu-sol-therm-034, case-13, 1.0, 0.0043 461 | pu-sol-therm-034, case-14, 1.0, 0.0044 462 | pu-sol-therm-034, case-15, 1.0, 0.0042 463 | u233-comp-therm-001, case-1, 1.0006, 0.0027 464 | u233-comp-therm-001, case-2, 1.0015, 0.0025 465 | u233-comp-therm-001, case-3, 1.0000, 0.0024 466 | u233-comp-therm-001, case-4, 1.0007, 0.0025 467 | u233-comp-therm-001, case-5, 1.0015, 0.0026 468 | u233-comp-therm-001, case-6, 1.0015, 0.0028 469 | u233-comp-therm-001, case-7, 0.9995, 0.0027 470 | u233-comp-therm-001, case-8, 1.0004, 0.0028 471 | u233-met-fast-001, , 1.0, 0.0010 472 | u233-met-fast-002, case-1, 1.0, 0.0010 473 | u233-met-fast-002, case-2, 1.0, 0.0011 474 | u233-met-fast-003, case-1, 1.0, 0.0010 475 | u233-met-fast-003, case-2, 1.0, 0.0010 476 | u233-met-fast-004, case-1, 1.0, 0.0007 477 | u233-met-fast-004, case-2, 1.0, 0.0008 478 | u233-met-fast-005, case-1, 1.0, 0.0030 479 | u233-met-fast-005, case-2, 1.0, 0.0030 480 | u233-met-fast-006, , 1.0, 0.0014 481 | u233-sol-inter-001, case-1, 1.0, 0.0083 482 | u233-sol-therm-001, case-1, 1.0, 0.0031 483 | u233-sol-therm-001, case-2, 1.0005, 0.0033 484 | u233-sol-therm-001, case-3, 1.0006, 0.0033 485 | u233-sol-therm-001, case-4, 0.9998, 0.0033 486 | u233-sol-therm-001, case-5, 0.9999, 0.0033 487 | u233-sol-therm-008, , 1.0006, 0.0029 488 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup( 7 | name='validation', 8 | author='The OpenMC Development Team', 9 | author_email='openmc-dev@googlegroups.com', 10 | description='A collection of validation scripts for OpenMC', 11 | url='https://github.com/openmc-dev/validation', 12 | packages=['validation', 'benchmarking'], 13 | package_data={'benchmarking': ['uncertainties.csv']}, 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'openmc-validate-neutron-physics=validation.neutron_physics:main', 17 | 'openmc-validate-photon-physics=validation.photon_physics:main', 18 | 'openmc-validate-photon-production=validation.photon_production:main', 19 | 'openmc-run-benchmarks=benchmarking.benchmark:main', 20 | 'openmc-plot-benchmarks=benchmarking.plot:main', 21 | 'openmc-report-benchmarks=benchmarking.report:main', 22 | ] 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /validation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/validation/50599804d0cce8d5f2345ef094611d00211b8c4d/validation/__init__.py -------------------------------------------------------------------------------- /validation/neutron_physics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | from pathlib import Path 6 | import re 7 | import shutil 8 | import subprocess 9 | 10 | import h5py 11 | from matplotlib import pyplot as plt 12 | import numpy as np 13 | 14 | import openmc 15 | from openmc.data import K_BOLTZMANN 16 | from .utils import zaid, szax, create_library, read_results 17 | 18 | 19 | def main(): 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('nuclide', type=str, 22 | help='Name of the nuclide, e.g. "U235"') 23 | parser.add_argument('-d', '--density', type=float, default=1., 24 | help='Density of the material in g/cm^3') 25 | parser.add_argument('-e', '--energy', type=float, default=1e6, 26 | help='Energy of the source in eV') 27 | parser.add_argument('-p', '--particles', type=int, default=100000, 28 | help='Number of source particles') 29 | parser.add_argument('-c', '--code', choices=['mcnp', 'serpent'], 30 | default='mcnp', 31 | help='Code to validate OpenMC against.') 32 | parser.add_argument('-s', '--suffix', type=str, default='70c', 33 | help='MCNP cross section suffix') 34 | parser.add_argument('-x', '--xsdir', type=str, help='XSDIR directory ' 35 | 'file. If specified, it will be used to locate the ' 36 | 'ACE table corresponding to the given nuclide and ' 37 | 'suffix, and an HDF5 library that can be used by ' 38 | 'OpenMC will be created from the data.') 39 | parser.add_argument('-t', '--thermal', type=str, help='ZAID of the ' 40 | 'thermal scattering data, e.g. "grph.10t". If ' 41 | 'specified, thermal scattering data will be assigned ' 42 | 'to the material.') 43 | parser.add_argument('-o', '--output-name', type=str, 44 | help='Name used for output.') 45 | args = parser.parse_args() 46 | 47 | model = NeutronPhysicsModel( 48 | args.nuclide, args.density, args.energy, args.particles, args.code, 49 | args.suffix, args.xsdir, args.thermal, args.output_name 50 | ) 51 | model.run() 52 | 53 | 54 | class NeutronPhysicsModel: 55 | """Monoenergetic, isotropic point source in an infinite geometry. 56 | 57 | Parameters 58 | ---------- 59 | nuclide : str 60 | Name of the nuclide 61 | density : float 62 | Density of the material in g/cm^3. 63 | energy : float 64 | Energy of the source (eV) 65 | particles : int 66 | Number of source particles. 67 | code : {'mcnp', 'serpent'} 68 | Code to validate against 69 | suffix : str 70 | Cross section suffix 71 | xsdir : str 72 | XSDIR directory file. If specified, it will be used to locate the ACE 73 | table corresponding to the given nuclide and suffix, and an HDF5 74 | library that can be used by OpenMC will be created from the data. 75 | thermal : str 76 | ZAID of the thermal scattering data. If specified, thermal scattering 77 | data will be assigned to the material. 78 | name : str 79 | Name used for output. 80 | 81 | Attributes 82 | ---------- 83 | nuclide : str 84 | Name of the nuclide 85 | density : float 86 | Density of the material in g/cm^3. 87 | energy : float 88 | Energy of the source (eV) 89 | particles : int 90 | Number of source particles. 91 | code : {'mcnp', 'serpent'} 92 | Code to validate against 93 | suffix : str 94 | Cross section suffix for MCNP 95 | xsdir : str 96 | XSDIR directory file. If specified, it will be used to locate the ACE 97 | table corresponding to the given nuclide and suffix, and an HDF5 98 | library that can be used by OpenMC will be created from the data. 99 | thermal : str 100 | ZAID of the thermal scattering data. If specified, thermal scattering 101 | data will be assigned to the material. 102 | name : str 103 | Name used for output. 104 | temperature : float 105 | Temperature (Kelvin) of the cross section data 106 | bins : int 107 | Number of bins in the energy grid 108 | batches : int 109 | Number of batches to simulate 110 | min_energy : float 111 | Lower limit of energy grid (eV) 112 | openmc_dir : pathlib.Path 113 | Working directory for OpenMC 114 | other_dir : pathlib.Path 115 | Working directory for MCNP or Serpent 116 | table_names : list of str 117 | Names of the ACE tables used in the model 118 | 119 | """ 120 | 121 | def __init__(self, nuclide, density, energy, particles, code, suffix, 122 | xsdir=None, thermal=None, name=None): 123 | self._temperature = None 124 | self._bins = 500 125 | self._batches = 100 126 | self._min_energy = 1.e-5 127 | self._openmc_dir = None 128 | self._other_dir = None 129 | 130 | self.nuclide = nuclide 131 | self.density = density 132 | self.energy = energy 133 | self.particles = particles 134 | self.code = code 135 | self.suffix = suffix 136 | self.xsdir = xsdir 137 | self.thermal = thermal 138 | self.name = name 139 | 140 | @property 141 | def energy(self): 142 | return self._energy 143 | 144 | @property 145 | def particles(self): 146 | return self._particles 147 | 148 | @property 149 | def code(self): 150 | return self._code 151 | 152 | @property 153 | def suffix(self): 154 | return self._suffix 155 | 156 | @property 157 | def xsdir(self): 158 | return self._xsdir 159 | 160 | @property 161 | def openmc_dir(self): 162 | if self._openmc_dir is None: 163 | self._openmc_dir = Path('openmc') 164 | os.makedirs(self._openmc_dir, exist_ok=True) 165 | return self._openmc_dir 166 | 167 | @property 168 | def other_dir(self): 169 | if self._other_dir is None: 170 | self._other_dir = Path(self.code) 171 | os.makedirs(self._other_dir, exist_ok=True) 172 | return self._other_dir 173 | 174 | @property 175 | def table_names(self): 176 | table_names = [zaid(self.nuclide, self.suffix)] 177 | if self.thermal is not None: 178 | table_names.append(self.thermal) 179 | return table_names 180 | 181 | @energy.setter 182 | def energy(self, energy): 183 | if energy <= self._min_energy: 184 | msg = (f'Energy {energy} eV must be above the minimum energy ' 185 | f'{self._min_energy} eV.') 186 | raise ValueError(msg) 187 | self._energy = energy 188 | 189 | @particles.setter 190 | def particles(self, particles): 191 | if particles % self._batches != 0: 192 | msg = (f'Number of particles {particles} must be divisible by ' 193 | f'the number of batches {self._batches}.') 194 | raise ValueError(msg) 195 | self._particles = particles 196 | 197 | @code.setter 198 | def code(self, code): 199 | if code not in ('mcnp', 'serpent'): 200 | msg = (f'Unsupported code {code}: code must be either "mcnp" or ' 201 | '"serpent".') 202 | raise ValueError(msg) 203 | executable = 'mcnp6' if code == 'mcnp' else 'sss2' 204 | if not shutil.which(executable, os.X_OK): 205 | msg = f'Unable to locate executable {executable} in path.' 206 | raise ValueError(msg) 207 | self._code = code 208 | 209 | @suffix.setter 210 | def suffix(self, suffix): 211 | match = '(7[0-4]c)|(8[0-6]c)|(71[0-6]nc)|[0][3,6,9]c|[1][2,5,8]c' 212 | if not re.match(match, suffix): 213 | msg = f'Unsupported cross section suffix {suffix}.' 214 | raise ValueError(msg) 215 | self._suffix = suffix 216 | 217 | @xsdir.setter 218 | def xsdir(self, xsdir): 219 | if xsdir is not None: 220 | xsdir = Path(xsdir) 221 | if not xsdir.is_file(): 222 | msg = f'Could not locate the XSDIR file {xsdir}.' 223 | raise ValueError(msg) 224 | self._xsdir = xsdir 225 | 226 | def _make_openmc_input(self): 227 | """Generate the OpenMC input XML 228 | 229 | """ 230 | # Define material 231 | mat = openmc.Material() 232 | mat.add_nuclide(self.nuclide, 1.0) 233 | if self.thermal is not None: 234 | name, suffix = self.thermal.split('.') 235 | thermal_name = openmc.data.thermal.get_thermal_name(name) 236 | mat.add_s_alpha_beta(thermal_name) 237 | mat.set_density('g/cm3', self.density) 238 | materials = openmc.Materials([mat]) 239 | if self.xsdir is not None: 240 | xs_path = (self.openmc_dir / 'cross_sections.xml').resolve() 241 | materials.cross_sections = str(xs_path) 242 | materials.export_to_xml(self.openmc_dir / 'materials.xml') 243 | 244 | # Set up geometry 245 | x1 = openmc.XPlane(x0=-1.e9, boundary_type='reflective') 246 | x2 = openmc.XPlane(x0=+1.e9, boundary_type='reflective') 247 | y1 = openmc.YPlane(y0=-1.e9, boundary_type='reflective') 248 | y2 = openmc.YPlane(y0=+1.e9, boundary_type='reflective') 249 | z1 = openmc.ZPlane(z0=-1.e9, boundary_type='reflective') 250 | z2 = openmc.ZPlane(z0=+1.e9, boundary_type='reflective') 251 | cell = openmc.Cell(fill=materials) 252 | cell.region = +x1 & -x2 & +y1 & -y2 & +z1 & -z2 253 | geometry = openmc.Geometry([cell]) 254 | geometry.export_to_xml(self.openmc_dir / 'geometry.xml') 255 | 256 | # Define source 257 | source = openmc.Source() 258 | source.space = openmc.stats.Point((0,0,0)) 259 | source.angle = openmc.stats.Isotropic() 260 | source.energy = openmc.stats.Discrete([self.energy], [1.]) 261 | 262 | # Settings 263 | settings = openmc.Settings() 264 | if self._temperature is not None: 265 | settings.temperature = {'default': self._temperature} 266 | settings.source = source 267 | settings.particles = self.particles // self._batches 268 | settings.run_mode = 'fixed source' 269 | settings.batches = self._batches 270 | settings.create_fission_neutrons = False 271 | settings.export_to_xml(self.openmc_dir / 'settings.xml') 272 | 273 | # Define tallies 274 | energy_bins = np.logspace(np.log10(self._min_energy), 275 | np.log10(1.0001*self.energy), self._bins+1) 276 | energy_filter = openmc.EnergyFilter(energy_bins) 277 | tally = openmc.Tally(name='tally') 278 | tally.filters = [energy_filter] 279 | tally.scores = ['flux'] 280 | tallies = openmc.Tallies([tally]) 281 | tallies.export_to_xml(self.openmc_dir / 'tallies.xml') 282 | 283 | def _make_mcnp_input(self): 284 | """Generate the MCNP input file 285 | 286 | """ 287 | # Create the problem description 288 | lines = ['Point source in infinite geometry'] 289 | 290 | # Create the cell cards: material 1 inside sphere, void outside 291 | lines.append('c --- Cell cards ---') 292 | if self._temperature is not None: 293 | kT = self._temperature * K_BOLTZMANN * 1e-6 294 | lines.append(f'1 1 -{self.density} -1 imp:n=1 tmp={kT}') 295 | else: 296 | lines.append(f'1 1 -{self.density} -1 imp:n=1') 297 | lines.append('2 0 1 imp:n=0') 298 | lines.append('') 299 | 300 | # Create the surface cards: box centered on origin with 2e9 cm sides` 301 | # and reflective boundary conditions 302 | lines.append('c --- Surface cards ---') 303 | lines.append('*1 rpp -1.e9 1e9 -1.e9 1.e9 -1.e9 1.e9') 304 | lines.append('') 305 | 306 | # Create the data cards 307 | lines.append('c --- Data cards ---') 308 | 309 | # Materials 310 | if re.match('(71[0-6]nc)', self.suffix): 311 | name = szax(self.nuclide, self.suffix) 312 | else: 313 | name = zaid(self.nuclide, self.suffix) 314 | lines.append(f'm1 {name} 1.0') 315 | if self.thermal is not None: 316 | lines.append(f'mt1 {self.thermal}') 317 | lines.append('nonu 2') 318 | 319 | # Physics: neutron transport 320 | lines.append('mode n') 321 | 322 | # Source definition: isotropic point source at center of sphere 323 | energy = self.energy * 1e-6 324 | lines.append(f'sdef cel=1 erg={energy}') 325 | 326 | # Tallies: neutron flux over cell 327 | lines.append('f4:n 1') 328 | min_energy = self._min_energy * 1e-6 329 | lines.append(f'e4 {min_energy} {self._bins-1}ilog {1.0001*energy}') 330 | 331 | # Problem termination: number of particles to transport 332 | lines.append(f'nps {self.particles}') 333 | 334 | # Write the problem 335 | with open(self.other_dir / 'inp', 'w') as f: 336 | f.write('\n'.join(lines)) 337 | 338 | def _make_serpent_input(self): 339 | """Generate the Serpent input file 340 | 341 | """ 342 | # Create the problem description 343 | lines = ['% Point source in infinite geometry'] 344 | lines.append('') 345 | 346 | # Set the cross section library directory 347 | if self.xsdir is not None: 348 | xsdata = (self.other_dir / 'xsdata').resolve() 349 | lines.append(f'set acelib "{xsdata}"') 350 | lines.append('') 351 | 352 | # Create the cell cards: material 1 inside sphere, void outside 353 | lines.append('% --- Cell cards ---') 354 | lines.append('cell 1 0 m1 -1') 355 | lines.append('cell 2 0 outside 1') 356 | lines.append('') 357 | 358 | # Create the surface cards: box centered on origin with 2e9 cm sides` 359 | # and reflective boundary conditions 360 | lines.append('% --- Surface cards ---') 361 | lines.append('surf 1 cube 0.0 0.0 0.0 1.e9') 362 | 363 | # Reflective boundary conditions 364 | lines.append('set bc 2') 365 | lines.append('') 366 | 367 | # Create the material cards 368 | lines.append('% --- Material cards ---') 369 | name = zaid(self.nuclide, self.suffix) 370 | if self.thermal is not None: 371 | Z, A, m = openmc.data.zam(self.nuclide) 372 | lines.append(f'mat m1 -{self.density} moder t1 {1000*Z + A}') 373 | else: 374 | lines.append(f'mat m1 -{self.density}') 375 | lines.append(f'{name} 1.0') 376 | 377 | # Add thermal scattering library associated with the nuclide 378 | if self.thermal is not None: 379 | lines.append(f'therm t1 {self.thermal}') 380 | lines.append('') 381 | 382 | # External source mode with isotropic point source at center of sphere 383 | lines.append('% --- Set external source mode ---') 384 | lines.append(f'set nps {self.particles} {self._batches}') 385 | energy = self.energy * 1e-6 386 | lines.append(f'src 1 n se {energy} sp 0.0 0.0 0.0') 387 | lines.append('') 388 | 389 | # Detector definition: flux energy spectrum 390 | lines.append('% --- Detector definition ---') 391 | lines.append('det 1 de 1 dc 1') 392 | 393 | # Energy grid definition: equal lethargy spacing 394 | min_energy = self._min_energy * 1e-6 395 | lines.append(f'ene 1 3 {self._bins} {min_energy} {1.0001*energy}') 396 | lines.append('') 397 | 398 | # Treat fission as capture 399 | lines.append('set nphys 0') 400 | 401 | # Turn on unresolved resonance probability treatment 402 | lines.append('set ures 1') 403 | 404 | # Write the problem 405 | with open(self.other_dir / 'input', 'w') as f: 406 | f.write('\n'.join(lines)) 407 | 408 | def _plot(self): 409 | """Extract and plot the results 410 | 411 | """ 412 | # Read results 413 | path = self.openmc_dir / f'statepoint.{self._batches}.h5' 414 | x1, y1, _ = read_results('openmc', path) 415 | if self.code == 'serpent': 416 | path = self.other_dir / 'input_det0.m' 417 | else: 418 | path = self.other_dir / 'outp' 419 | x2, y2, sd = read_results(self.code, path) 420 | 421 | # Convert energies to eV 422 | x1 *= 1e6 423 | x2 *= 1e6 424 | 425 | # Normalize the spectra 426 | y1 /= np.diff(np.insert(x1, 0, self._min_energy))*sum(y1) 427 | y2 /= np.diff(np.insert(x2, 0, self._min_energy))*sum(y2) 428 | 429 | # Compute the relative error 430 | err = np.zeros_like(y2) 431 | idx = np.where(y2 > 0) 432 | err[idx] = (y1[idx] - y2[idx])/y2[idx] 433 | 434 | # Set up the figure 435 | fig = plt.figure(1, facecolor='w', figsize=(8,8)) 436 | ax1 = fig.add_subplot(111) 437 | 438 | # Create a second y-axis that shares the same x-axis, keeping the first 439 | # axis in front 440 | ax2 = ax1.twinx() 441 | ax1.set_zorder(ax2.get_zorder() + 1) 442 | ax1.patch.set_visible(False) 443 | 444 | # Plot the spectra 445 | label = 'Serpent' if self.code == 'serpent' else 'MCNP' 446 | ax1.loglog(x2, y2, 'r', linewidth=1, label=label) 447 | ax1.loglog(x1, y1, 'b', linewidth=1, label='OpenMC', linestyle='--') 448 | 449 | # Plot the relative error and uncertainties 450 | ax2.semilogx(x2, err, color=(0.2, 0.8, 0.0), linewidth=1) 451 | ax2.semilogx(x2, 2*sd, color='k', linestyle='--', linewidth=1) 452 | ax2.semilogx(x2, -2*sd, color='k', linestyle='--', linewidth=1) 453 | 454 | # Set grid and tick marks 455 | ax1.tick_params(axis='both', which='both', direction='in', length=10) 456 | ax1.grid(b=False, axis='both', which='both') 457 | ax2.tick_params(axis='y', which='both', right=False) 458 | ax2.grid(b=True, which='both', axis='both', alpha=0.5, linestyle='--') 459 | 460 | # Set axes labels and limits 461 | ax1.set_xlim([self._min_energy, self.energy]) 462 | ax1.set_xlabel('Energy (eV)', size=12) 463 | ax1.set_ylabel('Spectrum', size=12) 464 | ax1.legend() 465 | ax2.set_ylabel("Relative error", size=12) 466 | title = f'{self.nuclide}' 467 | if self.thermal is not None: 468 | name, suffix = self.thermal.split('.') 469 | thermal_name = openmc.data.thermal.get_thermal_name(name) 470 | title += f' + {thermal_name}' 471 | title += f', {self.energy:.1e} eV Source' 472 | plt.title(title) 473 | 474 | # Save plot 475 | os.makedirs('plots', exist_ok=True) 476 | if self.name is not None: 477 | name = self.name 478 | else: 479 | name = f'{self.nuclide}' 480 | if self.thermal is not None: 481 | name += f'-{thermal_name}' 482 | name += f'-{self.energy:.1e}eV' 483 | if self._temperature is not None: 484 | name += f'-{self._temperature:.1f}K' 485 | plt.savefig(Path('plots') / f'{name}.png', bbox_inches='tight') 486 | plt.close() 487 | 488 | def run(self): 489 | """Generate inputs, run problem, and plot results. 490 | 491 | """ 492 | # Create HDF5 cross section library and Serpent XSDATA file 493 | if self.xsdir is not None: 494 | path = self.other_dir if self.code == 'serpent' else None 495 | create_library(self.xsdir, self.table_names, self.openmc_dir, path) 496 | 497 | # Get the temperature of the cross section data 498 | f = h5py.File(self.openmc_dir / (self.nuclide + '.h5'), 'r') 499 | temperature = list(f[self.nuclide]['kTs'].values())[0][()] 500 | self._temperature = temperature / K_BOLTZMANN 501 | 502 | # Generate input files 503 | self._make_openmc_input() 504 | 505 | if self.code == 'serpent': 506 | self._make_serpent_input() 507 | args = ['sss2', 'input'] 508 | else: 509 | self._make_mcnp_input() 510 | args = ['mcnp6'] 511 | if self.xsdir is not None: 512 | args.append(f'XSDIR={self.xsdir}') 513 | 514 | # Remove old MCNP output files 515 | for f in ('outp', 'runtpe'): 516 | try: 517 | os.remove(self.other_dir / f) 518 | except OSError: 519 | pass 520 | 521 | # Run code and capture and print output 522 | p = subprocess.Popen(args, cwd=self.code, stdout=subprocess.PIPE, 523 | stderr=subprocess.STDOUT, universal_newlines=True) 524 | while True: 525 | line = p.stdout.readline() 526 | if not line and p.poll() is not None: 527 | break 528 | print(line, end='') 529 | 530 | openmc.run(cwd='openmc') 531 | 532 | self._plot() 533 | -------------------------------------------------------------------------------- /validation/photon_physics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | from pathlib import Path 6 | import re 7 | import shutil 8 | import subprocess 9 | 10 | from matplotlib import pyplot as plt 11 | import numpy as np 12 | 13 | import openmc 14 | from openmc.data import ATOMIC_NUMBER, NEUTRON_MASS, K_BOLTZMANN 15 | from .utils import create_library, read_results 16 | 17 | 18 | def main(): 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument('element', type=str, 21 | help='Name of the element, e.g. "U"') 22 | parser.add_argument('-d', '--density', type=float, default=1., 23 | help='Density of the material in g/cm^3') 24 | parser.add_argument('-e', '--energy', type=float, default=1e6, 25 | help='Energy of the source in eV') 26 | parser.add_argument('-p', '--particles', type=int, default=1000000, 27 | help='Number of source particles') 28 | parser.add_argument('-t', '--electron-treatment', choices=('ttb', 'led'), 29 | default='ttb', help='Whether to use local energy' 30 | 'deposition or thick-target bremsstrahlung treatment ' 31 | 'for electrons and positrons.') 32 | parser.add_argument('-c', '--code', choices=['mcnp', 'serpent'], 33 | default='mcnp', 34 | help='Code to validate OpenMC against.') 35 | parser.add_argument('-s', '--suffix', default='12p', 36 | help='Photon cross section suffix') 37 | parser.add_argument('-x', '--xsdir', type=str, help='XSDIR directory ' 38 | 'file. If specified, it will be used to locate the ' 39 | 'ACE table corresponding to the given nuclide and ' 40 | 'suffix, and an HDF5 library that can be used by ' 41 | 'OpenMC will be created from the data.') 42 | parser.add_argument('-g', '--serpent_pdata', type=str, help='Directory ' 43 | 'containing the additional data files needed for ' 44 | 'photon physics in Serpent.') 45 | parser.add_argument('-o', '--output-name', type=str, 46 | help='Name used for output.') 47 | args = parser.parse_args() 48 | 49 | model = PhotonPhysicsModel( 50 | args.element, args.density, [(args.element, 1.)], args.energy, 51 | args.particles, args.electron_treatment, args.code, args.suffix, 52 | args.xsdir, args.serpent_pdata, args.output_name 53 | ) 54 | model.run() 55 | 56 | 57 | class PhotonPhysicsModel: 58 | """Monoenergetic, isotropic point source in an infinite geometry. 59 | 60 | Parameters 61 | ---------- 62 | material : str 63 | Name of the material. 64 | density : float 65 | Density of the material in g/cm^3. 66 | elements : list of tuple 67 | List in which each item is a 2-tuple consisting of an element string and 68 | the atom fraction. 69 | energy : float 70 | Energy of the source (eV) 71 | particles : int 72 | Number of source particles. 73 | electron_treatment : {'led' or 'ttb'} 74 | Whether to deposit electron energy locally ('led') or create secondary 75 | bremsstrahlung photons ('ttb'). 76 | code : {'mcnp', 'serpent'} 77 | Code to validate against 78 | suffix : str 79 | Photon cross section suffix 80 | xsdir : str 81 | XSDIR directory file. If specified, it will be used to locate the ACE 82 | table corresponding to the given element and suffix, and an HDF5 83 | library that can be used by OpenMC will be created from the data. 84 | serpent_pdata : str 85 | Directory containing the additional data files needed for photon 86 | physics in Serpent. 87 | name : str 88 | Name used for output. 89 | 90 | Attributes 91 | ---------- 92 | material : str 93 | Name of the material. 94 | density : float 95 | Density of the material in g/cm^3. 96 | elements : list of tuple 97 | List in which each item is a 2-tuple consisting of an element string and 98 | the atom fraction. 99 | energy : float 100 | Energy of the source (eV) 101 | particles : int 102 | Number of source particles. 103 | electron_treatment : {'led' or 'ttb'} 104 | Whether to deposit electron energy locally ('led') or create secondary 105 | bremsstrahlung photons ('ttb'). 106 | code : {'mcnp', 'serpent'} 107 | Code to validate against 108 | suffix : str 109 | Photon cross section suffix 110 | xsdir : str 111 | XSDIR directory file. If specified, it will be used to locate the ACE 112 | table corresponding to the given element and suffix, and an HDF5 113 | library that can be used by OpenMC will be created from the data. 114 | serpent_pdata : str 115 | Directory containing the additional data files needed for photon 116 | physics in Serpent. 117 | name : str 118 | Name used for output. 119 | bins : int 120 | Number of bins in the energy grid 121 | batches : int 122 | Number of batches to simulate 123 | cutoff_energy: float 124 | Photon cutoff energy (eV) 125 | openmc_dir : pathlib.Path 126 | Working directory for OpenMC 127 | other_dir : pathlib.Path 128 | Working directory for MCNP or Serpent 129 | table_names : list of str 130 | Names of the ACE tables used in the model 131 | 132 | """ 133 | 134 | def __init__(self, material, density, elements, energy, particles, 135 | electron_treatment, code, suffix, xsdir=None, 136 | serpent_pdata=None, name=None): 137 | self._bins = 500 138 | self._batches = 100 139 | self._cutoff_energy = 1.e3 140 | self._openmc_dir = None 141 | self._other_dir = None 142 | 143 | self.material = material 144 | self.density = density 145 | self.elements = elements 146 | self.energy = energy 147 | self.particles = particles 148 | self.electron_treatment = electron_treatment 149 | self.code = code 150 | self.suffix = suffix 151 | self.xsdir = xsdir 152 | self.serpent_pdata = serpent_pdata 153 | self.name = name 154 | 155 | @property 156 | def energy(self): 157 | return self._energy 158 | 159 | @property 160 | def particles(self): 161 | return self._particles 162 | 163 | @property 164 | def code(self): 165 | return self._code 166 | 167 | @property 168 | def suffix(self): 169 | return self._suffix 170 | 171 | @property 172 | def xsdir(self): 173 | return self._xsdir 174 | 175 | @property 176 | def serpent_pdata(self): 177 | return self._serpent_pdata 178 | 179 | @property 180 | def openmc_dir(self): 181 | if self._openmc_dir is None: 182 | self._openmc_dir = Path('openmc') 183 | os.makedirs(self._openmc_dir, exist_ok=True) 184 | return self._openmc_dir 185 | 186 | @property 187 | def other_dir(self): 188 | if self._other_dir is None: 189 | self._other_dir = Path(self.code) 190 | os.makedirs(self._other_dir, exist_ok=True) 191 | return self._other_dir 192 | 193 | @property 194 | def table_names(self): 195 | table_names = [] 196 | for element, _ in self.elements: 197 | Z = ATOMIC_NUMBER[element] 198 | table_names.append(f'{1000*Z}.{self.suffix}') 199 | return table_names 200 | 201 | @energy.setter 202 | def energy(self, energy): 203 | if energy <= self._cutoff_energy: 204 | msg = (f'Energy {energy} eV must be above the cutoff energy ' 205 | f'{self._cutoff_energy} eV.') 206 | raise ValueError(msg) 207 | self._energy = energy 208 | 209 | @particles.setter 210 | def particles(self, particles): 211 | if particles % self._batches != 0: 212 | msg = (f'Number of particles {particles} must be divisible by ' 213 | f'the number of batches {self._batches}.') 214 | raise ValueError(msg) 215 | self._particles = particles 216 | 217 | @code.setter 218 | def code(self, code): 219 | if code not in ('mcnp', 'serpent'): 220 | msg = (f'Unsupported code {code}: code must be either "mcnp" or ' 221 | '"serpent".') 222 | raise ValueError(msg) 223 | executable = 'mcnp6' if code == 'mcnp' else 'sss2' 224 | if not shutil.which(executable, os.X_OK): 225 | msg = f'Unable to locate executable {executable} in path.' 226 | raise ValueError(msg) 227 | self._code = code 228 | 229 | @suffix.setter 230 | def suffix(self, suffix): 231 | if not re.match('12p', suffix): 232 | msg = f'Unsupported cross section suffix {suffix}.' 233 | raise ValueError(msg) 234 | self._suffix = suffix 235 | 236 | @xsdir.setter 237 | def xsdir(self, xsdir): 238 | if xsdir is not None: 239 | xsdir = Path(xsdir) 240 | if not xsdir.is_file(): 241 | msg = f'Could not locate the XSDIR file {xsdir}.' 242 | raise ValueError(msg) 243 | self._xsdir = xsdir 244 | 245 | @serpent_pdata.setter 246 | def serpent_pdata(self, serpent_pdata): 247 | if self.code == 'serpent': 248 | if serpent_pdata is None: 249 | msg = ('Serpent photon data path is required to run a ' 250 | 'calculation with Serpent.') 251 | raise ValueError(msg) 252 | serpent_pdata = Path(serpent_pdata).resolve() 253 | if not serpent_pdata.is_dir(): 254 | msg = (f'Could not locate the Serpent photon data directory ' 255 | f'{serpent_pdata}.') 256 | raise ValueError(msg) 257 | self._serpent_pdata = serpent_pdata 258 | 259 | def _make_openmc_input(self): 260 | """Generate the OpenMC input XML 261 | 262 | """ 263 | # Define material 264 | mat = openmc.Material() 265 | for element, fraction in self.elements: 266 | mat.add_element(element, fraction) 267 | mat.set_density('g/cm3', self.density) 268 | materials = openmc.Materials([mat]) 269 | if self.xsdir is not None: 270 | xs_path = (self.openmc_dir / 'cross_sections.xml').resolve() 271 | materials.cross_sections = str(xs_path) 272 | materials.export_to_xml(self.openmc_dir / 'materials.xml') 273 | 274 | # Set up geometry 275 | x1 = openmc.XPlane(x0=-1.e9, boundary_type='reflective') 276 | x2 = openmc.XPlane(x0=+1.e9, boundary_type='reflective') 277 | y1 = openmc.YPlane(y0=-1.e9, boundary_type='reflective') 278 | y2 = openmc.YPlane(y0=+1.e9, boundary_type='reflective') 279 | z1 = openmc.ZPlane(z0=-1.e9, boundary_type='reflective') 280 | z2 = openmc.ZPlane(z0=+1.e9, boundary_type='reflective') 281 | cell = openmc.Cell(fill=materials) 282 | cell.region = +x1 & -x2 & +y1 & -y2 & +z1 & -z2 283 | geometry = openmc.Geometry([cell]) 284 | geometry.export_to_xml(self.openmc_dir / 'geometry.xml') 285 | 286 | # Define source 287 | source = openmc.Source() 288 | source.space = openmc.stats.Point((0,0,0)) 289 | source.angle = openmc.stats.Isotropic() 290 | source.energy = openmc.stats.Discrete([self.energy], [1.]) 291 | source.particle = 'photon' 292 | 293 | # Settings 294 | settings = openmc.Settings() 295 | settings.source = source 296 | settings.particles = self.particles // self._batches 297 | settings.run_mode = 'fixed source' 298 | settings.batches = self._batches 299 | settings.photon_transport = True 300 | settings.electron_treatment = self.electron_treatment 301 | settings.cutoff = {'energy_photon' : self._cutoff_energy} 302 | settings.export_to_xml(self.openmc_dir / 'settings.xml') 303 | 304 | # Define tallies 305 | energy_bins = np.logspace(np.log10(self._cutoff_energy), 306 | np.log10(1.0001*self.energy), self._bins+1) 307 | energy_filter = openmc.EnergyFilter(energy_bins) 308 | particle_filter = openmc.ParticleFilter('photon') 309 | tally = openmc.Tally(name='tally') 310 | tally.filters = [energy_filter, particle_filter] 311 | tally.scores = ['flux'] 312 | tallies = openmc.Tallies([tally]) 313 | tallies.export_to_xml(self.openmc_dir / 'tallies.xml') 314 | 315 | def _make_mcnp_input(self): 316 | """Generate the MCNP input file 317 | 318 | """ 319 | # Create the problem description 320 | lines = ['Point source in infinite geometry'] 321 | 322 | # Create the cell cards: material 1 inside sphere, void outside 323 | lines.append('c --- Cell cards ---') 324 | lines.append(f'1 1 -{self.density} -1 imp:p=1') 325 | lines.append('2 0 1 imp:p=0') 326 | lines.append('') 327 | 328 | # Create the surface cards: box centered on origin with 2e9 cm sides` 329 | # and reflective boundary conditions 330 | lines.append('c --- Surface cards ---') 331 | lines.append('*1 rpp -1.e9 1e9 -1.e9 1.e9 -1.e9 1.e9') 332 | lines.append('') 333 | 334 | # Create the data cards 335 | lines.append('c --- Data cards ---') 336 | 337 | # Materials 338 | material_card = 'm1' 339 | for element, fraction in self.elements: 340 | Z = openmc.data.ATOMIC_NUMBER[element] 341 | material_card += f' {Z}000.{self.suffix} -{fraction}' 342 | lines.append(material_card) 343 | 344 | # Energy in MeV 345 | energy = self.energy * 1e-6 346 | cutoff_energy = self._cutoff_energy * 1e-6 347 | 348 | # Physics: photon transport, 1 keV photon cutoff energy 349 | if self.electron_treatment == 'led': 350 | flag = 1 351 | else: 352 | flag = 'j' 353 | lines.append('mode p') 354 | lines.append(f'phys:p j {flag} j j j') 355 | lines.append(f'cut:p j {cutoff_energy}') 356 | 357 | # Source definition: isotropic point source at center of sphere 358 | lines.append(f'sdef cel=1 erg={energy}') 359 | 360 | # Tallies: photon flux over cell 361 | lines.append('f4:p 1') 362 | lines.append(f'e4 {cutoff_energy} {self._bins-1}ilog {1.0001*energy}') 363 | 364 | # Problem termination: number of particles to transport 365 | lines.append(f'nps {self.particles}') 366 | 367 | # Write the problem 368 | with open(self.other_dir / 'inp', 'w') as f: 369 | f.write('\n'.join(lines)) 370 | 371 | def _make_serpent_input(self): 372 | """Generate the Serpent input file 373 | 374 | """ 375 | # Create the problem description 376 | lines = ['% Point source in infinite geometry'] 377 | lines.append('') 378 | 379 | # Set the cross section library directory 380 | if self.xsdir is not None: 381 | xsdata = (self.other_dir / 'xsdata').resolve() 382 | lines.append(f'set acelib "{xsdata}"') 383 | 384 | # Set the photon data directory 385 | lines.append(f'set pdatadir "{self.serpent_pdata}"') 386 | lines.append('') 387 | 388 | # Create the cell cards: material 1 inside sphere, void outside 389 | lines.append('% --- Cell cards ---') 390 | lines.append('cell 1 0 m1 -1') 391 | lines.append('cell 2 0 outside 1') 392 | lines.append('') 393 | 394 | # Create the surface cards: box centered on origin with 2e9 cm sides` 395 | # and reflective boundary conditions 396 | lines.append('% --- Surface cards ---') 397 | lines.append('surf 1 cube 0.0 0.0 0.0 1.e9') 398 | 399 | # Reflective boundary conditions 400 | lines.append('set bc 2') 401 | lines.append('') 402 | 403 | # Create the material cards 404 | lines.append('% --- Material cards ---') 405 | lines.append(f'mat m1 -{self.density}') 406 | 407 | # Add element data 408 | for element, fraction in self.elements: 409 | Z = ATOMIC_NUMBER[element] 410 | name = f'{1000*Z}.{self.suffix}' 411 | lines.append(f'{name} {fraction}') 412 | 413 | # Turn on unresolved resonance probability treatment 414 | lines.append('set ures 1') 415 | 416 | # Set electron treatment 417 | if self.electron_treatment == 'led': 418 | lines.append('set ttb 0') 419 | else: 420 | lines.append('set ttb 1') 421 | 422 | # Energy in MeV 423 | energy = self.energy * 1e-6 424 | cutoff_energy = self._cutoff_energy * 1e-6 425 | 426 | # Set cutoff energy 427 | lines.append(f'set ecut 0 {cutoff_energy}') 428 | lines.append('') 429 | 430 | # External source mode with isotropic point source at center of sphere 431 | lines.append('% --- Set external source mode ---') 432 | lines.append(f'set nps {self.particles} {self._batches}') 433 | lines.append(f'src 1 g se {energy} sp 0.0 0.0 0.0') 434 | lines.append('') 435 | 436 | # Detector definition: flux energy spectrum 437 | lines.append('% --- Detector definition ---') 438 | lines.append('det 1 de 1 dc 1') 439 | 440 | # Energy grid definition: equal lethargy spacing 441 | lines.append(f'ene 1 3 {self._bins} {cutoff_energy} {1.0001*energy}') 442 | lines.append('') 443 | 444 | # Write the problem 445 | with open(self.other_dir / 'input', 'w') as f: 446 | f.write('\n'.join(lines)) 447 | 448 | def _plot(self): 449 | """Extract and plot the results 450 | 451 | """ 452 | # Read results 453 | path = self.openmc_dir / f'statepoint.{self._batches}.h5' 454 | x1, y1, _ = read_results('openmc', path) 455 | if self.code == 'serpent': 456 | path = self.other_dir / 'input_det0.m' 457 | else: 458 | path = self.other_dir / 'outp' 459 | x2, y2, sd = read_results(self.code, path) 460 | 461 | # Normalize the spectra 462 | cutoff_energy = self._cutoff_energy * 1e-6 463 | y1 /= np.diff(np.insert(x1, 0, cutoff_energy))*sum(y1) 464 | y2 /= np.diff(np.insert(x2, 0, cutoff_energy))*sum(y2) 465 | 466 | # Compute the relative error 467 | err = np.zeros_like(y2) 468 | idx = np.where(y2 > 0) 469 | err[idx] = (y1[idx] - y2[idx])/y2[idx] 470 | 471 | # Set up the figure 472 | fig = plt.figure(1, facecolor='w', figsize=(8,8)) 473 | ax1 = fig.add_subplot(111) 474 | 475 | # Create a second y-axis that shares the same x-axis, keeping the first 476 | # axis in front 477 | ax2 = ax1.twinx() 478 | ax1.set_zorder(ax2.get_zorder() + 1) 479 | ax1.patch.set_visible(False) 480 | 481 | # Plot the spectra 482 | label = 'Serpent' if self.code == 'serpent' else 'MCNP' 483 | ax1.loglog(x2, y2, 'r', linewidth=1, label=label) 484 | ax1.loglog(x1, y1, 'b', linewidth=1, label='OpenMC', linestyle='--') 485 | 486 | # Plot the relative error and uncertainties 487 | ax2.semilogx(x2, err, color=(0.2, 0.8, 0.0), linewidth=1) 488 | ax2.semilogx(x2, 2*sd, color='k', linestyle='--', linewidth=1) 489 | ax2.semilogx(x2, -2*sd, color='k', linestyle='--', linewidth=1) 490 | 491 | # Set grid and tick marks 492 | ax1.tick_params(axis='both', which='both', direction='in', length=10) 493 | ax1.grid(b=False, axis='both', which='both') 494 | ax2.tick_params(axis='y', which='both', right=False) 495 | ax2.grid(b=True, which='both', axis='both', alpha=0.5, linestyle='--') 496 | 497 | # Energy in MeV 498 | energy = self.energy * 1e-6 499 | 500 | # Set axes labels and limits 501 | ax1.set_xlim([cutoff_energy, energy]) 502 | ax1.set_xlabel('Energy (MeV)', size=12) 503 | ax1.set_ylabel('Spectrum', size=12) 504 | ax1.legend() 505 | ax2.set_ylabel("Relative error", size=12) 506 | title = f'{self.material}, {energy:.1e} MeV Source' 507 | plt.title(title) 508 | 509 | # Save plot 510 | os.makedirs('plots', exist_ok=True) 511 | if self.name is not None: 512 | name = self.name 513 | else: 514 | name = f'{self.material}-{energy:.1e}MeV' 515 | plt.savefig(Path('plots') / f'{name}.png', bbox_inches='tight') 516 | plt.close() 517 | 518 | def run(self): 519 | """Generate inputs, run problem, and plot results. 520 | 521 | """ 522 | # Create the HDF5 library 523 | if self.xsdir is not None: 524 | path = self.other_dir if self.code == 'serpent' else None 525 | create_library(self.xsdir, self.table_names, self.openmc_dir, path) 526 | 527 | # TODO: Currently the neutron libraries are still read in to OpenMC 528 | # even when doing pure photon transport, so we need to locate them and 529 | # register them with the library. 530 | path = os.getenv('OPENMC_CROSS_SECTIONS') 531 | lib = openmc.data.DataLibrary.from_xml(path) 532 | 533 | path = self.openmc_dir / 'cross_sections.xml' 534 | data_lib = openmc.data.DataLibrary.from_xml(path) 535 | 536 | for element, fraction in self.elements: 537 | element = openmc.Element(element) 538 | for nuclide, _, _ in element.expand(fraction, 'ao'): 539 | h5_file = lib.get_by_material(nuclide)['path'] 540 | data_lib.register_file(h5_file) 541 | 542 | data_lib.export_to_xml(path) 543 | 544 | # Generate input files 545 | self._make_openmc_input() 546 | 547 | if self.code == 'serpent': 548 | self._make_serpent_input() 549 | args = ['sss2', 'input'] 550 | else: 551 | self._make_mcnp_input() 552 | args = ['mcnp6'] 553 | if self.xsdir is not None: 554 | args.append(f'XSDIR={self.xsdir}') 555 | 556 | # Remove old MCNP output files 557 | for f in ('outp', 'runtpe'): 558 | try: 559 | os.remove(self.other_dir / f) 560 | except OSError: 561 | pass 562 | 563 | # Run code and capture and print output 564 | p = subprocess.Popen( 565 | args, cwd=self.other_dir, stdout=subprocess.PIPE, 566 | stderr=subprocess.STDOUT, universal_newlines=True 567 | ) 568 | 569 | while True: 570 | line = p.stdout.readline() 571 | if not line and p.poll() is not None: 572 | break 573 | print(line, end='') 574 | 575 | openmc.run(cwd=self.openmc_dir) 576 | 577 | self._plot() 578 | -------------------------------------------------------------------------------- /validation/photon_production.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | from pathlib import Path 6 | import re 7 | import shutil 8 | import subprocess 9 | 10 | import h5py 11 | from matplotlib import pyplot as plt 12 | import numpy as np 13 | 14 | import openmc 15 | from openmc.data import K_BOLTZMANN, NEUTRON_MASS 16 | from .utils import zaid, szax, create_library, read_results 17 | 18 | 19 | def main(): 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('nuclide', type=str, 22 | help='Name of the nuclide, e.g. "U235"') 23 | parser.add_argument('-d', '--density', type=float, default=1., 24 | help='Density of the material in g/cm^3') 25 | parser.add_argument('-e', '--energy', type=float, default=1e6, 26 | help='Energy of the source in eV') 27 | parser.add_argument('-p', '--particles', type=int, default=1000000, 28 | help='Number of source particles') 29 | parser.add_argument('-t', '--electron-treatment', choices=('ttb', 'led'), 30 | default='ttb', help='Whether to use local energy' 31 | 'deposition or thick-target bremsstrahlung treatment ' 32 | 'for electrons and positrons.') 33 | parser.add_argument('-c', '--code', choices=['mcnp', 'serpent'], 34 | default='mcnp', 35 | help='Code to validate OpenMC against.') 36 | parser.add_argument('-s', '--suffix', default='70c', 37 | help='Neutron cross section suffix') 38 | parser.add_argument('-k', '--photon-suffix', default='12p', 39 | help='Photon cross section suffix') 40 | parser.add_argument('-x', '--xsdir', type=str, help='XSDIR directory ' 41 | 'file. If specified, it will be used to locate the ' 42 | 'ACE table corresponding to the given nuclide and ' 43 | 'suffix, and an HDF5 library that can be used by ' 44 | 'OpenMC will be created from the data.') 45 | parser.add_argument('-g', '--serpent_pdata', type=str, help='Directory ' 46 | 'containing the additional data files needed for ' 47 | 'photon physics in Serpent.') 48 | parser.add_argument('-o', '--output-name', type=str, 49 | help='Name used for output.') 50 | args = parser.parse_args() 51 | 52 | model = PhotonProductionModel( 53 | args.nuclide, args.density, [(args.nuclide, 1.)], args.energy, 54 | args.particles, args.electron_treatment, args.code, args.suffix, 55 | args.photon_suffix, args.xsdir, args.serpent_pdata, args.output_name 56 | ) 57 | model.run() 58 | 59 | 60 | class PhotonProductionModel: 61 | """Monoenergetic, monodirectional neutron source directed down a thin, 62 | infinitely long cylinder ('Broomstick' problem). 63 | 64 | Parameters 65 | ---------- 66 | material : str 67 | Name of the material. 68 | density : float 69 | Density of the material in g/cm^3. 70 | nuclides : list of tuple 71 | List in which each item is a 2-tuple consisting of a nuclide string and 72 | the atom fraction. 73 | energy : float 74 | Energy of the source (eV) 75 | particles : int 76 | Number of source particles. 77 | electron_treatment : {'led' or 'ttb'} 78 | Whether to deposit electron energy locally ('led') or create secondary 79 | bremsstrahlung photons ('ttb'). 80 | code : {'mcnp', 'serpent'} 81 | Code to validate against 82 | suffix : str 83 | Neutron cross section suffix 84 | photon_suffix : str 85 | Photon cross section suffix 86 | xsdir : str 87 | XSDIR directory file. If specified, it will be used to locate the ACE 88 | table corresponding to the given nuclide and suffix, and an HDF5 89 | library that can be used by OpenMC will be created from the data. 90 | serpent_pdata : str 91 | Directory containing the additional data files needed for photon 92 | physics in Serpent. 93 | name : str 94 | Name used for output. 95 | 96 | Attributes 97 | ---------- 98 | material : str 99 | Name of the material. 100 | density : float 101 | Density of the material in g/cm^3. 102 | nuclides : list of tuple 103 | List in which each item is a 2-tuple consisting of a nuclide string and 104 | the atom fraction. 105 | energy : float 106 | Energy of the source (eV) 107 | particles : int 108 | Number of source particles. 109 | electron_treatment : {'led' or 'ttb'} 110 | Whether to deposit electron energy locally ('led') or create secondary 111 | bremsstrahlung photons ('ttb'). 112 | code : {'mcnp', 'serpent'} 113 | Code to validate against 114 | suffix : str 115 | Neutron cross section suffix 116 | photon_suffix : str 117 | Photon cross section suffix 118 | xsdir : str 119 | XSDIR directory file. If specified, it will be used to locate the ACE 120 | table corresponding to the given nuclide and suffix, and an HDF5 121 | library that can be used by OpenMC will be created from the data. 122 | serpent_pdata : str 123 | Directory containing the additional data files needed for photon 124 | physics in Serpent. 125 | name : str 126 | Name used for output. 127 | temperature : float 128 | Temperature (Kelvin) of the cross section data 129 | bins : int 130 | Number of bins in the energy grid 131 | batches : int 132 | Number of batches to simulate 133 | max_energy : float 134 | Upper limit of energy grid (eV) 135 | cutoff_energy: float 136 | Photon cutoff energy (eV) 137 | openmc_dir : pathlib.Path 138 | Working directory for OpenMC 139 | other_dir : pathlib.Path 140 | Working directory for MCNP or Serpent 141 | table_names : list of str 142 | Names of the ACE tables used in the model 143 | 144 | """ 145 | 146 | def __init__(self, material, density, nuclides, energy, particles, 147 | electron_treatment, code, suffix, photon_suffix, xsdir=None, 148 | serpent_pdata=None, name=None): 149 | self._temperature = None 150 | self._bins = 500 151 | self._batches = 100 152 | self._cutoff_energy = 1.e3 153 | self._openmc_dir = None 154 | self._other_dir = None 155 | 156 | self.material = material 157 | self.density = density 158 | self.nuclides = nuclides 159 | self.energy = energy 160 | self.particles = particles 161 | self.electron_treatment = electron_treatment 162 | self.code = code 163 | self.suffix = suffix 164 | self.photon_suffix = photon_suffix 165 | self.xsdir = xsdir 166 | self.serpent_pdata = serpent_pdata 167 | self.name = name 168 | 169 | @property 170 | def particles(self): 171 | return self._particles 172 | 173 | @property 174 | def code(self): 175 | return self._code 176 | 177 | @property 178 | def suffix(self): 179 | return self._suffix 180 | 181 | @property 182 | def photon_suffix(self): 183 | return self._photon_suffix 184 | 185 | @property 186 | def xsdir(self): 187 | return self._xsdir 188 | 189 | @property 190 | def serpent_pdata(self): 191 | return self._serpent_pdata 192 | 193 | @property 194 | def max_energy(self): 195 | if self.energy < 1.e6: 196 | return 1.e7 197 | else: 198 | return self.energy * 10 199 | 200 | @property 201 | def openmc_dir(self): 202 | if self._openmc_dir is None: 203 | self._openmc_dir = Path('openmc') 204 | os.makedirs(self._openmc_dir, exist_ok=True) 205 | return self._openmc_dir 206 | 207 | @property 208 | def other_dir(self): 209 | if self._other_dir is None: 210 | self._other_dir = Path(self.code) 211 | os.makedirs(self._other_dir, exist_ok=True) 212 | return self._other_dir 213 | 214 | @property 215 | def table_names(self): 216 | table_names = [] 217 | for nuclide, _ in self.nuclides: 218 | table_names.append(zaid(nuclide, self.suffix)) 219 | Z, A, m = openmc.data.zam(nuclide) 220 | photon_table = f'{1000*Z}.{self.photon_suffix}' 221 | if photon_table not in table_names: 222 | table_names.append(photon_table) 223 | return table_names 224 | 225 | @particles.setter 226 | def particles(self, particles): 227 | if particles % self._batches != 0: 228 | msg = (f'Number of particles {particles} must be divisible by ' 229 | f'the number of batches {self._batches}.') 230 | raise ValueError(msg) 231 | self._particles = particles 232 | 233 | @code.setter 234 | def code(self, code): 235 | if code not in ('mcnp', 'serpent'): 236 | msg = (f'Unsupported code {code}: code must be either "mcnp" or ' 237 | '"serpent".') 238 | raise ValueError(msg) 239 | executable = 'mcnp6' if code == 'mcnp' else 'sss2' 240 | if not shutil.which(executable, os.X_OK): 241 | msg = f'Unable to locate executable {executable} in path.' 242 | raise ValueError(msg) 243 | self._code = code 244 | 245 | @suffix.setter 246 | def suffix(self, suffix): 247 | match = '(7[0-4]c)|(8[0-6]c)|(71[0-6]nc)|[0][3,6,9]c|[1][2,5,8]c' 248 | if not re.match(match, suffix): 249 | msg = f'Unsupported cross section suffix {suffix}.' 250 | raise ValueError(msg) 251 | self._suffix = suffix 252 | 253 | @photon_suffix.setter 254 | def photon_suffix(self, photon_suffix): 255 | if not re.match('12p', photon_suffix): 256 | msg = f'Unsupported photon cross section suffix {photon_suffix}.' 257 | raise ValueError(msg) 258 | self._photon_suffix = photon_suffix 259 | 260 | @xsdir.setter 261 | def xsdir(self, xsdir): 262 | if xsdir is not None: 263 | xsdir = Path(xsdir) 264 | if not xsdir.is_file(): 265 | msg = f'Could not locate the XSDIR file {xsdir}.' 266 | raise ValueError(msg) 267 | self._xsdir = xsdir 268 | 269 | @serpent_pdata.setter 270 | def serpent_pdata(self, serpent_pdata): 271 | if self.code == 'serpent': 272 | if serpent_pdata is None: 273 | msg = ('Serpent photon data path is required to run a ' 274 | 'calculation with Serpent.') 275 | raise ValueError(msg) 276 | serpent_pdata = Path(serpent_pdata).resolve() 277 | if not serpent_pdata.is_dir(): 278 | msg = (f'Could not locate the Serpent photon data directory ' 279 | f'{serpent_pdata}.') 280 | raise ValueError(msg) 281 | self._serpent_pdata = serpent_pdata 282 | 283 | def _make_openmc_input(self): 284 | """Generate the OpenMC input XML 285 | 286 | """ 287 | # Define material 288 | mat = openmc.Material() 289 | for nuclide, fraction in self.nuclides: 290 | mat.add_nuclide(nuclide, fraction) 291 | mat.set_density('g/cm3', self.density) 292 | materials = openmc.Materials([mat]) 293 | if self.xsdir is not None: 294 | xs_path = (self.openmc_dir / 'cross_sections.xml').resolve() 295 | materials.cross_sections = str(xs_path) 296 | materials.export_to_xml(self.openmc_dir / 'materials.xml') 297 | 298 | # Instantiate surfaces 299 | cyl = openmc.XCylinder(boundary_type='vacuum', r=1.e-6) 300 | px1 = openmc.XPlane(boundary_type='vacuum', x0=-1.) 301 | px2 = openmc.XPlane(boundary_type='transmission', x0=1.) 302 | px3 = openmc.XPlane(boundary_type='vacuum', x0=1.e9) 303 | 304 | # Instantiate cells 305 | inner_cyl_left = openmc.Cell() 306 | inner_cyl_right = openmc.Cell() 307 | outer_cyl = openmc.Cell() 308 | 309 | # Set cells regions and materials 310 | inner_cyl_left.region = -cyl & +px1 & -px2 311 | inner_cyl_right.region = -cyl & +px2 & -px3 312 | outer_cyl.region = ~(-cyl & +px1 & -px3) 313 | inner_cyl_right.fill = mat 314 | 315 | # Create root universe and export to XML 316 | geometry = openmc.Geometry([inner_cyl_left, inner_cyl_right, outer_cyl]) 317 | geometry.export_to_xml(self.openmc_dir / 'geometry.xml') 318 | 319 | # Define source 320 | source = openmc.Source() 321 | source.space = openmc.stats.Point((0,0,0)) 322 | source.angle = openmc.stats.Monodirectional() 323 | source.energy = openmc.stats.Discrete([self.energy], [1.]) 324 | source.particle = 'neutron' 325 | 326 | # Settings 327 | settings = openmc.Settings() 328 | if self._temperature is not None: 329 | settings.temperature = {'default': self._temperature} 330 | settings.source = source 331 | settings.particles = self.particles // self._batches 332 | settings.run_mode = 'fixed source' 333 | settings.batches = self._batches 334 | settings.photon_transport = True 335 | settings.electron_treatment = self.electron_treatment 336 | settings.cutoff = {'energy_photon' : self._cutoff_energy} 337 | settings.export_to_xml(self.openmc_dir / 'settings.xml') 338 | 339 | # Define filters 340 | surface_filter = openmc.SurfaceFilter(cyl) 341 | particle_filter = openmc.ParticleFilter('photon') 342 | energy_bins = np.logspace(np.log10(self._cutoff_energy), 343 | np.log10(self.max_energy), self._bins+1) 344 | energy_filter = openmc.EnergyFilter(energy_bins) 345 | 346 | # Create tallies and export to XML 347 | tally = openmc.Tally(name='tally') 348 | tally.filters = [surface_filter, energy_filter, particle_filter] 349 | tally.scores = ['current'] 350 | tallies = openmc.Tallies([tally]) 351 | tallies.export_to_xml(self.openmc_dir / 'tallies.xml') 352 | 353 | def _make_mcnp_input(self): 354 | """Generate the MCNP input file 355 | 356 | """ 357 | # Create the problem description 358 | lines = ['Broomstick problem'] 359 | 360 | # Create the cell cards: material 1 inside cylinder, void outside 361 | lines.append('c --- Cell cards ---') 362 | if self._temperature is not None: 363 | kT = self._temperature * openmc.data.K_BOLTZMANN * 1e-6 364 | lines.append(f'1 1 -{self.density} -4 6 -7 imp:n,p=1 tmp={kT}') 365 | else: 366 | lines.append(f'1 1 -{self.density} -4 6 -7 imp:n,p=1') 367 | lines.append('2 0 -4 5 -6 imp:n,p=1') 368 | lines.append('3 0 #(-4 5 -7) imp:n,p=0') 369 | lines.append('') 370 | 371 | # Create the surface cards: cylinder with radius 1e-6 cm along x-axis 372 | lines.append('c --- Surface cards ---') 373 | lines.append('4 cx 1.0e-6') 374 | lines.append('5 px -1.0') 375 | lines.append('6 px 1.0') 376 | lines.append('7 px 1.0e9') 377 | lines.append('') 378 | 379 | # Create the data cards 380 | lines.append('c --- Data cards ---') 381 | 382 | # Materials 383 | material_card = 'm1' 384 | for nuclide, fraction in self.nuclides: 385 | if re.match('(71[0-6]nc)', self.suffix): 386 | name = szax(nuclide, self.suffix) 387 | else: 388 | name = zaid(nuclide, self.suffix) 389 | material_card += f' {name} -{fraction} plib={self.photon_suffix}' 390 | lines.append(material_card) 391 | 392 | # Energy in MeV 393 | energy = self.energy * 1e-6 394 | max_energy = self.max_energy * 1e-6 395 | cutoff_energy = self._cutoff_energy * 1e-6 396 | 397 | # Physics: neutron and neutron-induced photon, 1 keV photon cutoff energy 398 | if self.electron_treatment == 'led': 399 | flag = 1 400 | else: 401 | flag = 'j' 402 | lines.append('mode n p') 403 | lines.append(f'phys:p j {flag} j j j') 404 | lines.append(f'cut:p j {cutoff_energy}') 405 | 406 | # Source definition: point source at origin monodirectional along 407 | # positive x-axis 408 | lines.append(f'sdef cel=2 erg={energy} vec=1 0 0 dir=1 par=1') 409 | 410 | # Tallies: Photon current over surface 411 | lines.append('f1:p 4') 412 | lines.append(f'e1 {cutoff_energy} {self._bins-1}ilog {max_energy}') 413 | 414 | # Problem termination: number of particles to transport 415 | lines.append(f'nps {self.particles}') 416 | 417 | # Write the problem 418 | with open(self.other_dir / 'inp', 'w') as f: 419 | f.write('\n'.join(lines)) 420 | 421 | def _make_serpent_input(self): 422 | """Generate the Serpent input file 423 | 424 | """ 425 | # Create the problem description 426 | lines = ['% Broomstick problem'] 427 | lines.append('') 428 | 429 | # Set the cross section library directory 430 | if self.xsdir is not None: 431 | xsdata = (self.other_dir / 'xsdata').resolve() 432 | lines.append(f'set acelib "{xsdata}"') 433 | 434 | # Set the photon data directory 435 | lines.append(f'set pdatadir "{self.serpent_pdata}"') 436 | lines.append('') 437 | 438 | # Create the cell cards: material 1 inside cylinder, void outside 439 | lines.append('% --- Cell cards ---') 440 | lines.append('cell 1 0 m1 -1 3 -4') 441 | lines.append('cell 2 0 void -1 2 -3') 442 | lines.append('cell 3 0 outside 1') 443 | lines.append('cell 4 0 outside -2') 444 | lines.append('cell 5 0 outside 4') 445 | lines.append('') 446 | 447 | # Create the surface cards: cylinder with radius 1e-6 cm along x-axis 448 | lines.append('% --- Surface cards ---') 449 | lines.append('surf 1 cylx 0.0 0.0 1.0e-6') 450 | lines.append('surf 2 px -1.0') 451 | lines.append('surf 3 px 1.0') 452 | lines.append('surf 4 px 1.0e9') 453 | lines.append('') 454 | 455 | # Create the material cards 456 | lines.append('% --- Material cards ---') 457 | lines.append(f'mat m1 -{self.density}') 458 | elements = {} 459 | for nuclide, fraction in self.nuclides: 460 | # Add nuclide data 461 | name = zaid(nuclide, self.suffix) 462 | lines.append(f'{name} {fraction}') 463 | 464 | # Sum element fractions 465 | Z, A, m = openmc.data.zam(nuclide) 466 | name = f'{1000*Z}.{self.photon_suffix}' 467 | if name not in elements: 468 | elements[name] = fraction 469 | else: 470 | elements[name] += fraction 471 | 472 | # Add element data 473 | for name, fraction in elements.items(): 474 | lines.append(f'{name} {fraction}') 475 | lines.append('') 476 | 477 | # Turn on unresolved resonance probability treatment 478 | lines.append('set ures 1') 479 | 480 | # Set electron treatment 481 | if self.electron_treatment == 'led': 482 | lines.append('set ttb 0') 483 | else: 484 | lines.append('set ttb 1') 485 | 486 | # Turn on Doppler broadening of Compton scattered photons (on by 487 | # default) 488 | lines.append('set cdop 1') 489 | 490 | # Coupled neutron-gamma calculations (0 is off, 1 is analog, 2 is 491 | # implicit) 492 | lines.append('set ngamma 1') 493 | 494 | # Energy in MeV 495 | energy = self.energy * 1e-6 496 | max_energy = self.max_energy * 1e-6 497 | cutoff_energy = self._cutoff_energy * 1e-6 498 | 499 | # Set cutoff energy 500 | lines.append(f'set ecut 0 {cutoff_energy}') 501 | lines.append('') 502 | 503 | # External source mode with isotropic point source at center of sphere 504 | lines.append('% --- Set external source mode ---') 505 | lines.append(f'set nps {self.particles} {self._batches}') 506 | lines.append(f'src 1 n se {energy} sp 0.0 0.0 0.0 sd 1.0 0.0 0.0') 507 | lines.append('') 508 | 509 | # Detector definition: photon current over surface 510 | lines.append('% --- Detector definition ---') 511 | lines.append('det 1 p de 1 ds 1 1') 512 | 513 | # Energy grid definition: equal lethargy spacing 514 | lines.append(f'ene 1 3 {self._bins} {cutoff_energy} {max_energy}') 515 | lines.append('') 516 | 517 | # Write the problem 518 | with open(self.other_dir / 'input', 'w') as f: 519 | f.write('\n'.join(lines)) 520 | 521 | def _plot(self): 522 | """Extract and plot the results 523 | 524 | """ 525 | # Read results 526 | path = self.openmc_dir / f'statepoint.{self._batches}.h5' 527 | x1, y1, _ = read_results('openmc', path) 528 | if self.code == 'serpent': 529 | path = self.other_dir / 'input_det0.m' 530 | else: 531 | path = self.other_dir / 'outp' 532 | x2, y2, sd = read_results(self.code, path) 533 | 534 | # Normalize the spectra 535 | cutoff_energy = self._cutoff_energy * 1e-6 536 | y1 /= np.diff(np.insert(x1, 0, cutoff_energy))*sum(y1) 537 | y2 /= np.diff(np.insert(x2, 0, cutoff_energy))*sum(y2) 538 | 539 | # Compute the relative error 540 | err = np.zeros_like(y2) 541 | idx = np.where(y2 > 0) 542 | err[idx] = (y1[idx] - y2[idx])/y2[idx] 543 | 544 | # Set up the figure 545 | fig = plt.figure(1, facecolor='w', figsize=(8,8)) 546 | ax1 = fig.add_subplot(111) 547 | 548 | # Create a second y-axis that shares the same x-axis, keeping the first 549 | # axis in front 550 | ax2 = ax1.twinx() 551 | ax1.set_zorder(ax2.get_zorder() + 1) 552 | ax1.patch.set_visible(False) 553 | 554 | # Plot the spectra 555 | label = 'Serpent' if self.code == 'serpent' else 'MCNP' 556 | ax1.loglog(x2, y2, 'r', linewidth=1, label=label) 557 | ax1.loglog(x1, y1, 'b', linewidth=1, label='OpenMC', linestyle='--') 558 | 559 | # Plot the relative error and uncertainties 560 | ax2.semilogx(x2, err, color=(0.2, 0.8, 0.0), linewidth=1) 561 | ax2.semilogx(x2, 2*sd, color='k', linestyle='--', linewidth=1) 562 | ax2.semilogx(x2, -2*sd, color='k', linestyle='--', linewidth=1) 563 | 564 | # Set grid and tick marks 565 | ax1.tick_params(axis='both', which='both', direction='in', length=10) 566 | ax1.grid(b=False, axis='both', which='both') 567 | ax2.tick_params(axis='y', which='both', right=False) 568 | ax2.grid(b=True, which='both', axis='both', alpha=0.5, linestyle='--') 569 | 570 | # Energy in MeV 571 | energy = self.energy * 1e-6 572 | max_energy = self.max_energy * 1e-6 573 | 574 | # Set axes labels and limits 575 | ax1.set_xlim([cutoff_energy, max_energy]) 576 | ax1.set_xlabel('Energy (MeV)', size=12) 577 | ax1.set_ylabel('Particle Current', size=12) 578 | ax1.legend() 579 | ax2.set_ylabel("Relative error", size=12) 580 | title = f'{self.material}, {energy:.1e} MeV Source' 581 | plt.title(title) 582 | 583 | # Save plot 584 | os.makedirs('plots', exist_ok=True) 585 | if self.name is not None: 586 | name = self.name 587 | else: 588 | name = f'{self.material}-{energy:.1e}MeV' 589 | if self._temperature is not None: 590 | name += f'-{self._temperature:.1f}K' 591 | plt.savefig(Path('plots') / f'{name}.png', bbox_inches='tight') 592 | plt.close() 593 | 594 | def run(self): 595 | """Generate inputs, run problem, and plot results. 596 | 597 | """ 598 | # Create HDF5 cross section library and Serpent XSDATA file 599 | if self.xsdir is not None: 600 | path = self.other_dir if self.code == 'serpent' else None 601 | create_library(self.xsdir, self.table_names, self.openmc_dir, path) 602 | 603 | # Get the temperature of the cross section data 604 | nuclide = self.nuclides[0][0] 605 | f = h5py.File(self.openmc_dir / (nuclide + '.h5'), 'r') 606 | temperature = list(f[nuclide]['kTs'].values())[0][()] 607 | self._temperature = temperature / K_BOLTZMANN 608 | 609 | # Generate input files 610 | self._make_openmc_input() 611 | 612 | if self.code == 'serpent': 613 | self._make_serpent_input() 614 | args = ['sss2', 'input'] 615 | else: 616 | self._make_mcnp_input() 617 | args = ['mcnp6'] 618 | if self.xsdir is not None: 619 | args.append(f'XSDIR={self.xsdir}') 620 | 621 | # Remove old MCNP output files 622 | for f in ('outp', 'runtpe'): 623 | try: 624 | os.remove(self.other_dir / f) 625 | except OSError: 626 | pass 627 | 628 | # Run code and capture and print output 629 | p = subprocess.Popen(args, cwd=self.code, stdout=subprocess.PIPE, 630 | stderr=subprocess.STDOUT, universal_newlines=True) 631 | while True: 632 | line = p.stdout.readline() 633 | if not line and p.poll() is not None: 634 | break 635 | print(line, end='') 636 | 637 | openmc.run(cwd='openmc') 638 | 639 | self._plot() 640 | -------------------------------------------------------------------------------- /validation/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | 4 | import numpy as np 5 | 6 | import openmc.data 7 | from openmc.data import K_BOLTZMANN, NEUTRON_MASS 8 | 9 | 10 | class XSDIR: 11 | """XSDIR directory file 12 | 13 | Parameters 14 | ---------- 15 | filename : str 16 | Path of the XSDIR file to load. 17 | 18 | Attributes 19 | ---------- 20 | filename : str 21 | Path of the XSDIR file. 22 | datapath : str 23 | Directory where the data libraries are stored. 24 | atomic_weight_ratio : dict of int to double 25 | Dictionary whose keys are ZAIDs and values are atomic weight ratios. 26 | directory : dict of str to XSDIRTable 27 | Dictionary whose keys are table names and values the entries in an 28 | XSDIR cross section table description. 29 | 30 | """ 31 | 32 | def __init__(self, filename): 33 | self.filename = filename 34 | self.datapath = None 35 | self.atomic_weight_ratio = {} 36 | self.directory = {} 37 | 38 | self._read() 39 | 40 | def _read(self): 41 | """Read the XSDIR directory file. 42 | 43 | """ 44 | with open(self.filename) as f: 45 | # First section: read the datapath if it is specified 46 | line = f.readline() 47 | tokens = re.split('\s|=', line) 48 | if tokens[0].lower() == 'datapath': 49 | self.datapath = tokens[1] 50 | 51 | line = f.readline() 52 | while line.strip().lower() != 'atomic weight ratios': 53 | line = f.readline() 54 | 55 | # Second section: read the ZAID/atomic weight ratio pairs 56 | line = f.readline() 57 | while line.strip().lower() != 'directory': 58 | tokens = line.split() 59 | if len(tokens) > 1: 60 | items = {int(tokens[i]): float(tokens[i+1]) 61 | for i in range(0, len(tokens), 2)} 62 | self.atomic_weight_ratio.update(items) 63 | 64 | line = f.readline() 65 | 66 | # Third section: read the available data tables 67 | line = f.readline() 68 | while line: 69 | # Handle continuation lines 70 | while line[-2] == '+': 71 | line += f.readline() 72 | line = line.replace('+\n', '') 73 | 74 | # Store the entry if we need this table 75 | tokens = line.split() 76 | self.directory[tokens[0]] = XSDIRTable(line) 77 | 78 | line = f.readline() 79 | 80 | def export_to_xsdata(self, path='xsdata', table_names=None): 81 | """Create a Serpent XSDATA directory file. 82 | 83 | Parameters 84 | ---------- 85 | path : str 86 | Path to file to write. Defaults to 'xsdata'. 87 | table_names : None, str, or iterable, optional 88 | Tables from the XSDIR file to write to the XSDATA file. If None, 89 | all of the entries are written. If str or iterable, only the 90 | entries matching the table names are written. 91 | 92 | """ 93 | if table_names is None: 94 | table_names = self.directory.keys() 95 | else: 96 | table_names = set(table_names) 97 | 98 | # Classes of data included in the XSDATA file (continuous-energy 99 | # neutron, neutron dosimetry, thermal scattering, and continuous-energy 100 | # photoatomic) 101 | data_classes = {'c': 1, 'y': 2, 't': 3, 'p': 5} 102 | 103 | lines = [] 104 | for name in table_names: 105 | table = self.directory.get(name) 106 | if table is None: 107 | msg = f'Could not find table {name} in {self.filename}.' 108 | raise ValueError(msg) 109 | 110 | # Check file format 111 | if table.file_type != 'ascii': 112 | msg = f'Unsupported file type {table.file_type} for {name}.' 113 | raise ValueError(msg) 114 | 115 | if self.datapath is None: 116 | # Set the access route as the datapath if it is specified; 117 | # otherwise, set the parent directory of XSDIR as the datapath 118 | if table.access_route is not None: 119 | datapath = Path(table.access_route) 120 | else: 121 | datapath = Path(self.filename).parent 122 | else: 123 | datapath = Path(self.datapath) 124 | 125 | # Get the full path to the ace library 126 | ace_path = datapath / table.file_name 127 | if not ace_path.is_file(): 128 | raise ValueError(f'Could not find ACE file {ace_path}.') 129 | 130 | zaid, suffix = name.split('.') 131 | 132 | # Skip this table if it is not one of the data classes included in 133 | # XSDATA 134 | if suffix[-1] not in data_classes: 135 | continue 136 | 137 | # Get information about material and type of cross section data 138 | data_class = data_classes[suffix[-1]] 139 | if data_class == 3: 140 | ZA = 0 141 | m = 0 142 | else: 143 | zaid = int(zaid) 144 | _, element, Z, A, m = openmc.data.get_metadata(zaid, 'nndc') 145 | ZA = 1000*Z + A 146 | alias = f'{element}-' 147 | if A == 0: 148 | alias += 'nat.' 149 | elif m == 0: 150 | alias += f'{A}.' 151 | else: 152 | alias += f'{A}m.' 153 | alias += suffix 154 | 155 | # Calculate the atomic weight 156 | if zaid in self.atomic_weight_ratio: 157 | atomic_weight = self.atomic_weight_ratio[zaid] * NEUTRON_MASS 158 | else: 159 | atomic_weight = table.atomic_weight_ratio * NEUTRON_MASS 160 | 161 | # Calculate the temperature in Kelvin 162 | temperature = table.temperature / K_BOLTZMANN * 1e6 163 | 164 | # Entry in the XSDATA file 165 | lines.append(f'{name} {name} {data_class} {ZA} {m} ' 166 | f'{atomic_weight:.8f} {temperature:.1f} 0 {ace_path}') 167 | 168 | # Also write an entry with the alias if this is not a thermal 169 | # scattering table 170 | if data_class != 3: 171 | lines.append(f'{alias} {name} {data_class} {ZA} {m} ' 172 | f'{atomic_weight:.8f} {temperature:.1f} 0 {ace_path}') 173 | 174 | # Write the XSDATA file 175 | with open(path, 'w') as f: 176 | f.write('\n'.join(lines)) 177 | 178 | 179 | def get_tables(self, table_names): 180 | """Read ACE cross section tables from an XSDIR directory file. 181 | 182 | Parameters 183 | ---------- 184 | table_names : str or iterable 185 | Names of the ACE tables to load 186 | 187 | Returns 188 | ------- 189 | list of openmc.data.ace.Table 190 | ACE cross section tables 191 | 192 | """ 193 | if isinstance(table_names, str): 194 | table_names = [table_names] 195 | else: 196 | table_names = set(table_names) 197 | 198 | tables = [] 199 | for name in table_names: 200 | table = self.directory.get(name) 201 | if table is None: 202 | msg = f'Could not find table {name} in {self.filename}.' 203 | raise ValueError(msg) 204 | 205 | if self.datapath is None: 206 | # Set the access route as the datapath if it is specified; 207 | # otherwise, set the parent directory of XSDIR as the datapath 208 | if table.access_route is not None: 209 | datapath = Path(table.access_route) 210 | else: 211 | datapath = Path(self.filename).parent 212 | else: 213 | datapath = Path(self.datapath) 214 | 215 | # Get the full path to the ace library 216 | ace_path = datapath / table.file_name 217 | if not ace_path.is_file(): 218 | raise ValueError(f'Could not find ACE file {ace_path}.') 219 | 220 | zaid, suffix = name.split('.') 221 | if re.match('(8[0-6]c)|(71[0-6]nc)', suffix): 222 | nuclide, _, _, _, _ = openmc.data.get_metadata(int(zaid)) 223 | name = szax(nuclide, suffix) 224 | 225 | # Get the ACE table 226 | print(f'Converting table {name} from library {ace_path}...') 227 | tables.append(openmc.data.ace.get_table(ace_path, name)) 228 | 229 | return tables 230 | 231 | 232 | class XSDIRTable: 233 | """XSDIR description of a cross section table 234 | 235 | Parameters 236 | ---------- 237 | line : str 238 | Cross section table description from an XSDIR directory file. 239 | 240 | Attributes 241 | ---------- 242 | name : str 243 | ZAID of the table. 244 | atomic_weight_ratio : float 245 | Atomic mass ratio of the target nuclide. 246 | file_name : str 247 | Name of the library that contains the table. 248 | access_route : str 249 | Path to the library. 250 | file_type : {'ascii', 'binary'} 251 | File format. 252 | address : int 253 | For type 1 files the address is the line number in the file where the 254 | table starts. For type 2 files it is the record number of the first 255 | record of the table. 256 | table_length : int 257 | Length (total number of words) of the table. 258 | record_length : int 259 | For type 1 files the record length is unused. For type 2 files it is a 260 | multiple of the number of entries per record. 261 | entries_per_record : int 262 | For type 1 files this is unused. For type 2 files it is the number of 263 | entries per record. 264 | temperature : float 265 | Temperature in MeV at which a neutron table is processed. This is used 266 | only for neutron data. 267 | ptables : bool 268 | If true, it indicates a continuous-energy neutron nuclide has 269 | unresolved resonance range probability tables. 270 | 271 | """ 272 | def __init__(self, line): 273 | entries = line.split() 274 | num_entries = len(entries) 275 | 276 | self.name = entries[0] 277 | self.atomic_weight_ratio = float(entries[1]) 278 | self.file_name = entries[2] 279 | if entries[3] != '0': 280 | self.access_route = entries[3] 281 | else: 282 | self.access_route = None 283 | if entries[4] == '1': 284 | self.file_type = 'ascii' 285 | else: 286 | self.file_type = 'binary' 287 | self.address = int(entries[5]) 288 | self.table_length = int(entries[6]) 289 | if num_entries > 7: 290 | self.record_length = int(entries[7]) 291 | else: 292 | self.record_length = 0 293 | if num_entries > 8: 294 | self.entries_per_record = int(entries[8]) 295 | else: 296 | self.entries_per_record = 0 297 | if num_entries > 9: 298 | self.temperature = float(entries[9]) 299 | else: 300 | self.temperature = 0.0 301 | if num_entries > 10: 302 | self.ptables = entries[10].lower() == 'ptable' 303 | else: 304 | self.ptables = False 305 | 306 | 307 | def zaid(nuclide, suffix): 308 | """Return ZAID for a given nuclide and cross section suffix. 309 | 310 | Parameters 311 | ---------- 312 | nuclide : str 313 | Name of the nuclide 314 | suffix : str 315 | Cross section suffix for MCNP 316 | 317 | Returns 318 | ------- 319 | str 320 | ZA identifier 321 | 322 | """ 323 | Z, A, m = openmc.data.zam(nuclide) 324 | 325 | # Serpent metastable convention 326 | if re.match('[0][3,6,9]c|[1][2,5,8]c', suffix): 327 | # Increase mass number above 300 328 | if m > 0: 329 | while A < 300: 330 | A += 100 331 | 332 | # MCNP metastable convention 333 | else: 334 | # Correct the ground state and first excited state of Am242, which 335 | # are the reverse of the convention 336 | if A == 242 and m == 0: 337 | m = 1 338 | elif A == 242 and m == 1: 339 | m = 0 340 | 341 | if m > 0: 342 | A += 300 + 100*m 343 | 344 | if re.match('(71[0-6]nc)', suffix): 345 | suffix = f'8{suffix[2]}c' 346 | 347 | return f'{1000*Z + A}.{suffix}' 348 | 349 | 350 | def szax(nuclide, suffix): 351 | """Return SZAX for a given nuclide and cross section suffix. 352 | 353 | Parameters 354 | ---------- 355 | nuclide : str 356 | Name of the nuclide 357 | suffix : str 358 | Cross section suffix for MCNP 359 | 360 | Returns 361 | ------- 362 | str 363 | SZA identifier 364 | 365 | """ 366 | Z, A, m = openmc.data.zam(nuclide) 367 | 368 | # Correct the ground state and first excited state of Am242, which are 369 | # the reverse of the convention 370 | if A == 242 and m == 0: 371 | m = 1 372 | elif A == 242 and m == 1: 373 | m = 0 374 | 375 | if re.match('(7[0-4]c)|(8[0-6]c)', suffix): 376 | suffix = f'71{suffix[1]}nc' 377 | 378 | return f'{1000000*m + 1000*Z + A}.{suffix}' 379 | 380 | 381 | def create_library(xsdir, table_names, hdf5_dir, xsdata_dir=None): 382 | """Convert the ACE data from the MCNP or Serpent distribution into an 383 | HDF5 library that can be used by OpenMC and create and XSDATA directory 384 | file for use with Serpent. 385 | 386 | Parameters 387 | ---------- 388 | xsdir : str 389 | Path of the XSDIR directory file 390 | table_names : str or iterable 391 | Names of the ACE tables to convert 392 | hdf5_dir : str 393 | Directory to write the HDF5 library to 394 | xsdata_dir : str 395 | If specified, an XSDATA directory file containing entries for each of 396 | the table names provided will be written to this directory. 397 | 398 | """ 399 | # Create data library 400 | data_lib = openmc.data.DataLibrary() 401 | 402 | # Load the XSDIR directory file 403 | xsdir = XSDIR(xsdir) 404 | 405 | # Get the ACE cross section tables 406 | tables = xsdir.get_tables(table_names) 407 | 408 | for table in tables: 409 | zaid, suffix = table.name.split('.') 410 | 411 | # Convert cross section data 412 | if suffix[-1] == 'c': 413 | match = '(7[0-4]c)|(8[0-6]c)|(71[0-6]nc)' 414 | scheme = 'mcnp' if re.match(match, suffix) else 'nndc' 415 | data = openmc.data.IncidentNeutron.from_ace(table, scheme) 416 | elif suffix[-1] == 'p': 417 | data = openmc.data.IncidentPhoton.from_ace(table) 418 | elif suffix[-1] == 't': 419 | data = openmc.data.ThermalScattering.from_ace(table) 420 | else: 421 | msg = ('Unknown data class: cannot convert cross section data ' 422 | f'from table {table.name}') 423 | raise ValueError(msg) 424 | 425 | # Export HDF5 files and register with library 426 | h5_file = Path(hdf5_dir) / f'{data.name}.h5' 427 | data.export_to_hdf5(h5_file, 'w') 428 | data_lib.register_file(h5_file) 429 | 430 | # Write cross_sections.xml 431 | data_lib.export_to_xml(Path(hdf5_dir) / 'cross_sections.xml') 432 | 433 | # Write the Serpent XSDATA file 434 | if xsdata_dir is not None: 435 | xsdir.export_to_xsdata(Path(xsdata_dir) / 'xsdata', table_names) 436 | 437 | 438 | def read_results(code, filename): 439 | """Read the energy, mean, and standard deviation from the output 440 | 441 | Parameters 442 | ---------- 443 | code : {'openmc', 'mcnp', 'serpent'} 444 | Code which produced the output file 445 | filename : str 446 | Path to the output file 447 | 448 | Returns 449 | ------- 450 | energy : numpy.ndarray 451 | Energy bin values [MeV] 452 | mean : numpy.ndarray 453 | Sample mean of the tally 454 | std_dev : numpy.ndarray 455 | Sample standard deviation of the tally 456 | 457 | """ 458 | if code == 'openmc': 459 | with openmc.StatePoint(filename) as sp: 460 | t = sp.get_tally(name='tally') 461 | energy = t.find_filter(openmc.EnergyFilter).bins[:,1]*1e-6 462 | mean = t.mean[:,0,0] 463 | std_dev = t.std_dev[:,0,0] 464 | 465 | elif code == 'mcnp': 466 | with open(filename, 'r') as f: 467 | text = f.read() 468 | p = text.find('1tally') 469 | p = text.find('energy', p) + 10 470 | q = text.find('total', p) 471 | t = np.fromiter(text[p:q].split(), float) 472 | t.shape = (len(t) // 3, 3) 473 | energy = t[1:,0] 474 | mean = t[1:,1] 475 | std_dev = t[1:,2] 476 | 477 | elif code == 'serpent': 478 | with open(filename, 'r') as f: 479 | text = re.split('\[|\]', f.read()) 480 | t = np.fromiter(text[1].split(), float) 481 | t = t.reshape(len(t) // 12, 12) 482 | e = np.fromiter(text[3].split(), float) 483 | e = e.reshape(len(e) // 3, 3) 484 | energy = e[:,1] 485 | mean = t[:,10] 486 | std_dev = t[:,11] 487 | 488 | return energy, mean, std_dev 489 | --------------------------------------------------------------------------------