diff --git a/smr/build-assembly-long.py b/smr/build-assembly-long.py index 79734c3..623907c 100644 --- a/smr/build-assembly-long.py +++ b/smr/build-assembly-long.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse -import copy from math import pi, isclose from pathlib import Path @@ -9,10 +8,9 @@ import numpy as np from tqdm import tqdm import openmc -from smr.materials import materials, mats +from smr.materials import materials, clone from smr.surfaces import surfs, lattice_pitch, pin_pitch, bottom_fuel_stack, \ - top_active_core, pellet_OR, clad_OR, clad_IR, guide_tube_IR, guide_tube_OR, \ - active_fuel_length + top_active_core, pellet_OR, active_fuel_length from smr.pins import pin_universes, make_stack @@ -20,14 +18,18 @@ from smr.pins import pin_universes, make_stack parser = argparse.ArgumentParser() parser.add_argument('--multipole', action='store_true', help='Use multipole cross sections') -parser.add_argument('--no-multipole', action='store_false', +parser.add_argument('--no-multipole', dest='multipole', action='store_false', help='Do not use multipole cross sections') +parser.add_argument('--clone', action='store_true', + help='Clone materials for each cell instance') +parser.add_argument('--no-clone', dest='clone', action='store_false', + help='Do not clone materials for each cell instance') parser.add_argument('-a', '--axial', type=int, default=100, help='Number of axial subdivisions in fuel') parser.add_argument('-d', '--depleted', action='store_true', help='Whether UO2 compositions should represent depleted fuel') parser.add_argument('-o', '--output-dir', type=Path, default=None) -parser.set_defaults(multipole=True) +parser.set_defaults(clone=False, multipole=True) args = parser.parse_args() # Make directory for inputs @@ -101,11 +103,22 @@ h = active_fuel_length / args.axial fuel_mats = {} +# Count the number of instances for each cell and material +if args.clone: + geometry.determine_paths(instances_only=True) + for cell in tqdm(geometry.get_all_material_cells().values(), - desc='Assigning volume'): + desc='Differentiating materials / assigning volume'): if cell.fill in materials: + # Determine if this material is fuel + is_fuel = 'UO2 Fuel' in cell.fill.name + + # Fill cell with list of "differentiated" materials if requested + if args.clone: + cell.fill = [clone(cell.fill) for i in range(cell.num_instances)] + # Determine volume of each fuel material - if 'UO2 Fuel' in cell.fill.name: + if is_fuel: upper_right = cell.region.bounding_box[1] if isclose(upper_right[0], rings[0]): ri, ro = 0.0, rings[0] @@ -113,14 +126,25 @@ for cell in tqdm(geometry.get_all_material_cells().values(), ri, ro = rings[0], rings[1] else: ri, ro = rings[1], pellet_OR - if ri not in fuel_mats: - cell.fill = cell.fill.clone() - cell.fill.volume = pi * (ro*ro - ri*ri) * h - fuel_mats[ri] = cell.fill + + if args.clone: + for mat in cell.fill: + mat.volume = pi * (ro*ro - ri*ri) * h else: - cell.fill = fuel_mats[ri] + # In non-clone mode, we still need to create a copy of the + # material for each ring since they get different volumes + if ri not in fuel_mats: + cell.fill = cell.fill.clone() + cell.fill.volume = pi * (ro*ro - ri*ri) * h + fuel_mats[ri] = cell.fill + else: + cell.fill = fuel_mats[ri] else: - cell.fill.volume = 1.0 + if args.clone: + for mat in cell.fill: + mat.volume = 1.0 + else: + cell.fill.volume = 1.0 #### Create OpenMC "materials.xml" file print('Getting materials...') @@ -150,14 +174,13 @@ settings.particles = 10000 settings.output = {'tallies': False, 'summary': False} settings.source = source settings.sourcepoint = {'write': False} - -if args.multipole: - settings.temperature = { - 'multipole': True, - 'tolerance': 1000, - 'default': 531.5, - 'method': 'interpolation', +settings.temperature = { + 'default': 531.5, + 'method': 'interpolation', 'range': (500.0, 1300.0) - } +} +if args.multipole: + settings.temperature['multipole'] = True + settings.temperature['tolerance'] = 1000 settings.export_to_xml(str(directory / 'settings.xml')) diff --git a/smr/build-assembly-short.py b/smr/build-assembly-short.py index c06c67b..dc13456 100644 --- a/smr/build-assembly-short.py +++ b/smr/build-assembly-short.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse -import copy from math import pi, isclose from pathlib import Path @@ -9,7 +8,7 @@ import numpy as np from tqdm import tqdm import openmc -from smr.materials import materials, mats +from smr.materials import materials, clone from smr.surfaces import surfs, lattice_pitch, pin_pitch, bottom_fuel_stack, \ top_active_core, pellet_OR, clad_OR, clad_IR, guide_tube_IR, guide_tube_OR from smr.pins import pin_universes @@ -85,14 +84,6 @@ for halfspace in surfs['lat grid box inner']: # Define geometry with a single assembly geometry = openmc.Geometry(root_universe) - -def clone(material): - """Perform copy of material but share nuclide densities""" - shared_mat = copy.copy(material) - shared_mat.id = None - return shared_mat - - #### "Differentiate" the geometry if using distribmats h = 10.0*pin_pitch / args.axial if args.tallies == 'mat': diff --git a/smr/build-assembly.py b/smr/build-assembly.py index 392b114..d4bc166 100644 --- a/smr/build-assembly.py +++ b/smr/build-assembly.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 import argparse -import copy from pathlib import Path import numpy as np from tqdm import tqdm import openmc -from smr.materials import materials +from smr.materials import materials, clone from smr.surfaces import surfs, lattice_pitch, bottom_fuel_stack, top_active_core, pellet_OR from smr.assemblies import assembly_universes -from smr.plots import assembly_plots from smr import inlet_temperature @@ -19,8 +17,12 @@ from smr import inlet_temperature parser = argparse.ArgumentParser() parser.add_argument('--multipole', action='store_true', help='Use multipole cross sections') -parser.add_argument('--no-multipole', action='store_false', +parser.add_argument('--no-multipole', dest='multipole', action='store_false', help='Do not use multipole cross sections') +parser.add_argument('--clone', action='store_true', + help='Clone materials for each cell instance') +parser.add_argument('--no-clone', dest='clone', action='store_false', + help='Do not clone materials for each cell instance') parser.add_argument('-t', '--tallies', choices=('cell', 'mat'), default='mat', help='Whether to use distribmats or distribcells for tallies') parser.add_argument('-r', '--rings', type=int, default=10, @@ -30,7 +32,7 @@ parser.add_argument('-a', '--axial', type=int, default=196, parser.add_argument('-d', '--depleted', action='store_true', help='Whether UO2 compositions should represent depleted fuel') parser.add_argument('-o', '--output-dir', type=Path, default=None) -parser.set_defaults(multipole=True) +parser.set_defaults(clone=False, multipole=True) args = parser.parse_args() # Make directory for inputs @@ -49,25 +51,17 @@ if args.rings > 1: else: ring_radii = None assembly = assembly_universes(ring_radii, args.axial, args.depleted) -lattice_sides = openmc.model.get_rectangular_prism(lattice_pitch, lattice_pitch, - boundary_type='reflective') +lattice_sides = openmc.model.rectangular_prism(lattice_pitch, lattice_pitch, + boundary_type='reflective') main_cell = openmc.Cell( - fill=assembly['Assembly (3.1%) 16BA'], + fill=assembly['Assembly (3.1%)'], region=lattice_sides & +surfs['lower bound'] & -surfs['upper bound'] ) root_univ = openmc.Universe(cells=[main_cell]) geometry = openmc.Geometry(root_univ) - -def clone(material): - """Perform copy of material but share nuclide densities""" - shared_mat = copy.copy(material) - shared_mat.id = None - return shared_mat - - #### "Differentiate" the geometry if using distribmats -if args.tallies == 'mat': +if args.clone: # Count the number of instances for each cell and material geometry.determine_paths(instances_only=True) @@ -107,7 +101,7 @@ settings.inactive = 100 settings.particles = 10000 settings.output = {'tallies': False, 'summary': False} settings.source = source -settings.sourcepoint_write = False +settings.sourcepoint = {'write': False} settings.temperature = { 'default': inlet_temperature, 'method': 'interpolation', @@ -149,7 +143,3 @@ elif args.tallies == 'mat': tallies.append(tally) tallies.export_to_xml(str(directory / 'tallies.xml')) - -# Create plots -plots = assembly_plots(main_cell.fill) -plots.export_to_xml(str(directory / 'plots.xml')) diff --git a/smr/build-core-fresh.py b/smr/build-core-fresh.py index 4265056..9747ae8 100644 --- a/smr/build-core-fresh.py +++ b/smr/build-core-fresh.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 -import os -import shutil -import copy import argparse from math import pi from pathlib import Path import numpy as np - import openmc -from smr.materials import materials -from smr.plots import core_plots +from tqdm import tqdm + +from smr.materials import materials, clone from smr.surfaces import lattice_pitch, bottom_fuel_stack, top_active_core, \ pellet_OR, active_fuel_length from smr.core import core_geometry @@ -22,8 +19,12 @@ from smr import inlet_temperature parser = argparse.ArgumentParser() parser.add_argument('--multipole', action='store_true', help='Use multipole cross sections') -parser.add_argument('--no-multipole', action='store_false', +parser.add_argument('--no-multipole', dest='multipole', action='store_false', help='Do not use multipole cross sections') +parser.add_argument('--clone', action='store_true', + help='Clone materials for each cell instance') +parser.add_argument('--no-clone', dest='clone', action='store_false', + help='Do not clone materials for each cell instance') parser.add_argument('-r', '--rings', type=int, default=10, help='Number of annular regions in fuel') parser.add_argument('-a', '--axial', type=int, default=196, @@ -31,7 +32,7 @@ parser.add_argument('-a', '--axial', type=int, default=196, parser.add_argument('-d', '--depleted', action='store_true', help='Whether UO2 compositions should represent depleted fuel') parser.add_argument('-o', '--output-dir', type=Path, default=None) -parser.set_defaults(multipole=True) +parser.set_defaults(clone=False, multipole=True) args = parser.parse_args() # Make directory for inputs @@ -53,22 +54,36 @@ geometry = core_geometry(ring_radii, args.axial, args.depleted) h = active_fuel_length / args.axial fuel_mats = {} +# Count the number of instances for each cell and material +if args.clone: + geometry.determine_paths(instances_only=True) + fuel_volume = pi * pellet_OR**2 * h / args.rings -for cell in geometry.get_all_cells().values(): +for cell in tqdm(geometry.get_all_cells().values(), + desc='Differentiating materials / assigning volume'): if cell.fill in materials: + # Determine if this material is fuel + name = cell.fill.name + is_fuel = 'UO2 Fuel' in name + # Determine volume of each fuel material - if 'UO2 Fuel' in cell.fill.name: - r_o = cell.region.bounding_box[1][0] - if r_o not in fuel_mats: - cell.fill = cell.fill.clone() - cell.fill.volume = fuel_volume - fuel_mats[r_o] = cell.fill + if is_fuel: + if args.clone: + # Fill cell with list of "differentiated" materials if requested + cell.fill = [clone(cell.fill) for i in range(cell.num_instances)] + for mat in cell.fill: + mat.volume = fuel_volume else: - cell.fill = fuel_mats[r_o] + r_o = cell.region.bounding_box[1][0] + if (name, r_o) not in fuel_mats: + cell.fill = cell.fill.clone() + cell.fill.volume = fuel_volume + fuel_mats[name, r_o] = cell.fill + else: + cell.fill = fuel_mats[name, r_o] else: cell.fill.volume = 1.0 - #### Create OpenMC "materials.xml" file all_materials = geometry.get_all_materials() materials = openmc.Materials(all_materials.values()) @@ -93,8 +108,7 @@ settings.inactive = 100 settings.particles = 10000 settings.output = {'tallies': False, 'summary': False} settings.source = source -settings.sourcepoint_write = False - +settings.sourcepoint = {'write': False} settings.temperature = { 'default': inlet_temperature, 'method': 'interpolation', @@ -105,4 +119,3 @@ if args.multipole: settings.temperature['tolerance'] = 1000 settings.export_to_xml(str(directory / 'settings.xml')) - diff --git a/smr/build-core-long.py b/smr/build-core-long.py new file mode 100644 index 0000000..4033d6e --- /dev/null +++ b/smr/build-core-long.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +import argparse +from math import pi, isclose +from pathlib import Path + +import openmc +from smr.materials import materials +from smr.surfaces import bottom_fuel_stack, top_active_core, \ + pellet_OR, pin_pitch, clad_IR, clad_OR, active_fuel_length +from smr.core import core_geometry +from smr import inlet_temperature +import smr.surfaces + +# Define command-line options +parser = argparse.ArgumentParser() +parser.add_argument('--multipole', action='store_true', + help='Use multipole cross sections') +parser.add_argument('--no-multipole', dest='multipole', action='store_false', + help='Do not use multipole cross sections') +parser.add_argument('-a', '--axial', type=int, default=100, + help='Number of axial subdivisions in fuel') +parser.add_argument('-d', '--depleted', action='store_true', + help='Whether UO2 compositions should represent depleted fuel') +parser.add_argument('-o', '--output-dir', type=Path, default=None) +parser.set_defaults(multipole=True) +args = parser.parse_args() + +# Make directory for inputs +if args.output_dir is None: + if args.depleted: + directory = Path('core-long-depleted') + else: + directory = Path('core-long-fresh') +else: + directory = args.output_dir +directory.mkdir(exist_ok=True) + +# Modify lattice pitch +smr.surfaces.lattice_pitch = lattice_pitch = 17*smr.surfaces.pin_pitch + +ring_radii = [0.1*pin_pitch, 0.2*pin_pitch] + +geometry = core_geometry(ring_radii, args.axial, args.depleted) + +h = active_fuel_length / args.axial +fuel_mats = {} + +for cell in geometry.get_all_cells().values(): + if cell.fill in materials: + # Determine volume of each fuel material + name = cell.fill.name + if 'UO2 Fuel' in name: + upper_right = cell.region.bounding_box[1][0] + if isclose(upper_right, ring_radii[0]): + ri, ro = 0.0, ring_radii[0] + elif isclose(upper_right, ring_radii[1]): + ri, ro = ring_radii[0], ring_radii[1] + else: + ri, ro = ring_radii[1], pellet_OR + if (name, ri) not in fuel_mats: + cell.fill = cell.fill.clone() + cell.fill.volume = pi * (ro*ro - ri*ri) * h + fuel_mats[name, ri] = cell.fill + else: + cell.fill = fuel_mats[name, ri] + elif name == 'Helium': + cell.fill.volume = pi * (clad_IR**2 - pellet_OR**2) * h + elif name == 'M5': + # Clad is not subdivided + cell.fill.volume = pi * (clad_OR**2 - clad_IR**2) * active_fuel_length + else: + cell.fill.volume = 1.0 + + +#### Create OpenMC "materials.xml" file +all_materials = geometry.get_all_materials() +materials = openmc.Materials(all_materials.values()) +materials.export_to_xml(str(directory / 'materials.xml')) + +#### Create OpenMC "geometry.xml" file +geometry.export_to_xml(str(directory / 'geometry.xml')) + + +#### Create OpenMC "settings.xml" file + +# Construct uniform initial source distribution over fissionable zones +lower_left = [-7.*lattice_pitch/2., -7.*lattice_pitch/2., bottom_fuel_stack] +upper_right = [+7.*lattice_pitch/2., +7.*lattice_pitch/2., top_active_core] +source = openmc.source.Source(space=openmc.stats.Box(lower_left, upper_right)) +source.space.only_fissionable = True + +settings = openmc.Settings() +settings.batches = 200 +settings.inactive = 100 +settings.particles = 20_000_000 +settings.output = {'tallies': False, 'summary': False} +settings.source = source +settings.sourcepoint = {'write': False} +settings.temperature = { + 'default': inlet_temperature, + 'method': 'interpolation', + 'range': (300.0, 1500.0), +} +if args.multipole: + settings.temperature['multipole'] = True + settings.temperature['tolerance'] = 1000 + +settings.export_to_xml(str(directory / 'settings.xml')) + +# Check assembly power distribution +core_lattice = geometry.get_cells_by_fill_name('Main core')[0].fill +mesh = openmc.RegularMesh.from_rect_lattice(core_lattice) +assembly_power = openmc.Tally() +assembly_power.filters = [openmc.MeshFilter(mesh)] +assembly_power.scores = ['nu-fission'] +tallies = openmc.Tallies([assembly_power]) +tallies.export_to_xml(directory / 'tallies.xml') diff --git a/smr/build-core-short.py b/smr/build-core-short.py index 317a8a0..2f5339d 100644 --- a/smr/build-core-short.py +++ b/smr/build-core-short.py @@ -1,25 +1,18 @@ #!/usr/bin/env python3 -import os -import shutil -import copy import argparse from math import pi, isclose from pathlib import Path -import numpy as np - import openmc from smr.materials import materials -from smr.plots import core_plots -from smr.surfaces import lattice_pitch, bottom_fuel_stack, top_active_core, \ +from smr.surfaces import bottom_fuel_stack, top_active_core, \ pellet_OR, surfs, pin_pitch, clad_IR, clad_OR import smr.surfaces import smr.pins from smr.core import core_geometry from smr import inlet_temperature - # Define command-line options parser = argparse.ArgumentParser() parser.add_argument('--multipole', action='store_true', @@ -44,6 +37,9 @@ else: directory = args.output_dir directory.mkdir(exist_ok=True) +# Modify lattice pitch +smr.surfaces.lattice_pitch = lattice_pitch = 17*smr.surfaces.pin_pitch + # Modify fuel length length = 3. * pin_pitch smr.surfaces.active_fuel_length = length @@ -113,8 +109,7 @@ settings.inactive = 100 settings.particles = 10000 settings.output = {'tallies': False, 'summary': False} settings.source = source -settings.sourcepoint_write = False - +settings.sourcepoint = {'write': False} settings.temperature = { 'default': inlet_temperature, 'method': 'interpolation', diff --git a/smr/milestones.md b/smr/milestones.md new file mode 100644 index 0000000..bb89864 --- /dev/null +++ b/smr/milestones.md @@ -0,0 +1,20 @@ +# Milestone Models + +- **AD-SE-08-61, Coupled Multiphysics Driver Implementation** --- This milestone + used the singlerod short/long problems. Generating the model was done with the + script `tests/singlerod/make_openmc_model.py` from the ENRICO repository + (there is a `--short` command line option to generate the short version) + +- **AD-SE-08-66, Coupled Assembly Analysis** --- This milestone used the + assembly short and long (v2) problems. Generating the model was done with + `smr/build-assembly-long.py -a 100 --clone` on the ecp-benchmarks repository + (git commit `631fefe`, after pull request #14). In these models, materials are + fully differentiated across each fuel ring/axial segment. + +- **AD-SE-08-73, Full core coupled-physics simulation** --- This milestone used + the core-short and core-long (90 layer) models. Generating the models was done + with `smr/build-core-short.py` and `smr/build-assembly-long.py -a 90` on the + ecp-benchmarks repository (git commit `c7b89db`, after pull request #16). In + these models, materials are not differentiated and no grid spacers are + present. The lattice pitch is modified to be exactly 17 times the pin pitch + (slightly different than NuScale specification). diff --git a/smr/smr/core.py b/smr/smr/core.py index 68f3275..81763fe 100644 --- a/smr/smr/core.py +++ b/smr/smr/core.py @@ -5,9 +5,9 @@ import numpy as np import openmc from .materials import mats -from .surfaces import surfs, lattice_pitch from .reflector import reflector_universes from .assemblies import assembly_universes +from smr import surfaces def core_geometry(ring_radii, num_axial, depleted): @@ -34,6 +34,7 @@ def core_geometry(ring_radii, num_axial, depleted): # Construct main core lattice core = openmc.RectLattice(name='Main core') + lattice_pitch = surfaces.lattice_pitch core.lower_left = (-9*lattice_pitch/2, -9*lattice_pitch/2) core.pitch = (lattice_pitch, lattice_pitch) universes = np.tile(reflector['solid'], (9, 9)) @@ -119,6 +120,7 @@ def core_geometry(ring_radii, num_axial, depleted): core.universes = universes root_univ = openmc.Universe(universe_id=0, name='root universe') + surfs = surfaces.surfs # Cylinder filled with core lattice cell = openmc.Cell(name='Main core') diff --git a/smr/smr/materials.py b/smr/smr/materials.py index 72a39d6..ee6c754 100644 --- a/smr/smr/materials.py +++ b/smr/smr/materials.py @@ -1,5 +1,7 @@ """Instantiate the OpenMC Materials needed by the core model.""" +import copy + import openmc from openmc.data import atomic_weight, atomic_mass, water_density @@ -252,3 +254,10 @@ mats['UO2 3.1 depleted'] = mat # Construct a collection of Materials to export to XML materials = openmc.Materials(mats.values()) + + +def clone(material): + """Perform copy of material but share nuclide densities""" + shared_mat = copy.copy(material) + shared_mat.id = None + return shared_mat diff --git a/smr/smr/reflector.py b/smr/smr/reflector.py index ed143cc..74aa4bd 100644 --- a/smr/smr/reflector.py +++ b/smr/smr/reflector.py @@ -12,7 +12,7 @@ converted to actual dimensions by scaling according to the width of an assembly. import openmc from .materials import mats -from .surfaces import lattice_pitch +from smr import surfaces def make_reflector(name, parameters): @@ -91,6 +91,7 @@ def reflector_universes(): # All pixel widths are scaled according to the actual width of an assembly # divided by the width of an assembly in pixels + lattice_pitch = surfaces.lattice_pitch scale = lattice_pitch/width # Physical positions diff --git a/smr/smr/surfaces.py b/smr/smr/surfaces.py index d91ec92..b454c61 100644 --- a/smr/smr/surfaces.py +++ b/smr/smr/surfaces.py @@ -69,8 +69,7 @@ spacer_height = 1.750*INCHES # ML17013A274, Figure 4.2-7 # assembly parameters assembly_length = 95.89*INCHES # ML17013A274, Table 4.1-2 pin_pitch = 0.496*INCHES # ML17013A274, Table 4.1-2 -#lattice_pitch = 8.466*INCHES # ML17013A274, Table 4.1-2 -lattice_pitch = 17*pin_pitch +lattice_pitch = 8.466*INCHES # ML17013A274, Table 4.1-2 grid_strap_side = 21.47270 top_nozzle_height = 3.551*INCHES # ML17013A274, Figure 4.2-2 top_nozzle_width = 8.406*INCHES # ML17013A274, Figure 4.2-2