Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/block offsets #550

Merged
merged 23 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5847398
added block offset fitter for RNO-G
sjoerd-bouma Aug 11, 2022
27babda
fix docstrings
sjoerd-bouma Aug 11, 2022
37cd070
changed block offset fit function to standalone
sjoerd-bouma Oct 22, 2022
9bc2e3f
fixed bug (dt instead of sampling rate)
sjoerd-bouma Oct 24, 2022
607dc8f
Merge branch 'develop' of github.com:nu-radio/NuRadioMC into feature/…
sjoerd-bouma Apr 19, 2023
c005726
change to smarter minimization
sjoerd-bouma Apr 19, 2023
4f423f7
store fitted block offsets as a channelParameter, simplify interface
sjoerd-bouma Jul 10, 2023
256f21f
update changelog
sjoerd-bouma Jul 10, 2023
14e393f
Merge branch 'develop' of github.com:nu-radio/NuRadioMC into feature/…
sjoerd-bouma Jul 10, 2023
57f5a44
forgot to pass through optional kwarg
sjoerd-bouma Aug 30, 2023
162f014
remove commented import
sjoerd-bouma Sep 12, 2023
8c9bc68
Merge branch 'develop' of github.com:nu-radio/NuRadioMC into feature/…
sjoerd-bouma Sep 12, 2023
3d58f05
treat offsets given as dict correctly
sjoerd-bouma Sep 25, 2023
98f51e4
save the microseconds
sjoerd-bouma Sep 25, 2023
8a77811
add different block offsets fit options to mattak reader and store th…
sjoerd-bouma Sep 25, 2023
87a505d
change *= to prevent error due to Type change from int to float
sjoerd-bouma Sep 25, 2023
2384323
store offsets only if they are actually computed
sjoerd-bouma Sep 25, 2023
ba3b1b2
make blockoffsetfitter faster by setting maxiter=2 by default
sjoerd-bouma Sep 26, 2023
80ef76d
deprecate old baseline correction in readRNOGDataMattak
sjoerd-bouma Sep 26, 2023
291433e
remove inaccessible old baseline correction code
sjoerd-bouma Sep 26, 2023
30726a0
increase default number of iterations to 5
sjoerd-bouma Sep 29, 2023
69f6fda
make blockoffsetfitter a private attribute to reduce clutter in module
sjoerd-bouma Sep 29, 2023
c84a962
Merge branch 'develop' of github.com:nu-radio/NuRadioMC into feature/…
sjoerd-bouma Nov 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NuRadioReco/framework/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class channelParameters(Enum):
signal_receiving_zenith = 15 #: the zenith angle of direction at which the radio signal arrived at the antenna
signal_ray_type = 16 #: type of the ray propagation path of the signal received by this channel. Options are direct, reflected and refracted
signal_receiving_azimuth = 17 #: the azimuth angle of direction at which the radio signal arrived at the antenna
block_offsets = 18 #: 'block' or pedestal offsets. See `NuRadioReco.modules.RNO_G.channelBlockOffsetFitter`


class electricFieldParameters(Enum):
Expand Down
256 changes: 256 additions & 0 deletions NuRadioReco/modules/RNO_G/channelBlockOffsetFitter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
"""
Module to remove 'block offsets' from RNO-G voltage traces.

The function ``fit_block_offsets`` can be used standalone to perform an out-of-band
fit to the block offsets. Alternatively, the ``channelBlockOffsets`` class contains convenience
``add_offsets`` (to add block offsets in simulation) and ``remove_offsets`` methods that can be run
directly on a NuRadioMC/imported ``Event``. The added/removed block offsets are stored per channel
in the `NuRadioReco.framework.parameters.channelParameters.block_offsets` parameter.

"""

from NuRadioReco.utilities import units, fft
from NuRadioReco.framework.base_trace import BaseTrace
from NuRadioReco.framework.parameters import channelParameters
# from iminuit import Minuit
sjoerd-bouma marked this conversation as resolved.
Show resolved Hide resolved
import numpy as np
import scipy

class channelBlockOffsets:

def __init__(self, block_size=128, max_frequency=51*units.MHz):
"""
Add or remove block offsets to channel traces

This module adds, fits or removes 'block offsets' by fitting
them in a specified out-of-band region in frequency space.

Parameters
----------
block_size: int (default: 128)
The size (in samples) of the blocks
max_frequency: float (default: 51 MHz)
The maximum frequency to include in the out-of-band
block offset fit

"""
self.sampling_rate = None
self.block_size = block_size # the size (in samples) of the blocks
self._offset_fit = dict()
self._offset_inject = dict()
self._max_frequency = max_frequency

def add_offsets(self, event, station, offsets=1*units.mV, channel_ids=None):
"""
Add (simulated or reconstructed) block offsets to an event.

Added block offsets for each channel are stored in the
``channelParameters.block_offsets`` parameter.

Parameters
----------
event: Event object | None
station: Station
The station to add block offsets to
offsets: float | array | dict
offsets to add to the event. Default: 1 mV

- if a float, add gaussian-distributed of amplitude ``offsets``
to all channels specified;
- if an array, the length should be the same as the number
of blocks in a single trace, and the entries will be
interpreted as the amplitudes of the offsets;
- if a dict, the keys should be the channel ids, and each
value should contain either a float or an array to add to
each channel as specified above.

channel_ids: list | None
either a list of channel ids to apply the offsets to, or
None to apply the offsets to all channels in the station
(default: None).

"""

if channel_ids is None:
channel_ids = station.get_channel_ids()
for channel_id in channel_ids:
channel = station.get_channel(channel_id)
if isinstance(offsets, dict):
add_offsets = offsets[channel_id]
sjoerd-bouma marked this conversation as resolved.
Show resolved Hide resolved
elif len(np.atleast_1d(offsets)) == 1:
add_offsets = np.random.normal(
0, offsets, (channel.get_number_of_samples() // self.block_size)
)
else:
add_offsets = offsets

# save the added offsets as a channelParameter
if channel.has_parameter(channelParameters.block_offsets):
block_offsets_old = channel.get_parameter(channelParameters.block_offsets)
channel.set_parameter(channelParameters.block_offsets, block_offsets_old + offsets)
else:
channel.set_parameter(channelParameters.block_offsets, offsets)

channel.set_trace(
channel.get_trace() + np.repeat(add_offsets, self.block_size),
channel.get_sampling_rate()
)

def remove_offsets(self, event, station, mode='fit', channel_ids=None):
"""
Remove block offsets from an event

Fits and removes the block offsets from an event. The removed
offsets are stored in the ``channelParameters.block_offsets``
parameter.

Parameters
----------
event: NuRadioReco.framework.event.Event | None
station: NuRadioReco.framework.station.Station
The station to remove the block offsets from
mode: 'fit' | 'approximate' | 'stored'

- 'fit' (default): fit the block offsets with a minimizer
- 'approximate' : use the first guess from the out-of-band component,
without any fitting (slightly faster)
- 'stored': use the block offsets already stored in the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you simulate the block offsets would you not simulated it on a sim_station? Hence, the station would not have this parameter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'stored' option is probably superfluous, but I thought maybe one might want to undo the block offsets that have already been removed by e.g. the external calibration, or allow for some other fit to compute the block offsets.

In terms of simulation, right now we don't simulate the block offsets anyway, and because e.g. the timings are different I'm not sure how straightforward it would be to simulate them for the sim_station (rather than for the station, which is what I've been doing so far). Even if we end up doing this for the sim_station, I'd be inclined to leave it to the user to retrieve the simulated parameter from the sim_station to keep a cleaner separation between the two.

``channelParameters.block_offsets`` parameter. Will raise an error
if this parameter is not present.

channel_ids: list | None
List of channel ids to remove offsets from. If None (default),
remove offsets from all channels in ``station``

"""
if channel_ids is None:
channel_ids = station.get_channel_ids()

offsets = {}
if mode == 'stored': # remove offsets stored in channelParameters.block_offsets
offsets = {
channel_id: -station.get_channel(channel_id).get_parameter(channelParameters.block_offsets)
for channel_id in channel_ids}
else: # fit & remove offsets
for channel_id in channel_ids:
channel = station.get_channel(channel_id)
trace = channel.get_trace()

block_offsets = fit_block_offsets(
trace, self.block_size,
channel.get_sampling_rate(), self._max_frequency
)
offsets[channel_id] = -block_offsets

self.add_offsets(event, station, offsets, channel_ids)


def fit_block_offsets(
trace, block_size=128, sampling_rate=3.2*units.GHz,
max_frequency=50*units.MHz, mode='fit', return_trace = False,
tol=1e-6,):
"""
Fit 'block' offsets for a voltage trace

Fit block offsets ('rect'-shaped offsets from a baseline)
using a fit to the out-of-band spectrum of a voltage trace.

Parameters
----------
trace: numpy Array
the voltage trace
block_size: int (default: 128)
the number of samples in one block
sampling_rate: float (default: 3.2 GHz)
the sampling rate of the trace
max_frequency: float (default: 50 MHz)
the fit to the block offsets is performed
in the frequency domain, in the band up to
max_frequency
mode: 'fit' | 'approximate'
Whether to fit the block offsets (default)
or just use the first guess from the out-of-band
component (faster)
return_trace: bool (default: False)
if True, return the tuple (offsets, output_trace)
where the output_trace is the input trace with
fitted block offsets removed

Returns
-------
block_offsets: numpy array
The fitted block offsets.
output_trace: numpy array or None
The input trace with the fitted block offsets removed.
Returned only if return_trace=True

Other Parameters
----------------
tol: float (default: 1e-6)
tolerance parameter passed on to scipy.optimize.minimize
"""
dt = 1. / sampling_rate
spectrum = fft.time2freq(trace, sampling_rate)
frequencies = np.fft.rfftfreq(len(trace), dt)
n_blocks = len(trace) // block_size

mask = (frequencies > 0) & (frequencies < max_frequency) # a simple rectangular filter
frequencies_oob = frequencies[mask]
spectrum_oob = spectrum[mask]

# we use the bandpass-filtered trace to get a first estimate of
# the block offsets, by simply averaging over each block.
filtered_trace_fft = np.copy(spectrum)
filtered_trace_fft[~mask] = 0
filtered_trace = fft.freq2time(filtered_trace_fft, sampling_rate)

# obtain guesses for block offsets
a_guess = np.array([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not relevant but using np.split seems to be a bit faster

In [17]: %timeit np.mean(np.split(filtered_trace, n_blocks), axis=1)
19 µs ± 849 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

In [18]: %timeit a_guess = np.array([np.mean(filtered_trace[i*block_size:(i+1)*block_size]) for i in range(n_blocks)])
32.4 µs ± 1.17 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you ever check what the difference is when you use the unfiltered trace?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, implemented (though I agree I don't think this was the bottleneck). Using the unfiltered trace is a little bit faster but gives a ~4 times larger RMS difference between true and fitted block offset

np.mean(filtered_trace[i*block_size:(i+1)*block_size])
for i in range(n_blocks)
])
if mode == 'approximate':
block_offsets = a_guess
elif mode == 'fit':
# self._offset_guess[channel_id] = a_guess
# we can get rid of one parameter through a global shift
a_guess = a_guess[:-1] - a_guess[-1]

# we perform the fit out-of-band, in order to avoid
# distorting any actual signal

# most of the terms in the fit depend only on the frequencies,
# sampling rate and number of blocks. We therefore calculate these
# only once, outside the fit function.
pre_factor_exponent = np.array([
-2.j * np.pi * frequencies_oob * dt * ((j+.5) * block_size - .5)
for j in range(len(a_guess))
])
const_fft_term = (
1 / sampling_rate * np.sqrt(2) # NuRadio FFT normalization
* np.exp(pre_factor_exponent)
* np.sin(np.pi*frequencies_oob*block_size*dt)[None]
/ np.sin(np.pi*frequencies_oob*dt)[None]
)

def pedestal_fit(a):
fit = np.sum(a[:, None] * const_fft_term, axis=0)
chi2 = np.sum(np.abs(fit-spectrum_oob)**2)
return chi2

res = scipy.optimize.minimize(pedestal_fit, a_guess, tol=tol).x

block_offsets = np.zeros(len(res) + 1)
block_offsets[:-1] = res

# the fit is not sensitive to an overall shift,
# so we include the zero-meaning here
block_offsets += np.mean(trace) - np.mean(block_offsets)
else:
raise ValueError(f'Invalid value for mode={mode}. Accepted values are {{"fit", "approximate"}}')

if return_trace:
output_trace = trace - np.repeat(block_offsets, block_size)
return block_offsets, output_trace

return block_offsets
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ new features:
- added ability to generate high-low-triggered noise on a narrow band but return full-band waveforms
- phased array noise generation utility class: calculate trigger time and cut trace accordingly
- use Philox noise generator in noise adder module (this changes the default random sequence)
- added 'block offset' removal/simulation module for RNO-G

bugfixes:
- fixed/improved C++ raytracer not finding solutions for some near-horizontal or near-shadowzone vertices
Expand Down