Skip to content

Commit 4e24ad3

Browse files
committed
Let relative binning choose its own frequency resolution
epsilon had to be picked by hand with nothing to pick it from: the default in code is 0.5 while the examples use 0.03 to 0.1, a factor of sixteen with no guidance on which suits a given analysis. Accept epsilon = auto, which halves epsilon until the bin resolution check passes, so the resolution is set by the problem. On GW170817 that settles on 0.5 and stops; with a fiducial waveform far from the signal it keeps refining and then says so, since bins cannot rescue a bad fiducial. The bin layout moves into its own method so it can be redone, which also lets the shifted data used to build it be dropped once it has been. The epsilon that was used is now written to the output file, which the metadata docstring already claimed.
1 parent 430a376 commit 4e24ad3

2 files changed

Lines changed: 107 additions & 6 deletions

File tree

pycbc/inference/models/relbin.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,11 @@ class Relative(DistMarg, BaseGaussianNoise):
158158
an intrinsic parameter compatible with the chosen approximant.
159159
gammas : array of floats, optional
160160
Frequency powerlaw indices to be used in computing frequency bins.
161-
epsilon : float, optional
161+
epsilon : float or 'auto', optional
162162
Tuning parameter used in calculating the frequency bins. Lower values
163-
will result in higher resolution and more bins.
163+
will result in higher resolution and more bins. If 'auto', bins are
164+
added until they resolve the waveform ratio, so that the resolution
165+
is set by the problem rather than by hand.
164166
earth_rotation: boolean, optional
165167
Default is False. If True, then vary the fp/fc polarization values
166168
as a function of frequency bin, using a predetermined PN approximation
@@ -220,6 +222,10 @@ def __init__(
220222
if self.fid_params[k] == 'REPLACE':
221223
self.fid_params.pop(k)
222224

225+
# the shifted data and frequency limits the bin layout is built
226+
# from, kept only until the layout has been settled on
227+
shifted, limits = {}, {}
228+
223229
for ifo in data:
224230
# store data and frequencies
225231
d0 = self.data[ifo]
@@ -291,22 +297,83 @@ def __init__(
291297
self.h00[ifo] = numpy.array(curr_wav) # * tshift
292298
data_shifted = self.data[ifo] * numpy.conjugate(tshift)
293299

300+
shifted[ifo] = data_shifted
301+
limits[ifo] = (f_lo, f_hi)
302+
303+
layout = (shifted, limits, gammas, earth_rotation,
304+
int(earth_rotation_mode))
305+
if str(epsilon).lower() == 'auto':
306+
self.epsilon = self.refine_epsilon(*layout)
307+
else:
308+
self.epsilon = float(epsilon)
309+
self.setup_bin_layout(self.epsilon, *layout)
310+
self.check_bin_resolution()
311+
312+
def setup_bin_layout(self, epsilon, shifted, limits, gammas,
313+
earth_rotation, earth_rotation_mode):
314+
"""Place the frequency bins and compute the summary data for them.
315+
"""
316+
for ifo in self.data:
294317
logging.info("Computing frequency bins")
318+
f_lo, f_hi = limits[ifo]
295319
fbin_ind = setup_bins(
296320
f_full=self.f[ifo], f_lo=f_lo, f_hi=f_hi,
297-
gammas=gammas, eps=float(epsilon),
321+
gammas=gammas, eps=epsilon,
298322
)
299323
logging.info("Using %s bins for this model", len(fbin_ind))
300324

301325
self.fedges[ifo] = self.f[ifo][fbin_ind]
302326
self.edges[ifo] = fbin_ind
303-
self.init_from_frequencies(data_shifted, self.h00, fbin_ind, ifo)
327+
self.init_from_frequencies(shifted[ifo], self.h00, fbin_ind, ifo)
304328
self.antenna_time[ifo] = self.setup_antenna(
305329
earth_rotation,
306-
int(earth_rotation_mode),
330+
earth_rotation_mode,
307331
self.fedges[ifo])
308332
self.combine_layout()
309-
self.check_bin_resolution()
333+
334+
def refine_epsilon(self, *layout, start=1.0, threshold=1e-3, smallest=0.01):
335+
"""Halve epsilon until the bins resolve the waveform ratio.
336+
337+
Bins are added only where the model cannot do without them, so a
338+
problem needing few bins does not pay for one that needs many.
339+
Each step costs a bin layout and a handful of sparse waveforms,
340+
which is small against the analysis that follows.
341+
342+
Parameters
343+
----------
344+
start : float, optional
345+
The first, coarsest value to try.
346+
threshold : float, optional
347+
Stop once the interpolation error falls below this. See
348+
:py:meth:`check_bin_resolution` for what it means.
349+
smallest : float, optional
350+
Give up at this value rather than refining without end.
351+
352+
Returns
353+
-------
354+
float
355+
The epsilon that was settled on.
356+
"""
357+
epsilon = start
358+
while True:
359+
self.setup_bin_layout(epsilon, *layout)
360+
# inf so that only the final value is reported to the user
361+
error = self.check_bin_resolution(threshold=numpy.inf)
362+
if error <= threshold or epsilon <= smallest:
363+
break
364+
epsilon /= 2.
365+
366+
if error > threshold:
367+
logging.warning(
368+
"Refining epsilon stopped at %.3g without resolving the "
369+
"waveform ratio, which is still off by %.3g against a "
370+
"threshold of %.3g. The fiducial waveform is probably far "
371+
"from the posterior.", epsilon, error, threshold)
372+
else:
373+
logging.info("Chose epsilon %.3g, giving %s bins and an "
374+
"interpolation error of %.3g", epsilon,
375+
len(self.fedges[list(self.data)[0]]), error)
376+
return epsilon
310377

311378
def init_from_frequencies(self, data, h00, fbin_ind, ifo):
312379
bins = numpy.array(
@@ -636,6 +703,7 @@ def write_metadata(self, fp, group=None):
636703
attrs = fp[group].attrs
637704
for p, v in self.fid_params.items():
638705
attrs["{}_ref".format(p)] = v
706+
attrs["epsilon"] = self.epsilon
639707

640708
def interpolation_error_from_reference(self):
641709
""" Return the largest error made by interpolating the waveform

test/test_relbin_resolution.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ def test_check_is_cheap(self):
127127
self.assertTrue(numpy.isfinite(value))
128128
self.assertGreaterEqual(value, 0.)
129129

130+
# 'auto' picks a resolution that suits the problem
131+
132+
def test_auto_meets_the_threshold(self):
133+
model = self.model('auto')
134+
self.assertLess(model.check_bin_resolution(), 1e-3)
135+
136+
def test_auto_agrees_with_a_finer_setting(self):
137+
"""Stopping as soon as the check passes must not cost accuracy."""
138+
fine = self.model(0.005)
139+
fine.update(**self.q)
140+
141+
auto = self.model('auto')
142+
auto.update(**self.q)
143+
self.assertAlmostEqual(auto.loglr, fine.loglr, delta=0.05)
144+
145+
def test_auto_spends_bins_only_where_needed(self):
146+
"""A fiducial further from the signal should buy more bins."""
147+
near = self.model('auto')
148+
offset = dict(self.static)
149+
offset['mass1'], offset['mass2'] = 1.39, 1.36
150+
far = self.model('auto', static=offset)
151+
152+
self.assertGreater(far.epsilon, 0.)
153+
self.assertLess(far.epsilon, near.epsilon)
154+
self.assertGreater(len(far.fedges['H1']), len(near.fedges['H1']))
155+
156+
def test_epsilon_is_recorded(self):
157+
"""The value used has to be recoverable, since it is now chosen."""
158+
self.assertEqual(self.model(0.25).epsilon, 0.25)
159+
# config files hand every option over as a string
160+
self.assertEqual(self.model('0.25').epsilon, 0.25)
161+
self.assertIsInstance(self.model('auto').epsilon, float)
162+
130163

131164
suite = unittest.TestSuite()
132165
suite.addTest(

0 commit comments

Comments
 (0)