Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
34893ab
Add files via upload
anu1217 May 29, 2025
58e2ab4
Delete WC_Layers/R2S.yaml
anu1217 May 29, 2025
f664847
Delete WC_Layers/OpenMC_R2S.py
anu1217 May 29, 2025
127c9b9
Add files via upload
anu1217 May 29, 2025
f1fe7e5
Delete WC_Layers/OpenMC_ALARA_WC.yaml
anu1217 May 29, 2025
3362dd8
Delete WC_Layers/OpenMC_R2S.py
anu1217 May 30, 2025
7b8ab73
Add files via upload
anu1217 May 30, 2025
a283d38
Rename OpenMC_R2S.py to WC_Layers/OpenMC_R2S.py
anu1217 May 30, 2025
3f9e2e4
Changing source_mesh_index into a list of indices
anu1217 May 30, 2025
edf0b50
Fix geometry in make_spherical_shells()
anu1217 Jun 26, 2025
876ff49
Remove manual void assignment to region external to geometry
anu1217 Jul 10, 2025
1125243
Run OpenMC neutron model
anu1217 Jul 10, 2025
26fb5f9
Delete run_depletion() and rename deplete_wc to deplete_model
anu1217 Jul 10, 2025
870d22c
Add indentation for clarity
anu1217 Jul 10, 2025
f7758ef
Update OpenMC_R2S.py
anu1217 Jul 14, 2025
f37756b
Replace export_to_model_xml with settings.export_to_xml()
anu1217 Jul 14, 2025
3ba8c4d
Remove create_materials_obj() and create_geometry_obj()
anu1217 Jul 16, 2025
28fd523
Combine read_source_mesh() with extract_source_data()
anu1217 Jul 16, 2025
58ef075
Modify input to read_yaml()
anu1217 Jul 16, 2025
f1e36e5
Revise execution logic for PyNE and OpenMC workflows
anu1217 Jul 16, 2025
076afa3
Change discrete energy dist to None type
anu1217 Jul 16, 2025
8dceacf
Fix function calls
anu1217 Jul 17, 2025
37bdc27
Fix argparse-related calls
anu1217 Jul 17, 2025
cbd5043
Change how settings are assigned to openmc r2s & modify geometry to a…
anu1217 Jul 17, 2025
7269960
Remove commented lines
anu1217 Jul 17, 2025
65863e0
Fix magic numbers & make_openmc_photon_sources(), add dummy neutron t…
anu1217 Aug 27, 2025
eb10ff9
Add ray firing parameter
anu1217 Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
378 changes: 378 additions & 0 deletions WC_Layers/OpenMC_R2S.py
Comment thread
anu1217 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,378 @@
import openmc
import openmc.deplete
from pathlib import Path
import argparse
import yaml
import numpy as np
import h5py

def alara_element_densities(alara_fp):
'''
Creates a dictionary where keys = element names (str) and values = element density (float)
inputs:
alara_filepath : path to ALARA element library
'''
with open(alara_fp) as ALARA_Lib:
libLines = ALARA_Lib.readlines()
num_lines = len(libLines)
density_dict = {}
line_num = 0
while line_num < num_lines:
element_data = libLines[line_num].strip().split()
element_name = element_data[0].lower()
density_dict[element_name] = float(element_data[3])
line_num += int(element_data[4]) + 1
return density_dict

def make_materials(elements, density_dict):
'''
Creates an OpenMC Materials object using user-specified elements
inputs:
elements: iterable of element names (str)
density_dict: dictionary with keys = element names (str) and values = element density (float)
'''
mats = openmc.Materials([])
for element_id, element in enumerate(elements):
mat = openmc.Material(material_id=element_id+1, name=element)
mat.add_element(element, 1.00)
mat.set_density('g/cm3', density_dict.get(element.lower()))
mats.append(mat)
return mats

def make_spherical_shells(inner_radius, layers, outer_boundary_type):
'''
Creates a set of concentric spherical shells, each with its own material & inner/outer radius.
inputs:
inner_radius: the radius of the innermost spherical shell
layers: iterable of tuples of OpenMC Material object and its respective thickness (float)
'''
inner_sphere = openmc.Sphere(r = inner_radius)
cells = [openmc.Cell(fill = None, region = -inner_sphere)]
for (material, thickness) in layers:
outer_radius = inner_radius + thickness
outer_sphere = openmc.Sphere(r = outer_radius)
cells.append(openmc.Cell(fill = material, region = +inner_sphere & -outer_sphere))
inner_radius = outer_radius
inner_sphere = outer_sphere
material.volume = 4.0/3.0 * np.pi * ((inner_radius + thickness)**3 - (inner_radius)**3)
inner_radius = inner_radius + thickness
Comment thread
anu1217 marked this conversation as resolved.
Outdated
outer_sphere.boundary_type = outer_boundary_type
cells.append(openmc.Cell(fill = None, region = +outer_sphere))
geometry = openmc.Geometry(cells)
return geometry

def make_neutron_source(energy):
point_source = openmc.stats.Point(xyz=(0.0, 0.0, 0.0))
energy_dist = openmc.stats.Discrete(energy, 1.0)
neutron_source = openmc.Source(space = point_source, energy = energy_dist, strength = 1.0)
return neutron_source

def make_neutron_tallies(mesh_file):
mesh_filter = openmc.MeshFilter(openmc.UnstructuredMesh(mesh_file, library='moab'))
particle_filter = openmc.ParticleFilter('neutron')
energy_filter_flux = openmc.EnergyFilter.from_group_structure("VITAMIN-J-175")

neutron_flux_tally = openmc.Tally(name="Neutron flux spectrum")
neutron_flux_tally.filters = [mesh_filter, energy_filter_flux, particle_filter]
neutron_flux_tally.scores = ['flux']

return openmc.Tallies([neutron_flux_tally])

def make_settings(neutron_source, total_batches, inactive_batches, num_particles, run_mode):
Comment thread
anu1217 marked this conversation as resolved.
Outdated
sets = openmc.Settings()
sets.batches = total_batches
sets.inactive = inactive_batches
sets.particles = num_particles
sets.source = neutron_source
sets.run_mode = run_mode
return sets

#Only executed if external geometry and materials are imported
def make_depletion_volumes(neutron_model, mesh_file):
materials = neutron_model.materials
mesh_file = Path(mesh_file).resolve()
unstructured_mesh = openmc.UnstructuredMesh(mesh_file, library='moab')
mat_vols = unstructured_mesh.material_volumes(neutron_model, n_samples=25000000)
#returns dict-like object that maps mat ids to array of volumes equal to # of mesh elements

total_volumes = {}
for mat_id, volumes in mat_vols.items():
total_volumes[mat_id] = np.sum(volumes)
for material in materials:
material.volume = total_volumes[material.id]
return neutron_model

def deplete_wc(neutron_model, mesh_file, chain_file, timesteps, source_rates, norm_mode, timestep_units):
materials = neutron_model.materials
mesh_file = Path(mesh_file).resolve()
unstructured_mesh = openmc.UnstructuredMesh(mesh_file, library='moab')
model_nuclide_names = []
for material in materials:
material.depletable = True
material_nuclides = material.nuclides
for material_nuclide in material_nuclides:
model_nuclide_names.append(material_nuclide.name)

activation_mats = unstructured_mesh.get_homogenized_materials(neutron_model, n_samples=7000000)
activation_mats_object = openmc.Materials(activation_mats)
activation_mats_object.export_to_xml("Activation_Materials.xml")

depletion_chain = openmc.deplete.Chain.from_xml(Path(chain_file).resolve())

nuclide_names = []
for nuclide in depletion_chain.nuclides:
if nuclide.name in model_nuclide_names:
nuclide_names.append(nuclide.name)

fluxes, micros = openmc.deplete.get_microxs_and_flux(neutron_model, unstructured_mesh, nuclide_names,
depletion_chain.reactions,
openmc.mgxs.GROUP_STRUCTURES["VITAMIN-J-175"])
operator = openmc.deplete.IndependentOperator(openmc.Materials(activation_mats), fluxes, micros, chain_file=chain_file, normalization_mode = norm_mode)
copy_timesteps = list(timesteps)
copy_source_rates = list(source_rates)
integrator_list = []
for timestep in timesteps:
while copy_source_rates[-1] == 0:
integrator = openmc.deplete.PredictorIntegrator(operator, copy_timesteps, source_rates = copy_source_rates, timestep_units = timestep_units)
integrator_list.append(integrator)
copy_timesteps.pop()
copy_source_rates.pop()

#Letting integrator_list go from the fewest number of decay intervals to the most
integrator_list.reverse()
for int_index, integrator in enumerate(integrator_list):
integrator.integrate(path=f"depletion_results_decay_set_{int_index}.h5")
return activation_mats, unstructured_mesh, neutron_model

def extract_source_data(source_mesh_list, num_elements, num_photon_groups):
'''
Identifies the location of the source density dataset within each mesh file.

input:
source_mesh_list: iterable of .h5m filenames (str), whose files contain photon source information
output:
numpy array of source density data with rows = # of mesh elements and columns = # number of photon groups, with one array per source mesh
'''
sd_list = np.ndarray((len(source_mesh_list), num_elements, num_photon_groups))
for source_index, source_name in enumerate(source_mesh_list):
file = h5py.File(source_name, 'r')
sd_list[source_index,:] = file['tstt']['elements']['Tet4']['tags']['source_density'][:]
return sd_list

def make_alara_photon_sources(bounds, cells, mesh_file, source_mesh_indices, sd_list, neutron_model):
'''
Creates a list of OpenMC sources, complete with the relevant space and energy distributions

inputs:
bounds : iterable of photon energy bounds (float)
cells: list of OpenMC Cell objects
mesh_file: .h5/.h5m mesh onto which photon source will be distributed
source_mesh_indices: iterable of indices specifying the photon source from which data is extracted

output:
all_sources: dictionary of OpenMC independent sources
unstructured_mesh: OpenMC Unstructured Mesh object
photon_model : OpenMC Model object with photon sources
'''
all_sources = {}
unstructured_mesh = openmc.UnstructuredMesh(mesh_file, library='moab')
for source_mesh_index in source_mesh_indices:
source_list = []
for index, (lower_bound, upper_bound) in enumerate(zip(bounds[:-1],bounds[1:])):
mesh_dist = openmc.stats.MeshSpatial(unstructured_mesh, strengths=sd_list[source_mesh_index][:,index], volume_normalized=False)
energy_dist = openmc.stats.Uniform(a=lower_bound, b=upper_bound)
source_list.append(openmc.IndependentSource(space=mesh_dist, energy=energy_dist, strength=np.sum(sd_list[source_mesh_index][:, index]), particle='photon', domains=cells))
all_sources[source_mesh_index] = source_list
photon_model = neutron_model
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if this makes a copy? or just points the photon_model variable at the neutron_model? If the latter, than maybe you can just pass a model and change it as you need and return it back without distinghuishing between photon and neutron model

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just call the variable photon_model in the argument list and be done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer the second idea. Labeling them photon and neutron model has made it easier to keep track of where/which particle sources are being assigned to the model

photon_model.settings.source = all_sources[source_mesh_index]
photon_model.settings.export_to_xml(f'settings_{source_mesh_index}.xml')
return photon_model

def make_openmc_photon_sources(num_cooling_steps, activation_mats, unstructured_mesh, neutron_model, inputs):
sd_data = h5py.File(inputs['source_meshes'][0], 'r')['tstt']['elements']['Tet4']['tags']['source_density'][:]
for int_index in range(num_cooling_steps):
results = openmc.deplete.Results(f"depletion_results_decay_set_{int_index}.h5")
photon_sources = np.empty(sd_data.shape[0], dtype=object)
activated_mats_list = set(results[-1].index_mat.keys())

for mat_index, mat in enumerate(activation_mats) :
if str(mat.id) in activated_mats_list :
mat = results[-1].get_material(str(mat.id))
energy = mat.get_decay_photon_energy()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a good place for a little function

energy = get_decay_photon_energies(mat, results[-1])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on the benefit of doing this? get_decay_photon_energy() is also an openmc function, not my own

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question... (especially since I had to think about it myself to answer your question...)

Two motivations:

  1. It isolates the details of the logic behind checking if a material is in the activated material list and then extracting the right piece of the results, thus lowing the cognitive burden of understanding the bigger picture of what's going on here.

  2. That method could return None if either the material wasn't in the activated materials list or if the energy was None and then I think you could collapse the conditional clause somewhat

if energy == None:
photon_source = openmc.IndependentSource(
energy = energy,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does OpenMC care if this is None in this case? I know the strength is 0 anyway....

Copy link
Copy Markdown
Contributor Author

@anu1217 anu1217 Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe if energy is of type None then it doesn't have the integral attribute, so the strength has to be defined "manually"

particle = 'photon',
strength = 0.0)
Comment thread
anu1217 marked this conversation as resolved.
Outdated
else:
photon_source = openmc.IndependentSource(
energy = energy,
particle = 'photon',
strength = energy.integral())

else:
photon_source = openmc.IndependentSource(
energy = openmc.stats.Discrete(0, 1.0),
Comment thread
anu1217 marked this conversation as resolved.
Outdated
particle = 'photon',
strength = 0.0)

photon_sources[mat_index] = photon_source
photon_model = neutron_model
photon_model.settings.source = openmc.MeshSource(unstructured_mesh, photon_sources)
photon_model.settings.export_to_xml(f'settings_{int_index}.xml')

return photon_model

def make_photon_tallies(coeff_geom, photon_model, num_cooling_steps):
dose_energy, dose = openmc.data.dose_coefficients('photon', geometry=coeff_geom)
dose_filter = openmc.EnergyFunctionFilter(dose_energy, dose)

#Could also use an unstructured mesh filter here - spherical mesh filter for visualization purposes
spherical_mesh = openmc.SphericalMesh(np.arange(0, 3800, 10), origin = (0.0, 0.0, 0.0), mesh_id=2, name="spherical_mesh")
spherical_mesh_filter = openmc.MeshFilter(spherical_mesh)

particle_filter = openmc.ParticleFilter('photon')
energy_filter_flux = openmc.EnergyFilter.from_group_structure("VITAMIN-J-42")

flux_tally = openmc.Tally(tally_id=1, name="photon flux tally")
flux_tally.filters = [spherical_mesh_filter, energy_filter_flux, particle_filter]
flux_tally.scores = ['flux']

dose_tally = openmc.Tally(tally_id=2, name="dose tally")
dose_tally.filters = [spherical_mesh_filter, energy_filter_flux, particle_filter, dose_filter]
dose_tally.scores = ['flux']

photon_model.tallies = [flux_tally, dose_tally]
#Change boundary condition:
list(photon_model.geometry.get_all_surfaces().keys())[-1].boundary_type="white"

for int_index in range(num_cooling_steps):
#Reassign settings (which contains source) for each decay step
photon_model.settings = openmc.Settings.from_xml(f'settings_{int_index}.xml')
photon_model.export_to_model_xml(path=f'photon_model_{int_index}.xml')
sp_path = photon_model.run(f'photon_model_{int_index}.xml')
sp_path.rename(f'statepoint_openmc_{int_index}.h5')

#----------------------------------------------------------------------------------
#Define variables and execute all functions:

def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--OpenMC_WC_YAML', default = "R2S.yaml", help="Path (str) to YAML containing inputs for WC_Neutron_Transport")
parser.add_argument('--ext_mat_geom', default = False, help="Specify whether materials and geometry come from external model")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me when you would use this option? I'm not questioning whether it should exist, just want to know at which point in which workflow it gets used so that I can understand the rest of the code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this in to let the user choose between building nested spherical shells (which would execute make_spherical_shells()) and importing materials and geometry from some preexisting model (e.g. HCPB)

parser.add_argument("--neutron_transport", default=False, help="Create neutron transport model")
parser.add_argument('--pyne_r2s', default = False, help="Specify whether PyNE R2S or OpenMC R2S steps are executed (OpenMC by default)")
parser.add_argument('--photon_transport', default = False, help="Run only photon transport (no depletion)")
args = parser.parse_args()
return args

def read_yaml(args):
Comment thread
anu1217 marked this conversation as resolved.
Outdated
with open(args.OpenMC_WC_YAML, 'r') as transport_file:
inputs = yaml.safe_load(transport_file)
return inputs

def create_materials_obj(inputs):
Comment thread
anu1217 marked this conversation as resolved.
Outdated
densities = alara_element_densities(inputs['filename_dict']['elelib_fp'])
materials = make_materials(inputs['mat_info']['element_list'],
densities)
return materials

def create_geometry_obj(materials, inputs):
Comment thread
anu1217 marked this conversation as resolved.
Outdated
geom_info = inputs['geom_info']
layers = zip(materials, geom_info['thicknesses'])
geometry = make_spherical_shells(geom_info['inner_radius'],
layers,
geom_info['outer_boundary_type'])
return geometry

def create_neutron_model(inputs, materials, geometry):
settings_info = inputs['settings_info']
neutron_source = make_neutron_source(inputs['particle_energy'])
settings = make_settings(neutron_source,
settings_info['total_batches'],
settings_info['inactive_batches'],
settings_info['num_particles'],
settings_info['run_mode'])
neutron_model = openmc.model.Model(geometry = geometry, materials = materials, settings = settings)
neutron_model.export_to_model_xml("neutron_model.xml")
return neutron_model

#Convert the output of R2S Step2 to a format suitable for OpenMC photon transport:
def read_source_mesh(inputs):
#Find the size of the first source density dataset (assumed to be the same for all other datasets):
sd_data = h5py.File(inputs['source_meshes'][0], 'r')['tstt']['elements']['Tet4']['tags']['source_density'][:]
sd_list = extract_source_data(inputs['source_meshes'],
sd_data.shape[0],
sd_data.shape[1])
Comment thread
anu1217 marked this conversation as resolved.
Outdated
return sd_list

def create_alara_photon_model(inputs, neutron_model, sd_list):
cells = list(neutron_model.geometry.get_all_cells().values())
photon_model = make_alara_photon_sources(
inputs['source_info']['phtn_e_bounds'],
cells,
inputs['filename_dict']['mesh_file'],
inputs['file_indices']['source_mesh_indices'],
sd_list,
neutron_model
)
return photon_model

def run_depletion(inputs, neutron_model):
Comment thread
anu1217 marked this conversation as resolved.
Outdated
dep_params = inputs['dep_params']

activation_mats, unstructured_mesh, integrator, neutron_model = deplete_wc(neutron_model,
inputs['filename_dict']['mesh_file'],
dep_params['chain_file'],
dep_params['timesteps'],
dep_params['source_rates'],
dep_params['norm_mode'],
dep_params['timestep_units'])
return activation_mats, unstructured_mesh, integrator, neutron_model

def main():
args = parse_args()
inputs = read_yaml(args)

openmc.config['chain_file'] = inputs['dep_params']['chain_file']
if args.ext_mat_geom == True : #Import materials and geometry from external model
ext_model = openmc.model.Model.from_model_xml(inputs['filename_dict']['ext_model'])
materials = ext_model.materials
geometry = ext_model.geometry
else:
materials = create_materials_obj(inputs)
geometry = create_geometry_obj(materials, inputs)

#Settings also assigned here
neutron_model = create_neutron_model(inputs, materials, geometry)

#Choose between PyNE R2S steps and OpenMC R2S steps
if args.pyne_r2s == True:
if args.neutron_transport == True:
neutron_model.tallies = make_neutron_tallies(inputs['filename_dict']['mesh_file'])
neutron_model.export_to_model_xml("neutron_model.xml")
Comment thread
anu1217 marked this conversation as resolved.
Outdated
else:
Comment thread
anu1217 marked this conversation as resolved.
Outdated
sd_list = read_source_mesh(inputs)
photon_model = create_alara_photon_model(inputs, neutron_model, sd_list)
num_cooling_steps = len(inputs['file_indices']['source_mesh_indices'])

else:
# Run OpenMC-only R2S

# Set to True to run photon transport only (no depletion):
if args.photon_transport == True:
activation_mats = openmc.Materials.from_xml("Activation_Materials.xml")
mesh_file = Path(inputs['filename_dict']['mesh_file']).resolve()
unstructured_mesh = openmc.UnstructuredMesh(mesh_file, library='moab')
else:
if args.ext_mat_geom == True :
neutron_model = make_depletion_volumes(neutron_model, inputs['filename_dict']['mesh_file'])
activation_mats, unstructured_mesh, neutron_model = run_depletion(inputs, neutron_model)

num_cooling_steps = (inputs['dep_params']['source_rates']).count(0)
photon_model = make_openmc_photon_sources(num_cooling_steps, activation_mats, unstructured_mesh, neutron_model, inputs)

photon_tallies = make_photon_tallies(inputs['coeff_geom'], photon_model, num_cooling_steps)
Comment thread
anu1217 marked this conversation as resolved.
Outdated

if __name__ == "__main__":
main()
Loading