Skip to content

Commit

Permalink
Merge pull request #15 from neutrons/fbck
Browse files Browse the repository at this point in the history
add functional background
  • Loading branch information
mdoucet authored Nov 9, 2023
2 parents d0e1fd2 + 2fdec03 commit 2dab349
Show file tree
Hide file tree
Showing 13 changed files with 1,549 additions and 354 deletions.
237 changes: 237 additions & 0 deletions reduction/data/reference_fbck.txt

Large diffs are not rendered by default.

388 changes: 388 additions & 0 deletions reduction/data/template_fbck.xml

Large diffs are not rendered by default.

195 changes: 195 additions & 0 deletions reduction/lr_reduction/background.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import numpy as np
from lmfit.models import LinearModel


def determine_background_method(peak, bck):
"""
Check whether we have a background range defined by two pixels or four.
"""
if len(bck) == 2 or (len(bck) == 4 and bck[2] == 0 and bck[3] == 0):
return side_background()
elif not len(bck) == 4:
raise ValueError("Background ROI should be of length 2 or 4")

return functional_background(peak, bck)


def find_ranges_without_overlap(r1, r2):
"""
Returns the part of r1 that does not contain r2
When summing pixels for reflectivity, include the full range,
which means that for a range [a, b], b is included.
The range that we return must always exclude the pixels
included in r2.
"""
x1, x2 = r1
x3, x4 = r2

# r2 completely outside r1
if x4 < x1 or x3 > x2:
return [r1] # r1 is the range without r2

# r2 is entirely within r1
if x1 <= x3 and x2 >= x4:
return [[x1, x3 - 1], [x4 + 1, x2]] # ranges before and after r2

# r2 overlaps r1 from the right
if x1 <= x3:
return [[x1, x3 - 1]] # range before r2

# r2 overlaps r1 from the left
if x2 >= x4:
return [[x4 + 1, x2]] # range after r2

return [] # no range without r2


def functional_background(ws, event_reflectivity, peak, bck, low_res,
normalize_to_single_pixel=False, q_bins=None,
wl_dist=None, wl_bins=None, q_summing=False):
"""
"""
charge = ws.getRun().getProtonCharge()
# For each background range, exclue the peak
bck_ranges = find_ranges_without_overlap([bck[0], bck[1]], peak)
bck_ranges.extend(find_ranges_without_overlap([bck[2], bck[3]], peak))
print("Functional background: %s" % bck_ranges)

# Iterate through the ranges and gather the background points
bck_counts = []
d_bck_counts = []
pixels = []
for r in bck_ranges:
# This condition takes care of rejecting empty ranges,
# including the case where the second range was left to [0, 0],
# which has been the default before implementing this more flexible
# approach.
if not r[0] == r[1]:
_b, _d_b = event_reflectivity._reflectivity(ws, peak_position=0,
q_bins=q_bins,
peak=r, low_res=low_res,
theta=event_reflectivity.theta,
q_summing=q_summing,
wl_dist=wl_dist, wl_bins=wl_bins,
sum_pixels=False)
bck_counts.append(_b)
d_bck_counts.append(_d_b)
pixels.extend(list(range(r[0], r[1] + 1)))

# Put all those points together
_bck = np.vstack(bck_counts)
_d_bck = np.vstack(d_bck_counts)
pixels = np.asarray(pixels)

# Loop over the Q or TOF bins and fit the background
refl_bck = np.zeros(_bck.shape[1])
d_refl_bck = np.zeros(_bck.shape[1])

for i in range(_bck.shape[1]):
# Use average signal for background estimate
_estimate = np.mean(_bck[:, i])
linear = LinearModel()
pars = linear.make_params(slope=0, intercept=_estimate)

weights=1/_d_bck[:, i]
# Here we have counts normalized by proton charge, so if we want to
# assign an error of 1 on the counts, it should be 1/charge.
weights[_bck[:, i]==0]=charge

fit = linear.fit(_bck[:, i], pars, method='leastsq', x=pixels, weights=weights)

slope = fit.params['slope'].value
intercept = fit.params['intercept'].value
d_slope = np.sqrt(fit.covar[0][0])
d_intercept = np.sqrt(fit.covar[1][1])

# Compute background under the peak
total_bck = 0
total_err = 0
for k in range(peak[0], peak[1]+1):
total_bck += intercept + k * slope
total_err += d_intercept**2 + k**2 * d_slope**2

_pixel_area = peak[1] - peak[0] + 1.0

refl_bck[i] = (slope * (peak[1] + peak[0] + 1) + 2 * intercept) * _pixel_area / 2
d_refl_bck[i] = np.sqrt(d_slope**2 * (peak[1] + peak[0]+1)**2 + 4 * d_intercept**2
+ 4 * (peak[1] + peak[0]+1) * fit.covar[0][1]) * _pixel_area / 2

# In case we neen the background per pixel as opposed to the total sum under the peak
if normalize_to_single_pixel:
_pixel_area = peak[1] - peak[0]+1.0
refl_bck /= _pixel_area
d_refl_bck /= _pixel_area

return refl_bck, d_refl_bck


def side_background(ws, event_reflectivity, peak, bck, low_res,
normalize_to_single_pixel=False, q_bins=None,
wl_dist=None, wl_bins=None, q_summing=False):
"""
Original background substration done using two pixels defining the
area next to the specular peak that are considered background.
"""
q_bins = event_reflectivity.q_bins if q_bins is None else q_bins

# Background on the left of the peak only. We allow the user to overlap the peak
# on the right, but only use the part left of the peak.
if bck[0] < peak[0]-1 and bck[1] < peak[1]+1:
right_side = min(bck[1], peak[0]-1)
_left = [bck[0], right_side]
print("Left side background: [%s, %s]" % (_left[0], _left[1]))
refl_bck, d_refl_bck = event_reflectivity._roi_integration(ws, peak=_left,
low_res=low_res,
q_bins=q_bins,
wl_dist=wl_dist,
wl_bins=wl_bins,
q_summing=q_summing)
# Background on the right of the peak only. We allow the user to overlap the peak
# on the left, but only use the part right of the peak.
elif bck[0] > peak[0]-1 and bck[1] > peak[1]+1:
left_side = max(bck[0], peak[1]+1)
_right = [left_side, bck[1]]
print("Right side background: [%s, %s]" % (_right[0], _right[1]))
refl_bck, d_refl_bck = event_reflectivity._roi_integration(ws, peak=_right,
low_res=low_res,
q_bins=q_bins,
wl_dist=wl_dist,
wl_bins=wl_bins,
q_summing=q_summing)
# Background on both sides
elif bck[0] < peak[0]-1 and bck[1] > peak[1]+1:
_left = [bck[0], peak[0]-1]
refl_bck, d_refl_bck = event_reflectivity._roi_integration(ws, peak=_left,
low_res=low_res,
q_bins=q_bins,
wl_dist=wl_dist,
wl_bins=wl_bins,
q_summing=q_summing)
_right = [peak[1]+1, bck[1]]
_refl_bck, _d_refl_bck = event_reflectivity._roi_integration(ws, peak=_right,
low_res=low_res,
q_bins=q_bins,
wl_dist=wl_dist,
wl_bins=wl_bins,
q_summing=q_summing)
print("Background on both sides: [%s %s] [%s %s]" % (_left[0], _left[1], _right[0], _right[1]))

refl_bck = (refl_bck + _refl_bck)/2.0
d_refl_bck = np.sqrt(d_refl_bck**2 + _d_refl_bck**2)/2.0
else:
print("Invalid background: [%s %s]" % (bck[0], bck[1]))
refl_bck = np.zeros(q_bins.shape[0]-1)
d_refl_bck = refl_bck

# At this point we have integrated the region of interest and obtain the average per
# pixel, so unless that's what we want we need to multiply by the number of pixels
# used to integrate the signal.
if not normalize_to_single_pixel:
_pixel_area = peak[1] - peak[0]+1.0
refl_bck *= _pixel_area
d_refl_bck *= _pixel_area

return refl_bck, d_refl_bck
118 changes: 45 additions & 73 deletions reduction/lr_reduction/event_reduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import mantid.simpleapi as api
import numpy as np

from . import background


def get_wl_range(ws):
"""
Expand Down Expand Up @@ -59,7 +61,8 @@ def __init__(self, scattering_workspace, direct_workspace,
signal_peak, signal_bck, norm_peak, norm_bck,
specular_pixel, signal_low_res, norm_low_res,
q_min=None, q_step=-0.02, q_max=None,
tof_range=None, theta=1.0, instrument=None):
tof_range=None, theta=1.0, instrument=None,
functional_background=False):
"""
Pixel ranges include the min and max pixels.
Expand Down Expand Up @@ -98,6 +101,9 @@ def __init__(self, scattering_workspace, direct_workspace,
self._offspec_z_bins = None
self.summing_threshold = None

# Turn on functional background estimation
self.use_functional_bck = functional_background

# Process workspaces
if self.tof_range is not None:
self._ws_sc = api.CropWorkspace(InputWorkspace=scattering_workspace,
Expand Down Expand Up @@ -356,80 +362,37 @@ def _roi_integration(self, ws, peak, low_res, q_bins=None, wl_dist=None, wl_bins
d_refl_bck /= _pixel_area
return refl_bck, d_refl_bck

def _bck_subtraction(self, ws, peak, bck, low_res, normalize_to_single_pixel=False,
q_bins=None, wl_dist=None, wl_bins=None, q_summing=False):
"""
Abstracted out background subtraction process.
The options are the same as for the reflectivity calculation.
If wl_dist and wl_bins are supplied, the events will be weighted by flux.
If q_summing is True, the angle of each neutron will be recalculated according to
their position on the detector and place in the proper Q bin.
"""
q_bins = self.q_bins if q_bins is None else q_bins

# Background on the left of the peak only. We allow the user to overlap the peak on the right,
# but only use the part left of the peak.
if bck[0] < peak[0]-1 and bck[1] < peak[1]+1:
right_side = min(bck[1], peak[0]-1)
_left = [bck[0], right_side]
print("Left side background: [%s, %s]" % (_left[0], _left[1]))
refl_bck, d_refl_bck = self._roi_integration(ws, peak=_left, low_res=low_res,
q_bins=q_bins, wl_dist=wl_dist,
wl_bins=wl_bins, q_summing=q_summing)
# Background on the right of the peak only. We allow the user to overlap the peak on the left,
# but only use the part right of the peak.
elif bck[0] > peak[0]-1 and bck[1] > peak[1]+1:
left_side = max(bck[0], peak[1]+1)
_right = [left_side, bck[1]]
print("Right side background: [%s, %s]" % (_right[0], _right[1]))
refl_bck, d_refl_bck = self._roi_integration(ws, peak=_right, low_res=low_res,
q_bins=q_bins, wl_dist=wl_dist,
wl_bins=wl_bins, q_summing=q_summing)
# Background on both sides
elif bck[0] < peak[0]-1 and bck[1] > peak[1]+1:
_left = [bck[0], peak[0]-1]
refl_bck, d_refl_bck = self._roi_integration(ws, peak=_left, low_res=low_res,
q_bins=q_bins, wl_dist=wl_dist,
wl_bins=wl_bins, q_summing=q_summing)
_right = [peak[1]+1, bck[1]]
_refl_bck, _d_refl_bck = self._roi_integration(ws, peak=_right, low_res=low_res,
q_bins=q_bins, wl_dist=wl_dist,
wl_bins=wl_bins, q_summing=q_summing)
print("Background on both sides: [%s %s] [%s %s]" % (_left[0], _left[1], _right[0], _right[1]))

refl_bck = (refl_bck + _refl_bck)/2.0
d_refl_bck = np.sqrt(d_refl_bck**2 + _d_refl_bck**2)/2.0
else:
print("Invalid background: [%s %s]" % (bck[0], bck[1]))
refl_bck = np.zeros(q_bins.shape[0]-1)
d_refl_bck = refl_bck

# At this point we have integrated the region of interest and obtain the average per
# pixel, so unless that's what we want we need to multiply by the number of pixels
# used to integrate the signal.
if not normalize_to_single_pixel:
_pixel_area = peak[1] - peak[0]+1.0
refl_bck *= _pixel_area
d_refl_bck *= _pixel_area

return refl_bck, d_refl_bck

def bck_subtraction(self, normalize_to_single_pixel=False, q_bins=None, wl_dist=None, wl_bins=None,
q_summing=False):
"""
Higher-level call for background subtraction. Hides the ranges needed to define the ROI.
"""
return self._bck_subtraction(self._ws_sc, self.signal_peak, self.signal_bck, self.signal_low_res,
normalize_to_single_pixel=normalize_to_single_pixel, q_bins=q_bins,
wl_dist=wl_dist, wl_bins=wl_bins, q_summing=q_summing)
# Sanity check
if len(self.signal_bck) == 2 and self.use_functional_bck:
msg = "Background range incompatible with functional background: "
msg += "switching to averaging"
print(msg)
self.use_functional_bck = False

if self.use_functional_bck:
return background.functional_background(self._ws_sc, self, self.signal_peak,
self.signal_bck, self.signal_low_res,
normalize_to_single_pixel=normalize_to_single_pixel,
q_bins=q_bins, wl_dist=wl_dist, wl_bins=wl_bins,
q_summing=q_summing)
else:
return background.side_background(self._ws_sc, self, self.signal_peak, self.signal_bck,
self.signal_low_res,
normalize_to_single_pixel=normalize_to_single_pixel,
q_bins=q_bins, wl_dist=wl_dist, wl_bins=wl_bins,
q_summing=q_summing)

def norm_bck_subtraction(self):
"""
Higher-level call for background subtraction for the normalization run.
"""
return self._bck_subtraction(self._ws_db, self.norm_peak, self.norm_bck, self.norm_low_res,
normalize_to_single_pixel=False)
return background.side_background(self._ws_db, self, self.norm_peak, self.norm_bck,
self.norm_low_res, normalize_to_single_pixel=False)

def slice(self, x_min=0.002, x_max=0.004, x_bins=None, z_bins=None, # noqa A003
refl=None, d_refl=None, normalize=False):
Expand All @@ -453,16 +416,18 @@ def slice(self, x_min=0.002, x_max=0.004, x_bins=None, z_bins=None, # noqa A003

return z_bins, _spec, _d_spec

def _reflectivity(self, ws, peak_position, peak, low_res, theta, q_bins=None, q_summing=False, wl_dist=None, wl_bins=None):
def _reflectivity(self, ws, peak_position, peak, low_res, theta, q_bins=None, q_summing=False,
wl_dist=None, wl_bins=None, sum_pixels=True):
"""
Assumes that the input workspace is normalized by proton charge.
"""
charge = ws.getRun().getProtonCharge()
_q_bins = self.q_bins if q_bins is None else q_bins

refl = np.zeros(len(_q_bins)-1)
d_refl_sq = np.zeros(len(_q_bins)-1)
counts = np.zeros(len(_q_bins)-1)
shape = len(_q_bins)-1 if sum_pixels else ((peak[1]-peak[0]+1), len(_q_bins)-1)
refl = np.zeros(shape)
d_refl_sq = np.zeros(shape)
counts = np.zeros(shape)
_pixel_width = self.pixel_width if q_summing else 0.0

for i in range(low_res[0], int(low_res[1]+1)):
Expand Down Expand Up @@ -490,12 +455,19 @@ def _reflectivity(self, ws, peak_position, peak, low_res, theta, q_bins=None, q_
wl_weights = 1.0/np.interp(wl_list, wl_bins, wl_dist, np.inf, np.inf)
hist_weigths = wl_weights * qz / wl_list
_counts, _ = np.histogram(qz, bins=_q_bins, weights=hist_weigths)
refl += _counts
_counts, _ = np.histogram(qz, bins=_q_bins)
counts += _counts
_norm, _ = np.histogram(qz, bins=_q_bins)
if sum_pixels:
refl += _counts
counts += _norm
else:
refl[j-peak[0]] += _counts
counts[j-peak[0]] += _norm
else:
_counts, _ = np.histogram(qz, bins=_q_bins)
refl += _counts
if sum_pixels:
refl += _counts
else:
refl[j-peak[0]] += _counts

# The following is for information purposes
if q_summing:
Expand Down
2 changes: 1 addition & 1 deletion reduction/lr_reduction/reduction_template_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __init__(self):
# Signal selection
self.data_peak_range = [140, 150]
self.subtract_background = True
self.background_roi = [137, 153,100, 200]
self.background_roi = [137, 153, 0, 0]
self.tof_range = [9600., 21600.]
self.select_tof_range = True

Expand Down
Loading

0 comments on commit 2dab349

Please sign in to comment.