OpenMC/scripts/openmc-update-inputs

299 lines
9.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""Update OpenMC's input XML files to the latest format.
"""
import argparse
from itertools import chain
from random import randint
from shutil import move
import xml.etree.ElementTree as ET
import openmc.data
description = "Update OpenMC's input XML files to the latest format."
epilog = """\
If any of the given files do not match the most up-to-date formatting, then they
will be automatically rewritten. The old out-of-date files will not be deleted;
they will be moved to a new file with '.original' appended to their name.
Formatting changes that will be made:
geometry.xml: Lattices containing 'outside' attributes/tags will be replaced
with lattices containing 'outer' attributes, and the appropriate
cells/universes will be added. Any 'surfaces' attributes/elements on a cell
will be renamed 'region'.
materials.xml: Nuclide names will be changed from ACE aliases (e.g., Am-242m) to
HDF5/GNDS names (e.g., Am242_m1). Thermal scattering table names will be
changed from ACE aliases (e.g., HH2O) to HDF5/GNDS names (e.g., c_H_in_H2O).
"""
def parse_args():
"""Read the input files from the commandline."""
# Create argument parser.
parser = argparse.ArgumentParser(
description=description,
epilog=epilog,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('input', metavar='IN', type=str, nargs='+',
help='Input XML file(s).')
# Parse and return commandline arguments.
return parser.parse_args()
def get_universe_ids(geometry_root):
"""Return a set of universe id numbers."""
root = geometry_root
out = set()
# Get the ids of universes defined by cells.
for cell in root.iter('cell'):
# Get universe attributes/elements
if 'universe' in cell.attrib:
uid = cell.attrib['universe']
out.add(int(uid))
elif cell.find('universe') is not None:
elem = cell.find('universe')
uid = elem.text
out.add(int(uid))
else:
# Default to universe 0
out.add(0)
# Get the ids of universes defined by lattices.
for lat in root.iter('lattice'):
# Get id attributes.
if 'id' in lat.attrib:
uid = lat.attrib['id']
out.add(int(uid))
# Get id elements.
elif lat.find('id') is not None:
elem = lat.find('id')
uid = elem.text
out.add(int(uid))
return out
def get_cell_ids(geometry_root):
"""Return a set of cell id numbers."""
root = geometry_root
out = set()
# Get the ids of universes defined by cells.
for cell in root.iter('cell'):
# Get id attributes.
if 'id' in cell.attrib:
cid = cell.attrib['id']
out.add(int(cid))
# Get id elements.
elif cell.find('id') is not None:
elem = cell.find('id')
cid = elem.text
out.add(int(cid))
return out
def find_new_id(current_ids, preferred=None):
"""Return a new id that is not already present in current_ids."""
distance_from_preferred = 21
max_random_attempts = 10000
# First, try to find an id near the preferred number.
if preferred is not None:
assert isinstance(preferred, int)
for i in range(1, distance_from_preferred):
if (preferred - i not in current_ids) and (preferred - i > 0):
return preferred - i
if (preferred + i not in current_ids) and (preferred + i > 0):
return preferred + i
# If that was unsuccessful, attempt to randomly guess a new id number.
for i in range(max_random_attempts):
num = randint(1, 2147483647)
if num not in current_inds:
return num
# Raise an error if an id was not found.
raise RuntimeError('Could not find a unique id number for a new universe.')
def get_lat_id(lattice_element):
"""Return the id integer of the lattice_element."""
assert isinstance(lattice_element, ET.Element)
if 'id' in lattice_element.attrib:
return int(lattice_element.attrib['id'].strip())
elif any([child.tag == 'id' for child in lattice_element]):
elem = lattice_element.find('id')
return int(elem.text.strip())
else:
raise RuntimeError('Could not find the id for a lattice.')
def pop_lat_outside(lattice_element):
"""Return lattice's outside material and remove from attributes/elements."""
assert isinstance(lattice_element, ET.Element)
# Check attributes.
if 'outside' in lattice_element.attrib:
material = lattice_element.attrib['outside'].strip()
del lattice_element.attrib['outside']
# Check subelements.
elif any([child.tag == 'outside' for child in lattice_element]):
elem = lattice_element.find('outside')
material = elem.text.strip()
lattice_element.remove(elem)
# No 'outside' specified. This means the outside is a void.
else:
material = 'void'
return material
def update_geometry(geometry_root):
"""Update the given XML geometry tree. Return True if changes were made."""
root = geometry_root
was_updated = False
# Get a set of already-used universe and cell ids.
uids = get_universe_ids(root)
cids = get_cell_ids(root)
taken_ids = uids.union(cids)
# Replace 'outside' with 'outer' in lattices.
for lat in chain(root.iter('lattice'), root.iter('hex_lattice')):
# Get the lattice's id.
lat_id = get_lat_id(lat)
# Ignore lattices that have 'outer' specified.
if any([child.tag == 'outer' for child in lat]): continue
if 'outer' in lat.attrib: continue
# Pop the 'outside' material.
material = pop_lat_outside(lat)
# Get an id number for a new outer universe. Ideally, the id should
# be close to the lattice's id.
new_uid = find_new_id(taken_ids, preferred=lat_id)
assert new_uid not in taken_ids
# Add the new universe filled with the old 'outside' material to the
# geometry.
new_cell = ET.Element('cell')
new_cell.attrib['id'] = str(new_uid)
new_cell.attrib['universe'] = str(new_uid)
new_cell.attrib['material'] = material
root.append(new_cell)
taken_ids.add(new_uid)
# Add the new universe to the lattice's 'outer' attribute.
lat.attrib['outer'] = str(new_uid)
was_updated = True
# Remove 'type' from lattice definitions.
for lat in root.iter('lattice'):
elem = lat.find('type')
if elem is not None:
lat.remove(elem)
was_updated = True
if 'type' in lat.attrib:
del lat.attrib['type']
was_updated = True
# Change 'width' to 'pitch' in lattice definitions.
for lat in root.iter('lattice'):
elem = lat.find('width')
if elem is not None:
elem.tag = 'pitch'
was_updated = True
if 'width' in lat.attrib:
lat.attrib['pitch'] = lat.attrib['width']
del lat.attrib['width']
was_updated = True
# Change 'surfaces' to 'region' in cell definitions
for cell in root.iter('cell'):
elem = cell.find('surfaces')
if elem is not None:
elem.tag = 'region'
was_updated = True
if 'surfaces' in cell.attrib:
cell.attrib['region'] = cell.attrib['surfaces']
del cell.attrib['surfaces']
was_updated = True
return was_updated
def update_materials(root):
"""Update the given XML materials tree. Return True if changes were made."""
was_updated = False
for material in root.findall('material'):
for nuclide in material.findall('nuclide'):
if 'name' in nuclide.attrib:
nucname = nuclide.attrib['name'].replace('-', '')
# If a nuclide name is in the ZAID notation (e.g., a number),
# convert it to the proper nuclide name.
if nucname.strip().isnumeric():
nucname = openmc.data.ace.get_metadata(int(nucname))[0]
nucname = nucname.replace('Nat', '0')
if nucname.endswith('m'):
nucname = nucname[:-1] + '_m1'
nuclide.set('name', nucname)
was_updated = True
elif nuclide.find('name') is not None:
name_elem = nuclide.find('name')
nucname = name_elem.text
nucname = nucname.replace('-', '')
nucname = nucname.replace('Nat', '0')
if nucname.endswith('m'):
nucname = nucname[:-1] + '_m1'
name_elem.text = nucname
was_updated = True
for sab in material.findall('sab'):
if 'name' in sab.attrib:
sabname = sab.attrib['name']
sab.set('name', openmc.data.get_thermal_name(sabname))
was_updated = True
elif sab.find('name') is not None:
name_elem = sab.find('name')
sabname = name_elem.text
name_elem.text = openmc.data.get_thermal_name(sabname)
was_updated = True
return was_updated
if __name__ == '__main__':
args = parse_args()
for fname in args.input:
# Parse the XML data.
tree = ET.parse(fname)
root = tree.getroot()
was_updated = False
if root.tag == 'geometry':
was_updated = update_geometry(root)
elif root.tag == 'materials':
was_updated = update_materials(root)
if was_updated:
# Move the original geometry file to preserve it.
move(fname, fname + '.original')
# Write a new geometry file.
tree.write(fname, xml_declaration=True)