I am working on the file "Sessions/Session13/Day2/01-Introduction-to-GPs.ipynb".
The first task is:
Even though we derived the expression for the probability of a multidimensional Gaussian assuming no covariance between the different dimensions, the expression in Exercise 1 is general. In the most general case, the covariance matrix is allowed to have off-diagonal elements:
$$
\mathbf{\Sigma} =
\begin{pmatrix}
 \mathrm{Var(x_0)} & \mathrm{Cov(x_0, x_1)} & \cdots & \mathrm{Cov(x_0, x_N)} \ \
 \mathrm{Cov(x_1, x_0)} & \mathrm{Var(x_1)} & \cdots & \mathrm{Cov(x_1, x_N)} \ \
 \vdots & \vdots & \ddots & \vdots \ \
 \mathrm{Cov(x_N, x_0)} & \mathrm{Cov(x_N, x_1)} & \cdots & \mathrm{Var(x_N)}
\end{pmatrix}
$$
Here, $\mathrm{Var(x_i)}$ is the variance, or square of the standard deviation, along the $i^\mathrm{th}$ dimension, and $\mathrm{Cov(x_i, x_j)}$ is the covariance $-$ a measure of how two variables vary jointly $-$ between the $i^\mathrm{th}$ and $j^\mathrm{th}$ dimensions.
It's hard to picture a multivariate Gaussian, but it helps to imagine what the contours of equal probability (or equipotential surfaces) look like. For a two dimensional standard normal with no covariance ($\mathbf{\Sigma} = \mathbf{I}$), these are just circles. As I add to the off-diagonal terms of the matrix, I introduce a preferred direction in the space, since now the dimensions are correlated. The circle becomes an ellipse whose elongation increases with the covariance. In the general case of a multidimensional Gaussian, the contours form a multidimensional ellipsoid, elongated by different amounts along different dimensions.
So. How do we draw samples from a multivariate Gaussian given $\mathbf{\Sigma}$? We use np.random.multivariate_normal.
Here's a function draw_from_gaussian(mu, S, ndraws=1) that returns ndraws samples from a multivariate Gaussian with mean mu and covariance matrix S. The shape of the output is (ndraws, ndim) where ndim is the dimension of the Gaussian:
Can you write Python code for it?

def draw_from_gaussian(mu, S, ndraws=1):
    """
    Generate samples from a multivariate gaussian
    specified by covariance ``S`` and mean ``mu``.

    """
    return np.random.multivariate_normal(mu, S, (ndraws,))


I am working on the file "Sessions/Session13/Day2/01-Introduction-to-GPs.ipynb".
The first task is:
Let's also plot the corner plot to see how well we inferred the slope and the intercept:
Can you write Python code for it?

import corner
labels = ["slope", "intercept"]
truths = [m_true, b_true]
corner.corner(sampler.flatchain, truths=truths, labels=labels);


I am working on the file "Sessions/Session13/Day2/01-Introduction-to-GPs.ipynb".
The first task is:
Once the chain finishes running, we're going to plot several draws from it on top of the data. We'll also plot the true line that generated the dataset (given by the variables m_true and b_true).
Can you write Python code for it?

# Plot the data
plt.errorbar(t, y, yerr=sigma, fmt=".k", capsize=0)

# The positions where the prediction should be computed
x = np.linspace(0, 10, 500)

# Plot 24 posterior samples
samples = sampler.flatchain
for s in samples[np.random.randint(len(samples), size=24)]:
    m, b = s
    model = m * x + b
    plt.plot(x, model, color="#4682b4", alpha=0.3)

# Plot the truth
plt.plot(x, m_true * x + b_true, "C1", label="truth")

plt.ylabel("data")
plt.xlabel("time")
plt.title("fit assuming uncorrelated noise");


I am working on the file "Sessions/Session13/Day2/02-Fast-GPs.ipynb".
The first task is:

Exercise (the one and only)

Let's use what we've learned about GPs in a real application: fitting an exoplanet transit model in the presence of correlated noise.
Here is a (fictitious) light curve for a star with a transiting planet:
Can you write Python code for it?

import matplotlib.pyplot as plt

t, y, yerr = np.loadtxt("data/sample_transit.txt", unpack=True)
plt.errorbar(t, y, yerr=yerr, fmt=".k", capsize=0)
plt.xlabel("time")
plt.ylabel("relative flux");


I am working on the file "Sessions/Session13/Day2/02-Fast-GPs.ipynb".
The first task is:
Benchmarking GP codes
Implemented the right way, GPs can be super fast! Let's compare the time it takes to evaluate our GP likelihood and the time it takes to evaluate the likelihood computed with the snazzy george and celerite packages. We'll learn how to use both along the way. Let's create a large, fake dataset for these tests:
Can you write Python code for it?

import numpy as np

np.random.seed(0)
t = np.linspace(0, 10, 10000)
y = np.random.randn(10000)
sigma = np.ones(10000)


I am working on the file "Sessions/Session13/Day2/02-Fast-GPs.ipynb".
The first task is:
Our GP
Can you write Python code for it?

def ExpSquaredCovariance(t, A=1.0, l=1.0, tprime=None):
    """
    Return the ``N x M`` exponential squared
    covariance matrix.

    """
    if tprime is None:
        tprime = t
    TPrime, T = np.meshgrid(tprime, t)
    return A ** 2 * np.exp(-0.5 * (T - TPrime) ** 2 / l ** 2)

def ln_gp_likelihood(t, y, sigma=0, A=1.0, l=1.0):
    """
    Return the log of the GP likelihood for a datatset y(t)
    with uncertainties sigma, modeled with a Squared Exponential
    Kernel with amplitude A and lengthscale l.

    """
    # The covariance and its determinant
    npts = len(t)
    K = ExpSquaredCovariance(t, A=A, l=l) + sigma ** 2 * np.eye(npts)

    # The log marginal likelihood
    log_like = -0.5 * np.dot(y.T, np.linalg.solve(K, y))
    log_like -= 0.5 * np.linalg.slogdet(K)[1]
    log_like -= 0.5 * npts * np.log(2 * np.pi)

    return log_like


I am working on the file "Sessions/Session13/Day2/02-Fast-GPs.ipynb".
The first task is:
Time to evaluate the GP likelihood:
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day2/02-Fast-GPs.ipynb".
The first task is:
celerite
The george package is super useful for GP modeling, and I recommend you read over the docs and examples. It implements several different kernels that come in handy in different situations, and it has support for multi-dimensional GPs. But if all you care about are GPs in one dimension (in this case, we're only doing GPs in the time domain, so we're good), then celerite is what it's all about:
bash
pip install celerite
Check out the docs here, as well as several tutorials. There is also a paper that discusses the math behind celerite. The basic idea is that for certain families of kernels, there exist extremely efficient methods of factorizing the covariance matrices. Whereas GP fitting typically scales with the number of datapoints $N$ as $N^3$, celerite is able to do everything in order $N$ (!!!) This is a huge advantage, especially for datasets with tens or hundreds of thousands of data points. Using george or any homebuilt GP model for datasets larger than about 10,000 points is simply intractable, but with celerite you can do it in a breeze.
Next we repeat the timing tests, but this time using celerite. Note that the Exponential Squared Kernel is not available in celerite, because it doesn't have the special form needed to make its factorization fast. Instead, we'll use the Matern 3/2 kernel, which is qualitatively similar and can be approximated quite well in terms of the celerite basis functions:
python
kernel = celerite.terms.Matern32Term(np.log(1), np.log(1))
Note that celerite accepts the log of the amplitude and the log of the timescale. Other than this, we can compute the likelihood using the same syntax as george.
How much faster did it run? Is the value of the likelihood different from what you found above? Why?
Can you write Python code for it?

import celerite
from celerite import terms

%%time
kernel = terms.Matern32Term(np.log(1), np.log(1))
gp = celerite.GP(kernel)
gp.compute(t, sigma)

%%time
gp.log_likelihood(y)


I am working on the file "Sessions/Session13/Day2/02-Fast-GPs.ipynb".
The first task is:
george also offers a fancy GP solver called the HODLR solver, which makes some approximations that dramatically speed up the matrix algebra. Let's instantiate the GP object again by passing the keyword solver=george.HODLRSolver and re-compute the log likelihood. How long did that take? Did we get the same value for the log likelihood?
Can you write Python code for it?

%%time
gp = george.GP(kernel, solver=george.HODLRSolver)
gp.compute(t, sigma)

%%time
gp.log_likelihood(y)


I am working on the file "Sessions/Session13/Day2/answers/02-Fast-GPs.ipynb".
The first task is:
Time to evaluate the GP likelihood:
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day2/answers/02-Fast-GPs.ipynb".
The first task is:
celerite
The george package is super useful for GP modeling, and I recommend you read over the docs and examples. It implements several different kernels that come in handy in different situations, and it has support for multi-dimensional GPs. But if all you care about are GPs in one dimension (in this case, we're only doing GPs in the time domain, so we're good), then celerite is what it's all about:
bash
pip install celerite
Check out the docs here, as well as several tutorials. There is also a paper that discusses the math behind celerite. The basic idea is that for certain families of kernels, there exist extremely efficient methods of factorizing the covariance matrices. Whereas GP fitting typically scales with the number of datapoints $N$ as $N^3$, celerite is able to do everything in order $N$ (!!!) This is a huge advantage, especially for datasets with tens or hundreds of thousands of data points. Using george or any homebuilt GP model for datasets larger than about 10,000 points is simply intractable, but with celerite you can do it in a breeze.
Next we repeat the timing tests, but this time using celerite. Note that the Exponential Squared Kernel is not available in celerite, because it doesn't have the special form needed to make its factorization fast. Instead, we'll use the Matern 3/2 kernel, which is qualitatively similar and can be approximated quite well in terms of the celerite basis functions:
python
kernel = celerite.terms.Matern32Term(np.log(1), np.log(1))
Note that celerite accepts the log of the amplitude and the log of the timescale. Other than this, we can compute the likelihood using the same syntax as george.
How much faster did it run? Is the value of the likelihood different from what you found above? Why?
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day2/answers/02-Fast-GPs.ipynb".
The first task is:
george also offers a fancy GP solver called the HODLR solver, which makes some approximations that dramatically speed up the matrix algebra. Let's instantiate the GP object again by passing the keyword solver=george.HODLRSolver and re-compute the log likelihood. How long did that take? Did we get the same value for the log likelihood?
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Code from PracticalLombScargle by Jake Van der Plas
The functions below implement plotting routines developed by Jake to illustrate some properties of Fourier transforms.
This code is distributed under a BSD-3 licence, which is repeated below:
Copyright (c) 2015, Jake Vanderplas
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

Neither the name of PracticalLombScargle nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Can you write Python code for it?

def fourier_pairs_plot():

    fig, ax = plt.subplots(4, 2, figsize=(10, 6))
    fig.subplots_adjust(left=0.04, right=0.98, bottom=0.02, top=0.95,
                        hspace=0.3, wspace=0.2)

    x = np.linspace(-5, 5, 1000)

    for axi in ax.flat:
        axi.xaxis.set_major_formatter(plt.NullFormatter())
        axi.yaxis.set_major_formatter(plt.NullFormatter())

        # draw center line
        axi.axvline(0, linestyle='dotted', color='gray')
        axi.axhline(0, linestyle='dotted', color='gray')

    style_re = dict(linestyle='solid', color='k', linewidth=2)
    style_im = dict(linestyle='solid', color='gray', linewidth=2)
    text_style = dict(size=14, color='gray')

    # sine -> delta
    ax[0, 0].plot(x, np.cos(x),**style_re)
    ax[0, 0].set(xlim=(-5, 5), ylim=(-1.2, 1.2))
    ax[0, 0].annotate('', (-np.pi, 0), (np.pi, 0),
                      arrowprops=dict(arrowstyle='|-|', color='gray'))
    ax[0, 0].text(0, 0, '$1/f_0$', ha='center', va='bottom', **text_style)
    ax[0, 0].set_title('Sinusoid')

    ax[0, 1].plot([-5, 2, 2, 2, 5], [0, 0, 1, 0, 0], **style_re)
    ax[0, 1].plot([-5, -2, -2, -2, 5], [0, 0, 1, 0, 0], **style_re)
    ax[0, 1].set(xlim=(-5, 5), ylim=(-0.2, 1.2))
    ax[0, 1].annotate('', (0, 0.4), (2, 0.4), arrowprops=dict(arrowstyle='<-', color='gray'))
    ax[0, 1].annotate('', (0, 0.4), (-2, 0.4), arrowprops=dict(arrowstyle='<-', color='gray'))
    ax[0, 1].text(1, 0.45, '$+f_0$', ha='center', va='bottom', **text_style)
    ax[0, 1].text(-1, 0.45, '$-f_0$', ha='center', va='bottom', **text_style)
    ax[0, 1].set_title('Delta Functions')

    # gaussian -> gaussian
    ax[1, 0].plot(x, np.exp(-(2 * x) ** 2), **style_re)
    ax[1, 0].set(xlim=(-5, 5), ylim=(-0.2, 1.2))
    ax[1, 0].annotate('', (0, 0.35), (0.6, 0.35), arrowprops=dict(arrowstyle='<-', color='gray'))
    ax[1, 0].text(0, 0.4, '$\sigma$', ha='center', va='bottom', **text_style)
    ax[1, 0].set_title('Gaussian')

    ax[1, 1].plot(x, np.exp(-(x / 2) ** 2), **style_re)
    ax[1, 1].set(xlim=(-5, 5),...


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
From the plot the signal is clearly variable (unlike when $f = f_\mathrm{Ny}$). However, there are fewer than 2 observations per cycle.
Problem 2c
Overplot a source with $f = 2.7$ on the same data shown in 2b.
Can you write Python code for it?

x = np.linspace(# complete
y = gen_periodic_data( # complete
x_signal = np.linspace( # complete
y_signal = gen_periodic_data( # complete
fig, ax = plt.subplots(figsize=(8,4))
ax.scatter(x,y)
ax.plot(x_signal, y_signal)

y_high = gen_periodic_data( # complete
y_signal_high = gen_periodic_data( # complete
ax.scatter(x,y_high)
ax.plot(x_signal, y_signal_high)
ax.set_xlabel('x')
ax.set_ylabel('y')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Introduction to the Lomb-Scargle Periodogram
Version 0.2

By AA Miller (Northwester/CIERA)
15 Sep 2021
Today we examine the detection of periodic signals in noisy, irregular data (the standard for ground-based astronomical surveys).
This lecture is strongly influenced by Understanding the Lomb-Scarge Periodogram, by Jake VanderPlas, a former DSFP lecturer (VanderPlas 2017). Beyond that, the original papers by Lomb 1976 and Scargle 1982 are also worth a read.
There are many, many papers on the use of the Lomb-Scargle periodogram (and other period detection methods). A recent study led by former DSFP lecturer, Matthew Graham, conducted a systematic analysis of many of the most popular tools used to search for periodic signals on actual astronomical data (Graham et al. 2013).$^\dagger$
$^\dagger$Somewhat to my (our?) dismay, they found that none of the solutions work really well across all use cases.
Problem 1) Helper Functions
We need to simulate and plot the same types of data again and again. To start this lecture we will create a few helper functions to minimize repetitive commands (e.g., phase-folding a light curve).
Problem 1a
Create a function, gen_periodic_data, that creates simulated data (including noise) over a grid of user supplied positions:
$$ y = A\,sin\left(\frac{2 \pi x}{P} - \phi\right) + \sigma_y$$
where $A, P, \phi$ are inputs to the function. gen_periodic_data should include Gaussian noise, $\sigma_y$, for each output $y_i$.
Can you write Python code for it?

def gen_periodic_data(x, period=1, amplitude=1, phase=0, noise=0):
    '''Generate periodic data given the function inputs

    y = A*sin(2*pi*x/p - phase) + noise

    Parameters
    ----------
    x : array-like
        input values to evaluate the array

    period : float (default=1)
        period of the periodic signal

    amplitude : float (default=1)
        amplitude of the periodic signal

    phase : float (default=0)
        phase offset of the periodic signal

    noise : float (default=0)
        variance of the noise term added to the periodic signal

    Returns
    -------
    y : array-like
        Periodic signal evaluated at all points x
    '''

    y = # complete
    dy = # complete
    return y + dy


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Problem 1b
Generate a noise-free signal with $A = 2$ and $p = \pi$ over a regular grid between 0 and 10. Plot the results (and make sure gen_periodic_data behaves as you would expect).
Can you write Python code for it?

x = np.linspace( # complete
y = gen_periodic_data( # complete

fig, ax = plt.subplots()
ax.scatter(x,y, edgecolors='0.2', linewidths=0.5)
ax.set_xlabel('x')
ax.set_ylabel('y')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Problem 1d
Plot the phase folded data generated in 1b.
Does you plot match your expectations?
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Problem 2) A Brief Review of Fourier Analysis
In astronomical time series, we crave the detection of periodic signals because they can often provide fundamental insight into the sources we are studying (e.g., masses in a binary, pulsation timescales in stars, etc).
The standard$^\dagger$ choice for most astronomers to identify such signals is the Lomb-Scargle (LS) periodogram (Lomb 1976; Scargle 1982).
$^\dagger$Standard does not mean best, fastest, or even correct depending on your specific application.
At the heart of understanding any periodic signals is Fourier analysis. Thus, to understand how to interpret the LS periodogram, we first need to consider Fourier transforms.
Note - the following discussion is not complete. See Lecture I from this session for a more thorough review.
Given a continuous signal, $g(t)$ the Fourier transform of that signal is defined as:
$$\hat{\mathrm{g}}(f) = \int_{-\infty}^{\infty} g(t) \,e^{-2\pi i f t} \,dt,$$
where $i$ is an imaginary number.
The inverse of this equation is defined as:
$$ g(t) = \int_{-\infty}^{\infty} \hat{\mathrm{g}}(f) \,e^{2\pi i f t} \,df.$$
For convenience, we will use the Fourier transform operator $\mathcal{F}$, from which the previous equations reduce to:
$$\mathcal{F}(g) = \hat g$$
$$\mathcal{F}^{-1}(\hat{g}) = g$$
There are many useful properties of the Fourier transform including that the Fourier transform is a linear operator. Additionally, a time shift in the signal imparts a phase shift in the transform.
Perhaps most importantly for our present purposes, however, is that the squared amplitude of the resulting transform allows us to get rid of the imaginary component and measure the power spectral density or power spectrum:
$$ \mathcal{P}_g = \left|\mathcal{F}(g)\right|^2.$$
The power spectrum is a real-valued function that quantifies the contribution of each frequency $f$ to the total signal in $g$. The power spectrum thus provides a way to identify the dominant frequency in any given signal.
Next we consider some common Fourier pairs,...
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Problem 3b
Write a function to minimize the $\chi^2$ given everything but $A_f$ and $\phi_f$.
Hint - minimize within the scipy package is helpful.
Can you write Python code for it?

from scipy.optimize import minimize

def min_chi2(theta, y, y_unc, x, f):
    res = minimize( # complete
    return res.fun


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Problem 3c
Write a function, ls_periodogram, to calculate the LS periodogram for observations $y$, $\sigma_y$, $t$ over a frequency grid f_grid.
Can you write Python code for it?

def ls_periodogram(y, y_unc, x, f_grid):
    psd = np.empty_like(f_grid)
    chi2_0 = np.sum(((y - np.mean(y))/y_unc)**2)
    for f_num, f in enumerate(f_grid):
        psd[f_num] = 0.5*(chi2_0 - min_chi2([0,0], y, y_unc, x, f))

    return psd


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Problem 3d
Generate a periodic signal with 100 observations taken at random intervals over a time period of 10 days. Use an input period of 5.25, amplitude of 7.4, and variance of the noise = 0.8. Then compute and plot the periodogram for the simulated data. Do you recover the simulated period?
Hint - set the minimum frequency in the grid to $1/T$ where $T$ is the duration of the observations. Set the maximum frequnecy to 10, and use an equally spaced grid with 50 points.
Can you write Python code for it?

np.random.seed(185)
# calculate the periodogram

x = 10*np.random.rand(100)
y = gen_periodic_data(x, period=5.25, amplitude=7.4, noise=0.8)
y_unc = np.ones_like(x)*np.sqrt(0.8)

f_grid = np.linspace( # complete

psd_ls = ls_periodogram(y, y_unc, x, f_grid)

# plot the periodogram
fig, ax = plt.subplots()
ax.plot(1/f_grid, psd_ls)
ax.set_ylabel('P')
ax.set_xlabel('Period')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
Sampling a signal directly at the Nyquist frequency results in a lack of any variability. But does this just mean that $f_\mathrm{Ny}$ is special? What happens at $f > f_\mathrm{Ny}$?
Problem 2b
As above, generate and plot a periodic signal with $f = 0.7$ on an even grid from 0 to 10. Overplot the underlying signal in addition to the observations.
Can you write Python code for it?

x = np.linspace( # complete
y = gen_periodic_data( # complete
x_signal = np.linspace(# complete
y_signal = gen_periodic_data(# complete
fig, ax = plt.subplots(figsize=(8,4))
ax.scatter(x,y)
ax.plot(x_signal, y_signal)
ax.set_xlabel('x')
ax.set_ylabel('y')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
The common Fourier pairs are especially useful in light of the convolution theorem. Fourier transforms convert convolutions into point-wise products. We define a convolution as:
$$ [f \ast g] (t) = \int_{-\infty}^{\infty} f(\tau) \,g(t - \tau) \,d\tau,$$
where $\ast$ is the convolution symbol.
From the convolution theorem:
$$ \mathcal{F} {f \ast g} = \mathcal{F}(f)  \mathcal{F}(g) $$
Furthermore, the Fourier transform of a product is equal to the convolution of the Fourier transforms:
$$ \mathcal{F}{f \cdot g} = \mathcal{F}(f) \ast \mathcal{F}(g) $$
This property will be very important for understanding the Lomb-Scargle periodogram.
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day3/IntroductionToTheLombScarglePeriodogram.ipynb".
The first task is:
The observations are identical! Here is what you need to remember about the Nyquist frequency:

If you are going to obtain observations at regular intervals, and there is a specific signal you wish to detect, then be sure to sample the data such that $f_\mathrm{Ny} > f_\mathrm{signal}$.

For all $f > f_\mathrm{Ny}$, $f$ will be aliased with $f \pm 2n f_\mathrm{Ny}$ signals, where $n$ is an integer. Practically speaking, this means it does not make sense to search for signals with $f > f_\mathrm{Ny}$.

Finally, (and this is something that is often wrong in the literature) there is no Nyquist limit for unevenly sampled data (see VanderPlas 2017 for further details). Thus, for (virtually all) ground-based observing one need not worry about the Nyquist limit.

Staying on the topic of non-continuous observations, I present without derivation the discrete Fourier transform:
$$ \hat g_\mathrm{obs}(f) = \sum_{n = 0}^N g_n\,e^{-2\pi i f n\Delta t}$$
where $g_n = g(n\Delta t)$, and $\Delta t$ is the sampling interval. Our discussion of the Nyquist frequency tells us that we cannot detect frequencies $f > 1/2\Delta T$. Thus, the relevant frequencies to search for power given $\Delta t$ are between 0 and $f_\mathrm{Ny}$, which we can sample on a grid $\Delta f = 1/(N \Delta t)$.
From there:
$$\hat g_k = \sum_{n = 0}^N g_n\,e^{-2\pi i f knN}$$
where $\hat g_k = \hat g_\mathrm{obs} (k\Delta f)$. This is the discrete Fourier transform.
I said a full derivation will not be provided, and that is true. To understand how we went from a continuous integral to the summation above, recall that regular observations of a continous signal over a finite interval is equivalent to multiplying the continuous signal by a Dirac comb function and a window function. The delta functions from the Dirac comb function collapse the integral to a sum, while the window function limits that sum from $0$ to $N$.
From the discrete Fourier transform we can then calculate the periodogram (an estimator of the power spectrum):
$$\mathcal{P}(f) =...
Can you write Python code for it?

def chi2(theta, y, y_unc, x, f):
    a = theta[0]
    phi = theta[1]
    # complete
    return # complete


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Helper 2
Create a function, phase_plot, that takes x, y, and $P$ as inputs to create a phase-folded light curve (i.e., plot the data at their respective phase values given the period $P$).
Include an optional argument, y_unc, to include uncertainties on the y values, when available.
Can you write Python code for it?

def phase_plot(x, y, period, y_unc = 0.0, mag_plot=False):
    '''Create phase-folded plot of input data x, y

    Parameters
    ----------
    x : array-like
        data values along abscissa

    y : array-like
        data values along ordinate

    period : float
        period to fold the data

    y_unc : array-like
        uncertainty of the
    '''
    phases = (x/period) % 1
    if isinstance(y_unc, (np.floating, float)):
        y_unc = np.ones_like(x)*y_unc

    plot_order = np.argsort(phases)
    fig, ax = plt.subplots()
    ax.errorbar(phases[plot_order], y[plot_order], y_unc[plot_order],
                 fmt='o', mec="0.2", mew=0.1)
    ax.set_xlabel("phase")
    ax.set_ylabel("signal")
    if mag_plot:
        ax.set_ylim(ax.get_ylim()[::-1])
    fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Herein lies a fundamental issue regarding the LS periodogram: the model does not search for "periodicity." The LS model asks if the data support a sinusoidal signal. As astronomers we typically assume this question is good enough, but as we can see in the example of this eclipsing binary that is not the case [and this is not limited to eclipsing binaries].
We can see why LS is not sufficient for an EB by comparing the model to the phase-folded light curve:
Problem 2d
Overplot the model on top of the phase folded light curve.
Hint – you can access the best LS fit via the .model() method on LombScargle objects in astropy.
Can you write Python code for it?

phase_plot( # complete

phase_grid = np.linspace(0,1,1000)
plt.plot( # complete
plt.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Interestingly, for the $n=2, 3, 4$ harmonics, it appears as though we get the period that we have visually confirmed. However, by $n=5$ harmonics we no longer get a reasonable answer. Again - be very careful about adding harmonics, especially in large analysis pipelines.
Problem 2f
Plot the the $n = 4$ model on top of the light curve folded at the correct period.
Can you write Python code for it?

best_period = # complete

phase_plot( # complete

phase_grid = np.linspace(0,1,1000)
plt.plot( # complete
plt.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Interestingly, there are very strong peaks in the data at $P \approx 3\,\mathrm{d} \;\&\; 365\,\mathrm{d}$.
What is this telling us? Essentially that observations are likely to be repeated at intervals of 3 or 365 days (shorter period spikes are aliases of the 3 d peak).
This is important to understand, however, because this same power will be present in the periodogram where we search for the periodic signal.
Problem 1f
Calculate the periodogram for the data and compare it to the periodogram for the window function.
Can you write Python code for it?

ls = LombScargle( # complete

frequency, power = # complete

fig, (ax,ax2) = plt.subplots(2,1, sharex=True)
ax.plot( # complete
ax.set_ylabel("Power")
ax.set_ylim(0,1)

ax2.plot( # complete
ax2.set_ylabel("Power")
ax2.set_xlabel("Period (d)")
ax2.set_xlim(0,10)
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Now we have seen that nearly all the structure in the LS period vs. true period plot can be explained via aliasing with the window function! This is good (we understand why the results do not conform to what we expect), but also bad, (we were not able to recover the correct period for the majority of our sources). Ultimately, this means - be careful when inspecting the results of the LS periodogram as the peaks aren't driven solely by the signal from the source in question!
(If only there were some way to get rid of the sun, then we'd never have these problems...)
Conclusions
The Lomb-Scargle periodogram is a useful tool to search for sinusoidal signals in noisy, irregular data.
However, as highlighted throughout, there are many ways in which the methodology can run awry.
In closing, I will summarize some practical considerations from VanderPlas (2017):

Choose an appropriate frequency grid (defaults in LombScargle are not sufficient)
Calculate the LS periodogram for the observation times to search for dominant signals (e.g., 1 day in astro)
Compute LS periodogram for data (avoid multi-Fourier models if signal unknown)
Plot periodogram and various FAP levels (do not over-interpret FAP)
If window function shows strong aliasing, plot phased light curve at each peak (now add more Fourier terms if necessary)
If looking for a particular signal (e.g., detatched EBs), consider different methods that better match expected signal
Inject fake signals into data to understand systematics if using LS in a survey pipeline

Finally, Finally
As a very last note: know that there are many different ways to search for periodicity in astronomical data. Depending on your application (and computational resources), LS may be a poor choice (even though this is often the default choice by all astronomers!) Graham et al. (2013) provides a summary of several methods using actual astronomical data. The results of that study show that no single method is best. However, they also show that no single method performs particularly well:...
Can you write Python code for it?

def gen_periodic_data(x, period=1, amplitude=1, phase=0, noise=0):
    '''Generate periodic data given the function inputs

    y = A*cos(x/p - phase) + noise

    Parameters
    ----------
    x : array-like
        input values to evaluate the array

    period : float (default=1)
        period of the periodic signal

    amplitude : float (default=1)
        amplitude of the periodic signal

    phase : float (default=0)
        phase offset of the periodic signal

    noise : float (default=0)
        variance of the noise term added to the periodic signal

    Returns
    -------
    y : array-like
        Periodic signal evaluated at all points x
    '''

    y = amplitude*np.sin(2*np.pi*x/(period) - phase) + np.random.normal(0, np.sqrt(noise), size=len(x))
    return y


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
One way to combat this very specific issue is to include more Fourier terms at the harmonic of the best fit period. This is easy to implement in LombScargle with the nterms keyword. [Though always be weary of adding degrees of freedom to a model, especially at the large pipeline level of analysis.]
Problem 2e
Calculate the LS periodogram for the eclipsing binary, with nterms = 1, 2, 3, 4, 5. Report the best-fit period for each of these models.
Hint - we have good reason to believe that the best fit frequency is < 3 in this case, so set maximum_frequency = 3.
Can you write Python code for it?

for i in np.arange(1,6):
    frequency, power = # complete
    # complete
    print('For {:d} harmonics, P_LS = {:.8f}'.format( # complete


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Problem 2c
Now plot the light curve folded at twice the best LS period.
Which of these 2 is better?
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Problem 3d
Plot the LS recovered period vs. the true period for the simulated sources.
Do you notice anything interesting? Do you manage to recover the correct period most of the time?
Can you write Python code for it?

fig, ax = plt.subplots()
ax.plot( # complete
ax.set_ylim(0, 20)
ax.set_xlabel('True period (d)')
ax.set_ylabel('LS peak (d)')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Real World Considerations for the Lomb-Scargle Periodogram
Version 0.2

By AA Miller (Northwestern/CIERA)
23 Sep 2021
In Lecture III we built the software necessary to estimate the power spectrum via the Lomb-Scargle periodogram.
We also discovered that LS is somewhat slow. We will now leverage the faster implementation in astropy, while exploring some specific challenges related to real astrophysical light curves.
The helper functions from Lecture III are recreated at the end of this notebook - execute those cells and then the cell below to recreate the simulated data from Lecture III.
Can you write Python code for it?

np.random.seed(185)
# calculate the periodogram

x = 10*np.random.rand(100)
y = gen_periodic_data(x, period=5.25, amplitude=7.4, noise=0.8)
y_unc = np.ones_like(x)*np.sqrt(0.8)


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
There are many choices regarding the calculation of the periodogram, so read the docs.
Floating Mean Periodogram
A basic assumption that we preivously made is that the data are "centered" - in other words, our model explicitly assumes that the signal oscillates about a mean of 0.
For astronomical applications, this assumption can be harmful. Instead, it is useful to fit for the mean of the signal in addition to the periodic component (as is the default in LombScargle):
$$y(t;f) = y_0(f) + A_f \sin(2\pi f(t - \phi_f).$$
To illustrate why this is important for astronomy, assume that any signal fainter than $-2$ in our simulated data cannot be detected.
Problem 1c
Remove the observations from x and y where $y \le -2$ and calculate the periodogram both with and without fitting the mean (fit_mean = False in the call to LombScargle). Plot the periodograms. Do both methods recover the correct period?
Can you write Python code for it?

# complete
freq_no_mean, power_no_mean = LombScargle( # complete
freq_fit_mean, power_fit_mean = LombScargle( # complete

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
ax1.plot( # complete
ax2.plot( # complete
ax1.set_xlim(0,15)

fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Unlike ls_periodogram, LombScargle effectively takes no time to run on the simulated data.
Problem 1b
Plot the periodogram for the simulated data.
Can you write Python code for it?

fig, ax = plt.subplots()

ax.plot( # complete
# complete
# complete
# complete
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
We can see that the best fit model doesn't match the signal in the case where we do not allow a floating mean.
Can you write Python code for it?

fit_mean_model = LombScargle(x[bright], y[bright], y_unc[bright],
                             fit_mean=True).model(np.linspace(0,10,1000),
                                                  freq_fit_mean[np.argmax(power_fit_mean)])
no_mean_model = LombScargle(x[bright], y[bright], y_unc[bright],
                             fit_mean=False).model(np.linspace(0,10,1000),
                                                  freq_no_mean[np.argmax(power_no_mean)])

fig, ax = plt.subplots()
ax.errorbar(x[bright], y[bright], y_unc[bright], fmt='o', label='data')
ax.plot(np.linspace(0,10,1000), fit_mean_model, label='fit mean')
ax.plot(np.linspace(0,10,1000), no_mean_model, label='no mean')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
What in the...
We see that these lines account for a lot of the off-diagonal structure in this plot.
In this case, what is happening is that the true frequency of the signal $f_\mathrm{true}$ is being aliased by the window function and it's harmonics. In other words LS is pulling out $f_\mathrm{true} + n\delta{f}$, where $n$ is an integer and $\delta{f}$ is the observational cadence $3\,\mathrm{d}$. Many of the false positives can be explained via the window function.
Similarly, LS might be recovering higher order harmonics of the true period since we aren't trying to recover pure sinusoidal signals in this simulation. These harmonics would also be aliased by the window function, so LS will pull out $f_\mathrm{true}/m + n\delta{f}$, where $m$ is a postive integer.
Problem 3g
Recreate the plot in 3d and overplot lines for the $m = 2$ harmonic aliased with $n = -1, 1, 2$.
Hint - only plot the values where $P_\mathrm{LS} > 0$ since, by definition, we do not search for negative periods.
Can you write Python code for it?

p_grid = np.linspace(1e-1,10,100)

fig, ax = plt.subplots()
ax.plot(# complete
    # complete
    # complete
    # complete

ax.set_ylim(0, 2)
ax.set_xlabel('True period (d)')
ax.set_ylabel('LS peak (d)')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day3/RealWorldLombScargle.ipynb".
The first task is:
Window Functions
Recall that the convolution theorem tells us that:
$$\mathcal{F}[f\cdot g] = \mathcal{F}(f) \ast \mathcal{F}(g)$$
Telescope observations are effectively the product of a continous signal with several delta functions (corresponding to the times of observations). As a result, the convolution that produces the periodogram will retain signal from both the source and the observational cadence.
To illustrate this effect, let us simulate "realistic" observations for a 10 year telescope survey. We do this by assuming that a source is observed every 3 nights (the LSST cadence) within $\pm 4\,\mathrm{hr}$ of the same time, and that $\sim 30\%$ of the observations did not occur due to bad weather. We further assume that the source cannot be observed for 40% of the year because it is behind the sun.
Simulate a periodic signal with this cadence, a period = 220 days (typical for Miras), amplitude = 12.4, and noise = 1. Plot the simulated light curve.
Problem 1d
Simulate a periodic signal with 3 day cadence (and the observing conditions described above), a period = 220 days (typical for Miras), amplitude = 12.4, and variance of the noise = 1. Plot the simulated light curve.
Can you write Python code for it?

# set up simulated observations
t_obs = np.arange(0, 10*365, 3)      # 3d cadence
# complete
# complete
# complete

y = gen_periodic_data( # complete
# complete

fig, ax = plt.subplots()
ax.errorbar( # complete
ax.set_xlabel("Time (d)")

ax.set_ylabel("Flux (arbitrary units)")


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Conditional Entropy: Can Information Theory Beat the L-S Periodogram?
Version 0.2

By AA Miller
23 Sep 2021
Lecture IV focused on alternative methods to Lomb-Scargle when searching for periodic signals in astronomical time series. In this notebook you will develop the software necessary to search for periodicity via Conditional Entropy (my personal favorite method).
Conditional Entropy
Conditional Entropy (CE; Graham et al. 2013), and other entropy based methods, aim to minimize the entropy in binned (normalized magnitude, phase) space. CE, in particular, is good at supressing signal due to the window function.
When tested on real observations, CE outperforms most of the alternatives (e.g., LS, PDM, etc).

 (credit: Graham et al. 2013)

Conditional Entropy
The focus of today's exercise is conditional entropy (CE), which uses information theory and thus, in principle, works better in the presence of noise and outliers. Furthermore, CE does not make any assumptions about the underlying shape of the signal, which is useful when looking for non-sinusoidal patterns (such as transiting planets or eclipsing binaries).
For full details on the CE algorithm, see Graham et al. (2013).
Briefly, CE is based on the using the Shannon entropy (Cincotta et al. 1995), which is determined as follows:

Normalize the time series data $m(t_i)$ to occupy a uniform square over phase, $\phi$, and magnitude, $m$, at a given trial period, $p$.

Calculate the Shannon entropy, $H_0$, over the $k$ partitions in $(\phi, m)$:

$$H_0 = - \sum_{i=1}^{k} \mu_i \ln{(\mu_i)}\;\; \forall \mu_i \ne 0,$$
 where $\mu_i$ is the occupation probability for the $i^{th}$ partition, which is just the number of data points in that partition divided by the total number of points in the data set.

Iterate over multiple periods, and identify the period that minimizes the entropy (recall that entropy...
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Note a couple changes from the previous helper function –– we have added a grid to the plot (this will be useful for visualizing the entropy), and we have also normalized the brightness measurements from 0 to 1.
Problem 2) The Shannon entropy
As noted above, to calculate the Shannon entropy we need to sum the data over partitions in the normalized $(\phi, m)$ plane.
This is straightforward using histogram2d from numpy.
Problem 2a
Write a function shannon_entropy to calculate the Shannon entropy, $H_0$, for a timeseries, $m(t_i)$, at a given period, p.
Hint - use histogram2d and a 10 x 10 grid (as plotted above).
Can you write Python code for it?

def shannon_entropy(m, t, p):
    '''Calculate the Shannon entropy

    Parameters
    ----------
    m : array-like
        brightness measurements of the time-series data

    t : array-like (default=1)
        timestamps corresponding to the brightness measurements

    p : float
        period of the periodic signal

    Returns
    -------
    H0 : float
        Shannon entropy for m(t) at period p
    '''

    m_norm = # complete
    phases = # complete
    H, _, _ = np.histogram2d( # complete

    occupied = np.where(H > 0)
    H0 = # complete

    return H0


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 1b
Create a function, phase_plot, that takes x, y, and $P$ as inputs to create a phase-folded light curve (i.e., plot the data at their respective phase values given the period $P$).
Include an optional argument, y_unc, to include uncertainties on the y values, when available.
Can you write Python code for it?

def phase_plot(x, y, period, y_unc = 0.0):
    '''Create phase-folded plot of input data x, y

    Parameters
    ----------
    x : array-like
        data values along abscissa

    y : array-like
        data values along ordinate

    period : float
        period to fold the data

    y_unc : array-like
        uncertainty of the
    '''
    phases = (x/period) % 1
    if type(y_unc) == float:
        y_unc = np.zeros_like(x)

    plot_order = np.argsort(phases)
    norm_y = (y - np.min(y))/(np.max(y) - np.min(y))
    norm_y_unc = (y_unc)/(np.max(y) - np.min(y))

    plt.rc('grid', linestyle=":", color='0.8')
    fig, ax = plt.subplots()
    ax.errorbar(phases[plot_order], norm_y[plot_order], norm_y_unc[plot_order],
                 fmt='o', mec="0.2", mew=0.1)
    ax.set_xlabel("phase")
    ax.set_ylabel("signal")
    ax.set_yticks(np.linspace(0,1,11))
    ax.set_xticks(np.linspace(0,1,11))
    ax.grid()
    fig.tight_layout()


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 1c
Generate a signal with $A = 2$, $p = \pi$, and Gaussian noise with variance = 0.01 over a regular grid between 0 and 10. Plot the phase-folded results (and make sure the results behave as you would expect).
Hint - your simulated signal should have at least 100 data points.
Can you write Python code for it?

x = np.linspace( # complete
y = # complete

# complete plot


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 3) The Conditional Entropy
The CE is very similar to the Shannon entropy, though we need to condition the calculation on the occupation probability of the partitions in phase.
Problem 3a
Write a function conditional_entropy to calculate the CE, $H_c$, for a timeseries, $m(t_i)$, at a given period, p.
Hint - if you use histogram2d be sure to sum along the correct axes
Hint 2 - recall from session 8 that we want to avoid for loops, try to vectorize your calculation.
Can you write Python code for it?

def conditional_entropy(m, t, p):
    '''Calculate the conditional entropy

    Parameters
    ----------
    m : array-like
        brightness measurements of the time-series data

    t : array-like
        timestamps corresponding to the brightness measurements

    p : float
        period of the periodic signal

    Returns
    -------
    Hc : float
        Conditional entropy for m(t) at period p
    '''

    m_norm = # complete
    phases = # complete
    # complete
    # complete
    # complete
    Hc = # complete

    return Hc


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 3b
What is the conditional entropy for the simulated signal at periods = 1, $\pi$-0.05, and $\pi$?
Do these results make sense given your understanding of CE?
Can you write Python code for it?

print('For p = 1, \t\tH_c = {:.5f}'.format( # complete
print('For p = pi - 0.05, \tH_c = {:.5f}'.format( # complete
print('For p = pi, \t\tH_c = {:.5f}'.format( # complete


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 3c
Write a function, ce_periodogram, to calculate the conditional entropy for observations $m$, $t$ over a frequency grid f_grid.
Can you write Python code for it?

def ce_periodogram(m, t, f_grid):
    '''Calculate the conditional entropy at every freq in f_grid

    Parameters
    ----------
    m : array-like
        brightness measurements of the time-series data

    t : array-like
        timestamps corresponding to the brightness measurements

    f_grid : array-like
        trial periods for the periodic signal

    Returns
    -------
    ce_p : array-like
        conditional entropy for m(t) at every trial freq
    '''

    # complete
    for # complete in # complete
        # complete

    return ce_p


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 4c
Plot the Shannon entropy periodogram.
Can you write Python code for it?

fig, ax = plt.subplots()
# complete
# complete
# complete


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 4d
Plot the light curve phase-folded on the best-fit period, as measured by the Shannon entropy periodogram.
Does this look reasonable? Why or why not?
Hint - it may be helpful to zoom in on the periodogram.
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
Problem 4e
Using the conditional entropy, determine the best period for this light curve.
Can you write Python code for it?

ce_p = # complete

print("The best fit period is: {:.9f}".format( # complete


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
The Shannon entropy and CE return nearly identical results for a simulated sinusoidal signal. Now we will examine how each performs with actual astronomical observations.
Problem 4) SE vs. CE for real observations
Problem 4a
Load the data from our favorite eclipsing binary from this morning's LS exercise. Plot the light curve.
Hint - if you haven't already, download the example light curve.
Can you write Python code for it?

data = pd.read_csv("example_asas_lc.dat")

fig, ax = plt.subplots()
ax.errorbar( # complete
ax.set_xlabel('HJD (d)')
ax.set_ylabel('V (mag)')
ax.set_ylim(ax.get_ylim()[::-1])
fig.tight_layout()


I am working on the file "Sessions/Session13/Day4/ConditionalEntropy.ipynb".
The first task is:
We know the correct period of the simulated data is $\pi$, so it makes sense that this period minimizes the Shannon entropy.
Problem 2c
Write a function, se_periodogram to calculate the Shannon entropy for observations $m$, $t$ over a frequency grid f_grid.
Can you write Python code for it?

def se_periodogram(m, t, f_grid):
    '''Calculate the Shannon entropy at every freq in f_grid

    Parameters
    ----------
    m : array-like
        brightness measurements of the time-series data

    t : array-like
        timestamps corresponding to the brightness measurements

    f_grid : array-like
        trial periods for the periodic signal

    Returns
    -------
    se_p : array-like
        Shannon entropy for m(t) at every trial freq
    '''

    # complete
    for # complete in # complete
        # complete

    return se_p


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Helper 2
Create a function, phase_plot, that takes x, y, and $P$ as inputs to create a phase-folded light curve (i.e., plot the data at their respective phase values given the period $P$).
Include an optional argument, y_unc, to include uncertainties on the y values, when available.
Can you write Python code for it?

def phase_plot(x, y, period, y_unc = 0.0, mag_plot=False):
    '''Create phase-folded plot of input data x, y

    Parameters
    ----------
    x : array-like
        data values along abscissa

    y : array-like
        data values along ordinate

    period : float
        period to fold the data

    y_unc : array-like
        uncertainty of the
    '''
    phases = (x/period) % 1
    if type(y_unc) == float:
        y_unc = np.zeros_like(x)

    plot_order = np.argsort(phases)
    fig, ax = plt.subplots()
    ax.errorbar(phases[plot_order], y[plot_order], y_unc[plot_order],
                 fmt='o', mec="0.2", mew=0.1)
    ax.set_xlabel("phase")
    ax.set_ylabel("signal")
    if mag_plot:
        ax.set_ylim(ax.get_ylim()[::-1])
    fig.tight_layout()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Helper 3
Write a function plot_chains to show the individual chains from the multi-chain MCMC sampler emcee.
Can you write Python code for it?

#define function to plot walker chains
def plotChains(sampler, nburn, paramsNames, nplot=None):
    '''Plot individual chains from the emcee MCMC sampler

    Parameters
    ----------
    sampler : emcee EnsembleSampler object
        emcee affine-invariant multi-chain MCMC sampler

    nburn : int
        number of "burn-in" steps for the MCMC chains

    paramsNames : list
        names of the parameters to be shown

    nplot : int (default=None)
        number of chains to show in the visualization.
        In instances where the number of chains is
        very large (>> 100), then it can be helpful to
        downsample to provide more clarity.

    Returns
    -------
    ax : maptlotlib axes object
        multi panel plot showing the evoltion of
        each chain for the parameters in the model

    '''

    Nparams = len(paramsNames)
    nwalkers = sampler.get_chain().shape[1]

    fig, ax = plt.subplots(Nparams+1,1, figsize = (8,2*(Nparams+1)), sharex = True)
    fig.subplots_adjust(hspace = 0)
    ax[0].set_title('Chains')
    xplot = np.arange(sampler.get_chain().shape[0])

    if nplot is None:
        nplot=nwalkers
    selected_walkers = np.random.choice(range(nwalkers), nplot, replace=False)
    for i,p in enumerate(paramsNames):
        for w in selected_walkers:
            burn = ax[i].plot(xplot[:nburn], sampler.get_chain()[:nburn,w,i],
                              alpha = 0.4, lw = 0.7, zorder = 1)
            ax[i].plot(xplot[nburn:], sampler.get_chain(discard=nburn)[:,w,i],
                       color=burn[0].get_color(), alpha = 0.8, lw = 0.7, zorder = 1)

            ax[i].set_ylabel(p)
            if i==Nparams-1:
                ax[i+1].plot(xplot[:nburn], sampler.get_log_prob()[:nburn,w],
                             color=burn[0].get_color(), alpha = 0.4, lw = 0.7, zorder = 1)
                ax[i+1].plot(xplot[nburn:], sampler.get_log_prob(discard=nburn)[:,w],
                             color=burn[0].get_color(), alpha =...


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Non-parametric Measures of Periodicity – Return of the Gaps
Version 0.1

By AA Miller (Northwester/CIERA)
20 Sep 2021
In this lecture we will examine non-parametric methods to search for periodic signals in astronomical time series. Lecture III focused extensively on the Lomb-Scargle periodogram. LS is the "standard" in astronomy, in part because it was the first (good) method developed for noisy and sparse data.
LS is not without warts, however, (i) LS does not handle outliers well, and (ii) LS works best on purely sinusoidal signals.
Given non-Gaussian noise and that some signals (e.g., transiting planets) are not sinusoidal, we will now explore alternative methods to search for periodicity.
We begin today with a simulated signal (we will use multiple harmonics to make things a bit more challenging than a pure sinusoid). The cell below creates a periodic signal with $P = 0.7\,\mathrm{d}$, sampled over two months ($60\,\mathrm{d}$), with an average of two observations per night.
(If the slide below does not execute properly, jump to the end of this notebook and execute the cells with the various helper functions)
Can you write Python code for it?

np.random.seed(185)
t_obs = np.random.uniform(60, size=120)
phi = np.pi
var_y = 0.81

y = gen_periodic_data(t_obs, period=0.7,
                      amplitude=3, phase=phi,
                      noise=var_y)
y += gen_periodic_data(t_obs, period=0.7/2,
                       amplitude=3, phase=phi+np.pi/2)
y += gen_periodic_data(t_obs, period=0.7/3,
                       amplitude=2, phase=phi)

y_unc = np.ones_like(y)*np.sqrt(var_y)

phase_plot(t_obs, y, 0.7, y_unc = y_unc)


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Now you have the tools to fit a GP to a light curve and get an estimate of the best fit period (and to get an estimate of the uncertainty on that period to boot!).
As previously noted, you should be a bit worried about "burn in" and how the walkers were initialized throughout. If you plan to use GPs to search for periods in your own work, I highly recommend you read Angus et al. 2018 on the GP periodogram. Angus et al. provide far more intelligent methods for initializing the MCMC than what is presented here.
Helper Functions
We developed useful helper functions as part of Lecture III from this session.
These functions generate periodic data, and phase fold light curves on a specified period. These functions will once again prove useful, so we include them here in order to simulate data above.
We also add a helper function that can visualize the invidual MCMC chains that are produced by the emcee MCMC sampler.
Helper 1
Create a function, gen_periodic_data, that creates simulated data (including noise) over a grid of user supplied positions:
$$ y = A\,cos\left(\frac{x}{P} - \phi\right) + \sigma_y$$
where $A, P, \phi$ are inputs to the function. gen_periodic_data should include Gaussian noise, $\sigma_y$, for each output $y_i$.
Can you write Python code for it?

def gen_periodic_data(x, period=1, amplitude=1, phase=0, noise=0):
    '''Generate periodic data given the function inputs

    y = A*cos(x/p - phase) + noise

    Parameters
    ----------
    x : array-like
        input values to evaluate the array

    period : float (default=1)
        period of the periodic signal

    amplitude : float (default=1)
        amplitude of the periodic signal

    phase : float (default=0)
        phase offset of the periodic signal

    noise : float (default=0)
        variance of the noise term added to the periodic signal

    Returns
    -------
    y : array-like
        Periodic signal evaluated at all points x
    '''

    y = amplitude*np.sin(2*np.pi*x/(period) - phase) + np.random.normal(0, np.sqrt(noise), size=len(x))
    return y


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 1b
Write a function sl_periodogram to measure the string length for input data x, y, over a frequency grid f_grid.
Can you write Python code for it?

def sl_periodogram(x, y, f_grid = np.linspace(0.1,10,10)):
    '''Calculate the string length "periodogram"

    Parameters
    ----------
    x : array-like
        input time of observations

    y : array-like
        measured signal at input x

    f_grid : array_like (default=np.linspace(0.1,10,10))
        frequency grid for the period

    Returns
    -------
    sl_psd : array_like
        String length at every test frequency f

    '''

    sl_psd = np.zeros_like(f_grid)
    for f_num, f in enumerate(f_grid):
        sl_psd[f_num] = # complete

    return sl_psd


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 1c
Plot the string length periodogram for the simulated data. Does it make sense?
Hint - think about the optimal grid from Notebook III
Can you write Python code for it?

f_grid = np.arange(1/np.ptp(t_obs), 10, 1/5/np.ptp(t_obs))
sl_psd = sl_periodogram( # complete

fig, ax = plt.subplots()
ax.plot(1/f_grid, sl_psd, '0.2', lw=2)
ax.set_xlabel('Period (d)')
ax.set_ylabel('String Length')

ax.axvline(0.7, color='DarkOrange',
          lw=1, ls='--')

axins = plt.axes([.29, .22, .65, .27])
axins.plot(1/f_grid, sl_psd, '0.2', lw=2)
axins.axvline(0.7, color='DarkOrange',  lw=1, ls='--')
axins.set_xlim(0,3)

fig.subplots_adjust(left=0.1, right=0.99, top=0.98, bottom=0.1)


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 2b
Write a function pdm_periodogram to measure the relative reduction in the scatter for a phase-folded light curve with input data x, y, over a frequency grid f_grid.
Plot the periodogram.
Can you write Python code for it?

def pdm_periodogram(x, y, f_grid = np.linspace(0.1,10,10), **kwargs):
    '''Calculate the phase dispersion minimization "periodogram"

    Parameters
    ----------
    x : array-like
        input time of observations

    y : array-like
        measured signal at input x

    f_grid : array_like (default=np.linspace(0.1,10,10))
        frequency grid for the period

    Returns
    -------
    pdm_psd : array_like
        PDM at every test frequency f

    '''

    pdm_psd = np.zeros_like(f_grid)
    total_rms = np.std(y, ddof=1)
    for f_num, f in enumerate(f_grid):
        pdm_psd[f_num] = calc_pdm( # complete

    return pdm_psd

f_grid = np.arange(1/np.ptp(t_obs), 10, 1/5/np.ptp(t_obs))
pdm_psd = pdm_periodogram( # complete

print(f'The best-fit period is {1/f_grid[np.argmin(pdm_psd)]:.4f} d')

fig, ax = plt.subplots()
ax.plot(1/f_grid, pdm_psd, '0.2', lw=2)
ax.set_xlabel('Period (d)')
ax.set_ylabel('PDM statistic')
ax.axline((0.7,np.mean(pdm_psd)), (0.7, np.mean(pdm_psd)+1e-3),
          color='DarkOrange',  lw=1, ls='--')

axins = plt.axes([.29, .22, .65, .27])
axins.plot(1/f_grid, pdm_psd, '0.2', lw=2)
axins.axline((0.7,np.mean(pdm_psd)), (0.7, np.mean(pdm_psd)+1e-3),
          color='DarkOrange',  lw=1, ls='--')
axins.set_xlim(0,3)
fig.subplots_adjust(left=0.1, right=0.99, top=0.98, bottom=0.1)


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 4b
Plot the smooth representation of the data with spans of 0.05, 0.2, and 0.5 folded at a period of 0.7 d.
pseudocode 1
Can you write Python code for it?

phases = (t_obs/0.7) % 1

smooth_tweeter = smooth(y, t_obs, 1/0.7, span=0.05, y_unc=y_unc)
smooth_midrange = smooth(y, t_obs, 1/0.7, span=0.2, y_unc=y_unc)
smooth_woofer = smooth(y, t_obs, 1/0.7, span=0.5, y_unc=y_unc)

phase_plot(t_obs, y, 0.7, y_unc = 0.1*np.ones_like(y))
plt.plot(np.sort(phases), smooth_tweeter[np.argsort(phases)],
       label="span = 0.05")
plt.plot(np.sort(phases), smooth_midrange[np.argsort(phases)],
       label="span = 0.2")
plt.plot(np.sort(phases), smooth_woofer[np.argsort(phases)],
       label="span = 0.5")
plt.legend()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 4d
Smooth the best_span array using a span = 0.2, to create a smooth representation of the best smooth.
pseudocode 3
Can you write Python code for it?

span_midrange = smooth(best_span, t_obs, 1/0.7, span=0.2)

fig, ax = plt.subplots()
ax.plot(np.sort(phases), span_midrange[np.argsort(phases)])
ax.set_xlabel('phase')
ax.set_ylabel('optimal smooth')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 4h
Write a function supersmooth_periodogram to calculate the Supersmoother periodogram.
Unlike the other methods above, we can in this case calculate $\chi^2$ at each frequency. Use $\chi^2_0$ as a relative baseline as we did in calculating the LS periodogram.
Can you write Python code for it?

def supersmooth_periodogram(y, y_unc, x, f_grid):
    psd = np.empty_like(f_grid)
    chi2_0 = np.sum(((y - np.mean(y))/y_unc)**2)

    for f_num, f in enumerate(f_grid):
        supersmooth = calc_supersmooth(y, x, f, y_unc=y_unc)
        chi2 = np.sum((y - supersmooth)**2/y_unc**2)
        psd[f_num] = 0.5*(chi2_0 - chi2)

    return psd


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 4i
Calculate and plot the supersmoother periodogram for the simulated data.
Hint - this is much slower than above, use a frequency grid that goes from 0.6 to 3 and has 1000 points.
Can you write Python code for it?

f_grid = np.linspace(0.6, 3, 1000)

ss_psd = supersmooth_periodogram(y, y_unc, t_obs, f_grid)

fig, ax = plt.subplots()
ax.plot(1/f_grid, ss_psd)
ax.axvline(0.7, color='DarkOrange',
          lw=1, ls='--')
ax.set_xlabel('Period (d)')
ax.set_ylabel('Power')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5c
Write a function lnlike to calculate the log likelihood for the data given the model parameters theta. theta should include the log of the period, the log of the amplitude of the signal, the GP mean value b, and the log of $\Gamma$.
Hint - execute cell below.
$\log$ in all instances here and below refers to the natural logarithm, or $\log$ base $\mathcal{e}$.
Can you write Python code for it?

def lnlike(theta, t, y, yerr):
    lnper, lna, b, lngamma = theta
    gp = george.GP(np.exp(lna) *
                   kernels.ExpSine2Kernel(np.exp(lngamma), lnper))
    gp.compute(t, yerr)
    return gp.lnlikelihood(y - model(theta, t), quiet=True)


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5d
Write a function lnprior to calculate the log of the prior on theta. Use a wide and flat prior on every parameter. For numerical reasons, the function should return -np.inf if the prior probability is equal to zero.
Hint - execute the cell below
Can you write Python code for it?

def lnprior(theta):
    lnper, lna, b, lngamma = theta
    if (-20 < lna < 20 and
        -20 < b < 20 and
        -20 < lngamma < 20 and
        -10 < lnper < np.log(10)):
        return 0.0
    return -np.inf


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5e
Write a function lnprob to calculate the log of the product of the likelihood with the prior.
Can you write Python code for it?

def lnprob(p, x, y, yerr):
    lp = lnprior(p)
    return lp + lnlike(p, x, y, yerr) if np.isfinite(lp) else -np.inf


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5g
Initialize one third of your 150 walkers around each of the periods identified in the previous problem.
Run the MCMC for 500 steps following this initialization.
Can you write Python code for it?

initial1 = np.array([np.log(0.735), 1, 10, 1])
ndim = len(initial1)
nwalkers = 50
p1 = [np.array(initial1) + 1e-4 * np.random.randn(ndim)
      for i in range(nwalkers)]

initial2 = np.array([np.log(0.367), 1, 10, 1])
ndim = len(initial2)
nwalkers = 50
p2 = [np.array(initial2) + 1e-4 * np.random.randn(ndim)
      for i in range(nwalkers)]

initial3 = np.array([np.log(0.211), 1, 10, 1])
ndim = len(initial3)
nwalkers = 50
p3 = [np.array(initial3) + 1e-4 * np.random.randn(ndim)
      for i in range(nwalkers)]
p0 = p1+p2+p3

nwalkers = len(p0)

sampler = emcee.EnsembleSampler(nwalkers, ndim, lnprob,
                                args=(lc['hjd'],lc['mag'],lc['mag_unc']))
for sample in sampler.sample(p0, iterations=500, progress=True):
    continue


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5h
Plot the chains using the plotChains() helper function from the end of this notebook.
Can you write Python code for it?

paramsNames = ['ln(P)', 'ln(a)', 'b', '$ln(\gamma)$']
nburn = 350
plotChains(sampler, nburn, paramsNames)
plt.tight_layout()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5i
Plot $\ln P$ vs. log posterior.
Can you write Python code for it?

chain_lnp_end = sampler.chain[:,-1,0]
chain_lnprob_end = sampler.lnprobability[:,-1]
fig, ax = plt.subplots()
ax.scatter(chain_lnp_end, chain_lnprob_end, alpha=0.1)
ax.set_xlabel('ln(P)')
ax.set_ylabel('ln(Probability)')
fig.tight_layout()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5j
Now we will start a new set of MCMC chains all initialized around a "random ball" centered on the maximum a posteriori value from the previous set of simulations.$^\dagger$
Run a new MCMC with 150 walkers for 500 steps.
Plot the chains. Have they converged?
$^\dagger$It is important to remember there are no rules about where an MCMC chain is initialized. We can do whatever we want. I also caution to be mindful of this in future research endeavors, because there is also no guarantee that a finite MCMC chain has identified a global maximum for the posterior.
Can you write Python code for it?

p = p0[np.argmax(chain_lnprob_end)]
sampler.reset()

p0 = [p + 1e-8 * np.random.randn(ndim) for i in range(nwalkers)]
sampler.reset() # do not continue the existing chains
for sample in sampler.sample(p0, iterations=500, progress=True):
    continue

paramsNames = ['ln(P)', 'ln(a)', 'b', '$ln(\gamma)$']
nburn = 250
plotChains(sampler, nburn, paramsNames)
plt.tight_layout()


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
Problem 5k
Make a corner plot of the samples. What is the marginalized estimate$^\dagger$ for the period of this source?
How does this estimate compare to LS?
Given that we ultimately used wide and flat priors, which effectively played no role in the analysis of this problem, the discerning viewer might ask "Why did we bother to use Bayesian analysis at all?"
This problem answers that question – the data cannot be "marginalized" (i.e., we ignore everything but the final distribution on the period $P$) in any context except a Bayesian one. Ultimately, marginalization requires an integral where we "integrate out" our ignorance of the model parameters that we do not care about. These sorts of integrals require a Bayesian view of probability.
Can you write Python code for it?

samples = sampler.chain[:, nburn:, :].reshape((-1, ndim))
plot_samples = samples.copy()
plot_samples[:,0] = np.exp(samples[:,0])
fig = corner.corner(plot_samples,
                    labels=paramsNames,
                    quantiles=[0.16,0.50,0.84])

p16, p50, p84 = np.percentile(samples[:,0], [16,50,84])

print('ln(P) = {:.6f} +{:.6f} -{:.6f}'.format(p50, p84-p50, p50-p16))

print('GP Period = {:.6f}'.format(np.exp(p50)))


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
The string length method was able to recover the correct period for the simulated data.
The main downside to this method is that it does not account for the observational uncertainties at all.
Method 2) Phase Dispersion Minimization
Phase Dispersion Minimization (PDM; Jurkevich 1971, Stellingwerth 1978), like LS, folds the data at a large number of trial frequencies $f$.
The phased data are then binned, and the variance is calculated in each bin, combined, and compared to the overall variance of the signal. No functional form of the signal is assumed, and thus, non-sinusoidal signals can be found.

 (credit: Gaveen Freer - http://slideplayer.com/slide/4212629/#)

Problem 2a
Write a function called calc_pdm, that calculates the average dispersion/scatter in $N$ equally spaced bins for a phase-folded light curve with observations x, y, and frequency f. Specify the number of bins $N$ via keyword argument bins.
Can you write Python code for it?

def calc_pdm(x, y, f=1, bins=10):
    '''Calculate the phase dispersion minimization for observations at frequency f

    Parameters
    ----------
    x : array-like
        input time of observations

    y : array-like
        measured signal at input x

    f : float (default=1)
        frequency of the test period

    bins : int (default=10)

    Returns
    -------
    pdm : float
        the sum of the scatter in each bin
    '''
    phases = x*f % 1
    # complete
    # complete
    # complete
    # complete

    return pdm


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
We need a starting position for our MCMC chains/walkers. The use of a quasi-periodic kernel means the posterior is highly non-linear, so we cannot expect to start the walkers anywhere and still achieve reasonable results.
In this case we will use a little common sense, plus a little computational "brute force".
Problem 5f
Run LombScarge on the data and determine the top three peaks in the periodogram. Set nterms = 2, and the maximum frequency to 5 (this is arbitrary but sufficient in this case, since the period is > 0.2 d).
Hint - you may need to search more than the top 3 periodogram values to find the 3 peaks.
In Angus et al. 2018 a far more sophisticated approach is used for initializing the walkers using the autocorrelation function and adjusting the prior on $P$ - if you want to use GPs to infer periods for real observations I cannot recommend that source enough.
Can you write Python code for it?

frequency, power = LombScargle(lc['hjd'], lc['mag'], lc['mag_unc'], nterms=2).autopower(maximum_frequency=5)

print('Top LS period is {}'.format(1/frequency[np.argmax(power)]))
print(1/frequency[np.argsort(power)[::-1][0:5]])


I am working on the file "Sessions/Session13/Day4/NonparametricMeasuresOfPeriodicity.ipynb".
The first task is:
We now have 3 different "smooth" representations of the data. But even these "smooth" representations are a bit jagged, and in places we can see the influence of noise.
We will now identify the "optimal" span at every phase.
Probem 4c
Identify the best span at every phase via the residuals.
pseudocode 2
Can you write Python code for it?

smooth_list = np.vstack([[smooth_tweeter],
                         [smooth_midrange],
                         [smooth_woofer]])
span_list = np.vstack([[np.ones_like(smooth_tweeter)*0.05],
               [np.ones_like(smooth_midrange)*0.2],
               [np.ones_like(smooth_woofer)*0.5]])
resid = np.abs(y - smooth_list)

best_span = span_list[np.argmin(resid, axis=0), 0]


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Before we begin - download the LIGO data necessary for this notebook. This download may take a bit of time.
Problem 1) Detector Noise Curves
Read in the Hanford (H1) and Livingston (L1) amplitude spectral densities (ASD) data files (H1-ASD.txt, L1-ASD.txt).
Can you write Python code for it?

# Load the ASD data
dir = './Matched_Filter_Data/' #point to your local directory containing the data files

H1 = pd.read_csv(dir+'H1-ASD.txt', delim_whitespace=True)
L1 = pd.read_csv(dir+'L1-ASD.txt', delim_whitespace=True)


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Problem 1c
(no code) Looking at your plot of the PSDs, at approximately what frequencies are the detectors most sensitive?
Answer:
Problem 1d
These ASDs have been calculated from data sampled in the time domain. What is the maximum frequency of the ASDs? What does this imply for the minimum sampling rate for the time domain data? Explain why.
Hint: You can find the actual sampling rate in the GWOSC Open Data paper.
Can you write Python code for it?

min_sampling_rate = #complete

print('The minimum sampling rate is %.f'%(min_sampling_rate))


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Problem 2a
Plot the template waveforms $T(t)$. Try zooming into the last one or two seconds before merger (time = 0).
Can you write Python code for it?

templates = [bbh, nsbh, bns]

for i, T in enumerate(templates):
    plt.plot(#complete

plt.legend(loc = 'best')

plt.xlabel(#complete
plt.ylabel(#complete

#plt.xlim(-0.5, 0) #zoom into the last half second before merger

plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Problem 3b
Compute the Fourier transform the templates and plot the Fourier-transformed templates $\tilde{T}(f)$ as a function of frequency. Since the templates are all real, you only need to plot the positive Fourier frequencies and the magnitude of the Fourier transform.
Notice that the sampling rate is the same for all the templates (and all the timestreams), so the sampled frequencies are the same. (In other words, since the time bins are the same, the frequency bins are the same.)
Can you write Python code for it?

#first compute the corresponding frequency bins for the Fourier transform

sample_spacing = #complete
window_length = #complete
freq = np.fft.fftfreq(window_length, d=sample_spacing)

#now take the Fourier transforms of the templates

Tf_bbh = np.fft.fft(padded_bbh) #Fourier transform of the BBH padded template
Tf_nsbh = #complete #Fourier transform of the NSBH padded template
Tf_bns = #complete #Fourier transform of the BNS padded template

#select the positive frequencies and plot in log-log space

sel = freq > 0

labels = ['BBH', 'NSBH', 'BNS']
for i, Tf in enumerate([Tf_bbh, Tf_nsbh, Tf_bns]):
    plt.loglog(freq[sel], np.abs(Tf[sel]), label = labels[i])

plt.legend(loc = 'best')

plt.ylabel(#complete
plt.xlabel(#complete
plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Problem 3c
Compute the matched filter signal-to-noise ratio timeseries between the templates and the datastreams. Start by computing the matched filter SNR timeseries between the BBH template and the "(a) datastream" in the Hanford detector (H1-a).
Note: we need to interpolate the PSDs so that we can evaluate them at the same frequencies as the templates and datastreams.
Recall that one can calculate the matched filter signal-to-noise ratio timeseries by taking the inverse Fourier transform of
$$
\frac{\tilde{T}^*(f) \tilde{h}(f)}{S_n(f)}
$$
where $\tilde{T}^*(f)$ is the complex conjugate of the template in the frequency domain, $\tilde{h}(f)$ is the datastream in the frequency domain, and $S_n(f)$ is the PSD.
Can you write Python code for it?

def matched_filter_snr(template_Tf, freq_sampled, data_ht, psd):
    """ Compute the matched filter SNR time series
    template_Tf: waveform template in the frequency domain (Tf_bbh, Tf_nsbh, or Tf_bns from Problem 2)
    freq_sampled: frequency bins corresponding to template_Tf, (freq from Problem 2)
    data_ht: the datastream in the time domain, e.g. H1-a, L1-b
    psd: the dataset containing the PSD data from problem 1, either H1 or L1.
    returns: time, SNR values
    """

    #evaluate the PSD at the sampled frequencies:
    #because we are working only with real timeseries, the negative frequencies are the same as the positive frequencies

    psd_interp = np.interp(np.abs(freq_sampled), #complete

    #Fourier transform the strain data to the frequency domain
    data_hf = #complete

    #compute (unnormalized) matched filter SNR from template_Tf, data_hf, and psd_interp, in the frequency domain
    snr_freq = #complete

    #inverse Fourier transform snr_freq to the time domain
    snr_time = #complete

    return data_ht['time(s)'], np.abs(snr_time)

#compute the matched-filter SNR timeseries between the BBH template and H1-a

time, snr_time = matched_filter_snr(#complete

#plot the timeseries
plt.plot(time, snr_time)

plt.xlabel(#complete
plt.ylabel(#complete
plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Problem 3d
Plot the SNR timeseries for each template in each of the (a), (b) and (c) datastreams, in each detector (18 combinations total). Do you see spikes in the SNR value as a function of the time shift? Which waveform signals are embedded within which datastreams?
Note: each datastream may contain multiple signals.
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Problem 4 (challenge): visualize data
Experiment with whitening and bandpassing the data. This can make signals easier to see (for visualization purposes, as our eyes are not as good as matched filtering). As an example, have a look at Figure 3 of the LIGO & Virgo Data Analysis Guide.
Problem 4a
Remake the plots from Problem 3e using whitened data.
Can you write Python code for it?


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
Which waveform signals are embedded within which datastreams?
Answer:
Problem 3e
Using the results from part (d), overplot the signal time-domain waveforms over the datastreams that contain them. You can do this just for H1 datastreams, and just for one signal in each datastream. Your results should illustrate the power of matched filtering.
Note: think about how the zero time $\tau = 0$ is defined.
Can you write Python code for it?

# first we need to calculate the time at which the SNR timeseries peaks. We pick the loudest peak.

def calculate_tau_maxSNR(#complete

    return tau

tau_a = calculate_tau_maxSNR(#complete #time at which the correct template peaks in the H1a datastream

tau_b = calculate_tau_maxSNR(#complete #time at which the correct template peaks in the H1b datastream

tau_c = calculate_tau_maxSNR(#complete #time at which the correct template peaks in the H1c datastream

times = H1a['time(s)']

#make the plot for H1-a
plt.title("Datastream a")

plt.plot(times, H1a['strain']) #plot the datastream
plt.plot(#complete #overplot the correct template with the correct timelag
plt.xlabel(#complete
plt.ylabel(#complete
plt.xlim(0, 600)
plt.show()

#make the plot for H1-b
plt.title("Datastream b")
plt.plot(times, H1b['strain'])
plt.plot(#complete #overplot the correct template with the correct timelag
plt.xlabel(#complete
plt.ylabel(#complete
plt.xlim(0, 600)
plt.show()

#make the plot for H1-c
plt.title("Datastream c")
plt.plot(times, H1c['strain'])
plt.plot(#complete #overplot the correct template with the correct timelag
plt.xlabel(#complete
plt.ylabel(#complete
plt.xlim(0, 600)
plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorial.ipynb".
The first task is:
You have also been given template waveforms $T(t)$ for a binary black hole, neutron star black hole, and binary neutron star coalescence: bbh-waveform.txt, nsbh-waveform.txt and bns-waveform.txt.
Can you write Python code for it?

#load the waveform templates
bbh = pd.read_csv(dir+'bbh_waveform.txt', delim_whitespace=True)
nsbh = pd.read_csv(dir+'nsbh_waveform.txt', delim_whitespace=True)
bns = pd.read_csv(dir+'bns_waveform.txt', delim_whitespace=True)


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Before we begin - download the LIGO data necessary for this notebook. This download may take a bit of time.
Problem 1) Detector Noise Curves
Read in the Hanford (H1) and Livingston (L1) amplitude spectral densities (ASD) data files (H1-ASD.txt, L1-ASD.txt).
Can you write Python code for it?

# Load the ASD data
dir = './Matched_Filter_Data/' #point to your local directory containing the data files

H1 = pd.read_csv(dir+'H1-ASD.txt', delim_whitespace=True)
L1 = pd.read_csv(dir+'L1-ASD.txt', delim_whitespace=True)

H1


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Even with the knowledge of the correct locations of the signals, they are impossible to see by eye.  Thank you matched filtering!
Problem 4 (challenge): visualize data
Experiment with whitening and bandpassing the data. This can make signals easier to see (for visualization purposes, as our eyes are not as good as matched filtering). As an example, have a look at Figure 3 of the LIGO & Virgo Data Analysis Guide.
Problem 4a
Remake the plots from Problem 3e using whitened data.
Can you write Python code for it?

data_whitened_hf = np.fft.fft(H1a['strain'])/np.interp(np.abs(freq), H1['frequency(Hz)'], H1['ASD(Hz^-0.5)'])
data_whitened = np.fft.ifft(data_whitened_hf)

template_whitened_Tf = np.fft.fft(padded_nsbh)/np.interp(np.abs(freq), H1['frequency(Hz)'], H1['ASD(Hz^-0.5)'])
template_whitened = np.fft.ifft(template_whitened_Tf)

plt.plot(times, np.absolute(data_whitened)*np.real(np.sign(data_whitened)), alpha = 0.5)
plt.plot(times + tau_a, np.absolute(template_whitened) * np.real(np.sign(template_whitened)), alpha = 0.5)
plt.xlim(0, 600)


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 1b
Plot the PSDs for Hanford and Livingston.
Can you write Python code for it?

plt.loglog(H1['frequency(Hz)'], H1['PSD'], label = 'H1')
plt.loglog(L1['frequency(Hz)'], L1['PSD'], label = 'L1')
plt.legend(loc = 'best')

plt.ylabel('PSD (Hz$^{-1}$)')
plt.xlabel('frequency (Hz)')

plt.xlim(10, 2000)
plt.ylim(10**-47,10**-37)

plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 1c
(no code) Looking at your plot of the PSDs, at approximately what frequencies are the detectors most sensitive?
The detectors are most sensitive in the bucket around 100 Hz
Problem 1d
These ASDs have been calculated from data sampled in the time domain. What is the maximum frequency of the ASDs? What does this imply for the minimum sampling rate for the time domain data? Explain why.
Hint: You can find the actual sampling rate in the GWOSC Open Data paper.
Can you write Python code for it?

max_frequency = np.amax(H1['frequency(Hz)'])

min_sampling_rate = max_frequency*2

print('The minimum sampling rate is %.f'%(min_sampling_rate))


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 2b
Plot the datastreams $h(t)$. By eye, can you identify the presence of signals?
Can you write Python code for it?

H1_data = [H1a,H1b,H1c]
L1_data = [L1a,L1b,L1c]
labels = ['a','b','c']

# plot the strain data
f, axs = plt.subplots(nrows=3, ncols=2, sharex=True, figsize=(16,12))

# label plots
axs[0,0].set_title('Hanford', fontsize=20)
axs[0,1].set_title('Livingston', fontsize=20)

axs[2,0].set_xlabel('time (s)', fontsize=15)
axs[2,1].set_xlabel('time (s)', fontsize=15)

axs[0,0].set_ylabel('strain', fontsize=15)
axs[1,0].set_ylabel('strain', fontsize=15)
axs[2,0].set_ylabel('strain', fontsize=15)

idx=0
for H1_tmp, L1_tmp, lbl in zip(H1_data, L1_data, labels):
    axs[idx,0].plot(H1_tmp['time(s)'], H1_tmp['strain'], color='b', label='H1-'+lbl)
    axs[idx,1].plot(L1_tmp['time(s)'], L1_tmp['strain'], color='r', label='L1-'+lbl)
    axs[idx,0].legend(loc='upper left', prop={'size':15})
    axs[idx,1].legend(loc='upper left', prop={'size':15})
    axs[idx,0].set_xlim(0,H1_tmp['time(s)'].max())
    axs[idx,1].set_xlim(0,L1_tmp['time(s)'].max())
    idx+=1


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 3b
Compute the Fourier transform the templates and plot the Fourier-transformed templates $\tilde{T}(f)$ as a function of frequency. Since the templates are all real, you only need to plot the positive Fourier frequencies and the magnitude of the Fourier transform.
Note that the sampling rate is the same for all the templates (and all the timestreams), so the sampled frequencies are the same. (In other words, since the time bins are the same, the frequency bins are the same.)
Can you write Python code for it?

#first compute the corresponding frequency bins for the Fourier transform

sample_spacing = (H1a['time(s)'][1]-H1a['time(s)'][0]) #in seconds
window_length = len(H1a['time(s)'])
freq = np.fft.fftfreq(window_length, d=sample_spacing)

#now take the Fourier transforms of the templates

Tf_bbh = np.fft.fft(padded_bbh)
Tf_nsbh = np.fft.fft(padded_nsbh)
Tf_bns = np.fft.fft(padded_bns)

#select the positive frequencies and plot in log-log space

sel = freq > 0

labels = ['BBH', 'NSBH', 'BNS']
for i, Tf in enumerate([Tf_bbh, Tf_nsbh, Tf_bns]):
    plt.loglog(freq[sel], np.abs(Tf[sel]), label = labels[i])

plt.legend(loc = 'best')
plt.xlim(min(freq[sel]), max(freq[sel]))
plt.ylabel(r'$\tilde{T}(f)$')
plt.xlabel('Frequency (Hz)')
plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 3c
Compute the matched filter signal-to-noise ratio timeseries between the templates and the datastreams. Start by computing the matched filter SNR timeseries between the BBH template and the "(a) datastream" in the Hanford detector (H1-a).
Note: we need to interpolate the PSDs so that we can evaluate them at the same frequencies as the templates and datastreams.
Recall that one can calculate the matched filter signal-to-noise ratio timeseries by taking the inverse Fourier transform of
$$
\frac{\tilde{T}^*(f) \tilde{h}(f)}{S_n(f)}
$$
where $\tilde{T}^*(f)$ is the complex conjugate of the template in the frequency domain, $\tilde{h}(f)$ is the datastream in the frequency domain, and $S_n(f)$ is the PSD.
Can you write Python code for it?

def matched_filter_snr(template_Tf, freq_sampled, data_ht, psd):
    """ Compute the matched filter SNR time series
    template_Tf: waveform template in the frequency domain (Tf_bbh, Tf_nsbh, or Tf_bns from Problem 2)
    freq_sampled: frequency bins corresponding to template_Tf, (freq from Problem 2)
    data_ht: the datastream in the time domain, e.g. H1-a, L1-b
    psd: the PSD data from problem 1, either H1 or L1.
    """

    #evaluate the PSD at the frequencies:
    #because we are working only with real timeseries, the negative frequencies are the same as the positive frequencies
    psd_interp = np.interp(np.abs(freq_sampled), psd['frequency(Hz)'], psd['PSD'])

    #Fourier transform the data to the frequency domain
    data_hf = np.fft.fft(data_ht['strain'])

    #compute matched filter SNR in the frequency domain
    snr_freq = template_Tf.conjugate() * data_hf / psd_interp

    #inverse Fourier transform the SNR to the time domain
    snr_time = np.fft.ifft(snr_freq)

    return data_ht['time(s)'], np.abs(snr_time)

time, snr_time = matched_filter_snr(Tf_bbh, freq, H1a, H1)

#plot the timeseries
plt.plot(time, snr_time)
plt.xlabel(r'time shift $\tau$ (seconds)')
plt.ylabel(r'SNR $\times$ constant')
plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 3d
Plot the SNR timeseries for each template in each of the (a), (b) and (c) datastreams, in each detector (18 combinations total). Do you see spikes in the SNR value as a function of the time shift? Which waveform signals are embedded within which datastreams?
Note: each datastream may contain multiple signals.
Can you write Python code for it?

template_Tfs = {"BBH" : Tf_bbh, "NSBH" : Tf_nsbh, "BNS" : Tf_bns}

datastreams = {"a": (H1a, L1a), "b": (H1b, L1b), "c": (H1c, L1c)}

def plot_snr_timeseries(datastream):
    """
    plot the SNR timeseries for each of the three templates in the given datastream
    datastream is 'a', 'b' or 'c'
    """

    #set up two subplots, one for H1 and one for L1
    fig, axs = plt.subplots(nrows=1, ncols=2, sharex=True, figsize = (12, 4))
    plt.suptitle('Datastream '+datastream)

    axs[0].set_title('H1 (Hanford)')
    axs[1].set_title('L1 (Livingston)')
    for ax in axs:
        ax.set_xlabel(r'time shift $\tau$ (seconds)')
        ax.set_ylabel(r'SNR $\times$ constant')

    #loop over the templates
    for wf in template_Tfs.keys():

        template_Tf = template_Tfs[wf]

        time, snr_H1 = matched_filter_snr(template_Tf, freq, datastreams[datastream][0], H1)

        time, snr_L1 = matched_filter_snr(template_Tf, freq, datastreams[datastream][1], L1)

        #overplot the SNR timeseries for each template in the same detector (H1 or L1)
        axs[0].plot(time, snr_H1, label = wf, alpha = 0.5)
        axs[1].plot(time, snr_L1, label = wf, alpha = 0.5)

    axs[0].legend(loc = 'best')

    plt.show()

plot_snr_timeseries("a")

plot_snr_timeseries("b")

plot_snr_timeseries("c")


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
Problem 4b
Experiment with bandpassing the whitened data (e.g. using a Butterworth filter) to remove the noisiest frequencies, and remake some plots from Problem 3e.
Can you write Python code for it?

#see https://scipy-cookbook.readthedocs.io/items/ButterworthBandpass.html
from scipy.signal import butter, lfilter

def butter_bandpass(lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return b, a

def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
    b, a = butter_bandpass(lowcut, highcut, fs, order=order)
    y = lfilter(b, a, data)
    return y

lowcut = 30
highcut = 600
fs = 1./sample_spacing

data_bandpassed = butter_bandpass_filter(data_whitened, lowcut, highcut, fs, order = 8)
plt.plot(times, data_bandpassed, alpha = 0.5)
plt.plot(times + tau_a, np.absolute(template_whitened) * np.real(np.sign(template_whitened)), alpha = 0.5)
plt.xlim(0, 600)


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
The max frequency is 8192 Hz. The sampling rate must be at least twice this, so that we avoid aliasing. The maximum frequency in the  data must be less than or equal to the Nyquist frequency, whist is half the sampling rate. The data must therefore be sampled at 16384 Hz or higher. In fact, they are sampled at 16384 Hz.
Problem 2) datastreams and template waveforms
You have been given three noisy datastreams $h(t)$, each 600 seconds long, for the Hanford and Livingston detectors: *1-a.txt, *1-b.txt and *1-c.txt, where * is H for Hanford and L for Livingston.
These data streams all contain Gaussian noise drawn from the PSDs in Problem 1. They may additionally contain signals from compact binary inspirals.
Can you write Python code for it?

# load the data
H1a = pd.read_csv(dir+'H1-a.txt', delim_whitespace=True)
H1b = pd.read_csv(dir+'H1-b.txt', delim_whitespace=True)
H1c = pd.read_csv(dir+'H1-c.txt', delim_whitespace=True)
L1a = pd.read_csv(dir+'L1-a.txt', delim_whitespace=True)
L1b = pd.read_csv(dir+'L1-b.txt', delim_whitespace=True)
L1c = pd.read_csv(dir+'L1-c.txt', delim_whitespace=True)


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
We see that "datastream a" has two NSBH signals, "datastream b" has two BBH signals, and "datastream c" has two BNS signals.
Problem 3e
Using the results from part (d), overplot the signal time-domain waveforms over the datastreams that contain them. You can do this just for H1 datastreams, and just for one signal in each datastream. Your results should illustrate the power of matched filtering.
Note: think about how the zero time $\tau = 0$ is defined.
Can you write Python code for it?

# first we need to calculate the time at which the SNR timeseries peaks. We pick the loudest peak.

def calculate_tau_maxSNR(datastream, template):
    ds = datastreams[datastream]
    tmp = template_Tfs[template]

    time, snr_H1 = matched_filter_snr(tmp, freq, ds[0], H1)

    tau = time[np.argmax(snr_H1)]

    return tau

tau_a = calculate_tau_maxSNR("a", "NSBH") #time at which the correct template peaks in the H1a datastream

tau_b = calculate_tau_maxSNR("b", "BBH") #time at which the correct template peaks in the H1b datastream

tau_c = calculate_tau_maxSNR("c", "BNS") #time at which the correct template peaks in the H1c datastream

print(tau_a, tau_b, tau_c)

times = H1a['time(s)']

plt.plot(times, H1a['strain'])
sel = np.abs(padded_nsbh) >  0 #select only the part that we didn't zero pad
plt.plot(times[sel]+tau_a, padded_nsbh[sel])
plt.xlabel("time (seconds)")
plt.ylabel("strain")
plt.title("Datastream a")
plt.xlim(0, 600)
plt.show()

plt.plot(times, H1b['strain'])
sel = np.abs(padded_bbh) >  0 #select only the part that we didn't zero pad
plt.plot(times[sel]+tau_b, padded_bbh[sel])
plt.xlabel("time (seconds)")
plt.ylabel("strain")
plt.title("Datastream b")
plt.xlim(0, 600)
plt.show()

plt.plot(times, H1c['strain'])
sel = np.abs(padded_bns) >  0 #select only the part that we didn't zero pad
plt.plot(times[sel]+tau_c, padded_bns[sel])
plt.xlabel("time (seconds)")
plt.ylabel("strain")
plt.title("Datastream c")
plt.xlim(0, 600)
plt.show()


I am working on the file "Sessions/Session13/Day5/MatchedFilteringTutorialSolutions.ipynb".
The first task is:
You have also been given template waveforms $T(t)$ for a binary black hole, neutron star black hole, and binary neutron star coalescence: bbh-waveform.txt, nsbh-waveform.txt and bns-waveform.txt.
Can you write Python code for it?

#load the waveform templates
bbh = pd.read_csv(dir+'bbh_waveform.txt', delim_whitespace=True)
nsbh = pd.read_csv(dir+'nsbh_waveform.txt', delim_whitespace=True)
bns = pd.read_csv(dir+'bns_waveform.txt', delim_whitespace=True)

bbh


I am working on the file "Sessions/Session14/Day1/Clustering.ipynb".
The first task is:
Problem 1b
As a baseline for reference, make a scatter plot of the iris data in the sepal length-sepal width plane.
Can you write Python code for it?

fig, ax = plt.subplots()
ax.scatter( # complete
# complete
# complete
# complete