Skip to content

Commit c643460

Browse files
author
fzeiser
committed
Fix response interpolation above Compt.edge
This closes #131. Changed response function interpolation between Compton edge and he chosen max. energy. Before, there was a misunderstanding of the *bin-by-bin interpolation* in Guttormsen1996. It now used a fan-like interpolation, too. Most noticable for response functions with small fwhm (like 1/10 Magne recommends for unfolding).
1 parent 84c50c6 commit c643460

File tree

3 files changed

+165
-33
lines changed

3 files changed

+165
-33
lines changed

ompy/response.py

Lines changed: 112 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import numpy as np
1111
import pandas as pd
1212
from pathlib import Path
13-
from typing import Union, Optional, Tuple
13+
from typing import Union, Optional, Tuple, Dict, Any
1414
from scipy.interpolate import interp1d
1515
import logging
1616

@@ -144,7 +144,8 @@ def LoadZip(self,
144144
compton_matrix[i, 0:len(cmp_current)] = cmp_current
145145
i += 1
146146

147-
return resp, compton_matrix, np.linspace(a0_cmp, a1_cmp * (N_cmp - 1), N_cmp)
147+
return resp, compton_matrix, np.linspace(a0_cmp, a1_cmp * (N_cmp - 1),
148+
N_cmp)
148149

149150
def LoadDir(self,
150151
path: Union[str, Path],
@@ -201,7 +202,6 @@ def LoadDir(self,
201202
# "Eg_sim" means "gamma, simulated", and refers to the gamma energies
202203
# where we have simulated Compton spectra.
203204

204-
205205
# Get calibration and array length from highest-energy spectrum,
206206
# because the spectra may have differing length,
207207
# but the last is bound to be the longest.
@@ -270,15 +270,16 @@ def interpolate(y, fill_value="extrapolate"):
270270
fwhm_rel_1330 = (self.fwhm_abs / 1330 * 100)
271271
self.f_fwhm_rel_perCent = interpolate(self.resp['FWHM_rel_norm']
272272
* fwhm_rel_1330)
273-
def f_fwhm_abs(E):
273+
def f_fwhm_abs(E): # noqa
274274
return E * self.f_fwhm_rel_perCent(E)/100
275275

276276
self.f_fwhm_abs = f_fwhm_abs
277277

278278
def interpolate(self,
279279
Eout: np.ndarray = None,
280280
fwhm_abs: float = None,
281-
return_table: bool = False) -> Union[Matrix, Tuple[Matrix, pd.DataFrame]]:
281+
return_table: bool = False
282+
) -> Union[Matrix, Tuple[Matrix, pd.DataFrame]]:
282283
""" Interpolated the response matrix
283284
284285
Perform the interpolation for the energy range specified in Eout with
@@ -287,7 +288,13 @@ def interpolate(self,
287288
The interpolation is split into energy regions. Below the
288289
back-scattering energy Ebsc we interpolate linearly, then we apply the
289290
"fan method" (Guttormsen 1996) in the region from Ebsc up to the
290-
Compton edge, then linear extrapolation again the rest of the way.
291+
Compton edge, with a Compton scattering angle dependent interpolation.
292+
From the Compton edge to Egmax we also use a fan, but with a linear
293+
interpolation.
294+
295+
Note:
296+
Below the ~350 keV we only use a linear interpolation, as the
297+
fan method does not work. This is not described in Guttormsen 1996.
291298
292299
Args:
293300
folderpath: The path to the folder containing Compton spectra and
@@ -319,25 +326,26 @@ def interpolate(self,
319326
# Loop over rows of the response matrix
320327
# TODO for speedup: Change this to a cython
321328
for j, E in enumerate(Eout):
322-
323-
# Find maximal energy for current response (+n*sigma) function,
324-
# -> Better if the lowest energies of the simulated spectra are
325-
# above the gamma energy to be extrapolated
326-
oneSigma = fwhm_abs * self.f_fwhm_rel_perCent_norm(E) / 2.35
329+
oneSigma = fwhm_abs_array[j] / 2.35
327330
Egmax = E + 6 * oneSigma
328331
i_Egmax = min(index(Eout, Egmax), N_out-1)
329-
# LOG.debug("Response for E: {E:.0f} calc. up to {Egmax:.0f}")
332+
LOG.debug(f"Response for E {E:.0f} calc up to {Eout[i_Egmax]:.0f}")
330333

331334
compton = self.get_closest_compton(E)
332335

333336
R_low, i_bsc = self.linear_backscatter(E, compton)
334-
R_fan, i_last = self.fan_method(E, compton,
337+
R_fan, i_last = \
338+
self.fan_method_compton(E, compton,
339+
i_start=i_bsc+1, i_stop=i_Egmax)
340+
if i_last <= i_bsc+2: # fan method didn't do anything
341+
R_high = self.linear_to_end(E, compton,
335342
i_start=i_bsc+1, i_stop=i_Egmax)
336-
if i_last == i_bsc+1: # fan method didn't do anything
337-
i_last -= 1
338-
R_high = self.linear_to_end(E, compton,
339-
i_start=i_last+1, i_stop=i_Egmax)
340-
R[j, :] += R_low + R_fan + R_high
343+
R[j, :] += R_low + R_high
344+
else:
345+
R_high = self.fan_to_end(E, compton,
346+
i_start=i_last+1, i_stop=i_Egmax,
347+
fwhm_abs_array=fwhm_abs_array)
348+
R[j, :] += R_low + R_fan + R_high
341349

342350
# coorecton below E_sim[0]
343351
if E < self.resp['Eg'][0]:
@@ -364,7 +372,8 @@ def interpolate(self,
364372
response = Matrix(values=R, Eg=Eout, Ex=Eout)
365373

366374
if return_table:
367-
# Return the response matrix, as well as the other structures, FWHM and efficiency, interpolated to the Eout_array
375+
# Return the response matrix, as well as the other structures,
376+
# FWHM and efficiency, interpolated to the Eout_array
368377
response_table = {'E': Eout,
369378
'fwhm_abs': fwhm_abs_array,
370379
'fwhm_rel_%': self.f_fwhm_rel_perCent(Eout),
@@ -400,7 +409,7 @@ def iterpolate_checks(self):
400409
LOG.info(f"Note: Spectra outside of {self.resp['Eg'].min()} and "
401410
f"{self.resp['Eg'].max()} are extrapolation only.")
402411

403-
def get_closest_compton(self, E: float) -> Tuple[int, int]:
412+
def get_closest_compton(self, E: float) -> Dict[str, Any]:
404413
"""Find and rebin closest energies from available response functions
405414
406415
If `E < self.resp['Eg'].min()` the compton matrix will be replaced
@@ -410,7 +419,9 @@ def get_closest_compton(self, E: float) -> Tuple[int, int]:
410419
E (float): Description
411420
412421
Returns:
413-
Tuple[int, int]: `ilow` and `ihigh` are indices of closest energies
422+
Dict with entries `Elow` and `Ehigh`, and `ilow` and `ihigh`, the
423+
(indices) of closest energies. The arrays `counts_low` and
424+
`counts_high` are the corresponding arrays of `compton` spectra.
414425
"""
415426
N = len(self.resp['Eg'])
416427
# ilow = 0
@@ -429,8 +440,10 @@ def get_closest_compton(self, E: float) -> Tuple[int, int]:
429440
Elow = self.resp['Eg'][ilow]
430441
cmp_low = self.cmp_matrix[ilow, :]
431442

432-
cmp_low = rebin_1D(cmp_low, self.Ecmp_array, self.Eout)
433-
cmp_high = rebin_1D(cmp_high, self.Ecmp_array, self.Eout)
443+
Enew = np.arange(self.Eout[0], self.Ecmp_array[-1],
444+
self.Eout[1] - self.Eout[0])
445+
cmp_low = rebin_1D(cmp_low, self.Ecmp_array, Enew)
446+
cmp_high = rebin_1D(cmp_high, self.Ecmp_array, Enew)
434447

435448
compton = {"ilow": ilow,
436449
"ihigh": ihigh,
@@ -442,7 +455,8 @@ def get_closest_compton(self, E: float) -> Tuple[int, int]:
442455
return compton
443456

444457
def linear_cmp_interpolation(self, E: float, compton: dict,
445-
fill_value: str = "extrapolate")-> np.ndarray:
458+
fill_value: str = "extrapolate"
459+
) -> np.ndarray:
446460
"""Linear interpolation between the compton spectra
447461
448462
Args:
@@ -505,8 +519,77 @@ def linear_to_end(self, E: float, compton: dict,
505519
R[R < 0] = 0
506520
return R
507521

508-
def fan_method(self, E: float, compton: dict,
509-
i_start: int, i_stop: int) -> Tuple[np.ndarray, int]:
522+
def fan_to_end(self, E: float, compton: dict,
523+
i_start: int, i_stop: int,
524+
fwhm_abs_array: np.ndarray) -> np.ndarray:
525+
"""Linear(!) fan interpolation from Compton edge to Emax
526+
527+
The fan-part is "scaled" by the distance between the Compton edge and
528+
max(E). To get a reasonable scaling, we have to use ~6 sigma.
529+
530+
Note:
531+
We extrapolate up to self.N_out, and not i_stop, as a workaround
532+
connected to Magne's 1/10th FWHM unfolding [which results
533+
in a very small i_stop.]
534+
535+
Args:
536+
E (float): Incident energy
537+
compton (dict): Dict. with information about the compton spectra
538+
to interpolate between
539+
i_start (int): Index where to start (usually end of fan method)
540+
i_stop (int): Index where to stop (usually E+n*resolution)
541+
fwhm_abs_array (np.ndarray): FHWM array, absolute values
542+
543+
Returns:
544+
np.ndarray: Response for `E`
545+
"""
546+
547+
R = np.zeros(self.N_out)
548+
549+
Esim_low = compton["Elow"]
550+
Esim_high = compton["Ehigh"]
551+
Ecmp1 = self.E_compton(Esim_low, np.pi)
552+
Ecmp2 = self.E_compton(Esim_high, np.pi)
553+
i_low_edge = index(self.Eout, Ecmp1)
554+
i_high_edge = index(self.Eout, Ecmp2)
555+
556+
oneSigma = fwhm_abs_array[index(self.Eout, Esim_low)] / 2.35
557+
Egmax1 = Esim_low + 6 * oneSigma
558+
scale1 = (Egmax1 - Ecmp1) / (self.Eout[i_stop] - self.Eout[i_start])
559+
560+
oneSigma = fwhm_abs_array[index(self.Eout, Esim_high)] / 2.35
561+
Egmax2 = Esim_high + 6 * oneSigma
562+
scale2 = (Egmax2 - Ecmp2) / (self.Eout[i_stop] - self.Eout[i_start])
563+
564+
def lin_interpolation(x, x0, y0, x1, y1):
565+
return y0 + (y1-y0)*(x-x0)/(x1-x0)
566+
567+
i_edge = i_start-1
568+
# for i in range(i_edge+1, i_stop):
569+
for i in range(i_edge+1, self.N_out):
570+
i1 = int(i_low_edge + scale1 * (i - i_edge))
571+
i2 = int(i_high_edge + scale2 * (i - i_edge))
572+
573+
if i1 >= len(compton["counts_low"]):
574+
i1 = len(compton["counts_low"])-1
575+
if i2 >= len(compton["counts_high"]):
576+
i2 = len(compton["counts_high"])-1
577+
578+
c1 = compton["counts_low"][i1]
579+
c2 = compton["counts_high"][i2]
580+
y = lin_interpolation(E, Esim_low, c1, Esim_high, c2)
581+
R[i] = y
582+
583+
if len(R[R < 0]) != 0:
584+
LOG.debug(f"In linear fan method, {len(R[R < 0])} entries in R"
585+
"are negative and now set to 0")
586+
R[R < 0] = 0
587+
588+
return R
589+
590+
def fan_method_compton(self, E: float, compton: dict,
591+
i_start: int,
592+
i_stop: int) -> Tuple[np.ndarray, int]:
510593
"""Fan method
511594
512595
Args:
@@ -628,21 +711,19 @@ def E_compton(Eg, theta):
628711
"""
629712
Calculates the energy of an electron that is scattered an angle
630713
theta by a gamma-ray of energy Eg.
631-
Adapted from MAMA, file "folding.f", which references
632-
Canberra catalog ed.7, p.2.
633714
634715
Note:
635716
For `Eg <= 0.1` it returns `Eg`. (workaround)
636717
637718
Args:
638-
Eg: Energy of gamma-ray in keV
719+
Eg: Energy of incident gamma-ray in keV
639720
theta: Angle of scatter in radians
640721
641722
Returns:
642723
Energy Ee of scattered electron
643724
"""
644-
scattered = Eg / (1 + Eg / 511 * (1 - np.cos(theta)))
645-
electron = scattered * Eg / 511 * (1 - np.cos(theta))
725+
Eg_scattered = Eg / (1 + Eg / 511 * (1 - np.cos(theta)))
726+
electron = Eg - Eg_scattered
646727
return np.where(Eg > 0.1, electron, Eg)
647728

648729
@staticmethod

release_note.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Release notes for OMpy
2+
3+
## v.1.1.0
4+
Most important changes:
5+
- Changed response function interpolation between Compton edge and he chosen max. energy. Before, there was a
6+
misunderstanding of the *bin-by-bin interpolation* in Guttormsen1996. It now used a fan-like interpolation,
7+
too. Most noticable for response functions with small fwhm (like 1/10 Magne recommends for unfolding).
8+
9+
## v1.0.0
10+
Several changes, most important:
11+
12+
**theoretical framework**:
13+
- Corrected likelihood: Forgot non-constant K term when K=K(theta): c8c0046153b1eb269d00280b572f742b1a3cf4d7
14+
15+
**parameters and choices**:
16+
- unfolding parameters: 0fcafe2ff7770be8c2bb107256201af79739cdb3
17+
- unfolder and fg method use remove negatives only, no fill: 9edb48537cca1f88c3120a73fa8eb92f6ebb5177
18+
- Randomize p0 for decomposition 77dec9db9a3a34d5fd6195752c84cfbca0c26c39
19+
20+
**implementation and convenience**:
21+
- different save/load for vectors e5f7e52ce13cff04e8b23f50a00902be1d098bfc and parent commits
22+
- Enable pickling of normalizer instances via dill: 896b352686594a8c7dbe52904645cc5b900ba800
23+
24+
25+
## v0.9.1
26+
Changes:
27+
28+
- corrected version number
29+
(v 0.9.0 has still v.0.8.0 as the version number)
30+
31+
## v0.9
32+
Many changes to the API have occured since v.0.2. Here a (incomplete) summary of the main changes:
33+
34+
- `Vector` and `Matrix` are now in mid-bin calibration. Most or all other functions have been adopted.
35+
- Many changes (bugfix & readability) to the ensemble, decomposition and normalization classes.
36+
- Normalization of nld and gsf ensembles working
37+
- Parallelization, even though it could be more efficient for multinest (see #94 )
38+
- Renamed response functions submodule; run `git submodule update --init --recursive` after `git pull` to get the new files
39+
- remember to run `pip install -e .` such that changes to the cython files will be recompiled
40+
- Documentation now available via https://ompy.readthedocs.io
41+
- Installation requirements are hopefully all specified; docker file is provided with integration at https://hub.docker.com/r/oslocyclotronlab/ompy and [mybinder](https://mybinder.org/v2/gh/oslocyclotronlab/ompy/master?filepath=ompy%2Fnotebooks%2Fgetting_started.ipynb) can be used to rund the examples.
42+
- We have clean-up the history of the repo to downsize it.
43+
Here the warning message: *NB: Read this (only) if you have cloned the repo before October 2019: We cleaned the repository from old comits clogging the repo (big data files that should never have been there). Unfortunetely, this has the sideeffect that the history had to be rewritten: Previous commits now have a different SHA1 (git version keys). If you need anything from the previous repo, see ompy_Archive_Sept2019. This will unfortunately also destroy references in issues. The simplest way to get the new repo is to rerun the installation instructions below.*
44+
45+
## v0.2-beta
46+
This is the first public beta version of the OMpy library, the Oslo Method in Python.
47+
48+
**NB: Read this (only) if you have cloned the repo before October 2019 (which affects this release, v0.2-beta)**:
49+
We cleaned the repository from old comits clogging the repo (big data files that should never have been there). Unfortunetely, this has the sideeffect that the history had to be rewritten: Previous commits now have a different SHA1 (git version keys). If you need anything from the previous repo, see [ompy_Archive_Sept2019](https://github.com/oslocyclotronlab/ompy_Archive_Sept2019). This will unfortunately also destroy references in issues. The simplest way to get the new repo is to rerun the installation instructions below.
50+
51+
**In essence**: This tag does not work any longer; you have to download the version from https://zenodo.org/record/2654604

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121
# Version rutine taken from numpy
2222
MAJOR = 1
23-
MINOR = 0
24-
MICRO = 1
23+
MINOR = 1
24+
MICRO = 0
2525
VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO)
2626

2727

0 commit comments

Comments
 (0)