# GW Legacy Object and Target Identification

Using the DR8 Legacy Survey, identify all objects within the 90% credible interval of a GW alert angular reconstruction.

Then compare the selected objects against the bright targets selected from DR8.

```python
import os
import numpy as np
from astropy.io import fits
from astropy.table import Table, Column, join, hstack, vstack, unique
from scipy.stats import norm

try:
    from astropy.cosmology import Planck18_arXiv_v2
    cosmo = Planck18_arXiv_v2
except ImportError as e:
    print(e)
    from astropy.cosmology import Planck15
    cosmo = Planck15

import logging
import numpy as np
import healpy as hp
import matplotlib as mpl
import matplotlib.pyplot as plt

from gw_skymap import compute_quantiles, compute_contours, plot_mollmap, plot_cartmap

from desitarget import io, cuts
from desitarget.targetmask import desi_mask, bgs_mask

mpl.rc('font', size=12)
```

## DR8 Access Functions

Use the `desitarget.io` interface to access the DR8 sweep files and the corresponding photo-z catalogs. For more information see the [Legacy Survey DR8 File Directory](http://legacysurvey.org/dr8/files/) and the [photo-z catalog](https://arxiv.org/abs/2001.06018) developed for DR8 by Rongpu Zhou et al.

```python
def get_sweepfiles(sweepfolders=['/global/project/projectdirs/cosmo/data/legacysurvey/dr8/north/sweep/8.0', '/global/project/projectdirs/cosmo/data/legacysurvey/dr8/south/sweep/8.0']):
    """Given a search folder or folders, return a list of sweep files.

    For details on file locations see For details see http://legacysurvey.org/dr8/files/.

    Parameters
    ----------
    sweepfolders : str or list
        A folder or list of folders to search for sweep files.

    Returns
    -------
    sweeplist : list
        List of sweep files.
    """
    sweeplist = None

    if np.isscalar(sweepfolders):
        sweeplist = io.list_sweepfiles(sweepfolders)
    else:
        for fldr in sweepfolders:
            if sweeplist is None:
                sweeplist = io.list_sweepfiles(fldr)
            else:
                sweeplist += io.list_sweepfiles(fldr)

    return sweeplist

def get_objects_in_gw_contour(sweepfolders, lvc_hp_file, level=0.9):
    """Return a list of Legacy Survey objects inside a GW contour.

    Parameters
    ----------
    sweepfolders : str or list
        A folder or list of folders to search for sweep files.
    lvc_hp_file : str
        HEALPix file with LVC event reconstruction.
    level : float
        Contour level, in [0..1].

    Returns
    -------
    objtab : astropy.Table
        Table of Legacy Survey objects inside GW contour.
    """
    # Get luminosity distance to GW marginalized over all angles.
    hdus = fits.open(lvc_hp_file)
    header = hdus[1].header
    distmean = header['DISTMEAN']
    diststd = header['DISTSTD']

    # Get contours for the GW skymap. For further details, see
    # https://arxiv.org/abs/1605.04242
    gwmap, distmu, distsigma, distnorm = hp.read_map(lvc_hp_file, field=[0,1,2,3])
    gw_maxprob = np.max(gwmap)
    npix = len(gwmap)
    nside = hp.npix2nside(npix)
    thresh = compute_quantiles([level], gwmap)[0]
    logging.info('{:g}% confidence level of GW map {:s} = {:g}'.format(100*level, lvc_hp_file, thresh))

    # Get list of pixels above threshold defined by level.
    pixels = hp.ring2nest(nside, np.arange(npix))
    pixids = pixels[gwmap > thresh]

    # Loop through all sweeps, see if pixel indices match with any bricks.
    sweepfiles = get_sweepfiles(sweepfolders)
    gwsweeps = []
    for sf in sweepfiles:
        ramin, ramax, decmin, decmax = io.decode_sweep_name(sf)
        ra = [ramin, ramax, ramax, ramin]
        dec = [decmin, decmin, decmax, decmax]
        box = hp.ang2vec(ra, dec, lonlat=True)
        boxpix = hp.query_polygon(nside, box, nest=True)

        if not set(boxpix).isdisjoint(pixids):
            gwsweeps.append(sf)

    # Grab photo-z data and read everything into a table.
    gwspeeps, photozs = get_sweep_photo_z(gwsweeps)

    pixarea_deg2 = hp.nside2pixarea(nside, degrees=True)

    objtab = None
    for sf, pz in zip(gwsweeps, photozs):
        print(sf)
        data = Table(io.read_tractor(sf, columns=['BRICKID', 'BRICKNAME', 'OBJID', 'TYPE', 'RA', 'DEC',
                                                  'FLUX_G', 'FLUX_R', 'FLUX_Z',
                                                  'NOBS_G', 'NOBS_R', 'NOBS_Z',
                                                  'FRACIN_G', 'FRACIN_R', 'FRACIN_Z',
                                                  'FIBERFLUX_G', 'FIBERFLUX_R', 'FIBERFLUX_Z',
                                                  'PMRA', 'PMDEC', 'MASKBITS'
                                                  ]))
        data_pz = Table.read(pz)

        # Cut pixels not in 90% region.
        brickpix = hp.ang2pix(nside, data['RA'], data['DEC'], lonlat=True, nest=True)
        brickringpix = hp.nest2ring(nside, brickpix)

#         gw_prob = gwmap[hp.nest2ring(nside, brickpix)] / gw_maxprob
        dp_dA_deg2 = gwmap[brickringpix] / pixarea_deg2
        dist_mu = distmu[brickringpix]
        dist_sigma = distsigma[brickringpix]

        data.add_column(Column(name='angprob', data=dp_dA_deg2, unit='1/degree**2'))
        data.add_column(Column(name='distmu', data=dist_mu, unit='Mpc'))
        data.add_column(Column(name='distsigma', data=dist_sigma, unit='Mpc'))

        in_gw = np.isin(brickpix, pixids)
#         nonneg_z = data_pz['z_phot_mean'] >= 0
#         select = np.logical_and(in_gw, nonneg_z)
        select = in_gw

        if objtab is None:
            objtab = hstack([data[select], data_pz[select]])
        else:
            objtab = vstack([objtab, hstack([data[select], data_pz[select]])])

    # Compute luminosity distance given mean photo-z.
    nonzero_prob = objtab['angprob'] > 0
    logging.info('Computing luminosity distances for {:d} objects.'.format(np.sum(nonzero_prob)))

    d_L = np.zeros(len(objtab))
    nonneg_z = objtab['z_phot_mean'] >= 0
    d_L[nonzero_prob & nonneg_z] = cosmo.luminosity_distance(objtab[nonzero_prob & nonneg_z]['z_phot_mean']).to('Mpc').value
    objtab.add_column(Column(name='D_L', data=d_L, unit='Mpc'))

    return objtab

def get_sweep_photo_z(sweepfiles):
    """Return Legacy Survey photo-z files.

    Parameters
    ----------
    sweepfolders : str or list
        A folder or list of folders to search for sweep files.

    Returns
    -------
    sweeplist : list
        List of sweep files.
    photozlist : list
        List of photo-z files corresponding to sweep files.
    """
    # Search for matching photo-z files.
    sweeplist = sweepfiles
    photozlist = None
    logging.warning('Photo-z search hardcoded for DR8 v8.0.')

    for sweep in sweeplist:
        sweep_pz = sweep.replace('8.0', '8.0-photo-z').replace('.fits', '-pz.fits')

        # If photo-z file not found, remove corresponding sweep file from list.
        if os.path.exists(sweep_pz):
            if photozlist is None:
                photozlist = [sweep_pz]
            else:
                photozlist.append(sweep_pz)
        else:
            sweeplist.remove(sweep)

    return sweeplist, photozlist
```

### Access DR8 Catalog Data

Grab DR8 objects inside the 90% contour of a GW alert.

```python
gwfile = 'GW190412_skymap.fits.gz'
```

```python
logging.basicConfig(level=logging.INFO)

sweepfolders=['/global/project/projectdirs/cosmo/data/legacysurvey/dr8/north/sweep/8.0',
              '/global/project/projectdirs/cosmo/data/legacysurvey/dr8/south/sweep/8.0']

objcat = get_objects_in_gw_contour(sweepfolders, gwfile)
```

    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:368: UserWarning: If you are not specifying the input dtype and using the default np.float64 dtype of read_map(), please consider that it will change in a future version to None as to keep the same dtype of the input file: please explicitly set the dtype if it is important to you.
      warnings.warn(
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:391: UserWarning: NSIDE = 1024
      warnings.warn("NSIDE = {0:d}".format(nside))
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:400: UserWarning: ORDERING = NESTED in fits file
      warnings.warn("ORDERING = {0:s} in fits file".format(ordering))
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:428: UserWarning: INDXSCHM = IMPLICIT
      warnings.warn("INDXSCHM = {0:s}".format(schm))
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:486: UserWarning: Ordering converted to RING
      warnings.warn("Ordering converted to RING")
    INFO:root:90% confidence level of GW map GW190412_skymap.fits.gz = 5.45146e-05
    WARNING:root:Photo-z search hardcoded for DR8 v8.0.

    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/north/sweep/8.0/sweep-220p030-230p035.fits
    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/north/sweep/8.0/sweep-210p035-220p040.fits
    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/north/sweep/8.0/sweep-220p035-230p040.fits
    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/north/sweep/8.0/sweep-210p030-220p035.fits
    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/south/sweep/8.0/sweep-220p030-230p035.fits
    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/south/sweep/8.0/sweep-220p035-230p040.fits
    /global/project/projectdirs/cosmo/data/legacysurvey/dr8/south/sweep/8.0/sweep-210p030-220p035.fits

    INFO:root:Computing luminosity distances for 693283 objects.

```python
objcat
```

Table length=693283

BRICKID	BRICKNAME	OBJID	TYPE	RA	DEC	FLUX_G	FLUX_R	FLUX_Z	FRACIN_G	FRACIN_R	FRACIN_Z	NOBS_G	NOBS_R	NOBS_Z	FIBERFLUX_G	FIBERFLUX_R	FIBERFLUX_Z	MASKBITS	PMRA	PMDEC	angprob	distmu	distsigma	z_phot_mean	z_phot_median	z_phot_std	z_phot_l68	z_phot_u68	z_phot_l95	z_phot_u95	z_spec	survey	training	D_L

																					1 / deg2	Mpc	Mpc											Mpc

int32	bytes8	int32	bytes4	float64	float64	float32	float32	float32	float32	float32	float32	int16	int16	int16	float32	float32	float32	int16	float32	float32	float64	float64	float64	float32	float32	float32	float32	float32	float32	float32	float32	bytes10	bool	float64

520774	2200p350	120	REX	220.00547218267306	34.87891652956301	0.299475	0.3475637	0.83379847	0.96174586	0.98155975	0.9778689	3	3	3	0.13609639	0.15795028	0.3789196	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	1.1245359	1.1384652	0.17316614	0.97721624	1.2674745	0.826835	1.3724085	-99.0		False	7848.378821996121

520774	2200p350	188	REX	220.00515923398555	34.88593849092773	0.033515215	0.54722583	2.6155856	0.98841846	0.9942345	0.9953947	3	3	3	0.01958084	0.31970978	1.5281228	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	0.87700623	0.88085735	0.088218	0.7987393	0.95361805	0.715463	1.0721499	-99.0		False	5778.502622624202

520774	2200p350	189	REX	220.00416142052927	34.8846666769044	0.0319439	0.44346398	1.9286726	0.9944537	0.99713534	0.9898696	3	3	3	0.02117036	0.29389936	1.2781999	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	0.87286633	0.87083095	0.08737178	0.7987393	0.9549581	0.7334877	1.039759	-99.0		False	5745.003823197734

520774	2200p350	206	PSF	220.0103423627707	34.88651358031483	0.20157515	0.3935176	6.265988	0.99550956	0.99735004	0.9954478	3	3	3	0.15698245	0.30646315	4.8798184	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	0.9408334	0.9336581	0.16721074	0.782292	1.1056981	0.61344045	1.283881	-99.0		False	6299.9417680343995

520774	2200p350	207	PSF	220.00964713728507	34.88848907289682	3.7671883	9.913333	16.337103	0.998035	0.9990283	0.9985277	3	3	3	2.9221084	7.6895103	12.672259	0	-8.848959	-8.331199	0.018297946930730846	691.5288518785131	101.21136733610648	0.10149719	0.08302191	0.064052515	0.044936538	0.15850343	0.026605576	0.25802246	-99.0		False	483.42483881661315

520774	2200p350	264	REX	220.00521725303187	34.88895111662508	0.10648412	0.46953154	1.4247956	0.97379076	0.98419917	0.982465	3	3	3	0.046255935	0.20396112	0.618921	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	1.003098	0.98994505	0.19245338	0.8410225	1.1594908	0.67833716	1.31891	-99.0		False	6817.19690355357

520774	2200p350	286	REX	220.00296857153563	34.891072400832094	0.10847499	0.25110278	0.73652583	0.9699288	0.9827943	0.9792851	3	3	3	0.07403053	0.17136921	0.50265414	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	0.97122484	0.9861206	0.18175645	0.7910156	1.1604509	0.58081245	1.2765336	-99.0		False	6551.391512600344

520774	2200p350	303	REX	220.0072619352263	34.892946380344206	0.24502546	0.50076944	0.56281024	0.97856635	0.9888106	0.9855086	3	3	3	0.16633035	0.33993673	0.38205183	0	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	0.6804028	0.63718843	0.29804215	0.37182888	1.0439553	0.23886831	1.2687966	-99.0		False	4235.524395355795

520774	2200p350	313	PSF	220.00462172979192	34.894740910019316	0.29702884	0.8363302	1.5329294	0.9975393	0.9985922	0.99845195	3	3	3	0.2308587	0.650018	1.1914333	2048	0.0	0.0	0.018297946930730846	691.5288518785131	101.21136733610648	0.6090094	0.5672444	0.21280758	0.40167832	0.85674953	0.30279946	1.0728543	-99.0		False	3702.067606564681

...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...

518400	2191p345	3872	PSF	219.04165979458008	34.62008702254472	-0.2808396	0.21302198	2.2004673	0.9999993	0.9999982	0.9975924	3	3	4	0.0	0.16563648	1.7109861	2050	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	0.9696478	0.95863956	0.110597394	0.87780094	1.0627536	0.8131994	1.1768306	-99.0		False	6538.295554610893

518400	2191p345	3875	PSF	219.04582080403506	34.61561440662438	0.0987102	0.14294235	0.60161954	0.9989558	0.99939746	0.9963012	3	3	4	0.07689733	0.1113551	0.4686743	2050	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	0.9961203	0.97198975	0.16593741	0.86650884	1.1758518	0.6736173	1.293786	-99.0		False	6758.826184159665

518400	2191p345	3877	PSF	219.0505311943516	34.622249187437696	0.20834637	0.26262918	0.35262653	0.9996315	0.9997665	0.9994522	3	3	4	0.16196142	0.20415904	0.27411994	2050	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	1.0787265	1.1238459	0.25482282	0.85756683	1.3017149	0.4630921	1.4538361	-99.0		False	7456.0821625449325

518400	2191p345	3878	PSF	219.04894866050054	34.620827484027785	5.559911	9.300335	11.565174	0.9999986	1.0000004	0.9999967	3	3	4	4.306372	7.2034793	8.957687	2050	-11.34483	-1.2318364	0.017347518833312262	706.2289865620685	90.31774619051464	0.20580432	0.13469078	0.28438067	0.03217062	0.38219032	0.01499192	1.4010457	-99.0		False	1045.1464315696846

518400	2191p345	3879	PSF	219.04167297666635	34.623562739278064	13333.1045	14853.645	1689.3948	1.0000006	0.9999992	0.99758464	3	3	4	10335.588	11514.283	1309.589	2302	-2.5700262	12.713516	0.017347518833312262	706.2289865620685	90.31774619051464	0.0103215035	0.0040380796	0.013251308	0.0025412382	0.0147748	0.0017316352	0.05109316	-99.0		False	46.09371421170986

518400	2191p345	3880	REX	219.02649174360377	34.616632853768024	0.14120401	0.34628028	1.6189233	0.9828643	0.9899087	0.99472517	3	3	3	0.07871344	0.19303212	0.9024603	0	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	0.9903937	0.998581	0.14724684	0.8458413	1.137028	0.72661877	1.254385	-99.0		False	6710.996785131727

518400	2191p345	3881	PSF	219.0255641158013	34.61584210970091	0.32934555	0.49874195	0.80340356	0.9933562	0.99579316	0.99173474	3	3	3	0.25598338	0.3876465	0.6244443	0	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	1.0296072	1.0478766	0.23031433	0.82613856	1.2327766	0.5208811	1.3837968	-99.0		False	7039.853927765156

518400	2191p345	3921	REX	219.02888196521616	34.61826264545592	0.090415336	0.5954539	1.8108591	0.97695196	0.98506904	0.9897275	3	3	3	0.04614184	0.3038792	0.9241394	0	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	0.8139411	0.81440324	0.21307321	0.6535616	0.9777961	0.45102087	1.1454859	-99.0		False	5272.661453494475

518400	2191p345	3962	PSF	219.02965506486458	34.62092489427833	0.05217775	0.30892524	0.27253032	0.94846	0.9681175	0.97920954	3	3	3	0.040590167	0.24031943	0.21200705	0	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	0.7073338	0.65335613	0.24587893	0.48904362	0.9540711	0.37263086	1.2459624	-99.0		False	4440.721489489468

518400	2191p345	3980	REX	219.03040266732654	34.622584642783316	0.29571438	1.0674503	1.2317017	0.9102558	0.9267311	0.9312347	3	3	3	0.060014874	0.21663775	0.24997237	0	0.0	0.0	0.017347518833312262	706.2289865620685	90.31774619051464	0.7245742	0.7258577	0.28277138	0.45209324	1.0363222	0.2610013	1.3085917	-99.0		False	4573.171183368022

### Cuts on Luminosity Distance

Super-basic distance cut: for each DR8 object in the 90% angular credible interval, calculate the residual between its estimated mean luminosity distance computed from the DR8 photo-z catalog, $D_{L}$ and the mean marginal luminosity distance in the HEALPixel of the GW reconstruction, $d_L$:

$$
\begin{equation}
\Delta_i = \frac{D_{L,i} - d_{L,i}}{\sigma_{d_i}}
\end{equation}
$$

The residual ignores uncertainties in $d_L$ (which are non-Gaussian) and uses the Gaussian errors in $D_L$.

Here we select all objects with $|\Delta_i|\leq1.645$, defining the central interval 90% of the per-pixel luminosity distance.

```python
Delta_L = (objcat['D_L'] - objcat['distmu']) / objcat['distsigma']
rescut = np.abs(Delta_L) < 1.645
print('{} / {} objects ({:.2g}%) in 90% CI of GW luminosity distance.'.format(np.sum(rescut), len(objcat), 1e2*np.sum(rescut)/len(objcat)))
```

    6355 / 693283 objects (0.92%) in 90% CI of GW luminosity distance.

#### Photo-z Distribution after Cut

```python
fig, ax = plt.subplots(1,1, figsize=(6,5), tight_layout=True)
ax.hist(objcat['z_phot_mean'][rescut], bins=31)
ax.set(xlabel='photo $z$',
       ylabel='count',
       title='Target photo-z in GW 90% C.I.');
```

### Color-color diagrams

#### Including PSF Morph Types

```python
nonneg_mag = (objcat['FLUX_G'] > 0) & (objcat['FLUX_R'] > 0) & (objcat['FLUX_Z'] > 0)
select = rescut & nonneg_mag
g, r, z = [22.5 - 2.5*np.log10(objcat[select]['FLUX_{}'.format(_)]) for _ in 'GRZ']

# Cut on z<21 since dimmer galaxies have poor photo-z.
# See description at https://www.legacysurvey.org/dr8/files/#photometric-redshift-files-8-0-photo-z-sweep-brickmin-brickmax-pz-fits
#
# Also cut on r<21.
colcut = (z < 21) & (r < 21)

print('{} / {} objects ({:.2g}%) pass mag cuts.'.format(np.sum(colcut), np.sum(rescut), 1e2*np.sum(colcut)/np.sum(rescut)))
```

    6214 / 6355 objects (98%) pass mag cuts.

```python
fig, axes = plt.subplots(1,2, figsize=(12,5), gridspec_kw={'width_ratios':[1.1,1]})

ax = axes[0]
h = ax.hist2d(r[colcut] - z[colcut], g[colcut] - r[colcut],
              bins=[np.linspace(-0.5,1.5,81), np.linspace(-0.25,1.75,81)],
              norm=mpl.colors.LogNorm(), vmin=0.5,
              cmap='gray_r')
ax.set(xlim=(-0.5, 1.5), xlabel='$r-z$',
       ylim=(-0.25, 1.75), ylabel='$g-r$',
       aspect='equal')
ax.grid(ls=':')
fig.colorbar(h[3], ax=ax, fraction=0.046, pad=0.04)

ax = axes[1]
for _type in np.unique(objcat['TYPE']):
    select = rescut & nonneg_mag & (objcat['TYPE']==_type)
    print('{:4d} of type {}'.format(np.sum(select), _type))
    g_, r_, z_ = [22.5 - 2.5*np.log10(objcat[select]['FLUX_{}'.format(_)]) for _ in 'GRZ']

    ax.scatter(r_-z_, g_-r_, alpha=0.3, label=_type)

ax.set(xlim=(-0.5, 1.5), xlabel='$r-z$',
       ylim=(-0.25, 1.75), ylabel='$g-r$',
       aspect='equal')
ax.grid(ls=':')

ax.legend(fontsize=12, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)

fig.tight_layout();
```

#### Excluding PSF Morph Types

```python
nonpsf = objcat['TYPE'] != 'PSF'
select = rescut & nonneg_mag & nonpsf
g, r, z = [22.5 - 2.5*np.log10(objcat[select]['FLUX_{}'.format(_)]) for _ in 'GRZ']

colcut = (z < 21) & (r < 21)
```

```python
fig, axes = plt.subplots(1,2, figsize=(12,5), gridspec_kw={'width_ratios':[1.1,1]})

ax = axes[0]
h = ax.hist2d(r[colcut] - z[colcut], g[colcut] - r[colcut],
              bins=[np.linspace(-0.5,1.5,81), np.linspace(-0.25,1.75,81)],
              norm=mpl.colors.LogNorm(), vmin=0.5,
              cmap='gray_r')
ax.set(xlim=(-0.5, 1.5), xlabel='$r-z$',
       ylim=(-0.25, 1.75), ylabel='$g-r$',
       aspect='equal')
ax.grid(ls=':')
fig.colorbar(h[3], ax=ax, fraction=0.046, pad=0.04)

ax = axes[1]
for _type in np.unique(objcat['TYPE']):
    select_ = rescut & nonneg_mag & nonpsf & (objcat['TYPE']==_type)
    print('{:4d} of type {}'.format(np.sum(select_), _type))
    g_, r_, z_ = [22.5 - 2.5*np.log10(objcat[select_]['FLUX_{}'.format(_)]) for _ in 'GRZ']

    ax.scatter(r_-z_, g_-r_, alpha=0.3, label=_type)

ax.set(xlim=(-0.5, 1.5), xlabel='$r-z$',
       ylim=(-0.25, 1.75), ylabel='$g-r$',
       aspect='equal')
ax.grid(ls=':')

ax.legend(fontsize=12, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)

fig.tight_layout();
```

#### RA and Dec of Targets in 90% CI

Save imaging targets in 90% C.I. of the GW angular and distance maps in a table called `targimg90`.

```python
select = rescut & nonneg_mag & nonpsf

targimg90 = objcat[select][colcut]
ra90  = targimg90['RA']
dec90 = targimg90['DEC']
len(targimg90)
```

    2652

### Plot Contours and Targets

```python
fig = plot_mollmap(gwfile)
```

```python
fig = plot_cartmap(gwfile, tile_ra=[219.1,217.1,219.3,217.1], tile_dec=[37.0,36.75,35.8,35.55], angsize=3)
```

```python
fig = plot_cartmap(gwfile, targ_ra=ra90, targ_dec=dec90, angsize=3)
```

```python
fig = plot_cartmap(gwfile, tile_ra=[219.1,217.1,219.3,217.1], tile_dec=[37.0,36.75,35.8,35.55],
                   targ_ra=ra90, targ_dec=dec90,
                   angsize=3)
```

## Compare to DESI DR8 Target List

Compare selected targets with the DESI target list constructed from DR8.

```python
# Load sample of resolved DR8 target catalog using HEALPixels above threshold in the GW map.
# Get list of pixels above threshold defined by level.
gwmap = hp.read_map(gwfile)
npix = len(gwmap)
nside = hp.npix2nside(npix)
levels = [0.50, 0.90]
lev50, lev90 = compute_quantiles(levels, gwmap)

pixels = hp.ring2nest(nside, np.arange(npix))
pix90 = pixels[gwmap > lev90]

hpdirnames = ['/global/project/projectdirs/desi/target/catalogs/dr8/0.32.0/targets/main/resolve/bright',
              '/global/project/projectdirs/desi/target/catalogs/dr8/0.32.0/targets/main/resolve/dark']
readcols = ['TARGETID', 'BRICKID', 'BRICKNAME', 'BRICK_OBJID',
            'RA','DEC','FLUX_G','FLUX_R','FLUX_Z',
            'FIBERFLUX_G', 'FIBERFLUX_R', 'FIBERFLUX_Z',
            'DESI_TARGET', 'BGS_TARGET', 'MWS_TARGET']

targlist90 = None
for hpdirname in hpdirnames:
    if targlist90 is None:
        targlist90 = Table(io.read_targets_in_hp(hpdirname, nside=nside, pixlist=pix90, columns=readcols))
    else:
        targlist90 = vstack(targlist90, Table(io.read_targets_in_hp(hpdirname, nside=nside, pixlist=pix90, columns=readcols)))

targlist90.rename_column('BRICK_OBJID', 'OBJID')
targlist90 = unique(targlist90)
```

    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:368: UserWarning: If you are not specifying the input dtype and using the default np.float64 dtype of read_map(), please consider that it will change in a future version to None as to keep the same dtype of the input file: please explicitly set the dtype if it is important to you.
      warnings.warn(
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:391: UserWarning: NSIDE = 1024
      warnings.warn("NSIDE = {0:d}".format(nside))
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:400: UserWarning: ORDERING = NESTED in fits file
      warnings.warn("ORDERING = {0:s} in fits file".format(ordering))
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:428: UserWarning: INDXSCHM = IMPLICIT
      warnings.warn("INDXSCHM = {0:s}".format(schm))
    /global/common/software/desi/cori/desiconda/20200801-1.4.0-spec/conda/lib/python3.8/site-packages/healpy/fitsfunc.py:486: UserWarning: Ordering converted to RING
      warnings.warn("Ordering converted to RING")

```python
targlist90
```

Table length=26190

BRICKID	BRICKNAME	OBJID	RA	DEC	FLUX_G	FLUX_R	FLUX_Z	FIBERFLUX_G	FIBERFLUX_R	FIBERFLUX_Z	TARGETID	DESI_TARGET	BGS_TARGET	MWS_TARGET

int32	str8	int32	float64	float64	float32	float32	float32	float32	float32	float32	int64	int64	int64	int64

517202	2172p342	3420	217.32170704239283	34.35840678532735	12.638921	18.531113	16.345072	9.818534	14.395878	12.697653	35190939437763932	1152921504606848004	1028	0

517203	2175p342	3292	217.5175800107535	34.35568071560189	85.90801	137.87901	156.25786	66.71956	107.08229	121.35603	35190939441958108	2305843009213693952	0	768

517203	2175p342	3315	217.67716348895294	34.357030403683744	4.174772	10.211371	19.985794	1.9672549	4.811849	9.417796	35190939441958131	1152921504606846976	257	0

517203	2175p342	3416	217.50147214786998	34.363536365505034	3.4577537	10.584017	22.246555	1.637756	5.013092	10.537024	35190939441958232	1152921504606846976	257	0

517203	2175p342	3507	217.66025709201858	34.37328197517194	16.125097	55.553238	108.29026	4.0550365	13.970174	27.232143	35190939441958323	1152921504606846976	514	0

517203	2175p342	3539	217.38947442578856	34.373279599411276	9.161235	26.089565	45.344254	7.078085	20.157125	35.03354	35190939441958355	2305843009213693952	0	17

517203	2175p342	3579	217.57772038781792	34.37482991368313	7.555036	12.255058	18.409058	2.4707534	4.0078206	6.020387	35190939441958395	1152921504606846976	257	0

517204	2178p342	3006	217.698322414612	34.32746630799084	14.746882	28.900888	47.725773	6.8382215	13.401523	22.13074	35190939446152126	1152921504606846976	514	0

517204	2178p342	3376	217.69661710217235	34.348338429426946	34.036846	43.371677	44.015182	26.480774	33.7433	34.243954	35190939446152496	2305843017803628544	0	768

517204	2178p342	3451	217.69566892501612	34.3520748416281	31.835701	69.62782	99.6162	24.709175	54.041412	77.316795	35190939446152571	2305843009213693952	0	17

...	...	...	...	...	...	...	...	...	...	...	...	...	...	...

535827	2195p382	1149	219.45890292396695	38.19230405012936	6.6203666	12.657649	18.145159	1.1174905	2.136559	3.0628278	35191017556673661	1152921504606846976	257	0

535827	2195p382	1185	219.39503912639393	38.19562177314849	5.622457	11.120316	19.254799	1.9412695	3.8395188	6.6481175	35191017556673697	1152921504606846976	257	0

535827	2195p382	1341	219.44854858693395	38.20371366576783	2.2205915	9.894485	23.231268	0.9078275	4.045087	9.497461	35191017556673853	1152921504606846976	257	0

535827	2195p382	1349	219.4589526690255	38.20391911374192	9.922669	26.690067	49.597843	7.704513	20.723656	38.51053	35191017556673861	2305843009213693952	0	17

535827	2195p382	1546	219.38169548498422	38.21509592135576	32.689	41.548374	44.824276	25.45565	32.354645	34.905663	35191017556674058	3458764513820541956	1028	768

535827	2195p382	1594	219.3886495649766	38.21735099238032	23.435886	35.23999	40.299145	18.222393	27.400583	31.33429	35191017556674106	2305843009213693952	0	768

535827	2195p382	1690	219.39017120376147	38.22381318589947	187.19334	276.6022	293.65536	145.6013	215.14462	228.4088	35191017556674202	2305843009213693952	0	768

535827	2195p382	1972	219.39838161501046	38.24018871160725	3.6763546	11.155056	20.36888	1.2294501	3.7304847	6.8117805	35191017556674484	1152921504606846976	257	0

535827	2195p382	2505	219.39206237520213	38.27278713416629	6.21216	14.524345	25.823402	1.8336846	4.2872477	7.6224656	35191017556675017	1152921504606846976	257	0

535827	2195p382	2519	219.379677024461	38.27380236009604	94.251686	182.13329	241.84436	73.16267	141.38055	187.73116	35191017556675031	2305843009213693952	0	17

### Select Bright Targets

```python
bgs_bright = (targlist90['BGS_TARGET'] & bgs_mask.mask('BGS_BRIGHT')) != 0
print('{:d} / {:d} = {:.1f}% are BGS bright targets'.format(np.sum(bgs_bright), len(targlist90), 1e2*np.sum(bgs_bright)/len(targlist90)))
btarglist90 = targlist90[bgs_bright]
```

    8887 / 26190 = 33.9% are BGS bright targets

### Merge Bright Target List and Legacy Object List

```python
bmerg90 = join(btarglist90, targimg90)
bmerg90
```

Table length=1285

BRICKID	BRICKNAME	OBJID	RA	DEC	FLUX_G	FLUX_R	FLUX_Z	FIBERFLUX_G	FIBERFLUX_R	FIBERFLUX_Z	TARGETID	DESI_TARGET	BGS_TARGET	MWS_TARGET	TYPE	FRACIN_G	FRACIN_R	FRACIN_Z	NOBS_G	NOBS_R	NOBS_Z	MASKBITS	PMRA	PMDEC	angprob	distmu	distsigma	z_phot_mean	z_phot_median	z_phot_std	z_phot_l68	z_phot_u68	z_phot_l95	z_phot_u95	z_spec	survey	training	D_L

																									1 / deg2	Mpc	Mpc											Mpc

int32	str8	int32	float64	float64	float32	float32	float32	float32	float32	float32	int64	int64	int64	int64	bytes4	float32	float32	float32	int16	int16	int16	int16	float32	float32	float64	float64	float64	float32	float32	float32	float32	float32	float32	float32	float32	bytes10	bool	float64

517204	2178p342	3006	217.698322414612	34.32746630799084	14.746882	28.900888	47.725773	6.8382215	13.401523	22.13074	35190939446152126	1152921504606846976	514	0	DEV	0.9994961	0.999735	0.9990711	3	2	2	0	0.0	0.0	0.01805349887406861	727.1341182438007	68.04122174744968	0.13043317	0.132	0.04835773	0.09094202	0.15946013	0.06671418	0.18248098	0.121	AGES	True	632.9945304746334

517205	2181p342	4155	218.01651222310053	34.367024289638884	15.844108	25.228413	34.743473	3.6480696	5.8087845	7.9996057	35190939450347579	1152921504606846976	514	0	EXP	0.99863625	0.99927014	0.9989221	3	2	3	0	0.0	0.0	0.01773110154002257	734.1478248901077	71.72880806372346	0.15011331	0.14880313	0.04530541	0.10351113	0.19020343	0.08425448	0.24423756	0.086	AGES	True	737.5279265787558

518392	2167p345	3427	216.77749077571406	34.599112324068095	12.467034	33.201458	60.611603	4.951968	13.187785	24.07523	35190944428985699	1152921504606846976	514	0	DEV	0.9989129	0.9989112	0.9992322	3	3	3	0	0.0	0.0	0.018747119810922124	740.2405514676218	65.8342369359907	0.16679022	0.16745132	0.020375727	0.14774409	0.18141003	0.1290661	0.21066007	0.15	AGES	True	827.8420684862903

518392	2167p345	3770	216.85740162518186	34.62383308776465	23.696693	42.460014	68.408165	11.512006	20.627348	33.233128	35190944428986042	1152921504606846976	514	0	REX	0.99984276	0.99988425	0.9998141	3	3	4	0	0.0	0.0	0.018747119810922124	740.2405514676218	65.8342369359907	0.13384336	0.1306698	0.043583624	0.100164406	0.16537806	0.07123941	0.20047535	-99.0		False	650.947494671639

518393	2170p345	1871	217.0567900433536	34.48411439925034	12.929968	21.996403	29.030655	2.7429028	4.666214	6.1584272	35190944433178447	1152921504606846976	514	0	DEV	0.898563	0.8990796	0.8936767	5	3	3	0	0.0	0.0	0.018690270030463522	729.3810406051895	67.62080302704516	0.15510556	0.1593765	0.044551153	0.10323312	0.20077054	0.073163904	0.23232198	0.217	AGES	True	764.3984999732311

518393	2170p345	1885	216.93502083532078	34.4855446136992	10.75762	29.106361	65.793465	2.171131	5.8743215	13.278608	35190944433178461	1152921504606846976	514	0	DEV	0.9074176	0.7138627	0.95355326	4	3	2	0	0.0	0.0	0.016972625566651967	736.2871587526995	66.82453140652734	0.15020607	0.14861366	0.03602544	0.1272634	0.16883409	0.087340586	0.21735342	-99.0		False	738.0258980751505

518393	2170p345	1915	217.02866630192273	34.48634840699794	13.156257	36.196327	67.27595	5.0520887	13.899627	25.834406	35190944433178491	1152921504606846976	514	0	DEV	0.99734724	0.99755204	0.99828434	4	3	3	0	0.0	0.0	0.018690270030463522	729.3810406051895	67.62080302704516	0.15650101	0.1545615	0.016949337	0.14196181	0.17372322	0.12996574	0.1879321	0.15	AGES	True	771.9347863314695

518393	2170p345	3055	217.0648267743176	34.554737642971375	9.080436	16.893353	27.820974	4.0564957	7.546754	12.428441	35190944433179631	1152921504606846976	514	0	EXP	0.9999416	0.99990815	0.99938077	4	3	3	0	0.0	0.0	0.018690270030463522	729.3810406051895	67.62080302704516	0.15248819	0.14419352	0.034252472	0.12356032	0.18792471	0.095615745	0.21891962	0.121	AGES	True	750.292905757753

518393	2170p345	3113	217.18188209486695	34.558574829659214	7.7371125	18.871597	33.236805	3.4858189	8.502263	14.974253	35190944433179689	1152921504606846976	514	0	DEV	0.9994984	0.99954844	0.99776787	3	2	4	0	0.0	0.0	0.022934251222360365	725.2495014932111	68.32969931789121	0.13460998	0.13055232	0.019758925	0.12063115	0.1494968	0.09967856	0.18012904	0.128	AGES	True	654.9926366002887

...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...	...

535824	2185p382	1107	218.52722682927242	38.199211609148136	14.739609	31.279882	54.877193	2.6007233	5.519164	9.68278	35191017544090707	1152921504606846976	514	0	EXP	0.99811304	0.99829817	0.7503086	3	3	3	0	0.0	0.0	0.020738730249639632	718.2430339956314	67.46347366977615	0.15214862	0.15273093	0.0361559	0.123964205	0.17849028	0.08434387	0.20400219	-99.0		False	748.4657174221802

535824	2185p382	1233	218.683683207337	38.208347943616864	19.526527	55.660133	103.86749	8.172307	23.295063	43.470966	35191017544090833	1152921504606846976	514	0	DEV	0.7500023	0.999999	1.0000002	3	3	3	0	0.0	0.0	0.019954498015597243	719.7641305590067	65.63913001784675	0.15774465	0.15735501	0.02668559	0.14427258	0.17008524	0.11339835	0.19052361	-99.0		False	778.6604461832151

535824	2185p382	1997	218.48271527776257	38.26619669848076	25.759712	69.98396	141.94691	7.551453	20.515783	41.611702	35191017544091597	1152921504606846976	514	0	DEV	0.9967881	0.999846	0.99993664	3	3	3	0	0.0	0.0	0.020738730249639632	718.2430339956314	67.46347366977615	0.14164197	0.13771072	0.014886395	0.12808421	0.15588143	0.12087998	0.17943	-99.0		False	692.2572623363621

535824	2185p382	2005	218.4864308290955	38.26992569123575	60.43118	166.49287	333.59262	11.482482	31.635183	63.38568	35191017544091605	1152921504606846976	514	0	DEV	0.99337107	0.99332416	0.9946654	3	3	3	0	0.0	0.0	0.020738730249639632	718.2430339956314	67.46347366977615	0.1354715	0.1357984	0.007092447	0.12886089	0.14145274	0.1212517	0.14746566	0.13965961	SDSS	False	659.5426326710548

535824	2185p382	2330	218.6649391132775	38.29005105410981	40.47078	111.24249	210.59953	8.143422	22.383917	42.376274	35191017544091930	1152921504606846976	514	0	COMP	0.999075	0.9990429	0.9991663	3	3	3	0	0.0	0.0	0.019954498015597243	719.7641305590067	65.63913001784675	0.13489291	0.1348674	0.0102901235	0.12695712	0.14447622	0.1060109	0.15397221	0.1399779	SDSS	False	656.4864633064038

535825	2188p382	1027	218.90626133615785	38.1992426318086	9.645618	16.252201	21.798458	1.9193542	3.2339792	4.337613	35191017548284931	1152921504606846976	514	0	EXP	0.9983221	0.99834716	0.9965496	3	3	3	0	0.0	0.0	0.020453462656028754	723.1840560703097	65.2473953863434	0.16460413	0.16418464	0.04015563	0.13402976	0.19167401	0.08981387	0.23658219	-99.0		False	815.9139830947737

535825	2188p382	1436	219.01710865821315	38.227436907832676	15.670828	34.215424	56.051647	3.156193	6.891179	11.289118	35191017548285340	1152921504606846976	514	0	EXP	0.999752	0.9996738	0.9005101	4	4	3	0	0.0	0.0	0.022141823395066113	726.6933264676862	65.11265130417075	0.15720734	0.149055	0.04657958	0.11656906	0.202687	0.07956937	0.26614022	-99.0		False	775.7535950151439

535826	2192p382	1082	219.1757918345981	38.203968398172144	16.425348	42.669323	85.81605	2.9407518	7.6394043	15.364283	35191017552479290	1152921504606846976	514	0	EXP	0.9987185	0.9988496	0.9985924	3	3	3	0	0.0	0.0	0.021861761763492818	728.8669829513661	64.6434179238771	0.14781749	0.14193009	0.029199667	0.12793772	0.16767442	0.10427656	0.22094919	-99.0		False	725.2184431295879

535826	2192p382	2657	219.06022659190427	38.31963458808512	12.732157	36.278717	66.697586	4.5878105	13.072403	24.033312	35191017552480865	1152921504606846976	514	0	DEV	0.9920012	0.991563	0.9892246	4	4	3	0	0.0	0.0	0.016637930180319845	726.0948507372464	63.00263951046688	0.1618518	0.15976469	0.028187044	0.14833514	0.17650495	0.13513413	0.20500278	-99.0		False	800.934308016393

535827	2195p382	283	219.41426003191128	38.14255175128988	10.474364	16.020508	19.804758	2.4991102	3.822381	4.725277	35191017556672795	1152921504606846976	514	0	REX	0.99977463	0.99982	0.9997131	3	4	4	0	0.0	0.0	0.021728861048180115	728.0005008386934	63.15364993124503	0.15111306	0.15286885	0.029548699	0.12728617	0.17331778	0.08578966	0.21278122	-99.0		False	742.8976310357283

### Plot Bright Targets in GW Contour

Plot all BGS bright targets in the 90% region of the GW event, color coded using the relative angular probability.

```python
fig = plot_cartmap(gwfile, tile_ra=[219.1,217.1,219.3,217.1], tile_dec=[37.0,36.75,35.8,35.55],
                   targ_ra=bmerg90['RA'], targ_dec=bmerg90['DEC'],
                   angsize=3)
```

```python
fig, ax = plt.subplots(1,1, figsize=(7,5))

for _type in np.unique(bmerg90['TYPE']):
    select = bmerg90['TYPE']==_type
    print('{:4d} of type {}'.format(np.sum(select), _type))
    g_, r_, z_ = [22.5 - 2.5*np.log10(bmerg90[select]['FLUX_{}'.format(_)]) for _ in 'GRZ']

    ax.scatter(r_-z_, g_-r_, alpha=0.3, label=_type)

ax.set(xlim=(-0.5, 1.5), xlabel='$r-z$',
       ylim=(-0.25, 1.75), ylabel='$g-r$',
       aspect='equal')
ax.grid(ls=':')

ax.legend(fontsize=12, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)

fig.tight_layout();
```

```python
r_bgs = 22.5 - 2.5*np.log10(bmerg90['FLUX_R'])
```

```python
fig, ax = plt.subplots(1,1, figsize=(6,4), tight_layout=True)

binsr = np.linspace(15,22,29)

ax.hist(r, bins=binsr, label='$r$ (LS DR8)')
ax.hist(r_bgs, bins=binsr, label='$r$ (BGS DR8)')
ax.set(xlabel='$r$', ylabel='count')
ax.legend(fontsize=12);
```

```python

```


# Synthetic Data Experiments (Visualizations)

```julia
include("src/Misc.jl")
using .Misc
```

```julia
import PyPlot, Seaborn
import Suppressor: @suppress_err
```

```julia
MARKER = ["o", "v", "d", "X", "p", "*", "P"];
PATH = "./results/synt";
IMG = "./img/synt";
```

## On Hyperparameters of BAM

\begin{equation}
X^{(1)} = \left(\begin{array}{cccc}
    2 & 1 & 1 & 0 \\
    0 & 0 & 1 & 2 \\
    0 & 0 & 1 & 1
    \end{array} \right)
\end{equation}

```julia
result = load_json("$PATH/effective_params.json")

R = Int.(result["R"])
As = Float64.(result["a"])
dₑₚ = Int.(result["EP"])
log_PS = hcat(result["log_PS"]...);
log_PX = logsumexp(log_PS,1);
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(1, 1+length(As); figsize=(14,3))

ax[1].set_ylabel("# of \$ S \$")
for (i,a) ∈ enumerate(As)
    ax[i].set_title("\$ a = $a \$")
    ax[i].hist(log_PS[:,i]; bins=200, label="\$ \\log \\pi(S) \$")
    ax[i].axvline(log_PX[i]; label="\$ \\log \\mathcal{L}_X \$", linestyle="-.", linewidth=2.0, color="red")
    ax[i].set_xlabel("\$ \\log \\pi(S) \$")
    ax[i].legend()
end

ax[end].set_title("Effective Parameters")
ax[end].hist(dₑₚ; bins=200, label="\$ d_{EP} \$")
ax[end].set_xlabel("\$ d_{EP}\$")
ax[end].invert_xaxis()
ax[end].legend()

PyPlot.savefig("$IMG/alloc_tensor_dist.pdf", bbox_inches="tight");
end
```

## Comparison of SMC, VB and exact enumeration on toy data

\begin{equation}
X^{(1)} = \left(\begin{array}{cccc}
    2 & 1 & 1 & 0 \\
    0 & 0 & 1 & 2 \\
    0 & 0 & 1 & 1
    \end{array} \right)
\end{equation}

```julia
result = load_json("$PATH/marginal_lkhd.json")

As = Float64.(result["a"])
Rs = Int.(result["R"])

log_PX = hcat(result["log_PX"]...)
log_PX_smc = hcat(result["log_PX_smc"]...)
log_PX_vb = hcat(result["log_PX_vb"]...);
```

```julia
log_PR = log_PX .- logsumexp(log_PX,dims=1);
log_PR_smc = log_PX_smc .- logsumexp(log_PX_smc,dims=1);
log_PR_vb = log_PX_vb .- logsumexp(log_PX_vb,dims=1);
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(2, 3; figsize=(12,7.5))

for (r,R) in enumerate(Rs)
    ax[1,1].semilogx(As,log_PX[r,:], label="K = $R", marker=MARKER[r])
    ax[2,1].semilogx(As,log_PR[r,:], label="K = $R", marker=MARKER[r])
    ax[1,2].semilogx(As,log_PX_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[2,2].semilogx(As,log_PR_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[1,3].semilogx(As,log_PX_vb[r,:], label="K = $R", marker=MARKER[r])
    ax[2,3].semilogx(As,log_PR_vb[r,:], label="K = $R", marker=MARKER[r])
end
ax[1,1].set_ylabel("\$ \\log \\mathcal{L}_X(K) \$");
ax[2,1].set_ylabel("\$ \\log \\mathrm{Pr}(K \\mid X) \$");
ax[1,1].set_title("Marginal Log-Likelihood");
ax[1,2].set_title("Marginal Log-Likelihood (SMC)");
ax[1,3].set_title("Marginal Log-Likelihood (ELBO)");
ax[2,1].set_title("Posterior Log Odds");
ax[2,2].set_title("Posterior Log Odds (SMC)");
ax[2,3].set_title("Posterior Log Odds (ELBO)");

foreach(i -> ax[2,i].set_xlabel("\$ a \$"), 1:3);
foreach(i -> ax[1,i].set_xlim(ax[1,1].get_xlim()), 1:3);
foreach(i -> ax[2,i].set_ylim(-3), 1:3);
for i ∈ 1:2, j ∈ 1:3; ax[i,j].legend() end

PyPlot.savefig("$IMG/marginal_lkhd.pdf", bbox_inches="tight");
end
```

\begin{equation}
X^{(2)} = \left(\begin{array}{ccc}
    4 & 3 & 0 \\
    0 & 0 & 3 \\
    0 & 0 & 3
    \end{array} \right)
\end{equation}

```julia
result = load_json("$PATH/marginal_lkhd2.json")

As = Float64.(result["a"])
Rs = Int.(result["R"])

log_PX = hcat(result["log_PX"]...)
log_PX_smc = hcat(result["log_PX_smc"]...)
log_PX_vb = hcat(result["log_PX_vb"]...);
```

```julia
log_PR = log_PX .- logsumexp(log_PX,dims=1);
log_PR_smc = log_PX_smc .- logsumexp(log_PX_smc,dims=1);
log_PR_vb = log_PX_vb .- logsumexp(log_PX_vb,dims=1);
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(2, 3; figsize=(12,7.5))

for (r,R) in enumerate(Rs)
    ax[1,1].semilogx(As,log_PX[r,:], label="K = $R", marker=MARKER[r])
    ax[2,1].semilogx(As,log_PR[r,:], label="K = $R", marker=MARKER[r])
    ax[1,2].semilogx(As,log_PX_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[2,2].semilogx(As,log_PR_smc[r,:], label="K = $R", marker=MARKER[r])
    ax[1,3].semilogx(As,log_PX_vb[r,:], label="K = $R", marker=MARKER[r])
    ax[2,3].semilogx(As,log_PR_vb[r,:], label="K = $R", marker=MARKER[r])
end
ax[1,1].set_ylabel("\$ \\log \\mathcal{L}_X(K) \$");
ax[2,1].set_ylabel("\$ \\log \\mathrm{Pr}(K \\mid X) \$");
ax[1,1].set_title("Marginal Log-Likelihood");
ax[1,2].set_title("Marginal Log-Likelihood (SMC)");
ax[1,3].set_title("Marginal Log-Likelihood (ELBO)");
ax[2,1].set_title("Posterior Log Odds");
ax[2,2].set_title("Posterior Log Odds (SMC)");
ax[2,3].set_title("Posterior Log Odds (ELBO)");

foreach(i -> ax[2,i].set_xlabel("\$ a \$"), 1:3);
foreach(i -> ax[1,i].set_xlim(ax[1,1].get_xlim()), 1:3);
foreach(i -> ax[2,i].set_ylim(-3.5,0.0), 1:3);
for i ∈ 1:2, j ∈ 1:3; ax[i,j].legend() end

PyPlot.savefig("$IMG/marginal_lkhd2.pdf", bbox_inches="tight");
end
```

## Posterior Distribution of the number of tokens $S_+$

\begin{equation}
X^{(3)} = \left(\begin{array}{cc}
    3 & ? \\
    3 & 3 \\
    \end{array} \right)
\end{equation}

```julia
result₁ = load_json("$PATH/Sp_dist.json")

Ts = Int.(result₁["T"])
Rs = Int.(result₁["R"])
log_PT = hcat(result₁["log_PT"]...);
```

```julia
fig, ax = PyPlot.subplots(1, 1; figsize=(5,3))
for (r,R) in enumerate(Rs)
    ax.plot(Ts,log_PT[r,:], label="K = $R", marker=MARKER[r])
end
ax.set_title("Distribution of \$ \\mathbf{S}_+ \$");
ax.set_ylabel("\$ \\log \\mathrm{Pr}(\\mathbf{S}_+ = T,  X^{(3)} \\mid K) \$");
ax.set_xlabel("\$ T \$")
ax.legend();

PyPlot.savefig("$IMG/Sp_dist.pdf", bbox_inches="tight");
```

\begin{equation}
X^{(4)} = \left(\begin{array}{ccc}
    4 & ? \\
    4 & 1
    \end{array} \right)
\end{equation}

```julia
result₂ = load_json("$PATH/Sp_dist2.json")

Ts = Int.(result₂["T"])
Rs = Int.(result₂["R"])
log_PT = hcat(result₂["log_PT"]...);
```

```julia
fig, ax = PyPlot.subplots(1, 1; figsize=(5,3))
for (r,R) in enumerate(Rs)
    ax.plot(Ts,log_PT[r,:], label="K = $R", marker=MARKER[r])
end
ax.set_title("Distribution of \$ \\mathbf{S}_+ \$");
ax.set_ylabel("\$ \\log \\mathrm{Pr}(\\mathbf{S}_+ = T,  X^{(4)} \\mid K) \$");
ax.set_xlabel("\$ T \$")
ax.legend();

PyPlot.savefig("$IMG/Sp_dist2.pdf", bbox_inches="tight");
```

## Model Order Selection for CP/PARAFAC

### Confusion Matrix

```julia
result = load_json("$PATH/parafac_confusion.json")

I, J, K, T = result["I"], result["J"], result["K"], result["T"]
Rs = Int.(result["R"])
As = Float64.(result["a"])

R_true = Int.(result["R_true"])
R_smc = Int.(result["R_smc"])
R_vb = Int.(result["R_vb"]);
```

```julia
conf_smc = zeros(Int,length(Rs),length(Rs));
conf_vb = zeros(Int,length(Rs),length(Rs));
```

```julia
for i ∈ 1:length(R_true)
    conf_smc[R_true[i],R_smc[i]] += 1
    conf_vb[R_true[i],R_vb[i]] += 1
end
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(1; figsize=(4,3))
Seaborn.heatmap(conf_vb; annot=true, cmap="YlGnBu", cbar=false, xticklabels=Rs, yticklabels=Rs, linewidths=.4, ax=ax)

ax.set_title("Confusion Matrix of Model Orders")
ax.set_ylabel("\$ R_\\mathrm{true} \$")
ax.set_xlabel("\$ R_\\mathrm{VB} \$")

PyPlot.savefig("$IMG/parafac_confusion_vb.pdf", bbox_inches="tight");
end
```

```julia
@suppress_err begin
fig, ax = PyPlot.subplots(1; figsize=(4,3))
Seaborn.heatmap(conf_smc; annot=true, cmap="YlGnBu", cbar=false, xticklabels=Rs, yticklabels=Rs, linewidths=.4, ax=ax)

ax.set_title("Confusion Matrix of Model Orders")
ax.set_ylabel("\$ R_\\mathrm{true} \$")
ax.set_xlabel("\$ R_\\mathrm{SMC} \$")

PyPlot.savefig("$IMG/parafac_confusion_smc.pdf", bbox_inches="tight");
end
```

### An Example

```julia
result = load_json("$PATH/parafac_model_selection.json")

I, J, K, R_true = result["I"], result["J"], result["K"], result["R_true"]
Rs = Int.(result["R"])
a = result["a"]
log_PX_smc, log_PX_vb = Float64.(result["log_PX_smc"]), Float64.(result["log_PX_vb"]);
```

```julia
PyPlot.figure(figsize=(5,3))

PyPlot.axvline(R_true; label="G. Rank", color="black", linestyle="--")
PyPlot.plot(Rs, log_PX_vb; label="ELBO", marker=MARKER[5]);
PyPlot.plot(Rs, log_PX_smc; label="SIS/R", marker=MARKER[6]);

PyPlot.xlabel("\$R\$ (Model order)")
PyPlot.ylabel("\$ \\log \\mathcal{L}_X(R) \$")
PyPlot.ylim(-2200)
PyPlot.title("Model Scoring Example (PARAFAC)");
PyPlot.legend();

PyPlot.savefig("$IMG/cp_model_selection.pdf", bbox_inches="tight");
```

### Runtimes

```julia
result = load_json("$PATH/parafac_runtime.json")

T, P, R, a = result["T"], result["P"], result["R"], result["a"]
IJK = Array{Array{Int,1},1}(result["IJK"])
smc_times = hcat(result["smc"]...)
vb_times = hcat(result["vb"]...);
```

```julia
PyPlot.figure(figsize=(5,3))
PyPlot.plot(1:length(IJK),nanmean(vb_times,2); label="VB", marker=MARKER[5])
PyPlot.plot(1:length(IJK),nanmean(smc_times,2); label="SIS/R", marker=MARKER[6])
PyPlot.title("Average Runtimes (PARAFAC)")
PyPlot.ylabel("sec.")
PyPlot.xlabel(" \$ I_1 \\times I_2 \\times I_3 \$ (Dimensions of \$X\$)")
PyPlot.xticks(1:2:length(IJK),["\$ {$(4*i)}^3 \$" for i ∈ 1:2:length(IJK)])
PyPlot.legend()

PyPlot.savefig("$IMG/runtimes.pdf", bbox_inches="tight");
```


```python
%matplotlib widget
```

```python
import numpy as np
import matplotlib.pyplot as plt
import pydae.ssa as ssa
import scipy.signal as sctrl
```

```python
from vsc_lcl import vsc_lcl_class
```

## Instantiate system

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation = 1
syst.N_store = 100_000
syst.update()
```

## CTRL1 in state feedback

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 50e-6
#x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = [ 'i_sd_g01', 'i_sq_g01']     # outputs to consider in the controller
u_ctrl_list = ['eta_d_g01','eta_q_g01']     # intputs to consider in the controller
z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]
u_ctrl_idxs = [syst.inputs_run_list.index(item) for item in u_ctrl_list]

syst.Δt = Δt

## Calculate equilibirum point
syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0, 'C_m_g01':4e-6}],xy0=1000)
ssa.eval_ss(syst)

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Δt,method='zoh')

N_z_d,N_x_d = C_d.shape  # discreticed plant dimensions
N_x_d,N_u_d = B_d.shape

# convenient matrices
O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
B_c = B_d[:,u_ctrl_idxs]
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:][:,u_ctrl_idxs]

N_x_c,N_u_d = B_c.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref
omega_b = 2*np.pi*50

W =  np.block([
                [   np.cos(omega_b*Δt), -np.sin(omega_b*Δt)],
                [   np.sin(omega_b*Δt),  np.cos(omega_b*Δt)],
               ])

A_e = np.block([
                [    A_d,  B_c@W, O_xu],    # Δx_d
                [   O_ux,   O_uu, O_uu],    # Δx_r
                [ Δt*C_c, Δt*D_c, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

A_ctrl = A_e[N_x_d:,N_x_d:]
B_ctrl = B_e[N_x_d:]

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e6
Q_c[-2,-2] = 1e6

R_c = np.eye(B_c.shape[1])*100000

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt

syst.A_ctrl = A_ctrl
syst.B_ctrl = B_ctrl
syst.K_c = K_c
syst.N_x_d = N_x_d  # number of plant states
syst.N_u_d = N_u_d  # number of plant inputs
syst.N_z_c = N_z_c  # number of plant outputs considered for the controller
```

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()
times = np.arange(0.0,0.1,Δt)

syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0, 'C_m_g01':4e-6}],xy0=1000)
ssa.eval_A(syst)
i_sd = syst.get_value('i_sd_g01')
i_sq = syst.get_value('i_sq_g01')
v_sd = syst.get_value('v_sd_g01')
v_sq = syst.get_value('v_sq_g01')
i_td = syst.get_value('i_td_g01')
i_tq = syst.get_value('i_tq_g01')
v_md = syst.get_value('v_md_g01')
v_mq = syst.get_value('v_mq_g01')
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq
v_sq_0 = v_sq
v_sd_0 = v_sd
x_d_0 = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)
u_d_0 = np.array([eta_d,eta_q]).reshape(2,1)
x_r_0 = u_d_0
syst.Δx_e = np.zeros((10,1))
it = 0
for t in times:

    Δx_e = syst.Δx_e
    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    i_td = syst.get_value('i_td_g01')
    i_tq = syst.get_value('i_tq_g01')
    v_md = syst.get_value('v_md_g01')
    v_mq = syst.get_value('v_mq_g01')
    v_dc = syst.get_value('v_dc_g01')

    x_d = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)

    Δx_d = x_d - x_d_0
    Δx_r = syst.Δx_e[N_x_c:-N_u_d,:]
    Δx_i = syst.Δx_e[(N_x_c+N_u_d):,:]

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    v_sq = v_sq_0
    v_sd = v_sd_0
    if t>20e-3: i_sd_ref = 20
    if t>30e-3: i_sq_ref = 30
    if t>45e-3: v_sd =  163
    if t>45e-3: v_sq = -163
    epsilon_d = i_sd - i_sd_ref
    epsilon_q = i_sq - i_sq_ref

    epsilon = np.block([[epsilon_d],[epsilon_q]])

    Δu_r = -K_c @ Δx_e + np.block([[ (v_sd-v_sd_0)*2/v_dc],[(v_sq-v_sq_0)*2/v_dc]])

    Δx_r = W@Δu_r
    Δx_i += Δt*epsilon

    Δx_e = np.block([[Δx_d],[Δx_r],[Δx_i]])

    syst.Δx_e  =  Δx_e

    x_r = Δx_r + x_r_0

    eta_dq = x_r
    eta_d = eta_dq[0,0]
    eta_q = eta_dq[1,0]

    events=[{'t_end':t,'eta_d_g01':eta_d,'eta_q_g01':eta_q,'v_1_Q':v_sq,'v_1_D':v_sd}]
    syst.run(events)

#    eta_d_prev = eta_d
#    eta_q_prev = eta_q
    it += 1

syst.post();

```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7),sharex=True)

lines = axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
lines = axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

axes[1].plot(syst.T,syst.get_values('eta_D_g01'),label='eta_D_g01')
axes[1].plot(syst.T,syst.get_values('eta_Q_g01'),label='eta_Q_g01')
for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')

datacursor(lines, display='multiple')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    <mpldatacursor.datacursor.DataCursor at 0x28035551488>

```python
import sympy as sym

x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6 = sym.symbols('Dx_d_1,Dx_d_2,Dx_d_3,Dx_d_4,Dx_d_5,Dx_d_6')
x_r_1,x_r_2 = sym.symbols('Dx_r_1,Dx_r_2')
x_i_1,x_i_2 = sym.symbols('Dx_i_1,Dx_i_2')

x_e = sym.Matrix([x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6,x_r_1,x_r_2,x_i_1,x_i_2])
u_r = -K_c * x_e
```

```python
u_r_d = str(sym.N(u_r[0],8))
u_r_q = str(sym.N(u_r[1],8))

print(f'Du_r_1 = {u_r_d};')
print(f'Du_r_2 = {u_r_q};')
```

    Du_r_1 = -0.013355528*Dx_d_1 - 0.000487326*Dx_d_2 + 0.0021855202*Dx_d_3 + 8.7205357e-5*Dx_d_4 + 0.0071938875*Dx_d_5 + 0.00034010605*Dx_d_6 - 2.2549315*Dx_i_1 + 0.65433868*Dx_i_2 - 0.36460717*Dx_r_1 - 0.0049366387*Dx_r_2;
    Du_r_2 = 0.000487326*Dx_d_1 - 0.013355528*Dx_d_2 - 8.7205357e-5*Dx_d_3 + 0.0021855202*Dx_d_4 - 0.00034010605*Dx_d_5 + 0.0071938875*Dx_d_6 - 0.65433868*Dx_i_1 - 2.2549315*Dx_i_2 + 0.0049366387*Dx_r_1 - 0.36460717*Dx_r_2;

```python
Du_r_1,Du_r_2 = sym.symbols('Du_r_1,Du_r_2')
Du_r = sym.Matrix([Du_r_1,Du_r_2 ])

Dx_r = W@Du_r

Dx_r_1 = str(sym.N(Dx_r[0],8))
Dx_r_1 = str(sym.N(Dx_r[1],8))

print(f'Dx_r_1 = {u_r_d};')
print(f'Dx_r_2 = {u_r_q};')
```

$\displaystyle \left[\begin{matrix}0.999876632481661 Du_{r 1} - 0.0157073173118207 Du_{r 2}\\0.0157073173118207 Du_{r 1} + 0.999876632481661 Du_{r 2}\end{matrix}\right]$

```python
print(u_r[0])
```

    0.023914820787791*Dx_d_1 + 0.00124312201525765*Dx_d_2 + 0.00210868343440974*Dx_d_3 + 9.76673467759675e-5*Dx_d_4 - 0.0363072666129747*Dx_d_5 - 0.0013695946212219*Dx_d_6 - 10.022203616505*Dx_i_1 + 1.45742192739155*Dx_i_2 - 0.327372378893965*Dx_r_1 - 0.000502327486946109*Dx_r_2

```python
print(u_r[1])
```

    -0.00124312201525856*Dx_d_1 + 0.0239148207877911*Dx_d_2 - 9.76673467759516e-5*Dx_d_3 + 0.00210868343440976*Dx_d_4 + 0.00136959462122251*Dx_d_5 - 0.0363072666129741*Dx_d_6 - 1.45742192739181*Dx_i_1 - 10.0222036165041*Dx_i_2 + 0.000502327486927267*Dx_r_1 - 0.327372378893964*Dx_r_2

```python
syst.get_value('C_m_g01')
```

    4e-06

```python
from mpldatacursor import datacursor

data = np.outer(range(10), range(1, 5))

fig, ax = plt.subplots()
lines = ax.plot(data)
ax.set_title('Click somewhere on a line')

#datacursor(lines)
datacursor(display='multiple', draggable=True)
plt.show()
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

```python
import matplotlib.pyplot as plt
import numpy as np
from mpldatacursor import datacursor

data = np.outer(range(10), range(1, 5))

plt.plot(data)
plt.title('Click somewhere on a line')

datacursor()

plt.show()
```

```python

```

```python
Ts_ctr = 1/200;
Ts_med = 1/20000;

wN_ctr = 2*pi*1/Ts_ctr/2;
wN_med = 2*pi*1/Ts_med/2;

[nA, Wn] = buttord(wN_ctr, wN_med, -20*log10(0.7), -20*log10(0.1), 's');
[NUM_aaA,DEN_aaA] = butter(nA,Wn,'low','s');
```

```python
from scipy import signal
import matplotlib.pyplot as plt

```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(7, 7),sharex=True)

N, Wn = signal.buttord([20, 50], [14, 60], 3, 40, True)
b, a = signal.butter(N, Wn, 'band', True)
w, h = signal.freqs(b, a, np.logspace(1, 2, 500))
axes.plot(w/2/np.pi, 20 * np.log10(abs(h)))
plt.title('Butterworth bandpass filter fit to constraints')
axes.set_xlabel('Frequency [radians / second]')
axes.set_ylabel('Amplitude [dB]')
axes.grid(which='both', axis='both')
axes.fill([1,  14,  14,   1], [-40, -40, 99, 99], '0.9', lw=0) # stop
axes.fill([20, 20,  50,  50], [-99, -3, -3, -99], '0.9', lw=0) # pass
axes.fill([60, 60, 1e9, 1e9], [99, -40, -40, 99], '0.9', lw=0) # stop
axes.set_xlim([0, 20e3])

```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    (0, 20000.0)

```python
b
```

    array([1.59811317e+19, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
           0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
           0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
           0.00000000e+00, 0.00000000e+00])

```python
a
```

    array([1.00000000e+00, 2.48932358e+02, 4.39836594e+04, 5.53273916e+06,
           5.72496657e+08, 4.90725288e+10, 3.64084365e+12, 2.34845893e+14,
           1.34274060e+16, 6.81861593e+17, 3.10677599e+19, 1.27024773e+21,
           4.68749241e+22, 1.55890260e+24, 4.68749241e+25, 1.27024773e+27,
           3.10677599e+28, 6.81861593e+29, 1.34274060e+31, 2.34845893e+32,
           3.64084365e+33, 4.90725288e+34, 5.72496657e+35, 5.53273916e+36,
           4.39836594e+37, 2.48932358e+38, 1.00000000e+39])

```python

```


# 营销活动优化

## 目的和先决条件

此 Jupyter Notebook 描述了银行和金融服务行业常见的营销活动优化问题。

该问题使用 Gurobi Python API 构建，并使用 Gurobi Optimizer 解决。

在这个营销活动问题中，数学优化模型的关键参数是通过机器学习预测响应模型预估出来的。

营销活动优化问题是指在满足各种业务约束的情况下，为使总预期利润最大化而向每个客户提供何种产品。

该建模示例处于初级阶段，我们假设你了解Python，并且具有一些有关构建数学优化模型的知识。希望你可以从示例中了解所有遗漏的概念。

**注意：** 你可以通过单击 [此处](https://github.com/arvinxx/gurobi-and-mathematical-modeling/archive/master.zip) 下载包含此示例和其他示例的代码。为了正确运行此 Jupyter Notebook，你必须具有 Gurobi 许可证。如果你没有，则可以**商业用户身份**申请 [试用许可证](https://www.gurobi.com/downloads/request-an-evaluation-license/)，或以**学术用户身份**下载 [免费许可证](https://www.gurobi.com/academia/academic-program-and-licenses)。

## 动机

银行和金融服务行业营销的主要目标是“在正确的时间向正确的客户提供正确的产品”。然而，实际上能够实现这一目标是一项复杂且具有挑战性的任务。让这变得特别困难的是，公司拥有多种产品，并在一组复杂的业务约束下运作。为了最大化市场投资回报和满足业务约束，选择向哪些客户提供哪些产品是非常复杂的。

假设有一家大型银行，该银行已努力成为一家以客户为中心的机构，而不是一家垂直产品驱动型公司。该银行的目标是“通过提供满足客户独特需求的解决方案，尽最大可能帮助他们在经济上有所改善”。此目标的直接结果是，营销活动是多个产品活动，而不是单个产品活动。

这将数据科学和营销活动的定位过程从相当简单的单个响应模型变为一个相当复杂的过程，包括选择哪个产品通过哪个渠道提供给哪个客户。

银行的营销团队习惯于将业务规则直接应用于目标客户。例如，他们仅根据产品差距或营销人员的业务直觉来瞄准客户。该银行的营销人员还应用了RFM类型分析，使用一般近因、频率、货币度量以及产品缺口来针对特定的目标客户。

营销团队目前广泛使用的方法是依赖预测响应模型来锁定客户。这些模型估计客户响应特定报价的可能性，并且可以显着提高对产品报价的响应率。然而，当一家公司有好几种产品要推销，在营销计划中要考虑其他业务约束时，仅仅知道客户对特定报价做出响应概率是不够的。

一般来说，营销团队还面临一个问题，就是要知道该向客户提供哪种产品，而不仅仅是该向哪个客户提供产品。在实践中，使用了许多特殊规则:

* 基于响应率或估计预期盈利能力指标的优先级规则

* 对可以销售的产品进行优先级排序的业务规则

* 为特定活动选择客户的产品响应模型。

有一种方法很容易实现，但可能不能产生最佳的客户联系方案，它依赖于预期报价获利能力的指标，以选择向客户提供哪些产品。然而，这种方法的一个缺点是它不能有效地处理客户联系方案上的复杂约束。

为了解决这个营销活动优化问题，M.D.Cohen[1] 提出了一种基于丰业银行数据的MIP方法。营销活动优化问题考虑了11种独特的报价：5种投资、3种贷款和3种日常银行报价。投资优惠包括担保投资证书（GICs）、共同基金、注册教育储蓄计划（RESP）和两个独特的折扣经纪服务。贷款优惠包括一项抵押贷款和两项信用卡贷款。

日常银行服务包括两项 Scotia 网上银行服务中的一项和存款账户获取服务。在这里，“活动”一词指的是一个大型的、积极主动的客户联系活动，包括11个不同的优惠;它可以被认为是十一个单一的产品活动，通常是在同一时间提供给一个不重叠的客户群。该活动的潜在目标市场包括大约250万客户。

在此Jupyter Notebook中，我们将使用此MIP方法来解决银行的营销活动优化问题。应该注意的是，几乎所有行业的任何公司都可以使用这种方法在其业务限制优化营销活动方案。

## 问题描述

银行的营销团队需要确定向每个客户提供什么样的产品，以使营销活动的投资回报最大化，同时考虑以下限制因素：

* 活动可用资金的限制。

* 活动中可以提供的最低产品数量的限制。

* 活动必须满足的投资回报率最低要求。

## 解决方法

数学优化（也称为数学编程）是一种声明性方法，其中建模者制定了一个优化问题，该问题捕获了复杂决策问题的关键特征。然后 Gurobi 使用最先进的数学和计算机科学技术来解决这一类数学优化问题。

数学优化模型具有五个组成部分：

* 数据集（Sets）
* 参数(Parameters)
* 决策变量(Decision variables)
* 约束（Constraints）
* 目标函数（Objective function(s)）

现在，我们针对此营销活动优化问题提出一种MIP方法。

Cohen[1]提出的MIP解决方案方法是对传统的短视方法（myopic approach）的改进，即选择对特定产品具有最大预期价值的客户，因为它从银行的角度产生了一个全局最优的解决方案，并允许有效地在构建有关客户和业务部门的业务约束。这种方法考虑到有限的资源和其他业务限制。

我们假设对客户/要约的预期增量利润，成本和业务约束的估计可以用作营销活动优化方法的输入。优化阶段与这些输入的构造无关。

MIP方法涉及一个策略和操作问题。

对于策略问题，我们根据单个预期利润参数汇总客户。可以使用诸如预测响应模型之类的数据科学技术来确定估计的个人预期利润。关键思想是对估计的单个预期利润进行聚类，然后将聚类质心视为代表单个聚类中所有单个客户的数据的代表。这种聚合使问题可以表述为线性规划问题，因此该模型无需为单个客户分配报价，而是在考虑业务约束的同时，为每个产品报价在每个集群中确定要最大化营销活动投资回报率的比例。通常，集群中的客户数量将成千上万，这是策略问题的主要决策变量，因此，这些变量可以视为连续变量；因此，线性规划方法是合理的。

操作问题可以表述为MIP模型，其中估计的个人预期利润和战术模型的输出可以用作输入，以将*产品报价* 分配给每个集群的单个客户，从而使整个营销活动投资回报最大化。

## 策略模型制定

### 集合与索引

$k \in K$: 类的索引和集合。

$j \in J$: 产品的索引和集合。

### 参数
$\pi_{k,j}$: 将产品 $j \in J$ 提供给类 $k \in K$ 的客户时所获得的平均期望利润。

$\nu_{k,j}$: 将产品 $j \in J$ 提供给类 $k \in K$ 的客户时平均可变成本。

$N_{k}$: 类 $k \in K$ 中包含的客户数量。

$Q_{j}$: 产品 $j \in J$ 的最低要约数量。

$R$: 企业最低回报率。此最低回报率用于计算营销活动的投资回报率。

$B$: 营销活动预算。

$M$: Big M 惩罚。此惩罚与预算修正相关，预算修正是满足其他业务限制所必需的。

### 决策变量
$y_{k,j} \geq 0$: 类 $k \in K$  中的客户接受产品 $j \in J$ 要约的客户数量。

$z \geq 0$: 增加预算，以便有一个可行的营销活动。

### 目标函数

- **总利润**： 最大限度地提高营销活动的预期总利润，并对任何对预算的调整进行严厉惩罚。

\begin{equation}
\text{Max} \quad Z = \sum_{k \in K} \sum_{j \in J} \pi_{k,j} \cdot y_{k,j} - M \cdot z
\tag{0}
\end{equation}

### 约束条件

- **要约数量**： 每个集群中产品的最大要约数量受集群中客户数量的限制。

\begin{equation}
\sum_{j \in J} y_{k,j} \leq N_{k} \quad \forall k \in K
\tag{1}
\end{equation}

- **预算**： 营销活动预算约束要求活动的总成本应该低于活动预算。有可能需要增加预算以确保模型的可行性。因为为了使所有产品达到最低要约数量，可能需要增加预算。

\begin{equation}
\sum_{k \in K} \sum_{j \in J} \nu_{k,j} \cdot y_{k,j} \leq B + z
\tag{2}
\end{equation}

- **要约限制**： 每个产品的最低要约数量。

\begin{equation}
\sum_{k \in K} y_{k,j} \geq Q_{j}  \quad \forall j \in J
\tag{3}
\end{equation}

- **ROI**： 最小 ROI 约束可确保总利润与成本的比率至少为 1 + 企业的最低预期回报率。

\begin{equation}
\sum_{k \in K} \sum_{j \in J} \pi_{k,j} \cdot y_{k,j} \geq (1+R) \cdot \sum_{k \in K} \sum_{j \in J} \nu_{k,j} \cdot y_{k,j}
\tag{4}
\end{equation}

## 操作模型制定

一旦找到了策略模型中所有 $j \in J$ 和 $k \in K$ 的最优值 $y_{k,j}$ ，我们就需要确定集群 $k$ 中的哪个个体客户得到哪个产品的要约。假设给定集群 $k \in K$ 中，产品 $j_1$ 和 $j_2$ 的要约分配为正，即 $y_{k,j_1} > 0$ 和$y_{k,j_2} > 0$。那么，集群 $k$ 中的顾客 $y_{k,j_1}$ 和 $y_{k,j_2}$ 必须分别是产品 $j_1 $和 $j_2$ 的要约。要做到这一点，最优的方法是使用单个客户的预期利润来解决分配问题，而不是使用集群的预期利润。

我们现在提供操作问题的表述。

### 集合与索引
$i \in I^{k}$: 集群 $k \in K$ 中客户的索引和集合。

$j \in J^{k}$: 集群 $k \in K$ 中提供给客户的产品索引和子集，其中 $J^{k} = \{ j \in J: y_{k,j} > 0 \}$ .

### 参数

$r_{k,i,j}$: 从客户 $i \in I^{k}$ 的产品要约 $j \in J^{k}$ 的预期个人利润。

$Y_{k,j} = \lfloor y_{k,j} \rfloor $: 在集群 $k$ 中产品 $j \in J^{k}$ 提供要约的客户数量
.

### 决策变量
$x_{k,i,j} \in \{0,1 \}$: 如果产品 $j \in J^{k}$  给客户 $i \in I^{k}$ 提供了要约，此变量等于1, 否则为 0。

### 目标函数
- **总利润**： 最大化总个人利润。

\begin{equation}
\text{Max} \quad Z = \sum_{k \in K} \sum_{i \in I^{k}} \sum_{j \in J^{k}} r_{k,i,j} \cdot x_{k,i,j}
\tag{0}
\end{equation}

### 约束

- **产品要约**： 将产品的要约分配给每个集群的客户。

\begin{equation}
\sum_{i \in  I^{k}}  x_{k,i,j} = Y_{k,j}  \quad \forall j \in J^{k}, k \in K
\tag{1}
\end{equation}

- **要约限制**： 最多可以向集群的一个客户提供一个产品。

\begin{equation}
\sum_{j \in J^{k}} x_{k,i,j} \leq 1 \quad \forall i \in I^{k}, k \in K
\tag{2}
\end{equation}

- **布尔限制**： 是否向集群 $k$ 的客户提供产品要约。

\begin{equation}
x_{k,i,j} \in \{0,1 \} \quad \forall i \in I^{k},  j \in J^{k}, k \in K
\tag{3}
\end{equation}

## 问题实例

我们考虑两个产品、十个客户和两个客户群。公司的最低回报率预期是百分之二十。

### 策略问题数据

下表定义了提供产品时每个集群中平均客户的预期利润。

|  | Product 1 | Product 2 |
| --- | --- |  --- |
| cluster 1 | $\$2,000$ | $\$1,000$ |
| cluster 2 | $\$3,000$ | $\$2,000$ |

下表确定了向集群中的普通客户提供产品的预期成本。

|  | Product 1 | Product 2 |
| --- | --- |  --- |
| cluster 1 | $\$200$ | $\$100$ |
| cluster 2 | $\$300$ | $\$200$ |

这个营销活动的预算是$\$200$。

下表给出了每个集群中的客户数量。

|  | Num. Customers |
| --- | --- |
| cluster 1 | 5 |
| cluster 2 | 5 |

下表列出每种产品的最低要约数量。

|  | Min Offers |
| --- | --- |
| product 1 | 2 |
| product 2 | 2 |

### 操作问题数据

下表显示了向每个集群中每个客户提供产品的预期利润。

|  | Product 1 | Product 2 |
| --- | --- |  --- |
| cluster 1, customer 1 | $\$2,050$ | $\$1,050$ |
| cluster 1, customer 2 | $\$1,950$ | $\$950$ |
| cluster 1, customer 3 | $\$2,000$ | $\$1,000$ |
| cluster 1, customer 4 | $\$2,100$ | $\$1,100$ |
| cluster 1, customer 5 | $\$1,900$ | $\$900$ |
| cluster 2, customer 6 | $\$3,000$ | $\$2,000$ |
| cluster 2, customer 7 | $\$2,900$ | $\$1,900$ |
| cluster 2, customer 8 | $\$3,050$ | $\$2,050$ |
| cluster 2, customer 9 | $\$3,100$ | $\$2,100$ |
| cluster 2, customer 10 | $\$2,950$ | $\$1,950$ |

下表显示了向集群中的客户提供产品的成本。

|  | Product 1 | Product 2 |
| --- | --- |  --- |
| cluster 1, customer 1 | $\$205$ | $\$105$ |
| cluster 1, customer 2 | $\$195$ | $\$95$ |
| cluster 1, customer 3 | $\$200$ | $\$100$ |
| cluster 1, customer 4 | $\$210$ | $\$110$ |
| cluster 1, customer 5 | $\$190$ | $\$90$ |
| cluster 2, customer 6 | $\$300$ | $\$200$ |
| cluster 2, customer 7 | $\$290$ | $\$190$ |
| cluster 2, customer 8 | $\$305$ | $\$205$ |
| cluster 2, customer 9 | $\$310$ | $\$210$ |
| cluster 2, customer 10 | $\$295$ | $\$195$ |

## Python 实现

首先导入 Gurobi 的 Python 模块和相关 Python 库。然后用给定的数据初始化数据结构。

```python
import gurobipy as gp
from gurobipy import GRB

# 官方测试版本 Gurobi v9.0.0 & Python 3.7.0

# 译者测试版本 Gurobi v9.1.0 & Python 3.8.6

### 集合

products = ['p1', 'p2']
clusters = ['k1', 'k2']
```

### 预期利润

下表显示了提供产品时每个集群中客户的预期利润。

|  | Product 1 | Product 2 |
| --- | --- |  --- |
| cluster 1 | $\$2,000$ | $\$1,000$ |
| cluster 2 | $\$3,000$ | $\$2,000$ |

```python
### 参数

# 预期利润
cp, expected_profit = gp.multidict({
    ('k1', 'p1'): 2000,
    ('k1', 'p2'): 1000,
    ('k2', 'p1'): 3000,
    ('k2', 'p2'): 2000
})

```

### 预期成本

下表显示了产品对客户的预期成本。

|  | Product 1 | Product 2 |
| --- | --- |  --- |
| cluster 1 | $\$200$ | $\$100$ |
| cluster 2 | $\$300$ | $\$200$ |

```python
# 预期成本

cp, expected_cost = gp.multidict({
    ('k1', 'p1'): 200,
    ('k1', 'p2'): 100,
    ('k2', 'p1'): 300,
    ('k2', 'p2'): 200
})
```

### 客户数量

下表中列出了每个群集中的客户数量。

|  | Num. Customers |
| --- | --- |
| cluster 1 | 5 |
| cluster 2 | 5 |

```python
# 每个集群中的客户数量

clusters, number_customers = gp.multidict({
    ('k1'): 5,
    ('k2'): 5
})
```

### 最低要约数量

下表列出每种产品的最低要约数量。

|  | 最低要约 |
| --- | --- |
| product 1 | 2 |
| product 2 | 2 |

```python
# 每个产品的最低要约数量

products, min_offers = gp.multidict({
    ('p1'): 2,
    ('p2'): 2
})
```

### 标量

公司的最低回报率是百分之二十 ($R = 0.20$).

可用于市场营销活动的预算是 $\$200$.

```python
# 标量

R = 0.20

# 预算紧张
budget = 200
```

## 执行模型制定

### 决策变量

$y_{k,j} \geq 0$: 集群 $k \in K$ 中被提供产品 $j \in J$ 要约的客户数量。

$z \geq 0$: 为了使营销活动可行的额外增加的预算。

```python
# 初始化决策变量
mt = gp.Model('Tactical')

### 决策变量

# 分配要约给集群中的客户。

y = mt.addVars(cp, name="allocate")

# 预算纠正

z = mt.addVar(name="budget_correction")
```

    Using license file /Users/arvinxx/gurobi.lic
    Academic license - for non-commercial use only - expires 2021-02-01

### 约束

- **要约数量**： 每个集群中产品的最大要约数量受集群中客户数量的限制。

\begin{equation}
\sum_{j \in J} y_{k,j} \leq N_{k} \quad \forall k \in K
\tag{1}
\end{equation}

其中：

$y_{k,j} \geq 0$:  集群 $k \in K$ 中被提供产品 $j \in J$ 要约的客户数量。

$N_{k}$: 集群 $k \in K$ 中总共客户数量。

```python
### 约束

# 约束：集群中的要约数量

maxOffers_cons = mt.addConstrs((y.sum(k,'*') <= number_customers[k]  for k in clusters), name='maxOffers')

```

### 约束

- **预算**： 营销活动预算约束要求活动的总成本应该低于活动预算。有可能需要增加预算以确保模型的可行性。因为为了使所有产品达到最低要约数量，可能需要增加预算。

\begin{equation}
\sum_{k \in K} \sum_{j \in J} \nu_{k,j} \cdot y_{k,j} \leq B + z
\tag{2}
\end{equation}

其中：

$y_{k,j} \geq 0$:  集群 $k \in K$ 中被提供产品 $j \in J$ 要约的客户数量。

$z \geq 0$: 为了使营销活动可行的额外增加的预算。

$\nu_{k,j}$: 与向集群 $k \in K$ 的客户提供产品 $j \in J$ 的平均成本。

$B$: 营销活动总预算。

```python
# 预算约束

budget_con = mt.addConstr((y.prod(expected_cost) - z <= budget), name='budget')
```

### 约束

- **要约限制**： 每个产品的最低要约数量。

\begin{equation}
\sum_{k \in K} y_{k,j} \geq Q_{j}  \quad \forall j \in J
\tag{3}
\end{equation}

其中：

$y_{k,j} \geq 0$: 集群 $k \in K$ 中被提供产品 $j \in J$ 要约的客户数量。

$Q_{j}$: 产品 $j \in J$ 的最小要约数量。

```python
# 约束：最小要约数量

minOffers_cons = mt.addConstrs( (y.sum('*',j) >= min_offers[j] for j in products), name='min_offers')

```

### 约束

- **ROI**： 最小 ROI 约束可确保总利润与成本的比率至少为 1 + 企业的最低预期回报率。

\begin{equation}
\sum_{k \in K} \sum_{j \in J} \pi_{k,j} \cdot y_{k,j} \geq (1+R) \cdot \sum_{k \in K} \sum_{j \in J} \nu_{k,j} \cdot y_{k,j}
\tag{4}
\end{equation}

其中：

$y_{k,j} \geq 0$:  集群 $k \in K$ 中被提供产品 $j \in J$ 要约的客户数量。

$\pi_{k,j}$: 与向集群 $k \in K$ 的客户提供产品 $j \in J$ 的预期利润。

$\nu_{k,j}$: 与向集群 $k \in K$ 的客户提供产品 $j \in J$ 的平均成本。

$R$: 企业要求的最低 ROI。

```python
# 约束：保证 ROI

ROI_con = mt.addConstr((y.prod(expected_profit) - (1 + R)*y.prod(expected_cost) >= 0), name='ROI')
```

### 目标函数
- **总利润**： 最大化市场营销活动的总预期利润，并对任何对预算的修正进行惩罚。

\begin{equation}
\text{Max} \quad Z = \sum_{k \in K} \sum_{j \in J} \pi_{k,j} \cdot y_{k,j} - M \cdot z
\tag{0}
\end{equation}

其中：

$y_{k,j} \geq 0$: 集群 $k \in K$ 中被提供产品 $j \in J$ 要约的客户数量。

$z \geq 0$: 为了使营销活动可行，额外增加的预算。

$\pi_{k,j}$: 与向集群 $k \in K$ 的客户提供产品 $j \in J$ 的预期利润。

**注意：**  $M$  的值应该高于任何预期利润，以确保仅在不增加此参数且模型不可行时才增加预算。

```python
### 目标哈数

# 最大化总利润

M = 10000

mt.setObjective(y.prod(expected_profit) -M*z, GRB.MAXIMIZE)
```

```python
# 验证模型公式

mt.write('tactical.lp')
```

```python
# 执行优化引擎

mt.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (mac64)
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads
    Optimize a model with 6 rows, 5 columns and 17 nonzeros
    Model fingerprint: 0x1eac2f22
    Coefficient statistics:
      Matrix range     [1e+00, 3e+03]
      Objective range  [1e+03, 1e+04]
      Bounds range     [0e+00, 0e+00]
      RHS range        [2e+00, 2e+02]
    Presolve removed 1 rows and 0 columns
    Presolve time: 0.00s
    Presolved: 5 rows, 5 columns, 13 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    2.5000000e+04   8.787500e+01   0.000000e+00      0s
           4   -3.9940000e+06   0.000000e+00   0.000000e+00      0s

    Solved in 4 iterations and 0.01 seconds
    Optimal objective -3.994000000e+06

```python
### 输出报告

# 为集群客户提供产品要约的最优分配方案

total_expected_profit = 0
total_expected_cost = 0

print("\n📃 最优分配方案")
print("--------------------------------------------")
for k,p in cp:
    if y[k,p].x > 1e-6:
        #print(y[k,p].varName, y[k,p].x)
        print(f"集群 {k} 中被提供产品 {p} 要约的客户数量为：{y[k,p].x}")
        total_expected_profit += expected_profit[k,p]*y[k,p].x
        total_expected_cost += expected_cost[k,p]*y[k,p].x

increased_budget = '${:,.2f}'.format(z.x)
print(f"\n营销预算的增加预算为： {increased_budget}.")

# 财务报告

optimal_ROI = round(100*total_expected_profit/total_expected_cost,2)
min_ROI = round(100*(1+R),2)

money_expected_profit = '${:,.2f}'.format(total_expected_profit)
money_expected_cost = '${:,.2f}'.format(total_expected_cost)
money_budget = '${:,.2f}'.format(budget)

print(f"\n📃 财务报告")
print("--------------------------------------------")
print(f"最优总预期利润为： {money_expected_profit}.")
print(f"最优总预期成本为： {money_expected_cost} ，其中目标预算为： {money_budget}，额外开支为：{increased_budget}.")
print(f"最优 ROI 为： {optimal_ROI}% ，最小为 {min_ROI}%.")
```

    📃 最优分配方案
    --------------------------------------------
    集群 k1 中被提供产品 p1 要约的客户数量为：2.0
    集群 k1 中被提供产品 p2 要约的客户数量为：2.0

    营销预算的增加预算为： $400.00.

    📃 财务报告
    --------------------------------------------
    最优总预期利润为： $6,000.00.
    最优总预期成本为： $600.00 ，其中目标预算为： $200.00，额外开支为：$400.00.
    最优 ROI 为： 1000.0% ，最小为 120.0%.

## 分析

将产品分配到集群的成本需要增加预算 $\$400$ 。总预期利润是 $\$6,000$ 。总成本为 $\$600$ ，初步预算为 $\$200$ ，额外增加的预算为 $\$400$。预期投资回报率为 1000%，远高于最低 ROI 要求。

## 操作模型制定

### 客户预期利润

```python
### 集合

customers = ['c1', 'c2','c3','c4','c5','c6','c7','c8','c9','c10']

### 参数

# 为集群中的不同客户提供不同产品的预期利润表
ccp, customer_profit = gp.multidict({
    ('k1', 'c1', 'p1'): 2050,
    ('k1', 'c1', 'p2'): 1050,
    ('k1', 'c2', 'p1'): 1950,
    ('k1', 'c2', 'p2'): 950,
    ('k1', 'c3', 'p1'): 2000,
    ('k1', 'c3', 'p2'): 1000,
    ('k1', 'c4', 'p1'): 2100,
    ('k1', 'c4', 'p2'): 1100,
    ('k1', 'c5', 'p1'): 1900,
    ('k1', 'c5', 'p2'): 900,
    ('k2', 'c6', 'p1'): 3000,
    ('k2', 'c6', 'p2'): 2000,
    ('k2', 'c7', 'p1'): 2900,
    ('k2', 'c7', 'p2'): 1900,
    ('k2', 'c8', 'p1'): 3050,
    ('k2', 'c8','p2'): 2050,
    ('k2', 'c9', 'p1'): 3100,
    ('k2', 'c9', 'p2'): 3100,
    ('k2', 'c10', 'p1'): 2950,
    ('k2', 'c10', 'p2'): 2950
})
```

### 客户要约成本

```python
# 为集群中的不同客户提供不同产品的预期成本表

ccp, customer_cost = gp.multidict({
    ('k1', 'c1', 'p1'): 205,
    ('k1', 'c1', 'p2'): 105,
    ('k1', 'c2', 'p1'): 195,
    ('k1', 'c2', 'p2'): 95,
    ('k1', 'c3', 'p1'): 200,
    ('k1', 'c3', 'p2'): 100,
    ('k1', 'c4', 'p1'): 210,
    ('k1', 'c4', 'p2'): 110,
    ('k1', 'c5', 'p1'): 190,
    ('k1', 'c5', 'p2'): 90,
    ('k2', 'c6', 'p1'): 300,
    ('k2', 'c6', 'p2'): 200,
    ('k2', 'c7', 'p1'): 290,
    ('k2', 'c7', 'p2'): 190,
    ('k2', 'c8', 'p1'): 305,
    ('k2', 'c8','p2'): 205,
    ('k2', 'c9', 'p1'): 310,
    ('k2', 'c9', 'p2'): 310,
    ('k2', 'c10', 'p1'): 295,
    ('k2', 'c10', 'p2'): 295
})
```

##  操作模型实现

### 决策变量
$x_{k,i,j} \in \{0,1 \}$: 如果产品 $j \in J^{k}$  给客户 $i \in I^{k}$ 提供了要约，此变量等于1, 否则为 0。

```python
# 定义并初始化模型
mo = gp.Model('Operational')

### 定义变量

x = mo.addVars(ccp, vtype=GRB.BINARY, name="assign")
```

### 约束

- **产品要约**： 将产品的要约分配给每个集群的客户。

\begin{equation}
\sum_{i \in  I^{k}}  x_{k,i,j} = Y_{k,j}  \quad \forall j \in J^{k}, k \in K
\tag{1}
\end{equation}

其中：

$x_{k,i,j} \in \{0,1 \}$:  如果产品 $j \in J^{k}$  给客户 $i \in I^{k}$ 提供了要约，此变量等于1, 否则为 0。

$Y_{k,j} = \lfloor y_{k,j} \rfloor $: 在集群 $k$ 中产品 $j \in J^{k}$ 提供要约的客户数量。

```python
# 约束：产品要约

productOffers = {}

for k in clusters:
    for j in products:
            productOffers[k,j] = mo.addConstr(gp.quicksum(x[k,i,j] for kk,i,jj in ccp if (kk ==k and jj == j)) ==
                                              int(y[k,j].x), name='prodOffers_' + str(k) + ',' + str(j) )
```

### 约束

- **要约限制**： 最多可以向集群的一个客户提供一个产品。

\begin{equation}
\sum_{j \in J^{k}} x_{k,i,j} \leq 1 \quad \forall i \in I^{k}, k \in K
\tag{2}
\end{equation}

其中：

$x_{k,i,j} \in \{0,1 \}$: 如果产品 $j \in J^{k}$  给客户 $i \in I^{k}$ 提供了要约，此变量等于1, 否则为 0。

```python
# 对集群中每个客户的要约数量进行限制。

ki = [('k1', 'c1'),
      ('k1', 'c2'),
      ('k1', 'c3'),
      ('k1', 'c4'),
      ('k1', 'c5'),
      ('k2', 'c6'),
      ('k2', 'c7'),
      ('k2', 'c8'),
      ('k2', 'c9'),
      ('k2', 'c10')]

customerOffers = {}

for k,i in ki:
    customerOffers[k,i] = mo.addConstr(gp.quicksum(x[k,i,j] for kk,ii,j in ccp if (kk == k and ii == i) ) <= 1,
                                          name ='custOffers_' + str(k) + ',' + str(i) )
```

### 目标函数

- **总利润**： 最大化总预期利润。

\begin{equation}
\text{Max} \quad Z = \sum_{k \in K}  \sum_{i \in I^{k}} \sum_{j \in J^{k}} r_{k,i,j} \cdot x_{k,i,j}
\tag{0}
\end{equation}

其中：

$x_{k,i,j} \in \{0,1 \}$: 如果产品 $j \in J^{k}$  给客户 $i \in I^{k}$ 提供了要约，此变量等于1, 否则为 0。

$r_{k,i,j}$: 从客户 $i \in I^{k}$ 的产品要约 $j \in J^{k}$ 的预期客户利润。

```python
### 目标函数

# 最大化总利润

mo.setObjective(x.prod(customer_profit), GRB.MAXIMIZE)
```

```python
# 验证模型

mo.write('operational.lp')

# 运行优化

mo.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (mac64)
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads
    Optimize a model with 14 rows, 20 columns and 40 nonzeros
    Model fingerprint: 0xe1e9d99f
    Variable types: 0 continuous, 20 integer (20 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      Objective range  [9e+02, 3e+03]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 2e+00]
    Found heuristic solution: objective 6050.0000000
    Presolve removed 7 rows and 10 columns
    Presolve time: 0.00s
    Presolved: 7 rows, 10 columns, 20 nonzeros
    Variable types: 0 continuous, 10 integer (10 binary)

    Root relaxation: objective 6.100000e+03, 2 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

    *    0     0               0    6100.0000000 6100.00000  0.00%     -    0s

    Explored 0 nodes (2 simplex iterations) in 0.01 seconds
    Thread count was 12 (of 12 available processors)

    Solution count 2: 6100 6050

    Optimal solution found (tolerance 1.00e-04)
    Best objective 6.100000000000e+03, best bound 6.100000000000e+03, gap 0.0000%

```python
### 输出报告

# 向客户提供产品要约的最优分配方案

total_customer_profit = 0
total_customer_cost = 0

kvalue = None
first = True
num_assignments = 0

print("\n📃 最优分配方案")
print("--------------------------------------------")
for k,i,j in ccp:
    if k != kvalue:
        prevk = kvalue
        kvalue = k
        if not first:
            print("--------------------------------------------")
            print(f"集群 {prevk} 中分配的要约数量为 {num_assignments}")
            print("--------------------------------------------")
            num_assignments = 0
        if first:
            first = False
    if x[k,i,j].x > 0.5:
        #print(x[k,i,j].varName, x[k,i,j].x)
        profit = '${:,.2f}'.format(customer_profit[k,i,j])
        cost = '${:,.2f}'.format(customer_cost[k,i,j])
        print(f"集群 {k} 的客户 {i} 获得产品 {j}的要约：利润为 {profit}，成本为 {cost}")
        total_customer_profit += customer_profit[k,i,j]*x[k,i,j].x
        total_customer_cost += customer_cost[k,i,j]*x[k,i,j].x
        num_assignments += 1
print("--------------------------------------------")
print(f"集群 {kvalue} 中分配的要约数量为 {num_assignments}")
print("--------------------------------------------\n")

# 财务报告

customers_ROI = round(100*total_customer_profit/total_customer_cost,2)

money_customers_profit = '${:,.2f}'.format(total_customer_profit)
money_customers_cost = '${:,.2f}'.format(total_customer_cost)

print(f"\n📃 财务报告")
print("--------------------------------------------")
print(f"最优总客户预期利润为：{money_customers_profit}.")
print(f"最优总成本为： {money_customers_cost} ，其中预期预算为 {money_budget}，额外预算为： {increased_budget}.")
print(f"最优 ROI 为 {customers_ROI}%，最小值为： {min_ROI}%.")

```

    📃 最优分配方案
    --------------------------------------------
    集群 k1 的客户 c1 获得产品 p2的要约：利润为 $1,050.00，成本为 $105.00
    集群 k1 的客户 c2 获得产品 p2的要约：利润为 $950.00，成本为 $95.00
    集群 k1 的客户 c3 获得产品 p1的要约：利润为 $2,000.00，成本为 $200.00
    集群 k1 的客户 c4 获得产品 p1的要约：利润为 $2,100.00，成本为 $210.00
    --------------------------------------------
    集群 k1 中分配的要约数量为 4
    --------------------------------------------
    --------------------------------------------
    集群 k2 中分配的要约数量为 0
    --------------------------------------------

    📃 财务报告
    --------------------------------------------
    最优总客户预期利润为：$6,100.00.
    最优总成本为： $610.00 ，其中预期预算为 $200.00，额外预算为： $400.00.
    最优 ROI 为 1000.0%，最小值为： 120.0%.

## 分析
每个顾客最多只能得到一个产品要约。 产品 p2 的要约提供给了客户 c1 和 c2， 产品 p1 的要约提供给了客户 c3 和 c4。产品 p1 和 p2 都提供了两份要约——这是来自策略模型的约束。然而需要注意的是，为了确保这些严格的业务限制，预算需要增加 $\$400$。

提供产品要约的总成本是 $\$610$，，这稍微违反了可用的总预算 of $\$600$. 总客户预期利润为 $\$6,100$。ROI 为 1,000 %，远高于最低 ROI 要求。

如果需要约束总可用预算，可以向该营销模型添加以下约束：

- **预算**： 约束总可用预算。

\begin{equation}
\sum_{k \in K}  \sum_{i \in I^{k}} \sum_{j \in J^{k}} c_{k,i,j} \cdot x_{k,i,j} \leq B'
\tag{4}
\end{equation}

新的预算是原预算加上修正后的预算，即 $B' = B + z$

## 场景 1

约束总可用预算，在这个场景下，操作模型为:

### 目标函数

- **总利润**： 最大化总预期利润。

\begin{equation}
\text{Max} \quad Z = \sum_{k \in K}  \sum_{i \in I^{k}} \sum_{j \in J^{k}} r_{k,i,j} \cdot x_{k,i,j}
\tag{0}
\end{equation}

### 约束

- **产品要约**： 将产品的要约分配给每个集群的客户。

\begin{equation}
\sum_{i \in  I^{k}}  x_{k,i,j} = Y_{k,j}  \quad \forall j \in J^{k}, k \in K
\tag{1}
\end{equation}

- **要约限制**： 最多可以向集群的一个客户提供一个产品。

\begin{equation}
\sum_{j \in J^{k}} x_{k,i,j} \leq 1 \quad \forall i \in I^{k}, k \in K
\tag{2}
\end{equation}

- **预算**： 约束总可用预算。

\begin{equation}
\sum_{k \in K}  \sum_{i \in I^{k}} \sum_{j \in J^{k}} c_{k,i,j} \cdot x_{k,i,j} \leq B'
\tag{3}
\end{equation}

```python
### 约束总预算的营销模型

# 声明模型
mob = gp.Model('OperationalB')

### 决策变量

xb = mob.addVars(ccp, vtype=GRB.BINARY, name="assign")
```

```python
# 约束：产品要约

productOffersb = {}

for k in clusters:
    for j in products:
            productOffersb[k,j] = mob.addConstr(gp.quicksum(xb[k,i,j] for kk,i,jj in ccp if (kk ==k and jj == j)) ==
                                              int(y[k,j].x), name='prodOffersb_' + str(k) + ',' + str(j) )
```

```python
# 约束：对集群中每个客户的要约数量进行限制。

customerOffersb = {}

for k,i in ki:
    customerOffersb[k,i] = mob.addConstr(gp.quicksum(xb[k,i,j] for kk,ii,j in ccp if (kk == k and ii == i) ) <= 1,
                                          name ='custOffersb_' + str(k) + ',' + str(i) )
```

```python
# 约束：预算

# 新预算
new_budget = budget + z.x

totBudget = mob.addConstr(xb.prod(customer_cost) <= new_budget, name='total_budget')
```

```python
### 目标函数

# 最大化总利润

mob.setObjective(xb.prod(customer_profit), GRB.MAXIMIZE)
```

```python
# 验证模型

mob.write('operationalB.lp')

# 运行优化

mob.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (mac64)
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads
    Optimize a model with 15 rows, 20 columns and 60 nonzeros
    Model fingerprint: 0x96da858f
    Variable types: 0 continuous, 20 integer (20 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 3e+02]
      Objective range  [9e+02, 3e+03]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 6e+02]
    Found heuristic solution: objective 5950.0000000
    Presolve removed 7 rows and 10 columns
    Presolve time: 0.00s
    Presolved: 8 rows, 10 columns, 28 nonzeros
    Variable types: 0 continuous, 10 integer (10 binary)

    Root relaxation: objective 6.000000e+03, 2 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

    *    0     0               0    6000.0000000 6000.00000  0.00%     -    0s

    Explored 0 nodes (2 simplex iterations) in 0.01 seconds
    Thread count was 12 (of 12 available processors)

    Solution count 2: 6000 5950

    Optimal solution found (tolerance 1.00e-04)
    Best objective 6.000000000000e+03, best bound 6.000000000000e+03, gap 0.0000%

```python
### 输出报告

# 向客户提供产品要约的最优分配方案

total_customer_profitb = 0
total_customer_costb = 0

kvalueb = None
firstb = True
num_assignmentsb = 0

print("\n📃 最优分配方案")
print("--------------------------------------------")
for k,i,j in ccp:
    if k != kvalueb:
        prevkb = kvalueb
        kvalueb = k
        if not firstb:
            print("--------------------------------------------")
            print(f"集群 {prevkb} 中分配的要约数量为 {num_assignmentsb}")
            print("--------------------------------------------")
            num_assignmentsb = 0
        if firstb:
            firstb = False
    if xb[k,i,j].x > 0.5:
        #print(x[k,i,j].varName, x[k,i,j].x)
        profitb = '${:,.2f}'.format(customer_profit[k,i,j])
        costb = '${:,.2f}'.format(customer_cost[k,i,j])
        print(f"集群 {k} 的客户 {i} 获得产品 {j}的要约：利润为 {profitb}，成本为 {costb}")
        total_customer_profitb += customer_profit[k,i,j]*xb[k,i,j].x
        total_customer_costb += customer_cost[k,i,j]*xb[k,i,j].x
        num_assignmentsb += 1
print("--------------------------------------------")
print(f"集群 {kvalueb} 中分配的要约数量为 {num_assignmentsb}")
print("--------------------------------------------\n")

# Financial reports

customers_ROIb = round(100*total_customer_profitb/total_customer_costb,2)

money_customers_profitb = '${:,.2f}'.format(total_customer_profitb)
money_customers_costb = '${:,.2f}'.format(total_customer_costb)

print(f"\n📃 财务报告")
print("--------------------------------------------")
print(f"最优客户利润为： {money_customers_profitb}.")
print(f"最优客户总成本为 {money_customers_costb} ，其中原预算为： {money_budget} ，超额预算为： {increased_budget}.")
print(f"最佳 ROI 为 {customers_ROIb}% ， 最小 ROI 为  {min_ROI}%.")
```

    📃 最优分配方案
    --------------------------------------------
    集群 k1 的客户 c1 获得产品 p2的要约：利润为 $1,050.00，成本为 $105.00
    集群 k1 的客户 c2 获得产品 p1的要约：利润为 $1,950.00，成本为 $195.00
    集群 k1 的客户 c4 获得产品 p1的要约：利润为 $2,100.00，成本为 $210.00
    集群 k1 的客户 c5 获得产品 p2的要约：利润为 $900.00，成本为 $90.00
    --------------------------------------------
    集群 k1 中分配的要约数量为 4
    --------------------------------------------
    --------------------------------------------
    集群 k2 中分配的要约数量为 0
    --------------------------------------------

    📃 财务报告
    --------------------------------------------
    最优客户利润为： $6,000.00.
    最优客户总成本为 $600.00 ，其中原预算为： $200.00 ，超额预算为： $400.00.
    最佳 ROI 为 1000.0% ， 最小 ROI 为  120.0%.

## 分析

每个客户最多只能被提供一个产品要约。产品p1和p2提供给至少两个客户。将产品分配给客户的成本是 $\$600$，这等于最初sh可用的总预算。ROI为1,000％，远远高于所需的最低ROI。

在这个场景中，我们限定总可用预算为定值，从而得到一个不同的执行方案。产品 p1 提供给客户 c2 和 c4，产品 p2 提供给 c1 和 c5。

##  结论

在这个 Jupyter Notebook 中，我们讨论了营销活动对银行业的重要性。我们讨论了机器学习预测响应模型可用于提供营销活动优化问题的输入数据。我们展示了如何将营销活动优化问题分解为策略问题和操作问题。

这个策略问题被表述为一个线性规划问题，我们将机器学习预测响应模型产生的数据进行聚合。策略问题的解决方案确定了向集群中的客户提供哪些产品，同时最大化了营销活动的预期利润，并考虑了以下约束：

* 活动预算的限制。
* 对活动中可以提供的产品的最小数量的限制。
* 必须满足的投资回报率门槛。

该操作问题被表述为MIP模型，其中预期的客户利润和策略模型的输出可以作为输入，将产品提供分配给单个客户，从而使客户总利润最大化。我们考虑了两个操作问题的案例。在第一种情况下，不强制限定由策略问题决定的总可用预算。这意味着这个问题的最佳解决方案可能会稍微超出总可用预算。在第二种情况下，我们限定总可用预算。

##  参考

[1] M. D. Cohen. *Exploiting response models—optimizing cross-sell and up-sell opportunities in banking.* Information Systems. Vol. 29. issue 4, June 2004, Pages 327-341

Copyright © 2020 Gurobi Optimization, LLC

翻译 By Arvin Xu


# A (simplified) search with mainstream software

In this tutorial, we will run a full search for pulsations in real data and obtain the orbital solution of an unknown binary pulsar.

## Exercise 1: low-DM pulsar

Enter the directory `data/lowdm`

```
$ cd data/lowdm
```

You will find a file named `guppi_55757_B1508+55_0004_0001.fil`. The source in these data is a very well-known pulsar, B1508+55, with a sharp profile and low dispersion measure. Data were collected with the [Green Bank Telescope](https://science.nrao.edu/facilities/gbt/), at 820 Hz, with a total band of 200 MHz split in 1024 channels.

For a rough periodicity search, we do not need all this information. Therefore, as a first step we sum up all these data in a single channel.

```
$ prepdata guppi_55757_B1508+55_0004_0001.fil -o 1508
```
This will create two files: `1508.dat`, containing the light curve, and `1508.inf`, a text file with some information about the observation. Let us see what the Fourier transform (see first lesson, this is actually the absolute value of the Fourier transform, or the square root of the PDS) looks like.

```
$ realfft 1508.dat
$ explorefft 1508.fft

      Interactive FFT Explorer
         by Scott M. Ransom
            October, 2001

 Button or Key       Effect
 -------------       ------
 Left Mouse or I     Zoom in  by a factor of 2
 Right Mouse or O    Zoom out by a factor of 2
 Middle Mouse or D   Show details about a selected frequency
 <                   Shift left  by a full screen width
 >                   Shift right by a full screen width
 ,                   Shift left  by 1/8 of the screen width
 .                   Shift right by 1/8 of the screen width
 + or =              Increase the power scale (make them taller)
 - or _              Decrease the power scale (make them shorter)
 S                   Scale the powers automatically
 N                   Re-normalize the nowers by one of several methods
 P                   Print the current plot to a file
 G                   Go to a specified frequency
 L                   Load a zaplist showing potential RFI locations
 Z                   Add a frequency chunk to the RFI zaplist
 H                   Show the harmonics of the center frequency
 ?                   Show this help screen
 Q                   Quit

Examining B1508+55 data from '1508.fft'.

Memory mapping the input FFT.  This may take a while...

```
The second command will open a PGPLOT window where you will be able to zoom and look at all the periodicities contained in the data. The mouse and keyboard keys to touch to perform the various actions (zoom, scroll, etc) are listed in the terminal.

You will notice that the fundamental frequency of the pulsation (~5 Hz) is not the most prominent peak in the FT. This is due to the shape of the pulse.
Now, we can use a program that automatically looks at the FT and performs quality checks of the signals contained in it, ranking them (ignore the `-zmax` switch for now, we do that to limit the search to the routine methods and not the advanced ones).

```
$ accelsearch 1508.fft -zmax 0 -numharm 16
```

```
prepfold -accelcand 1 -accelfile 1508_ACCEL_0.cand guppi_55757_B1508+55_0004_0001.fil
```

## Exercise 2: high-DM pulsar

*TBD*

## Exercise 3: binary pulsar.

In the directory `data/split` you will find 20 observations, 3 ks each, of a simulated binary pulsar.

### 3.1 Finding the local period and pdot in each observation

We start from an estimate of the period: 0.05124 s. We run an accelerated epoch folding search in each file around this period. The switch `-p` suggests a starting period for the search; `-mjds -events -double` are there only because of the format of the data; `-npfact 10` extends the range of periods and pdots we will be using for the search by a factor 10.
```
$ prepfold psr_0.events -p 0.05124 -mjds -events -double -npfact 10
$ prepfold psr_1.events -p 0.05124 -mjds -events -double -npfact 10
(...)
```
or, to make it quick,
```
$ for i in psr_*.events; do prepfold $i -p 0.05124 -mjds -events -double -npfact 10; done
```

### 3.2. Approximate values for the orbital modulation
Plot the best solutions seen so far (`-pb 1 -T0 0 -x 1` is just telling `fitorb.py` to use those starting values for the orbital period, the ascending node passage and the projected semi-major axis respectively.):
```
$ fitorb.py psr*.bestprof -p 0.05124 -pb 1 -T0 0 -x 1
```
We see a modulation with a period of ~2.3 days. Let's play using different starting values for the parameters until the fit of the parameters is good. `fitorb.py` outputs a series of parameters like
```
PEPOCH      49354.042438
P0  0.0512450030268001
BINARY BT
A1   5.00038618340817
E                   0
T0  -0.469972316212605
PB   2.30000329860082
OM                  0
```
Let us copy and paste this output to a new file called `fit.par`.

### 3.3a. "Freezing" the local solutions
**Attention**: This step is overly complicated (having to use different approximate parameter files for each chunk, instead of the approximate orbital solution found above for all) because of a bug in PRESTO. I filed an Issue to try to correct it.

To calculate TOAs, PRESTO wants bestprofs obtained without searching. So, we have to rerun `prepfold` giving it already the best parameters found in the previous search, and telling it not to perform additional searches. This can be done in many ways.

1. Easy but possibly inconsistent(?)
One is to read in each bestprof the lines
```
# P_bary (ms)      =  51.240437722057   +/- 1.25e-06
# P'_bary (s/s)    =  2.33518439161511e-10 +/- 3.24e-12
# P''_bary (s/s^2) =  0                 +/- 7e-15
```
and calling `prepfold` with the options
```
$ prepfold psr_0.events -p 0.051240437722057 -pdot 2.33518439161511e-10 -pddot 0 -nosearch -mjds -events -double -npfact 10
```
The drawback of this option is that _some .bestprof files will be overwritten, others not_ (because of how the period is used in the file name), and it will be more difficult

2. Slightly trickier, but surely consistent
In step 1 we obtained local best values for $p$, $\dot{p}$ and $\ddot{p}$ in each observation. For each observation file, we have a corresponding file that ends in `.bestprof` and contains these best values. For each of these local solutions, we create a file with the same name but extension `.par`, copying the values of each parameter and converting to the relevant units, as follows:
\begin{align}
\nu &= \frac{1}{p} \\
\dot{\nu} &= -\frac{1}{p^2}\dot{p} \\
\ddot{\nu} &= \frac{2}{p^3}\dot{p} - \frac{1}{p^2}\ddot{p} \\
\end{align}
So, the parameter file will look like
```
PSRJ         FAKE                # Just any name, but it must be here!
PEPOCH       49353.232270000000  # In the bestprof: Epoch_bary (MJD) =  49353.232270000000
F0           0.051251497947019   # Convert to Hz the line
                                 # P_bary (ms)      =  51.251497947019  +/- 1.39e-06
F1           4.46613404291e-08   # Convert to Hz/s the period line plus
                                 # P'_bary (s/s)    =  -1.17312659353888e-10 +/- 3.44e-12
F2           2.0445609693e-16    # Convert to Hz/s^2 the period and pdot lines, plus
                                 # P''_bary (s/s^2) =  0                 +/- 7.44e-15
```
Refold, this time with the `-timing` option
```
$ prepfold psr_0.events -timing psr_0_51.24ms_Cand.pfd.par -mjds -events -double
$ prepfold psr_1.events -timing psr_1_51.24ms_Cand.pfd.par -mjds -events -double
(...)
```
or, more rapidly
```
$ for i in psr*.events; do bestprof=\${i/.events/}*.bestprof; echo $bestprof ; \
    prepfold $i -timing ${bestprof/.bestprof/.par} -mjds -events -double; done
```

### 3.3b. Calculate TOAs
Now, to calculate TOAs we use the `get_TOA.py` script in PRESTO, with the following syntax
```
get_TOAs.py -n 1 -e -g 0.2 psr_1_PSR_FAKE.pfd.par
```
`-n 1` says that we are calculating one TOA per file; `-e` is again related to the original file format; `-g 0.2` means to use a Gaussian with width 0.2.
The TOAs will be printed in the screen:
```
$ get_TOAs.py -n 1 -e -g 0.2 psr_0_PSR_51.24.pfd
/psr/Soft/presto/lib/python/psr_utils.py:669: RuntimeWarning: invalid value encountered in divide
  DM/(0.000241*freq_emitted*freq_emitted), 0.0)
@                  0.000 49353.0180863062417  1785.39
```
You can ignore the first warning message. Beware, instead, of error or warning messages about FFTFIT failing to do something. They often mean that you have bad statistics in your data or the template you chose is not adequate.
Copy all lines starting by "@" to a file called `all.tim`

### 3.4. Refine the solution with Tempo2

*TBD*