# This notebook
The purpose of this notebook is to generate all the plots (and some supplementary plots) for Becker, Gallo et al. (submitted). All the needed supplementary code and data files are included in this GitHub repository.

```python
%pylab inline
from astropy.io import fits
import pandas as pd
import glob
import os
import trappist_machine
import scipy.interpolate
```

    Populating the interactive namespace from numpy and matplotlib

```python
# set up file paths - for other users, these will need to be updated!
computer_path = '/Users/jcbecker/Documents/GitHub/'
current_dir = computer_path + 'trappist1/'
assorteddata = current_dir + 'AssortedData/'
extraFigures = current_dir + 'ExtraFigures/'
paperFigures = current_dir + 'PaperFigures/'

# set up other variables to save time
regen_Q_values = False # if true, re-create the file used for the k/Q constraints. Takes a while to run!
```

# Import data

The orbital parameters used in this analysis come from [Grimm et al. 2018](https://www.aanda.org/articles/aa/full_html/2018/05/aa32233-17/T3.html). We won't be running Monte Carlo sims over the errors in this paper, so we will import the best fit values of each parameter.

```python
## trappist system data
trappist1 = pd.read_csv(assorteddata + "trappist_system_properties.csv") # from Grimm+ 2018
trappist1

```

      Planet
      Mass(Mearth)
      Radius(Rearth)
      Period(days)
      a(AU)
      e
      i(deg)

      0
      TRAPPIST-1 h
      0.331
      0.773
      18.767953
      0.061935
      0.00567
      89.80

      1
      TRAPPIST-1 g
      1.148
      1.148
      12.354473
      0.046877
      0.00208
      89.71

      2
      TRAPPIST-1 f
      0.934
      1.046
      9.205585
      0.038534
      0.01007
      89.68

      3
      TRAPPIST-1 e
      0.772
      0.910
      6.099043
      0.029283
      0.00510
      89.86

      4
      TRAPPIST-1 d
      0.297
      0.784
      4.049959
      0.022280
      0.00837
      89.75

      5
      TRAPPIST-1 c
      1.156
      1.095
      2.421807
      0.015815
      0.00654
      89.67

      6
      TRAPPIST-1 b
      1.017
      1.121
      1.510876
      0.011548
      0.00622
      89.65

```python
# define global truths about TRAPPIST-1

dist = 12.1 # parsecs
Rearth_to_Mjup = 0.08911498 # 1 earth radii is 0.08 Rjup
trappist_lum = 0.000522 #solar luminosity, used value is bolometric. Visual luminosity is 0.00000373 L_sun.
solar_lum = 3.848 * 10**33.0 # erg/sec
mstar = 0.089 # solar masses

```

# Plotting the lightcurve (UVOT)
The 300 ks of SWIFT data is split into epochs at which the data was taken, and a flux estimate or upper limit is derived using heasoft software XSelect at each of those epochs.

```python
manual_lc = pd.read_csv(assorteddata + "manual_LC.csv")
manual_lc['newtime_start'] = 51910 + (manual_lc['time_start'] ) / (60.* 60. *24.) # convert time to JD, since time units in SWIFT headers is funny.
manual_lc['newtime_end'] = 51910 + (manual_lc['time_end'] ) / (60.* 60. *24.)

```

```python
# print table for the paper. The latex printout can be copy&pasted into the .tex draft.
# If more data is added, rerun this and replace table in paper.
print_cols = ['obs_ID', 'newtime_start', 'newtime_end','count']
manual_lc.sort_values('obs_ID')[print_cols].to_latex(index = False)
```

    '\\begin{tabular}{rrrr}\n\\toprule\n     obs\\_ID &  newtime\\_start &   newtime\\_end &   count \\\\\n\\midrule\n 10283001.0 &   58010.348779 &  58010.772233 &  0.0200 \\\\\n 10283002.0 &   58095.975270 &  58096.984039 &  0.0220 \\\\\n 10283003.0 &   58097.648316 &  58097.718778 &  0.0200 \\\\\n 10283004.0 &   58103.413035 &  58103.811122 &  0.0180 \\\\\n 10283005.0 &   58105.003031 &  58105.010428 &  0.0230 \\\\\n 10283006.0 &   58113.704544 &  58113.986122 &  0.0230 \\\\\n 10283007.0 &   58114.700832 &  58114.852095 &  0.0150 \\\\\n 10283008.0 &   58117.955622 &  58118.306262 &  0.0120 \\\\\n 10283009.0 &   58124.928569 &  58125.000012 &  0.0240 \\\\\n 10283010.0 &   58126.323775 &  58126.405568 &  0.0200 \\\\\n 10283011.0 &   58131.504108 &  58131.843762 &  0.0210 \\\\\n 10283012.0 &   58133.164500 &  58133.298580 &  0.0260 \\\\\n 10283013.0 &   58136.635220 &  58136.957650 &  0.0410 \\\\\n 10283014.0 &   58236.915500 &  58236.994455 &  0.0240 \\\\\n 10283015.0 &   58237.506675 &  58237.914594 &  0.0210 \\\\\n 10283016.0 &   58243.298947 &  58243.560428 &  0.0540 \\\\\n 10283017.0 &   58244.294514 &  58244.306260 &  0.0370 \\\\\n 10283018.0 &   58250.925080 &  58251.000012 &  0.0300 \\\\\n 10283019.0 &   58251.520821 &  58251.807649 &  0.0280 \\\\\n 10283020.0 &   58257.901041 &  58257.988900 &  0.0270 \\\\\n 10283021.0 &   58258.705638 &  58258.719456 &  0.0270 \\\\\n 10283022.0 &   58264.806395 &  58265.626402 &  0.0240 \\\\\n 10283023.0 &   58271.511782 &  58271.870845 &  0.0150 \\\\\n 10283024.0 &   58278.021731 &  58278.770845 &  0.0130 \\\\\n 10283025.0 &   58279.746508 &  58279.765983 &  0.0430 \\\\\n 10283026.0 &   58285.594931 &  58286.000012 &  0.0180 \\\\\n 10283027.0 &   58286.137720 &  58286.809734 &  0.0170 \\\\\n 10283028.0 &   58287.314815 &  58287.314815 &  0.0330 \\\\\n 10283029.0 &   58293.316351 &  58294.052806 &  0.0260 \\\\\n 10283030.0 &   58296.417096 &  58296.969456 &  0.0190 \\\\\n 10283031.0 &   58297.758323 &  58297.891678 &  0.0300 \\\\\n 10283032.0 &   58298.756438 &  58299.959056 &  0.0260 \\\\\n 10283034.0 &   58305.782756 &  58306.995150 &  0.0190 \\\\\n 10283035.0 &   58307.048495 &  58307.923622 &  0.0190 \\\\\n        NaN &   58264.806395 &  58264.806395 &  0.0072 \\\\\n\\bottomrule\n\\end{tabular}\n'

```python
# Plot the lightcurve for the paper

idx = (manual_lc['detection_type'] != 'SUM') & pd.isnull(manual_lc['counterr']) & pd.notnull(manual_lc['count'])
idx_witherrs = (manual_lc['detection_type'] != 'SUM') & pd.notnull(manual_lc['counterr']) & pd.notnull(manual_lc['count'])

time_centers = np.true_divide(manual_lc.loc[idx]['time_end'].values - manual_lc.loc[idx]['time_start'].values, 2)
idx2 = manual_lc['detection_type'] == 'SUM'

xarr_sec = manual_lc.loc[idx]['time_start'].values + time_centers
xarr = 51910 + (xarr_sec) / (60.* 60. *24.) # the 51910 figure is from: https://swift.gsfc.nasa.gov/analysis/suppl_uguide/time_guide.html
xarr_err = time_centers / (60.* 60. *24.)
yarr = manual_lc.loc[idx]['count'].values
figsize(12,5)
fig, ax1 = subplots()
ax1.errorbar(xarr, yarr, xerr = xarr_err,
         yerr = [np.mean(yarr)*0.3] * len(yarr),
         uplims = 1, marker='.', color='grey', markersize = 15,linestyle = 'None')
ax1.plot(np.linspace(58000, 58325, 10),
     [manual_lc.loc[idx2]['count'].values[0]] * 10, linewidth=2.6, color='Orange')
ax1.fill_between(np.linspace(58000, 58325, 10),
             [manual_lc.loc[idx2]['count'].values[0] - manual_lc.loc[idx2]['counterr'].values[0]] * 10,
             [manual_lc.loc[idx2]['count'].values[0] + manual_lc.loc[idx2]['counterr'].values[0]] * 10,
             color='Orange', alpha=0.3)

#
#
time_centers = np.true_divide(manual_lc.loc[idx_witherrs]['time_end'].values - manual_lc.loc[idx_witherrs]['time_start'].values, 2)
xarr_sec = manual_lc.loc[idx_witherrs]['time_start'].values + time_centers
xarr = 51910 + (xarr_sec) / (60.* 60. *24.)
xarr_err = time_centers / (60.* 60. *24.)
yarr = manual_lc.loc[idx_witherrs]['count'].values
yarr_err = manual_lc.loc[idx_witherrs]['counterr'].values
ax1.errorbar(xarr, yarr, xerr = xarr_err,
         yerr = yarr_err,
        marker='.', color='IndianRed', markersize = 15,linestyle = 'None')
#
#
#

ax1.set_xlim(58000, 58325)
ax1.set_ylabel("SWIFT UVW2 \n Corrected Rate (count/s)", fontsize=17)
ax1.set_xlabel("Time, MJD (JD - 2400000.5)", fontsize=17) #MJD not BJD - see SWIFT website above

# make the limits and second axis
ylimz = [0, 0.065] # these will be the limits in count/sec for both y-axes. Set it once to make sure they both match.
ax1.set_ylim(ylimz) # Left y-axis.
xticks(fontsize=15)
yticks(fontsize=15)
ax2 = ax1.twinx()
ax2.set_ylim(np.multiply(ylimz, 5.77* 2033 / 100)) # right y-axis
ax2.set_ylabel("Flux at Earth \n[10$^{-14}$ erg cm$^{-2}$ s$^{-1}$ ]", fontsize=17)
xticks(fontsize=15)
yticks(fontsize=15)
savefig(paperFigures + "lcdata_bestfit_mag.pdf", dpi=100, bbox_inches = 'tight')

```

# Filter responses / normalization factor
SWIFT's UVW2 band which we are using has a little different coverage compared to GALEX bands, which are typically used. Just to help guide our understanding, let's just check and look at how different the various NUV/FUV/UV filters are. The [filter profiles](http://svo2.cab.inta-csic.es/svo/theory/fps3/index.php?id=Swift/UVOT.UVW2&&mode=browse&gname=Swift&gname2=UVOT#filter) can be found online at the mission's document pages. I estimated the normalizations from published instrument papers. This plot won't go in the paper.

```python
uvw2_rsf = pd.read_csv(assorteddata + "Swift_UVOT.UVW2.dat", names = ['wavelength', 'eff_area_cm2'], delim_whitespace=True)
galex_nuv = pd.read_csv(assorteddata + "GALEX_GALEX.NUV.dat", names = ['wavelength', 'eff_area_cm2'], delim_whitespace=True)
galex_fuv = pd.read_csv(assorteddata + "GALEX_GALEX.FUV.dat", names = ['wavelength', 'eff_area_cm2'], delim_whitespace=True)
galex_vega = 4.531e-9 #(erg/cm2/s/A)
uvw2_vega = 5.237e-9 #(erg/cm2/s/A)
galex_fuv_vega =6.491e-9

# filter profiles:
# http://svo2.cab.inta-csic.es/svo/theory/fps3/index.php?id=Swift/UVOT.UVW2&&mode=browse&gname=Swift&gname2=UVOT#filter
# the coefficients 20 and 60 correct the file approximately to match with the effective area definitions I found in
# the instrumentation documentation. This is not exact, but it's pretty close, and this plot is just illustrative anyway..

figsize(6,5)
fill_between(galex_nuv['wavelength'], 60 * galex_nuv['eff_area_cm2'] / np.max(galex_nuv['eff_area_cm2'] ) , color='Purple', alpha=0.1)
fill_between(uvw2_rsf['wavelength'], uvw2_rsf['eff_area_cm2'] ,  color='Salmon', alpha = 0.1)
plot(uvw2_rsf['wavelength'], uvw2_rsf['eff_area_cm2'] , label="SWIFT UVW2", color='Salmon')
plot(galex_nuv['wavelength'], 60 * galex_nuv['eff_area_cm2'] / np.max(galex_nuv['eff_area_cm2'] ) , label="GALEX NUV", color='Purple')
galex_eff_ar = 60 * galex_nuv['eff_area_cm2'] / np.max(galex_nuv['eff_area_cm2'] )
fill_between(galex_fuv['wavelength'], 20 * galex_fuv['eff_area_cm2'] / np.max(galex_fuv['eff_area_cm2']),  color='Green', alpha = 0.1)
plot(galex_fuv['wavelength'], 20 * galex_fuv['eff_area_cm2']/ np.max(galex_fuv['eff_area_cm2']), label="GALEX FUV", color='Green')
swift_eff_ar = uvw2_rsf['eff_area_cm2']
xlabel("Wavelength (Angstroms)", fontsize=17)
ylabel("Effective Area (cm$^{2}$)", fontsize=17)
ylim(0,62)
xlim(1250, 3500)
legend()
xticks(fontsize=12)
yticks(fontsize=14)
savefig(extraFigures + "filter_response_comparison.pdf", dpi=100, bbox_inches = 'tight')

```

```python
countrate = manual_lc.loc[idx2]['count'].values[0]
counterr = manual_lc.loc[idx2]['counterr'].values[0]
convfactor = 5.77 # Brown 2016 multiply by count rate to give the flux density in units of 10−16 erg s−1 cm−2 Å−1 - this (5.77) was the value Edmund suggested
fit_val = countrate * convfactor * 10**-16*  2033. #correct for the width of the waveband - I got this value (2033) from Edmund
fiterr = counterr * 5.77 * 10**-16*  2033.
```

# X-ray data: SWIFT XRT

At first, I thought this data was terrible, but it's actually not. This data requires stacking to get a reliable flux estimate, but we do have one if we stack the entire 300 ks. Edmund did this reduction, and here are his files:

combined_evt.fits – the full events list for the combined XRT data (can be opened as an image, but each event is tagged with time, energy, and position).

trappist_full_xrt_lc.fits – the light curve for the source region (columns include: time bin, count rate, error, and fractional exposure). The light curve was binned to 5000 s and the fractional exposure is the fraction of 5000 s covered by any one bin.

bkg_full_xrt_lc.fits – the light curve for the background region

bkg.reg, xmm.reg – the region files in ds9 format.

Then, we also have: output.lc, created using lcmath and the two \*fits light curves described above.

This data was taken in event mode (each photon hit is recorded as an event), so we will bin the data into 5000 second chunks instead of using the epochs that we used for the UVOT exposures (which were integrated over the exposure times).

```python
lcdata = fits.open(assorteddata + "SWIFT_XRT/output.lc") # read in background subtracted LC
bkgdata = fits.open(assorteddata + "SWIFT_XRT/bkg_full_xrt_lc.fits") # read in background subtracted LC

# format time column:
lc_x = 51910 + 5.270701373846318E+08 / (60.* 60. *24.) + \
            np.true_divide(np.add(0, list(zip(*lcdata[1].data))[0]),60.* 60. *24.)
lc_y = list(zip(*lcdata[1].data))[1]
lc_er = list(zip(*lcdata[1].data))[2]
lc_ff = list(zip(*lcdata[1].data))[3]
xrt_lc = pd.DataFrame({'time':lc_x,'flux':lc_y,'error': lc_er, 'filling_factor': lc_ff})
solid_detection = (xrt_lc['flux'] - xrt_lc['error']) > 0
xrt_lc['detect'] = solid_detection
xrt_lc['back_level']= list(zip(*bkgdata[1].data))[1]
assert list(zip(*bkgdata[1].data))[0] == list(zip(*lcdata[1].data))[0] # source and background should have same time array; stop if not

detection_type = []
err = []
true_flux = []
for item in xrt_lc.index:
    if xrt_lc.iloc[item]['flux'] < 0: #if less than background, we do not have reliable detection, so replace value with upper limit set at background level.
                                    # We already subtracted background using lcmath, so less than 0 means 'less than background' here.
        true_flux.append(xrt_lc.iloc[item]['back_level'])
        detection_type.append('upper_limit')
        err.append(NaN)
    elif (xrt_lc.iloc[item]['flux'] - xrt_lc.iloc[item]['error']) <= 0: # then, this data point is indistinguishable from zero. Also upper limit.
        true_flux.append(xrt_lc.iloc[item]['flux'])
        detection_type.append('upper_limit')
        err.append(NaN)
    else:
        true_flux.append(xrt_lc.iloc[item]['flux'])
        detection_type.append('detection')
        err.append(xrt_lc.iloc[item]['error'])

xrt_lc['xrt_flux'] = true_flux
xrt_lc['detection_type'] = detection_type
xrt_lc['xrt_error'] = err

xrt_lc.to_csv("AssortedData/SWIFT_XRT/xray_lightcurve_processed.csv") # save this# default - don't include totally empty bins
idxnull  = (xrt_lc['filling_factor'] > 0.0)
##
idx_upplim = idxnull & (xrt_lc['detection_type'] =='upper_limit')
figsize(12,5)
fig, ax1 = subplots()
ax1.errorbar(xrt_lc.loc[idx_upplim]['time'], xrt_lc.loc[idx_upplim]['xrt_flux'], alpha = 0.2, uplims = [1] * len(xrt_lc.loc[idx_upplim]['xrt_flux']), yerr = [0.002] * len(xrt_lc.loc[idx_upplim]['xrt_flux']), color='grey',marker='o',linestyle='None')
idx_detect = idxnull &(xrt_lc['detection_type'] =='detection') # points with detections only
ax1.errorbar(xrt_lc.loc[idx_detect]['time'], xrt_lc.loc[idx_detect]['xrt_flux'], yerr = xrt_lc.loc[idx_detect]['xrt_error'], color='DarkRed',marker='o',linestyle='None')
ax1.plot(np.linspace(58000, 58325, 10),
     [0.0024] * 10, linewidth=2.6, color='HotPink', label = "Detections")
ax1.fill_between(np.linspace(58000, 58325, 10),
             [0.0024 - 0.00015] * 10,
             [0.0024 + 0.00015] * 10,
             color='Pink', alpha=0.3)

ax1.plot(np.linspace(58000, 58325, 10),
     [0.00026] * 10, linewidth=2.6, color='Orange')
ax1.fill_between(np.linspace(58000, 58325, 10),
             [0.00026 - 0.00003] * 10,
             [0.00026 + 0.00003] * 10,
             color='Orange', alpha=0.3)
ax1.set_ylabel("SWIFT XRT \n Corrected Rate (count/s)", fontsize=17)
ax1.set_xlabel("Date (MJD - 2400000.5)", fontsize=17)
xticks(fontsize=15)
yticks(fontsize=15)
ax1.set_xlim(58000, 58325)
ax1.set_ylim(0, 25e-3)

savefig(paperFigures + "lcdata_xray_subtracted.pdf", dpi=100, bbox_inches = 'tight')

```

```python
print( np.average(xrt_lc.loc[idx_detect]['xrt_flux'].values, weights = 1./xrt_lc.loc[idx_detect]['xrt_error'].values))
print( np.sum(1./np.power(1./xrt_lc.loc[idx_detect]['xrt_error'].values,2.)))
```

    0.002387190936025226
    0.00015375166280284414

The photon count estimate (0.00042 counts/sec $\pm$ 0.00005 counts/sec) comes from the stacked 300 ks exposure (and can be computed in XSPEC). To convert that to a flux, we need to make assumptions about the spectrum shape.

One way to do this is using this online tool: https://heasarc.gsfc.nasa.gov/cgi-bin/Tools/w3pimms/w3pimms.pl to convert the count rate into a flux. We use the 'Swift/XRT/PC count rate' option since this data was collected in Photon Collecting mode. The energy input range runs from 0.3 keV to 10 keV (put that in manually). We put in a nH column density, then choose a model for the spectrum.

We chose APEC (https://heasarc.gsfc.nasa.gov/xanadu/xspec/manual/node134.html) as the model.
[APEC](https://iopscience.iop.org/article/10.1086/322992/pdf) calculates emissivities for a hot, diffuse, optically thin plasma that is in collisionally-ionized equiliubrium. These are the conditions in the stellar corona creating the observed X-ray flux, so we choose the log(Temperature) we expect for the M8 to be log(T) = 6.5 (you can change this, it doesn't change the flux much). We choose the abundances to be 0.2 Solar (also you can change that without changing flux much).

The exact temperature we use for the corona temperature changes the count rate to flux conversion.

The referee suggested we use instead as done in Wheatley 2017 a two-temperature APEC model. For the Wheatley 2017 model, the Swift XRT count rate measured in the 0.3-10 keV band (or 0.3-2 keV, since the model produces almost no emission above 2 keV) translates to flux as follows:

1 count/s -> 2.67e-11 erg/s/cm$^2$ (for a bandpass of 0.3-10 keV)

Using the parameters described above as well as our computed count rate and error on the count rate, we compute an x-ray flux and lower and upper limits of: (see next cell)

```python
# Old values, which we are not using anymore:
flux_from_pimms = 1.063E-14 # yields erg/s/cm^2
flux_from_pimmsmin =   9.360E-15 # yields erg/s/cm^2
flux_from_pimmsmax = 1.189E-14  # yields erg/s/cm^2

# New values:
rawflux = 0.00042 # counts / sec
rawfluxerr = 0.00005 # counts / sec
flux_from_2param_APEC = (rawflux) * 2.67e-11 # yields erg/s/cm^2
flux_from_2param_APECerr = (rawfluxerr) * 2.67e-11 # yields erg/s/cm^2

print("total flux (ergs/s/cm^2):", flux_from_2param_APEC, " and error:", flux_from_2param_APECerr)
```

    total flux (ergs/s/cm^2): 1.1214e-14  and error: 1.335e-15

```python
# Flaring count rate to flux, using APEC two parameter model:
rawflux = 0.0024 # counts / sec
rawfluxerr = 0.00015 # counts / sec
flux_from_2param_APEC_flare = (rawflux) * 2.67e-11 # yields erg/s/cm^2
flux_from_2param_APEC_flare_err = (rawfluxerr) * 2.67e-11 # yields erg/s/cm^2
print("loud flux (ergs/s/cm^2):", flux_from_2param_APEC_flare, " and error:", flux_from_2param_APEC_flare_err)
```

    loud flux (ergs/s/cm^2): 6.407999999999999e-14  and error: 4.0049999999999994e-15

```python
# Also, we can find the baseline flux (but we won't use this - except to add to table):
raw_baseline, raw_baseline_err = 0.00026, 0.00003
print( "quiescent flux (ergs/s/cm^2):", 2.67e-11 * raw_baseline, " and error:", 2.67e-11 * raw_baseline_err)
```

    quiescent flux (ergs/s/cm^2): 6.941999999999999e-15  and error: 8.01e-16

# Convert between wavebands to compute the XUV flux.
Here we are using the empirical scaling relations from a collection of literature papers.

Originally, I had to convert UV->X-ray->EUV, but since we have a reliable estimate of the average X-ray flux now, I can use that vaue (which comes from the online calculator) directly. I will also do the extrapolation from UV to X-ray, just to check what it is and how good the empirical models in the literature are. The models in the literature use mostly much more massive stars than M8Vs, so it's likely that they won't be 'that' good.

The papers that I used in figuring all this out are as follows:
1. [Sanz-Forcada et al. 2011](https://arxiv.org/pdf/1105.0550.pdf) - Table 5 was helpful for seeing the types of stars for which measurements in different wavebands exist, and Equation 3 is the EUV <-> X-ray scaling relation
2. [Chadney et al. 2015](https://arxiv.org/pdf/1412.3380.pdf) - Equations 2a and 2b, also see Figure 2
3. [Stelzer et al. 2013](https://academic.oup.com/mnras/article/431/3/2063/1068594) - Equation 4 combined with Table 1, and Figure 14 which visually presents the empirical correlations.

```python
fit_valnew = fit_val
dist_to_surf = 5.627e-4# AU
dist_to_earth = 12.1 * 206265 # AU
uv_at_surface = fit_valnew / np.power(dist_to_surf/dist_to_earth,2)
uv_at_surfacemin = (fit_valnew - fiterr) / np.power(dist_to_surf/dist_to_earth,2)
uv_at_surfacemax = (fit_valnew + fiterr) / np.power(dist_to_surf/dist_to_earth,2)
uv_at_earth = uv_at_surface / np.power(dist_to_earth / dist_to_surf,2)

uvw2lum = trappist_machine.flux_to_lum(fit_valnew, 12.1) # parsecs
uvw2lumlow = trappist_machine.flux_to_lum(fit_valnew - fiterr, 12.1)
uvw2lumup = trappist_machine.flux_to_lum(fit_valnew + fiterr, 12.1)

print( " ----- ")
print( " USING REAL XRAY FLUX ")
print( " ----- ")

## Define the x-ray flux from the values found from APEC two-parameter model
res_xrayflux_direct = [flux_from_2param_APEC - flux_from_2param_APECerr, flux_from_2param_APEC, flux_from_2param_APEC + flux_from_2param_APECerr]

res_xraylum_fraction_direct = np.true_divide(trappist_machine.flux_to_lum([res_xrayflux_direct, res_xrayflux_direct, res_xrayflux_direct],12.1),   ( solar_lum* trappist_lum))
xray_direct_at_surface = res_xrayflux_direct[1] / np.power(dist_to_surf/dist_to_earth,2)
euv_over_Fx  = 425 * (xray_direct_at_surface)**-0.42 # this is euv/Fx, fixed for measured value

# EUV with true measured xray flux
euvlum = trappist_machine.flux_to_lum(euv_over_Fx * res_xrayflux_direct[1], 12.1)
euvlummin = trappist_machine.flux_to_lum(euv_over_Fx * res_xrayflux_direct[0], 12.1)
euvlummax = trappist_machine.flux_to_lum(euv_over_Fx * res_xrayflux_direct[2], 12.1)

# also x-ray, again:
xraylum_direct = trappist_machine.flux_to_lum(res_xrayflux_direct[1], 12.1)
xraylummin_direct = trappist_machine.flux_to_lum(res_xrayflux_direct[0], 12.1)
xraylummax_direct = trappist_machine.flux_to_lum(res_xrayflux_direct[2], 12.1)

## finally the sum:
xuvmiddle = euvlum + xraylum_direct
xuvmax = euvlummax + xraylummax_direct
xuvmin = euvlummin +xraylummin_direct

print ("euv luminosity level:", euvlum)
print( "euv flux level:", trappist_machine.planet_flux(euvlum, 12.1 * 206265))
print ("euv fractional luminosity:: %e" % (  euvlum /  ( solar_lum* trappist_lum)))

```

     -----
     USING REAL XRAY FLUX
     -----
    euv luminosity level: 4.75770972222334e+26
    euv flux level: 2.71624586120575e-14
    euv fractional luminosity:: 2.368604e-04

```python
# get the values for the table AND their errors. This can be copy pasted into the stellar parameters table in the paper.
#res_xrayflux = [xray_at_earthmin, xray_at_earth, xray_at_earthmax]

#res_xraylum_fraction = np.true_divide(trappist_machine.flux_to_lum([xray_at_earthmin, xray_at_earth, xray_at_earthmax],12.1),   ( solar_lum* trappist_lum))
print ("$F_{UV, uvw2, Earth}$  & %0.1e $\pm %0.1e$  & This Work \\\ " % (fit_val,fiterr))
print ("$L_{UV, uvw2}$ / L$_{*}$  & %0.1e $\pm %0.1e $  & This Work \\\ " % ( uvw2lum/  ( solar_lum* trappist_lum),  (uvw2lumup - uvw2lum)/  ( solar_lum* trappist_lum)))
#print "$L_{X}$ / L$_{*}$  & %0.1e $^{+%0.1e}_{%0.1e}$  & This Work, extrapolated$ \\\ " % (xraylum/ ( solar_lum* trappist_lum), (xraylummax - xraylum)/ ( solar_lum* trappist_lum), (xraylum - xraylummin) / ( solar_lum* trappist_lum))
print ("$L_{X}$ / L$_{*}$ (total)  & %0.1e $\pm %0.1e$  & This Work, measured \\\ " % (xraylum_direct/ ( solar_lum* trappist_lum), (xraylummax_direct - xraylum_direct)/ ( solar_lum* trappist_lum)))
print( "$L_{X}$ / L$_{*}$ (detections)  & %0.1e $\pm %0.1e$  & This Work, measured \\\ " % ((trappist_machine.flux_to_lum(flux_from_2param_APEC_flare, 12.1) / ( solar_lum* trappist_lum)), (trappist_machine.flux_to_lum(flux_from_2param_APEC_flare_err, 12.1) / ( solar_lum* trappist_lum))))
print( "$L_{X}$ / L$_{*}$ (baseline)  & %0.1e $\pm %0.1e$  & This Work, measured \\\ " % ((trappist_machine.flux_to_lum(2.67e-11 *raw_baseline, 12.1) / ( solar_lum* trappist_lum)), (trappist_machine.flux_to_lum(2.67e-11 *raw_baseline_err, 12.1) / ( solar_lum* trappist_lum))))
print( "$F_{X}$ (total)  & %0.1e $\pm %0.1e$ & This Work, measured \\\ " % (res_xrayflux_direct[1], res_xrayflux_direct[2] - res_xrayflux_direct[1],))
print( "$F_{X}$ (detections)  & %0.1e $\pm %0.1e$ & This Work, measured \\\ " % (flux_from_2param_APEC_flare, flux_from_2param_APEC_flare_err))
print( "$F_{X}$ (baseline)  & %0.1e $\pm %0.1e$  & This Work, measured \\\ " % (2.67e-11 * raw_baseline, 2.67e-11 * raw_baseline_err))
print ("$L_{EUV}$ / L$_{*}$ (total)  & %0.1e $\pm %0.1e$   & This Work, extrapolated \\\ " % ( euvlum/  ( solar_lum* trappist_lum),  (euvlummax - euvlum)/  ( solar_lum* trappist_lum)))
#print "$F_{EUV}$  & %0.1e $^{+%0.1e}_{%0.1e}$   & This Work, extrapolated \\\ " % (trappist_machine.planet_flux(euvlum, 12.1 * 206265), trappist_machine.planet_flux(euvlummax, 12.1 * 206265), trappist_machine.planet_flux(euvlummin, 12.1 * 206265))
print ("$L_{XUV}$ / L$_{*}$ (total) & %0.1e $\pm %0.1e$  & This Work, extrapolated \\\ " % ((xuvmiddle) /  ( solar_lum* trappist_lum), (xuvmax - xuvmiddle) /  ( solar_lum* trappist_lum)))

```

    $F_{UV, uvw2, Earth}$  & 8.4e-15 $\pm 1.4e-15$  & This Work \\
    $L_{UV, uvw2}$ / L$_{*}$  & 7.4e-05 $\pm 1.2e-05 $  & This Work \\
    $L_{X}$ / L$_{*}$ (total)  & 9.8e-05 $\pm 1.2e-05$  & This Work, measured \\
    $L_{X}$ / L$_{*}$ (detections)  & 5.6e-04 $\pm 3.5e-05$  & This Work, measured \\
    $L_{X}$ / L$_{*}$ (baseline)  & 6.1e-05 $\pm 7.0e-06$  & This Work, measured \\
    $F_{X}$ (total)  & 1.1e-14 $\pm 1.3e-15$ & This Work, measured \\
    $F_{X}$ (detections)  & 6.4e-14 $\pm 4.0e-15$ & This Work, measured \\
    $F_{X}$ (baseline)  & 6.9e-15 $\pm 8.0e-16$  & This Work, measured \\
    $L_{EUV}$ / L$_{*}$ (total)  & 2.4e-04 $\pm 2.8e-05$   & This Work, extrapolated \\
    $L_{XUV}$ / L$_{*}$ (total) & 3.3e-04 $\pm 4.0e-05$  & This Work, extrapolated \\

So, we find that the measured value of the x-ray flux is a factor of a few lower than the extrapolated value. The extrapolations are right in line with the Wheatley+ estimates, but our measured values are a bit lower (despite the fact we seem to have caught at least one flare in our time series, which could be biasing us even higher depending on how frequent flares actually are).

Does this matter? Let's check how it affects the mass loss rates and the ocean retention.

```python
flux_xuv_average = trappist_machine.planet_flux(euvlum + xraylum_direct, trappist1['a(AU)'].values)
flux_xuv_average
trappist1['XUV_flux(erg,cm-2,s-1)'] = flux_xuv_average
# get the mass loss rates, put it in the pandas planet frame
flux_scaled = trappist1['XUV_flux(erg,cm-2,s-1)'] #this needs to be scaled
G = 6.674*10**(-8.0) # cm3g−1s−2
eta = 0.1 # 10-20 percent for low mass planets; Owen & Alvarez 2016
alpha = 1 #planet cross section increases in the UV , less important for cooler planets
K = 1
mdot = (eta * np.pi * flux_scaled * alpha**2.0 * (np.multiply(trappist1['Radius(Rearth)'],1.0) * 6.371*10**8.0 )**3.0) / \
            (G * 5.9721986*10**27.0 * np.multiply(trappist1['Mass(Mearth)'],1) * K)# eq 1 of Wheatley

trappist1['mass_loss(g/s)'] = mdot # create mass loss column of table

```

```python
trappist1
```

      Planet
      Mass(Mearth)
      Radius(Rearth)
      Period(days)
      a(AU)
      e
      i(deg)
      XUV_flux(erg,cm-2,s-1)
      mass_loss(g/s)

      0
      TRAPPIST-1 h
      0.331
      0.773
      18.767953
      0.061935
      0.00567
      89.80
      62.324396
      1.772641e+07

      1
      TRAPPIST-1 g
      1.148
      1.148
      12.354473
      0.046877
      0.00208
      89.71
      108.795402
      2.922446e+07

      2
      TRAPPIST-1 f
      0.934
      1.046
      9.205585
      0.038534
      0.01007
      89.68
      161.008678
      4.021145e+07

      3
      TRAPPIST-1 e
      0.772
      0.910
      6.099043
      0.029283
      0.00510
      89.86
      278.805922
      5.547038e+07

      4
      TRAPPIST-1 d
      0.297
      0.784
      4.049959
      0.022280
      0.00837
      89.75
      481.596689
      1.592676e+08

      5
      TRAPPIST-1 c
      1.156
      1.095
      2.421807
      0.015815
      0.00654
      89.67
      955.836690
      2.212689e+08

      6
      TRAPPIST-1 b
      1.017
      1.121
      1.510876
      0.011548
      0.00622
      89.65
      1792.808629
      5.061533e+08

```python
# solve optimization problem:
# n-body sims stabilty (ecc damping), mass loss rates, tidal Qs, system age
# we will leave this for a future work
```

# Tidal Q Analysis

The planets' orbits will evolve The magnitude of these effects can be estimated using the following expression from Goldreich 1963 (with expressions written in a functionally identical form to that used in Barnes 2008):
\begin{equation}
da/dt = \Big( \frac{-63}{2}\frac{1}{Q' m_p}  \sqrt{GM^3} r_p^5 e^2 + \frac{9}{2}\frac{\sigma}{Q_{*}'}  \sqrt{G/M} R^5 m_{p} \Big) a^{-11/2},
\end{equation}
where $Q' = 3Q / 2k_{2}$, $m_p$ denotes a planetary mass, $r_p$ denotes a planetary radius, $G$ is the gravitational constant, $M$ denotes the stellar mass, $R$ the stellar radius, $a$ denotes the planetary semi-major axis, $e$  the planetary eccentricity, and the term $\sigma = sign(2\Omega - 3n)$ (where $\Omega$ is the stellar spin rate and $n$ the planetary mean motion) described the relative frequencies of the stellar rotational period and planetary orbital period.
In Gillon 2016, the TRAPPIST-1 rotation rate was estimated using ground-based data to be $P_{rot}= 1.40$ days, but follow-up work using data from the K2 spacecraft (Luger 2017, Vida 207) provided an updated value of $P_{rot}= 3.3$ days, which we use in this analysis.
This value of $P_{rot}= 3.3$ days (giving the star a rotational period situated in between the orbital periods of planets c and d) means that the sign term $\sigma$ cannot be neglected, as different planets will be affected differently by tides raised on the star.

Without a non-zero eccentricity, the orbital energy will not significantly change, and so $a$ will not change.
A secondary paired equation can be written to describe the evolution of eccentricity over time \citep{Goldreich1963},
\begin{equation}
de/dt = \Big(\frac{-63}{4}\frac{1}{Q' m_p}  \sqrt{GM^3} r_p^5 + \frac{171}{16}\frac{\sigma}{Q_{*}'}  \sqrt{G/M} R^5 m_{p} \Big) e\ a^{-13/2},
\end{equation}
where the first term is due to tides raised on the planet and the second term is due to tides raised on the star.

Also, we need an expression for the mass loss rate (Luger et al. 2015, Bourrier et al. 2017):
\begin{equation}
    \dot{m_p} = \epsilon ( r_{XUV} / r_p )^{2} ( 3 F_{XUV} / 4 G \rho K )
    \label{eq:mdot}
\end{equation}
where $\epsilon$ is the heating efficiency, $r_p$ the true radius of the planet, $r_{XUV}$ the effective planetary radius in the XUV waveband, $F_{XUV}$ the XUV flux received by the planet, $G$ the gravitational constant, and $K$ is a tidal enhancement factor, where:
\begin{equation}
    K = 1 - \frac{3}{2} \frac{r_{XUV}}{r_{roche}} +  \frac{1}{2}\frac{r_{XUV}^{2}}{r_{roche}^2}
\end{equation}
where $r_{roche}$ is the planetary Roche Lobe ($r_{roche}~=~(m_p / 2 M_{*})^{1/3} a$).

```python
from scipy.integrate import odeint, ode

perz = np.copy(trappist1['Period(days)'].values)
perz.sort()
color_to_use = ['#6a11d6','#7dd611','#d6117d','#d6a111','#1125d6']
linez = [1,1.5,2,2.5,3] #line widths for plotting to make it color-blind accessible
y0 = [0.011,0.00622, 0.00270 * 330.0] # initial conditions at y0
t = np.logspace(1, 10, 10000)
abserr = 1.0e-10
relerr = 1.0e-8
dmdtval = 0
e_zero = y0[1]

figsize(6,3)
for i, qval in enumerate(np.true_divide([1,10,100,1000,10000], 5.)):
    sol = odeint(trappist_machine.tidal_evo, y0, t, args = (qval,1e-5, perz[0],  dmdtval), atol=abserr, rtol=relerr)
    asol,esol,msol = sol.T
    if i == 0:
        plot(t, esol / e_zero, label = r"$Q' = (3 Q) / 2 k_{2}$ = %i" % (3. * qval / (2 * 0.3)), linewidth=linez[i], color=color_to_use[i])
    else:
        plot(t, esol / e_zero, label = r"$Q'$ = %i" % (3. * qval / (2 * 0.3)), color=color_to_use[i], linewidth=linez[i])
legend()
for i, qval in enumerate(np.true_divide([1,10,100,1000,10000], 5.)):
    sol = odeint(trappist_machine.tidal_evo, y0, t, args = (qval,1e-5, perz[0], 0), atol=abserr, rtol=relerr)
    asol,esol,msol =sol.T
    if i == 0:
        plot(t, esol / e_zero, label = r"$Q' = (3 Q) / 2 k_{2}$ = %i" % (3. * qval / (2 * 0.3)), linewidth=linez[i],linestyle="--", color=color_to_use[i])
    else:
        plot(t, esol / e_zero, label = r"$Q'$ = %i" % (3. * qval / (2 * 0.3)), color=color_to_use[i], linestyle="--", linewidth=linez[i])

#ylim(0.0108,0.0115)
xlabel("Time (years) post disk dispersal", fontsize=17)
#xlabel("3/2 * Q/k", fontsize=17)
ylabel("$e_b(t)$ /$e_b(0)$", fontsize=17)
xticks(fontsize=14)
yticks(fontsize=14)
xlim(1e4, 7.6e9)
ylim(0,1)
xscale("log")
savefig(paperFigures + "old-trappist-ecc.pdf", dpi=150, bbox_inches = 'tight')
```

```python
from scipy.integrate import odeint, ode

perz = np.copy(trappist1['Period(days)'].values)
perz.sort()
color_to_use = ['#6a11d6','#7dd611','#d6117d','#d6a111','#1125d6']
linez = [1,1.5,2,2.5,3] #line widths for plotting to make it color-blind accessible
y0 = [0.011,0.00622, 0.00270 * 330.0] # initial conditions at y0
t = np.logspace(1, 10, 10000)
abserr = 1.0e-10
relerr = 1.0e-8
dmdtval = trappist1.loc[trappist1['Planet'] == 'TRAPPIST-1 b']['mass_loss(g/s)'].values[0] * 5.2804674*10**(-21.)
e_zero = trappist1.loc[trappist1['Planet'] == 'TRAPPIST-1 b']['e'].values[0]

figsize(6,6)
subplot(2,1,1)
for i, qval in enumerate(np.true_divide([1,10,100,1000,10000], 5.)):
    sol = odeint(trappist_machine.tidal_evo, y0, t, args = (qval,1e-5, perz[0],  1*dmdtval), atol=abserr, rtol=relerr)
    asol,esol,msol = sol.T
    sol2 = odeint(trappist_machine.tidal_evo, y0, t, args = (qval,1e-5, perz[0],  0), atol=abserr, rtol=relerr)
    asol2,esol2,msol2 = sol2.T
    subplot(2,1,1)

    if i == 0:
        plot(t, esol / e_zero, label = r"$Q' = (3 Q) / 2 k_{2}$ = %i" % (3. * qval / (2 * 0.3)), linewidth=linez[i], color=color_to_use[i])
    else:
        plot(t, esol / e_zero, label = r"$Q'$ = %i" % (3. * qval / (2 * 0.3)), color=color_to_use[i], linewidth=linez[i])

    subplot(2,1,2)
    if i == 0:
        plot(t, 100 * np.abs(np.subtract(esol,esol2)) / e_zero, label = r"$Q' = (3 Q) / 2 k_{2}$ = %i" % (3. * qval / (2 * 0.3)), linewidth=linez[i], color=color_to_use[i])
    else:
        plot(t, 100 *np.abs(np.subtract(esol,esol2)) / e_zero, label = r"$Q'$ = %i" % (3. * qval / (2 * 0.3)), color=color_to_use[i], linewidth=linez[i])

for i in [1,2]: #make plot parameters identical for each subplot
    subplot(2,1,i)
    #ylim(0.0108,0.0115)
    #xlabel("3/2 * Q/k", fontsize=17)
    xlim(1e5, 1e10)
    xscale("log")

# top plot
subplot(2,1,1)
ylabel("$e_b(t)$ /$e_b(0)$", fontsize=17)
xticks([])
ylim(0,1)
yticks(fontsize=14)

#second plot
subplot(2,1,2)
ylabel("Deviation ($\delta e_b(t)$ /$e_b(0)$) \n from constant mass model", fontsize=13)
xlabel("Time (years) post disk dispersal", fontsize=17)
ylim(0,0.33)
legend()
xticks(fontsize=15)
yticks([0.3,0.2,.1,0],['0.3 %','0.2 %','0.1 %','0 %'],fontsize=12)

subplots_adjust(hspace = 0)
savefig(paperFigures + "trappist-ecc.pdf", dpi=150, bbox_inches = 'tight')
```

#  DE/DT DA/DT DL/DT
What happens to the eccentricity of the planets as the orbits evolve, according to a simple tidal model? This can also be computed using the timescale expression and not the full time-series.

If we increase $dm/dt$ enough, it will start to change the solutions: for $de/dt$, this can even mean that the tide raised on the star becomes less and less important than the tide raised on the planet. However, the expected values of $dm/dt$ for this system are really too small to matter, and the imbalance between the sizes of the two terms if already extreme because $Q_{*}$ is so large.

```python
# make e plot with all planets
# don't forget to make things radians. and convert back at the end.
t = np.linspace(0, 7.6e9, 100000)
#integrate it
from scipy.integrate import odeint, ode
abserr = 1.0e-10
relerr = 1.0e-8

colors_for_this = ["#666666","#004C70","#0093D1","#F2635F","#F4D00C","#E0A025","k"]
## first look at planet b alone, its future evolution from this current point.

#
figsize(4,10)
qvals = [10./5.,100./5., 1000./5]
for knum in range(len(qvals)):
    subplot(len(qvals), 1,knum +1)
    counter =0
    for i, planet in enumerate(['TRAPPIST-1 b','TRAPPIST-1 c','TRAPPIST-1 d','TRAPPIST-1 e','TRAPPIST-1 f','TRAPPIST-1 g', 'TRAPPIST-1 h']):
        sma = trappist1.loc[trappist1['Planet'] == planet]['a(AU)'].values[0]
        ecc = trappist1.loc[trappist1['Planet'] == planet]['e'].values[0]
        mass = trappist1.loc[trappist1['Planet'] == planet]['Mass(Mearth)'].values[0]
        per = trappist1.loc[trappist1['Planet'] == planet]['Period(days)'].values[0]
        dmdtval = trappist1.loc[trappist1['Planet'] == planet]['mass_loss(g/s)'].values[0] * 5.2804674*10**(-21.) #convert units
        y0 = [sma, ecc, mass] # initial conditions at y0
        sol = odeint(trappist_machine.tidal_evo, y0, t, args = (qvals[knum],1e-5,per, dmdtval), atol=abserr, rtol=relerr) # solve this
        asol,esol,msol = sol.T
        plot(t /1e9, esol, label = "%s" % planet, color=colors_for_this[i])
        if esol[-1] < 0.001: # add text labels so we know which planet is which if seen without color
            text(7.7 + counter * 0.35,esol[-1], planet[-1], color = colors_for_this[i], fontsize=17)
            counter = counter + 1
        else:
            text(7.7,esol[-1], planet[-1], color = colors_for_this[i], fontsize=17)

    if knum != len(qvals) -1:
        xticks([],[],fontsize=14)
        yticks(fontsize=14)
    xlim(0,9.2)
    text(3.4,0.0085, "Q' = %i" % (3. * qvals[knum] / (0.3 * 2)), fontweight = 'bold', fontsize=20, alpha=0.7)
    ylabel("Eccentrcity ($e$)", fontsize=15)
xticks(fontsize=14)
subplots_adjust(hspace = 0)
xlabel("Time (Gyr)", fontsize=17)
yticks(fontsize=14)
savefig(paperFigures + "trappist-all-ecc-pumpedn_botherms.pdf", dpi=150, bbox_inches = 'tight')
```

# Find the values of k/Q that keep eccentricites non-zero over system age.
This cell takes a little while to run (on the order of ~30 minutes), so the results will also be saved as "timescale_plot_data.pkl" and can be loaded rather than re-generated. We note that since $dm/dt$ is small, the derived timescales should not vary compared to the classic analytic expression; however, we solve the ODE in anticipation that for systems with larger $dm/dt$, the changing planetary mass could matter over long time scales.

```python
# don't forget to make things radians. and convert back at the end.
t = np.linspace(0, 10e9, 1000000)

colors_for_this = ["#666666","#004C70","#0093D1","#F2635F","#F4D00C","#E0A025","k"]

#
figsize(4,10)
qvals = np.logspace(-1, 4, 60)
answers = pd.DataFrame()
endtimes = pd.DataFrame()

# using the errors from the Grimm paper, find the place that we couldn't see if the result is incompatable with zero or not:
incomp_with_zero = [0.00622 - 2 * 0.00304, 0.00654 - 3 * 0.00188, 0.00837 - 8* 0.00093, 0.00510 - 8* 0.00058, 0.01007 - 7* 0.00068,
                    0.00208 - 3* 0.00058, 0.00567 - 4 * 0.00121] # comes from errors on each measured value of the ecc
if regen_Q_values ==True:
    for knum in range(len(qvals)):
        tmpz =[]
        timez = []
        for i, planet in enumerate(['TRAPPIST-1 b','TRAPPIST-1 c','TRAPPIST-1 d','TRAPPIST-1 e','TRAPPIST-1 f','TRAPPIST-1 g', 'TRAPPIST-1 h']):
            sma = trappist1.loc[trappist1['Planet'] == planet]['a(AU)'].values[0]
            ecc = trappist1.loc[trappist1['Planet'] == planet]['e'].values[0]
            mass = trappist1.loc[trappist1['Planet'] == planet]['Mass(Mearth)'].values[0]
            per = trappist1.loc[trappist1['Planet'] == planet]['Period(days)'].values[0]
            dmdtval = trappist1.loc[trappist1['Planet'] == planet]['mass_loss(g/s)'].values[0] * 5.2804674*10**(-21.) #convert units

            y0 = [sma, ecc, mass] # initial conditions at y0
            sol = odeint(trappist_machine.tidal_evo, y0, t, args = (qvals[knum],1e-5,per, dmdtval), atol=abserr, rtol=relerr)
            asol,esol,msol = zip(*sol)
            tmpz.append(esol[-1])
            try:
                timeend = min(pd.Series(t).loc[pd.Series(esol) < (incomp_with_zero[i])])
            except ValueError:
                timeend = NaN
            timez.append(timeend)

        answers = answers.append({'mean': np.mean(tmpz), 'b':tmpz[0],'c':tmpz[1], 'd':tmpz[2],
                                 'e':tmpz[3], 'f':tmpz[4], 'g':tmpz[5], 'h':tmpz[6],
                                  'btime':timez[0],'ctime':timez[1], 'dtime':timez[2],
                                 'etime':timez[3], 'ftime':timez[4], 'gtime':timez[5], 'htime':timez[6],
                                 'q': qvals[knum]}, ignore_index=True)

```

```python
if regen_Q_values ==True:
    answers['k/Q'] =0.3 / answers['q']
    answers.to_pickle(assorteddata + "timescale_plot_data.pkl") # save results
```

```python
answers = pd.read_pickle(assorteddata + "timescale_plot_data.pkl") # shortcut to make plot without generating results
```

```python
assorteddata + "timescale_plot_data.pkl"
```

    '/Users/jcbecker/Documents/GitHub/trappist1/AssortedData/timescale_plot_data.pkl'

```python
figsize(6,4)
colors_for_this = ["#666666","#004C70","#0093D1","#F2635F","#F4D00C","#E0A025","k"]

for i, plano in enumerate(['b','c','d','e']):
    x = 3/2. * 1./answers['k/Q']
    plot(x, answers[plano +'time'] /1e9, color=colors_for_this[i], linewidth = 2, label=plano)
    fill_between(np.append(x.values, 10000), np.append((answers[plano +'time'].fillna(10e9).values/1e9),10), color= colors_for_this[i], alpha=0.03)

ytimes = np.linspace(0,9)
pap_est = 1.2*10**3*(ytimes/5)
fill_between(np.append(pap_est, 10000), np.append(ytimes,10), color= '#458B00', alpha=0.08)
plot(pap_est, ytimes, '#3B5323', linewidth=1.4,linestyle=":", alpha=0.6)
hlines(7.6, 0, 5000, 'SlateGrey', linestyle=":")
xscale('log')
xlim(1, 5e3)
ylim(0.1, 8)
xticks(fontsize=14)
yticks(fontsize=14)
xlabel("$Q' = 3 Q / (2 k_{2})$", fontsize=17)
ylabel("$t_{ecc, damp}$ (Gyr)", fontsize=17)

# the arrows are added manually at the intersection point between curve and 7.6 Gyr line

endt = [600, 125, 54, 3] # initial guesses
for i, plano in enumerate(['b','c','d','e']):
    x = 3/2. * 1./answers['k/Q']
    y = scipy.interpolate.interp1d(x, answers[plano +'time']/1e9)
    xtest = np.linspace(1,endt[i], 5000)
    ynew = y(xtest)
    plot(xtest[np.argmin(np.abs(ynew - 7.6))], 7.6, marker ='>', color=colors_for_this[i],markersize = 10,)

text(2000, 5.7,"Constraint from \nPapaloizou et al. 2018", color = '#3B5323', fontsize=14, rotation=90)
legend(fontsize= 15, loc='lower center')

savefig(paperFigures + "timescale_plot_t1.pdf",  dpi=150, bbox_inches = 'tight')
```

# Make TRAPPIST array ready to print to put in the paper
We have computed the mass loss rate due to the measured UV and extrapolated XUV flux. Now, we want to print a table of these values for the paper. We will also put the planetary properties in desired units.

```python
print_cols = ['Planet', 'Period(days)','Radius(Rearth)', 'Mass(Mearth)', 'e', 'XUV_flux(erg,cm-2,s-1)',
              u'mass_loss(g/s)','mass_loss(mearth/Gyr)'] # these will go in paper, in order
trappist1['mass_loss(mearth/Gyr)'] = trappist1['mass_loss(g/s)'] * 5.2804674*10**(-21.) * 1.0e9# Earth masses per year * gyr/ yr
trappist1.sort_values('Period(days)')[print_cols].to_latex(index = False)
```

    '\\begin{tabular}{lrrrrrrr}\n\\toprule\n       Planet &  Period(days) &  Radius(Rearth) &  Mass(Mearth) &        e &  XUV\\_flux(erg,cm-2,s-1) &  mass\\_loss(g/s) &  mass\\_loss(mearth/Gyr) \\\\\n\\midrule\n TRAPPIST-1 b &      1.510876 &           1.121 &         1.017 &  0.00622 &             1792.808629 &    5.061533e+08 &               0.002673 \\\\\n TRAPPIST-1 c &      2.421807 &           1.095 &         1.156 &  0.00654 &              955.836690 &    2.212689e+08 &               0.001168 \\\\\n TRAPPIST-1 d &      4.049959 &           0.784 &         0.297 &  0.00837 &              481.596689 &    1.592676e+08 &               0.000841 \\\\\n TRAPPIST-1 e &      6.099043 &           0.910 &         0.772 &  0.00510 &              278.805922 &    5.547038e+07 &               0.000293 \\\\\n TRAPPIST-1 f &      9.205585 &           1.046 &         0.934 &  0.01007 &              161.008678 &    4.021145e+07 &               0.000212 \\\\\n TRAPPIST-1 g &     12.354473 &           1.148 &         1.148 &  0.00208 &              108.795402 &    2.922446e+07 &               0.000154 \\\\\n TRAPPIST-1 h &     18.767953 &           0.773 &         0.331 &  0.00567 &               62.324396 &    1.772641e+07 &               0.000094 \\\\\n\\bottomrule\n\\end{tabular}\n'

```python
trappist1.to_csv(assorteddata + "TRAPPIST_derived.csv")
```

# Check luminosity evolution of a star of this mass from Baraffe models

```python
# Luminosity evolution: BHC 2015
figsize(6,4)
namez = ['M/Ms','log t(yr)','Teff','L/Ls','g','R/Rs','Log(Li/Li0)','log Tc','log ROc', 'Mrad', 'Rrad', 'k2conv', 'k2rad']
model = pd.read_csv(assorteddata + "BHAC15_tracks+structure.txt", dtype ='float',names = namez, skiprows=50, delim_whitespace=True)
idx = (model['M/Ms'] == 0.0900)
# L/Ls: log luminosity in units of solar luminosity (value used Ls=3.839d+33)
plot(np.power(10,model.loc[idx]['log t(yr)'].values) / 1e9, np.power(10,model.loc[idx]['L/Ls']),
     linewidth = 3, label = "BHC 2015 Model")
xlabel("Time (Gyr)", fontsize=17)
ylabel("$L_{bol}$ / L${\odot}$", fontsize=17)
xticks(fontsize=14)
yticks(fontsize=14)
plot(7.6, trappist_lum, markersize = 15, color = "Red", linestyle='None',marker="*", label="TRAPPIST-1")
yscale('log')
xscale("log")
legend()
title("Baraffe 2015, M = 0.09 Msun", fontsize=17)
title("0.09 M$_{\odot}$ Stellar Evolution", fontsize = 17)
savefig(extraFigures + "BHAC_stellarmodel.pdf",  dpi=150, bbox_inches = 'tight')
```

# Plot results from [VPlanet](https://github.com/VirtualPlanetaryLaboratory/vplanet) runs.
We know ocean retention will depend on the XUV flux.
To test how our derived XUV luminosity ratio affects the ocean evolution, we will run two integrations with VPlanet. We will also try running integrations with different values of Q, to see if that matters.

```python
planets = ['b','c','d','e','f','g','h']
namz = ['Time','SurfWaterMass','OxygenMass','Ecc']
figsize(6,4)
for i, planet in enumerate(planets):
    file_loc = current_dir + "VPlanetRuns/run01/trappist1.%s.forward" % planet
    data = pd.read_csv(file_loc, names = namz, delim_whitespace=True)
    plot(data['Time'], data['SurfWaterMass'] / data['SurfWaterMass'].values[0], label='_nolegend_', linewidth = 4, alpha=0.3, c = colors_for_this[i])
    plot(data['Time'], data['SurfWaterMass'] / data['SurfWaterMass'].values[0], label=planet, linewidth = 2, c = colors_for_this[i])
xscale("log")
legend(fontsize=12, loc='upper left')
xlabel("Time (years)", fontsize=17)
ylabel("Fractional Ocean Retention", fontsize=17)
yticks(fontsize=14)
xticks(fontsize=14)
ylim(0,1)
annotate("$L_{XUV} = 3.7 \cdot 10^{-4}$ L$_{bol}$\n$Q'$ = 600", (1e6, 0.05), fontsize = 14)
savefig(paperFigures + "trappist_water_midQ_trueLXUV.pdf", dpi=150, bbox_inches = 'tight')

```

```python
planets = ['b','c','d','e','f','g','h']
namz = ['Time','SurfWaterMass','OxygenMass','Ecc']
figsize(6,4)
for i, planet in enumerate(planets):
    file_loc = current_dir + "VPlanetRuns/run02/trappist1.%s.forward" % planet
    data = pd.read_csv(file_loc, names = namz, delim_whitespace=True)
    plot(data['Time'], data['SurfWaterMass'] / data['SurfWaterMass'].values[0], label='_nolegend_', linewidth = 4, alpha=0.3, c = colors_for_this[i])
    plot(data['Time'], data['SurfWaterMass'] / data['SurfWaterMass'].values[0], label=planet, linewidth = 2, c = colors_for_this[i])
xscale("log")
legend(fontsize=12, loc='upper left')
xlabel("Time (years)", fontsize=17)
ylabel("Fractional Ocean Retention", fontsize=17)
yticks(fontsize=14)
xticks(fontsize=14)
ylim(0,1)
annotate("$L_{XUV} = 3.7 \cdot 10^{-4}$ L$_{bol}$\n$Q'$ = 60", (1e6, 0.05), fontsize = 14)
savefig(extraFigures + "trappist_water_midQ_lowLXUV.pdf", dpi=150, bbox_inches = 'tight')

```

```python
planets = ['b','c','d','e','f','g','h']
namz = ['Time','SurfWaterMass','OxygenMass','Ecc']
figsize(6,4)
for i, planet in enumerate(planets):
    file_loc = current_dir + "VPlanetRuns/run03/trappist1.%s.forward" % planet
    data = pd.read_csv(file_loc, names = namz, delim_whitespace=True)
    plot(data['Time'], data['SurfWaterMass'] / data['SurfWaterMass'].values[0], label='_nolegend_', linewidth = 4, alpha=0.3, c = colors_for_this[i])
    plot(data['Time'], data['SurfWaterMass'] / data['SurfWaterMass'].values[0], label=planet, linewidth = 2, c = colors_for_this[i])
xscale("log")
legend(fontsize=12, loc='upper left')
xlabel("Time (years)", fontsize=17)
ylabel("Fractional Ocean Retention", fontsize=17)
yticks(fontsize=14)
xticks(fontsize=14)
ylim(0,1)
annotate("$L_{XUV} = 1 \cdot 10^{-4}$ L$_{bol}$\n$Q'$ = 600", (1e6, 0.05), fontsize = 14)
savefig(extraFigures + "trappist_water_midQ_lowLXUV.pdf", dpi=150, bbox_inches = 'tight')

```

# Check luminosity evolution computed by VPlanet

```python
planets = ['b','c','d','e','f','g','h']
namz = ['Time', 'Luminosity', 'LXUVTot' ,'Temperature', 'HZLimRecVenus', 'HZLimRunaway', 'HZLimMaxGreenhouse', 'HZLimEarlyMars']
figsize(6,4)
file_loc = current_dir + "VPlanetRuns/AtmEsc+EqTide/trappist1.star.forward"
data = pd.read_csv(file_loc, names = namz, delim_whitespace=True)
plot(data['Time'] / 1e9, data['LXUVTot'] / trappist_lum, linewidth = 2, c = 'Purple', label="$L_{XUV}$ (Total) Computed by VPlanet")
xscale("log")
#legend(fontsize=12, loc='upper left')
xlabel("Time (Gyr)", fontsize=17)
ylabel("$L_{XUV}$ / $L_{bol}$", fontsize=17)
yticks(fontsize=14)
xticks(fontsize=14)
#ylim(0,1)
yscale("log")
#annotate("$L_{XUV} = 3 \cdot 10^{-4}$ L$_{bol}$\n$Q'$ = 600", (1e6, 0.05), fontsize = 14)
errorbar(7.6, (xuvmiddle) /  ( solar_lum* trappist_lum), yerr = (xuvmax - xuvmiddle) /  ( solar_lum* trappist_lum), markersize = 10, color = "Red", linestyle='None',marker=".", label="Derived value for TRAPPIST-1")
legend()
savefig(extraFigures + "star_test.pdf", dpi=150, bbox_inches = 'tight')

```

```python

```

```python

```


# PyFolding SI Notebook 8
---

[Authors] ERGM & ARL

---

## This is to Global fit Equilibrium Curves from a dimeric protein to dimeric equilibrium models ##

In this notebook we will show how equilbrium folding data from dimeric proteins can be imported into a notebook and fitted to dimeric Equilibrium denaturation models using global fits over multiple datasets.

If you are less script/computer orientated, you can simply change the data paths and other parameters for your proteins and re-run the jupyter notebook ( "Kernal/Restart & Run all" from the menu above).

---

## Data Format

Please see PyFolding SI Notebooks 1 and 2 for the format your data has to be in to enable this type of analysis.
Remember for Ising Model Analysis here each protein dataset (equilibrium denaturation curve) must have its own .csv

---

#### First off lets load pyfolding & pyplot into this ipython notebook (pyplot allows us to plot more complex figures of our results):

---

```python
# use this command to tell Jupyter to plot figures inline with the text
%matplotlib inline

# import pyfolding, the pyfolding models and ising models
import pyfolding
from pyfolding import *

# import the package for plotting, call it plt
import matplotlib.pyplot as plt

# import numpy as well
import numpy as np
```

    <IPython.core.display.Javascript object>

    PyFolding: Jupyter autoscrolling has been disabled

---

#### Now, we need to load some data to analyse.

I will import the equilibrium denaturations of dimeric tr-LcrH at a series of protein concentrations from:

`Singh S.K., Boyle A.L. & Main E.R. (2013) “LcrH, a class II chaperone from the type three secretion system, has a highly flexible native structure.” J Biol Chem., 288, (6) 4048-55.`

I will load 4 denaturation curves that correspond tr-LcrH at the following protein concentrations:

Protein     |Filename     |Total Protein Concentration
:--------:  |:-------:    |:-------:
tr-LcrH     | 50uM_1.csv  |  50 uM
tr-LcrH     | 50uM_2.csv  |  50 uM
tr-LcrH     | 80uM_1.csv  |  80 uM
tr-LcrH     | 80uM_2.csv  |  80 uM

---

```python
# we will load all of the data together, as follows:

# arguments are "path", "filename"
pth = "../examples/LcrH"

# this is a set of commands to automate loading the data
# for each denaturation
fn = ["50uM_1.csv","50uM_2.csv","80uM_1.csv","80uM_2.csv"]

# Here we are loading all the curves in a list called "proteins"
# and assigning them names
proteins = [pyfolding.read_equilibrium_data(pth,f) for f in fn]

# also store the total protein conentration for each denaturation
Pt = [50e-6, 50e-6, 80e-6, 80e-6]

```

```python
# Set temperature to 25.00°C
# (NOTE: Careful, this sets the temperature for all subsequent calculations)
pyfolding.set_temperature(25.)
```

    Set temperature to 25.00°C
    (NOTE: Careful, this sets the temperature for all subsequent calculations)

```python
# the following commands plot all the data curves on one plot

plt.figure(figsize=(10,6))
for c in proteins:
    plt.plot(c.x, c.y, '.')

# the following commands plot all the data curves on one plot,
# where "loc" command determines where the legend goes.
plt.legend([c.ID for c in proteins], loc='best')
plt.title("LcrH Equilm Curves")
plt.xlim([0, 8])                # x axis from 0 to 8
plt.show()

```

```python
# Command imports pyfolding models
from pyfolding.models import *

# command lists models
list_models()

# After the model name:
#'Verified: True' model has been rigourously and it functions as expected.
#'Verified:False' signifies that the model has not been rigourously tested.

```

    [('ChevronPolynomialFit', 'Verified: True'),
     ('HeteropolymerIsingEquilibrium', 'Verified: False'),
     ('HomozipperIsingEquilibrium', 'Verified: True'),
     ('ParallelTwoStateChevron', 'Verified: False'),
     ('ParallelTwoStateUnfoldingChevron', 'Verified: False'),
     ('TemplateModel', 'Verified: False'),
     ('ThreeStateChevron', 'Verified: True'),
     ('ThreeStateDimericIEquilibrium', 'Verified: True'),
     ('ThreeStateEquilibrium', 'Verified: True'),
     ('ThreeStateFastPhaseChevron', 'Verified: True'),
     ('ThreeStateMonoIEquilibrium', 'Verified: True'),
     ('ThreeStateSequentialChevron', 'Verified: True'),
     ('TwoStateChevron', 'Verified: True'),
     ('TwoStateChevronMovingTransition', 'Verified: True'),
     ('TwoStateDimerEquilibrium', 'Verified: True'),
     ('TwoStateEquilibrium', 'Verified: True'),
     ('TwoStateEquilibriumSloping', 'Verified: True')]

```python
# We are going to fit this data to 3 state denaturation that unfolds via
# a dimeric intermediate (as per the J.B.C. paper)
# Lets print the equation & Info

models.ThreeStateDimericIEquilibrium().info()
```

$$\begin{equation} \
                \begin{aligned} \
                & \Upsilon_{rel} = \Upsilon_N F_N + \Upsilon_I F_I + \Upsilon_D F_D \\ \
                \text{expanded:} \\ \
                & \Upsilon_{rel} = \Upsilon_N \cdot \frac{2PtF_D^2} {K_1 K_2} + \Upsilon_I \frac{2PtF_D^2} {K_2} + \Upsilon_D * (F_D) \\ \
                \\ \
                \text{where:} \\ \
                & F_D = \frac {- K_1 K_2 + \sqrt{((K_1 K_2)^2 + 8(1+K_1)(K_1 K_2)Pt)}} {4Pt (1 + K_1)} \\ \
                & K_1 = \exp \frac{-\Delta G_{H_20}^1 + m_1 x} {RT} \\ \
                & K_2 = \exp \frac{-\Delta G_{H_20}^2 + m_2 x} {RT}\
                \end{aligned}\
                \end{equation}$$

     Three State model for a dimer denaturation Equilibrium - Dimeric Intermediate.

        Folding Scheme:
            N2 <-> I2 <-> 2D

        Params:
            Y_rel = spectroscopic signal at a given concentration of urea
            Y_N = spectroscopic signal for native state
            Y_D = spectroscopic signal for denatured state
            Y_I = spectroscopic signal for intermediate state
            F_D = fraction denatured monomers
            F_N = fraction native dimers
            F_I = fraction intermediate dimers
            Pt = total protein concentration. This variable needs to be set per denaturation curve.
            K1 = equilibrium contstant of unfolding native to intermediate state
            K2 = equilibrium contstant of unfolding intermediate to denatured state
            DG1 = stability of native state relative to intermediate state
            m1 = m-value of native to intermediate transition
            DG2 = stability of intermediate state relative to denatured state
            m2 = m-value of intermediate to denatured transition
            x = denaturant concentration (M)
            R = Universal Gas Constant (kcal.mol-1.K-1)
            T = Temperature (Kelvin)

        Reference:
            Mallam and Jackson. Folding studies on a knotted protein.
            Journal of Molecular Biology (2005) vol. 346 (5) pp. 1409-1421

---

### Automatic global fitting to the Three State Dimeric Intermediate Equilibrium model

We could fit individual denaturations to this model.  However, one aspect of the model is that it should be able to fit globally to a number of denaturations. We will try to fit this with the Three State Dimeric Intermediate Equilibrium model

---

```python
global_fit = core.GlobalFit()
global_fit.fit_funcs = [models.ThreeStateDimericIEquilibrium
                        for i in xrange(len(proteins))]
global_fit.constants = [(('Pt',c),) for c in Pt]
global_fit.shared = ['DG1','DG2','m1','m2'] #
global_fit.x = [p.x for p in proteins]
global_fit.y = [p.y for p in proteins]
global_fit.ID = [p.ID for p in proteins]

global_fit.initialise()
```

```python
print global_fit.params.keys(), len(global_fit.params.keys())
```

    ['DG2', 'DG1', 'm1', 'm2', 'Y_N_{50uM_1}', 'Y_I_{50uM_1}', 'Y_D_{50uM_1}', 'Y_N_{50uM_2}', 'Y_I_{50uM_2}', 'Y_D_{50uM_2}', 'Y_N_{80uM_1}', 'Y_I_{80uM_1}', 'Y_D_{80uM_1}', 'Y_N_{80uM_2}', 'Y_I_{80uM_2}', 'Y_D_{80uM_2}'] 16

```python
# this commands gives our initial parameters for fitting
p0 = [5.,1.7,1.,1.7,
      -200,-50,-10,
      -200,-50,-10,
      -200,-50,-10,
      -200,-100,-10]

# this command gives fitting constraints
b = bounds=((0.,0.,-3.,-3.,
             -300,-300,-300,
             -300,-300,-300,
             -300,-300,-300,
             -300,-300,-300),
            (20.,20.,3.,3.,
             0,0,0,
             0,0,0,
             0,0,0,
             0,0,0))

# this commands checks that our inputs for the initial parameters and
# constraints (bounds) above
# match the number of our variables
print len(b[0]), len(b[1]), len(p0)

#this commands tell pyfolding to fit our data
out, covar = global_fit.fit( p0=p0, bounds=b )

# print out the results of the fit
for result in global_fit.results:
    result.display()
```

    16 16 16
    ================================================================================
     Fitting results
    ================================================================================
     ID: 50uM_1
     Model: ThreeStateDimericIEquilibrium
     Optimiser: pyfolding.GlobalFit and scipy.optimize.curve_fit
     Temperature: 25.00°C

     (s) DG1      1.66967 ± 0.19094      	 95% CI[     1.62134,      1.71800]
     (s) m1       1.07183 ± 0.11736      	 95% CI[     1.04213,      1.10154]
     (s) DG2     13.91136 ± 0.63253      	 95% CI[    13.75126,     14.07146]
     (s) m2       1.71686 ± 0.11881      	 95% CI[     1.68679,      1.74694]
     (f) Y_N   -133.91502 ± 2.65244      	 95% CI[  -134.58638,   -133.24366]
     (f) Y_I    -75.49314 ± 3.07308      	 95% CI[   -76.27097,    -74.71531]
     (f) Y_D     -6.57648 ± 1.04424      	 95% CI[    -6.84079,     -6.31218]
     (c) Pt       0.00005
    --------------------------------------------------------------------------------
     R^2: 	0.99661
     DOF: 	133
    |SS|: 	1.81e+03
    ================================================================================

    ================================================================================
     Fitting results
    ================================================================================
     ID: 50uM_2
     Model: ThreeStateDimericIEquilibrium
     Optimiser: pyfolding.GlobalFit and scipy.optimize.curve_fit
     Temperature: 25.00°C

     (s) DG1      1.66967 ± 0.19094      	 95% CI[     1.62134,      1.71800]
     (s) m1       1.07183 ± 0.11736      	 95% CI[     1.04213,      1.10154]
     (s) DG2     13.91136 ± 0.63253      	 95% CI[    13.75126,     14.07146]
     (s) m2       1.71686 ± 0.11881      	 95% CI[     1.68679,      1.74694]
     (f) Y_N   -135.06345 ± 2.51075      	 95% CI[  -135.69895,   -134.42796]
     (f) Y_I    -72.07032 ± 3.03212      	 95% CI[   -72.83778,    -71.30285]
     (f) Y_D     -5.47747 ± 1.06953      	 95% CI[    -5.74818,     -5.20676]
     (c) Pt       0.00005
    --------------------------------------------------------------------------------
     R^2: 	0.99375
     DOF: 	133
    |SS|: 	1.81e+03
    ================================================================================

    ================================================================================
     Fitting results
    ================================================================================
     ID: 80uM_1
     Model: ThreeStateDimericIEquilibrium
     Optimiser: pyfolding.GlobalFit and scipy.optimize.curve_fit
     Temperature: 25.00°C

     (s) DG1      1.66967 ± 0.19094      	 95% CI[     1.62134,      1.71800]
     (s) m1       1.07183 ± 0.11736      	 95% CI[     1.04213,      1.10154]
     (s) DG2     13.91136 ± 0.63253      	 95% CI[    13.75126,     14.07146]
     (s) m2       1.71686 ± 0.11881      	 95% CI[     1.68679,      1.74694]
     (f) Y_N   -226.85435 ± 3.62134      	 95% CI[  -227.77095,   -225.93775]
     (f) Y_I   -126.95009 ± 4.24583      	 95% CI[  -128.02476,   -125.87542]
     (f) Y_D    -10.23377 ± 1.20873      	 95% CI[   -10.53972,     -9.92783]
     (c) Pt       0.00008
    --------------------------------------------------------------------------------
     R^2: 	0.99589
     DOF: 	133
    |SS|: 	1.81e+03
    ================================================================================

    ================================================================================
     Fitting results
    ================================================================================
     ID: 80uM_2
     Model: ThreeStateDimericIEquilibrium
     Optimiser: pyfolding.GlobalFit and scipy.optimize.curve_fit
     Temperature: 25.00°C

     (s) DG1      1.66967 ± 0.19094      	 95% CI[     1.62134,      1.71800]
     (s) m1       1.07183 ± 0.11736      	 95% CI[     1.04213,      1.10154]
     (s) DG2     13.91136 ± 0.63253      	 95% CI[    13.75126,     14.07146]
     (s) m2       1.71686 ± 0.11881      	 95% CI[     1.68679,      1.74694]
     (f) Y_N   -207.47574 ± 3.31055      	 95% CI[  -208.31368,   -206.63780]
     (f) Y_I   -122.90644 ± 3.83992      	 95% CI[  -123.87836,   -121.93451]
     (f) Y_D    -16.04308 ± 1.67988      	 95% CI[   -16.46827,    -15.61788]
     (c) Pt       0.00008
    --------------------------------------------------------------------------------
     R^2: 	0.99668
     DOF: 	133
    |SS|: 	1.81e+03
    ================================================================================

---

### To plot the fitted data we need to use this script:

---

```python
results = global_fit.results
plt.figure(figsize=(14,8))
for i, p in enumerate(proteins):
    plt.plot(p.x, p.y, 'ko')
    plt.plot(results[i].x_fit, results[i].y_fit, '-', label=p.ID)
plt.legend()
plt.xlabel(p.denaturant_label, fontsize=constants.FONT_SIZE)
plt.ylabel('Elipticity @ 222 nm', fontsize=constants.FONT_SIZE)
plt.show()
```

### We can also simulate the data as follows:

This is also a good way to work out what initial parameters might work well

```python
#Lets first choose some values for simulation:
DG1 = 1.7
DG2 = 14.
m1 = 1.
m2 = 1.7
Y_N = -200
Y_I = -120
Y_D = -10
Pt = 80e-6

# Then lets simulate what curve we would obtain with these values:
x = np.linspace(0.,10.,100)
m = models.ThreeStateDimericIEquilibrium()
y = m(x, DG1, m1, DG2, m2, Y_N, Y_I, Y_D, Pt)

#Then lets plot the simulation against the data we have:
plt.figure()
for i, p in enumerate(proteins):
    plt.plot(p.x, p.y, 'ko')
plt.plot(x,y,'r-')
plt.show()
print y[0]
```

---

#### End of this Notebook.

---


# 第二十讲：策略搜索算法

回忆[上一讲](chapter19.ipynb)的内容，实际上上一讲的例子就是一个部分可观测MDP的特例。

在原LQR问题中有模型$s_{t+1}=As_t+Ba_t+w_t$，我们改变了条件，假设只能观测到$y_t=Cs_t+v_t$（不能直接观测到状态，只能观测到某些关于状态的函数，并需要根据这些观测值对最优动作作出判断）。

在状态完全可观测的情形下，我们可以根据$a_t=L_ts_t$选择动作；而在部分可观测的例子中，我们会使用$s_{t\mid t}$估计$s_t$，然后使用Kalman滤波的思想$s_{t\mid y_0,\cdots,y_t}\sim\mathcal N\left(s_{t\mid t},\varSigma_{t\mid t}\right)$计算这个估计$s_{t\mid t}$。

最后，我们使用$a_t=L_ts_{t\mid t}$选择最优动作。事实上这个算法让我们能够在不完全清楚系统每一个状态的前提下选择尽可能好的策略，而在一个部分可观测MDP中求最优策略是一个NP困难问题。

### 10.3 部分可观测MDP（POMDP: Partial Observed MDP）

我们现在给出POMDP的正式描述——七元组$(S,A,Y,\{P_{sa}\},\{O_s\},T,R)$：
* $Y$是可观测到的值的集合；
* $O$是可观测到的值的分布；

  也就是在每个$s_t$下都能够观测到某个服从$y_t\sim O_{s_t}$的随机变量。上一讲中我们介绍的在线性动力学系统中的特例可以使用Kalman过滤算法，从观测值中估计状态，再通过状态计算最优策略。然而这个算法仅对上一讲那个的特例（上一讲的例子也是POMDP的特例）有效，不过这个算法对普通的POMDP问题也有一定的效果，只是对该问题这个算法不一定是最优的算法。

## 12. 策略搜索（Policy Search）

再来介绍一个不同的强化学习算法——策略搜索算法，看名字就大概可以猜出，它可以被用于完全可观测MDP（Fully Observed MDP）或部分可观测MDP的最优策略求解。下面我们先来介绍策略搜索算法在完全可观测MDP中的应用。（在后面我们会介绍如何将策略搜索应用在POMDP上，但这并不能保证算法会找到全局最优解，策略搜索通常在POMDP中找到的是局部最优策略。解决POMDP是异常困难的，而策略搜索算法在实践中被证明是最好的POMDP解决算法之一。）

* 首先定义$\varPi$为策略的集合，而我们的算法就是在$\varPi$中搜索$\pi\in\varPi$。可以类比前面在监督学习算法中的概念——从假设类$\mathcal H$中搜索最优假设$h$（参见[第九讲](chapter09.ipynb)）。

  在之前的关于MDP的算法中，我们都是通过解出最优价值函数$V^*$进而得到最优策略$\pi^*$的。而在策略搜索算法中——有时被称作直接搜索算法——我们会“直接”去寻找最优策略（不再通过中间的步骤——计算最优价值函数）。

* 接下来再定义随机策略——是一个函数$\pi:S\times A\to\mathbb R$，$\pi(s,a)$表示在状态$s$执行动作$a$的概率。从定义就可以知道，函数$\pi$实际上是事件“在状态$s$执行动作$a$的概率”的概率密度函数，满足$\displaystyle\sum_a\pi(s,a)=1,\pi(s,a)\geq0$。换句话说，对于每一个状态，函数$\pi$都确定了一个在$a$上的分布。

  举个例子，假设我们要执行随机的策略$\pi$，而在当前的MDP中有三个动作$a_t\in\{a_1,a_2,a_3\}$，那么我们执行动作$a_1$的概率就是$\pi(s,a_1)$、执行动作$a_2$的概率就是$\pi(s,a_2)$、执行动作$a_3$的概率就是$\pi(s,a_3)$。这个过程就是执行一个随机策略。

下面继续使用倒置钟摆的例子来讲解策略搜索算法，我们定义$\phi$为钟摆偏离竖直位的角度，$a_1$是向右的加速度，$a_2$是向左的加速度。再找一个奖励函数，杆不论倒向那一边都对模型做出惩罚。提出一类随机策略意味着提出一类函数，这类函数被用来估计在状态$s_t$下将要执行的行动$a_t$。而现在，我们选择逻辑函数作为我们的策略，因为它是我们经常使用的一个很方便的函数：
$$
\begin{align}
\pi_\theta(s,a_1)&=\frac{1}{1+e^{-\theta^Ts}}\\
\pi_\theta(s,a_2)&=1-\frac{1}{1+e^{-\theta^Ts}}
\end{align}
$$
因为我们不是选择$a_1$就是选择$a_2$，所以选择它们的概率之和为$1$。举个例子解释这个选择的合理性，假设我们的状态向量为$s=\begin{bmatrix}1\\x\\\dot x\\\phi\\\dot\phi\end{bmatrix}$（这里添加了$1$作为截距项给逻辑回归一个额外的特征值），如果我们选择$\theta=\begin{bmatrix}0\\0\\0\\1\\0\end{bmatrix}$，则有$P\displaystyle\left(a=\textrm{"right"}\right)=\frac{1}{1+e^{-\theta^Ts}}=\frac{1}{1+e^{-\phi}}$，这说明“向右加速”的概率只取决于“杆的倾角$\phi$”，所以当的杆向右偏的时候小车就会尝试向右加速以接住杆。当然，这个例子里的参数选择并不是最优的，因为它忽略的别的参数。现在，我们的目标就是调整$\theta$，使得当执行$\pi_\theta$时能够让杆尽可能的保持竖直，也就是说，我们的目标是选择一个参数$\theta$使得执行$\pi_\theta$时的预期总收益最大化。再举个例子，如果有$d$个离散的动作，那么我们也可以用[第四讲](chapter04.ipynb)提到的softmax回归$\displaystyle\theta_1,\cdots,\theta_d;\ \pi_{\theta_i}(s,a_i)=\frac{e^{\theta_i^Ts}}{\sum_je^{\theta_j^Ts}}$，策略类型的选择是可以自己确定的，线性函数、使用二次特征值的线性函数、监督学习算法等等。

### 12.1 增强算法（Reinforce algorithm）

设$s_0$是一个固定的初始化状态（即从一个确定的初始状态分布中抽样得到），我们的目标是：

$$
\begin{align}
\max\ &\mathrm E[R(s_0,a_0)+\cdots+R(s_T,a_T]\\
&=\sum_{(s_0,a_0),\cdots,(s_T,a_T)}P(s_0a_0\cdots s_Ta_T)[R(s_0,a_0)+\cdots+R(s_T,a_T)]\tag{1}\\
&=\sum_{(s_0,a_0),\cdots,(s_T,a_T)}P(s_0)\pi_\theta(s_0,a_0)P_{s_0a_0}(s_1)\pi_\theta(s_1,a_1)P_{s_1a_1}(s_2)\cdots P_{s_{T-1}\ a_{T-1}}\ (s_T)\pi_\theta(s_T,a_T)\cdot\underbrace{[R(s_0,a_0)+\cdots+R(s_T,a_T)]}_{\textrm{payoff}}\tag{2}
\end{align}
$$

在后面我们就将$R(s_0,a_0)+\cdots+R(s_T,a_T)$部分简称为收益（payoff），先写出算法步骤：

* 重复：`{`
  * 抽样：$s_0,a_0,s_1,a_1,\cdots,s_T,a_t$
  * 计算收益：$R(s_0,a_0)+\cdots+R(s_T,a_T)$
  * 更新参数：$\displaystyle\theta:=\theta+\alpha\left[\frac{\nabla_\theta\pi_\theta(s_0,a_0)}{\pi_\theta(s_0,a_0)}+\cdots+\frac{\nabla_\theta\pi_\theta(s_T,a_T)}{\pi_\theta(s_T,a_T)}\right]\cdot[R(s_0,a_0)+\cdots+R(s_T,a_T)]$

  `}`

算法的第一步就是随机抽样得到一个状态-动作序列，也就是在MDP中执行当前的随机策略——从某个$s_0$开始，根据随机策略选择一个动作$a_0$，然后看状态转换概率给出的下一个状态，继续重复执行策略。第二步就是计算收益。第三步更新参数。

由于这个算法执行的是随机策略，所以我们想知道它为什么这样更新参数。我们可以计算更新参数步骤的期望，只要更新参数后带来的收益呈增长趋势，我们就对算法是满意的。实际上这个算法类似随机梯度上升，它将在局部最优解附近“游荡”，但总趋势是向局部最优解前进。

回顾上面的$(2)$式，它表示概率与收益之积在所有状态上的和，我们现在要做的就是对$(2)$式求关于$\theta$的导数，因为我们想对这个式子做梯度上升：
$$
\begin{align}
\nabla_\theta\mathrm E\left[R(s_0,a_0)+\cdots+R(s_T,a_T)\right]
=\sum_{(s_0,a_0),\cdots,(s_T,a_T)}
&\Bigg[\ P(s_0)\underline{(\nabla_\theta\pi_\theta(s_0,a_0))}P_{s_0a_0}(s_1)\pi_\theta(s_1,a_1)P_{s_1a_1}(s_2)\cdots P_{s_{T-1}\ a_{T-1}}\ (s_T)\pi_\theta(s_T,a_T)\\
&+P(s_0)\pi_\theta(s_0,a_0)P_{s_0a_0}(s_1)\underline{(\nabla_\theta\pi_\theta(s_1,a_1))}P_{s_1a_1}(s_2)\cdots P_{s_{T-1}\ a_{T-1}}\ (s_T)\pi_\theta(s_T,a_T)\\
&+\cdots\\
&+P(s_0)\pi_\theta(s_0,a_0)P_{s_0a_0}(s_1)\pi_\theta(s_1,a_1)P_{s_1a_1}(s_2)\cdots P_{s_{T-1}\ a_{T-1}}\ (s_T)\underline{(\nabla_\theta\pi_\theta(s_T,a_T))}\ \Bigg]\\
&\times[R(s_0,a_0)+\cdots+R(s_T,a_T)]\\
=\sum_{(s_0,a_0),\cdots,(s_T,a_T)}&P(s_0)\pi_\theta(s_0,a_0)P_{s_0a_0}(s_1)\pi_\theta(s_1,a_1)P_{s_1a_1}(s_2)\cdots P_{s_{T-1}\ a_{T-1}}\ (s_T)\pi_\theta(s_T,a_T)\\
&\times\left[\frac{\nabla_\theta\pi_\theta(s_0,a_0)}{\pi_\theta(s_0,a_0)}+\frac{\nabla_\theta\pi_\theta(s_1,a_1)}{\pi_\theta(s_1,a_1)}+\cdots+\frac{\nabla_\theta\pi_\theta(s_T,a_T)}{\pi_\theta(s_T,a_T)}\right]\\
&\times[R(s_0,a_0)+\cdots+R(s_T,a_T)]\\
=\sum_{(s_0,a_0),\cdots,(s_T,a_T)}&P(s_0a_0\cdots s_Ta_T)\cdot\left[\frac{\nabla_\theta\pi_\theta(s_0,a_0)}{\pi_\theta(s_0,a_0)}+\cdots+\frac{\nabla_\theta\pi_\theta(s_T,a_T)}{\pi_\theta(s_T,a_T)}\right]\\
&\times[R(s_0,a_0)+\cdots+R(s_T,a_T)]\\
\end{align}\\
\Downarrow\\
\nabla_\theta\mathrm E\left[R(s_0,a_0)+\cdots+R(s_T,a_T)\right]=\mathrm E\left[\left(\frac{\nabla_\theta\pi_\theta(s_0,a_0)}{\pi_\theta(s_0,a_0)}+\cdots+\frac{\nabla_\theta\pi_\theta(s_T,a_T)}{\pi_\theta(s_T,a_T)}\right)\cdot\left(R(s_0,a_0)+\cdots+R(s_T,a_T)\right)\right]
$$
第一步应用了几个相乘函数的求导法则$\displaystyle\frac{\mathrm d}{\mathrm d\theta}f(\theta)g(\theta)h(\theta)=f'(\theta)g(\theta)h(\theta)+f(\theta)g'(\theta)h(\theta)+f(\theta)g(\theta)h'(\theta)$。

但是，使用加强算法的时候，我们通常认为对于待解决的问题，存在一个简单的函数（如线性函数、逻辑函数等），描述了从状态空间到动作空间的映射。对于倒置钟摆这种简单的任务，事实可能就是这样的（杆向右偏时小车就向右加速接住杆）。实际上对于很多低级别的控制任务（如开车时右边有障碍就应该向左转向以避让），人类也是条件反射式的做出“本能”动作。对于这种较为简单的、极短时间内的“本能”判断，通常能够找到一个由简单函数组成的合理的策略类。

与其相反的是复杂任务，需要很长的多步推理的任务，比如象棋、围棋等。在这种活动中做出决策需要多步严密的因果推理，这是一种高级别的控制任务，所以就不能使用“本能”了。在这种任务中我们有时会用前面提到的价值函数的线性近似。另外，在POMDP中如果使用$\hat s$近似真实状态（比如在Kalman滤波中的$\hat s=s_{t\mid t}$），则我们仍然可以使用策略搜索算法$\displaystyle\pi_\theta(\hat s,a_t)=\frac{1}{1+e^{-\theta^T\hat s}}$。

最后，关于加强算法——这个算法在做上升时本质上是没有明确方向的（虽然其总体期望上是正确的），这可能会导致加强算法需要大量的迭代才到达到最优值附近；而且，在算法中，每次迭代都需要一次抽样，如果对于一个物理实体来说成本可能会很高，比如我们在研究控制一个机器人，那么这个机器人可能就需要做很多次动作（多次迭代，每次都需要抽样），所以这个算法通常运行在模拟器中。

### 12.2 Pegasus算法

在[第十七讲](chapter17.ipynb)我们提到模拟器的应用。模拟器可以接受状态$s_t$和动作$a_t$，输出下一步的状态$s_{t+1}$，$s_{t+1}$通常是一个从随机状态转换概率中抽样的随机状态，也就是说在模拟器中$s_{t+1}$是一个关于$s_t,a_t$的随机函数。比如我们在直升机的例子中，使用线性回归、局部加权回归等监督学习算法，就可以构建一个非线性动力学模型，在这个模型中，$s_{t+1}$就是一个关于$s_t,a_t$的随机函数。我们可以使用模拟器估计任意的状态-动作序列对应的预期总收益（将每个状态的奖励函数值相加即可）。

同样是在[第十七讲](chapter17.ipynb)我们定义了策略，它接受当前状态，输出当前状态下应执行的动作，那么结合模拟器，就可以估计任意策略的预期总收益了（选定初始状态$s_0$后，输入策略$\pi$得到$a_0$，再将$s_0,a_0$输入模拟器得到$s_1$；将$s_1$带入策略$\pi$得到$a_1$，再将$s_1,a_1$带入模拟器得到$s_2$……将所有状态带入奖励函数，求和即可估计出该策略的预期总收益）。那么，我们就可以尝试找到使这个由模拟器估计出的总收益最大的策略，也就是找到该策略对应的参数即可。从概念上讲，这是一个不错的思路，但是实际上几乎不可行：由于模拟器的随机性，即使我们对同一个策略做估计时，也会得到有细微的差别两个不同的结果，所以在策略空间中对策略进行估计时，如果策略空间维数较高，那么我们很难知道空间中策略对应的收益的分布趋势。

模拟器会根据输入的状态-动作输出一个带有机性的下一步状态，通常，能够做到这一点的模拟器都会调用一个随机数生成器。那么，为了降低使用模拟器对策略空间进行搜索的难度，我们可以在MDP中每一步调用模拟器时，固定随机数生成器得到的值（也就是让每一步的生成器只生成器只运行一次，然后就固定这个值），如果我们每次估计策略都使用同一组随机数序列，则模拟器就不再是随机的了，此时对同一个策略进行多次估计，得到的也将是相同的值（取消了随机数也就意味着确定了在策略空间上的收益函数，这极大程度上降低了最优策略的求解难度）。虽然我们现在通过模拟器只能得到的策略预期总收益的估计（因为固定了随机数，模拟器不再是原来通过试验数据拟合出来的系统了），但这个估计值离实际的预期总收益相乘不大，所以现在我们就可以使用诸如梯度上升等算法在策略空间中搜索最优策略了。

关于这个随机数，可以举个例子，在直升机模型中，假设随机数是对直升机在风场中的模拟，不同的随机数对应不同的风。在对风场的模拟中，我们不是对每一种模式的风都进行建模估计，而是采样不同模式的风，对它们做平均的估计。在模拟器中，我们对控制策略做收益估计，并不是只做一次，而是做很多次，然后取所有测试的平均收益。这就相当于在不同的风场中做了多次测试，然后得出控制策略在不同风场状况下的平均预期收益。

### 12.3 强化学习算法小结

我们一共介绍了两种强化学习算法，第一种方法是通过求解最优价值函数推导出最优策略，这种方法常被用于解决需要“深思熟虑”才能解决的问题，它涉及的问题一般都会有多步推导，且后面步骤的决策也会影响前面的步骤，常见于诸如俄罗斯方块、象棋等场景；第二种方法是通过策略搜索直接找到最优策略，这种方法通常被用于解决“条件反射”式的问题，比如直升机悬停控制、汽车障碍避让等场景。强化学习算法的核心是根据状态做出的决策序列，它的适用场景为“需要依次根据状态做出决策的问题，而且决策可能具有长期影响”。列出一些RL算法已经应用的场景：医疗决策——根据病人所处的状态选择不同的治疗方案；用于减少排队等待时间——其中包括流水线上的效率提升问题等；用于博弈论中某些场景——比如怎样大量抛售股票同时又将对市场的影响降到最低；也应用于运筹学——最大化工厂的生产效率同时降低成本……。


```python
import holoviews as hv
hv.extension('bokeh')
hv.opts.defaults(hv.opts.Curve(width=500),
                 hv.opts.Image(width=500, colorbar=True, cmap='Viridis'))
```

```python
import numpy as np
import scipy.signal
import scipy.fft
from IPython.display import Audio
```

# Sistemas para el procesamiento de señales

## Introducción

### Definición de sistema

Hasta ahora hemos realizado *análisis de señales*, es decir el estudio de las señales y sus propiedades en el dominio del tiempo y frecuencia

En esta unidad nos enfocaremos en el *procesamiento de señales*, es decir el diseño de **sistemas** que procesan una **señal de entrada** y producen una **señal de salida**

Usaremos

- $x[n]$ para denotar la señal (discreta) de entrada y $X[k]$ su espectro
- $y[n]$ para denotar la señal (discreta) de salida e $Y[k]$ su espectro

### Ejemplos de sistemas

Utilizando sistemas podemos modificar una señal para mejorar su calidad o remover efectos indeseados

- Un sistema para reducir el ruido de una señal de electroencefalograma (EEG)

- Un sistema para mejorar una imagen fuera de foco (sharpening)

- Un sistema para eliminar el eco de un audio

## Propiedades y ejemplos de sistemas

### Sistemas sin memoria

Diremos que un sistema $\Phi$ es un sistema **sin memoria** si

$$
y[n] = \Phi(x[n]),
$$

es decir que la salida del sistema en un instante $n$ dado depende solo de la entrada en ese mismo instante

Veamos algunos ejemplos

**Sistema atenuador/amplificador ideal**

$$
y[n] = A x[n],
$$

donde $A>0$ se llama *ganancia*

Este sistema puede atenuar la entrada si $0<A<1$ y amplificar si $A>1$

**Sistema saturador (clamp)**

$$
y[n] = \begin{cases} B &x[n] > B \\x[n] & x[n] \in [-B, B]\\ -B & x[n] < -B\end{cases}
$$

Este sistema limita los valores de la señal de entrada en un rango fijo

**Sistema rectificador**

$$
y[n] = | x[n] |
$$

Este sistema eliminar la parte negativa de la señal de entrada

### Sistema Lineal

Diremos que un sistema $\Phi$ es lineal si cumple con las siguientes propiedades

**Homogeneidad**

Un cambio en la amplitud de la entrada produce un cambio equivalente en la salida

$$
\Phi(cx[n]) = c \Phi(x[n]) = c y[n]
$$

**Aditividad**

Señales que se suman en la entrada producen señales que se suman en la salida

$$
\Phi(x_1[n] + x_2[n]) = \Phi(x_1[n]) + \Phi(x_2[n]) = y_1[n] + y_2[n]
$$

Es decir que las señales pasan por el sistema sin interactuar entre ellas

**Otras propiedades de los sistemas lineales**

Producto de las propiedades anteriores se tiene que una cascada de sistemas lineales forma un sistema lineal equivalente y además la cascada de sistemas es **conmutativa**, es decir que el orden de los sistemas en la cascada no altera el resultado final

La siguiente figura ejemplifica esta propiedad

Otra propiedad interesante de los sistemas lineales es que cumplen el **Principio de superposición:**

1. Si descomponemos una señal en $M$ componentes: $x[n] = x_1[n] + x_2[n] + \ldots +  x_M[n]$
1. Y aplicamos un **sistema lineal** a cada componente: $y_j[n] = \Phi(x_j[n])$
1. Podemos recuperar la salida total usando **aditividad**: $y_1[n] + y_2[n] + \ldots +  y_M[n] = y[n]$

La siguiente figura ejemplifica esta propiedad

### Sistemas con memoria

Un sistema $\Phi$ es un sistema con memoria si su salida actual depende sólo de la entrada actual, las entradas anteriores o las salidas anteriores

$$
\begin{align}
y[n] = \Phi(x[n], & x[n-1], x[n-2], \ldots, x[0], \\ \nonumber
& y[n-1], y[n-2], \ldots, y[0]) \nonumber
\end{align}
$$

esto también se conoce como **sistema causal**

Un **sistema con memoria no-causal** usa entradas futuras (es decir $x[n+1]$, $x[n+2]$, ...) y por ende solo se puede implementar de forma offline, es decir una vez que sea ha observado toda la señal

A continuación veremos algunos ejemplos de sistemas con memoria causales

**Sistema con un retardo (delay)**

Definido como

$$
y[n] = x[n-m],
$$

donde
- la salida depende solo de "una" entrada anterior
- el valor de m define que tan "antigua" es la entrada pasada

El *delay* o retarno no afecta la amplitud de los componentes frecuenciales de la señal pero si su fase, como muestra la siguiente figura

```python
n = np.arange(0, 200, step=1)
x = lambda m: np.sin(2.0*np.pi*0.05*(n-m))
f = scipy.fft.rfftfreq(d=1, n=len(n))

p = []
for m in [0, 4, 8]:
    x_delayed = x(m)
    p.append(hv.Curve((n, x_delayed), 'Tiempo [s]', 'Señal'))
    X = scipy.fft.rfft(x_delayed)
    Xm = np.absolute(X)
    p.append(hv.Curve((f, Xm), 'Frecuencia [Hz]', 'Espectro de amplitud'))
    Xp = np.angle(X)
    p.append(hv.Curve((f, Xm*Xp/np.amax(Xm)), 'Frecuencia [Hz]', 'Espectro de fase'))
```

```python
hv.Layout(p).cols(3).opts(hv.opts.Curve(width=250, height=200))
```

**Sistema reverberador o eco**

Definido como

$$
y[n] = x[n] + A x[n-m],
$$

donde
- la salida depende de una entrada "pasada" y la entrada actual
- la ganancia controla si el "eco" se atenua o amplifica

Al contrario del sistema anterior, el eco si puede modificar el espectro de amplitud.

Notemos el efecto de interferencia constructiva y destructiva al modificar el retardo, como muestra la siguiente animación

```python
n = np.arange(0, 200, step=1)
x = lambda m, A=1: A*np.sin(2.0*np.pi*0.05*(n-m))
f = scipy.fft.rfftfreq(d=1, n=len(n))

buffer = {}
for m in range(0, 40, 2):
    xm = x(m)
    buffer[m] = (xm, np.abs(scipy.fft.rfft(x(0) + xm)))
```

```python
hMap1 = hv.HoloMap(kdims='m')
hMap2 = hv.HoloMap(kdims='m')
hMap3 = hv.HoloMap(kdims='m')

for m, (xm, X) in buffer.items():
    hMap1[m] = hv.Curve((n, xm), 'Tiempo', 'x', label='Ax[n-m]')
    hMap2[m] = hv.Curve((n, x(0) + xm), 'Tiempo', 'y')
    hMap3[m] = hv.Curve((f, X), 'Frecuencia', 'Espectro')

p_clean = hv.Curve((n, x(0)), 'Tiempo', 'x', label='x[n]').opts(width=500, height=200)
plot = (p_clean * hMap1 + hMap2 + hMap3).cols(1).opts(hv.opts.Curve(height=200),
                                               hv.opts.Overlay(legend_position='top_right'))

hv.output(plot, holomap='gif', fps=5)
```

El siguiente video muestra un experimento que ejemplifica la interferencia destructiva en una onda mecánica: https://www.youtube.com/watch?v=IU8xeJlJ0mk

**Sistemas con múltiples ecos**

Pueden combinarse más retardos para hacer un sistema reverberante más complejo

Por ejemplo el siguiente sistema

$$
y[n] = x[n] + A_1 x[n-m_1] + A_2 x[n-m_2] + A_3 x[n-m_3] + \ldots,
$$

que lo implementamos como

```python
Fs = 44100;
n = np.arange(0, 4, step=1.0/Fs)
x = lambda m: np.sin(2.0*np.pi*880*(n-m))*np.exp(-(n-m)**2/0.5**2)*np.heaviside(n-m, 0)
y = x(0) + 0.5*x(1.) + 0.25*x(2.) + 0.125*x(3.)
```

Da como resultado gráfico lo siguiente:

```python
hv.Curve((n, y), 'Tiempo [s]', 'Señal con eco')
```

y el resultado sonoro es:

```python
Audio(y, rate=Fs, normalize=False)
```

## Sistema de respuesta finita al impulso (FIR)

Generalizando el ejemplo de sistema lineal reverberante a $L$ retardos llegamos a

$$
\begin{align}
y[n] &= h[0] x[n] + h[1] x[n-1] + h[2] x[n-2] + \ldots + h[L] x[n-L] \nonumber \\
&= \sum_{j=0}^{L} h[j] x[n-j] \nonumber \\
&= (h* x)[n] \nonumber
\end{align}
$$

que se puede modelar como una convolución discreta entre $h$ y $x$

Este sistema se conoce como

- Sistema FIR (finite impulse response)
- Sistema MA (moving average)
- Sistema todo-zeros

y es de orden L (posee L+1 coeficientes)

**Intepretación como media movil (MA)**

El sistema FIR es equivalente a una media movil ponderada que se aplica sobre la entrada, donde los coeficientes del sistema son los ponderadores

Por ejemplo sea un sistema de 3 coeficientes $h[0]=a$, $h[1]=b$ y $h[2]=c$

$$
\begin{align}
y[n] = (h*x)[n] &= \sum_{j=0}^{2} h[j] x[n-j] \nonumber \\
&= a x[n] + b x[n-1] + c x[n-2] \nonumber
\end{align}
$$

donde cada salida se calcula a partir de

$$
\overbrace{x[0], x[1], x[2]}^{y[2]} , x[3], x[4], \ldots
$$
$$
x[0], \overbrace{x[1], x[2] , x[3]}^{y[3]}, x[4], \ldots
$$
$$
x[0], x[1], \overbrace{x[2] , x[3], x[4]}^{y[4]}, \ldots
$$

Un detalle es que para obtener el valor de $y[0]$ e $y[1]$ se deben establecer "condiciones de borde", como por ejemplo $x[-2] = x[-1]= 0$

A continuación veremos algunos ejemplos de aplicaciones usando filtros FIR sencillos

### Eliminando ruido blanco aditivo

En ciertos problemas la señal que observamos $x[n]$ no es exactamente la señal que deseamos $s[n]$, sino una versión corrupta de la misma.

Un ejemplo muy típico es cuando la señal que deseamos estudiar debe ser transmitida desde su lugar de origen hasta nosotros. El canal suele agregar ruido a la señal y adicionalmente el ruido aumenta mientras más largo sea el recorrido de nuestra señal.

Un tipo particular de corrupción es el siguiente

$$
x[n] = s[n] + \epsilon[n], \quad \epsilon[n] \sim \mathcal{N}(0,\sigma^2)
$$

es decir una corrupción con ruido blanco aditivo y gaussiano, muy común en las transmisiones satelitales y espaciales

Consideremos a modo de ejemplo la siguiente señal observada que fue contaminada por ruido blanco aditivo:

```python
np.random.seed(0)
n = np.arange(0, 200, step=1)
C = 5*np.exp(-0.5*(n[:, np.newaxis] - n[:, np.newaxis].T)**2/10**2)
# Señal limpia (lo que no conocemos)
s = np.random.multivariate_normal(np.zeros_like(n), C)/8
# Datos: Señal + ruido (lo que medimos)
x = s + np.random.randn(len(n))/4

p2 = hv.Curve((n, s), 'Tiempo', 'Señal', label='Deseada (s)').opts(color='k')
p1 = hv.Scatter((n, x), 'Tiempo', 'Señal', label='Observada (x)').opts(width=500, height=250)
hv.Overlay([p1, p2]).opts(legend_position='top')
```

Podemos usar un sistema FIR de tipo promediador para "suavizar la contaminación" e intentar recuperar la señal deseada

Sea un sistema FIR con $L$ coeficientes idénticos e iguales a $1/L$

```python
L = 10
h = np.ones(shape=(L,))/L
y = scipy.signal.convolve(x, h, mode='same', method='auto')
```

La siguiente gráfica interactiva muestra el resultado de convolucionar este sistema con los datos contaminados

```python
hMap1 = hv.HoloMap(kdims='Instante')
hMap2 = hv.HoloMap(kdims='Instante')

for m in range(0, len(n)-L, 5):
    c = np.zeros_like(n, dtype=np.float64);
    c[m:m+L] = h
    hMap1[m] = hv.Curve((n, c), 'Tiempo', 'Entrada', label='h').opts(color='r')
    hMap2[m] = hv.Curve((n[:m], y[:m]), 'Tiempo', 'Salida', label='y')

p_obs = hv.Scatter((n, x), 'Tiempo', 'Entrada', label='x')
p_clean = hv.Curve((n, s), 'Tiempo', 'Salida', label='s').opts(color='k', height=200)
(hMap1 * p_obs + hMap2 * p_clean).cols(1).opts(hv.opts.Curve(height=200),
                                               hv.opts.Overlay(legend_position='top_left'))
```

:::{note}

Este filtro promedia los datos vecinos resultando una versión suavizada de los mismos. Esta versión suavizada se aproxima a la "señal limpia" que está escondida en el ruido.

:::

En general, mientras más "largo" sea el filtro mayor será el efecto de suavizado

### Encontrando cambios en una señal

Sea la siguiente señal escalonada

```python
n = np.arange(0, 100, step=1)
x = np.zeros_like(n, dtype=np.float64)
x[20:] += 1.
x[40:] += 1.
x[80:] -= 1.

hv.Curve((n, x), 'Tiempo', 'x').opts(width=500, height=200)
```

Si nos interesa encontrar cambios en la señal podemos usar un sistema de tipo diferenciador

$$
y[n] = (h * x)[n] = \frac{1}{2}x[n] - \frac{1}{2} x[n-1]
$$

```python
h = np.array([0.5, -0.5])
y = scipy.signal.convolve(x, h, mode='same', method='auto')
```

La siguiente gráfica interactiva muestra el resultado de convolucionar este sistema con la señal escalonada

```python
hMap1 = hv.HoloMap(kdims='Instante')
hMap2 = hv.HoloMap(kdims='Instante')

for m in range(0, len(n)-len(h), 5):
    c = np.zeros_like(n, dtype=np.float64);
    c[m:m+len(h)] = h
    hMap1[m] = hv.Curve((n, c), 'Tiempo', label='h')
    hMap2[m] = hv.Curve((n[:m+1], y[:m+1]), 'Tiempo', 'Salida')

p_obs = hv.Curve((n, x), 'Tiempo', 'Entrada', label='x')
(p_obs * hMap1 + hMap2).cols(1).opts(hv.opts.Curve(height=200),
                                     hv.opts.Overlay(legend_position='top_left'))
```

:::{note}

Los pulsos en la convolución (salida) están asociados a un cambio (ascenso o descenso) en la señal original

:::

En un caso más general, este filtro nos da información de la derivada de la señal, es decir de su velocidad o tasa de cambio

### Remover una tendencia

En ciertas situaciones la señal que nos interesa (señal deseada) puede aparecer combinada con otras señales (interferencia)

Considere el siguiente ejemplo:

```python
np.random.seed(0);
n = np.arange(0, 150, step=1)
C = np.exp(-0.5*(n[:, np.newaxis] - n[:, np.newaxis].T)**2/30**2)
x_tendencia = 3*np.random.multivariate_normal(np.zeros_like(n), C)+2.5
x_deseada = np.sin(2.0*np.pi*0.1*n)
x = x_deseada + x_tendencia

p3=hv.Curve((n, x_deseada), 'Tiempo', 'Señal', label='Deseada (s)').opts(color='k', alpha=0.75)
p2=hv.Curve((n, x_tendencia), 'Tiempo', 'Señal', label='Tendencia').opts(alpha=0.75)
p1=hv.Curve((n, x), 'Tiempo', 'Señal', label='Observada (x)').opts(height=250)
hv.Overlay([p1,p2,p3]).opts(legend_position='bottom_right')
```

Supongamos que nuestro sensor retorna la señal observada (azul) pero que lo que en realidad necesitamos es la señal deseada (negra)

El filtro que se muestra a continuación es capaz de separar la señal deseada (negro) de la tendencia (rojo) a partir de la señal observada (azul). En próximas lecciones veremos como diseñar este tipo de filtros

```python
h = np.array([ 0.00099858,  0.00172998,  0.00288273,  0.00437671,  0.00567733,  0.00580422,
              0.00350188, -0.00245489, -0.01289227, -0.02790429, -0.04665419, -0.06738128,
              -0.08763411, -0.10469179, -0.11608356,  0.88063544, -0.11608356, -0.10469179,
              -0.08763411, -0.06738128, -0.04665419, -0.02790429, -0.01289227, -0.00245489,
              0.00350188,  0.00580422,  0.00567733,  0.00437671,  0.00288273,  0.00172998,
              0.00099858])

hv.Curve((h), 'Tiempo', 'Filtro (h)').opts(height=200)
```

```python
y = scipy.signal.convolve(x, h, mode='same', method='auto')
```

```python
hMap1 = hv.HoloMap(kdims='Instante')
hMap2 = hv.HoloMap(kdims='Instante')

for m in range(0, len(n)-len(h), 5):
    c = np.zeros_like(n, dtype=np.float64);
    c[m:m+len(h)] = h
    hMap1[m] = hv.Curve((n, c), 'Tiempo', 'Entrada', label='h')
    hMap2[m] = hv.Curve((n[:m], y[:m]), 'Tiempo', 'Salida', label='y').opts(ylim=(-1.2, 1.2))

p_obs = hv.Curve((n, x), 'Tiempo', 'Señal', label='x')
p_target = hv.Curve((n, x_deseada), 'Tiempo', 'Señal', label='s').opts(color='k', alpha=.75, width=4)
(p_obs * hMap1 + hMap2 *p_target).cols(1).opts(hv.opts.Curve(height=200))
```

## Convolución con scipy

Podemos convolucionar una señal en Python usando

```python
scipy.signal.convolve(in1, # Señal de entrada
                      in2, # Coeficientes del sistema
                      mode='full',
                      method='auto'
                     )
```

donde el argumento `method` puede ser

- `direct`: Realiza la convolución en el dominio del tiempo
- `fft`: Realiza la convolución multiplicando los espectros
- `auto`: Se decide automaticamente en base al largo de las señales

y el argumento `mode` indica donde se hace la convolución

Para ejemplificar la influencia de este argumento consideremos una señal $x=[a,b,c]$ y un sistema $h=[d, e]$

- Si uso `mode=valid` el resultado será $y=[ad+be, bd+ce]$
- Si uso `mode=same` el resultado será $y=[ae, ad+be, bd+ce]$, es decir se agregan ceros al principio de $x$ tal que $y$ sea del mismo largo que $x$
- Si uso `mode=full` el resultado será $y=[ae, ad+be, bd+ce, cd]$, es decir se agregan ceros al principio y al final de $x$

### Eliminando ruido versión 2.0

Considerando la misma señal contaminada con ruido blanco que vimos anteriormente utilizaremos `scipy.signal.convolve` para convolucionar con un filtro que suavice el ruido.

Probaremos dos sistemas FIR

- coeficientes idénticos e iguales a $1/L$, es decir una ventana rectangular
- coeficientes que decaen suavemente a cero, como por ejemplo una ventana de Hamming

con distintos valores de $L$ (largo del filtro)

En este caso los valores los obtenemos usando la función de scipy

```python

scipy.signal.get_window(window, # String con el nombre de la ventana
                        Nx, # Entero con el largo de la ventana
                        ...)
```

La ventana rectangular se llama `boxcar` mientras que la de Hamming se llama `hamming`. En la documentación de la función se puede revisar otras ventanas disponibles

```python
# La señal de ejemplo de la sección 7.3.1
np.random.seed(0)
n = np.arange(0, 200, step=1)
C = 5*np.exp(-0.5*(n[:, np.newaxis] - n[:, np.newaxis].T)**2/10**2)
s = np.random.multivariate_normal(np.zeros_like(n), C)/8
x = s + np.random.randn(len(n))/4

# La filtramos con distintas funciones y L para luego visualizar el resultado
filters = {}
results = {}
for L in [5, 10, 20, 30, 40]:
    for filter_name in ['boxcar', 'hamming']:
        h = scipy.signal.get_window(filter_name, L)
        h = h/np.sum(h)
        if not L in filters:
            filters[L] = {}
            results[L] = {}
        filters[L][filter_name] = h
        results[L][filter_name] = scipy.signal.convolve(x, h, mode='same', method='auto')
```

```python
hMap1 = hv.HoloMap(kdims='L')
hMap2 = hv.HoloMap(kdims='L')

for L, curves in filters.items():
    p = []
    for filter_name, h in curves.items():
        p.append(hv.Curve((range(L), h), 'Largo del filtro (L)', 'Filtro (h)', label=filter_name))
    hMap1[L] = hv.Overlay(p)

for L, curves in results.items():
    p = []
    for filter_name, y in curves.items():
        p.append(hv.Curve((n, y), 'Tiempo', 'Señal', label=filter_name).opts(line_width=2, alpha=0.75))
    hMap2[L] = hv.Overlay(p)

p_clean = hv.Curve((n, s), 'Tiempo', 'Señal', label='Deseada (s)').opts(color='k')
p_data = hv.Scatter((n, x), 'Tiempo', 'Datos (x)').opts(width=500, height=200)
(p_data + hMap1 + hMap2 * p_clean).cols(1).opts(hv.opts.Curve(height=250),
                                                hv.opts.Overlay(legend_position='top'))
```

:::{note}

Mientras más larga es la ventana (L) mayor es el suavizado. Adicionalmente la ventana de Hamming produce un filtrado más suave que la rectangular

:::

En la lección siguiente veremos como diseñar un filtro, es decir como calcular los valores de `h` para resolver una tarea en particular

```python

```