From a496c3689e32d62271ac6ad3bfb8d77d48108207 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Sat, 11 Jul 2026 17:20:27 +0100 Subject: [PATCH] docs(chapter_1): restore truncated tutorials + add completeness CI check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tutorial_1_grids_and_galaxies and tutorial_2_ray_tracing were cut off mid-generation during the workspace bootstrap, each ending on a docstring that promised a plot with no code following and no Wrap Up (frozen at the truncated length since bootstrap commit #1; the original workspace source no longer exists). Restore the lost content: - tutorial_1: log10 image + __Galaxies__ + __Units__ + __Wrap Up__ + advanced topics, adapted from the complete HowToGalaxy sibling to the autolens API and lens framing (429 -> 672 lines). - tutorial_2: log-space convergence/potential + __Ray Tracing Grids__ (lens equation) + __Ray Tracing Images__ + __Galaxies__ + __Tracer__ + __Mappings__ + __Wrap Up__, sourced from guides/tracer.py (214 -> 391 lines). Both validated end-to-end and via the curated smoke suite (6/6). Prevent recurrence: add .github/scripts/check_tutorials_complete.py + a CI workflow that fails when any tutorial lacks a terminal __Wrap Up__/__Summary__ section — a truncated script never reaches its wrap-up, so absence of the marker reliably flags lost content. Normalize five complete-but-unmarked tutorials (tutorial_4_dealing_with_failure, tutorial_4_bayesian_regularization, tutorial_6_slam, tutorial_8_model_fit, tutorial_searches) with the terminal marker so they pass. Regenerate notebooks + navigator catalogue. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KExk2jkn2ya21b6NZLALpB --- .github/scripts/check_tutorials_complete.py | 80 ++++ .github/workflows/tutorials_complete.yml | 20 + llms-full.txt | 4 +- .../tutorial_1_grids_and_galaxies.ipynb | 401 +++++++++++++++++- .../tutorial_2_ray_tracing.ipynb | 302 ++++++++++++- .../tutorial_4_dealing_with_failure.ipynb | 2 + .../tutorial_6_slam.ipynb | 2 + .../tutorial_4_bayesian_regularization.ipynb | 2 + .../tutorial_8_model_fit.ipynb | 2 + .../chapter_optional/tutorial_searches.ipynb | 23 +- .../tutorial_1_grids_and_galaxies.py | 245 ++++++++++- .../tutorial_2_ray_tracing.py | 179 +++++++- .../tutorial_4_dealing_with_failure.py | 2 + .../tutorial_6_slam.py | 2 + .../tutorial_4_bayesian_regularization.py | 2 + .../tutorial_8_model_fit.py | 2 + scripts/chapter_optional/tutorial_searches.py | 11 + workspace_index.json | 15 +- 18 files changed, 1284 insertions(+), 12 deletions(-) create mode 100644 .github/scripts/check_tutorials_complete.py create mode 100644 .github/workflows/tutorials_complete.yml diff --git a/.github/scripts/check_tutorials_complete.py b/.github/scripts/check_tutorials_complete.py new file mode 100644 index 0000000..670b8dd --- /dev/null +++ b/.github/scripts/check_tutorials_complete.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Fail if any HowTo tutorial script looks truncated. + +Several tutorial scripts were once cut off mid-generation (a long file was +re-emitted and the output was severed), losing all content below the cut and +leaving a script that ends part-way through — typically on a docstring that +promises a plot or code block which never appears. This check guards against +that regression recurring. + +A tutorial is considered *complete* when it contains a recognised terminal +section marker (``__Wrap Up__`` or ``__Summary__``). A truncated script never +reaches its wrap-up, so the absence of the marker is a reliable signal that the +script lost content. Deliberate "not written yet" stub tutorials still carry a +``__Wrap Up__`` section, so they pass. + +A second, cheaper guard flags any script whose final docstring block ends on a +colon (``:``) — the classic "a plot/code block follows" promise left dangling +by a mid-docstring cutoff. + +Run from the repo root:: + + python scripts/check_tutorials_complete.py + +Exit status is non-zero if any tutorial fails, listing each offender. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +TERMINAL_MARKERS = ("__wrap up__", "__summary__") + + +def final_docstring(text: str) -> str | None: + """Return the last triple-quoted block if it sits at the end of the file.""" + blocks = list(re.finditer(r'"""(.*?)"""', text, re.DOTALL)) + if not blocks: + return None + last = blocks[-1] + if text[last.end():].strip() == "": + return last.group(1) + return None + + +def check(path: Path) -> str | None: + """Return a failure reason for a truncated-looking tutorial, else None.""" + text = path.read_text(encoding="utf-8") + if not any(marker in text.lower() for marker in TERMINAL_MARKERS): + return "no __Wrap Up__/__Summary__ terminal section (looks truncated)" + trailing = final_docstring(text) + if trailing is not None and trailing.strip().endswith(":"): + return "final docstring ends on ':' (dangling promise of a following block)" + return None + + +def main(root: str) -> int: + scripts_dir = Path(root) / "scripts" + files = sorted(scripts_dir.rglob("tutorial_*.py")) + + failures = [(f, reason) for f in files if (reason := check(f)) is not None] + + print(f"Checked {len(files)} tutorial scripts under {scripts_dir}.") + if failures: + print(f"\n{len(failures)} tutorial(s) look incomplete / truncated:\n") + for f, reason in failures: + print(f" [FAIL] {f.relative_to(root)} — {reason}") + print( + "\nEach tutorial must end with a `__Wrap Up__` (or `__Summary__`) " + "section. If a script is genuinely truncated, restore its lost " + "content; if it is complete, add the terminal section." + ) + return 1 + + print("All tutorial scripts have a terminal section — none look truncated.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1] if len(sys.argv) > 1 else ".")) diff --git a/.github/workflows/tutorials_complete.yml b/.github/workflows/tutorials_complete.yml new file mode 100644 index 0000000..a3fa780 --- /dev/null +++ b/.github/workflows/tutorials_complete.yml @@ -0,0 +1,20 @@ +name: Tutorials Complete + +# Guards against tutorial scripts being truncated / cut off mid-generation (a +# long file re-emitted with the output severed, losing every section below the +# cut). Each tutorial_*.py must end with a `__Wrap Up__` (or `__Summary__`) +# terminal section; the check is pure-stdlib and needs no library install. +# See scripts/check_tutorials_complete.py. + +on: [push, pull_request] + +jobs: + tutorials-complete: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Check tutorials are not truncated + run: python .github/scripts/check_tutorials_complete.py . diff --git a/llms-full.txt b/llms-full.txt index c4f1d29..0e34750 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -12,9 +12,9 @@ AUTO-GENERATED by PyAutoBuild — do not edit by hand; regenerate with generate. - [Tutorial 0: Visualization](scripts/chapter_1_introduction/tutorial_0_visualization.py): In this tutorial, we quickly cover visualization in **PyAutoLens** and make sure images display clearly in your Jupyter notebook and on your computer screen. - Contents: Directories, Dataset, Subplots, Plot Customization, Overlays, Wrap Up - [HowToLens: Introduction](scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py): A strong gravitational lens is a system where two (or more) galaxies align perfectly down our line of sight from Earth such that the foreground galaxy's mass curves space-time in on itself, such that the light of a background source galaxy is deflected and magnified. This means we can see the background source galaxy multiple times, as multiple arcs or rings, because multiple paths through the foreground galaxy's mass are taken by the source's light. - - Contents: Grids, Geometry, Light Profiles, One Dimension Projection + - Contents: Grids, Geometry, Light Profiles, One Dimension Projection, Galaxies, Units - [Tutorial 2: Ray Tracing](scripts/chapter_1_introduction/tutorial_2_ray_tracing.py): Strong gravitational lensing occurs when the mass of a foreground galaxy (or galaxies) curves space-time around it, causing light rays from a background source to appear deflected. - - Contents: Grid, Mass Profiles + - Contents: Grid, Mass Profiles, Ray Tracing Grids, Ray Tracing Images, Galaxies, Tracer, Mappings - [Tutorial 5: More Ray Tracing](scripts/chapter_1_introduction/tutorial_3_more_ray_tracing.py): We'll now reinforce the ideas that we learnt about ray-tracing in the previous tutorial and introduce the following new concepts: - Contents: Initial Setup, Concise Code, Critical Curves, Caustics, Units, More Complexity, Multi Galaxy Ray Tracing, Wrap Up - [Tutorial 4: Point Sources](scripts/chapter_1_introduction/tutorial_4_point_sources.py): This tutorial is not wrriten yet, but will explain how point source lensing works. diff --git a/notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb b/notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb index 1a125b5..b8884e6 100644 --- a/notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb +++ b/notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb @@ -59,7 +59,9 @@ "- **Grids:** A `Grid2D` is a set of two-dimensional $(y,x)$ coordinates that represent points in space where we.\n", "- **Geometry:** The above grid is centered on the origin (0.0\", 0.0\").\n", "- **Light Profiles:** Galaxies are collections of stars, gas, dust, and other astronomical objects that emit light.\n", - "- **One Dimension Projection:** We often want to calculative 1D quantities of a light profile, for example to plot how its light." + "- **One Dimension Projection:** We often want to calculative 1D quantities of a light profile, for example to plot how its light.\n", + "- **Galaxies:** Galaxies are collections of light profiles that represent a galaxy's luminous emission.\n", + "- **Units:** By assuming a redshift for a galaxy we can convert its quantities from arcseconds to kiloparsecs." ] }, { @@ -705,10 +707,405 @@ "Since galaxy light distributions often cover a wide range of values, they are typically better visualized on a log10 \n", "scale. This approach helps highlight details in the faint outskirts of a light profile.\n", "\n", - "The `plot_array`/`subplot_\\*` object has a `use_log10` option that applies this transformation automatically. Below, you can see \n", + "The `plot_array`/`subplot_\\*` object has a `use_log10` option that applies this transformation automatically. Below, you can see\n", "that the image plotted in log10 space reveals more details." ] }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(\n", + " array=sersic_light_profile.image_2d_from(grid=grid),\n", + " title=\"Sersic Image\",\n", + " use_log10=True,\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Galaxies__\n", + "\n", + "Now, let's introduce `Galaxy` objects, which are a key component in **PyAutoLens**.\n", + "\n", + "A light profile represents a single feature of a galaxy, such as its bulge or disk. To model a complete galaxy,\n", + "we combine multiple light profiles into a `Galaxy` object. This allows us to create images that include different\n", + "components of a galaxy.\n", + "\n", + "In addition to light profiles, a `Galaxy` has a `redshift`, which indicates how far away it is from Earth. The redshift\n", + "is essential for performing unit conversions using cosmological calculations, such as converting arc-seconds into\n", + "kiloparsecs. (A kiloparsec is a distance unit in astronomy, equal to about 3.26 million light-years.)\n", + "\n", + "Redshifts are especially important in strong lensing, where the foreground lens galaxy and background source galaxy\n", + "lie at two different redshifts. We are not yet performing any lensing calculations in this tutorial, so for now we\n", + "simply use a single galaxy to build up intuition for the `Galaxy` object.\n", + "\n", + "Let's start by creating a galaxy with two `Sersic` light profiles, which we will consider to represent a bulge and\n", + "disk component of the galaxy, the two most important structures seen in galaxies." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "bulge = al.lp.Sersic(\n", + " centre=(0.0, 0.0),\n", + " ell_comps=(0.0, 0.111111),\n", + " intensity=1.0,\n", + " effective_radius=1.0,\n", + " sersic_index=2.5,\n", + ")\n", + "\n", + "disk = al.lp.Sersic(\n", + " centre=(0.0, 0.0),\n", + " ell_comps=(0.0, 0.3),\n", + " intensity=0.3,\n", + " effective_radius=3.0,\n", + " sersic_index=1.0,\n", + ")\n", + "\n", + "galaxy = al.Galaxy(redshift=0.5, bulge=bulge, disk=disk)\n", + "\n", + "print(galaxy)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can pass a 2D grid to a light profile to compute its image using the `image_2d_from` method.\n", + "\n", + "The same approach works for a `Galaxy` object:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "image = galaxy.image_2d_from(grid=grid)\n", + "\n", + "print(\"Intensity of `Grid2D` pixel 0:\")\n", + "print(image.native[0, 0])\n", + "print(\"Intensity of `Grid2D` pixel 1:\")\n", + "print(image.native[0, 1])\n", + "print(\"Intensity of `Grid2D` pixel 2:\")\n", + "print(image.native[0, 2])\n", + "print(\"...\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can plot the galaxy's image, just like how we did for a light profile." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(array=galaxy.image_2d_from(grid=grid), title=\"Galaxy Bulge+Disk Image\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The bulge dominates the center of the image, and is pretty much the only luminous emission we can see on a linear\n", + "scale. The disk's emission is present, but it is much fainter and spread over a larger area.\n", + "\n", + "We can confirm this using the `subplot_galaxy_light_profiles` method, which plots each individual light profile\n", + "separately." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.subplot_galaxy_light_profiles(galaxy=galaxy, grid=grid)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because galaxy light distributions often follow a log10 pattern, plotting in log10 space helps reveal details in the\n", + "outskirts of the light profile, in this case the emission of the disk.\n", + "\n", + "This is especially helpful to separate the bulge and disk profiles, which have different intensities and sizes." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(\n", + " array=galaxy.image_2d_from(grid=grid),\n", + " title=\"Galaxy Bulge+Disk Image\",\n", + " use_log10=True,\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the tools above, we can visualize each light profile's contribution in 1D.\n", + "\n", + "1D plots show the intensity of the light profile as a function of distance from the profile's center. The bulge\n", + "and disk profiles in this example share the same `centre`, meaning that plotting them together on the same 1D plot\n", + "shows how they vary relative to one another.\n", + "\n", + "If the `centre` of the profiles were different, when you make the 1D plot you would need to decide whether to plot the\n", + "profiles offset from one another or plot them both from zero." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "grid_2d_projected = grid.grid_2d_radial_projected_from(\n", + " centre=galaxy.bulge.centre, angle=galaxy.bulge.angle()\n", + ")\n", + "bulge_image_1d = galaxy.bulge.image_2d_from(grid=grid_2d_projected)\n", + "\n", + "grid_2d_projected = grid.grid_2d_radial_projected_from(\n", + " centre=galaxy.disk.centre, angle=galaxy.disk.angle()\n", + ")\n", + "disk_image_1d = galaxy.disk.image_2d_from(grid=grid_2d_projected)\n", + "\n", + "plt.plot(grid_2d_projected[:, 1], bulge_image_1d, label=\"Bulge\")\n", + "plt.plot(grid_2d_projected[:, 1], disk_image_1d, label=\"Disk\")\n", + "plt.xlabel(\"Radius (arcseconds)\")\n", + "plt.ylabel(\"Luminosity\")\n", + "plt.legend()\n", + "plt.show()\n", + "plt.close()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can group multiple galaxies at the same redshift into a `Galaxies` object, which is created from a list of\n", + "individual galaxies.\n", + "\n", + "In a strong lens, we ultimately group together a foreground lens galaxy and a background source galaxy. For now, we\n", + "simply create a second galaxy and combine it with the original galaxy into a `Galaxies` object, to see how the light\n", + "of multiple galaxies is represented." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "extra_galaxy = al.Galaxy(\n", + " redshift=0.5,\n", + " bulge=al.lp.Sersic(\n", + " centre=(0.2, 0.3),\n", + " ell_comps=(0.0, 0.111111),\n", + " intensity=1.0,\n", + " effective_radius=1.0,\n", + " sersic_index=2.5,\n", + " ),\n", + ")\n", + "\n", + "galaxies = al.Galaxies(galaxies=[galaxy, extra_galaxy])" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Galaxies` object has similar methods to those for light profiles and individual galaxies.\n", + "\n", + "For example, `image_2d_from` sums the images of all the galaxies." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "image = galaxies.image_2d_from(grid=grid)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can plot the combined image of all the galaxies, just like with other plotters." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title=\"Image\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A subplot of each individual galaxy image can also be created." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.subplot_galaxies(galaxies=galaxies, grid=grid)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because galaxy light distributions often follow a log10 pattern, plotting in log10 space helps reveal details in the\n", + "outskirts of the light profile.\n", + "\n", + "This is especially helpful when visualizing how multiple galaxies overlap." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title=\"Image\", use_log10=True)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Units__\n", + "\n", + "Earlier, we mentioned that a galaxy's `redshift` allows us to convert between arcseconds and kiloparsecs.\n", + "\n", + "A redshift measures how much a galaxy's light is stretched by the Universe's expansion. A higher redshift means the\n", + "galaxy is further away, and its light has been stretched more. By knowing a galaxy's redshift, we can convert angular\n", + "distances (like arcseconds) to physical distances (like kiloparsecs).\n", + "\n", + "To perform this conversion, we use a cosmological model that describes the Universe's expansion. Below, we use\n", + "the `Planck15` cosmology, which is based on observations from the Planck satellite." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "cosmology = al.cosmo.Planck15()\n", + "\n", + "kpc_per_arcsec = cosmology.kpc_per_arcsec_from(redshift=galaxy.redshift)\n", + "\n", + "print(\"Kiloparsecs per Arcsecond:\")\n", + "print(kpc_per_arcsec)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This `kpc_per_arcsec` can be used as a conversion factor between arcseconds and kiloparsecs when plotting images of\n", + "galaxies.\n", + "\n", + "We compute this value and plot the image, which by default is shown in units of arcseconds." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(array=galaxy.image_2d_from(grid=grid), title=\"Image\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Wrap Up__\n", + "\n", + "In this tutorial, you've learnt the basic quantities used to describe the galaxies that make up a strong lens, before\n", + "we introduce any lensing calculations.\n", + "\n", + "Let's summarise what we've covered:\n", + "\n", + "- **Grids**: A grid is a set of 2D $(y,x)$ coordinates that represent the positions where we measure the light of a\n", + "galaxy.\n", + "\n", + "- **Geometry**: We showed how to shift, rotate, and convert grids to elliptical coordinates.\n", + "\n", + "- **Light Profiles**: Light profiles are analytic functions that describe how a galaxy's light is distributed in\n", + "space. We used the `Sersic` profile to create images of galaxies.\n", + "\n", + "- **Galaxies**: Galaxies are collections of light profiles. We created galaxies with multiple light profiles, combined\n", + "them into a `Galaxies` object, and visualized their images.\n", + "\n", + "- **Units**: By assuming redshifts for galaxies we can convert their quantities from arcseconds to physical units like\n", + "kiloparsecs.\n", + "\n", + "In the next tutorial, we'll introduce the mass of a galaxy and perform our first lensing calculation, whereby the\n", + "light of a background source galaxy is deflected by the mass of a foreground lens galaxy.\n", + "\n", + "__Advanced Topics__\n", + "\n", + "The following advanced topics are not important for a new user learning the software for the first time. However,\n", + "once you are an expert user, the following guides and concepts are important for doing accurate strong lens analysis,\n", + "and thus may be things you want to commit to memory as future references.\n", + "\n", + "__Other Unit Conversion__\n", + "\n", + "Above, we used a redshift to convert between arcseconds and kiloparsecs. This is just one example of a unit conversion\n", + "that can be performed using a galaxy's redshift.\n", + "\n", + "There are many other unit conversions that can be performed, such as converting the units of a galaxy's image to what\n", + "Astronomers call an AB magnitude system, which is a system used to measure the brightness of galaxies.\n", + "\n", + "The `autolens_workspace/*/guides/units` module contains many examples of unit conversions and how to use them,\n", + "but they will not be covered in the *HowToLens* tutorials.\n", + "\n", + "__Over Sampling__\n", + "\n", + "Over sampling is a numerical technique where the images of light profiles and galaxies are evaluated\n", + "on a higher resolution grid than the image data to ensure the calculation is accurate.\n", + "\n", + "For a new user, the details of over-sampling are not important, therefore just be aware that all calculations use an\n", + "adaptive over sampling scheme with high accuracy across all use cases.\n", + "\n", + "Once you are more experienced, you should read up on over-sampling in more detail via\n", + "the `autolens_workspace/*/guides/over_sampling.ipynb` notebook." + ] + }, { "cell_type": "code", "metadata": {}, diff --git a/notebooks/chapter_1_introduction/tutorial_2_ray_tracing.ipynb b/notebooks/chapter_1_introduction/tutorial_2_ray_tracing.ipynb index 7f8eaf2..083cf1d 100644 --- a/notebooks/chapter_1_introduction/tutorial_2_ray_tracing.ipynb +++ b/notebooks/chapter_1_introduction/tutorial_2_ray_tracing.ipynb @@ -59,12 +59,17 @@ "- **Tracer**: Introduce the `Tracer` object, which automates the ray-tracing process and allows us to compute\n", " images of the entire lens system.\n", "\n", - "- **Mappings**: Visualize how image pixels map to the source plane and vice versa using the `lines=`/`positions=` overlays object.\n", + "- **Mappings**: Visualize how coordinates in the image-plane map to the source-plane by plotting the ray-traced grids.\n", "\n", "__Contents__\n", "\n", "- **Grid:** In the previous tutorial, we created 2D grids of (y,x) coordinates and showed how shifting and.\n", - "- **Mass Profiles:** To perform lensing calculations, we use mass profiles available in the `mass_profile` module." + "- **Mass Profiles:** To perform lensing calculations, we use mass profiles available in the `mass_profile` module.\n", + "- **Ray Tracing Grids:** The lens equation uses deflection angles to map image-plane coordinates to the source-plane.\n", + "- **Ray Tracing Images:** Evaluating a source's light on the ray-traced grid produces its lensed image.\n", + "- **Galaxies:** A `Galaxy` can contain both light and mass profiles, forming realistic lens and source galaxies.\n", + "- **Tracer:** The `Tracer` object automates ray-tracing for a system of galaxies at different redshifts.\n", + "- **Mappings:** Every image-plane coordinate maps to a source-plane coordinate via the lens equation." ] }, { @@ -374,6 +379,299 @@ "The **convergence** and **potential** can be better understood when plotted in logarithmic space:" ] }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(\n", + " array=sis_mass_profile.convergence_2d_from(grid=image_plane_grid),\n", + " title=\"Convergence in Log10 Space\",\n", + " use_log10=True,\n", + ")\n", + "aplt.plot_array(\n", + " array=sis_mass_profile.potential_2d_from(grid=image_plane_grid),\n", + " title=\"Potential in Log10 Space\",\n", + " use_log10=True,\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Ray Tracing Grids__\n", + "\n", + "We now have all the tools we need to perform our first ray-tracing calculation.\n", + "\n", + "Ray tracing uses a mass profile's deflection angles to map coordinates from the image-plane (where we observe the\n", + "lensed source) to the source-plane (where the source truly is). This mapping is described by the **lens equation**:\n", + "\n", + "$\\beta = \\theta - \\alpha(\\theta)$\n", + "\n", + "where $\\theta$ are the image-plane coordinates, $\\alpha(\\theta)$ are the deflection angles of the mass profile, and\n", + "$\\beta$ are the corresponding source-plane coordinates.\n", + "\n", + "We compute the deflection angles of our mass profile on the `image_plane_grid`, and then use the grid's\n", + "`grid_2d_via_deflection_grid_from` method to subtract them and produce the ray-traced `source_plane_grid`." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "deflections = sis_mass_profile.deflections_yx_2d_from(grid=image_plane_grid)\n", + "\n", + "source_plane_grid = image_plane_grid.grid_2d_via_deflection_grid_from(\n", + " deflection_grid=deflections\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's plot the image-plane grid and the ray-traced source-plane grid.\n", + "\n", + "The image-plane grid is uniform, but the source-plane grid is distorted. This distortion is caused by the mass\n", + "profile's deflection angles, which bend the light rays as they pass the lens galaxy. The coordinates near the\n", + "center of the mass are deflected the most." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_grid(grid=image_plane_grid, title=\"Image Plane Grid\")\n", + "aplt.plot_grid(grid=source_plane_grid, title=\"Source Plane Grid (Ray Traced)\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Ray Tracing Images__\n", + "\n", + "The source-plane grid tells us where each image-plane coordinate lands in the source-plane after being deflected.\n", + "\n", + "To compute the lensed image of a source galaxy, we place a light profile in the source-plane and evaluate its light\n", + "on the ray-traced `source_plane_grid`. Because many image-plane coordinates map to the same region of the\n", + "source-plane, the source appears multiply imaged, forming arcs or a complete Einstein ring.\n", + "\n", + "Let's create a source light profile and compute its lensed image." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "source_light_profile = al.lp.ExponentialCore(\n", + " centre=(0.1, 0.1),\n", + " ell_comps=(0.0, 0.1),\n", + " intensity=0.1,\n", + " effective_radius=0.2,\n", + ")\n", + "\n", + "lensed_image = source_light_profile.image_2d_from(grid=source_plane_grid)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When we plot this image, the source's light appears as a strongly lensed Einstein ring.\n", + "\n", + "This is the **image-plane image** or **observed image** we discussed at the start of the tutorial: the appearance\n", + "of the source *after* its light has been deflected by the lens galaxy's mass.\n", + "\n", + "*Exercise*: Try changing the `centre` of the source light profile. Observe how moving the source relative to the\n", + "lens galaxy changes the lensed image from a ring into arcs or multiple images." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_array(array=lensed_image, title=\"Lensed Source Image\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Galaxies__\n", + "\n", + "In the previous tutorial we saw that a `Galaxy` can contain one or more light profiles. A `Galaxy` can also contain\n", + "mass profiles, and can contain both at the same time.\n", + "\n", + "This lets us construct realistic lens and source galaxies:\n", + "\n", + "- The **lens galaxy** has a mass profile (which deflects light) and typically a light profile (its own emission).\n", + "- The **source galaxy** has a light profile (the light we see lensed), and is at a higher redshift." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "lens_galaxy = al.Galaxy(\n", + " redshift=0.5,\n", + " mass=sis_mass_profile,\n", + ")\n", + "\n", + "source_galaxy = al.Galaxy(\n", + " redshift=1.0,\n", + " bulge=source_light_profile,\n", + ")\n", + "\n", + "print(lens_galaxy)\n", + "print(source_galaxy)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Tracer__\n", + "\n", + "Performing the ray-tracing calculation manually, as we did above, becomes cumbersome once we have multiple galaxies\n", + "at multiple redshifts.\n", + "\n", + "The `Tracer` object automates the entire ray-tracing process. We create it from a list of galaxies, ordered by their\n", + "redshift, and it uses their redshifts and a cosmological model to set up the strong lens system." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy])" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Tracer` has an `image_2d_from` method, just like light profiles and galaxies. It performs all the ray-tracing\n", + "for us and returns the image of the entire strong lens system.\n", + "\n", + "This produces the same lensed Einstein ring we computed manually above, but in a single line of code." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "image = tracer.image_2d_from(grid=image_plane_grid)\n", + "\n", + "aplt.plot_array(array=image, title=\"Image of Strong Lens System via Tracer\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `subplot_tracer` method plots a subplot of the most important quantities of the strong lens system, including\n", + "its image, convergence, potential and deflection angles." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.subplot_tracer(tracer=tracer, grid=image_plane_grid)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Mappings__\n", + "\n", + "The `Tracer` also gives us access to the grids of every plane in the strong lens system, via the\n", + "`traced_grid_2d_list_from` method.\n", + "\n", + "This returns a list, where the first entry is the image-plane grid and the second entry is the ray-traced\n", + "source-plane grid. These are the same two grids we computed manually earlier, but now produced by the tracer." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "traced_grid_list = tracer.traced_grid_2d_list_from(grid=image_plane_grid)\n", + "\n", + "image_plane_grid_traced = traced_grid_list[0]\n", + "source_plane_grid_traced = traced_grid_list[1]" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By plotting the image-plane and source-plane grids together, we can visualize the **mappings** between them.\n", + "\n", + "Every coordinate in the image-plane maps to a coordinate in the source-plane via the lens equation. This mapping is\n", + "the heart of strong lens modeling: to fit a lens, we ray-trace the image-plane grid to the source-plane, evaluate\n", + "the source's light there, and compare the resulting lensed image to the observed data." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "aplt.plot_grid(grid=image_plane_grid_traced, title=\"Image Plane Grid\")\n", + "aplt.plot_grid(grid=source_plane_grid_traced, title=\"Source Plane Grid\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Wrap Up__\n", + "\n", + "In this tutorial, you performed your first lensing calculations. Let's summarise what we've learnt:\n", + "\n", + "- **Mass Profiles**: Mass profiles are analytic functions that describe the mass distribution of a galaxy. They are\n", + "used to compute deflection angles, as well as the convergence and gravitational potential.\n", + "\n", + "- **Ray Tracing Grids**: The lens equation, $\\beta = \\theta - \\alpha(\\theta)$, uses deflection angles to map\n", + "image-plane coordinates to the source-plane, producing a distorted ray-traced grid.\n", + "\n", + "- **Ray Tracing Images**: By evaluating a source galaxy's light on the ray-traced source-plane grid, we compute the\n", + "lensed image of the source, which appears as arcs or an Einstein ring.\n", + "\n", + "- **Galaxies**: A `Galaxy` can contain both light and mass profiles, allowing us to construct realistic lens and\n", + "source galaxies.\n", + "\n", + "- **Tracer**: The `Tracer` object automates the ray-tracing process for a system of galaxies at different redshifts,\n", + "computing the image of the entire strong lens system in a single line of code.\n", + "\n", + "In the next tutorial, we'll extend these ideas to more complex mass and light distributions, building towards the\n", + "realistic strong lens systems we observe in real data." + ] + }, { "cell_type": "code", "metadata": {}, diff --git a/notebooks/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.ipynb b/notebooks/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.ipynb index b0c845e..21ea7ef 100644 --- a/notebooks/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.ipynb +++ b/notebooks/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.ipynb @@ -808,6 +808,8 @@ " by the earlier searches. We can therefore guide each search on how to sample a complex lens model's parameter space \n", " in a way that can be fully generalized to any strong lens.\n", " \n", + "__Wrap Up__\n", + "\n", "To wrap up chapter 2, we have a few more tutorials, where we will discuss masking in more detail, the `Result` object\n", "and how to make **PyAutoLens** run faster." ] diff --git a/notebooks/chapter_3_search_chaining/tutorial_6_slam.ipynb b/notebooks/chapter_3_search_chaining/tutorial_6_slam.ipynb index d263bfd..74c2df6 100644 --- a/notebooks/chapter_3_search_chaining/tutorial_6_slam.ipynb +++ b/notebooks/chapter_3_search_chaining/tutorial_6_slam.ipynb @@ -41,6 +41,8 @@ "of earlier searches (e.g. in the Source pipeline) to fit different models in the `Light` and `Mass` pipelines for the\n", "lens's light and mass.\n", "\n", + "__Wrap Up__\n", + "\n", "Whether you should use individual searches, pipelines, The SLaM pipelines or write your own model-fitting script\n", "depends on the scope of your scientific analysis. I would advise you begin by trying to adapting the scripts in the\n", "`autolens_workspace`to fit your data, and also try using the SLaM pipelines once you are a confident **PyAutoLens** user." diff --git a/notebooks/chapter_4_pixelizations/tutorial_4_bayesian_regularization.ipynb b/notebooks/chapter_4_pixelizations/tutorial_4_bayesian_regularization.ipynb index b50bab2..e9dee34 100644 --- a/notebooks/chapter_4_pixelizations/tutorial_4_bayesian_regularization.ipynb +++ b/notebooks/chapter_4_pixelizations/tutorial_4_bayesian_regularization.ipynb @@ -479,6 +479,8 @@ " values of the image pixels whose variances are increased were initially very high (e.g. they were fit poorly by the \n", " lens model).\n", "\n", + "__Wrap Up__\n", + "\n", "In summary, the log evidence is maximized for solutions which most accurately reconstruct the highest S/N realization of\n", "the observed image, without over-fitting its noise and using the fewest correlated source pixels. By employing this\n", "framework throughout, **PyAutoLens** objectively determines the final lens model following the principles of Bayesian\n", diff --git a/notebooks/chapter_4_pixelizations/tutorial_8_model_fit.ipynb b/notebooks/chapter_4_pixelizations/tutorial_8_model_fit.ipynb index 7bf90df..60a9687 100644 --- a/notebooks/chapter_4_pixelizations/tutorial_8_model_fit.ipynb +++ b/notebooks/chapter_4_pixelizations/tutorial_8_model_fit.ipynb @@ -11,6 +11,8 @@ "\n", "`autolens_workspace/*/imaging/features/pixelizations/modeling.py`\n", "\n", + "__Wrap Up__\n", + "\n", "Once complete, you can then move on to the next HowToLens tutorial and gain more insight into adaptive\n", "pixelizations." ] diff --git a/notebooks/chapter_optional/tutorial_searches.ipynb b/notebooks/chapter_optional/tutorial_searches.ipynb index a14731b..7d8d8a0 100644 --- a/notebooks/chapter_optional/tutorial_searches.ipynb +++ b/notebooks/chapter_optional/tutorial_searches.ipynb @@ -458,10 +458,31 @@ "\n", "print(\"The search has finished run - you may now continue the notebook.\")\n", "\n", - "aplt.subplot_fit_imaging(fit=result_emcee.max_log_likelihood_fit)\n" + "aplt.subplot_fit_imaging(fit=result_emcee.max_log_likelihood_fit)" ], "outputs": [], "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "__Wrap Up__\n", + "\n", + "This tutorial showed how the same lens model can be fitted using different non-linear searches (e.g. `Nautilus`,\n", + "`Emcee`, `PySwarms`). Each search explores parameter space in a different way, and the best choice depends on the\n", + "dimensionality and complexity of the model you are fitting.\n", + "\n", + "For the vast majority of lens models in **PyAutoLens**, the default nested sampling search `Nautilus` is recommended,\n", + "as it is robust, efficient and requires little manual tuning." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [], + "outputs": [], + "execution_count": null } ], "metadata": { diff --git a/scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py b/scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py index 689560a..cd504a1 100644 --- a/scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py +++ b/scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py @@ -55,6 +55,8 @@ - **Geometry:** The above grid is centered on the origin (0.0", 0.0"). - **Light Profiles:** Galaxies are collections of stars, gas, dust, and other astronomical objects that emit light. - **One Dimension Projection:** We often want to calculative 1D quantities of a light profile, for example to plot how its light. +- **Galaxies:** Galaxies are collections of light profiles that represent a galaxy's luminous emission. +- **Units:** By assuming a redshift for a galaxy we can convert its quantities from arcseconds to kiloparsecs. """ @@ -424,6 +426,247 @@ Since galaxy light distributions often cover a wide range of values, they are typically better visualized on a log10 scale. This approach helps highlight details in the faint outskirts of a light profile. -The `plot_array`/`subplot_\*` object has a `use_log10` option that applies this transformation automatically. Below, you can see +The `plot_array`/`subplot_\*` object has a `use_log10` option that applies this transformation automatically. Below, you can see that the image plotted in log10 space reveals more details. """ +aplt.plot_array( + array=sersic_light_profile.image_2d_from(grid=grid), + title="Sersic Image", + use_log10=True, +) + +""" +__Galaxies__ + +Now, let's introduce `Galaxy` objects, which are a key component in **PyAutoLens**. + +A light profile represents a single feature of a galaxy, such as its bulge or disk. To model a complete galaxy, +we combine multiple light profiles into a `Galaxy` object. This allows us to create images that include different +components of a galaxy. + +In addition to light profiles, a `Galaxy` has a `redshift`, which indicates how far away it is from Earth. The redshift +is essential for performing unit conversions using cosmological calculations, such as converting arc-seconds into +kiloparsecs. (A kiloparsec is a distance unit in astronomy, equal to about 3.26 million light-years.) + +Redshifts are especially important in strong lensing, where the foreground lens galaxy and background source galaxy +lie at two different redshifts. We are not yet performing any lensing calculations in this tutorial, so for now we +simply use a single galaxy to build up intuition for the `Galaxy` object. + +Let's start by creating a galaxy with two `Sersic` light profiles, which we will consider to represent a bulge and +disk component of the galaxy, the two most important structures seen in galaxies. +""" +bulge = al.lp.Sersic( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.111111), + intensity=1.0, + effective_radius=1.0, + sersic_index=2.5, +) + +disk = al.lp.Sersic( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.3), + intensity=0.3, + effective_radius=3.0, + sersic_index=1.0, +) + +galaxy = al.Galaxy(redshift=0.5, bulge=bulge, disk=disk) + +print(galaxy) + +""" +We can pass a 2D grid to a light profile to compute its image using the `image_2d_from` method. + +The same approach works for a `Galaxy` object: +""" +image = galaxy.image_2d_from(grid=grid) + +print("Intensity of `Grid2D` pixel 0:") +print(image.native[0, 0]) +print("Intensity of `Grid2D` pixel 1:") +print(image.native[0, 1]) +print("Intensity of `Grid2D` pixel 2:") +print(image.native[0, 2]) +print("...") + +""" +We can plot the galaxy's image, just like how we did for a light profile. +""" +aplt.plot_array(array=galaxy.image_2d_from(grid=grid), title="Galaxy Bulge+Disk Image") + +""" +The bulge dominates the center of the image, and is pretty much the only luminous emission we can see on a linear +scale. The disk's emission is present, but it is much fainter and spread over a larger area. + +We can confirm this using the `subplot_galaxy_light_profiles` method, which plots each individual light profile +separately. +""" +aplt.subplot_galaxy_light_profiles(galaxy=galaxy, grid=grid) + +""" +Because galaxy light distributions often follow a log10 pattern, plotting in log10 space helps reveal details in the +outskirts of the light profile, in this case the emission of the disk. + +This is especially helpful to separate the bulge and disk profiles, which have different intensities and sizes. +""" +aplt.plot_array( + array=galaxy.image_2d_from(grid=grid), + title="Galaxy Bulge+Disk Image", + use_log10=True, +) + +""" +Using the tools above, we can visualize each light profile's contribution in 1D. + +1D plots show the intensity of the light profile as a function of distance from the profile's center. The bulge +and disk profiles in this example share the same `centre`, meaning that plotting them together on the same 1D plot +shows how they vary relative to one another. + +If the `centre` of the profiles were different, when you make the 1D plot you would need to decide whether to plot the +profiles offset from one another or plot them both from zero. +""" +grid_2d_projected = grid.grid_2d_radial_projected_from( + centre=galaxy.bulge.centre, angle=galaxy.bulge.angle() +) +bulge_image_1d = galaxy.bulge.image_2d_from(grid=grid_2d_projected) + +grid_2d_projected = grid.grid_2d_radial_projected_from( + centre=galaxy.disk.centre, angle=galaxy.disk.angle() +) +disk_image_1d = galaxy.disk.image_2d_from(grid=grid_2d_projected) + +plt.plot(grid_2d_projected[:, 1], bulge_image_1d, label="Bulge") +plt.plot(grid_2d_projected[:, 1], disk_image_1d, label="Disk") +plt.xlabel("Radius (arcseconds)") +plt.ylabel("Luminosity") +plt.legend() +plt.show() +plt.close() + +""" +We can group multiple galaxies at the same redshift into a `Galaxies` object, which is created from a list of +individual galaxies. + +In a strong lens, we ultimately group together a foreground lens galaxy and a background source galaxy. For now, we +simply create a second galaxy and combine it with the original galaxy into a `Galaxies` object, to see how the light +of multiple galaxies is represented. +""" +extra_galaxy = al.Galaxy( + redshift=0.5, + bulge=al.lp.Sersic( + centre=(0.2, 0.3), + ell_comps=(0.0, 0.111111), + intensity=1.0, + effective_radius=1.0, + sersic_index=2.5, + ), +) + +galaxies = al.Galaxies(galaxies=[galaxy, extra_galaxy]) + +""" +The `Galaxies` object has similar methods to those for light profiles and individual galaxies. + +For example, `image_2d_from` sums the images of all the galaxies. +""" +image = galaxies.image_2d_from(grid=grid) + +""" +We can plot the combined image of all the galaxies, just like with other plotters. +""" +aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title="Image") + +""" +A subplot of each individual galaxy image can also be created. +""" +aplt.subplot_galaxies(galaxies=galaxies, grid=grid) + +""" +Because galaxy light distributions often follow a log10 pattern, plotting in log10 space helps reveal details in the +outskirts of the light profile. + +This is especially helpful when visualizing how multiple galaxies overlap. +""" +aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title="Image", use_log10=True) + +""" +__Units__ + +Earlier, we mentioned that a galaxy's `redshift` allows us to convert between arcseconds and kiloparsecs. + +A redshift measures how much a galaxy's light is stretched by the Universe's expansion. A higher redshift means the +galaxy is further away, and its light has been stretched more. By knowing a galaxy's redshift, we can convert angular +distances (like arcseconds) to physical distances (like kiloparsecs). + +To perform this conversion, we use a cosmological model that describes the Universe's expansion. Below, we use +the `Planck15` cosmology, which is based on observations from the Planck satellite. +""" +cosmology = al.cosmo.Planck15() + +kpc_per_arcsec = cosmology.kpc_per_arcsec_from(redshift=galaxy.redshift) + +print("Kiloparsecs per Arcsecond:") +print(kpc_per_arcsec) + +""" +This `kpc_per_arcsec` can be used as a conversion factor between arcseconds and kiloparsecs when plotting images of +galaxies. + +We compute this value and plot the image, which by default is shown in units of arcseconds. +""" +aplt.plot_array(array=galaxy.image_2d_from(grid=grid), title="Image") + +""" +__Wrap Up__ + +In this tutorial, you've learnt the basic quantities used to describe the galaxies that make up a strong lens, before +we introduce any lensing calculations. + +Let's summarise what we've covered: + +- **Grids**: A grid is a set of 2D $(y,x)$ coordinates that represent the positions where we measure the light of a +galaxy. + +- **Geometry**: We showed how to shift, rotate, and convert grids to elliptical coordinates. + +- **Light Profiles**: Light profiles are analytic functions that describe how a galaxy's light is distributed in +space. We used the `Sersic` profile to create images of galaxies. + +- **Galaxies**: Galaxies are collections of light profiles. We created galaxies with multiple light profiles, combined +them into a `Galaxies` object, and visualized their images. + +- **Units**: By assuming redshifts for galaxies we can convert their quantities from arcseconds to physical units like +kiloparsecs. + +In the next tutorial, we'll introduce the mass of a galaxy and perform our first lensing calculation, whereby the +light of a background source galaxy is deflected by the mass of a foreground lens galaxy. + +__Advanced Topics__ + +The following advanced topics are not important for a new user learning the software for the first time. However, +once you are an expert user, the following guides and concepts are important for doing accurate strong lens analysis, +and thus may be things you want to commit to memory as future references. + +__Other Unit Conversion__ + +Above, we used a redshift to convert between arcseconds and kiloparsecs. This is just one example of a unit conversion +that can be performed using a galaxy's redshift. + +There are many other unit conversions that can be performed, such as converting the units of a galaxy's image to what +Astronomers call an AB magnitude system, which is a system used to measure the brightness of galaxies. + +The `autolens_workspace/*/guides/units` module contains many examples of unit conversions and how to use them, +but they will not be covered in the *HowToLens* tutorials. + +__Over Sampling__ + +Over sampling is a numerical technique where the images of light profiles and galaxies are evaluated +on a higher resolution grid than the image data to ensure the calculation is accurate. + +For a new user, the details of over-sampling are not important, therefore just be aware that all calculations use an +adaptive over sampling scheme with high accuracy across all use cases. + +Once you are more experienced, you should read up on over-sampling in more detail via +the `autolens_workspace/*/guides/over_sampling.ipynb` notebook. +""" diff --git a/scripts/chapter_1_introduction/tutorial_2_ray_tracing.py b/scripts/chapter_1_introduction/tutorial_2_ray_tracing.py index 55edfc2..e24ba8b 100644 --- a/scripts/chapter_1_introduction/tutorial_2_ray_tracing.py +++ b/scripts/chapter_1_introduction/tutorial_2_ray_tracing.py @@ -54,12 +54,17 @@ - **Tracer**: Introduce the `Tracer` object, which automates the ray-tracing process and allows us to compute images of the entire lens system. -- **Mappings**: Visualize how image pixels map to the source plane and vice versa using the `lines=`/`positions=` overlays object. +- **Mappings**: Visualize how coordinates in the image-plane map to the source-plane by plotting the ray-traced grids. __Contents__ - **Grid:** In the previous tutorial, we created 2D grids of (y,x) coordinates and showed how shifting and. - **Mass Profiles:** To perform lensing calculations, we use mass profiles available in the `mass_profile` module. +- **Ray Tracing Grids:** The lens equation uses deflection angles to map image-plane coordinates to the source-plane. +- **Ray Tracing Images:** Evaluating a source's light on the ray-traced grid produces its lensed image. +- **Galaxies:** A `Galaxy` can contain both light and mass profiles, forming realistic lens and source galaxies. +- **Tracer:** The `Tracer` object automates ray-tracing for a system of galaxies at different redshifts. +- **Mappings:** Every image-plane coordinate maps to a source-plane coordinate via the lens equation. """ @@ -212,3 +217,175 @@ """ The **convergence** and **potential** can be better understood when plotted in logarithmic space: """ +aplt.plot_array( + array=sis_mass_profile.convergence_2d_from(grid=image_plane_grid), + title="Convergence in Log10 Space", + use_log10=True, +) +aplt.plot_array( + array=sis_mass_profile.potential_2d_from(grid=image_plane_grid), + title="Potential in Log10 Space", + use_log10=True, +) + +""" +__Ray Tracing Grids__ + +We now have all the tools we need to perform our first ray-tracing calculation. + +Ray tracing uses a mass profile's deflection angles to map coordinates from the image-plane (where we observe the +lensed source) to the source-plane (where the source truly is). This mapping is described by the **lens equation**: + +$\beta = \theta - \alpha(\theta)$ + +where $\theta$ are the image-plane coordinates, $\alpha(\theta)$ are the deflection angles of the mass profile, and +$\beta$ are the corresponding source-plane coordinates. + +We compute the deflection angles of our mass profile on the `image_plane_grid`, and then use the grid's +`grid_2d_via_deflection_grid_from` method to subtract them and produce the ray-traced `source_plane_grid`. +""" +deflections = sis_mass_profile.deflections_yx_2d_from(grid=image_plane_grid) + +source_plane_grid = image_plane_grid.grid_2d_via_deflection_grid_from( + deflection_grid=deflections +) + +""" +Let's plot the image-plane grid and the ray-traced source-plane grid. + +The image-plane grid is uniform, but the source-plane grid is distorted. This distortion is caused by the mass +profile's deflection angles, which bend the light rays as they pass the lens galaxy. The coordinates near the +center of the mass are deflected the most. +""" +aplt.plot_grid(grid=image_plane_grid, title="Image Plane Grid") +aplt.plot_grid(grid=source_plane_grid, title="Source Plane Grid (Ray Traced)") + +""" +__Ray Tracing Images__ + +The source-plane grid tells us where each image-plane coordinate lands in the source-plane after being deflected. + +To compute the lensed image of a source galaxy, we place a light profile in the source-plane and evaluate its light +on the ray-traced `source_plane_grid`. Because many image-plane coordinates map to the same region of the +source-plane, the source appears multiply imaged, forming arcs or a complete Einstein ring. + +Let's create a source light profile and compute its lensed image. +""" +source_light_profile = al.lp.ExponentialCore( + centre=(0.1, 0.1), + ell_comps=(0.0, 0.1), + intensity=0.1, + effective_radius=0.2, +) + +lensed_image = source_light_profile.image_2d_from(grid=source_plane_grid) + +""" +When we plot this image, the source's light appears as a strongly lensed Einstein ring. + +This is the **image-plane image** or **observed image** we discussed at the start of the tutorial: the appearance +of the source *after* its light has been deflected by the lens galaxy's mass. + +*Exercise*: Try changing the `centre` of the source light profile. Observe how moving the source relative to the +lens galaxy changes the lensed image from a ring into arcs or multiple images. +""" +aplt.plot_array(array=lensed_image, title="Lensed Source Image") + +""" +__Galaxies__ + +In the previous tutorial we saw that a `Galaxy` can contain one or more light profiles. A `Galaxy` can also contain +mass profiles, and can contain both at the same time. + +This lets us construct realistic lens and source galaxies: + +- The **lens galaxy** has a mass profile (which deflects light) and typically a light profile (its own emission). +- The **source galaxy** has a light profile (the light we see lensed), and is at a higher redshift. +""" +lens_galaxy = al.Galaxy( + redshift=0.5, + mass=sis_mass_profile, +) + +source_galaxy = al.Galaxy( + redshift=1.0, + bulge=source_light_profile, +) + +print(lens_galaxy) +print(source_galaxy) + +""" +__Tracer__ + +Performing the ray-tracing calculation manually, as we did above, becomes cumbersome once we have multiple galaxies +at multiple redshifts. + +The `Tracer` object automates the entire ray-tracing process. We create it from a list of galaxies, ordered by their +redshift, and it uses their redshifts and a cosmological model to set up the strong lens system. +""" +tracer = al.Tracer(galaxies=[lens_galaxy, source_galaxy]) + +""" +The `Tracer` has an `image_2d_from` method, just like light profiles and galaxies. It performs all the ray-tracing +for us and returns the image of the entire strong lens system. + +This produces the same lensed Einstein ring we computed manually above, but in a single line of code. +""" +image = tracer.image_2d_from(grid=image_plane_grid) + +aplt.plot_array(array=image, title="Image of Strong Lens System via Tracer") + +""" +The `subplot_tracer` method plots a subplot of the most important quantities of the strong lens system, including +its image, convergence, potential and deflection angles. +""" +aplt.subplot_tracer(tracer=tracer, grid=image_plane_grid) + +""" +__Mappings__ + +The `Tracer` also gives us access to the grids of every plane in the strong lens system, via the +`traced_grid_2d_list_from` method. + +This returns a list, where the first entry is the image-plane grid and the second entry is the ray-traced +source-plane grid. These are the same two grids we computed manually earlier, but now produced by the tracer. +""" +traced_grid_list = tracer.traced_grid_2d_list_from(grid=image_plane_grid) + +image_plane_grid_traced = traced_grid_list[0] +source_plane_grid_traced = traced_grid_list[1] + +""" +By plotting the image-plane and source-plane grids together, we can visualize the **mappings** between them. + +Every coordinate in the image-plane maps to a coordinate in the source-plane via the lens equation. This mapping is +the heart of strong lens modeling: to fit a lens, we ray-trace the image-plane grid to the source-plane, evaluate +the source's light there, and compare the resulting lensed image to the observed data. +""" +aplt.plot_grid(grid=image_plane_grid_traced, title="Image Plane Grid") +aplt.plot_grid(grid=source_plane_grid_traced, title="Source Plane Grid") + +""" +__Wrap Up__ + +In this tutorial, you performed your first lensing calculations. Let's summarise what we've learnt: + +- **Mass Profiles**: Mass profiles are analytic functions that describe the mass distribution of a galaxy. They are +used to compute deflection angles, as well as the convergence and gravitational potential. + +- **Ray Tracing Grids**: The lens equation, $\beta = \theta - \alpha(\theta)$, uses deflection angles to map +image-plane coordinates to the source-plane, producing a distorted ray-traced grid. + +- **Ray Tracing Images**: By evaluating a source galaxy's light on the ray-traced source-plane grid, we compute the +lensed image of the source, which appears as arcs or an Einstein ring. + +- **Galaxies**: A `Galaxy` can contain both light and mass profiles, allowing us to construct realistic lens and +source galaxies. + +- **Tracer**: The `Tracer` object automates the ray-tracing process for a system of galaxies at different redshifts, +computing the image of the entire strong lens system in a single line of code. + +In the next tutorial, we'll extend these ideas to more complex mass and light distributions, building towards the +realistic strong lens systems we observe in real data. +""" diff --git a/scripts/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.py b/scripts/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.py index 13a9aff..25516e4 100644 --- a/scripts/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.py +++ b/scripts/chapter_2_lens_modeling/tutorial_4_dealing_with_failure.py @@ -483,6 +483,8 @@ by the earlier searches. We can therefore guide each search on how to sample a complex lens model's parameter space in a way that can be fully generalized to any strong lens. +__Wrap Up__ + To wrap up chapter 2, we have a few more tutorials, where we will discuss masking in more detail, the `Result` object and how to make **PyAutoLens** run faster. """ diff --git a/scripts/chapter_3_search_chaining/tutorial_6_slam.py b/scripts/chapter_3_search_chaining/tutorial_6_slam.py index 3eeef3d..3e627a5 100755 --- a/scripts/chapter_3_search_chaining/tutorial_6_slam.py +++ b/scripts/chapter_3_search_chaining/tutorial_6_slam.py @@ -36,6 +36,8 @@ of earlier searches (e.g. in the Source pipeline) to fit different models in the `Light` and `Mass` pipelines for the lens's light and mass. +__Wrap Up__ + Whether you should use individual searches, pipelines, The SLaM pipelines or write your own model-fitting script depends on the scope of your scientific analysis. I would advise you begin by trying to adapting the scripts in the `autolens_workspace`to fit your data, and also try using the SLaM pipelines once you are a confident **PyAutoLens** user. diff --git a/scripts/chapter_4_pixelizations/tutorial_4_bayesian_regularization.py b/scripts/chapter_4_pixelizations/tutorial_4_bayesian_regularization.py index 1f4f858..f0c7c79 100644 --- a/scripts/chapter_4_pixelizations/tutorial_4_bayesian_regularization.py +++ b/scripts/chapter_4_pixelizations/tutorial_4_bayesian_regularization.py @@ -318,6 +318,8 @@ def perform_fit_with_source_galaxy(dataset, source_galaxy): values of the image pixels whose variances are increased were initially very high (e.g. they were fit poorly by the lens model). +__Wrap Up__ + In summary, the log evidence is maximized for solutions which most accurately reconstruct the highest S/N realization of the observed image, without over-fitting its noise and using the fewest correlated source pixels. By employing this framework throughout, **PyAutoLens** objectively determines the final lens model following the principles of Bayesian diff --git a/scripts/chapter_4_pixelizations/tutorial_8_model_fit.py b/scripts/chapter_4_pixelizations/tutorial_8_model_fit.py index 5db8066..4519f71 100755 --- a/scripts/chapter_4_pixelizations/tutorial_8_model_fit.py +++ b/scripts/chapter_4_pixelizations/tutorial_8_model_fit.py @@ -6,6 +6,8 @@ `autolens_workspace/*/imaging/features/pixelizations/modeling.py` +__Wrap Up__ + Once complete, you can then move on to the next HowToLens tutorial and gain more insight into adaptive pixelizations. """ diff --git a/scripts/chapter_optional/tutorial_searches.py b/scripts/chapter_optional/tutorial_searches.py index 20cddcb..beac222 100644 --- a/scripts/chapter_optional/tutorial_searches.py +++ b/scripts/chapter_optional/tutorial_searches.py @@ -303,3 +303,14 @@ print("The search has finished run - you may now continue the notebook.") aplt.subplot_fit_imaging(fit=result_emcee.max_log_likelihood_fit) + +""" +__Wrap Up__ + +This tutorial showed how the same lens model can be fitted using different non-linear searches (e.g. `Nautilus`, +`Emcee`, `PySwarms`). Each search explores parameter space in a different way, and the best choice depends on the +dimensionality and complexity of the model you are fitting. + +For the vast majority of lens models in **PyAutoLens**, the default nested sampling search `Nautilus` is recommended, +as it is robust, efficient and requires little manual tuning. +""" diff --git a/workspace_index.json b/workspace_index.json index 8f8ff0c..7e99c09 100644 --- a/workspace_index.json +++ b/workspace_index.json @@ -19,9 +19,13 @@ "Grids", "Geometry", "Light Profiles", - "One Dimension Projection" + "One Dimension Projection", + "Galaxies", + "Units" + ], + "cross_refs": [ + "/guides/over_sampling.ipynb" ], - "cross_refs": [], "notebook": "notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb", "path": "scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py", "summary": "A strong gravitational lens is a system where two (or more) galaxies align perfectly down our line of sight from Earth such that the foreground galaxy's mass curves space-time in on itself, such that the light of a background source galaxy is deflected and magnified. This means we can see the background source galaxy multiple times, as multiple arcs or rings, because multiple paths through the foreground galaxy's mass are taken by the source's light.", @@ -30,7 +34,12 @@ { "contents": [ "Grid", - "Mass Profiles" + "Mass Profiles", + "Ray Tracing Grids", + "Ray Tracing Images", + "Galaxies", + "Tracer", + "Mappings" ], "cross_refs": [], "notebook": "notebooks/chapter_1_introduction/tutorial_2_ray_tracing.ipynb",