# Copyright (C) 2013 Ian W. Harry
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import logging
import numpy
from pycbc.tmpltbank.lambda_mapping import generate_mapping
logger = logging.getLogger('pycbc.tmpltbank.calc_moments')
[docs]
def determine_eigen_directions(metricParams, preserveMoments=False,
vary_fmax=False, vary_density=None):
"""
This function will calculate the coordinate transfomations that are needed
to rotate from a coordinate system described by the various Lambda
components in the frequency expansion, to a coordinate system where the
metric is Cartesian.
Parameters
-----------
metricParams : metricParameters instance
Structure holding all the options for construction of the metric.
preserveMoments : boolean, optional (default False)
Currently only used for debugging.
If this is given then if the moments structure is already set
within metricParams then they will not be recalculated.
vary_fmax : boolean, optional (default False)
If set to False the metric and rotations are calculated once, for the
full range of frequency [f_low,f_upper).
If set to True the metric and rotations are calculated multiple times,
for frequency ranges [f_low,f_low + i*vary_density), where i starts at
1 and runs up until f_low + (i+1)*vary_density > f_upper.
Thus values greater than f_upper are *not* computed.
The calculation for the full range [f_low,f_upper) is also done.
vary_density : float, optional
If vary_fmax is True, this will be used in computing the frequency
ranges as described for vary_fmax.
Returns
--------
metricParams : metricParameters instance
Structure holding all the options for construction of the metric.
**THIS FUNCTION ONLY RETURNS THE CLASS**
The following will be **added** to this structure
metricParams.evals : Dictionary of numpy.array
Each entry in the dictionary corresponds to the different frequency
ranges described in vary_fmax. If vary_fmax = False, the only entry
will be f_upper, this corresponds to integrals in [f_low,f_upper). This
entry is always present. Each other entry will use floats as keys to
the dictionary. These floats give the upper frequency cutoff when it is
varying.
Each numpy.array contains the eigenvalues which, with the eigenvectors
in evecs, are needed to rotate the
coordinate system to one in which the metric is the identity matrix.
metricParams.evecs : Dictionary of numpy.matrix
Each entry in the dictionary is as described under evals.
Each numpy.matrix contains the eigenvectors which, with the eigenvalues
in evals, are needed to rotate the
coordinate system to one in which the metric is the identity matrix.
metricParams.metric : Dictionary of numpy.matrix
Each entry in the dictionary is as described under evals.
Each numpy.matrix contains the metric of the parameter space in the
Lambda_i coordinate system.
metricParams.moments : Moments structure
See the structure documentation for a description of this. This
contains the result of all the integrals used in computing the metrics
above. It can be used for the ethinca components calculation, or other
similar calculations.
"""
evals = {}
evecs = {}
metric = {}
unmax_metric = {}
# First step is to get the moments needed to calculate the metric
if not (metricParams.moments and preserveMoments):
get_moments(metricParams, vary_fmax=vary_fmax,
vary_density=vary_density)
# What values are going to be in the moments
# J7 is the normalization factor so it *MUST* be present
list = metricParams.moments['J7'].keys()
# We start looping over every item in the list of metrics
for item in list:
# Here we convert the moments into a form easier to use here
Js = {}
for i in range(-7,18):
Js[i] = metricParams.moments['J%d'%(i)][item]
logJs = {}
for i in range(-1,18):
logJs[i] = metricParams.moments['log%d'%(i)][item]
loglogJs = {}
for i in range(-1,18):
loglogJs[i] = metricParams.moments['loglog%d'%(i)][item]
logloglogJs = {}
for i in range(-1,18):
logloglogJs[i] = metricParams.moments['logloglog%d'%(i)][item]
loglogloglogJs = {}
for i in range(-1,18):
loglogloglogJs[i] = metricParams.moments['loglogloglog%d'%(i)][item]
mapping = generate_mapping(metricParams.pnOrder)
# Calculate the metric
gs, unmax_metric_curr = calculate_metric(Js, logJs, loglogJs,
logloglogJs, loglogloglogJs, mapping)
metric[item] = gs
unmax_metric[item] = unmax_metric_curr
# And the eigenvalues
evals[item], evecs[item] = numpy.linalg.eig(gs)
# Numerical error can lead to small negative eigenvalues.
for i in range(len(evals[item])):
if evals[item][i] < 0:
# Due to numerical imprecision the very small eigenvalues can
# be negative. Make these positive.
evals[item][i] = -evals[item][i]
if evecs[item][i,i] < 0:
# We demand a convention that all diagonal terms in the matrix
# of eigenvalues are positive.
# This is done to help visualization of the spaces (increasing
# mchirp always goes the same way)
evecs[item][:,i] = - evecs[item][:,i]
metricParams.evals = evals
metricParams.evecs = evecs
metricParams.metric = metric
metricParams.time_unprojected_metric = unmax_metric
return metricParams
[docs]
def get_moments(metricParams, vary_fmax=False, vary_density=None):
"""
This function will calculate the various integrals (moments) that are
needed to compute the metric used in template bank placement and
coincidence.
Parameters
-----------
metricParams : metricParameters instance
Structure holding all the options for construction of the metric.
vary_fmax : boolean, optional (default False)
If set to False the metric and rotations are calculated once, for the
full range of frequency [f_low,f_upper).
If set to True the metric and rotations are calculated multiple times,
for frequency ranges [f_low,f_low + i*vary_density), where i starts at
1 and runs up until f_low + (i+1)*vary_density > f_upper.
Thus values greater than f_upper are *not* computed.
The calculation for the full range [f_low,f_upper) is also done.
vary_density : float, optional
If vary_fmax is True, this will be used in computing the frequency
ranges as described for vary_fmax.
Returns
--------
None : None
**THIS FUNCTION RETURNS NOTHING**
The following will be **added** to the metricParams structure
metricParams.moments : Moments structure
This contains the result of all the integrals used in computing the
metrics above. It can be used for the ethinca components calculation,
or other similar calculations. This is composed of two compound
dictionaries. The first entry indicates which moment is being
calculated and the second entry indicates the upper frequency cutoff
that was used.
In all cases x = f/f0.
For the first entries the options are:
moments['J%d' %(i)][f_cutoff]
This stores the integral of
x**((-i)/3.) * delta X / PSD(x)
moments['log%d' %(i)][f_cutoff]
This stores the integral of
(numpy.log(x**(1./3.))) x**((-i)/3.) * delta X / PSD(x)
moments['loglog%d' %(i)][f_cutoff]
This stores the integral of
(numpy.log(x**(1./3.)))**2 x**((-i)/3.) * delta X / PSD(x)
moments['loglog%d' %(i)][f_cutoff]
This stores the integral of
(numpy.log(x**(1./3.)))**3 x**((-i)/3.) * delta X / PSD(x)
moments['loglog%d' %(i)][f_cutoff]
This stores the integral of
(numpy.log(x**(1./3.)))**4 x**((-i)/3.) * delta X / PSD(x)
The second entry stores the frequency cutoff used when computing
the integral. See description of the vary_fmax option above.
All of these values are nomralized by a factor of
x**((-7)/3.) * delta X / PSD(x)
The normalization factor can be obtained in
moments['I7'][f_cutoff]
"""
# NOTE: Unless the TaylorR2F4 metric is used the log^3 and log^4 terms are
# not needed. As this calculation is not too slow compared to bank
# placement we just do this anyway.
psd_amp = metricParams.psd.data
psd_f = numpy.arange(len(psd_amp), dtype=float) * metricParams.deltaF
new_f, new_amp = interpolate_psd(psd_f, psd_amp, metricParams.deltaF)
# Need I7 first as this is the normalization factor
funct = lambda x,f0: 1
I7 = calculate_moment(new_f, new_amp, metricParams.fLow, \
metricParams.fUpper, metricParams.f0, funct,\
vary_fmax=vary_fmax, vary_density=vary_density)
# Do all the J moments
moments = {}
moments['I7'] = I7
for i in range(-7,18):
funct = lambda x,f0: x**((-i+7)/3.)
moments['J%d' %(i)] = calculate_moment(new_f, new_amp, \
metricParams.fLow, metricParams.fUpper, \
metricParams.f0, funct, norm=I7, \
vary_fmax=vary_fmax, vary_density=vary_density)
# Do the logx multiplied by some power terms
for i in range(-1,18):
funct = lambda x,f0: (numpy.log((x*f0)**(1./3.))) * x**((-i+7)/3.)
moments['log%d' %(i)] = calculate_moment(new_f, new_amp, \
metricParams.fLow, metricParams.fUpper, \
metricParams.f0, funct, norm=I7, \
vary_fmax=vary_fmax, vary_density=vary_density)
# Do the loglog term
for i in range(-1,18):
funct = lambda x,f0: (numpy.log((x*f0)**(1./3.)))**2 * x**((-i+7)/3.)
moments['loglog%d' %(i)] = calculate_moment(new_f, new_amp, \
metricParams.fLow, metricParams.fUpper, \
metricParams.f0, funct, norm=I7, \
vary_fmax=vary_fmax, vary_density=vary_density)
# Do the logloglog term
for i in range(-1,18):
funct = lambda x,f0: (numpy.log((x*f0)**(1./3.)))**3 * x**((-i+7)/3.)
moments['logloglog%d' %(i)] = calculate_moment(new_f, new_amp, \
metricParams.fLow, metricParams.fUpper, \
metricParams.f0, funct, norm=I7, \
vary_fmax=vary_fmax, vary_density=vary_density)
# Do the logloglog term
for i in range(-1,18):
funct = lambda x,f0: (numpy.log((x*f0)**(1./3.)))**4 * x**((-i+7)/3.)
moments['loglogloglog%d' %(i)] = calculate_moment(new_f, new_amp, \
metricParams.fLow, metricParams.fUpper, \
metricParams.f0, funct, norm=I7, \
vary_fmax=vary_fmax, vary_density=vary_density)
metricParams.moments = moments
[docs]
def interpolate_psd(psd_f, psd_amp, deltaF):
"""
Function to interpolate a PSD to a different value of deltaF. Uses linear
interpolation.
Parameters
----------
psd_f : numpy.array or list or similar
List of the frequencies contained within the PSD.
psd_amp : numpy.array or list or similar
List of the PSD values at the frequencies in psd_f.
deltaF : float
Value of deltaF to interpolate the PSD to.
Returns
--------
new_psd_f : numpy.array
Array of the frequencies contained within the interpolated PSD
new_psd_amp : numpy.array
Array of the interpolated PSD values at the frequencies in new_psd_f.
"""
# In some cases this will be a no-op. I thought about removing this, but
# this function can take unequally sampled PSDs and it is difficult to
# check for this. As this function runs quickly anyway (compared to the
# moment calculation) I decided to always interpolate.
new_psd_f = []
new_psd_amp = []
fcurr = psd_f[0]
for i in range(len(psd_f) - 1):
f_low = psd_f[i]
f_high = psd_f[i+1]
amp_low = psd_amp[i]
amp_high = psd_amp[i+1]
while(1):
if fcurr > f_high:
break
new_psd_f.append(fcurr)
gradient = (amp_high - amp_low) / (f_high - f_low)
fDiff = fcurr - f_low
new_psd_amp.append(amp_low + fDiff * gradient)
fcurr = fcurr + deltaF
return numpy.asarray(new_psd_f), numpy.asarray(new_psd_amp)
[docs]
def calculate_moment(psd_f, psd_amp, fmin, fmax, f0, funct,
norm=None, vary_fmax=False, vary_density=None):
"""
Function for calculating one of the integrals used to construct a template
bank placement metric. The integral calculated will be
\int funct(x) * (psd_x)**(-7./3.) * delta_x / PSD(x)
where x = f / f0. The lower frequency cutoff is given by fmin, see
the parameters below for details on how the upper frequency cutoff is
chosen
Parameters
-----------
psd_f : numpy.array
numpy array holding the set of evenly spaced frequencies used in the PSD
psd_amp : numpy.array
numpy array holding the PSD values corresponding to the psd_f
frequencies
fmin : float
The lower frequency cutoff used in the calculation of the integrals
used to obtain the metric.
fmax : float
The upper frequency cutoff used in the calculation of the integrals
used to obtain the metric. This can be varied (see the vary_fmax
option below).
f0 : float
This is an arbitrary scaling factor introduced to avoid the potential
for numerical overflow when calculating this. Generally the default
value (70) is safe here. **IMPORTANT, if you want to calculate the
ethinca metric components later this MUST be set equal to f_low.**
funct : Lambda function
The function to use when computing the integral as described above.
norm : Dictionary of floats
If given then moment[f_cutoff] will be divided by norm[f_cutoff]
vary_fmax : boolean, optional (default False)
If set to False the metric and rotations are calculated once, for the
full range of frequency [f_low,f_upper).
If set to True the metric and rotations are calculated multiple times,
for frequency ranges [f_low,f_low + i*vary_density), where i starts at
1 and runs up until f_low + (i+1)*vary_density > f_upper.
Thus values greater than f_upper are *not* computed.
The calculation for the full range [f_low,f_upper) is also done.
vary_density : float, optional
If vary_fmax is True, this will be used in computing the frequency
ranges as described for vary_fmax.
Returns
--------
moment : Dictionary of floats
moment[f_cutoff] will store the value of the moment at the frequency
cutoff given by f_cutoff.
"""
# Must ensure deltaF in psd_f is constant
psd_x = psd_f / f0
deltax = psd_x[1] - psd_x[0]
mask = numpy.logical_and(psd_f > fmin, psd_f < fmax)
psdf_red = psd_f[mask]
comps_red = psd_x[mask] ** (-7./3.) * funct(psd_x[mask], f0) * deltax / \
psd_amp[mask]
moment = {}
moment[fmax] = comps_red.sum()
if norm:
moment[fmax] = moment[fmax] / norm[fmax]
if vary_fmax:
for t_fmax in numpy.arange(fmin + vary_density, fmax, vary_density):
moment[t_fmax] = comps_red[psdf_red < t_fmax].sum()
if norm:
moment[t_fmax] = moment[t_fmax] / norm[t_fmax]
return moment
[docs]
def calculate_metric(Js, logJs, loglogJs, logloglogJs, loglogloglogJs, \
mapping):
"""
This function will take the various integrals calculated by get_moments and
convert this into a metric for the appropriate parameter space.
Parameters
-----------
Js : Dictionary
The list of (log^0 x) * x**(-i/3) integrals computed by get_moments()
The index is Js[i]
logJs : Dictionary
The list of (log^1 x) * x**(-i/3) integrals computed by get_moments()
The index is logJs[i]
loglogJs : Dictionary
The list of (log^2 x) * x**(-i/3) integrals computed by get_moments()
The index is loglogJs[i]
logloglogJs : Dictionary
The list of (log^3 x) * x**(-i/3) integrals computed by get_moments()
The index is logloglogJs[i]
loglogloglogJs : Dictionary
The list of (log^4 x) * x**(-i/3) integrals computed by get_moments()
The index is loglogloglogJs[i]
mapping : dictionary
Used to identify which Lambda components are active in this parameter
space and map these to entries in the metric matrix.
Returns
--------
metric : numpy.matrix
The resulting metric.
"""
# How many dimensions in the parameter space?
maxLen = len(mapping.keys())
metric = numpy.zeros(shape=(maxLen,maxLen), dtype=float)
unmax_metric = numpy.zeros(shape=(maxLen+1,maxLen+1), dtype=float)
for i in range(16):
for j in range(16):
calculate_metric_comp(metric, unmax_metric, i, j, Js,
logJs, loglogJs, logloglogJs,
loglogloglogJs, mapping)
return metric, unmax_metric
[docs]
def calculate_metric_comp(gs, unmax_metric, i, j, Js, logJs, loglogJs,
logloglogJs, loglogloglogJs, mapping):
"""
Used to compute part of the metric. Only call this from within
calculate_metric(). Please see the documentation for that function.
"""
# Time term in unmax_metric. Note that these terms are recomputed a bunch
# of time, but this cost is insignificant compared to computing the moments
unmax_metric[-1,-1] = (Js[1] - Js[4]*Js[4])
# Normal terms
if 'Lambda%d'%i in mapping and 'Lambda%d'%j in mapping:
gammaij = Js[17-i-j] - Js[12-i]*Js[12-j]
gamma0i = (Js[9-i] - Js[4]*Js[12-i])
gamma0j = (Js[9-j] - Js[4] * Js[12-j])
gs[mapping['Lambda%d'%i],mapping['Lambda%d'%j]] = \
0.5 * (gammaij - gamma0i*gamma0j/(Js[1] - Js[4]*Js[4]))
unmax_metric[mapping['Lambda%d'%i], -1] = gamma0i
unmax_metric[-1, mapping['Lambda%d'%j]] = gamma0j
unmax_metric[mapping['Lambda%d'%i],mapping['Lambda%d'%j]] = gammaij
# Normal,log cross terms
if 'Lambda%d'%i in mapping and 'LogLambda%d'%j in mapping:
gammaij = logJs[17-i-j] - logJs[12-j] * Js[12-i]
gamma0i = (Js[9-i] - Js[4] * Js[12-i])
gamma0j = logJs[9-j] - logJs[12-j] * Js[4]
gs[mapping['Lambda%d'%i],mapping['LogLambda%d'%j]] = \
gs[mapping['LogLambda%d'%j],mapping['Lambda%d'%i]] = \
0.5 * (gammaij - gamma0i*gamma0j/(Js[1] - Js[4]*Js[4]))
unmax_metric[mapping['Lambda%d'%i], -1] = gamma0i
unmax_metric[-1, mapping['Lambda%d'%i]] = gamma0i
unmax_metric[-1, mapping['LogLambda%d'%j]] = gamma0j
unmax_metric[mapping['LogLambda%d'%j], -1] = gamma0j
unmax_metric[mapping['Lambda%d'%i],mapping['LogLambda%d'%j]] = gammaij
unmax_metric[mapping['LogLambda%d'%j],mapping['Lambda%d'%i]] = gammaij
# Log,log terms
if 'LogLambda%d'%i in mapping and 'LogLambda%d'%j in mapping:
gammaij = loglogJs[17-i-j] - logJs[12-j] * logJs[12-i]
gamma0i = (logJs[9-i] - Js[4] * logJs[12-i])
gamma0j = logJs[9-j] - logJs[12-j] * Js[4]
gs[mapping['LogLambda%d'%i],mapping['LogLambda%d'%j]] = \
0.5 * (gammaij - gamma0i*gamma0j/(Js[1] - Js[4]*Js[4]))
unmax_metric[mapping['LogLambda%d'%i], -1] = gamma0i
unmax_metric[-1, mapping['LogLambda%d'%j]] = gamma0j
unmax_metric[mapping['LogLambda%d'%i],mapping['LogLambda%d'%j]] =\
gammaij
# Normal,loglog cross terms
if 'Lambda%d'%i in mapping and 'LogLogLambda%d'%j in mapping:
gammaij = loglogJs[17-i-j] - loglogJs[12-j] * Js[12-i]
gamma0i = (Js[9-i] - Js[4] * Js[12-i])
gamma0j = loglogJs[9-j] - loglogJs[12-j] * Js[4]
gs[mapping['Lambda%d'%i],mapping['LogLogLambda%d'%j]] = \
gs[mapping['LogLogLambda%d'%j],mapping['Lambda%d'%i]] = \
0.5 * (gammaij - gamma0i*gamma0j/(Js[1] - Js[4]*Js[4]))
unmax_metric[mapping['Lambda%d'%i], -1] = gamma0i
unmax_metric[-1, mapping['Lambda%d'%i]] = gamma0i
unmax_metric[-1, mapping['LogLogLambda%d'%j]] = gamma0j
unmax_metric[mapping['LogLogLambda%d'%j], -1] = gamma0j
unmax_metric[mapping['Lambda%d'%i],mapping['LogLogLambda%d'%j]] = \
gammaij
unmax_metric[mapping['LogLogLambda%d'%j],mapping['Lambda%d'%i]] = \
gammaij
# log,loglog cross terms
if 'LogLambda%d'%i in mapping and 'LogLogLambda%d'%j in mapping:
gammaij = logloglogJs[17-i-j] - loglogJs[12-j] * logJs[12-i]
gamma0i = (logJs[9-i] - Js[4] * logJs[12-i])
gamma0j = loglogJs[9-j] - loglogJs[12-j] * Js[4]
gs[mapping['LogLambda%d'%i],mapping['LogLogLambda%d'%j]] = \
gs[mapping['LogLogLambda%d'%j],mapping['LogLambda%d'%i]] = \
0.5 * (gammaij - gamma0i*gamma0j/(Js[1] - Js[4]*Js[4]))
unmax_metric[mapping['LogLambda%d'%i], -1] = gamma0i
unmax_metric[-1, mapping['LogLambda%d'%i]] = gamma0i
unmax_metric[-1, mapping['LogLogLambda%d'%j]] = gamma0j
unmax_metric[mapping['LogLogLambda%d'%j], -1] = gamma0j
unmax_metric[mapping['LogLambda%d'%i],mapping['LogLogLambda%d'%j]] = \
gammaij
unmax_metric[mapping['LogLogLambda%d'%j],mapping['LogLambda%d'%i]] = \
gammaij
# Loglog,loglog terms
if 'LogLogLambda%d'%i in mapping and 'LogLogLambda%d'%j in mapping:
gammaij = loglogloglogJs[17-i-j] - loglogJs[12-j] * loglogJs[12-i]
gamma0i = (loglogJs[9-i] - Js[4] * loglogJs[12-i])
gamma0j = loglogJs[9-j] - loglogJs[12-j] * Js[4]
gs[mapping['LogLogLambda%d'%i],mapping['LogLogLambda%d'%j]] = \
0.5 * (gammaij - gamma0i*gamma0j/(Js[1] - Js[4]*Js[4]))
unmax_metric[mapping['LogLogLambda%d'%i], -1] = gamma0i
unmax_metric[-1, mapping['LogLogLambda%d'%j]] = gamma0j
unmax_metric[mapping['LogLogLambda%d'%i],mapping['LogLogLambda%d'%j]] =\
gammaij