#!/usr/bin/env python
#
# FOLLOWUP TEST (if known injection)
#    util_PrintInjection.py --inj mdc.xml.gz --event 0 --verbose
#   util_PrintInjection.py --inj maxpt_zero_noise.xml.gz.xml.gz --event 0
"""
Integrate the extrinsic parameters of the prefactored likelihood function.
"""

import sys
import functools
from optparse import OptionParser, OptionGroup

import numpy
import numpy as np
try:
  import cupy
  xpy_default=cupy
  identity_convert = cupy.asnumpy
  identity_convert_togpu = cupy.asarray
  junk_to_check_installed = cupy.array(5)  # this will fail if GPU not installed correctly

  print(cupy.show_config())  # print provenance/debugging information
  cupy_success=True
except:
  print(' no cupy')
#  import numpy as cupy  # will automatically replace cupy calls with numpy!
  xpy_default=numpy  # just in case, to make replacement clear and to enable override
  identity_convert = lambda x: x  # trivial return itself
  identity_convert_togpu = lambda x:x
  cupy_success=False



import lal
from ligo.lw import utils, lsctables, table, ligolw
from ligo.lw.utils import process
import glue.lal
#import pylal

import RIFT.lalsimutils as lalsimutils
import RIFT.likelihood.factored_likelihood as factored_likelihood
import RIFT.integrators.mcsampler as mcsampler
import RIFT.misc.sky_rotations as sky_rotations
try:
    import RIFT.integrators.mcsamplerEnsemble as mcsamplerEnsemble
    mcsampler_gmm_ok = True
except:
    print(" No mcsamplerEnsemble ")
    mcsampler_gmm_ok = False
try:
    import RIFT.integrators.mcsamplerGPU as mcsamplerGPU
    mcsampler_gpu_ok = True
except:
    print( " No mcsamplerGPU ")
    mcsampler_gpu_ok = False

import RIFT.likelihood.priors_utils as priors_utils
import RIFT.misc.xmlutils as xmlutils


class EvenBivariateLinearInterpolator:
    def __init__(self, x0, dx, y0, dy, f):
        self._x0 = x0
        self._dx = dx
        self._y0 = y0
        self._dy = dy
        self._fgrid = xpy_default.asarray(f)

        self._dx_inv = 1.0 / self._dx
        self._dy_inv = 1.0 / self._dy

        self._N, self._M = self._fgrid.shape

    def __call__(self, x, y):
        # Compute the fractional indices into the lookup table where the free
        # parameters lie.
        i_mid = self._dx_inv * (x - self._x0)
        j_mid = self._dy_inv * (y - self._y0)

        # Compute the floor and ceiling of the fractional indices to get the
        # indices of the boundaries of the bin `x` and `y` lie in.
        # NOTE: In the case where `x` or `y` lie directly on a boundary, the
        # floor and ceiling will be equal, but the output is written in such a
        # way that we'd still get the correct result.
        i_lo = xpy_default.floor(i_mid).astype(int)
        j_lo = xpy_default.floor(j_mid).astype(int)
        i_hi = xpy_default.ceil(i_mid).astype(int)
        j_hi = xpy_default.ceil(j_mid).astype(int)

        # Compute just the fractional part of each index from the low and high
        # points.
        p = i_mid - i_lo
        q = j_mid - j_lo
        p_ = 1-p
        q_ = 1-q

        # Compute the interpolated result.
        f_approx =  p_*q_ * self._fgrid[i_lo,j_lo]
        f_approx += p *q_ * self._fgrid[i_hi,j_lo]
        f_approx += p_*q  * self._fgrid[i_lo,j_hi]
        f_approx += p *q  * self._fgrid[i_hi,j_hi]

        return f_approx


try:
  from ligo.skymap.io import fits as bfits
except:
  print(" -no bayestar- ")

__author__ = "Evan Ochsner <evano@gravity.phys.uwm.edu>, Chris Pankow <pankow@gravity.phys.uwm.edu>, R. O'Shaughnessy"

#
# Pinnable parameters -- for command line processing
#
LIKELIHOOD_PINNABLE_PARAMS = ["right_ascension", "declination", "psi", "distance", "phi_orb", "t_ref", "inclination"]

def get_pinned_params(opts):
    """
    Retrieve a dictionary of user pinned parameters and their pin values.
    """
    return dict([(p,v) for p, v in opts.__dict__.items() if p in LIKELIHOOD_PINNABLE_PARAMS and v is not None]) 

def get_unpinned_params(opts, params):
    """
    Retrieve a set of unpinned parameters.
    """
    return params - set([p for p, v in opts.__dict__.items() if p in LIKELIHOOD_PINNABLE_PARAMS and v is not None])

#
# Option parsing
#

optp = OptionParser()
optp.add_option("-c", "--cache-file", default=None, help="LIGO cache file containing all data needed.")
optp.add_option("-C", "--channel-name", action="append", help="instrument=channel-name, e.g. H1=FAKE-STRAIN. Can be given multiple times for different instruments.")
optp.add_option("-p", "--psd-file", action="append", help="instrument=psd-file, e.g. H1=H1_PSD.xml.gz. Can be given multiple times for different instruments.")
optp.add_option("-k", "--skymap-file", help="Use skymap stored in given FITS file.")
optp.add_option("-x", "--coinc-xml", help="gstlal_inspiral XML file containing coincidence information.")
optp.add_option("-I", "--sim-xml", help="XML file containing mass grid to be evaluated")
optp.add_option("-E", "--event", default=0,type=int, help="Event number used for this run")
optp.add_option("--soft-fail-event-range",action='store_true',help='Soft failure (exit 0) if event ID is out of range. This happens in pipelines, if we have pre-built a DAG attempting to analyze more points than we really have')
optp.add_option("-f", "--reference-freq", type=float, default=100.0, help="Waveform reference frequency. Required, default is 100 Hz.")
optp.add_option("--fmin-template", dest='fmin_template', type=float, default=40, help="Waveform starting frequency (for 22 mode).  Default is 40 Hz. Also equal to starting frequency for integration") 
optp.add_option("--fmin-template-correct-for-lmax",action='store_true',help="Modify amount of data selected, waveform starting frequency to account for l-max, to better insure all requested modes start within the targeted band")
optp.add_option("--fmin-ifo", action='append' , help="Minimum frequency for each IFO. Implemented by setting the PSD=0 below this cutoff. Use with care.") 
#optp.add_option("--nr-params",default=None, help="List of specific NR parameters and groups (and masses?) to use for the grid.")
#optp.add_option("--nr-index",type=int,default=-1,help="Index of specific NR simulation to use [integer]. Mass used: mtot= m1+m2")
optp.add_option('--nr-group', default=None,help="If using a *ssingle specific simulation* specified on the command line, provide it here")
optp.add_option('--nr-param', default=None,help="If using a *ssingle specific simulation* specified on the command line, provide it here")
optp.add_option("--nr-lookup",action='store_true', help=" Look up parameters from an NR catalog, instead of using the approximant specified")
optp.add_option("--nr-lookup-group",action='append', help="Restriction on 'group' for NR lookup")
optp.add_option("--nr-hybrid-use",action='store_true',help="Enable use of NR (or ROM!) hybrid, using --approx as the default approximant and with a frequency fmin")
optp.add_option("--nr-hybrid-method",default="taper_add",help="Hybridization method for NR (or ROM!).  Passed through to LALHybrid. pseudo_aligned_from22 will provide ad-hoc higher modes, if the early-time hybridization model only includes the 22 mode")
optp.add_option("--rom-group",default=None)
optp.add_option("--rom-param",default=None)
optp.add_option("--rom-use-basis",default=False,action='store_true',help="Use the ROM basis for inner products.")
optp.add_option("--rom-limit-basis-size-to",default=None,type=int)
optp.add_option("--rom-integrate-intrinsic",default=False,action='store_true',help='Integrate over intrinsic variables. REQUIRES rom_use_basis at present. ONLY integrates in mass ratio as present')
optp.add_option("--nr-perturbative-extraction",default=False,action='store_true')
optp.add_option("--nr-use-provided-strain",default=False,action='store_true')
optp.add_option("--no-memory",default=False,action='store_true', help="At present, turns off m=0 modes. Use with EXTREME caution only if requested by model developer")
optp.add_option("--use-external-EOB",default=False,action='store_true')
optp.add_option("--maximize-only",default=False, action='store_true',help="After integrating, attempts to find the single best fitting point")
optp.add_option("--dump-lnL-time-series",default=False, action='store_true',help="(requires --sim-xml) Dump lnL(t) at the injected parameters")
optp.add_option("-a", "--approximant", default="TaylorT4", help="Waveform family to use for templates. Any approximant implemented in LALSimulation is valid.")
optp.add_option("-A", "--amp-order", type=int, default=0, help="Include amplitude corrections in template waveforms up to this e.g. (e.g. 5 <==> 2.5PN), default is Newtonian order.")
optp.add_option("--l-max", type=int, default=2, help="Include all (l,m) modes with l less than or equal to this value.")
optp.add_option("-s", "--data-start-time", type=float, default=None, help="GPS start time of data segment. If given, must also give --data-end-time. If not given, sane start and end time will automatically be chosen.")
optp.add_option("-e", "--data-end-time", type=float, default=None, help="GPS end time of data segment. If given, must also give --data-start-time. If not given, sane start and end time will automatically be chosen.")
optp.add_option("--data-integration-window-half",default=75*1e-3,type=float,help="Only change this window size if you are an expert. The window for time integration is -/+ this quantity around the event time")
optp.add_option("-F", "--fmax", type=float, help="Upper frequency of signal integration. Default is use PSD's maximum frequency.")
optp.add_option("--srate",default=16384,type=int,help="Sampling rate. Change ONLY IF YOU ARE ABSOLUTELY SURE YOU KNOW WHAT YOU ARE DOING.")
optp.add_option("-t", "--event-time", type=float, help="GPS time of the event --- probably the end time. Required if --coinc-xml not given.")
optp.add_option("-i", "--inv-spec-trunc-time", type=float, default=8., help="Timescale of inverse spectrum truncation in seconds (Default is 8 - give 0 for no truncation)")
optp.add_option("-w", "--window-shape", type=float, default=0, help="Shape of Tukey window to apply to data (default is no windowing)")
optp.add_option("-m", "--time-marginalization", action="store_true", help="Perform marginalization over time via direct numerical integration. Default is false.")
optp.add_option("-d", "--distance-marginalization", action="store_true", help="Perform marginalization over distance via a look-up table. Default is false.")
optp.add_option("-l", "--distance-marginalization-lookup-table", default=None, help="Look-up table for distance marginalization.")
optp.add_option("--vectorized", action="store_true", help="Perform manipulations of lm and timeseries using numpy arrays, not LAL data structures.  (Combine with --gpu to enable GPU use, where available)")
optp.add_option("--gpu", action="store_true", help="Perform manipulations of lm and timeseries using numpy arrays, CONVERTING TO GPU when available. You MUST use this option with --vectorized (otherwise it is a no-op). You MUST have a suitable version of cupy installed, your cuda operational, etc")
optp.add_option("--force-xpy", action="store_true", help="Use the xpy code path.  Use with --vectorized --gpu to use the fallback CPU-based code path. Useful for debugging.")
optp.add_option("-o", "--output-file", help="Save result to this file.")
optp.add_option("-O", "--output-format", default='xml', help="[xml|hdf5]")
optp.add_option("-S", "--save-samples", action="store_true", help="Save sample points to output-file. Requires --output-file to be defined.")
optp.add_option("-L", "--save-deltalnL", type=float, default=float("Inf"), help="Threshold on deltalnL for points preserved in output file.  Requires --output-file to be defined")
optp.add_option("-P", "--save-P", type=float,default=0, help="Threshold on cumulative probability for points preserved in output file.  Requires --output-file to be defined")
optp.add_option("--verbose",action='store_true')

#
# Add the integration options
#
integration_params = OptionGroup(optp, "Integration Parameters", "Control the integration with these options.")
# Default is actually None, but that tells the integrator to go forever or until n_eff is hit.
integration_params.add_option("--n-max", type=int, help="Total number of samples points to draw. If this number is hit before n_eff, then the integration will terminate. Default is 'infinite'.",default=1e7)
integration_params.add_option("--n-eff", type=int, default=100, help="Total number of effective samples points to calculate before the integration will terminate. Default is 100")
integration_params.add_option("--fairdraw-extrinsic-output", action='store_true' , help="Output is fair draw, rather than being comprehensive")
integration_params.add_option("--n-chunk", type=int, help="Chunk'.",default=10000)
integration_params.add_option("--convergence-tests-on",default=False,action='store_true')
integration_params.add_option("--seed", type=int, help="Random seed to use. Default is to not seed the RNG.")
integration_params.add_option("--no-adapt", action="store_true", help="Turn off adaptive sampling. Adaptive sampling is on by default.")
integration_params.add_option("--no-adapt-distance", action="store_true", help="Turn off adaptive sampling, just for distance. Adaptive sampling is on by default.")
integration_params.add_option("--adapt-weight-exponent", type=float, default=1.0, help="Exponent to use with weights (likelihood integrand) when doing adaptive sampling. Used in tandem with --adapt-floor-level to prevent overconvergence. Default is 1.0.")
integration_params.add_option("--adapt-floor-level", type=float, default=0.1, help="Floor to use with weights (likelihood integrand) when doing adaptive sampling. This is necessary to ensure the *sampling* prior is non zero during adaptive sampling and to prevent overconvergence. Default is 0.1 (no floor)")
integration_params.add_option("--adapt-adapt",action='store_true',help="Adapt the tempering exponent")
integration_params.add_option("--adapt-log",action='store_true',help="Use a logarithmic tempering exponent")
integration_params.add_option("--interpolate-time", default=False,help="If using time marginalization, compute using a continuously-interpolated array. (Default=false)")
integration_params.add_option("--d-prior",default='Euclidean' ,type=str,help="Distance prior for dL.  Options are dL^2 (Euclidean) and 'pseudo-cosmo'  .")
integration_params.add_option("--d-max", default=10000,type=float,help="Maximum distance in volume integral. Used to SET THE PRIOR; changing this value changes the numerical answer.")
integration_params.add_option("--d-min", default=1,type=float,help="Minimum distance in volume integral. Used to SET THE PRIOR; changing this value changes the numerical answer.")
integration_params.add_option("--declination-cosine-sampler",action='store_true',help="If specified, the parameter used for declination is cos(dec), not dec")
integration_params.add_option("--inclination-cosine-sampler",action='store_true',help="If specified, the parameter used for inclination is cos(dec), not dec")
integration_params.add_option("--internal-sky-network-coordinates",action='store_true',help="If specified, perform integration in sky coordinates aligned with the first two IFOs provided")
integration_params.add_option("--internal-sky-network-coordinates-raw",action='store_true',help="If specified, does not attempt to organize IFO network sensibly, uses them AS PROVIDED IN ORDER.")
integration_params.add_option("--manual-logarithm-offset",type=float,default=0,help="Target value of logarithm lnL. Integrand is reduced by exp(-manual_logarithm_offset).  Important for high-SNR sources!   Should be set dynamically")
integration_params.add_option("--sampler-method",default="adaptive_cartesian",help="adaptive_cartesian|GMM|adaptive_cartesian_gpu")
integration_params.add_option("--sampler-xpy",default=None,help="numpy|cupy  if the adaptive_cartesian_gpu sampler is active, use that.")
optp.add_option_group(integration_params)

#
# Add the intrinsic parameters
#
intrinsic_params = OptionGroup(optp, "Intrinsic Parameters", "Intrinsic parameters (e.g component mass) to use.")
intrinsic_params.add_option("--pin-to-sim", help="Pin values to sim_inspiral table entry.")
intrinsic_params.add_option("--pin-distance-to-sim",action='store_true', help="Pin *distance* value to sim entry. Used to enable source frame reconstruction with NR.")
intrinsic_params.add_option("--mass1", type=float, help="Value of first component mass, in solar masses. Required if not providing coinc tables.")
intrinsic_params.add_option("--mass2", type=float, help="Value of second component mass, in solar masses. Required if not providing coinc tables.")
intrinsic_params.add_option("--eff-lambda", type=float, help="Value of effective tidal parameter. Optional, ignored if not given.")
intrinsic_params.add_option("--deff-lambda", type=float, help="Value of second effective tidal parameter. Optional, ignored if not given")
optp.add_option_group(intrinsic_params)


#
# Add options to integrate over intrinsic parameters.  Same conventions as util_ManualOverlapGrid.py.  
# Parameters have special names, and we adopt priors that use those names.
# NOTE: Only 'q' implemented
#
intrinsic_int_params = OptionGroup(optp, "Intrinsic integrated parameters", "Intrinsic parameters to integrate over. ONLY currently used with ROM version")
intrinsic_int_params.add_option("--parameter",action='append')
intrinsic_int_params.add_option("--parameter-range",action='append',type=str)
intrinsic_int_params.add_option("--adapt-intrinsic",action='store_true')
optp.add_option_group(intrinsic_int_params)


#
# Add the pinnable parameters
#
pinnable = OptionGroup(optp, "Pinnable Parameters", "Specifying these command line options will pin the value of that parameter to the specified value with a probability of unity.")
for pin_param in LIKELIHOOD_PINNABLE_PARAMS:
    option = "--" + pin_param.replace("_", "-")
    pinnable.add_option(option, type=float, help="Pin the value of %s." % pin_param)
optp.add_option_group(pinnable)

opts, args = optp.parse_args()

if opts.gpu and xpy_default is numpy:
    print( " Override --gpu  (not available);  use --force-xpy to require the identical code path is used (with xpy =[np|cupy]")
    opts.gpu=False
    if opts.force_xpy:
        opts.gpu=True


manual_avoid_overflow_logarithm=opts.manual_logarithm_offset

deltaT = None
fSample= opts.srate # change sampling rate
if not(fSample is None):
  deltaT =1./fSample

intrinsic_param_names = opts.parameter
valid_intrinsic_param_names = ['q']
if intrinsic_param_names:
 for param in intrinsic_param_names:
    
    # Check if in the valid list
    if not(param in valid_intrinsic_param_names):
            print( ' Invalid param ', param, ' not in ', valid_intrinsic_param_names)
            sys.exit(0)
    param_ranges = []
    if len(intrinsic_param_names) == len(opts.parameter_range):
        param_ranges = numpy.array(map(eval, opts.parameter_range))
        # Rescale mass-dependent ranges to SI units
        for p in ['mc', 'm1', 'm2', 'mtot']:
          if p in intrinsic_param_names:
            indx = intrinsic_param_names.index(p)
            param_ranges[indx]= numpy.array(param_ranges[indx])* lal.MSUN_SI



# Check both or neither of --data-start/end-time given
if opts.data_start_time is None and opts.data_end_time is not None:
    raise ValueError("You must provide both or neither of --data-start-time and --data-end-time.")
if opts.data_end_time is None and opts.data_start_time is not None:
    raise ValueError("You must provide both or neither of --data-start-time and --data-end-time.")

#
# Import NR grid
#
NR_template_group=None
NR_template_param=None
if opts.nr_group and opts.nr_param:
    import NRWaveformCatalogManager3 as nrwf
    NR_template_group = opts.nr_group
    if nrwf.internal_ParametersAreExpressions[NR_template_group]:
        NR_template_param = eval(opts.nr_param)
    else:
        NR_template_param = opts.nr_param
# if opts.nr_params and opts.nr_index>-1:
#     import re
#     datNR=[]
#     with open(opts.nr_params, "r") as f:
#         for line in f:
#             line=line.strip()
#             line = line.partition('#')[0]
#             line=re.split('\s+', line)
#             if len(line)>=3:
#                     datNR.append(line[0:3])    # take first 3 - required.


#
# Hardcoded variables
#
template_min_freq = opts.fmin_template # minimum frequency of template
#t_ref_wind = 50e-3 # Interpolate in a window +/- this width about event time. 
t_ref_wind = opts.data_integration_window_half
T_safety = 2. # Safety buffer (in sec) for wraparound corruption

#
# Inverse spectrum truncation control
#
T_spec = opts.inv_spec_trunc_time
if T_spec == 0.: # Do not do inverse spectrum truncation
    inv_spec_trunc_Q = False
    T_safety += 8. # Add a bit more safety buffer in this case
else:
    inv_spec_trunc_Q = True

#
# Integrator options
#
n_max = opts.n_max # Max number of extrinsic points to evaluate at
n_eff = opts.n_eff # Effective number of points evaluated

#
# Initialize the RNG, if needed
#
# TODO: Do we seed a given instance of the integrator, or set it for all
# or both?
if opts.seed is not None:
    numpy.random.seed(opts.seed)

#
# Gather information about a injection put in the data
#
if opts.pin_to_sim is not None:
    xmldoc = utils.load_filename(opts.pin_to_sim)
    sim_table = table.get_table(xmldoc, lsctables.SimInspiralTable.tableName)
    assert len(sim_table) == 1
    sim_row = sim_table[0]

#
# Gather information from the detection pipeline
#
if opts.coinc_xml is not None:
    xmldoc = utils.load_filename(opts.coinc_xml)
    coinc_table = table.get_table(xmldoc, lsctables.CoincInspiralTable.tableName)
    assert len(coinc_table) == 1
    coinc_row = coinc_table[0]
    event_time = coinc_row.get_end()
    print( "Coinc XML loaded, event time: %s" % str(coinc_row.get_end()))
elif opts.event_time is not None:
    event_time = glue.lal.LIGOTimeGPS(opts.event_time)
    print( "Event time from command line: %s" % str(event_time))
else:
    raise ValueError("Either --coinc-xml or --event-time must be provided to parse event time.")


#
# Set masses 
#
if opts.mass1 is not None and opts.mass2 is not None:
    m1, m2 = opts.mass1, opts.mass2
elif opts.coinc_xml is not None:
    sngl_inspiral_table = table.get_table(xmldoc, lsctables.SnglInspiralTable.tableName)
    assert len(sngl_inspiral_table) == len(coinc_row.ifos.split(","))
    m1, m2 = None, None
    for sngl_row in sngl_inspiral_table:
        # NOTE: gstlal is exact match, but other pipelines may not be
        assert m1 is None or (sngl_row.mass1 == m1 and sngl_row.mass2 == m2)
        m1, m2 = sngl_row.mass1, sngl_row.mass2
elif opts.sim_xml:
    True
else:
    raise ValueError("Need either --mass1 --mass2, --coinc-xml, or --sim-xml to retrieve masses.")


#
# Template descriptors
#

fiducial_epoch = lal.LIGOTimeGPS()
fiducial_epoch = event_time.seconds + 1e-9*event_time.nanoseconds   # no more direct access to gpsSeconds

# Struct to hold template parameters
if opts.sim_xml:
    print( "====Loading injection XML:", opts.sim_xml, opts.event, " =======")
    P_list = lalsimutils.xml_to_ChooseWaveformParams_array(str(opts.sim_xml))
    if len(P_list) < opts.event  and opts.soft_fail_event_range:
        print( " Event out of range; soft exit")
        sys.exit(0)
    P = P_list[opts.event]  # Load in the physical parameters of the injection.  
    P.radec =False  # do NOT propagate the epoch later
    P.fref = opts.reference_freq
    P.fmin = template_min_freq
    P.tref = fiducial_epoch  # the XML table
    m1 = P.m1/lal.MSUN_SI
    m2 =P.m2/lal.MSUN_SI
    lambda1, lambda2 = P.lambda1,P.lambda2
    if  opts.pin_distance_to_sim:
        dist_in =P.dist
        opts.distance = dist_in/lal.PC_SI/1e6  # create de facto pinnable parrameter. Use Mpc unit
    P.dist = factored_likelihood.distMpcRef * 1.e6 * lal.PC_SI   # use *nonstandard* distance
    P.phi=0.0
    P.psi=0.0
    P.incl = 0.0       # only works for aligned spins. Be careful.
    P.fref = opts.reference_freq
    if opts.approximant != "TaylorT4": # not default setting
        P.approx = lalsimutils.lalsim.GetApproximantFromString(opts.approximant)  # allow user to override the approx setting. Important for NR followup, where no approx set in sim_xml!
else:
 lambda1, lambda2 = 0, 0
 if opts.eff_lambda is not None:
        lambda1, lambda2 = lalsimutils.tidal_lambda_from_tilde(m1, m2, opts.eff_lambda, opts.deff_lambda or 0)
 P = lalsimutils.ChooseWaveformParams(
	approx = lalsimutils.lalsim.GetApproximantFromString(opts.approximant),
    fmin = template_min_freq,
    radec = False,   # do NOT propagate the epoch later
    incl = 0.0,       # only works for aligned spins. Be careful.
    phiref = 0.0,
    theta = 0.0,
    phi = 0.0,
    psi = 0.0,
    m1 = m1 * lal.MSUN_SI,
    m2 = m2 * lal.MSUN_SI,
    lambda1 = lambda1,
    lambda2 = lambda2,
    ampO = opts.amp_order,
    fref = opts.reference_freq,
    tref = fiducial_epoch,
    dist = factored_likelihood.distMpcRef * 1.e6 * lal.PC_SI
    )

#
if 200./((m1+m2)/20) < template_min_freq:
    print( " WARNING: Minimum frequency is smaller than ISCO,  usually causeing template generation to fail")


print( " --- Template for intrinsic parameters ---- " )
P.print_params()

# User requested bounds for data segment
if not (opts.data_start_time == None) and  not (opts.data_end_time == None):
    start_time =  opts.data_start_time
    end_time =  opts.data_end_time
    print( "Fetching data segment with start=", start_time)
    print( "                             end=", end_time)

# Automatically choose data segment bounds so region of interest isn't corrupted
# FIXME: Use estimate, instead of painful full waveform generation call here.
else:
    approxTmp = P.approx
    LmaxEff = 2
    if opts.fmin_template_correct_for_lmax:
      LmaxEff=opts.l_max
    T_tmplt = lalsimutils.estimateWaveformDuration(P,LmaxEff) + 4  # Much more robust than the previous (slow, prone-to-crash approach)
#     if (m1+m2)<30:
#         P.approx = lalsimutils.lalsim.TaylorT4  # should not impact length much.  IMPORTANT because EOB calls will fail at the default sampling rate
#         htmplt = lalsimutils.hoft(P)   # Horribly wasteful waveform gneeration solely to estimate duration.  Will also crash if spins are used.
#         P.approx = approxTmp
#         T_tmplt = - float(htmplt.epoch)    # ASSUMES time returned by hlm is a RELATIVE time that does not add tref. P.radec=False !
#         print fiducial_epoch, event_time, htmplt.epoch
#     else:
#         print " Using estimate for waveform length in a high mass regime: beware!"
#         T_tmplt = lalsimutils.estimateWaveformDuration(P) + 4
    T_seg = T_tmplt + T_spec + T_safety # Amount before and after event time
#    if opts.use_external_EOB:
#        T_seg *=2   # extra safety factor
    start_time = float(event_time) - T_seg
    end_time = float(event_time) + T_seg
    print( "Fetching data segment with start=", start_time)
    print( "                             end=", end_time)
    print( "\t\tEvent time is: ", float(event_time))
    print( "\t\tT_seg is: ", T_seg)

#
# Load in data and PSDs
#
data_dict, psd_dict = {}, {}

for inst, chan in map(lambda c: c.split("="), opts.channel_name):
    print( "Reading channel %s from cache %s" % (inst+":"+chan, opts.cache_file))
    data_dict[inst] = lalsimutils.frame_data_to_non_herm_hoff(opts.cache_file,
            inst+":"+chan, start=start_time, stop=end_time,
            window_shape=opts.window_shape,verbose=opts.verbose,deltaT=deltaT)
    if opts.verbose:
        print( "Frequency binning: %f, length %d" % (data_dict[inst].deltaF,
            data_dict[inst].data.length))

flow_ifo_dict = {}
if opts.fmin_ifo:
 for inst, freq_str in map(lambda c: c.split("="), opts.fmin_ifo):
    freq_low_here = float(freq_str)
    print( "Reading low frequency cutoff for instrument %s from %s" % (inst, freq_str), freq_low_here)
    flow_ifo_dict[inst] = freq_low_here

for inst, psdf in map(lambda c: c.split("="), opts.psd_file):
    print( "Reading PSD for instrument %s from %s" % (inst, psdf))
    psd_dict[inst] = lalsimutils.get_psd_series_from_xmldoc(psdf, inst)

    deltaF = data_dict[inst].deltaF
    psd_dict[inst] = lalsimutils.resample_psd_series(psd_dict[inst], deltaF)
    print( "PSD deltaF after interpolation %f" % psd_dict[inst].deltaF)

    # implement cutoff.  
    if inst in flow_ifo_dict.keys():
        if isinstance(psd_dict[inst], lal.REAL8FrequencySeries):
            psd_fvals = psd_dict[inst].f0 + deltaF*numpy.arange(psd_dict[inst].data.length)
            psd_dict[inst].data.data[ psd_fvals < flow_ifo_dict[inst]] = 0 # 
        else:
            print( 'FAIL')
            sys.exit(0)
#        elif isinstance(psd_dict[inst], pylal.xlal.datatypes.real8frequencyseries.REAL8FrequencySeries):  # for backward compatibility
#            psd_fvals = psd_dict[inst].f0 + deltaF*numpy.arange(len(psd_dict[inst].data))
#            psd_dict[inst].data[psd_fvals < ifo_dict[inst]] =0


    assert psd_dict[inst].deltaF == deltaF

    # Highest freq. at which PSD is defined
    #if isinstance(psd_dict[inst],
    #        pylal.xlal.datatypes.real8frequencyseries.REAL8FrequencySeries):
    #    fmax = psd_dict[inst].f0 + deltaF * (len(psd_dict[inst].data) - 1)
    if isinstance(psd_dict[inst], lal.REAL8FrequencySeries):
        fmax = psd_dict[inst].f0 + deltaF * (psd_dict[inst].data.length - 1)

    # Assert upper limit of IP integral does not go past where PSD defined
    assert opts.fmax is None or opts.fmax<= fmax
    # Allow us to target a smaller upper limit than provided by the PSD. Important for numerical PSDs that turn over at high frequency
    if opts.fmax and opts.fmax < fmax:
        fmax = opts.fmax # fmax is now the upper freq. of IP integral

# Ensure data and PSDs keyed to same detectors
if sorted(psd_dict.keys()) != sorted(data_dict.keys()):
    print >>sys.stderr, "Got a different set of instruments based on data and PSDs provided."

# Ensure waveform has same sample rate, padded length as data
#
# N.B. This assumes all detector data has same sample rate, length
#
# data_dict holds 2-sided FrequencySeries, so their length is the same as
# that of the original TimeSeries that was FFT'd = Nsamples
# Also, deltaF = 1/T, with T = the duration (in sec) of the original TimeSeries
# Therefore 1/(data.length*deltaF) = T/Nsamples = deltaT
P.deltaT = 1./ (data_dict[list(data_dict.keys())[0]].data.length * deltaF)
P.deltaF = deltaF


#
# Perform the Precompute stage
#

# N.B. There is an implicit assumption all detectors use the same
# upper frequency limit for their inner product integrals
# N.B. P.fmin is being used to set the lower freq. limit of the IP integrals
# while in principal we may want to set it separately

if opts.nr_perturbative_extraction:
    print( " ------------ USING PERTURBATIVE EXTRACTION ------ ")

t_window = 0.15
rholms_intp, cross_terms, cross_terms_V,  rholms, rest=factored_likelihood.PrecomputeLikelihoodTerms(
        fiducial_epoch, t_window, P, data_dict, psd_dict, opts.l_max, fmax,
        False, inv_spec_trunc_Q, T_spec,
        NR_group=NR_template_group,NR_param=NR_template_param,
        use_external_EOB=opts.use_external_EOB,nr_lookup=opts.nr_lookup,nr_lookup_valid_groups=opts.nr_lookup_group,perturbative_extraction=opts.nr_perturbative_extraction,use_provided_strain=opts.nr_use_provided_strain,hybrid_use=opts.nr_hybrid_use,hybrid_method=opts.nr_hybrid_method,ROM_group=opts.rom_group,ROM_param=opts.rom_param,ROM_use_basis=opts.rom_use_basis,verbose=not opts.rom_integrate_intrinsic,ROM_limit_basis_size=opts.rom_limit_basis_size_to,no_memory=opts.no_memory,skip_interpolation=opts.vectorized)

### DEBUGGING CHECK: what keys are used (for rom)
if opts.rom_use_basis:
    detector_list = data_dict.keys()
    print( " Keys for each instrument (ROM sanity check) ", rholms_intp[detector_list[0]].keys())

if opts.rom_use_basis and not opts.rom_integrate_intrinsic:
    print( " ROM : Trivial use (fixed extrinsic): Reconstruct standard expressions ")
    # If ROM basis, transform to standard form, and proceed normally.
    # Note this is NOT allowing us to change intrinsic parameters
    # It is included to enable a code test: running with hlm directly provided by ROM, vs running with coefficient reconstruction
    rholms_intp, cross_terms, cross_terms_V, rholms, rest = factored_likelihood.ReconstructPrecomputedLikelihoodTermsROM(P, rest, rholms_intp, cross_terms, cross_terms_V, rholms,verbose=False)

if opts.dump_lnL_time_series and opts.sim_xml:
    # LOAD IN PARAMETERS
    P = lalsimutils.xml_to_ChooseWaveformParams_array(str(opts.sim_xml))[opts.event]  # Load in the physical parameters of the injection.  
    print( " ---- Compare at intrinsic parameters (masses irrelevant) ---- ")
    P.print_params()
    tvals = numpy.linspace(-2*t_ref_wind,2*t_ref_wind,numpy.sqrt(3)*4*t_ref_wind/P.deltaT)  # make sure I have enough points to oversample. Use irrational number, and oversample
    lnLvals = numpy.ones(len(tvals))
    for indx in numpy.arange(len(tvals)):
        P.tref = fiducial_epoch + tvals[indx]
        if opts.rom_integrate_intrinsic:
            rholms_intp_A, cross_terms_A, cross_terms_V_A, rholms_A, rest_A = factored_likelihood.ReconstructPrecomputedLikelihoodTermsROM(P, rest, rholms_intp, cross_terms, cross_terms_V, rholms,verbose=False)
            # No time-domain interpolation stored for ROMs,  must use raw data
            lnLvals[indx] = factored_likelihood.FactoredLogLikelihood(P, rholms_A, rholms_intp_A, cross_terms_A, cross_terms_V_A, opts.l_max,interpolate=False)
        else:
            lnLvals[indx] = factored_likelihood.FactoredLogLikelihood(P, rholms, rholms_intp, cross_terms, cross_terms_V, opts.l_max,interpolate=not opts.rom_use_basis)
    numpy.savetxt("lnL_time_dump"+opts.output_file+".dat", numpy.array([tvals+fiducial_epoch,lnLvals]).T)
    print( " Peak lnL value : ", numpy.max(lnLvals))
    sys.exit(0)


if opts.pin_to_sim:
    P.copy_lsctables_sim_inspiral(sim_row)
    print( "Pinned parameters from sim_inspiral")
    print( "\tRA", P.phi, sim_row.longitude )
    print( "\tdec", P.theta, sim_row.latitude )
    print( "\tt ref %d.%d" % (P.tref.gpsSeconds, P.tref.gpsNanoSeconds), sim_row.get_time_geocent())
    print( "\torb phase", P.phiref, sim_row.coa_phase) # ref. orbital phase
    print( "\tinclination", P.incl, sim_row.inclination) # inclination)
    print( "\tpsi", P.psi, sim_row.polarization) # polarization angle
    print( "\tdistance", P.dist/(1e6 * lalsimutils.lsu_PC), sim_row.distance)  # luminosity distance

    logL = factored_likelihood.FactoredLogLikelihood(P, rholms_intp, cross_terms, opts.l_max)
    print( "Pinned log likelihood: %g, (%g in \"SNR\")" % (logL, numpy.sqrt(2*logL)))
    tref = float(P.tref)
    tvals = numpy.arange(tref-0.01, tref+0.01, 0.00001)
    logLs = []
    for t in tvals:
        P.tref = lal.LIGOTimeGPS(t)
        logLs.append(factored_likelihood.FactoredLogLikelihood(P, rholms_intp, cross_terms, opts.l_max))
    import matplotlib
    matplotlib.use("Agg")
    from matplotlib import pyplot
    print( "Maximum logL is %g, (%g in \"SNR\")" % (max(logLs), numpy.sqrt(2*max(logLs))))
    print( "Which occurs at sample", numpy.argmax(logLs))
    print( "This corresponds to time %.20g" % tvals[numpy.argmax(logLs)])
    print( "The data event time is:  %.20g" % sim_row.get_time_geocent())
    print( "Difference from geocenter t_ref is %.20g" %\
            (tvals[numpy.argmax(logLs)] - sim_row.get_time_geocent()) )
    print( "This difference in discrete samples: %.20g" %\
            ((tvals[numpy.argmax(logLs)]-sim_row.get_time_geocent())/P.deltaT) )
    pyplot.plot(tvals-tref, logLs)
    pyplot.ylabel("log Likelihood")
    pyplot.xlabel("time (relative to %10.5f)" % tref)
    pyplot.axvline(0, color="k")
    pyplot.title("lnL(t),\n value at event time: %f" % logL)
    pyplot.grid()
    pyplot.savefig("logL.png")
    integral = numpy.sum( numpy.exp(logLs) * P.deltaT )
    print( "Integral over t of likelihood is:", integral)
    print( "The log of the integral is:", numpy.log(integral))
    exit()

#
# Set up parameters and bounds
#

# PROBLEM: if too large, you can MISS a source. Does NOT need to be fixed for all masses *IF* the problem really has strong support
dmin = opts.d_min    # min distance
dmax = opts.d_max  # max distance FOR ANY SOURCE EVER. EUCLIDEAN



if not opts.rom_integrate_intrinsic: #not opts.rom_use_basis:  # Does not work for ROM 

 crossTermNet = 0
 for key in cross_terms.keys():
    crossTermNet += float(numpy.abs(cross_terms[key][(2,2),(2,2)]))
# first estimate tends to have problems for NR
 dmax_sampling_guess = numpy.min([factored_likelihood.distMpcRef*(numpy.sqrt(crossTermNet)/8), dmax])
 distBoundGuess = factored_likelihood.estimateUpperDistanceBoundInMpc(rholms, cross_terms)
else:
 dmax_sampling_guess = dmax
 distBoundGuess = dmax

print( "Recommended distance for sampling ", dmax_sampling_guess, " and probably near ", distBoundGuess, " smaller than  ", dmax)
print( "    (recommendation not yet used) ")
if distBoundGuess/dmax > 1: 
    print( " *******  WARNING ******** ")
    print( " Your distance cutoff for integration may not include the source distance ")
if distBoundGuess/dmax < 0.05 and not  opts.no_adapt:
    print( " *******  WARNING ******** ")
    print( " Adaptive integrator in distance can fail if the dynamic range of distance is too great (bins?) ")
    print( " Please set --d-max appropriately for your problem ")

param_limits = { "psi": (0, 2*numpy.pi),
    "phi_orb": (0, 2*numpy.pi),
    "distance": (dmin, dmax),   # CAN LEAD TO CATASTROPHIC FAILURE if dmax is too large (adaptive fails - too few bins)
    "right_ascension": (0, 2*numpy.pi),
    "declination": (-numpy.pi/2, numpy.pi/2),
    "t_ref": (-t_ref_wind, t_ref_wind),
    "inclination": (0, numpy.pi)
}

#
# Parameter integral sampling strategy
#
params = {}
sampler = mcsampler.MCSampler()
xpy_asarray_already = functools.partial(xpy_default.asarray,dtype=np.float64)
if opts.sampler_method == "adaptive_cartesian_gpu":
    sampler = mcsamplerGPU.MCSampler()
    sampler.xpy = xpy_default
    sampler.identity_convert=identity_convert
    mcsampler  = mcsamplerGPU  # force use of routines in that file, for properly configured GPU-accelerated code as needed

    xpy_asarray_already = lambda x: x  # do nothing because we are already on the board for GPU-generated 

    if opts.sampler_xpy == "numpy":
      mcsampler.set_xpy_to_numpy()
      sampler.xpy= numpy
      sampler.identity_convert= lambda x: x
if opts.sampler_method == "GMM":
    sampler = mcsamplerEnsemble.MCSampler()


#
# Psi -- polarization angle
# sampler: uniform in [0, pi)
#
psi_sampler = mcsampler.ret_uniform_samp_vector_alt( 
    param_limits["psi"][0], param_limits["psi"][1])
psi_sampler_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector, 
    param_limits["psi"][0], param_limits["psi"][1])
sampler.add_parameter("psi", 
    pdf = psi_sampler, 
    cdf_inv = psi_sampler_cdf_inv, 
    left_limit = param_limits["psi"][0], 
    right_limit = param_limits["psi"][1],
    prior_pdf = mcsampler.uniform_samp_psi)

#
# Phi - orbital phase
# sampler: uniform in [0, 2*pi)
#
phi_sampler = mcsampler.ret_uniform_samp_vector_alt(
    param_limits["phi_orb"][0], param_limits["phi_orb"][1])
phi_sampler_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector, 
    param_limits["phi_orb"][0], param_limits["phi_orb"][1])
sampler.add_parameter("phi_orb",
    pdf = phi_sampler,
    cdf_inv = phi_sampler_cdf_inv,
    left_limit = param_limits["phi_orb"][0], 
    right_limit = param_limits["phi_orb"][1],
    prior_pdf = mcsampler.uniform_samp_phase)

#
# inclination - angle of system angular momentum with line of sight
# sampler: cos(incl) uniform in [-1, 1)
#

adapt_extra_extrinsic=False
if opts.sampler_method == "adaptive_cartesian_gpu":  # this is a better/more stable/faster adaptive code, trust it to adapt in more extrinsic dimensions
  adapt_extra_extrinsic=True

if not opts.inclination_cosine_sampler:
 incl_sampler = mcsampler.cos_samp_vector # this is NOT dec_samp_vector, because the angular zero point is different!
 incl_sampler_cdf_inv = mcsampler.cos_samp_cdf_inv_vector
 sampler.add_parameter("inclination", 
    pdf = incl_sampler, 
    cdf_inv = incl_sampler_cdf_inv, 
    left_limit = param_limits["inclination"][0], 
    right_limit = param_limits["inclination"][1],
    prior_pdf = mcsampler.uniform_samp_theta) # do NOT adapt if we are using a prior that goes to zero at the edges
else:
 incl_sampler =  mcsampler.ret_uniform_samp_vector_alt(-1.0,1.0)
 incl_sampler_cdf_inv = lambda x: x*2.0-1.#functools.partial(mcsampler.uniform_samp_cdf_inv_vector,-1,1) 
 sampler.add_parameter("inclination", 
    pdf = incl_sampler, 
    cdf_inv = incl_sampler_cdf_inv, 
    left_limit = -1, 
    right_limit = 1,
    prior_pdf = incl_sampler,
    adaptive_sampling=adapt_extra_extrinsic)

#
# Distance - luminosity distance to source in parsecs
# sampler: uniform distance over [dmin, dmax), adaptive sampling
#
#dist_sampler = functools.partial(mcsampler.uniform_samp_vector,     param_limits["distance"][0], param_limits["distance"][1])
#dist_sampler_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector,     param_limits["distance"][0], param_limits["distance"][1])
if not opts.distance_marginalization:
    dist_sampler = mcsampler.ret_uniform_samp_vector_alt( param_limits["distance"][0], param_limits["distance"][1])
    dist_sampler_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector,     param_limits["distance"][0], param_limits["distance"][1])
    dist_prior_pdf =   lambda x: x**2/(param_limits["distance"][1]**3/3.                   - param_limits["distance"][0]**3/3.)
    if opts.d_prior == 'pseudo_cosmo':
        nm = priors_utils.dist_prior_pseudo_cosmo_eval_norm(param_limits["distance"][0],param_limits["distance"][1])
        dist_prior_pdf =functools.partial( priors_utils.dist_prior_pseudo_cosmo, nm=nm)
    #dist_sampler_cdf_inv=None
    sampler.add_parameter("distance",
        pdf = dist_sampler,
        cdf_inv = dist_sampler_cdf_inv,
        left_limit = param_limits["distance"][0],
        right_limit = param_limits["distance"][1],
        prior_pdf = dist_prior_pdf,
        adaptive_sampling = not (opts.no_adapt or opts.no_adapt_distance))


# 
# Rotate sky coordinates
#
if opts.internal_sky_network_coordinates:
  ifo_list = list(psd_dict)
  if not(opts.internal_sky_network_coordinates_raw):
    # remove V and K : the sky ring is almost always only HL
    if 'K1' in ifo_list:
      ifo_list.remove('K1')
    if 'V1' in ifo_list:
      ifo_list.remove('V1')
  # problem: ordering of psd_dict is rarely rational; almost always we want an HL network, rarely V
  if len(ifo_list) <2:
    opts.internal_sky_network_coordinates = False # stop using this code
  else:
    sky_rotations.assign_sky_frame(ifo_list[0], ifo_list[1], fiducial_epoch)
    frm = identity_convert_togpu(sky_rotations.frm)
    my_rotation = functools.partial(lalsimutils.polar_angles_in_frame_alt,frm,xpy=xpy_default) 
  


#
# Intrinsic parameters
#
sampler_lookup = {}
sampler_inv_lookup = {}
sampler_lookup['q'] = mcsampler.q_samp_vector   # only one intrinsic parameter possible
sampler_lookup['M'] = mcsampler.M_samp_vector
sampler_inv_lookup['q'] = mcsampler.q_cdf_inv_vector
sampler_inv_lookup['M'] = None # mcsampler.M_cdf_inv_vector

if opts.rom_use_basis and opts.rom_integrate_intrinsic:
 for p in intrinsic_param_names:
    indx = intrinsic_param_names.index(p)
    qmin,qmax = param_ranges[indx]
    q_pdf =mcsampler.ret_uniform_samp_vector_alt(qmin, qmax)   # sample uniformly by default
#    q_pdf =functools.partial(sampler_lookup[p], qmin, qmax)   # sample uniformly by default
    q_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector, qmin,qmax) # sample uniformly by default
#    q_cdf_inv= None
    q_pdf_prior = functools.partial(sampler_lookup[param], qmin, qmax)  # true prior, from lookup
    sampler.add_parameter(p, 
        pdf=q_pdf, 
        cdf_inv=q_cdf_inv, 
        left_limit=qmin, right_limit=qmax,prior_pdf=q_pdf_prior, 
        adaptive_sampling = opts.adapt_intrinsic)



if opts.skymap_file is not None:
    #
    # Right ascension and declination -- use a provided skymap
    #
    smap, _ = bfits.read_sky_map(opts.skymap_file)
    # FIXME: Uncomment for 'mixed' map
    #smap = 0.9*smap + 0.1*numpy.ones(len(smap))/len(smap)
    ss_sampler = mcsampler.HealPixSampler(smap)
    #isotropic_2d_sampler = numpy.vectorize(lambda dec, ra: mcsampler.dec_samp_vector(dec)/2/numpy.pi)
    isotropic_bstar_sampler = numpy.vectorize(lambda dec, ra: 1.0/len(smap))

    # FIXME: Should the left and right limits be modified?
    sampler.add_parameter(("declination", "right_ascension"), 
        pdf = ss_sampler.pseudo_pdf,
        cdf_inv = ss_sampler.pseudo_cdf_inverse, 
        left_limit = (param_limits["declination"][0], param_limits["right_ascension"][0]),
        right_limit = (param_limits["declination"][1], param_limits["right_ascension"][1]),
        prior_pdf = isotropic_bstar_sampler)

else:
    #
    # Right ascension - angle in radians from prime meridian plus hour angle
    # sampler: uniform in [0, 2pi), adaptive sampling
    #
    ra_sampler = mcsampler.ret_uniform_samp_vector_alt(
        param_limits["right_ascension"][0], param_limits["right_ascension"][1])
    ra_sampler_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector,
        param_limits["right_ascension"][0], param_limits["right_ascension"][1])
    sampler.add_parameter("right_ascension", 
        pdf = ra_sampler, 
        cdf_inv = ra_sampler_cdf_inv, 
        left_limit = param_limits["right_ascension"][0],
        right_limit =  param_limits["right_ascension"][1],
        prior_pdf = mcsampler.uniform_samp_phase,
        adaptive_sampling = (not opts.no_adapt) and (not opts.internal_sky_network_coordinates)) # TOO DANGEROUS to double-adapt in sky, ends up overconverging too easily. Just leave the whole sky ring in place if we want to do this, it's usually there and we don't lose that much.  If we have full localization, we should just remove the sky network coordinates argument!

    #
    # declination - angle in radians from the north pole piercing the celestial
    # sky sampler: cos(dec) uniform in [-1, 1), adaptive sampling
    #
    if not opts.declination_cosine_sampler:
     dec_sampler = mcsampler.dec_samp_vector
     dec_sampler_cdf_inv = mcsampler.dec_samp_cdf_inv_vector
     sampler.add_parameter("declination", 
        pdf = dec_sampler, 
        cdf_inv = dec_sampler_cdf_inv, 
        left_limit = param_limits["declination"][0], 
        right_limit = param_limits["declination"][1],
        prior_pdf = mcsampler.uniform_samp_dec,
        adaptive_sampling = not opts.no_adapt)
    else:
     # Sample uniformly in cos(polar_theta), =1 for north pole, -1 for south pole.
     # Propagate carefully in conversions: time of flight libraries use RA,DEC
     dec_sampler = mcsampler.ret_uniform_samp_vector_alt(-1.0,1.0)
     dec_sampler_cdf_inv = lambda x: x*2.0-1. #functools.partial(mcsampler.uniform_samp_cdf_inv_vector,-1,1)
     sampler.add_parameter("declination", 
        pdf = dec_sampler, 
        cdf_inv = dec_sampler_cdf_inv, 
        left_limit = -1, 
        right_limit = 1,
        prior_pdf = dec_sampler,
        adaptive_sampling = not opts.no_adapt)
#
# Determine pinned and non-pinned parameters
#

pinned_params = get_pinned_params(opts)
unpinned_params = get_unpinned_params(opts, sampler.params)
print( "{0:<25s} {1:>5s} {2:>5s} {3:>20s} {4:<10s}".format("parameter", "lower limit", "upper limit", "pinned?", "pin value"))
plen = len(sorted(sampler.params, key=lambda p: len(p))[-1])
for p in sampler.params:
    if p in pinned_params:
        pinned, value = True, "%1.3g" % pinned_params[p]
    else:
        pinned, value = False, ""

    if isinstance(p, tuple):
        for subp, subl, subr in zip(p, sampler.llim[p], sampler.rlim[p]):
            subp = subp + " "*min(0, plen-len(subp))
            print( "|{0:<25s} {1:>1.3g}   {2:>1.3g} {3:>20s} {4:<10s}".format(subp, subl, subr, str(False), ""))
    else:
        p = p + " "*min(0, plen-len(p))
        print( "{0:<25s} {1:>1.3g}   {2:>1.3g} {3:>20s} {4:<10s}".format(p, sampler.llim[p], sampler.rlim[p], str(pinned), value))

# Special case: t_ref is assumed to be relative to the epoch
if "t_ref" in pinned_params:
    pinned_params["t_ref"] -= float(fiducial_epoch)

#
# Provide convergence tests
# FIXME: Currently using hardcoded thresholds, poorly hand-tuned
#
test_converged = {}
if opts.convergence_tests_on:
    test_converged['neff'] = functools.partial(mcsampler.convergence_test_MostSignificantPoint,1./opts.n_eff)  # most significant point less than 1/neff of probability.  Exactly equivalent to usual neff threshold.
    test_converged["normal_integral"] = functools.partial(mcsampler.convergence_test_NormalSubIntegrals, 25, 0.01, 0.1)   # 20 sub-integrals are gaussian distributed [weakly; mainly to rule out outliers] *and* relative error < 10%, based on sub-integrals . Should use # of intervals << neff target from above.  Note this sets our target error tolerance on  lnLmarg.  Note the specific test requires >= 20 sub-intervals, which demands *very many* samples (each subintegral needs to be converged).

#
# Merge options into one big ol' kwargs dict
#

pinned_params.update({ 
    # Iteration settings and termination conditions
    "n": min(opts.n_chunk, n_max), # Number of samples in a chunk
    "nmax": n_max, # Total number of samples to draw before termination
    "neff": n_eff, # Total number of effective samples to collect before termination

    "convergence_tests" : test_converged,    # Dictionary of convergence tests

    # Adaptive sampling settings
    "tempering_exp": opts.adapt_weight_exponent if not opts.no_adapt else 0.0, # Weights will be raised to this power to prevent overconvergence
    "tempering_log": opts.adapt_log,
    "tempering_adapt": opts.adapt_adapt,

    "floor_level": opts.adapt_floor_level if not opts.no_adapt else 0.0, # The new sampling distribution at the end of each chunk will be floor_level-weighted average of a uniform distribution and the (L^tempering_exp p/p_s)-weighted histogram of sampled points.
    "history_mult": 10, # Multiplier on 'n' - number of samples to estimate marginalized 1-D histograms
    "n_adapt": 100 if not opts.no_adapt else 0, # Number of chunks to allow adaption over

    # Verbosity settings
    "verbose": True, #not opts.rom_integrate_intrinsic, 
    "extremely_verbose": False, 

    # Sample caching
    "save_intg": opts.save_samples, # Cache the samples (and integrand values)?
    "igrand_threshold_deltalnL": opts.save_deltalnL, # Threshold on distance from max L to save sample
    "igrand_threshold_p": opts.save_P, # Threshold on cumulative probability contribution to cache sample
    "igrand_fairdraw_samples": opts.fairdraw_extrinsic_output,
    "igrand_fairdraw_samples_max": opts.n_eff
})
if opts.sampler_method == "adaptive_cartesian_gpu":
  pinned_params.update({"save_no_samples":True})   # do not exhaust GPU memory with MC samples!  
if opts.sampler_method == "GMM":
    n_step =pinned_params["n"]
    n_max_blocks = ((1.0*int(opts.n_max))/n_step)
    # pairing coordinates for adaptive integration: see definition of order below
    #    (distance,inclination)
    #    (ra, dec)
    #    (psi) (orb_phase)   # only do 1d adaptivity there, 
#    gmm_dict = {tuple([2]):None,tuple([4]):None,(3,5):None,(0,1):None} 
#    comp_dict = {tuple([2]):1,tuple([4]):1,(3,5):3,(0,1):4} 
    gmm_dict = {tuple([2]):None,tuple([4]):None,(3,5):None,tuple([0]):None,tuple([1]):None} 
    n_sky = 4
#    if len(psd_dict.keys()) > 2:
#        n_sky=2  # presumably we have localized better, avoid failure modes
    comp_dict = {tuple([2]):1,tuple([4]):1,(3,5):3,tuple([0]):n_sky,tuple([1]):n_sky} 

    extra_args = {'n_comp':comp_dict,'max_iter':n_max_blocks,'gmm_dict':gmm_dict}  # made up for now, should adjust
    pinned_params.update(extra_args)

    # cannot pin params at present
    # unpinned params MUST be full call signature of likelihood, IN ORDER
    if not(opts.time_marginalization):
      unpinned_params =['right_ascension','declination', 't_ref','phi_orb','inclination','psi','distance']
    else:
      unpinned_params =['right_ascension','declination', 'phi_orb','inclination','psi','distance']

#
# Call the likelihood function for various extrinsic parameter values
#
if not opts.time_marginalization:
    #
    # tref - GPS time of geocentric end time
    # sampler: uniform in +/-2 ms window around estimated end time 
    #
    tref_sampler = mcsampler.ret_uniform_samp_vector_alt(
        param_limits["t_ref"][0], param_limits["t_ref"][1])

    tref_sampler_cdf_inv = functools.partial(mcsampler.uniform_samp_cdf_inv_vector, 
                                            param_limits["t_ref"][0], param_limits["t_ref"][1])
    sampler.add_parameter("t_ref", 
                          pdf = tref_sampler, 
                          cdf_inv = None, 
                          left_limit = param_limits["t_ref"][0], 
                          right_limit = param_limits["t_ref"][1],
                          prior_pdf = mcsampler.ret_uniform_samp_vector_alt(param_limits["t_ref"][0], param_limits["t_ref"][1]))

    #
    # A note of caution:
    # In order to make the pinning interface work consistently, the names of 
    # parameters given to the sampler must match the argument names in the
    # called function. This is because the sampler has to reconstruct the
    # argument order to pass the right values, and it can only do that by
    # comparing the parameter names it knows to the arguments that are passed
    # to it.
    #
    def likelihood_function(right_ascension, declination, t_ref, phi_orb,
            inclination, psi, distance):

        dec = numpy.copy(declination).astype(numpy.float64)
        if opts.declination_cosine_sampler:
            dec = numpy.pi/2 - numpy.arccos(dec)
        incl = numpy.copy(inclination).astype(numpy.float64)
        if opts.inclination_cosine_sampler:
            incl = numpy.arccos(incl)

        # use EXTREMELY many bits
        lnL = numpy.zeros(right_ascension.shape,dtype=numpy.float128)
        i = 0
        for ph, th, tr, phr, ic, ps, di in zip(right_ascension, dec,
                t_ref, phi_orb, incl, psi, distance):
            P.phi = ph # right ascension
            P.theta = th # declination
            P.tref = fiducial_epoch + tr # ref. time (rel to epoch for data taking)
            P.phiref = phr # ref. orbital phase
            P.incl = ic # inclination
            P.psi = ps # polarization angle
            P.dist = di* 1.e6 * lalsimutils.lsu_PC # luminosity distance

            lnL[i] = factored_likelihood.FactoredLogLikelihood(
                    P, rholms, rholms_intp, cross_terms, cross_terms_V, opts.l_max)
            i+=1
    
        return numpy.exp(lnL - manual_avoid_overflow_logarithm)
    res, var, neff, dict_return = sampler.integrate(likelihood_function, *unpinned_params, **pinned_params)

else: # Sum over time for every point in other extrinsic params
 if not (opts.rom_integrate_intrinsic or opts.vectorized) :
    def likelihood_function(right_ascension, declination, phi_orb, inclination,
            psi, distance):
        dec = numpy.copy(declination).astype(numpy.float64)  # get rid of 'object', and allocate space
        if opts.declination_cosine_sampler:
            dec = numpy.pi/2 - numpy.arccos(dec)
        incl = numpy.copy(inclination).astype(numpy.float64)
        if opts.inclination_cosine_sampler:
            incl = numpy.arccos(incl)

        # use EXTREMELY many bits
        lnL = numpy.zeros(right_ascension.shape,dtype=numpy.float128)
        i = 0
        tvals = numpy.linspace(-t_ref_wind,t_ref_wind,int((t_ref_wind)*2/P.deltaT))  # choose an array at the target sampling rate. P is inherited globally

        for ph, th, phr, ic, ps, di in zip(right_ascension, dec,
                phi_orb, incl, psi, distance):
            P.phi = ph # right ascension
            P.theta = th # declination
            P.tref = fiducial_epoch  # see 'tvals', above
            P.phiref = phr # ref. orbital phase
            P.incl = ic # inclination
            P.psi = ps # polarization angle
            P.dist = di* 1.e6 * lalsimutils.lsu_PC # luminosity distance
            
            
            lnL[i] = factored_likelihood.FactoredLogLikelihoodTimeMarginalized(tvals,
                    P, rholms_intp, rholms, cross_terms, cross_terms_V,                   
                    opts.l_max,interpolate=opts.interpolate_time)
            i+=1
    
        return numpy.exp(lnL -manual_avoid_overflow_logarithm)

    args = likelihood_function.__code__.co_varnames[:likelihood_function.__code__.co_argcount]
    print( " --------> Arguments ", args)
    res, var, neff, dict_return = sampler.integrate(likelihood_function, *unpinned_params, **pinned_params)
 elif opts.vectorized: # use array-based multiplications, fewer for loops
    lookupNKDict = {}
    lookupKNDict={}
    lookupKNconjDict={}
    ctUArrayDict = {}
    ctVArrayDict={}
    rholmArrayDict={}
    rholms_intpArrayDict={}
    epochDict={}
    nEvals=0

    for det in rholms_intp.keys():
        print( " Packing ", det)
        lookupNKDict[det],lookupKNDict[det], lookupKNconjDict[det], ctUArrayDict[det], ctVArrayDict[det], rholmArrayDict[det], rholms_intpArrayDict[det], epochDict[det] = factored_likelihood.PackLikelihoodDataStructuresAsArrays( rholms[det].keys(), rholms_intp[det], rholms[det], cross_terms[det],cross_terms_V[det])
        if opts.gpu and (not xpy_default is np):
            lookupNKDict[det] = cupy.asarray(lookupNKDict[det])
            rholmArrayDict[det] = cupy.asarray(rholmArrayDict[det])
            ctUArrayDict[det] = cupy.asarray(ctUArrayDict[det])
            ctVArrayDict[det] = cupy.asarray(ctVArrayDict[det])
            epochDict[det] = cupy.asarray(epochDict[det])
    if (not opts.gpu):
      if opts.distance_marginalization:
          raise RuntimeError(
              "Distance marginalization only supported on vectorized RIFT"
          )
      def likelihood_function(right_ascension, declination, phi_orb, inclination,
            psi, distance):
        global nEvals
        tvals = numpy.linspace(-t_ref_wind,t_ref_wind,int((t_ref_wind)*2/P.deltaT))  # choose an array at the target sampling rate. P is inherited globally
        dec = numpy.copy(declination).astype(numpy.float64)
        if opts.declination_cosine_sampler:
            dec = numpy.pi/2 - numpy.arccos(dec)
        incl = numpy.copy(inclination).astype(numpy.float64)
        if opts.inclination_cosine_sampler:
            incl = numpy.arccos(incl)

        # use EXTREMELY many bits
        lnL = numpy.zeros(right_ascension.shape,dtype=numpy.float128)
        P.phi = right_ascension.astype(float)  # cast to float
        P.theta = dec #declination.astype(float)
        P.tref = float(fiducial_epoch)
        P.phiref = phi_orb.astype(float)
        P.incl = incl #inclination.astype(float)
        P.psi = psi.astype(float)
        P.dist = (distance* 1.e6 * lalsimutils.lsu_PC).astype(float) # luminosity distance

        lnL = factored_likelihood.DiscreteFactoredLogLikelihoodViaArrayVector(tvals,
                    P, lookupNKDict, rholmArrayDict, ctUArrayDict, ctVArrayDict,epochDict,Lmax=opts.l_max)
        nEvals +=len(right_ascension)
        return numpy.exp(lnL-manual_avoid_overflow_logarithm)
    else:
        print( " Using CUDA GPU likelihood, if cupy available ")
        if not opts.distance_marginalization:
            def likelihood_function(right_ascension, declination, phi_orb, inclination, psi, distance):
                global nEvals
                tvals = xpy_default.linspace(-t_ref_wind,t_ref_wind,int((t_ref_wind)*2/P.deltaT))  # choose an array at the target sampling rate. P is inherited globally
                P.phi = xpy_asarray_already(right_ascension)  # cast to float
                if opts.declination_cosine_sampler:
                    P.theta = numpy.pi/2 - xpy_default.arccos(xpy_asarray_already(declination))
                else:
                    P.theta = xpy_asarray_already(declination)
                P.tref = float(fiducial_epoch)
                P.phiref = xpy_asarray_already(phi_orb)
                if opts.inclination_cosine_sampler:
                    P.incl = xpy_default.arccos(xpy_asarray_already(inclination))
                else:
                     P.incl = xpy_asarray_already(inclination)
                P.psi = xpy_asarray_already(psi)
                P.dist = xpy_asarray_already(distance* 1.e6 * lalsimutils.lsu_PC) # luminosity distance

                # rotate sky if needed
                if opts.internal_sky_network_coordinates:
                  P.theta,P.phi = my_rotation(np.pi/2 - P.theta,P.phi)
                  P.theta = np.pi/2 - P.theta
                  P.phi = xpy_default.mod(P.phi, 2*np.pi)

                lnL = factored_likelihood.DiscreteFactoredLogLikelihoodViaArrayVectorNoLoop(tvals,
                    P, lookupNKDict, rholmArrayDict, ctUArrayDict, ctVArrayDict,epochDict,Lmax=opts.l_max,xpy=xpy_default)
                nEvals +=len(right_ascension)
                return identity_convert(xpy_default.exp(lnL-manual_avoid_overflow_logarithm))
        else:
            xmin = factored_likelihood.distMpcRef / dmax
            xmax = factored_likelihood.distMpcRef / dmin
            lookup_table = np.load(opts.distance_marginalization_lookup_table)
            bmax = xpy_default.asarray(lookup_table["bmax"])
            sqrt_bmax = xpy_default.sqrt(bmax)
            bref = xpy_default.asarray(lookup_table["bref"])
            s_array = xpy_default.asarray(lookup_table["s_array"])
            smin = s_array[0]
            smax = s_array[-1]
            t_array = xpy_default.asarray(lookup_table["t_array"])
            tmax = t_array[-1]
            lnI_array = xpy_default.asarray(lookup_table["lnI_array"])

            intp = EvenBivariateLinearInterpolator(s_array[0], s_array[1] - s_array[0], t_array[0], t_array[1] - t_array[0], lnI_array)
            lnLmarg_const = xpy_default.log(3. / (xmin**(-3.) - xmax**(-3.)))

            def exponent_max(x0, b):
                x0_expmax = xpy_default.clip(x0, a_min=xmin, a_max=xmax)
                return b * x0_expmax * (x0 - 0.5*x0_expmax)

            def b_to_t(b):
                # TODO: this function is duplicate with what is in util_InitMargTable
#                return np.arcsinh(b / bref)
                b_by_bref = b / bref
                return xpy_default.arcsinh(b_by_bref, out=b_by_bref)


            def x0_to_s(x0):
                # TODO: this function is duplicate with what is in util_InitMargTable
#                return np.arcsinh(np.sqrt(bmax) * (x0 - xmin)) - np.arcsinh(np.sqrt(bmax) * (xmax - x0))
                A = x0 - xmin
                A *= sqrt_bmax
                xpy_default.arcsinh(A, out=A)

                B = xmax - x0
                B *= sqrt_bmax
                xpy_default.arcsinh(B, out=B)

                A -= B
                return A


            def distmarg_loglikelihood(kappa_sq, rho_sq):
                x0 = kappa_sq / rho_sq
#                lnI = np.ones(shape=x0.shape) * -np.inf
                lnI = xpy_default.full_like(x0, xpy_default.NINF)
                s = x0_to_s(x0)
                t = b_to_t(rho_sq)
#                in_bounds = (s > smin) * (s < smax) * (t < tmax)
                in_bounds = (s > smin) & (s < smax) & (t < tmax)
                lnI[in_bounds] = intp(s[in_bounds], t[in_bounds])
                return exponent_max(x0, rho_sq) + lnI + lnLmarg_const

            def likelihood_function(right_ascension, declination, phi_orb, inclination, psi):
                global nEvals
                tvals = xpy_default.linspace(-t_ref_wind,t_ref_wind,int((t_ref_wind)*2/P.deltaT))  # choose an array at the target sampling rate. P is inherited globally
                P.phi = xpy_asarray_already(right_ascension)  # cast to float
                if opts.declination_cosine_sampler:
                    P.theta = numpy.pi/2 - xpy_default.arccos(xpy_asarray_already(declination))
                else:
                    P.theta = xpy_asarray_already(declination)
                P.tref = float(fiducial_epoch)
                P.phiref = xpy_asarray_already(phi_orb)
                if opts.inclination_cosine_sampler:
                    P.incl = xpy_default.arccos(xpy_asarray_already(inclination))
                else:
                     P.incl = xpy_asarray_already(inclination)
                P.psi = xpy_asarray_already(psi)
                P.dist = xpy_asarray_already(factored_likelihood.distMpcRef * 1.e6 * lalsimutils.lsu_PC) # luminosity distance

                # rotate sky if needed
                if opts.internal_sky_network_coordinates:
                  P.theta,P.phi = my_rotation(np.pi/2 - P.theta,P.phi)
                  P.theta = np.pi/2 - P.theta
                  P.phi = xpy_default.mod(P.phi, 2*np.pi)

                lnL = factored_likelihood.DiscreteFactoredLogLikelihoodViaArrayVectorNoLoop(tvals,
                    P, lookupNKDict, rholmArrayDict, ctUArrayDict, ctVArrayDict,epochDict,Lmax=opts.l_max,xpy=xpy_default, loglikelihood=distmarg_loglikelihood)
                nEvals +=len(right_ascension)
                return identity_convert(xpy_default.exp(lnL-manual_avoid_overflow_logarithm))

    args = likelihood_function.__code__.co_varnames[:likelihood_function.__code__.co_argcount]
    print( " --------> Arguments ", args)
    res, var, neff, dict_return = sampler.integrate(likelihood_function, *unpinned_params, **pinned_params)
 else: # integrate over intrinsic variables. Right now those variables ahave HARDCODED NAMES, alas
    def likelihood_function(right_ascension, declination, phi_orb, inclination,
            psi, distance,q):
        dec = numpy.copy(declination).astype(numpy.float64)
        if opts.declination_cosine_sampler:
            dec = numpy.pi/2 - numpy.arccos(dec)
        incl = numpy.copy(inclination).astype(numpy.float64)
        if opts.inclination_cosine_sampler:
            incl = numpy.arccos(incl)

        lnL = numpy.zeros(len(right_ascension),dtype=numpy.float128)
        i = 0
        tvals = numpy.linspace(-t_ref_wind,t_ref_wind,int((t_ref_wind)*2/P.deltaT))  # choose an array at the target sampling rate. P is inherited globally


        t_start =lal.GPSTimeNow() 
#        print " ILE with intrinsic (ROM) ", rholms_intp["H1"].keys()
#        for qi in q:
#         P.assign_param('q',qi)  # mass ratio
       #      print " Re-constructing U,V, rho for ", qi, " time cost ", t_end - t_start
        for ph, th, phr, ic, ps, di,qi in zip(right_ascension, dec,
                phi_orb, incl, psi, distance,q):
             # Reconstruct U,V using ROM fits.  PROBABLY should do this once for every q, rather than deep on the loop
            P.assign_param('q',qi)  # mass ratio
            rholms_intp_A, cross_terms_A, cross_terms_V_A, rholms_A, rest_A = factored_likelihood.ReconstructPrecomputedLikelihoodTermsROM(P, rest, rholms_intp, cross_terms, cross_terms_V, rholms,verbose=False)


            # proceed for rest
            P.phi = ph # right ascension
            P.theta = th # declination
            P.tref = fiducial_epoch  # see 'tvals', above
            P.phiref = phr # ref. orbital phase
            P.incl = ic # inclination
            P.psi = ps # polarization angle
            P.dist = di* 1.e6 * lalsimutils.lsu_PC # luminosity distance
            
            
            lnL[i] = factored_likelihood.FactoredLogLikelihoodTimeMarginalized(tvals,
                    P, rholms_intp_A, rholms_A, cross_terms_A, cross_terms_V_A,                   
                    opts.l_max,interpolate=opts.interpolate_time)
            if numpy.isnan(lnL[i]) or lnL[i]<-200:
                lnL[i] = -200   # regularize  : a hack, for now, to deal with rare ROM problems. Only on the ROM logic fork
            i+=1
        t_end =lal.GPSTimeNow() 
        print( " Cost per evaluation ", (t_end - t_start)/len(q))
        print( " Max lnL for this iteration ", numpy.max(lnL))
        return numpy.exp(lnL - manual_avoid_overflow_logarithm)

    args = likelihood_function.__code__.co_varnames[:likelihood_function.__code__.co_argcount]
    print( " --------> Arguments ", args)
    res, var, neff, dict_return = sampler.integrate(likelihood_function, *unpinned_params, **pinned_params)


print( " lnLmarg is ", numpy.log(res+manual_avoid_overflow_logarithm), " with expected relative error ", numpy.sqrt(var)/res)
print( " note neff is ", neff, "; compare neff^(-1/2) = ", 1/numpy.sqrt(neff))


# Convert declination, inclination  parameters in sampler if needed
if opts.inclination_cosine_sampler:
    sampler._rvs["inclination"] = numpy.arccos(sampler._rvs["inclination"].astype(numpy.float64))
if opts.declination_cosine_sampler:
    sampler._rvs["declination"] = numpy.pi/2 - numpy.arccos(sampler._rvs["declination"].astype(numpy.float64))

# Unpack paired sky coordinates
if ("declination", "right_ascension") in sampler.params:
    sampler._rvs["declination"], sampler._rvs["right_ascension"] = sampler._rvs[("declination", "right_ascension")]

# Insert reference distance if it was marginalized over
if "distance" not in sampler._rvs:
    sampler._rvs["distance"] = np.full_like(
        sampler._rvs["psi"],
        factored_likelihood.distMpcRef #*1e6*lal.PC_SI,
    )

if opts.maximize_only and opts.output_file:
    # Pick the best extrinsic parameters, except for time (assumed not set: time marginalization)
    indx_guess = numpy.argmax(sampler._rvs["integrand"])   # start search near maximum-likelihood point. (WARNING: can be very close by)
    P.radec=True
    P.phi = sampler._rvs["right_ascension"][indx_guess]
    P.theta = sampler._rvs["declination"][indx_guess]
    P.phiref = sampler._rvs["phi_orb"][indx_guess]
    P.incl = sampler._rvs["inclination"][indx_guess]
    P.psi = sampler._rvs["psi"][indx_guess]
    P.dist = sampler._rvs["distance"][indx_guess]*1e6*lal.PC_SI
    print( " ---- Best extrinsic paramers in MC   ---- ")
    P.print_params()
    lalsimutils.ChooseWaveformParams_array_to_xml([P],"notime_raw_maxpt_"+opts.output_file) # best point, not including time

    import scipy.optimize
    def fn_scaled(x):
          P.phi = float(x[0]*2*numpy.pi) # right ascension
          P.theta = float((x[1])*numpy.pi) # declination, really polar angle. NOT zero on equator
          P.tref = fiducial_epoch + float((x[2]-0.5)*2*t_ref_wind) # ref. time (rel to epoch for data taking)
          P.phiref = float(x[3]*numpy.pi*2) # ref. orbital phase
          P.incl = float(x[4]*numpy.pi) # inclination
          P.psi = float(x[5]*numpy.pi) # polarization angle
          P.dist = x[6]*dmax* 1.e6 * lalsimutils.lsu_PC # luminosity distance
    
          return -1.0* factored_likelihood.FactoredLogLikelihood(
                    P, rholms,rholms_intp, cross_terms, cross_terms_V, opts.l_max)

    # Pick the best extrinsic parameters, except for time (assumed not set: time marginalization)
    x0 =numpy.array( [ \
       sampler._rvs["right_ascension"][indx_guess]/(2*numpy.pi) , \
       (sampler._rvs["declination"][indx_guess]/numpy.pi),  \
       0.5, \
       (sampler._rvs["phi_orb"][indx_guess]/(2*numpy.pi)), \
       sampler._rvs["inclination"][indx_guess]/(numpy.pi),\
       sampler._rvs["psi"][indx_guess]/numpy.pi,\
       sampler._rvs["distance"][indx_guess]/dmax\
            ],dtype=numpy.float128)
    x0 = numpy.fmod(x0,numpy.ones(len(x0)))  # had BETTER be defined on this range!
    # Pick the best starting time. BRUTE FORCE METHOD: use grid
    def fn_scaled_t(t,x0):
        return fn_scaled( [x0[0], x0[1], t, x0[3], x0[4], x0[5], x0[6]])
    npts_guess = int(t_ref_wind*2/(0.5*1e-4))   # Need to have enough points to fully explore the peak, timing to sub-ms accuracy
    print( " Using ", npts_guess, " time points to select the best time, fixing the remaining extrinsic parameters ")
    tvals_scaled_guess = numpy.linspace(0,1,npts_guess)
    lnLvals = numpy.array([-1*fn_scaled_t(t,x0) for t in tvals_scaled_guess])   # note the two -1's cancel
#    from matplotlib import pyplot as plt;  plt.plot(tvals_scaled_guess,lnLvals,'o'); plt.show(); numpy.savetxt("dump-lnL.dat", numpy.array([tvals_scaled_guess*2*t_ref_wind,lnLvals]).T)
    tbest = tvals_scaled_guess[numpy.argmax(lnLvals)]
    print( " ---- Best extrinsic paramers in MC, after time offset   ---- ")
    P.tref = fiducial_epoch + tbest
    P.print_params()
    lalsimutils.ChooseWaveformParams_array_to_xml([P],"withtime_raw_maxpt_"+opts.output_file) # best point, not including time
    x0[2] = tbest
    x0p = x0
    # Refine best starting time with a search
#    t_scaled_est = scipy.optimize.brent(fn_scaled_t, brack=(tbest-0.01,tbest,tbest+0.01),args=(x0),maxiter=500)
#    print "Scaled starting time estimate after, before ",  t_scaled_est, tbest
#    x0p[2] = t_scaled_est
    t_best_now = lal.LIGOTimeGPS()   # create correct data type
    t_best_now = fiducial_epoch + float((x0p[2]-0.5)*2*t_ref_wind)
    t_best_now_s = int(numpy.floor(t_best_now))
    t_best_now_ns = int((t_best_now - t_best_now_s)*1e9)
    print( " Fitting best (geocentric) time : ", str(t_best_now_s)+ '.'+ str(t_best_now_ns) , " relative time ", (x0p[2]-0.5)*2*t_ref_wind)
    x0  = x0p
    # Full multi-d search for best point
    print( " Starting point [dimensionless] ", x0)
    print( " Starting point [physical] ", [x0[0]*2*numpy.pi, numpy.pi*(x0[1]),  t_ref_wind*2*(x0[2]-0.5), 2*numpy.pi*x0[3], numpy.pi*x0[4], numpy.pi*x0[5], x0[6]*dmax])
    lnLstart = -1*fn_scaled(x0)   # note the two -1's cancel. Compare to 'rho^2/2' value reported by code
    print( " lnL at start :", lnLstart, " [note can be lower than peak because of time offset]: best reported by ILE (including weights) is ", numpy.log(numpy.max(sampler._rvs["integrand"])))
    x = scipy.optimize.fmin(fn_scaled,x0, xtol=1e-5,ftol=1e-3,maxiter=opts.n_max,maxfun=opts.n_max)
    lnLmax = -1.0*fn_scaled(x)
    if lnLmax < lnLstart:
        print( " Maximization failed to improve our initial point ! ")
        x = x0p
        lnLmax = lnLstart
    t_best_now = fiducial_epoch + float((x[2]-0.5)*2*t_ref_wind)
    print( "Best lnL = ", lnLmax)
    print( "Best time =", t_best_now)
    print( "Best point", [x[0]*2*numpy.pi, numpy.pi*(x[1]),  t_ref_wind*2*(x[2]-0.5), 2*numpy.pi*x[3], numpy.pi*x[4], numpy.pi*x[5], x[6]*dmax])
    # Output best fit.  Note STANDARD output will occur as usual
    P_max = P.manual_copy()
    P_max.tref = t_best_now
    P_max.phi = float(x[0]*2*numpy.pi)
    P_max.theta = float(numpy.pi*(x[1]))
    P_max.phiref =float(x[3]*numpy.pi*2)
    P_max.incl = float(x[4]*numpy.pi)
    P_max.psi = float(x[5]*numpy.pi)
    P_max.dist = x[6]*dmax*1e6*lal.PC_SI
    P_max.m1 = lal.MSUN_SI*m1
    P_max.m2 = lal.MSUN_SI*m2
    print( " Sanity check: log likelihood for this set is ", factored_likelihood.FactoredLogLikelihood(
                    P_max, rholms, rholms_intp, cross_terms, cross_terms_V, opts.l_max))
    print( " ---- Best extrinsic paramers after polishing   ---- ")
    P_max.print_params()
    P_list = [P_max]
    lalsimutils.ChooseWaveformParams_array_to_xml(P_list,"maxpt_"+opts.output_file)
#    sys.exit(0)
    # sampler._rvs  ={}
    # sampler._rvs["t_ref"]            = numpy.array([t_ref_wind*2*(x[2]-0.5)])
    # sampler._rvs["distance"]       = numpy.array([x[6]*dmax])
    # sampler._rvs["psi"]  = numpy.array([x[5]*numpy.pi])
    # sampler._rvs["right_ascension"]  = numpy.array([x[0]*2*numpy.pi])
    # sampler._rvs["declination"]  = numpy.array([)
    # sampler._rvs["inclination"]  = numpy.array([x[4]*numpy.pi])
    # sampler._rvs["phi_orb"]  = numpy.array([x[3]*numpy.pi])
    # sampler._rvs["integrand"] = numpy.array([lnLmax])
    # sampler._rvs["alpha1"]  = numpy.array([1])
    # sampler._rvs["alpha2"]  = numpy.array([1])
    # sampler._rvs["alpha3"]  = numpy.array([1])


# Ascii output of core result (XML can be mangled by GLUE and is not lightweight)
#   ONLY KEEPS MASSES
#   FOR GENERIC GRIDS, key on the 'event_id', which uniquely identified which element of the sim_inspiral table was used
#
if opts.output_file:
    fname_output_txt = opts.output_file  + ".dat"
    if opts.sim_xml: 
        event_id = opts.event
    else:
        event_id = -1
    if opts.event == None:
        event_id = -1
    if not (P.lambda1>0 or P.lambda2>0):
      if not opts.pin_distance_to_sim:
        numpy.savetxt(fname_output_txt, numpy.array([[event_id, m1, m2, P.s1x, P.s1y, P.s1z, P.s2x, P.s2y, P.s2z,  numpy.log(res)+manual_avoid_overflow_logarithm, numpy.sqrt(var)/res,sampler.ntotal, neff ]]))  #dict_return["convergence_test_results"]["normal_integral]"
      else:
        # Use case for this scenario is NR, where lambda is not present
        numpy.savetxt(fname_output_txt, numpy.array([[event_id, m1, m2, P.s1x, P.s1y, P.s1z, P.s2x, P.s2y, P.s2z, pinned_params["distance"],  numpy.log(res)+manual_avoid_overflow_logarithm, numpy.sqrt(var)/res,sampler.ntotal, neff ]]))  #dict_return["convergence_test_results"]["normal_integral]"
    else:
        # Alternative output format if lambda is active
        numpy.savetxt(fname_output_txt, numpy.array([[event_id, m1, m2, P.s1x, P.s1y, P.s1z, P.s2x, P.s2y, P.s2z,  P.lambda1, P.lambda2, numpy.log(res)+manual_avoid_overflow_logarithm, numpy.sqrt(var)/res,sampler.ntotal, neff ]]))  #dict_return["convergence_test_results"]["normal_integral]"

# Standard XML output
#   ONLY KEEPS MASSES
#   NOT SAFE FOR GENERIC GRIDS
if opts.output_file:
    xmldoc = ligolw.Document()
    xmldoc.appendChild(ligolw.LIGO_LW())
    process.register_to_xmldoc(xmldoc, sys.argv[0], opts.__dict__)
    if opts.save_samples:
        samples = sampler._rvs
        # FIXME: Does sim insp do kpc or mpc
        samples["distance"] = samples["distance"]
        if not opts.time_marginalization:
            samples["t_ref"] += float(fiducial_epoch)
        else:
            samples["t_ref"] = float(fiducial_epoch)*numpy.ones(len(sampler._rvs["psi"]))
        samples["polarization"] = samples["psi"]
        samples["coa_phase"] = samples["phi_orb"]
        if ("declination", "right_ascension") in sampler.params:
            samples["latitude"], samples["longitude"] = samples[("declination", "right_ascension")]
        else:
            samples["latitude"] = samples["declination"]
            samples["longitude"] = samples["right_ascension"]
        samples["loglikelihood"] = numpy.log(samples["integrand"])+ manual_avoid_overflow_logarithm  # export with consistent offset
        if not opts.rom_integrate_intrinsic:
            # ILE mode: insert fixed model parameters
            samples["mass1"] = numpy.ones(samples["psi"].shape)*m1 # opts.mass1
            samples["mass2"] = numpy.ones(samples["psi"].shape)*m2 # opts.mass2
            samples["spin1x"] =numpy.ones(samples["psi"].shape)*P.s1x
            samples["spin1y"] =numpy.ones(samples["psi"].shape)*P.s1y
            samples["spin1z"] =numpy.ones(samples["psi"].shape)*P.s1z
            samples["spin2x"] =numpy.ones(samples["psi"].shape)*P.s2x
            samples["spin2y"] =numpy.ones(samples["psi"].shape)*P.s2y
            samples["spin2z"] =numpy.ones(samples["psi"].shape)*P.s2z
        else:
            # Intrinsic integration mode.  mtot is fixed but all others varying. Populate it.
            mtot = m1+m2
            # convention: q =m2/m1 < 1
            samples["mass1"] = mtot/(1+samples['q'])
            samples["mass2"] = mtot*samples['q']/(1+samples['q'])
            samples["spin1x"] =numpy.ones(samples["psi"].shape)*0
            samples["spin1y"] =numpy.ones(samples["psi"].shape)*0
            samples["spin1z"] =numpy.ones(samples["psi"].shape)*0
            samples["spin2x"] =numpy.ones(samples["psi"].shape)*0
            samples["spin2y"] =numpy.ones(samples["psi"].shape)*0
            samples["spin2z"] =numpy.ones(samples["psi"].shape)*0
            for spin_param in ["spin1x", "spin1y", "spin1z", "spin2x", "spin2y", "spin2z"]:
                spin_param_short =spin_param.replace('pin','')
                if (spin_param.replace('pin','') in intrinsic_param_names) and (spin_param_short in samples):
                    samples[spin_param] = samples[spin_param_short]
        if opts.output_format == 'xml':
            xmlutils.append_samples_to_xmldoc(xmldoc, samples)
        else:
            print( "writing hdf5 output file ", opts.output_file)
            # Do the write MANUALLY, in the interests of speed and to avoid lots of superfluous casts
            # Should reproduce functionality of ChooseWaveformParams_array_to_hdf5
            ref = numpy.ones(len(samples["distance"]))
            arr  = [ ref*P.m1/lal.MSUN_SI, ref*P.m2/lal.MSUN_SI, \
                         ref*P.s1x, ref*P.s1y, ref*P.s1z, ref*P.s2x, ref*P.s2y, ref*P.s2z,  \
                         samples["distance"],samples["inclination"], samples["phi_orb"], samples["declination"], samples["right_ascension"], samples["t_ref"], samples["psi"], \
                         ref*P.lambda1, ref*P.lambda2, ref*P.fref, ref*P.fmin, \
                         samples["loglikelihood"], samples["joint_prior"], samples["joint_s_prior"]
                     ]
            arr = numpy.array(arr,dtype=float).T
            import h5py
            f = h5py.File(opts.output_file+".hdf5", "w")
            f.create_dataset("waveform_parameters", (len(arr), len(arr[0])), dtype='f')   # initialization is giving an error...
            for indx in numpy.arange(len(arr)):
                f["waveform_parameters"][indx] = arr[indx]
            # Add attributes to describe monte carlo integration ... only produced by ILE
#            f.create_dataset("marginal_extrinsic_likelihood", (1,),dtype='f')
            f["marginal_extrinsic_likelihood"] = numpy.log(res)+ manual_avoid_overflow_logarithm
            f["marginal_extrinsic_likelihood"].attrs.create("lnLmarg", numpy.log(res)+manual_avoid_overflow_logarithm)
            f["marginal_extrinsic_likelihood"].attrs.create("sigma_lnLmarg", numpy.sqrt(var)/res )
            f["marginal_extrinsic_likelihood"].attrs.create("N", sampler.ntotal)
            f["marginal_extrinsic_likelihood"].attrs.create("n_eff", neff)
            

    # FIXME: likelihood or loglikehood
    # FIXME: How to encode variance?
    if opts.output_format == "xml":
        dict_out={"mass1": m1, "mass2": m2, "alpha5":P.lambda1, "alpha6":P.lambda2, "event_duration": numpy.sqrt(var)/res, "ttotal": sampler.ntotal}
        if 'distance' in pinned_params:
            dict_out['distance'] = pinned_params["distance"]
        converged_result = False
        if "convergence_test_results" in dict_return:
          converged_result = dict_return["convergence_test_results"]["normal_integral"]
        xmlutils.append_likelihood_result_to_xmldoc(xmldoc, numpy.log(res)+manual_avoid_overflow_logarithm, neff=neff, converged=converged_result, **dict_out)
        utils.write_filename(xmldoc, opts.output_file, compress=opts.output_file.endswith(".gz"))
