{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"In /home/fruzsina/.config/matplotlib/stylelib/paper.mplstyle: \n",
"The text.latex.preview rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.\n",
"In /home/fruzsina/.config/matplotlib/stylelib/paper-elsevier.mplstyle: \n",
"The text.latex.preview rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.\n",
"Missing colon in file PosixPath('/home/fruzsina/.config/matplotlib/stylelib/kicc.mplstyle'), line 61 ('patch.linewidth = 0.5')\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n"
],
"text/plain": [
""
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import numpy as np\n",
"import pyoscode\n",
"import matplotlib.pyplot as plt\n",
"plt.ion()\n",
"import scipy.special as sp\n",
"import scipy.integrate\n",
"from scipy.optimize import minimize_scalar\n",
"\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"from ipywidgets import interact, IntSlider, FloatSlider,FloatLogSlider\n",
"%config InlineBackend.figure_formats = ['svg']\n",
"%matplotlib inline\n",
"\n",
"\n",
"from IPython.core.display import HTML\n",
"HTML(\"\"\"\n",
"\n",
"\"\"\")\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [],
"source": [
"def wkb_step_airy(t,x,dx,h,order,n):\n",
" \n",
" times = np.linspace(t,t+h,n)\n",
" ds0 = 1j*(np.sqrt(t))\n",
" ds1 = -1./4*(t**(-1))\n",
" ds2 = 1j*5./32*(t**(-5./2))\n",
" ds3 = 15./64*(t**(-4.))\n",
" s0 = 1j*2./3*((times)**(3./2)-t**(3./2))\n",
" s1 = -1./4*np.log((times)/t)\n",
" s2 = -1j*5./48*((times)**(-3./2)-t**(-3./2))\n",
" s3 = -5./64*((times)**(-3)-t**(-3))\n",
" \n",
" alls = [s0,s1,s2,s3]\n",
" allds = [ds0,ds1,ds2,ds3]\n",
" fp = np.exp(sum(alls[:order+1]))\n",
" fm = np.conj(fp)\n",
" dfp = sum(allds[:order+1])\n",
" dfm = np.conj(dfp)\n",
" \n",
" Ap = (dx - x*dfm)/(dfp - dfm)\n",
" Am = (dx - x*dfp)/(dfm - dfp)\n",
" \n",
" y = Ap*fp + Am*fm\n",
" return times,y\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {
"hidePrompt": false,
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"# pyoscode: fast solutions of oscillatory ODEs in physics\n",
"\n",
"\n",
"\n",
"### Fruzsina Agocs\n",
"\n",
"\n",
"\n",
"
\n",
"
\n",
"
\n",
"
\n",
"
\n",
"\n",
"To run these slides in jupyter,\\\n",
"head to https://github.com/fruzsinaagocs/oscode and\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Introduction"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"- pyoscode is a numerical solver for **osc**illatory **o**rdinary **d**ifferential **e**quations\n",
"- ODEs can be oscillatory through\n",
" - an oscillatory forcing term, $$\\ddot{x} + x = C\\cos{\\omega t},\\; \\omega \\gg 1$$\n",
" - oscillatory coefficients, $$\\dot{x} = xt + 10x\\cos{\\omega t},\\; \\omega \\gg 1$$\n",
" - slowly changing, but high frequency, $$\\ddot{x} + 2\\gamma(t)\\dot{x} + \\omega^2(t) x = 0,\\; \\omega \\gg 1$$\n",
"- oscode deals with the third type:\n",
" - one-dimensional\n",
" - ordinary\n",
" - $\\omega(t)$, $\\gamma(t)$ vary slowly for _at least part of the integration range_"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"### Motivation"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"- all extremely common in physics: pendula, suspension, circuits, celestial mechanics, ...\n",
"- even if $\\omega(t)$ changes slowly, if $\\omega \\gg 1$, numerical solution slow\n",
"- e.g. $\\ddot{x} + tx = 0$, the Airy equation:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"slideshow": {
"slide_type": "-"
}
},
"outputs": [],
"source": [
"def f(t,x):\n",
" return np.array([x[1],-t*x[0]])\n",
"\n",
"ti = 1.0\n",
"tf = 1e3\n",
"x0 = np.array([sp.airy(-ti)[0], -sp.airy(-ti)[1]])"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"slideshow": {
"slide_type": "-"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True\n",
"x(t=1000.0)=0.0302, dx/dt=-2.3852\n",
"analytic solution: x=0.0560, dx/dt=-2.6331\n",
"CPU times: user 1.63 s, sys: 0 ns, total: 1.63 s\n",
"Wall time: 1.64 s\n"
]
}
],
"source": [
"%%time\n",
"sol = scipy.integrate.solve_ivp(f,(ti,tf),x0,rtol=1e-3)\n",
"print(sol.success)\n",
"print(\"x(t={})={:.4f}, dx/dt={:.4f}\".format(tf,sol.y[0][-1],sol.y[1][-1]))\n",
"print(\"analytic solution: x={:.4f}, dx/dt={:.4f}\".format(sp.airy(-tf)[0],-sp.airy(-tf)[1]))"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"- why the noticeable wall time?\n",
" - `scipy.integrate.solve_ivp`'s default method is 4(5) order Runge-Kutta\n",
" - Runge-Kutta excellent for non-stiff, non-oscillatory equations/regions\n",
" - cannot deal with high-frequency oscillations (will see why)\n",
" - the Airy frequency $\\omega(t) = \\sqrt{t}$, steadily increases with time"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## A brief summary of the algorithm\n",
"\n",
"Based on [arxiv:1906.01421](https://arxiv.org/abs/1906.01421)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"### Runge-Kutta (RK) steps \n",
"\n",
"- $ \\dot{\\vec{x}} = F(\\vec{x}) $\n",
"- have (estimate of) solution $x$ at $t_i$, want to get $x$ at $t_i+h$\n",
"- Taylor expand: \n",
"\n",
"$$x(t_i+h) = x(t_i) + hF(x(t_i)) + \\frac{1}{2}h^2\\left.\\frac{dF}{dt}\\right\\vert_{t=t_i} + \\mathcal{O}(h^3)$$\n",
"\n",
"- and replace derivatives of $F$ with evaluations of $F$ at gridpoints:\n",
"\n",
"$$ x(t_i+h) = x(t_i) + \\sum_{j,\\;t_i only affecting amplitude/phase"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"### “RKWKB” method\n",
"\n",
"- step along the numerical solution (initial value problem solver)\n",
"- 2 components:\n",
" - _adaptive stepsize_ : change stepsize ($h$) based on estimate of local error\n",
" - _switching_ : between RK and WKB steps\n",
" - whichever gives larger $h$ to keep local error within user-specified tolerance\n",
" - use common set of function evaluations $\\to$ minimum evaluations per step\n",
"- local error estimated as difference between $n^{\\mathrm{th}}$ and $(n-1)^{\\mathrm{th}}$ method\n",
"- details in [arxiv:1906.01421](https://arxiv.org/abs/1906.01421)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"hideCode": true,
"hidePrompt": true,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "e3e16221fa564f099c5dea011fbcb982",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"interactive(children=(FloatSlider(value=-20.0, continuous_update=False, description='tf', max=20.0, min=-20.0,…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "5d66d0cbe4a1472d8e3b0a370b53a25b",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"interactive(children=(FloatSlider(value=-20.0, continuous_update=False, description='tf', max=20.0, min=-20.0,…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"n = 40.\n",
"ts1 = np.linspace(-n/2,n/2,100000)\n",
"ws1 = np.log(np.sqrt((n**2-1.)))-np.log(1+ts1**2)\n",
"gs1 = np.zeros_like(ts1)\n",
"ti1 = ts1[0]\n",
"tf_dense = ts1[-1]\n",
"xi1 = np.sqrt(1.+ti1**2)/n*np.cos(n*np.arctan(ti1))\n",
"dxi1 = 1./(n*np.sqrt(1+ti1**2))*(ti1*np.cos(n*np.arctan(ti1))-n*np.sin(n*np.arctan(ti1)))\n",
"xs_ana = np.sqrt(1.+ts1**2)/n*(np.cos(n*np.arctan(ts1))) \n",
"solution1 = pyoscode.solve(ts1,ws1,gs1,ti1,tf_dense,xi1,dxi1,logw=True,rtol=1e-3)\n",
"\n",
"def burst_f(t,y):\n",
" return np.array([y[1],-(n**2-1.)/(1.+t**2)**2*y[0]])\n",
"\n",
"def burst_rk(tf):\n",
" \n",
" sol = scipy.integrate.solve_ivp(burst_f,(ti1,tf),np.array([xi1,dxi1]),rtol=1e-3)\n",
" t_rk = sol.t\n",
" final_ind = np.argmax(t_rk >= tf)\n",
" \n",
" t_rk = t_rk[:final_ind+1]\n",
" x_rk = sol.y[0][:final_ind+1]\n",
" \n",
" plt.figure(figsize=(10,2.5))\n",
" plt.tight_layout()\n",
" plt.title(\"scipy.integrate.solve_ivp solving the equation $\\ddot{x}+\\\\frac{n^2-1}{(1+t^2)^2}x=0$, with $n=40$\")\n",
" plt.plot(ts1,xs_ana,label='analytic solution',color='black',lw=0.7)\n",
" plt.xlabel('t')\n",
" plt.ylabel('x')\n",
" plt.plot(t_rk,x_rk ,'.',label='RK',ms=9.0,color='C1')\n",
" plt.legend() \n",
"\n",
"def burst(tf):\n",
" \n",
" t_osc = np.array(solution1['t'])\n",
" final_ind = np.argmax(t_osc >= tf)\n",
" \n",
" t_osc = t_osc[:final_ind+1]\n",
" x_osc = np.array(solution1['sol'])[:final_ind+1]\n",
" types = np.array(solution1['types'])[:final_ind+1]\n",
" t_rk = t_osc[types==0]\n",
" x_rk = x_osc[types==0]\n",
" t_wkb = t_osc[types==1]\n",
" x_wkb = x_osc[types==1]\n",
" plt.figure(figsize=(10,2.5))\n",
" plt.tight_layout()\n",
" plt.title(\"oscode solving the equation $\\ddot{x}+\\\\frac{n^2-1}{(1+t^2)^2}x=0$, with $n=40$\")\n",
" plt.plot(ts1,xs_ana,label='analytic solution',color='black',lw=0.7)\n",
" plt.xlabel('t')\n",
" plt.ylabel('x')\n",
" plt.plot(t_rk,x_rk ,'.',label='RK',ms=9.0,color='C1')\n",
" plt.plot(t_wkb,x_wkb,'.',label='WKB',ms=9.0,color='C0')\n",
" plt.legend()\n",
" \n",
"\n",
"interact(burst, \n",
" tf=FloatSlider(min=ti1, max=tf_dense,\n",
" step=1.0, continuous_update=False, value=ti1));\n",
"\n",
"interact(burst_rk, \n",
" tf=FloatSlider(min=ti1, max=tf_dense,\n",
" step=1.0, continuous_update=False, value=ti1));"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"hideCode": true,
"hidePrompt": true,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "8dabd2d315fb4e3893789f0487df2db0",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"interactive(children=(FloatLogSlider(value=100.0, description='n2', max=5.0, min=2.0, step=1.0), Output()), _d…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def burst2(n2):\n",
" \n",
" ts2 = np.linspace(-n2,n2,500000)\n",
" ws2 = np.log(np.sqrt((n2**2-1.)))-np.log(1+ts2**2)\n",
" gs2 = np.zeros_like(ts2)\n",
" ti2 = ts2[0]\n",
" tf_dense = ts2[-1]\n",
" xi2 = np.sqrt(1.+ti2**2)/n2*np.cos(n2*np.arctan(ti2))\n",
" dxi2 = 1./(n2*np.sqrt(1+ti2**2))*(ti2*np.cos(n2*np.arctan(ti2))-n2*np.sin(n2*np.arctan(ti2)))\n",
" xs_ana2 = np.sqrt(1.+ts2**2)/n2*(np.cos(n2*np.arctan(ts2))) \n",
" solution2 = pyoscode.solve(ts2,ws2,gs2,ti2,tf_dense,xi2,dxi2,logw=True,rtol=1e-3)\n",
" \n",
" t_osc = np.array(solution2['t'])\n",
"\n",
" oscs = np.zeros(len(t_osc))\n",
" for i,t in enumerate(t_osc):\n",
" if i!=0:\n",
" oscs[i] = ((n2*np.arctan(t)/(2*np.pi))-(n2*np.arctan(t_osc[i-1])/(2*np.pi)))\n",
" else:\n",
" oscs[i] = None\n",
" \n",
" types = np.array(solution2['types'])\n",
" t_rk = t_osc[types==0]\n",
" t_wkb = t_osc[types==1]\n",
" plt.figure(figsize=(10,5))\n",
" plt.tight_layout()\n",
" plt.title(\"Number of oscillations traversed per step, $\\ddot{{x}}+\\\\frac{{n^2-1}}{{(1+t^2)^2}}x=0$, n={}\".format(n2))\n",
" plt.xlabel('t')\n",
" plt.ylabel('$N_{\\mathrm{osc}}$')\n",
" plt.semilogy(t_osc,oscs,color='black')\n",
" plt.semilogy(t_rk,oscs[types==0] ,'.',label='RK',ms=9.0,color='C1')\n",
" plt.semilogy(t_wkb,oscs[types==1],'.',label='WKB',ms=9.0,color='C0')\n",
" plt.ylim((1e-2,1e4))\n",
" plt.legend()\n",
" \n",
"\n",
"interact(burst2, \n",
" n2=FloatLogSlider(base=10, min=2, max=5,\n",
" step=1), continuous_update=False);"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"- Airy equation revisited with pyoscode (~2 s runtime with `scipy`):"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"slideshow": {
"slide_type": "-"
}
},
"outputs": [],
"source": [
"ts = np.linspace(0,1e3,5000)\n",
"ws = np.sqrt(ts)\n",
"gs = np.zeros_like(ts)\n",
"\n",
"# Initial conditions\n",
"ti = 1.0\n",
"tf = ts[-1]\n",
"x0 = sp.airy(-ti)[0]\n",
"dx0 = -sp.airy(-ti)[1]"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"slideshow": {
"slide_type": "-"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"x(t=1000.0)=0.0560+0.0000j, dx/dt=-2.6292+0.0000j\n",
"analytic solution: x=0.0560, dx/dt=-2.6331\n",
"CPU times: user 646 µs, sys: 132 µs, total: 778 µs\n",
"Wall time: 638 µs\n"
]
}
],
"source": [
"%%time\n",
"pyoscode_sol = pyoscode.solve(ts,ws,gs,ti,tf,x0,dx0,rtol=1e-3)\n",
"pyosc_y = pyoscode_sol['sol']\n",
"pyosc_dy = pyoscode_sol['dsol']\n",
"print(\"x(t={})={:.4f}, dx/dt={:.4f}\".format(tf,pyosc_y[-1],pyosc_dy[-1]))\n",
"print(\"analytic solution: x={:.4f}, dx/dt={:.4f}\".format(sp.airy(-tf)[0],-sp.airy(-tf)[1]))"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Examples in physics"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"### Cosmology\n",
"\n",
"- origins of large-scale structure thought to have been quantum fluctuations \n",
"- fluctuations grew during a period of accelerated expansion, inflation ($t < \\;\\sim 10^{-32}$ s)\n",
"- power spectrum of _gauge-invariant curvature perturbations_ $\\to$ cosmic microwave background (CMB)\n",
"- to infer physics of early universe, need to assume or compute (primordial) power spectrum\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"- power spectrum: $|$amplitude$|^2$ as a function of Fourier wavenumber $k$\n",
"- computation of spectrum _can be_ bottleneck in forward modelling, e.g.\n",
" - closed universes [arxiv:1907.08524](https://arxiv.org/abs/1907.08524)\n",
" - axion monodromy models [arxiv:1502.02114](https://arxiv.org/abs/1502.02114)\n",
" - initial conditions set at very early times [arXiv:2002.07042](https://arxiv.org/abs/2002.07042)\n",
"\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"- need time-evolution of gauge-invariant curvature perturbations of wavenumber $k$:\n",
" - Mukhanov-Sasaki equation (assuming spatially flat universe, single-field inflation)\n",
" $$ \\ddot{\\mathcal{R}}_k + 2\\left( \\frac{\\ddot{\\phi}}{\\phi} -\\frac{1}{2} \\dot{\\phi}^2 + \\frac{3}{2}\\right)\\dot{\\mathcal{R}}_k + \\left( \\frac{k}{aH} \\right)^2\\mathcal{R}_k = 0 $$\n",
" - $a$: scale factor, $H$: Hubble parameter, $\\phi$: inflaton field\n",
" - using $N = \\ln{a}$ as independent variable\n",
" - oscillator with time-dependent frequency and damping\n",
" - frequency $\\propto k \\to$ can get large, computationally challenging\n",
" - $\\omega$, $\\gamma$ not closed-form functions\n",
" "
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"hideCode": false,
"hidePrompt": true,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"