├── README.md ├── camb_theory.dat ├── old ├── Pixell Part 1.ipynb ├── Pixell Part 2.ipynb ├── PixellIntroduction.ipynb ├── PixellSoapackSymlens.ipynb └── Sims.ipynb ├── pixell_fourier_space_operations.ipynb ├── pixell_map_manipulation.ipynb ├── pixell_matched_filtering.ipynb ├── pixell_reprojection_and_resampling.ipynb ├── pixell_simulations.ipynb ├── pixell_spherical_harmonics.ipynb └── test_scalCls.dat /README.md: -------------------------------------------------------------------------------- 1 | # pixell_tutorials 2 | These notebooks have been developed by the Simons Observatory collaboration to demonstrate how one can use the [`pixell`](https://github.com/simonsobs/pixell) repository to perform analyses on rectangular (CAR) maps. 3 | 4 | Each notebook has been written to work in Google colab and can be accessed either by clicking the colab button in the notebook file above, or using the links below. In case the notebook is too large to render in your browser, simply access colab by changing `github` in the notebook url to `githubtocolab` and refresh. Note: this will open a frozen, read-only version. It will still run, but if you want to edit or save your work, you will need to first save a copy of the colab notebook in your personal Google drive. If you want to adapt these notebooks to run on your laptop or on a cluster, you can also clone the repository! However, you may need to manage the notebook dependencies manually. 5 | 6 | ## Notebooks: 7 | 8 | We recommend working through notebooks in the following order: 9 | 10 | - [Map manipulation](https://github.com/simonsobs/pixell_tutorials/blob/master/pixell_map_manipulation.ipynb): The basics of loading, plotting, and operating on recatngular (CAR) maps with `pixell` 11 | 12 | - [Fourier-space operations](https://github.com/simonsobs/pixell_tutorials/blob/master/pixell_fourier_space_operations.ipynb): Two-dimensional Fourier transforms with `pixell` 13 | 14 | - [Spherical harmonics](https://github.com/simonsobs/pixell_tutorials/blob/master/pixell_spherical_harmonics.ipynb): Spherical harmonic transforms with `pixell` 15 | 16 | - [Reprojection and resampling](https://github.com/simonsobs/pixell_tutorials/blob/master/pixell_reprojection_and_resampling.ipynb): Transforming maps to and from HEALPix to CAR, and between different geometries within `pixell` 17 | 18 | - [Matched filtering](https://github.com/simonsobs/pixell_tutorials/blob/master/pixell_matched_filtering.ipynb): Demonstrating `pixell` utilities for identifying compact objects in maps 19 | 20 | - [Simulations](https://github.com/simonsobs/pixell_tutorials/blob/master/pixell_simulations.ipynb): Demonstrating `pixell` utilities for simulating maps, including Gaussian fields, lensing, and compact objects 21 | -------------------------------------------------------------------------------- /pixell_map_manipulation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "provenance": [], 7 | "include_colab_link": true 8 | }, 9 | "kernelspec": { 10 | "name": "python3", 11 | "display_name": "Python 3" 12 | }, 13 | "language_info": { 14 | "name": "python" 15 | } 16 | }, 17 | "cells": [ 18 | { 19 | "cell_type": "markdown", 20 | "metadata": { 21 | "id": "view-in-github", 22 | "colab_type": "text" 23 | }, 24 | "source": [ 25 | "\"Open" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "source": [ 31 | "# Map Manipulation with pixell\n", 32 | "\n", 33 | "\n", 34 | "*Written by the ACT Collaboration*\n", 35 | "\n", 36 | "---\n", 37 | "\n", 38 | "This notebook, and the accompanying notebooks included in this set, are designed to help users who are new to working with [`pixell`](https://github.com/simonsobs/pixell/) get started with the package. As a set these notebooks will guide users through examples of how to read in and display maps, how to perform spherical harmonic transform and calculate simple spectra, how to transform the maps and how to study point sources in the maps.\n", 39 | "\n", 40 | "The `pixell` library allows users to load,\n", 41 | "manipulate and analyze maps stored in rectangular pixelization. It is\n", 42 | "mainly targeted for use with maps of the sky (e.g. CMB intensity and polarization maps, stacks of 21 cm intensity maps, binned galaxy positions or shear) in cylindrical projection.\n", 43 | "\n", 44 | "In this introductory notebook we will explain the basis for the sky maps used in `pixell` and walk through examples of how to read in CMB maps and inspect them. We'll also explain how to relate the pixels to locations on the sky and how to inspect smaller patches of the sky." 45 | ], 46 | "metadata": { 47 | "id": "yPF5lzLN9DJB" 48 | } 49 | }, 50 | { 51 | "cell_type": "code", 52 | "source": [ 53 | "# Download the data needed for the notebook\n", 54 | "!wget -O act_planck_dr5.01_s08s18_AA_f150_night_map_d56_I.fits https://phy-act1.princeton.edu/public/zatkins/act_planck_dr5.01_s08s18_AA_f150_night_map_d56_I.fits" 55 | ], 56 | "metadata": { 57 | "id": "GPzMLmYO98my" 58 | }, 59 | "execution_count": null, 60 | "outputs": [] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": { 66 | "id": "kDFKT4bz89Ra" 67 | }, 68 | "outputs": [], 69 | "source": [ 70 | "# Install neccesary packages\n", 71 | "!pip install pixell" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "source": [ 77 | "# Import packages\n", 78 | "from pixell import enmap, utils, enplot\n", 79 | "import numpy as np" 80 | ], 81 | "metadata": { 82 | "id": "s3We41ht94xX" 83 | }, 84 | "execution_count": null, 85 | "outputs": [] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "source": [ 90 | "## About `pixell` and `ndmap` objects\n", 91 | "\n", 92 | "The `pixell` library supports manipulation of sky maps that are represented as 2-dimensional grids of rectangular pixels. The supported projection and pixelization schemes are a subset of the schemes supported by FITS conventions. In addition, we provide support for a *plain* coordinate system, corresponding to a Cartesian plane with identically shaped pixels (useful for true flat-sky calculations).\n", 93 | "\n", 94 | "In `pixell`, a map is encapsulated in an `ndmap`, which combines two objects: a numpy array (of at least two dimensions) whose two trailing dimensions correspond to two coordinate axes of the map, and a `wcs` object that specifies the World Coordinate System. The `wcs` component is an instance of Astropy’s `astropy.wcs.wcs.WCS` class. The combination of the wcs and the shape of the numpy array completely specifies the footprint of a map of the sky, and is called the `geometry`. This library helps with manipulation of `ndmap` objects in ways that are aware of and preserve the validity of the wcs information.\n", 95 | "\n", 96 | "The `wcs` information describes the correspondence between celestial coordinates (typically the Right Ascension, or RA, in the Equatorial system) and the pixel indices in the two right-most axes. In some projections, such as CEA or CAR, rows (and columns) of the pixel grid will often follow lines of constant declination (and RA). In other projections, this will not be the case.\n", 97 | "\n", 98 | "The WCS system is very flexible in how celestial coordinates may be associated with the pixel array. By observing certain conventions, we can make life easier for users of our maps. We recommend the following:\n", 99 | "\n", 100 | "The first pixel, index [0,0], should be the one that you would normally display (on a monitor or printed figure) in the lower left-hand corner of the image. The pixel indexed by [0,1] should appear to the right of [0,0], and pixel [1,0] should be above pixel [0,0]. (This recommendation originates in FITS standards documentation.)\n", 101 | "When working with large maps that are not near the celestial poles, RA should be roughly horizontal and declination should be roughly vertical. (It should go without saying that you should also present information “as it would appear on the sky”, i.e. with RA increasing to the left!)\n", 102 | "The examples in the rest of this document are designed to respect these two conventions." 103 | ], 104 | "metadata": { 105 | "id": "RFGNWqMC9yb5" 106 | } 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "source": [ 111 | "### Creating an `ndmap`\n", 112 | "\n", 113 | "We can define an `ndmap` by using `pixell` to specify a geometry. For example, if we want to create an empty map we would do the following:\n", 114 | "\n" 115 | ], 116 | "metadata": { 117 | "id": "a34eVMiNhajB" 118 | } 119 | }, 120 | { 121 | "cell_type": "code", 122 | "source": [ 123 | "# Define area of map using numpy\n", 124 | "# pixell wants the box in the following format:\n", 125 | "# [[dec_from, RA_from], [dec_to, RA_to]]\n", 126 | "# Note RA goes \"from\" left \"to\" right!\n", 127 | "box = np.array([[-5, 10], [5, -10]]) * utils.degree\n", 128 | "\n", 129 | "# Define a map geometry\n", 130 | "# the width and height of each pixel will be .5 arcmin\n", 131 | "shape, wcs = enmap.geometry(pos=box, res=0.5 * utils.arcmin, proj='car')\n", 132 | "\n", 133 | "# Create an empty ndmap\n", 134 | "empty_map = enmap.zeros((3,) + shape, wcs=wcs)" 135 | ], 136 | "metadata": { 137 | "id": "fKrkMasI2tCN" 138 | }, 139 | "execution_count": null, 140 | "outputs": [] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "source": [ 145 | "## Inspecting maps\n", 146 | "\n", 147 | "The `ndmap` class extends the `numpy.ndarray` class, and thus has all of the usual attributes (`.shape`, `.dtype`, etc.) of an `ndarray`. It is likely that an `ndmap` object can be used in any functions that usually operate on an ndarray; this includes the usual numpy array arithmetic, slicing, broadcasting, etc.\n", 148 | "\n", 149 | "An `ndmap` must have at least two dimensions. The two right-most axes represent celestial coordinates (typically declination and RA, respectively). Maps can have arbitrary number of leading dimensions, but many of the `pixell` CMB-related tools interpret 3D arrays with shape `(ncomp,Ny,Nx)` as representing `Ny` x `Nx` maps of intensity, polarization Q and U Stokes parameters, in that order." 150 | ], 151 | "metadata": { 152 | "id": "Q3g5_r_shIAl" 153 | } 154 | }, 155 | { 156 | "cell_type": "code", 157 | "source": [ 158 | "# Check out the ndmap\n", 159 | "# does the shape make sense given the bounding box and resolution?\n", 160 | "print(empty_map.shape)\n", 161 | "print(empty_map.dtype)\n", 162 | "print(empty_map + np.pi)\n", 163 | "print(empty_map[0, 10:15, 90:95] == 0)" 164 | ], 165 | "metadata": { 166 | "id": "5XRFJaPKokI0" 167 | }, 168 | "execution_count": null, 169 | "outputs": [] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "source": [ 174 | "The `ndmap` also has a new attribute, the `wcs`:" 175 | ], 176 | "metadata": { 177 | "id": "uFKarnbjor6r" 178 | } 179 | }, 180 | { 181 | "cell_type": "code", 182 | "source": [ 183 | "print(empty_map.wcs)" 184 | ], 185 | "metadata": { 186 | "id": "8sb4i_JahHPd" 187 | }, 188 | "execution_count": null, 189 | "outputs": [] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "source": [ 194 | "It has everything we need to map pixels to and from the sky: the cylindrical projection we are using (`car`), the size of the pixels (in degrees), the location on the sky of a reference pixel (in degrees) and the location in the array of the reference pixel.\n", 195 | "\n", 196 | "NOTE: the `ndmap` data contains declination in the second-to-last axis and RA in the last axis, because this corresponds to the varying rows and columns of the array. But in the `wcs`, which is built by `astropy` outside of `pixell`, information is stored in the opposite order: RA first, then declination. Note the size of the pixels in RA is negative: the RA of pixels farther to the right in the array is *less*.\n", 197 | "\n", 198 | "We can also add a wcs to a numpy array. Sometimes this is necessary after performing a numpy operation on a ndmap as it might remove the `wcs`:" 199 | ], 200 | "metadata": { 201 | "id": "6ccUogf63Vn1" 202 | } 203 | }, 204 | { 205 | "cell_type": "code", 206 | "source": [ 207 | "stacked_map = np.concatenate([empty_map, empty_map])\n", 208 | "\n", 209 | "print(stacked_map.shape)\n", 210 | "print(stacked_map.wcs)" 211 | ], 212 | "metadata": { 213 | "id": "qxAtK54Ll672" 214 | }, 215 | "execution_count": null, 216 | "outputs": [] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "source": [ 221 | "Let's fix this:" 222 | ], 223 | "metadata": { 224 | "id": "gz67xNgbl9ef" 225 | } 226 | }, 227 | { 228 | "cell_type": "code", 229 | "source": [ 230 | "# Let's add a wcs to this data by doing this\n", 231 | "omap = enmap.ndmap(stacked_map, wcs)\n", 232 | "\n", 233 | "# Or this\n", 234 | "omap = enmap.samewcs(stacked_map, empty_map)\n", 235 | "\n", 236 | "# This does the same thing, but force-copies the data array.\n", 237 | "omap = enmap.enmap(stacked_map, wcs)" 238 | ], 239 | "metadata": { 240 | "id": "ZxuSKXvu3WxV" 241 | }, 242 | "execution_count": null, 243 | "outputs": [] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "source": [ 248 | "Note that `ndmap` and `samewcs` will not copy the underlying data array if they don’t have to; the returned object will reference the same memory used by the input array (as though you had done `numpy.asarray`). In contrast, `enmap.enmap` will always create a copy of the input data." 249 | ], 250 | "metadata": { 251 | "id": "NvfzcZqb5XRZ" 252 | } 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "source": [ 257 | "## Reading a map from disk\n", 258 | "\n", 259 | "An entire map in `FITS` or `HDF` format can be loaded using `read_map`, which is found in the module `pixell.enmap`. The `enmap` module contains the majority of map manipulation functions." 260 | ], 261 | "metadata": { 262 | "id": "1h_7mdbC5BvZ" 263 | } 264 | }, 265 | { 266 | "cell_type": "code", 267 | "source": [ 268 | "imap = enmap.read_map('act_planck_dr5.01_s08s18_AA_f150_night_map_d56_I.fits')" 269 | ], 270 | "metadata": { 271 | "id": "NX2A3eIk-y0J" 272 | }, 273 | "execution_count": null, 274 | "outputs": [] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "source": [ 279 | "Alternatively, one can select a rectangular region specified through its bounds using the box argument," 280 | ], 281 | "metadata": { 282 | "id": "vJp2SxIT59iD" 283 | } 284 | }, 285 | { 286 | "cell_type": "code", 287 | "source": [ 288 | "dec_min = -7 ; ra_min = 5 ; dec_max = 3 ; ra_max = -5\n", 289 | "\n", 290 | "# All coordinates in pixell are specified in radians\n", 291 | "box = np.array([[dec_min, ra_min], [dec_max, ra_max]]) * utils.degree\n", 292 | "\n", 293 | "imap_box = enmap.read_map(\"act_planck_dr5.01_s08s18_AA_f150_night_map_d56_I.fits\", box=box)" 294 | ], 295 | "metadata": { 296 | "id": "VAJKp6aj6AqH" 297 | }, 298 | "execution_count": null, 299 | "outputs": [] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "source": [ 304 | "We can perform computations on the array like any other array:" 305 | ], 306 | "metadata": { 307 | "id": "POgT0x9Dp6nh" 308 | } 309 | }, 310 | { 311 | "cell_type": "code", 312 | "source": [ 313 | "print(np.mean(imap))" 314 | ], 315 | "metadata": { 316 | "id": "zQ1LqS_W-8Ul" 317 | }, 318 | "execution_count": null, 319 | "outputs": [] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "source": [ 324 | "## Visualizing maps\n", 325 | "\n", 326 | "We can use the `pixell.enplot` functions to visualize ndmaps. For example, if we want to plot this imap_box we first create the plot and then show it. This can also be done with a saved map on the command line (i.e. `enplot map_name.fits`). There are several plotting options built-in to the `enplot` function. They are listed in the documentation here: https://pixell.readthedocs.io/en/latest/reference.html#module-pixell.enplot" 327 | ], 328 | "metadata": { 329 | "id": "3XcvCCF6J9dq" 330 | } 331 | }, 332 | { 333 | "cell_type": "code", 334 | "source": [ 335 | "# Code to plot maps\n", 336 | "enplot.pshow(imap_box, colorbar=True, downgrade=2)" 337 | ], 338 | "metadata": { 339 | "id": "EllLz41SKCoA" 340 | }, 341 | "execution_count": null, 342 | "outputs": [] 343 | }, 344 | { 345 | "cell_type": "markdown", 346 | "source": [ 347 | "### Selecting regions of the sky" 348 | ], 349 | "metadata": { 350 | "id": "8PaE13yJIgDB" 351 | } 352 | }, 353 | { 354 | "cell_type": "markdown", 355 | "source": [ 356 | "We may select a region of this map using array slicing. Note that wcs information is correctly adjusted when the array is sliced; for example the object returned by `imap[:50,:50]` is a view into the `imap` data attached to a new `wcs` object that correctly describes the footprint of the extracted pixels. BUT be cautious when assigning an extracted map to a new variable as operations on that variable will also affect the original map.\n" 357 | ], 358 | "metadata": { 359 | "id": "trBrnei7vkVm" 360 | } 361 | }, 362 | { 363 | "cell_type": "code", 364 | "source": [ 365 | "# view one section of the map. Note that wcs is updated\n", 366 | "print(f'Original Shape: {imap.shape}, Original WCS: {imap.wcs}')\n", 367 | "imap_extract = imap[50:100,50:100]\n", 368 | "print(f'New Shape: {imap_extract.shape}, New WCS: {imap_extract.wcs} \\n')\n", 369 | "\n", 370 | "# Visualize the map cut out\n", 371 | "plot = enplot.plot(imap_extract)\n", 372 | "enplot.show(plot)\n", 373 | "\n", 374 | "# note that opperations on imap_extract also affects imap\n", 375 | "print(f'Original Mean: {np.mean(imap)}')\n", 376 | "imap_extract *= 1e6\n", 377 | "print(f'Mean after modification: {np.mean(imap)}')\n", 378 | "\n", 379 | "# Let's get the imap back to it's original state\n", 380 | "imap = enmap.read_map('act_planck_dr5.01_s08s18_AA_f150_night_map_d56_I.fits')" 381 | ], 382 | "metadata": { 383 | "id": "ELOz2zvDgwkc" 384 | }, 385 | "execution_count": null, 386 | "outputs": [] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "source": [ 391 | "Alternatively, We can select a coordinate box to creat a subplot around by defining the bottom left and top right coordinates. This opperation will also output the correct wcs for the submap. For example, if we want to create a 0.5x0.5 deg submap around the coordinates a RA of 5 and a DEC of -1 we would use the code below. Note that changing the submap will not affect the original map." 392 | ], 393 | "metadata": { 394 | "id": "D_qX8KTsv9T0" 395 | } 396 | }, 397 | { 398 | "cell_type": "code", 399 | "source": [ 400 | "# First we need to define our coordinates and radius in radians (utils.degree converts degrees to radians)\n", 401 | "ra = 5. * utils.degree\n", 402 | "dec = -1. * utils.degree\n", 403 | "radius = 0.5 * utils.degree\n", 404 | "\n", 405 | "# Next we create our submap by defining a box in coordinate space\n", 406 | "imap_sub = imap.submap([[dec - radius, ra - radius], [dec + radius, ra + radius]])\n", 407 | "\n", 408 | "# Visualize the map corner\n", 409 | "plot = enplot.plot(imap_extract)\n", 410 | "enplot.show(plot)\n", 411 | "\n", 412 | "# Note that the shape and wcs are updated\n", 413 | "print(imap.shape, imap.wcs)\n", 414 | "print(imap_sub.shape, imap_sub.wcs, '\\n')\n", 415 | "\n", 416 | "# Opperations on the submap do not affect the mean map\n", 417 | "print(np.mean(imap))\n", 418 | "imap_sub *= 1e6\n", 419 | "print(np.mean(imap))" 420 | ], 421 | "metadata": { 422 | "id": "UmhBZaNcIlWT" 423 | }, 424 | "execution_count": null, 425 | "outputs": [] 426 | }, 427 | { 428 | "cell_type": "markdown", 429 | "source": [ 430 | "## Downgrading\n", 431 | "\n", 432 | "`enmap.downgrade` downgrades maps by an integer factor by averaging pixels. We can also downgrade by different integer factors in either direction." 433 | ], 434 | "metadata": { 435 | "id": "6FkNv0VuKQLE" 436 | } 437 | }, 438 | { 439 | "cell_type": "code", 440 | "source": [ 441 | "# Using enmap.downgrade, careful with quadrature\n", 442 | "# TODO: What do you mean careful with qudrature?\n", 443 | "\n", 444 | "# Downgrade both directions by a factor of 2\n", 445 | "imap_downgrade = imap.downgrade(2)\n", 446 | "print(imap_downgrade.shape)\n", 447 | "\n", 448 | "# Downgrade in y by 2 and in x by 3\n", 449 | "imap_downgrade = imap.downgrade((2, 3))\n", 450 | "print(imap_downgrade.shape)" 451 | ], 452 | "metadata": { 453 | "id": "Oi6iv-1gItXo" 454 | }, 455 | "execution_count": null, 456 | "outputs": [] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "source": [ 461 | "## Relating pixels to the sky\n", 462 | "\n", 463 | "The geometry specified through `shape` and `wcs` contains all the information to get properties of the map related to the sky. `pixell` always specifies the Y coordinate first. So a sky position is often in the form `(dec,ra)` where `dec` could be the declination and `ra` could be the RA in radians in the equatorial coordinate system.\n", 464 | "\n", 465 | "The pixel corresponding to ra=8, dec=2 can be obtained like" 466 | ], 467 | "metadata": { 468 | "id": "IvQMAk-rIo9C" 469 | } 470 | }, 471 | { 472 | "cell_type": "code", 473 | "source": [ 474 | "dec = 2\n", 475 | "ra = 8\n", 476 | "coords = np.deg2rad(np.array((dec,ra)))\n", 477 | "ypix, xpix = enmap.sky2pix(imap.shape, imap.wcs, coords)\n", 478 | "print(ypix, xpix)" 479 | ], 480 | "metadata": { 481 | "id": "92wV1owtKXEJ" 482 | }, 483 | "execution_count": null, 484 | "outputs": [] 485 | }, 486 | { 487 | "cell_type": "markdown", 488 | "source": [ 489 | "We can also use the map directly to perform this calculation:" 490 | ], 491 | "metadata": { 492 | "id": "90yyD1nZktXC" 493 | } 494 | }, 495 | { 496 | "cell_type": "code", 497 | "source": [ 498 | "ypix, xpix = imap.sky2pix(coords)\n", 499 | "print(ypix, xpix)" 500 | ], 501 | "metadata": { 502 | "id": "lDYISOgskqbA" 503 | }, 504 | "execution_count": null, 505 | "outputs": [] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "source": [ 510 | "We can pass a large number of coordinates for a vectorized conversion. In this case coords should have the shape (2,Ncoords), where Ncoords is the number of coordinates you want to convert, with the first row containing declination and the second row containing RA. For instance," 511 | ], 512 | "metadata": { 513 | "id": "LK5QxpA4lFUo" 514 | } 515 | }, 516 | { 517 | "cell_type": "code", 518 | "source": [ 519 | "dec = np.array([-5, 0, 3])\n", 520 | "ra = np.array([5, 0, -5])\n", 521 | "\n", 522 | "coords = np.deg2rad(np.array((dec,ra)))\n", 523 | "print(coords.shape, '\\n')\n", 524 | "\n", 525 | "ypix, xpix = imap.sky2pix(coords)\n", 526 | "print(ypix, xpix)" 527 | ], 528 | "metadata": { 529 | "id": "U9d_4ifwk3s1" 530 | }, 531 | "execution_count": null, 532 | "outputs": [] 533 | }, 534 | { 535 | "cell_type": "markdown", 536 | "source": [ 537 | "Let's find the values of the map at these positions. Most of the work is done, but we must convert each position to an integer value as the returned pixel coordinates are in general fractional." 538 | ], 539 | "metadata": { 540 | "id": "ifEDGvPIl_GR" 541 | } 542 | }, 543 | { 544 | "cell_type": "code", 545 | "source": [ 546 | "ypix = ypix.astype(int)\n", 547 | "xpix = xpix.astype(int)\n", 548 | "imap[ypix, xpix]" 549 | ], 550 | "metadata": { 551 | "id": "0WqNmYoBlmDG" 552 | }, 553 | "execution_count": null, 554 | "outputs": [] 555 | }, 556 | { 557 | "cell_type": "markdown", 558 | "source": [ 559 | "Similarly, pixel coordinates can be converted to sky coordinates" 560 | ], 561 | "metadata": { 562 | "id": "ThFxPh8knRa4" 563 | } 564 | }, 565 | { 566 | "cell_type": "code", 567 | "source": [ 568 | "ypix = 100\n", 569 | "xpix = 100\n", 570 | "pixes = np.array([ypix, xpix])\n", 571 | "dec, ra = np.rad2deg(imap.pix2sky(pixes)) # pix2sky, sky2pix work in radians\n", 572 | "print(dec, ra)" 573 | ], 574 | "metadata": { 575 | "id": "sRfCrBaZmU6O" 576 | }, 577 | "execution_count": null, 578 | "outputs": [] 579 | }, 580 | { 581 | "cell_type": "markdown", 582 | "source": [ 583 | "Using the `enmap.posmap` function, you can get a map of shape `(2,Ny,Nx)` containing the coordinate positions in radians of each pixel of the map." 584 | ], 585 | "metadata": { 586 | "id": "jTBpsryuoBfA" 587 | } 588 | }, 589 | { 590 | "cell_type": "code", 591 | "source": [ 592 | "posmap = imap.posmap()\n", 593 | "dec = posmap[0] # dec in radians\n", 594 | "ra = posmap[1] # ra in radians\n", 595 | "print(dec[0][0], ra[0][0])" 596 | ], 597 | "metadata": { 598 | "id": "GodVmtwLoE3I" 599 | }, 600 | "execution_count": null, 601 | "outputs": [] 602 | }, 603 | { 604 | "cell_type": "markdown", 605 | "source": [ 606 | "Using the `enmap.pixmap` function, you can get a map of shape `(2,Ny,Nx)` containing the integer pixel coordinates of each pixel of the map." 607 | ], 608 | "metadata": { 609 | "id": "KPcFKt39nwL3" 610 | } 611 | }, 612 | { 613 | "cell_type": "code", 614 | "source": [ 615 | "pixmap = imap.pixmap()\n", 616 | "pixy = pixmap[0]\n", 617 | "pixx = pixmap[1]\n", 618 | "print(pixy[1][0], pixx[0][1])" 619 | ], 620 | "metadata": { 621 | "id": "McXDQIFUnjo_" 622 | }, 623 | "execution_count": null, 624 | "outputs": [] 625 | }, 626 | { 627 | "cell_type": "markdown", 628 | "source": [ 629 | "## Exercise: stacking on clusters (based on [this notebook](https://github.com/ACTCollaboration/DR4_DR5_Notebooks/blob/master/Notebooks/Section_4_visualize_objects.ipynb); see also [this notebook](https://github.com/ACTCollaboration/DR6_Notebooks/blob/main/ACT_DR6_ymap_stacking.ipynb) later in the course!)\n", 630 | "\n", 631 | "We will apply what we've learned to do a common analysis technique with our ACT data: stacking maps on galaxy clusters. We might want to do this in order to learn about the gas distribution in and around clusters, or to tease out their mass from weak lensing. To do this we will need a catalogue of cluster locations and more ACT data:" 632 | ], 633 | "metadata": { 634 | "id": "m9nZ3yyCmDn4" 635 | } 636 | }, 637 | { 638 | "cell_type": "code", 639 | "source": [ 640 | "# this is a full ACT DR5 map, downgraded so that it doesn't take up too much\n", 641 | "# memory\n", 642 | "!wget -O act_planck_dr5.01_s08s18_AA_f150_night_map_dg_I.fits https://phy-act1.princeton.edu/public/zatkins/act_planck_dr5.01_s08s18_AA_f150_night_map_dg_I.fits\n", 643 | "\n", 644 | "# this is the ACT DR5 tSZ cluster catalogue...how did we make it??\n", 645 | "!wget https://astro.ukzn.ac.za/~mjh/ACTDR5/v1.0b3/DR5_cluster-catalog_v1.0b3.fits" 646 | ], 647 | "metadata": { 648 | "id": "1vklQ3KLG5h-" 649 | }, 650 | "execution_count": null, 651 | "outputs": [] 652 | }, 653 | { 654 | "cell_type": "code", 655 | "source": [ 656 | "import astropy.table as atpy\n", 657 | "\n", 658 | "# get the map\n", 659 | "imap = enmap.read_map('act_planck_dr5.01_s08s18_AA_f150_night_map_dg_I.fits')\n", 660 | "\n", 661 | "# Read in ras and decs from a cluster catalog\n", 662 | "tab = atpy.Table().read('DR5_cluster-catalog_v1.0b3.fits', format='fits')\n", 663 | "\n", 664 | "# convert them to radians\n", 665 | "ras = tab['RADeg'] * utils.degree\n", 666 | "decs = tab['decDeg'] * utils.degree\n", 667 | "\n", 668 | "print(ras.shape, decs.shape)" 669 | ], 670 | "metadata": { 671 | "id": "djNK_VWUfQaA" 672 | }, 673 | "execution_count": null, 674 | "outputs": [] 675 | }, 676 | { 677 | "cell_type": "markdown", 678 | "source": [ 679 | "What does the full ACT DR5 + Planck data look like? Here we are only using the temperature data, not polarization:" 680 | ], 681 | "metadata": { 682 | "id": "UBO_lOV9MZhF" 683 | } 684 | }, 685 | { 686 | "cell_type": "code", 687 | "source": [ 688 | "enplot.pshow(imap, downgrade=8, colorbar=True, ticks=15, range=300)" 689 | ], 690 | "metadata": { 691 | "id": "f4wVEUXBMffv" 692 | }, 693 | "execution_count": null, 694 | "outputs": [] 695 | }, 696 | { 697 | "cell_type": "markdown", 698 | "source": [ 699 | "Notice the bright band of the milky way galaxy cutting across the edge of the map. It's not on the equator because the ACT data is natively in celestial coordinates (we can play around with this later). The rest of the blobby pattern, with blobs that are about a degree in size, is the CMB!\n", 700 | "\n", 701 | "Let's cut out a pixel of size .5 degrees around a random cluster and take a look:" 702 | ], 703 | "metadata": { 704 | "id": "Yx4RrgTDMPU5" 705 | } 706 | }, 707 | { 708 | "cell_type": "code", 709 | "source": [ 710 | "# try this for a bunch of different cluster indexes!\n", 711 | "n = 128\n", 712 | "radius = 0.5 * utils.degree\n", 713 | "\n", 714 | "imap_sub = imap.submap([[decs[n] - radius, ras[n] - radius], [decs[n] + radius, ras[n] + radius]])\n", 715 | "\n", 716 | "enplot.pshow(imap_sub, upgrade=16, colorbar=True, grid=False)" 717 | ], 718 | "metadata": { 719 | "id": "B3LG8I5fMXfm" 720 | }, 721 | "execution_count": null, 722 | "outputs": [] 723 | }, 724 | { 725 | "cell_type": "markdown", 726 | "source": [ 727 | "Not a whole lot there? This is why a stack makes sense: if clusters generally look similar in the map, but their signal is very faint, we can average their locations and try to beat-down the noise. What is the source of \"noise\" in this case?" 728 | ], 729 | "metadata": { 730 | "id": "rftIfgozQb0a" 731 | } 732 | }, 733 | { 734 | "cell_type": "markdown", 735 | "source": [], 736 | "metadata": { 737 | "id": "vys_pqMxNd31" 738 | } 739 | }, 740 | { 741 | "cell_type": "code", 742 | "source": [ 743 | "stack = 0\n", 744 | "num = 0\n", 745 | "for n in range(len(decs)):\n", 746 | " stack += imap.submap([[decs[n] - radius, ras[n] - radius], [decs[n] + radius, ras[n] + radius]])\n", 747 | " num += 1\n", 748 | "\n", 749 | " if n % 500 == 0: print(f'We have done {n} clusters')" 750 | ], 751 | "metadata": { 752 | "id": "LL7jxTqoG_YR" 753 | }, 754 | "execution_count": null, 755 | "outputs": [] 756 | }, 757 | { 758 | "cell_type": "code", 759 | "source": [ 760 | "enplot.pshow(stack/num, upgrade=16, colorbar=True, grid=False)" 761 | ], 762 | "metadata": { 763 | "id": "WFKzvLFpR8kb" 764 | }, 765 | "execution_count": null, 766 | "outputs": [] 767 | }, 768 | { 769 | "cell_type": "markdown", 770 | "source": [ 771 | "Nice! Clusters do kind of look the same in the map: they look like a cold spot. Why?\n", 772 | "\n", 773 | "Notice anything else weird about the average cluster, maybe about it's shape? Does this make sense to you? Why or why not?" 774 | ], 775 | "metadata": { 776 | "id": "L0aP2HylR_1J" 777 | } 778 | } 779 | ] 780 | } 781 | -------------------------------------------------------------------------------- /pixell_simulations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "yPF5lzLN9DJB" 7 | }, 8 | "source": [ 9 | "# Simulations with pixell\n", 10 | "\n", 11 | "*Written by the ACT Collaboration*\n", 12 | "\n", 13 | "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/simonsobs/pixell_tutorials/blob/master/pixell_simulations.ipynb)\n", 14 | "\n", 15 | "---\n", 16 | "\n", 17 | "\n", 18 | "This notebook, and the accompanying notebooks included in this set, are designed to help users who are new to working with [`pixell`](https://github.com/simonsobs/pixell/) get started with the package. As a set these notebooks will guide users through examples of how to read in and display maps, how to perform spherical harmonic transform and calculate simple spectra, how to transform the maps and how to study point sources in the maps.\n", 19 | "\n", 20 | "The `pixell` library allows users to load,\n", 21 | "manipulate and analyze maps stored in rectangular pixelization. It is\n", 22 | "mainly targeted for use with maps of the sky (e.g. CMB intensity and polarization maps, stacks of 21 cm intensity maps, binned galaxy positions or shear) in cylindrical projection.\n", 23 | "\n", 24 | "In this notebook we will use pixell functions to simulate millimeter-wave sky maps, including different components such as CMB, CMB lensing, Extragalactic Point Sources and Galaxy Clusters, and simplistic noise.\n", 25 | "\n", 26 | "- [x] `enmap.randmap` (flat-sky Gaussian simulations, CMB and noise)\n", 27 | "\n", 28 | "- [x] `curvedsky.randmap` (curved-sky Gaussian simulations, CMB and noise)\n", 29 | "\n", 30 | "- [x] `lensing.lens_map` (flat-sky lensing operation)\n", 31 | "\n", 32 | "- [x] `lensing.lens_map_curved` (curved-sky lensing operation)\n", 33 | "\n", 34 | "- [x] `pointsrcs.sim_objects` (injecting point sources)\n", 35 | "\n", 36 | "\n" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": { 43 | "id": "kDFKT4bz89Ra" 44 | }, 45 | "outputs": [], 46 | "source": [ 47 | "# Install neccesary packages\n", 48 | "!pip install pixell camb" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": { 55 | "id": "s3We41ht94xX" 56 | }, 57 | "outputs": [], 58 | "source": [ 59 | "# Import packages\n", 60 | "import numpy as np\n", 61 | "import matplotlib.pyplot as plt\n", 62 | "from pixell import powspec\n", 63 | "from pixell import utils\n", 64 | "from pixell import enmap\n", 65 | "from pixell import curvedsky\n", 66 | "from pixell import lensing\n", 67 | "from pixell import pointsrcs\n", 68 | "from pixell import enplot" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": { 74 | "id": "RFGNWqMC9yb5" 75 | }, 76 | "source": [ 77 | "**Making a map from scratch**\n", 78 | "\n", 79 | "For details on how to manipulate maps in pixell take a look at [\"Pixell_map_manipulation.ipynb\"](https://github.com/simonsobs/pixell_tutorials/blob/master/Pixell_map_manipulation.ipynb). First lets make a zeros map on a certain patch of the sky, let's use the region covering right ascension from 15 to 35 degrees and declination from -10 to -2 (you can try any patch). Here we need to define the vertices of a rectangle from left bottom to top right, these are RA0,DEC0 = 35,-10 and RA1,DEC1 = 15,-2 (think about the sky as going from right ascension 180 to -180 and declinations -90 to +90. We will use 0.5 arcminutes pixels in plate carrée projection, this means all pixels cover 0.5 arcminutes in latitude and longitude" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": { 86 | "id": "NX2A3eIk-y0J" 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | " # DEC0, RA0 DEC1 RA1\n", 91 | "box = np.array([[-10, 35], [-2, 15]]) * utils.degree\n", 92 | "shape, wcs = enmap.geometry(pos=box, res=0.5*utils.arcmin, proj='car')\n", 93 | "# we will use only the temperature component or parameter stokes I\n", 94 | "omap = enmap.zeros(shape, wcs)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": { 101 | "id": "-d8ndl4btd96" 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "# we can use enplot and see what we have\n", 106 | "enplot.pshow(omap, range=500, colorbar=True, downgrade=2)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": { 112 | "id": "G9UeyFDdtipD" 113 | }, 114 | "source": [ 115 | "looks like an empty map in the region that we wanted. We can also check the pixel size, since all pixels cover 0.5 in width and height we should have 8x60/0.5 = 960 pixels in height and 20x60/0.5 = 2400 in width\n" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "metadata": { 122 | "id": "DTXdCdZ2tnLZ" 123 | }, 124 | "outputs": [], 125 | "source": [ 126 | "omap.shape" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": { 132 | "id": "6PyaTjLxPhOZ" 133 | }, 134 | "source": [ 135 | "This means that dimension = (number of stokes parameters, declination pixels, right ascension pixels)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": { 141 | "id": "_vJ5OYvI9yjp" 142 | }, 143 | "source": [ 144 | "**Gaussian realization of CMB**\n", 145 | "\n", 146 | "There are two ways of simulating things, using flat sky approximation or curved sky. Let's start with flat sky, there is a handy function from `enmap` called `rand_map`, which draws a random realization from a power spectrum. To call this function we need to pass the following arguments:\n", 147 | "\n", 148 | "`shape`: shape of the map that we want to simulate, in our case omap.shape\n", 149 | "\n", 150 | "`wcs`: world coordinates of the map, in our case omap.wcs\n", 151 | "\n", 152 | "`cov`: power spectrum that we want to include, more on this later\n", 153 | "\n", 154 | "`scalar = False`: If we have polarization components, treat them as polarization\n", 155 | "\n", 156 | "`seed = None`: The random seed that we want to use, if None if will generate one\n", 157 | "\n", 158 | "`pixel_units = False`: The input power spectrum uses steradians\n", 159 | "\n", 160 | "`iau = False`: The default polarization convention in the CMB community (sad)\n", 161 | "\n", 162 | "`spin = [0,2]`: Apply a EB -> QU rotation\n", 163 | "\n", 164 | "The three first argument are mandatory, but you may need to modify the others depending on your application (see the `pixell` documentation)." 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": { 170 | "id": "7JsQibuUPyii" 171 | }, 172 | "source": [ 173 | "We need a power spectrum, specifically we want to simulate the CMB, for that we will use the Planck TT PS (you can try any other cosmology).\n", 174 | "\n", 175 | "First we download the file in the format L TT EE BB TE\n" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "metadata": { 182 | "id": "hZB9rpVyRDT8" 183 | }, 184 | "outputs": [], 185 | "source": [ 186 | "!wget -O planck_lensedCls.dat https://raw.githubusercontent.com/simonsobs/nemo/main/nemo/data/planck_lensedCls.dat" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": { 192 | "id": "UvkiTJlPRHGb" 193 | }, 194 | "source": [ 195 | "We can plot the file to be sure if it contains the typical CMB power spectrum in the following way\n" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": { 202 | "id": "LmpnSCHxRTK6" 203 | }, 204 | "outputs": [], 205 | "source": [ 206 | "fileps = \"planck_lensedCls.dat\"\n", 207 | "L, TT, EE, BB, TE = np.loadtxt(fileps, usecols=(0, 1, 2, 3, 4), unpack=True)\n", 208 | "plt.loglog(L, TT)\n", 209 | "# plt.yscale(\"log\")\n", 210 | "plt.xlabel(r\"$\\ell$\")\n", 211 | "plt.ylabel(r\"$\\ell (\\ell+1) C_{\\ell}^{TT}/(2 \\pi)$\")" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": { 218 | "id": "6aGoL8QvGIfU" 219 | }, 220 | "outputs": [], 221 | "source": [ 222 | "TT.shape" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": { 228 | "id": "UsSrZQqRtzhu" 229 | }, 230 | "source": [ 231 | "We can then pass this Planck TT power spectrum to the pixell function `powspec.read_spectrum`, so that the array is reshaped in the form that Pixell expects, and the 2$\\pi/(\\ell (\\ell+1))$ factors are applied." 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "metadata": { 238 | "id": "UEIWlvj4Tvxh" 239 | }, 240 | "outputs": [], 241 | "source": [ 242 | "ps = powspec.read_spectrum(fileps, scale=True, expand=None)" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": { 249 | "id": "hswCbdCvFeJP" 250 | }, 251 | "outputs": [], 252 | "source": [ 253 | "ell = np.arange(len(ps[0]))\n", 254 | "plt.semilogx(L, TE)\n", 255 | "plt.semilogx(ell, ps[3] * (ell*(ell+1)) / (2*np.pi))" 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": { 261 | "id": "XtGDnuEpvsZg" 262 | }, 263 | "source": [ 264 | "The pixell function `enmap.rand_map` will then take in the shape and wcs that we defined above, as well as the power spectrum from the cell above to make the corresponding map." 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "metadata": { 271 | "id": "-dIh1pIIRvoZ" 272 | }, 273 | "outputs": [], 274 | "source": [ 275 | "# T only\n", 276 | "cmbmap = enmap.rand_map(omap.shape, omap.wcs, cov=ps[0])\n", 277 | "enplot.pshow(cmbmap, range=500, colorbar=True, downgrade=2)" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": { 283 | "id": "A7xJ4kx9UN2g" 284 | }, 285 | "source": [ 286 | "If you run the previous code multiple times, you will see a different version each time, that's because each random realization is different but it is sampled from the same probability distribution (given by the power spectrum).\n", 287 | "\n", 288 | "We can also make this polarized. We also need a 3-dimensional map to hold the simulation. We also need to change the `cov` parameter so that it includes all polarization components. We have a TT, EE, BB, and TE spectrum. We need to put this into a `(3, 3, nl)` array, rather than just an `(nl,)` array:" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": null, 294 | "metadata": { 295 | "id": "4nWl2s4hj7L0" 296 | }, 297 | "outputs": [], 298 | "source": [ 299 | "nl = ps.shape[-1]\n", 300 | "l = np.arange(nl)\n", 301 | "\n", 302 | "ps_square = np.zeros((3, 3, nl))\n", 303 | "print(ps_square.shape)\n", 304 | "\n", 305 | "ps_square[0, 0] = ps[0]\n", 306 | "ps_square[0, 1] = ps[3]\n", 307 | "ps_square[1, 0] = ps[3]\n", 308 | "ps_square[1, 1] = ps[1]\n", 309 | "ps_square[2, 2] = ps[2]\n", 310 | "\n", 311 | "plt.semilogy(l, l*(l+1)/2/np.pi*ps_square[0, 0], label='00')\n", 312 | "plt.semilogy(l, l*(l+1)/2/np.pi*ps_square[1, 1], label='11')\n", 313 | "plt.semilogy(l, l*(l+1)/2/np.pi*ps_square[2, 2], label='22')\n", 314 | "plt.ylim(1e-3, 1e4)\n", 315 | "plt.legend()\n", 316 | "plt.show()\n", 317 | "\n", 318 | "plt.plot(l, l*(l+1)/2/np.pi*ps_square[0, 1], label='01')\n", 319 | "plt.legend()" 320 | ] 321 | }, 322 | { 323 | "cell_type": "markdown", 324 | "metadata": { 325 | "id": "nM_pj4yLmgFH" 326 | }, 327 | "source": [ 328 | "Then we provide this square array as the `cov` parameter instead:" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": null, 334 | "metadata": { 335 | "id": "XinX36L4miXy" 336 | }, 337 | "outputs": [], 338 | "source": [ 339 | "# T, Q, U\n", 340 | "cmbmap_pol = enmap.rand_map((3, *shape), wcs, cov=ps_square)\n", 341 | "for i in range(3):\n", 342 | " enplot.pshow(cmbmap_pol[i], range=[500, 25, 25][i], colorbar=True, downgrade=2)" 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": { 348 | "id": "46fOKO6QUQh2" 349 | }, 350 | "source": [ 351 | "Now this is using flat sky approximation, so it uses the Fourier transform of the patch. If we want to account for the curvature of the sky, which is important for big patches we need to use the `curvedsky` module, in that module we can also find a `rand_map` function." 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": null, 357 | "metadata": { 358 | "id": "klCE_gs5USqX" 359 | }, 360 | "outputs": [], 361 | "source": [ 362 | "# full sky goes from ra,dec +180, -90 to ra, dec -180, +90\n", 363 | "# we can use bigger pixels such as 4 arcmin to avoid using too much memory\n", 364 | " #DEC0, RA0 DEC1 RA1\n", 365 | "shape, wcs = enmap.fullsky_geometry(res=4*utils.arcmin, proj='car', variant='CC')\n", 366 | "# we will use only the temperature component or parameter stokes I\n", 367 | "omap = enmap.zeros((1, *shape), wcs)\n", 368 | "# checking the map with a plot\n", 369 | "enplot.pshow(omap, range=500, downgrade=4, ticks=15, colorbar=True)" 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": { 375 | "id": "F3tSf_0rv5l_" 376 | }, 377 | "source": [ 378 | "This `curvedsky.rand_map` takes slightly different parameters than `enmap.rand_map`, in order they are\n", 379 | "\n", 380 | "`shape`: shape of the map that we want to simulate, in our case omap.shape\n", 381 | "\n", 382 | "`wcs`: world coordinates of the map, in our case omap.wcs\n", 383 | "\n", 384 | "`ps`: power spectrum that we want to include. Can take an `(nl,)` shaped ps or a `(ncomp, ncomp, nl)` ps.\n", 385 | "\n", 386 | "`lmax=None`: maximum multipole range to sample\n", 387 | "\n", 388 | "`dtype=np.float32`: type of the resulting array\n", 389 | "\n", 390 | "`seed=None`: number of the seed, if None it will generate one\n", 391 | "\n", 392 | "`spin=[0,2]`: type of spin, in we have a IQU map it is [0,2], if we give as an input a single power spectrum it will take that into consideration to generate only one map\n", 393 | "\n", 394 | "`method=\"auto\"`: the default \"auto\" is fine\n", 395 | "\n", 396 | "`verbose=False`: gives text information about the process\n", 397 | "\n", 398 | "Note: it is typically sufficient to use single-precision maps (`dtype=np.float32`) which would speed up computation and use less memory. The default for this function is double precision (`dtype=np.float64`)." 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": null, 404 | "metadata": { 405 | "id": "0Bc0gGwaUjVv" 406 | }, 407 | "outputs": [], 408 | "source": [ 409 | "# now simulating the CMB taking into account the curvature of the sky\n", 410 | "# this may take some time since it uses spherical harmonics\n", 411 | "\n", 412 | "# T only\n", 413 | "cmbmap = curvedsky.rand_map(omap.shape, omap.wcs, ps=ps[0], lmax=3000, dtype=np.float32, verbose=True)\n", 414 | "enplot.pshow(cmbmap, range=500, downgrade=4, ticks=15, colorbar=True)" 415 | ] 416 | }, 417 | { 418 | "cell_type": "markdown", 419 | "metadata": { 420 | "id": "jQpjNRplUZT6" 421 | }, 422 | "source": [ 423 | "Ok, it looks interesting. Notice the distortion at the poles; is this what you expected? To put it in context, consider this image of the earth in the same CAR projection:" 424 | ] 425 | }, 426 | { 427 | "cell_type": "markdown", 428 | "metadata": { 429 | "id": "HXP3TMUKcyjz" 430 | }, 431 | "source": [ 432 | "![](https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Equirectangular_projection_SW.jpg/1920px-Equirectangular_projection_SW.jpg)" 433 | ] 434 | }, 435 | { 436 | "cell_type": "markdown", 437 | "metadata": { 438 | "id": "0NQ3gf4GyFR0" 439 | }, 440 | "source": [ 441 | "This makes sense now. Each pixel in the top row of pixels (and bottom row) are in fact the same physical point -- the pole! Using `enmap.rand_map` in this case might make a more uniform looking picture but it would be physically wrong -- we can't use the flat sky approximation when our range of declinations is large!" 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": null, 447 | "metadata": { 448 | "id": "tshbkq-GUVNY" 449 | }, 450 | "outputs": [], 451 | "source": [ 452 | "# now simulating the CMB in this patch in the flat sky approximation\n", 453 | "# NOTE: THIS IS WRONG\n", 454 | "cmbmap = enmap.rand_map(omap.shape, omap.wcs, cov=ps[0])\n", 455 | "enplot.pshow(cmbmap, range=500, colorbar=True, downgrade=4, ticks=15)" 456 | ] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "metadata": { 461 | "id": "7f70XuQq1jmN" 462 | }, 463 | "source": [ 464 | "There are no distortions at the poles -- this is *wrong*. We need the distortions!\n", 465 | "\n", 466 | "Just like the flatsky case, we can draw a polarized simulation. We again need a map to hold the 3 polarization components, and need to give it a power spectrum that puts the components in a square:" 467 | ] 468 | }, 469 | { 470 | "cell_type": "code", 471 | "execution_count": null, 472 | "metadata": { 473 | "id": "c3TSGZwepku0" 474 | }, 475 | "outputs": [], 476 | "source": [ 477 | "# T, Q, U\n", 478 | "cmbmap_pol = curvedsky.rand_map((3, *shape), wcs, ps=ps_square, lmax=3000, dtype=np.float32, verbose=True)\n", 479 | "for i in range(3):\n", 480 | " enplot.pshow(cmbmap_pol[i], range=[500, 25, 25][i], downgrade=4, ticks=15, colorbar=True)" 481 | ] 482 | }, 483 | { 484 | "cell_type": "markdown", 485 | "metadata": { 486 | "id": "FIgq7BMw9ysb" 487 | }, 488 | "source": [ 489 | "**Lensing**" 490 | ] 491 | }, 492 | { 493 | "cell_type": "markdown", 494 | "metadata": { 495 | "id": "y2TT6w_8xldi" 496 | }, 497 | "source": [ 498 | "To make flat-sky lensing simulations, we need unlensed CMB power spectra, as well as a lensing potential power spectrum. We obtain these from CAMB by running the following cells:" 499 | ] 500 | }, 501 | { 502 | "cell_type": "code", 503 | "execution_count": null, 504 | "metadata": { 505 | "id": "12A-gpoJ4QyW" 506 | }, 507 | "outputs": [], 508 | "source": [ 509 | "import camb\n", 510 | "\n", 511 | "pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.06, omk=0, tau=0.06,\n", 512 | " As=2e-9, ns=0.965, halofit_version='mead', lmax=4000)\n", 513 | "pars.set_for_lmax(4000, lens_potential_accuracy=4)\n", 514 | "\n", 515 | "# calculate results for these parameters\n", 516 | "results = camb.get_results(pars)\n", 517 | "powers = results.get_cmb_power_spectra(pars, CMB_unit='muK', raw_cl=True)\n", 518 | "TT, EE, BB, TE = powers[\"unlensed_scalar\"].T\n", 519 | "ell = np.arange(len(TT))\n", 520 | "\n", 521 | "# for simplicity assume lensing is uncorrelated with the CMB power spectra, but\n", 522 | "# we could include correlations too as CAMB calculates them\n", 523 | "PP = powers[\"lens_potential\"][:, 0]" 524 | ] 525 | }, 526 | { 527 | "cell_type": "markdown", 528 | "metadata": { 529 | "id": "ey-PxhdX1sGT" 530 | }, 531 | "source": [ 532 | "Before we create the lensed simulations, we need to package the CMB and lensing potential power spectra in a format that pixell will understand:" 533 | ] 534 | }, 535 | { 536 | "cell_type": "code", 537 | "execution_count": null, 538 | "metadata": { 539 | "id": "x1VNVLpyObKx" 540 | }, 541 | "outputs": [], 542 | "source": [ 543 | "ps_init = np.vstack((ell, TT, EE, TE, PP))\n", 544 | "ps_init = np.atleast_2d(ps_init)\n", 545 | "ps_init = powspec.expand_inds(np.array(ps_init[0],dtype=int), ps_init[1:])\n", 546 | "ps_cmb = ps_init[:3]\n", 547 | "ps_cmb = powspec.sym_expand(ps_cmb, scheme=\"diag\", ncomp=3)\n", 548 | "ps_lens = ps_init[3]" 549 | ] 550 | }, 551 | { 552 | "cell_type": "markdown", 553 | "metadata": { 554 | "id": "2ueUvhz_19A2" 555 | }, 556 | "source": [ 557 | "We are then ready to use the pixell function `enmap.rand_map` to make a $\\phi$ map from the lensing potential power spectrum, as well as unlensed CMB maps from the CMB power spectra.\n", 558 | "\n", 559 | "Let's be careful to go back to the small patch of sky for which we used `enmap.rand_map` in the beginning of this notebook, where the flat-sky limit is approximately valid:" 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": null, 565 | "metadata": { 566 | "id": "UFF6_hOB17n4" 567 | }, 568 | "outputs": [], 569 | "source": [ 570 | " # DEC0, RA0 DEC1 RA1\n", 571 | "box = np.array([[-10, 35], [-2, 15]]) * utils.degree\n", 572 | "shape, wcs = enmap.geometry(pos=box, res=0.5*utils.arcmin, proj='car')\n", 573 | "\n", 574 | "phi_map = enmap.rand_map(shape, wcs, cov=ps_lens)\n", 575 | "cmbmap = enmap.rand_map((3, *shape), wcs, cov=ps_cmb)" 576 | ] 577 | }, 578 | { 579 | "cell_type": "markdown", 580 | "metadata": { 581 | "id": "PTeynEA72dDM" 582 | }, 583 | "source": [ 584 | "The pixell function `lensing.lens_map` will take in the CMB map we created above, as well as the gradient of the $\\phi$ map and make a lensed version of the CMB maps. It has the following arguments:\n", 585 | "\n", 586 | "`imap`: the unlensed map with shape `(..., ny, nx)` to be lensed\n", 587 | "\n", 588 | "`grad_phi`: the map of the gradient of the lensing potential with shape `(2, ny, nx)`, obtained from `enmap.grad_phi`\n", 589 | "\n", 590 | "`order=3`: related to the pixel-space interpolation (fine to keep)\n", 591 | "\n", 592 | "`mode=\"spline\"`: same as above\n", 593 | "\n", 594 | "`border=\"cyclic\"`: same as above\n", 595 | "\n", 596 | "`trans=False`: if True, perform adjoint of lensing (not delensing)\n", 597 | "\n", 598 | "`deriv=False`: whether to return derivatives of the interpolation\n", 599 | "\n", 600 | "`h=1e-7`: finite difference size for the derivative\n", 601 | "\n", 602 | "For standard lensing, we can accept all the defaults and just pass our unlensed map and lensing realization:" 603 | ] 604 | }, 605 | { 606 | "cell_type": "code", 607 | "execution_count": null, 608 | "metadata": { 609 | "id": "2-XwNNvI2at7" 610 | }, 611 | "outputs": [], 612 | "source": [ 613 | "grad_phi = enmap.grad(phi_map)\n", 614 | "lensedcmb = lensing.lens_map(cmbmap, grad_phi, order=3, mode=\"spline\", border=\"cyclic\", trans=False, deriv=False, h=1e-7)" 615 | ] 616 | }, 617 | { 618 | "cell_type": "markdown", 619 | "metadata": { 620 | "id": "blYntsOt2v46" 621 | }, 622 | "source": [ 623 | "Let's take a look at the lensed CMB maps we just created, followed by the corresponding unlensed CMB map:" 624 | ] 625 | }, 626 | { 627 | "cell_type": "code", 628 | "execution_count": null, 629 | "metadata": { 630 | "id": "3wEccFDQxOit" 631 | }, 632 | "outputs": [], 633 | "source": [ 634 | "enplot.pshow(lensedcmb[0], colorbar=True, downgrade=2, range=300)\n", 635 | "enplot.pshow(cmbmap[0], colorbar=True, downgrade=2)" 636 | ] 637 | }, 638 | { 639 | "cell_type": "markdown", 640 | "metadata": { 641 | "id": "zyTrdUeOe_rv" 642 | }, 643 | "source": [ 644 | "The effect of lensing is made more obvious by looking at the difference between the output and input (unlensed) CMB maps:" 645 | ] 646 | }, 647 | { 648 | "cell_type": "code", 649 | "execution_count": null, 650 | "metadata": { 651 | "id": "tEojjqfve-JE" 652 | }, 653 | "outputs": [], 654 | "source": [ 655 | "enplot.pshow(lensedcmb[0] - cmbmap[0], colorbar=True, downgrade=2, range=50)" 656 | ] 657 | }, 658 | { 659 | "cell_type": "markdown", 660 | "metadata": { 661 | "id": "jJ1yHWKa21Iy" 662 | }, 663 | "source": [ 664 | "We can then compare the power spectra of these lensed maps to the lensed power spectra from CAMB. To calculate the power spectra of the lensed maps we created, we will first apodize the maps, and use the pixell functions `enmap.map2harm` and enmap.lbin to calculate the power spectra. If you need a refresher of this procedure, please refer to the notebook [\"Fourier Operations with pixell\"](https://github.com/simonsobs/pixell_tutorials/blob/master/Pixell_fourier_space_operations.ipynb)." 665 | ] 666 | }, 667 | { 668 | "cell_type": "markdown", 669 | "metadata": { 670 | "id": "VBX8FYSb4H7E" 671 | }, 672 | "source": [ 673 | "Apodization is done as follows:" 674 | ] 675 | }, 676 | { 677 | "cell_type": "code", 678 | "execution_count": null, 679 | "metadata": { 680 | "id": "kLXytVYRo46N" 681 | }, 682 | "outputs": [], 683 | "source": [ 684 | "apod_pix = 200 # number of pixels at the edge to apodize\n", 685 | "taper = enmap.apod(cmbmap*0.0 + 1.0, apod_pix)" 686 | ] 687 | }, 688 | { 689 | "cell_type": "code", 690 | "execution_count": null, 691 | "metadata": { 692 | "id": "J5X1wU3mo9OZ" 693 | }, 694 | "outputs": [], 695 | "source": [ 696 | "for i in range(3):\n", 697 | " enplot.pshow((taper * lensedcmb)[i], downgrade=2, colorbar=True)" 698 | ] 699 | }, 700 | { 701 | "cell_type": "markdown", 702 | "metadata": { 703 | "id": "xRHS9Zq64Lb2" 704 | }, 705 | "source": [ 706 | "We can then compute the 2D FFT with `enmap.map2harm` and use `enmap.lbin` to bin the power spectra into 1D." 707 | ] 708 | }, 709 | { 710 | "cell_type": "code", 711 | "execution_count": null, 712 | "metadata": { 713 | "id": "Ek0NoO8tNhj_" 714 | }, 715 | "outputs": [], 716 | "source": [ 717 | "harm_lensed = enmap.map2harm(taper * lensedcmb, normalize=\"phys\")\n", 718 | "w2 = np.mean(taper**2)\n", 719 | "power = (harm_lensed[:, None] * np.conj(harm_lensed)).real/w2\n", 720 | "cl, ls1 = power.lbin(bsize=40)" 721 | ] 722 | }, 723 | { 724 | "cell_type": "markdown", 725 | "metadata": { 726 | "id": "P4oKvpPf4z0R" 727 | }, 728 | "source": [ 729 | "Here, we also load the CAMB lensed power spectra to compare to the lensed power spectra from the simulation we created in this notebook." 730 | ] 731 | }, 732 | { 733 | "cell_type": "code", 734 | "execution_count": null, 735 | "metadata": { 736 | "id": "u9SE_LlSk_qR" 737 | }, 738 | "outputs": [], 739 | "source": [ 740 | "powers_lensed_camb = powers[\"lensed_scalar\"].T" 741 | ] 742 | }, 743 | { 744 | "cell_type": "code", 745 | "execution_count": null, 746 | "metadata": { 747 | "id": "hxHxVjwfdcJm" 748 | }, 749 | "outputs": [], 750 | "source": [ 751 | "dll_fac = (ls1*(ls1+1))/(2*np.pi)\n", 752 | "camb_dll_fac = (ell*(ell+1))/(2*np.pi)\n", 753 | "\n", 754 | "fig, ax = plt.subplots(2, 2)\n", 755 | "\n", 756 | "ax[0,0].plot(ls1,cl[0, 0]*dll_fac,label=\"This notebook\")\n", 757 | "ax[0,0].plot(powers_lensed_camb[0]*camb_dll_fac,label=\"CAMB lensed\")\n", 758 | "ax[0,0].set_xlim(20, 4000)\n", 759 | "\n", 760 | "ax[0,1].plot(ls1,cl[1, 1]*dll_fac,label=\"This notebook\")\n", 761 | "ax[0,1].plot(powers_lensed_camb[1]*camb_dll_fac,label=\"CAMB lensed\")\n", 762 | "ax[0,1].set_xlim(20, 4000)\n", 763 | "\n", 764 | "ax[1,0].plot(ls1,cl[2, 2]*dll_fac,label=\"This notebook\")\n", 765 | "ax[1,0].plot(powers_lensed_camb[2]*camb_dll_fac,label=\"CAMB lensed\")\n", 766 | "ax[1,0].set_xlim(20, 4000)\n", 767 | "\n", 768 | "ax[1,1].plot(ls1,cl[0, 1]*dll_fac,label=\"This notebook\")\n", 769 | "ax[1,1].plot(powers_lensed_camb[3]*camb_dll_fac,label=\"CAMB lensed\")\n", 770 | "ax[1,1].set_xlim(20, 4000)\n", 771 | "\n", 772 | "ax[0,0].set_xlabel(\"$\\ell$\")\n", 773 | "ax[0,0].set_ylabel(\"TT\")\n", 774 | "ax[0,1].set_xlabel(\"$\\ell$\")\n", 775 | "ax[0,1].set_ylabel(\"EE\")\n", 776 | "ax[1,0].set_xlabel(\"$\\ell$\")\n", 777 | "ax[1,0].set_ylabel(\"BB\")\n", 778 | "ax[1,1].set_xlabel(\"$\\ell$\")\n", 779 | "ax[1,1].set_ylabel(\"TE\")\n", 780 | "\n", 781 | "plt.tight_layout()\n", 782 | "plt.legend()\n", 783 | "plt.show()" 784 | ] 785 | }, 786 | { 787 | "cell_type": "markdown", 788 | "metadata": { 789 | "id": "HUYzOURssu-1" 790 | }, 791 | "source": [ 792 | "Nice! There are clearly some lensing-induced B-modes, but they don't have quite the right shape. This is likely due to the fact that we've done everything on the flat sky and/or haven't properly accounted for the mode-coupling due to the sky mask.\n", 793 | "\n", 794 | "Fortunately, just as for the Gaussian realization from the CMB, `pixell` also supports full-sky lensing operations that accounts for spherical geometry. Because we can work on the full-sky, we won't need to apodize the map; however, this will not apply to a realistic simulation for ACT or SO data, which only observes a fraction of the sky.\n", 795 | "\n", 796 | "As for the Gaussian realization, the curved-sky lensing operation `lensing.lens_map_curved` has new arguments:\n", 797 | "\n", 798 | "`shape`: with `wcs`, the geometry of the output map\n", 799 | "\n", 800 | "`wcs`: with shape, the geometry of the output map\n", 801 | "\n", 802 | "`phi_alm`: the spherical harmonic realization of the lensing potential realization\n", 803 | "\n", 804 | "`cmb_alm`: the spherical harmonics of the unlensed CMB realization\n", 805 | "\n", 806 | "`phi_ainfo=None`: describes the spherical harmonic ordering convention for `phi` (you will know if you need to adjust this)\n", 807 | "\n", 808 | "`maplmax=None`: deprecated\n", 809 | "\n", 810 | "`dtype=np.float64`: output precision, needs to be double for accurate calculations (may crash otherwise)\n", 811 | "\n", 812 | "`spin=[0,2]`: like in `curvedsky.rand_map`\n", 813 | "\n", 814 | "`output=\"l\"`: returns only the lensed CMB map. `lu` returns the lensed and unlensed CMB. More options for lensing maps in the [function documentation](https://github.com/simonsobs/pixell/blob/a61ca1a3ae99e4c8627c86a0fe2401d4bfe67d66/pixell/lensing.py#L134)\n", 815 | "\n", 816 | "`geodesic=True`: slower but accurate, `False` for faster but less accurate\n", 817 | "\n", 818 | "`verbose=False`: print helpful messages\n", 819 | "\n", 820 | "`delta_theta=None`: can leave this as-is\n", 821 | "\n", 822 | "In the below, besides supplying the required arguments, the only other important argument we change is to also return the unlensed CMB so we can compare:" 823 | ] 824 | }, 825 | { 826 | "cell_type": "code", 827 | "execution_count": null, 828 | "metadata": { 829 | "id": "PpV-vQ77wt6T" 830 | }, 831 | "outputs": [], 832 | "source": [ 833 | "# first get a full-sky realization of the phi map and the polarized, unlensed cmb\n", 834 | "# use low-resolution to reduce memory and runtime\n", 835 | "shape, wcs = enmap.fullsky_geometry(res=4*utils.arcmin, proj='car')\n", 836 | "\n", 837 | "# pixell needs the maps in spherical harmonics\n", 838 | "phi_alm = curvedsky.rand_alm(ps=ps_lens, lmax=2700)\n", 839 | "cmb_alm = curvedsky.rand_alm(ps=ps_cmb, lmax=2700)\n", 840 | "\n", 841 | "# get the lensed cmb map\n", 842 | "lensedcmb, cmbmap = lensing.lens_map_curved((3, *shape), wcs, phi_alm, cmb_alm, output=\"lu\", verbose=True)" 843 | ] 844 | }, 845 | { 846 | "cell_type": "markdown", 847 | "metadata": { 848 | "id": "MbTUbm2M023R" 849 | }, 850 | "source": [ 851 | "As before, let's take a look at the lensed CMB map, the unlensed CMB map, and their difference:" 852 | ] 853 | }, 854 | { 855 | "cell_type": "code", 856 | "execution_count": null, 857 | "metadata": { 858 | "id": "G-gBkklV09An" 859 | }, 860 | "outputs": [], 861 | "source": [ 862 | "enplot.pshow(lensedcmb[0], colorbar=True, downgrade=4, ticks=15)\n", 863 | "enplot.pshow(cmbmap[0], colorbar=True, downgrade=4, ticks=15)\n", 864 | "enplot.pshow(lensedcmb[0] - cmbmap[0], colorbar=True, downgrade=4, ticks=15)" 865 | ] 866 | }, 867 | { 868 | "cell_type": "markdown", 869 | "metadata": { 870 | "id": "Qt44MhuCL2WK" 871 | }, 872 | "source": [ 873 | "Now we can take the full-sky power spectrum without a mask to see if the lensing produced the correct lensed CMB power spectrum. A nice benefit of this test compared to the flat-sky test is that we have more sky area and so the measured power spectrum is also less noisy:" 874 | ] 875 | }, 876 | { 877 | "cell_type": "code", 878 | "execution_count": null, 879 | "metadata": { 880 | "id": "36bGziOGL1rR" 881 | }, 882 | "outputs": [], 883 | "source": [ 884 | "lensed_alm = curvedsky.map2alm(lensedcmb, lmax=2700)\n", 885 | "cl = curvedsky.alm2cl(lensed_alm[:, None], lensed_alm)" 886 | ] 887 | }, 888 | { 889 | "cell_type": "code", 890 | "execution_count": null, 891 | "metadata": { 892 | "id": "K_M_UVJ2MY2P" 893 | }, 894 | "outputs": [], 895 | "source": [ 896 | "ell = np.arange(cl.shape[-1])\n", 897 | "dll_fac = (ell*(ell+1))/(2*np.pi)\n", 898 | "\n", 899 | "camb_ell = np.arange(powers_lensed_camb[0].shape[-1])\n", 900 | "camb_dll_fac = (camb_ell*(camb_ell+1))/(2*np.pi)\n", 901 | "\n", 902 | "fig, ax = plt.subplots(2, 2, figsize=(10, 6))\n", 903 | "\n", 904 | "ax[0,0].plot(cl[0, 0]*dll_fac,label=\"This notebook\")\n", 905 | "ax[0,0].plot(powers_lensed_camb[0]*camb_dll_fac,label=\"CAMB lensed\")\n", 906 | "ax[0,0].set_xlim(20, 2700)\n", 907 | "\n", 908 | "ax[0,1].plot(cl[1, 1]*dll_fac,label=\"This notebook\")\n", 909 | "ax[0,1].plot(powers_lensed_camb[1]*camb_dll_fac,label=\"CAMB lensed\")\n", 910 | "ax[0,1].set_xlim(20, 2700)\n", 911 | "\n", 912 | "ax[1,0].plot(cl[2, 2]*dll_fac,label=\"This notebook\")\n", 913 | "ax[1,0].plot(powers_lensed_camb[2]*camb_dll_fac,label=\"CAMB lensed\")\n", 914 | "ax[1,0].set_xlim(20, 2700)\n", 915 | "\n", 916 | "ax[1,1].plot(cl[0, 1]*dll_fac,label=\"This notebook\")\n", 917 | "ax[1,1].plot(powers_lensed_camb[3]*camb_dll_fac,label=\"CAMB lensed\")\n", 918 | "ax[1,1].set_xlim(20, 2700)\n", 919 | "\n", 920 | "ax[0,0].set_xlabel(\"$\\ell$\")\n", 921 | "ax[0,0].set_ylabel(\"TT\")\n", 922 | "ax[0,1].set_xlabel(\"$\\ell$\")\n", 923 | "ax[0,1].set_ylabel(\"EE\")\n", 924 | "ax[1,0].set_xlabel(\"$\\ell$\")\n", 925 | "ax[1,0].set_ylabel(\"BB\")\n", 926 | "ax[1,1].set_xlabel(\"$\\ell$\")\n", 927 | "ax[1,1].set_ylabel(\"TE\")\n", 928 | "\n", 929 | "plt.tight_layout()\n", 930 | "plt.legend()\n", 931 | "plt.show()" 932 | ] 933 | }, 934 | { 935 | "cell_type": "markdown", 936 | "metadata": { 937 | "id": "7hBJxBOkNny0" 938 | }, 939 | "source": [ 940 | "Hooray, that worked well -- look at how much better the BB spectrum matches! Evidently the flat-sky lensing example was indeed biased either by its flat-sky treatment and/or simplistic handling of the mask when measuring the power spectrum. However, we should keep in mind that the lensing is not accurate out to the bandlimit -- we need to build-in some buffer to our analysis by lensing to a high bandlimit but only using larger scales (in this case, e.g., <2000)." 941 | ] 942 | }, 943 | { 944 | "cell_type": "markdown", 945 | "metadata": { 946 | "id": "AzJ6umw3QylK" 947 | }, 948 | "source": [ 949 | "**Point Sources**" 950 | ] 951 | }, 952 | { 953 | "cell_type": "markdown", 954 | "metadata": { 955 | "id": "vMfdDrc6Uz-3" 956 | }, 957 | "source": [ 958 | "In CMB maps we typically have extragalactic point sources emission and galaxy clusters through the Sunyaev Zel'dovich effect. The former are bright spots in the map and the latter it is a decrement for frequencies below 220 GHz and increment above 220 GHz, the effect it null approximately at 220 GHz.\n", 959 | "\n", 960 | "To simulate point sources we need a profile, since these galaxies are usually unresolved points in the sky their profile is the telescope beam. For a 6-meter telescope the full width at half maximum of the beam is roughly 1.4 arcminutes at 150 GHz, we can simplify the profile as Gaussian.\n", 961 | "\n", 962 | "Considering that a unit normalized Gaussian centered at zero has the form $\\exp{\\left( -\\frac{r^2}{2\\sigma^2} \\right)}$ and the full width at half maximum is $\\mathrm{FWHM} = 2 \\sqrt{2 \\ln(2)} \\sigma$, we have the following functional form for the beam: $\\exp{\\left(-\\frac{4\\ln(2)r^2}{\\mathrm{FWHM}^2}\\right)}$" 963 | ] 964 | }, 965 | { 966 | "cell_type": "code", 967 | "execution_count": null, 968 | "metadata": { 969 | "id": "NvSpC9LmVHGm" 970 | }, 971 | "outputs": [], 972 | "source": [ 973 | "# Generating a Gaussian beam\n", 974 | "FWHM = 1.4\n", 975 | "r = np.linspace(0, 60, 1000) # 60 arcminutes or 1 degree\n", 976 | "B = np.exp(-(4*np.log(2)*r**2) / (FWHM**2))\n", 977 | "plt.plot(r, B)\n", 978 | "plt.xlim(0, 5)\n", 979 | "plt.ylabel(\"Beam amplitude\")\n", 980 | "plt.xlabel(\"Radius (arcmin)\")\n", 981 | "# we need to pass the radius to radians\n", 982 | "r *= 1/60 * np.pi / 180" 983 | ] 984 | }, 985 | { 986 | "cell_type": "markdown", 987 | "metadata": { 988 | "id": "_nvc372KVKd-" 989 | }, 990 | "source": [ 991 | "Now we can generate sources, for that we will use the `pointsrcs` module, in specific the function `sim_objects`, this function requires the following:\n", 992 | "\n", 993 | "`shape`: Shape of the map in which we will inject sources, in our case omap.shape\n", 994 | "\n", 995 | "`wcs`: World coordinates of the map in which we will inject sources in our case omap.wcs\n", 996 | "\n", 997 | "`poss`: Position of the sources that we want to inject in the form [dec,ra] where dec and ra are arrays of floats with the declination and right ascension of the sources in radians\n", 998 | "\n", 999 | "`amp`: Amplitude of the sources that we want to inject, this is the peak value of the beam in temperature units as an array of floats\n", 1000 | "\n", 1001 | "`profile`: The radial profile of the sources, in case of point sources this profile is the beam, but for example for cluster it could be a more extended profile, the profile is given as [r,B] where r is the radius in radians and B is the profile amplitude\n", 1002 | "\n", 1003 | "`vmin=None`: The lowest value to simulate in map units, by default it takes 1e-3*amp.\n", 1004 | "\n", 1005 | "`pixwin=False`: If we want to apply the pixel window function after simulating the objects.\n", 1006 | "\n", 1007 | "A nice feature of this function is that it automatically handles the \"distorted\" geometry near the poles, so that radially-symmetric objects will appear horizontally stretched (as they should). This won't be evident in the small cutout below but it is important for a full-sky simulation:\n" 1008 | ] 1009 | }, 1010 | { 1011 | "cell_type": "code", 1012 | "execution_count": null, 1013 | "metadata": { 1014 | "id": "F9-AaBFoVMsW" 1015 | }, 1016 | "outputs": [], 1017 | "source": [ 1018 | " # DEC0, RA0 DEC1 RA1\n", 1019 | "box = np.array([[-10, 35],[-2, 15]]) * utils.degree\n", 1020 | "shape, wcs = enmap.geometry(pos=box, res=0.5 * utils.arcmin, proj='car')\n", 1021 | "# we will use only the temperature component or parameter stokes I\n", 1022 | "\n", 1023 | "# we will generate 100 sources\n", 1024 | "nsrc = 100\n", 1025 | "# we choose a logspace between 100 and 10000\n", 1026 | "amp = np.logspace(2.0, 4.0, nsrc)\n", 1027 | "# the position are random values inside omap\n", 1028 | "dec = np.random.uniform(-10, -2, nsrc) * np.pi / 180\n", 1029 | "ra = np.random.uniform(15, 35, nsrc) *np.pi / 180\n", 1030 | "\n", 1031 | "# we generate the sourcemap here\n", 1032 | "srcmap = pointsrcs.sim_objects(shape, wcs, [dec, ra], amp, [r, B],\n", 1033 | " min=np.min(amp)*1e-4)\n", 1034 | "\n", 1035 | "# plotting the result\n", 1036 | "enplot.pshow(srcmap, range=500)\n", 1037 | "\n", 1038 | "# adding to a cmb map\n", 1039 | "cmbmap = curvedsky.rand_map(shape, wcs, ps=ps[0])\n", 1040 | "enplot.pshow(srcmap + cmbmap, range=500)" 1041 | ] 1042 | }, 1043 | { 1044 | "cell_type": "markdown", 1045 | "source": [ 1046 | "**Simplistic Noise**\n", 1047 | "\n", 1048 | "Finally we consider simple, additive noise that you can add to any of the previous realizations of the underlying \"signal.\" Unlike the signal, which dies off quickly as a function of $\\ell$, the noise is not bandlimited. Realistic noise in the maps is quite complicated (see https://arxiv.org/abs/2303.04180), so here we just show a simplified example. In fact, we've already used all of the tools.\n", 1049 | "\n", 1050 | "We want to go back the case of using `enmap.rand_map` -- the flat-sky code -- to generate Gaussian realizations on the full-sky. This was unphysical for the signal, but for the noise, it is faster, sufficient, and in some ways better (again, the realization is not bandlimited).\n", 1051 | "\n", 1052 | "We define a realistic noise power spectrum and level, and can also exploit that noise is ~symmetric in E and B to just perform a \"scalar\" rather than spin-2 transform:" 1053 | ], 1054 | "metadata": { 1055 | "id": "8mmWPSZwTsbn" 1056 | } 1057 | }, 1058 | { 1059 | "cell_type": "code", 1060 | "execution_count": null, 1061 | "metadata": { 1062 | "id": "WlPiL9zOiBtS" 1063 | }, 1064 | "outputs": [], 1065 | "source": [ 1066 | "# make a polarized noise ps function\n", 1067 | "def T_and_P_noise_ps(ell, white_level=30, noise_ps_scaling=-4, T_knee=3000,\n", 1068 | " T_cap=300, P_knee=300, P_cap=100):\n", 1069 | " \"\"\"Get the temperature and polarization noise power spectra evaluated at ell.\n", 1070 | " Follows this model:\n", 1071 | "\n", 1072 | " PS(ell) = white_level**2 * ((ell/knee)**scaling + 1), ell > cap\n", 1073 | " PS(ell) = white_level**2 * ((cap/knee)**scaling + 1), ell <= cap\n", 1074 | "\n", 1075 | " Parameters\n", 1076 | " ----------\n", 1077 | " ell : (...) np.ndarray\n", 1078 | " Angular scales.\n", 1079 | " white_level : scalar\n", 1080 | " Temperature white noise level in uK-arcmin. Polarization is this times\n", 1081 | " sqrt(2).\n", 1082 | " noise_ps_scaling : scalar\n", 1083 | " Power-law scaling of the low-ell noise power spectra.\n", 1084 | " T_knee : scalar\n", 1085 | " Ell-knee of temperature power spectrum.\n", 1086 | " T_cap : scalar\n", 1087 | " Minimum ell at which the spectrum is capped.\n", 1088 | " P_knee : scalar\n", 1089 | " Ell-knee of polarization power spectrum.\n", 1090 | " P_cap : scalar\n", 1091 | " Minimum ell at which the spectrum is capped.\n", 1092 | "\n", 1093 | " Returns\n", 1094 | " -------\n", 1095 | " (3, ...) np.ndarray\n", 1096 | " The polarization noise power spectra in T, Q, U. Assumed diagonal over\n", 1097 | " polarization.\n", 1098 | " \"\"\"\n", 1099 | " T = np.zeros_like(ell)\n", 1100 | " mask = ell <= T_cap\n", 1101 | " T[mask] = white_level**2 * ((T_cap/T_knee)**noise_ps_scaling + 1)\n", 1102 | " T[~mask] = white_level**2 * ((ell[~mask]/T_knee)**noise_ps_scaling + 1)\n", 1103 | "\n", 1104 | " P = np.zeros_like(ell)\n", 1105 | " mask = ell <= P_cap\n", 1106 | " P[mask] = 2 * white_level**2 * ((P_cap/P_knee)**noise_ps_scaling + 1)\n", 1107 | " P[~mask] = 2 * white_level**2 * ((ell[~mask]/P_knee)**noise_ps_scaling + 1)\n", 1108 | "\n", 1109 | "\n", 1110 | " # convert to steradians (note, not square radians!). the below lines first\n", 1111 | " # convert to square radians, then from square radians to steradians\n", 1112 | " T *= utils.arcmin**2 * (4*np.pi / (np.pi * 2*np.pi))\n", 1113 | " P *= utils.arcmin**2 * (4*np.pi / (np.pi * 2*np.pi))\n", 1114 | "\n", 1115 | " # put into square shape and return\n", 1116 | " out = np.zeros((3, 3, *ell.shape), ell.dtype)\n", 1117 | " out[0, 0] = T\n", 1118 | " out[1, 1] = P\n", 1119 | " out[2, 2] = P\n", 1120 | "\n", 1121 | " return out" 1122 | ] 1123 | }, 1124 | { 1125 | "cell_type": "markdown", 1126 | "source": [ 1127 | "Let's see what the default looks like as a function of $\\ell$. This includes large-scale correlated noise from the atmosphere as would be observed by the SO LAT, but the SO SAT includes a half-wave plate and processing filters to mitigate this, so the noise spectra would need adjustment:" 1128 | ], 1129 | "metadata": { 1130 | "id": "4649BFRFGl57" 1131 | } 1132 | }, 1133 | { 1134 | "cell_type": "code", 1135 | "source": [ 1136 | "ell = np.arange(10000, dtype=np.float64)\n", 1137 | "noise_ps = T_and_P_noise_ps(ell)\n", 1138 | "plt.loglog(ell, noise_ps[0, 0])\n", 1139 | "plt.loglog(ell, noise_ps[1, 1])" 1140 | ], 1141 | "metadata": { 1142 | "id": "idE-iUuHGWLA" 1143 | }, 1144 | "execution_count": null, 1145 | "outputs": [] 1146 | }, 1147 | { 1148 | "cell_type": "markdown", 1149 | "source": [ 1150 | "Let's make a convenience function to draw noise realizations, since this will also need to take into account the area of each pixel to keep the white-noise level consistent:" 1151 | ], 1152 | "metadata": { 1153 | "id": "p3frLSYIAny_" 1154 | } 1155 | }, 1156 | { 1157 | "cell_type": "code", 1158 | "source": [ 1159 | "def get_noise_sim(shape, wcs, seed=None, **T_and_P_noise_ps_kwargs):\n", 1160 | " \"\"\"Draw a noise realization from the constructed noise powre spectrum\n", 1161 | " that also accounts for the smaller, and thus noisier, pixels near the\n", 1162 | " poles.\n", 1163 | "\n", 1164 | " Parameters\n", 1165 | " ----------\n", 1166 | " shape : (ny, nx) tuple\n", 1167 | " The footprint of the map.\n", 1168 | " wcs : astropy.wcs.wcs.WCS\n", 1169 | " The geometry of the map. Assumes units of degrees (the pixell\n", 1170 | " default for wcs).\n", 1171 | " seed : int or list of int\n", 1172 | " Random seed.\n", 1173 | " T_and_P_noise_ps_kwargs : dict\n", 1174 | " Keyword arguments to be passed to T_and_P_noise_ps.\n", 1175 | "\n", 1176 | " Returns\n", 1177 | " -------\n", 1178 | " (3, ny, nx) enmap.ndmap\n", 1179 | " Noise realization (polarized) drawn from T_and_P_noise_ps, with the\n", 1180 | " correct white-noise level, correlated noise shape, and corrected for\n", 1181 | " pixel areas.\n", 1182 | " \"\"\"\n", 1183 | " # get the noise ps. for enmap.rand_map to work, this needs to be\n", 1184 | " # evaluated at integer ells and cover all the angular scales in\n", 1185 | " # out 2d fourier space\n", 1186 | " ell = np.arange(0, np.ceil(enmap.modlmap(shape, wcs).max()) + 1)\n", 1187 | " noise_ps = T_and_P_noise_ps(ell, **T_and_P_noise_ps_kwargs)\n", 1188 | "\n", 1189 | " # draw a noise realization\n", 1190 | " noise_sim = enmap.rand_map((3, *shape), wcs, cov=noise_ps, seed=seed,\n", 1191 | " scalar=True)\n", 1192 | "\n", 1193 | " # normalize by pixel area. we want this in terms of fraction of a\n", 1194 | " # \"flat-sky\" pixel\n", 1195 | " pixsize_steradians = enmap.pixsizemap(shape, wcs, broadcastable=True)\n", 1196 | " pix_area_deg = np.abs(np.prod(wcs.wcs.cdelt))\n", 1197 | " pix_area_steradians = pix_area_deg * utils.degree**2\n", 1198 | " frac_pixsize = pixsize_steradians / pix_area_steradians\n", 1199 | "\n", 1200 | " return noise_sim / np.sqrt(frac_pixsize)" 1201 | ], 1202 | "metadata": { 1203 | "id": "xHb547KoHEd2" 1204 | }, 1205 | "execution_count": null, 1206 | "outputs": [] 1207 | }, 1208 | { 1209 | "cell_type": "markdown", 1210 | "source": [ 1211 | "Let's try it out on the full-sky:" 1212 | ], 1213 | "metadata": { 1214 | "id": "zbaSqjRtKnTc" 1215 | } 1216 | }, 1217 | { 1218 | "cell_type": "code", 1219 | "source": [ 1220 | "shape, wcs = enmap.fullsky_geometry(res=4*utils.arcmin, proj='car')\n", 1221 | "\n", 1222 | "noise_sim = get_noise_sim(shape, wcs)\n", 1223 | "\n", 1224 | "for i in range(3):\n", 1225 | " enplot.pshow(noise_sim[i], downgrade=4, ticks=15, colorbar=True)" 1226 | ], 1227 | "metadata": { 1228 | "id": "Sj0bsLPVKqBy" 1229 | }, 1230 | "execution_count": null, 1231 | "outputs": [] 1232 | }, 1233 | { 1234 | "cell_type": "markdown", 1235 | "source": [ 1236 | "### Put it all together\n", 1237 | "\n", 1238 | "Let's take all the above pieces and make a not-so-unrealistic simulation of data as would be observed by an actual CMB experiment:\n", 1239 | "\n", 1240 | "\n", 1241 | "\n", 1242 | "1. We'll draw a full-sky unlensed T, Q, U CMB realization and a full-sky realization of the lensing potential and use it to \"lens\" the CMB\n", 1243 | "2. We'll add a new step to the above: convolve the resulting signal with\n", 1244 | "the instrumental beam so that it is consistent with the point sources\n", 1245 | "3. We'll inject a sample of point sources with the same beam size. Assume they are not polarized\n", 1246 | "4. We'll add a T, Q, U noise realization to the signal\n", 1247 | "\n", 1248 | "This simulation omits quite a lot of real-world complexity in each of its parts --- we won't include any scan-synchronous signal, beam leakage or pixel window function, polarized or extended or \"SZ-like\" sources, or realistic noise --- but it is actually fairly representative.\n", 1249 | "\n", 1250 | "We'll wrap it in a big convenience function. Play around with all the inputs or add even more arguments to make it more tunable!\n", 1251 | "\n", 1252 | "(Exercise: An obvious way to streamline the below function would be to allow the user to pass pre-built signal or noise maps. That way someone could more efficiently add multiple noise realizations to the same signal realization, without rebuilding the same signal map each time)\n" 1253 | ], 1254 | "metadata": { 1255 | "id": "oOnru2LQLR-3" 1256 | } 1257 | }, 1258 | { 1259 | "cell_type": "code", 1260 | "source": [ 1261 | "import healpy as hp\n", 1262 | "\n", 1263 | "def get_signal_and_noise_sim(shape, wcs, lmax, ps_cmb, ps_lens, cmb_seed=None,\n", 1264 | " phi_seed=None, src_pos_seed=None, noise_seed=None,\n", 1265 | " beam_fwhm=1.4, nsrc=10000, white_level=30,\n", 1266 | " noise_ps_scaling=-4, T_knee=3000, T_cap=300,\n", 1267 | " P_knee=300, P_cap=100):\n", 1268 | " \"\"\"Draw a semi-realistic end-to-end sim including lensed CMB, temperature\n", 1269 | " point sources, instrumental beam, and instrumental and atmospheric noise.\n", 1270 | "\n", 1271 | " Parameters\n", 1272 | " ----------\n", 1273 | " shape : (ny, nx) tuple\n", 1274 | " The footprint of the map.\n", 1275 | " wcs : astropy.wcs.wcs.WCS\n", 1276 | " The geometry of the map. Assumes units of degrees (the pixell\n", 1277 | " default for wcs).\n", 1278 | " lmax : int\n", 1279 | " maximum multipole for the signal\n", 1280 | " ps_cmb : (3, 3, nl) np.ndarray\n", 1281 | " CMB TEB power spectra with covariances\n", 1282 | " ps_lens : (nl,) np.ndarray\n", 1283 | " Lensing potential power spectrum\n", 1284 | " cmb_seed : int or list of int\n", 1285 | " Seed for unlensed CMB\n", 1286 | " phi_seed : int or list of int\n", 1287 | " Seed for lensing potential\n", 1288 | " src_pos_seed : int or list of int\n", 1289 | " Seed for point source locations\n", 1290 | " noise_seed : int or list of int\n", 1291 | " Seed for noise\n", 1292 | " beam_fwhm : scalar\n", 1293 | " Beam width in arcmin\n", 1294 | " nsrc : int\n", 1295 | " Number of point sources logarithmically distributed between\n", 1296 | " 100 and 10000 uK\n", 1297 | " white_level : scalar\n", 1298 | " Temperature white noise level in uK-arcmin. Polarization is this times\n", 1299 | " sqrt(2).\n", 1300 | " noise_ps_scaling : scalar\n", 1301 | " Power-law scaling of the low-ell noise power spectra.\n", 1302 | " T_knee : scalar\n", 1303 | " Ell-knee of temperature power spectrum.\n", 1304 | " T_cap : scalar\n", 1305 | " Minimum ell at which the spectrum is capped.\n", 1306 | " P_knee : scalar\n", 1307 | " Ell-knee of polarization power spectrum.\n", 1308 | " P_cap : scalar\n", 1309 | " Minimum ell at which the spectrum is capped.\n", 1310 | "\n", 1311 | " Returns\n", 1312 | " -------\n", 1313 | " (3, ny, nx) enmap.ndmap\n", 1314 | " The simulation\n", 1315 | " \"\"\"\n", 1316 | "\n", 1317 | " # get lensed CMB. assume lensing indep. of CMB (not true)\n", 1318 | " phi_alm = curvedsky.rand_alm(ps=ps_lens, lmax=lmax, seed=phi_seed)\n", 1319 | " cmb_alm = curvedsky.rand_alm(ps=ps_cmb, lmax=lmax, seed=cmb_seed)\n", 1320 | " lensedcmb = lensing.lens_map_curved((3, *shape), wcs, phi_alm, cmb_alm,\n", 1321 | " output=\"l\", verbose=True)[0]\n", 1322 | "\n", 1323 | " # convolve it with the beam. fwhm in radians\n", 1324 | " bl = hp.gauss_beam(beam_fwhm * utils.arcmin, lmax=lmax)\n", 1325 | " lensedcmb_alm = curvedsky.map2alm(lensedcmb, lmax=lmax)\n", 1326 | " curvedsky.almxfl(lensedcmb_alm, bl, out=lensedcmb_alm)\n", 1327 | " curvedsky.alm2map(lensedcmb_alm, lensedcmb)\n", 1328 | "\n", 1329 | " # add srcs (already convolved with beam)\n", 1330 | " r = np.linspace(0, 60, 1000) * utils.arcmin # 60 arcminutes or 1 degree\n", 1331 | " B = np.exp(-4*np.log(2)*r**2 / (beam_fwhm*utils.arcmin)**2)\n", 1332 | " amp = np.logspace(2.0, 4.0, nsrc)\n", 1333 | "\n", 1334 | " src_rng = np.random.default_rng(src_pos_seed)\n", 1335 | " dec = src_rng.uniform(-90, 90, nsrc) * utils.degree\n", 1336 | " ra = src_rng.uniform(-180, 180, nsrc) * utils.degree\n", 1337 | " srcmap = pointsrcs.sim_objects(shape, wcs, [dec, ra], amp, [r, B],\n", 1338 | " vmin=np.min(amp)*1e-4)\n", 1339 | "\n", 1340 | " # and noise\n", 1341 | " noisemap = get_noise_sim(shape, wcs, seed=noise_seed, white_level=white_level,\n", 1342 | " noise_ps_scaling=noise_ps_scaling, T_knee=T_knee,\n", 1343 | " T_cap=T_cap, P_knee=P_knee, P_cap=P_cap)\n", 1344 | "\n", 1345 | " out = lensedcmb + noisemap\n", 1346 | " out[0] += srcmap\n", 1347 | " return out" 1348 | ], 1349 | "metadata": { 1350 | "id": "d7YD88NvLU_B" 1351 | }, 1352 | "execution_count": null, 1353 | "outputs": [] 1354 | }, 1355 | { 1356 | "cell_type": "code", 1357 | "source": [ 1358 | "totalsim = get_signal_and_noise_sim(shape, wcs, 2700, ps_cmb, ps_lens)\n", 1359 | "\n", 1360 | "for i in range(3):\n", 1361 | " enplot.pshow(totalsim[i], downgrade=4, ticks=15, colorbar=True)" 1362 | ], 1363 | "metadata": { 1364 | "id": "tA4kJ4eLTIFa" 1365 | }, 1366 | "execution_count": null, 1367 | "outputs": [] 1368 | } 1369 | ], 1370 | "metadata": { 1371 | "colab": { 1372 | "provenance": [] 1373 | }, 1374 | "kernelspec": { 1375 | "display_name": "Python 3", 1376 | "name": "python3" 1377 | }, 1378 | "language_info": { 1379 | "name": "python" 1380 | } 1381 | }, 1382 | "nbformat": 4, 1383 | "nbformat_minor": 0 1384 | } 1385 | --------------------------------------------------------------------------------