Overreporting Counts with Pulse Height Tally for Cs137 Point Source & 2x2 NaI

Hello,

I have been trying to simulate a 2"x2" NaI detector and Cs137 point source. The goal is to obtain the same amount of counts under the curve as the real measurements. However, my simulation results are too high (no consistent scaling factor).

For the real case, I used Genie 2K and obtained the counts by setting a region of interest over the entire curve. The background was 46 cpm and dead time was negligible (<1%).

The source used was a Cs-137 check source of initially 9383 Bq (July 1 2013), measured to be 7215 Bq at the time of the experiment.

I then placed the source at distances of 2.5, 5, and 10 cm from the detector, which was set standing up on the table. A 0 cm measurement was taken with the check source placed underneath the detector. Note that this value I am less concerned over, as the point source in the simulation has been placed directly against the detector, whereas the real check source has some thickness. Thus, the results for this case may not be representative of the real case.

V0.15.0 of OpenMC was used. The OpenMC model (pasted below) is kept as simple as can be, modelling only air, the detector crystal, and a thin aluminum casing on the walls only (with a large, r=20, out-of-bounds sphere surrounding it all). The source is an isotropic point source moved along the X-axis away from the detector wall.

The source strength is found in the manner used in the gamma detector example, with the mass set to ~2.24 ng (found by comparing the specific activity of 3.2 TBq/g to the known activity of 7215 Bq). At runtime, this calculates a strength of 6665 Bq.

The tally uses an energy filter as described in the same example, with the 0-1 keV bin cut out. The “pulse-height” score is used and the counts obtained from the get_reshaped_data sum. I am not sure if this is the correct method, as in this portion I’m aping what I’ve seen in other examples.

The results are as follows:

Case Net Real (cpm) Simulated (cpm)
Contact 239 659
2.5 cm 72 92
5 cm 24 46
10 cm 12 18

The main questions I have are:

  1. Am I misusing the pulse-height tally? If so, is there a more appropriate tally for comparing the counts?
  2. Am I retrieving the tally data correctly for purposes of finding counts?
  3. Are there any limits of OpenMC that I need to consider in post-processing, e.g., intrinsic efficiency (as there can be no modelling of detector electronics)?

Thank you for your time.


CODE

# Detector: 2x2 NaI
# Source: Cs-137, 7215 Bq (originally 9383 Bq on July 1 2013)
# OpenMC Version 0.15.0
# IMPORTS
import openmc as omc
import numpy as np
from scipy.constants import Avogadro


# FUNCTIONS
def write_strings_to_txt(stringlist, filename, dir_dest, append=False):
    mode = 'w'
    if append:
        mode = 'a'
    with open(dir_dest + filename, mode) as f:
        f.write('\n'.join(stringlist))


# MATERIALS
FRACTION_ATOMIC = "ao"

mat_air = omc.Material(name="air_dry_near_sea_level_noC")
mat_air.set_density("g/cm3", 0.001205)
mat_air.add_element('C', 0.000150, percent_type=FRACTION_ATOMIC)
mat_air.add_element('N', 0.784431, percent_type=FRACTION_ATOMIC)
mat_air.add_element('O', 0.210748, percent_type=FRACTION_ATOMIC)
mat_air.add_element('Ar', 0.004671, percent_type=FRACTION_ATOMIC)

mat_NaI = omc.Material(name="NaI")
mat_NaI.set_density("g/cm3", 3.667)
mat_NaI.add_element('Na', 0.499999, percent_type=FRACTION_ATOMIC)
mat_NaI.add_element('I', 0.500001, percent_type=FRACTION_ATOMIC)

aluminum = omc.Material(name="aluminum")
aluminum.add_element('Al', 1)
aluminum.set_density('g/cm3', 2.7)

materials = omc.Materials([mat_air, mat_NaI, aluminum])
materials.cross_sections = r"/home/USER/endfb-viii.0-hdf5/cross_sections.xml"
materials.export_to_xml()

# MEASUREMENTS
IN_TO_CM = 2.54
meas_detector_radius = 1 * IN_TO_CM
meas_detector_height = 2 * IN_TO_CM

# POSITIONS
posz_detector_bottom = 0
posz_detector_top = posz_detector_bottom + meas_detector_height

# GEOMETRY
surf_detector_cyl = omc.ZCylinder(r=meas_detector_radius, x0=0, y0=0)
surf_detector_bottom = omc.ZPlane(z0=posz_detector_bottom)
surf_detector_top = omc.ZPlane(z0=posz_detector_top)
surf_oob_sphere = omc.Sphere(r=20, x0=0, y0=0, z0=0, boundary_type="vacuum")
surf_casing_cyl_outer = omc.ZCylinder(r=meas_detector_radius+0.05, x0=0, y0=0)

region_detector = -surf_detector_cyl & +surf_detector_bottom & -surf_detector_top
region_casing = +surf_detector_cyl & -surf_casing_cyl_outer & +surf_detector_bottom & -surf_detector_top
region_air = -surf_oob_sphere & ~region_detector & ~region_casing
region_oob = +surf_oob_sphere

cell_detector = omc.Cell(fill=mat_NaI, region=region_detector, name="TARGET")
cell_casing = omc.Cell(fill=aluminum, region=region_casing, name="casing")
cell_air = omc.Cell(fill=mat_air, region=region_air)
cell_oob = omc.Cell(region=region_oob)
cell_list = [cell_detector, cell_casing, cell_air, cell_oob]

universe = omc.Universe(cells=cell_list)

geometry = omc.Geometry(universe)
geometry.export_to_xml()

# Plot Geometry
plot_width = 20
plot_pixel_resolution = (400, 400)

view_xy = "xy"
view_xz = "xz"
view_yz = "yz"

viewlist = [view_xy, view_xz, view_yz]

plot_color_by_material = "material"
DictColors = {mat_air: (255, 255, 255), mat_NaI: (177, 156, 217), aluminum: (100, 100, 100)}

plotslist = []

for viewindx in range(len(viewlist)):
    plotname = "geometryview_" + viewlist[viewindx]
    plot = omc.Plot(name=plotname)
    plot.width = (plot_width, plot_width)
    plot.pixels = plot_pixel_resolution
    plot.color_by = plot_color_by_material
    plot.colors = DictColors
    plot.basis = viewlist[viewindx]
    plot.filename = plotname

    plot.origin = (0, 0, 0)

    plotslist.append(plot)

# Export Plots
plots = omc.Plots(plotslist)
plots.export_to_xml()

# Create Images
omc.plot_geometry()



# SIMULATE
distances = [0, 2.5+meas_detector_radius, 5+meas_detector_radius, 10+meas_detector_radius]  # Placed outside surface
for x in range(len(distances)):
    meas_source_distance = distances[x]
    omc.config['chain_file'] = "/home/USER/PycharmProjects/Cs137_PointSource_Test/chain_simple.xml"
    energy_bins = np.linspace(0, 1e6, 1001)[1:]  # Cut off the first bin to avoid no-energy cases


    half_life_Cs137_seconds = 30.17 * 365 * 24 * 60 * 60
    half_life_Ba137_m1_seconds = 2.55 * 60
    branching_ratio = 0.946
    fraction_atoms_Ba137_m1 = (half_life_Ba137_m1_seconds * branching_ratio) / half_life_Cs137_seconds

    number_atoms_Ba137_m1 = (2.244168*10**-9 / 137) * Avogadro * fraction_atoms_Ba137_m1
    number_atoms_Cs137 = (2.244168*10**-9 / 137) * Avogadro * (1 - fraction_atoms_Ba137_m1)

    cs137_strength = np.sum(omc.data.decay_photon_energy("Cs137").p) * number_atoms_Cs137
    ba137_strength = np.sum(omc.data.decay_photon_energy("Ba137_m1").p) * number_atoms_Ba137_m1  # 7264.53 >  Secular equilibrium

    sourceBa = omc.IndependentSource(angle=omc.stats.Isotropic())
    sourceBa.space = omc.stats.Point((meas_source_distance, 0, 0))

    sourceBa.energy = omc.data.decay_photon_energy("Ba137_m1")
    sourceBa.strength = ba137_strength
    sourceBa.particle = "photon"

    sourceCs = omc.IndependentSource(angle=omc.stats.Isotropic())
    sourceCs.space = omc.stats.Point((meas_source_distance, 0, 0))
    sourceCs.energy = omc.data.decay_photon_energy("Cs137")
    sourceCs.strength = cs137_strength
    sourceCs.particle = "photon"

    settings = omc.Settings()
    settings.batches = 5
    settings.particles = 10**5
    settings.inactive = 0
    settings.photon_transport = True
    settings.run_mode = "fixed source"
    settings.source = [sourceBa, sourceCs]

    cell_filter = omc.CellFilter(cell_detector)
    energy_filter = omc.EnergyFilter(energy_bins)
    # photon_particle_filter = omc.ParticleFilter("photon")
    name_tally = "pulse-height"
    cell_tally = omc.Tally(name=name_tally)
    cell_tally.filters = [cell_filter, energy_filter]
    cell_tally.scores = [name_tally]
    tallies = omc.Tallies([cell_tally])
    tallies.export_to_xml()

    model = omc.Model(geometry, materials, settings, tallies)
    statepoint_filename = model.run()

    S_TO_MIN = 60
    with omc.StatePoint(statepoint_filename) as statepoint:
        tally_result = statepoint.get_tally(name=name_tally)
        sum_tally = tally_result.get_reshaped_data(value="sum").flatten()[0] * S_TO_MIN
        std_dev = tally_result.get_reshaped_data(value="std_dev").flatten()[0] * S_TO_MIN
        rel_err = tally_result.get_reshaped_data(value="rel_err").flatten()[0] * S_TO_MIN

    output = ["Score Type: " + name_tally,
              f"Source Distance of (cm, from origin): {meas_source_distance:.1f}",
              f"Tally (cpm): {sum_tally:.1f} +/- {rel_err:.1f}",
              '\n']

    path = r"/home/USER/PycharmProjects/Cs137_PointSource_Test/"
    name = "output" + name_tally + ".txt"
    write_strings_to_txt(output, name, path, append=True)

write_strings_to_txt(["----------------------------------\n"], name, path, append=True)