diff --git a/README.md b/README.md index d049d49..a1741e9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [Installation Guide](https://pyautogalaxy.readthedocs.io/en/latest/installation/overview.html) | [PyAutoGalaxy readthedocs](https://pyautogalaxy.readthedocs.io/en/latest/index.html) | +[Browse Chapter 1 With Images](markdown/README.md) | [autogalaxy_workspace](https://github.com/PyAutoLabs/autogalaxy_workspace) diff --git a/config/build/markdown_examples.yaml b/config/build/markdown_examples.yaml new file mode 100644 index 0000000..880428d --- /dev/null +++ b/config/build/markdown_examples.yaml @@ -0,0 +1,15 @@ +# Curated examples rendered to executed markdown pages (markdown/) with real +# output images, for GitHub browsing. Built by PyAutoBuild's generate_markdown.py. +# Batch 2b: chapter_1_introduction (no non-linear searches — fast). +- script: scripts/chapter_1_introduction/tutorial_0_visualization.py + max_minutes: 30 +- script: scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py + max_minutes: 30 +- script: scripts/chapter_1_introduction/tutorial_2_data.py + max_minutes: 30 +- script: scripts/chapter_1_introduction/tutorial_3_fitting.py + max_minutes: 30 +- script: scripts/chapter_1_introduction/tutorial_4_methods.py + max_minutes: 30 +- script: scripts/chapter_1_introduction/tutorial_5_summary.py + max_minutes: 30 diff --git a/markdown/README.md b/markdown/README.md new file mode 100644 index 0000000..00edc53 --- /dev/null +++ b/markdown/README.md @@ -0,0 +1,12 @@ +# HowToGalaxy examples, executed — browse with output images + +Every page below is the corresponding example script **fully executed**, rendered to markdown with its real output images, so you can read the examples on GitHub exactly as they run. Each page links back to the `.py` script and Jupyter notebook it was generated from. + +- [Tutorial 0: Visualization](chapter_1_introduction/tutorial_0_visualization.md) — from `scripts/chapter_1_introduction/tutorial_0_visualization.py` +- [HowToGalaxy: Introduction](chapter_1_introduction/tutorial_1_grids_and_galaxies.md) — from `scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py` +- [Tutorial 2: Data](chapter_1_introduction/tutorial_2_data.md) — from `scripts/chapter_1_introduction/tutorial_2_data.py` +- [Tutorial 3: Fitting](chapter_1_introduction/tutorial_3_fitting.md) — from `scripts/chapter_1_introduction/tutorial_3_fitting.py` +- [tutorial_4_methods](chapter_1_introduction/tutorial_4_methods.md) — from `scripts/chapter_1_introduction/tutorial_4_methods.py` +- [Tutorial 9: Summary](chapter_1_introduction/tutorial_5_summary.md) — from `scripts/chapter_1_introduction/tutorial_5_summary.py` + +These pages are regenerated manually by PyAutoBuild's `generate_markdown.py` when a curated script changes. diff --git a/markdown/chapter_1_introduction/tutorial_0_visualization.md b/markdown/chapter_1_introduction/tutorial_0_visualization.md new file mode 100644 index 0000000..b1b5cb8 --- /dev/null +++ b/markdown/chapter_1_introduction/tutorial_0_visualization.md @@ -0,0 +1,213 @@ +> ✏️ **This page is auto-generated from [`scripts/chapter_1_introduction/tutorial_0_visualization.py`](../../scripts/chapter_1_introduction/tutorial_0_visualization.py) — do not edit it directly.** +> It shows the example fully executed, with its real output images. +> Run it yourself via the [Python script](../../scripts/chapter_1_introduction/tutorial_0_visualization.py) or the [Jupyter notebook](../../notebooks/chapter_1_introduction/tutorial_0_visualization.ipynb). + +Tutorial 0: Visualization +========================= + +In this tutorial, we quickly cover visualization in **PyAutoGalaxy** and make sure images display clearly in your +Jupyter notebook and on your computer screen. + +__Contents__ + +- **Directories:** Set the working directory so PyAutoGalaxy can find configs, data and output folders. +- **Dataset:** Load an example imaging dataset of a galaxy. +- **Plot Customization:** Customize matplotlib options like title, figure size and colormap. +- **Subplots:** Plot all components of a dataset simultaneously using subplots. +- **Visuals:** Add visual overlays like masks and grids to figures. +- **Wrap Up:** Summary of visualization in PyAutoGalaxy. + + +```python + +from autoconf import setup_notebook; setup_notebook() +``` + + 2026-07-11 16:20:09,770 - matplotlib.font_manager - INFO - Failed to extract font properties from /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf: Can not load face (unknown file format; error code 0x2) + + + 2026-07-11 16:20:09,868 - matplotlib.font_manager - INFO - generated new fontManager + + + Working Directory has been set to `HowToGalaxy` + + +If the printed working directory does not match the workspace path on your computer, you can manually set it +as follows (the example below shows the path I would use on my laptop. The code is commented out so you do not +use this path in this tutorial! + + +```python +# workspace_path = "/Users/Jammy/Code/PyAuto/autogalaxy_workspace" +# #%cd $workspace_path +# print(f"Working Directory has been set to `{workspace_path}`") +``` + +__Dataset__ + +The `dataset_path` specifies where the dataset is located, which is the +directory `autogalaxy_workspace/dataset/imaging/simple__sersic`. + +There are many example simulated images of galaxies in this directory that will be used throughout the +**HowToGalaxy** lectures. + + +```python +from pathlib import Path + +import autogalaxy as ag +import autogalaxy.plot as aplt + +dataset_path = Path("dataset", "imaging", "simple__sersic") +``` + +__Dataset Auto-Simulation__ + +If the dataset does not already exist on your system, it will be created by running the corresponding +simulator script. This ensures that all example scripts can be run without manually simulating data first. + + +```python +if not dataset_path.exists(): + import subprocess + import sys + + subprocess.run( + [sys.executable, "scripts/simulators/sersic.py"], + check=True, + ) + +``` + +We now load this dataset from .fits files and create an instance of an `imaging` object. + + +```python +dataset = ag.Imaging.from_fits( + data_path=dataset_path / "data.fits", + noise_map_path=dataset_path / "noise_map.fits", + psf_path=dataset_path / "psf.fits", + pixel_scales=0.1, +) +``` + +We can plot the data as follows: + + +```python +aplt.plot_array(array=dataset.data, title="Data") +``` + + + +![png](tutorial_0_visualization_files/tutorial_0_visualization_11_0.png) + + + +__Plot Customization__ + +Does the figure display correctly on your computer screen? + +If not, you can customize common matplotlib options by passing them directly to `plot_array`: + + - `title=`: Set the figure title. + - `figsize=`: Control the figure size as a `(width, height)` tuple. + - `colormap=`: Set the matplotlib colormap name (e.g. `"jet"`, `"gray"`). + - `xlabel=`, `ylabel=`: Override the default axis labels. + + +```python +aplt.plot_array( + array=dataset.data, + title="Data", +) +``` + + + +![png](tutorial_0_visualization_files/tutorial_0_visualization_13_0.png) + + + +Many matplotlib options can be customized, but for now we're only concerned with making sure figures display clear in +your Jupyter Notebooks. Nevertheless, a comprehensive API reference guide of all available plot arguments can +be found in the `autogalaxy_workspace/*/guides/plot` package. You should check this out once you are more familiar with +**PyAutoGalaxy**. + +Ideally, we would not specify a `figsize` every time we plot an image. Fortunately, default values can be fully +customized via the config files. + +Checkout the `mat_wrap.yaml` file in `autogalaxy_workspace/config/visualize/mat_wrap`. + +All default matplotlib values are here. There are a lot of entries, so lets focus on whats important for displaying +figures: + + - mat_wrap.yaml -> Figure -> figure: -> figsize + - mat_wrap.yaml -> YLabel -> figure: -> fontsize + - mat_wrap.yaml -> XLabel -> figure: -> fontsize + - mat_wrap.yaml -> TickParams -> figure: -> labelsize + - mat_wrap.yaml -> YTicks -> figure: -> labelsize + - mat_wrap.yaml -> XTicks -> figure: -> labelsize + +Don't worry about all the other files or options listed for now, as they'll make a lot more sense once you are familiar +with **PyAutoGalaxy**. + +If you had to change any of the above settings to get the figures to display clearly, you should update their values +in the corresponding config files above (you will need to reset your Jupyter notebook server for these changes to +take effect, so make sure you have the right values using the `figsize` argument in the cell above beforehand!). + +__Subplots__ + +In addition to plotting individual figures, **PyAutoGalaxy** can also plot subplots showing all components of a +dataset simultaneously. + +Lets plot a subplot of our `Imaging` data: + + +```python +aplt.subplot_imaging_dataset(dataset=dataset) +``` + + + +![png](tutorial_0_visualization_files/tutorial_0_visualization_15_0.png) + + + +__Visuals__ + +Visuals can be added to any figure by passing them as keyword arguments directly to `plot_array`. + +For example, we can plot a mask on the image above by passing `mask=mask`. + +The `visuals` example illustrates every overlay argument, for example `mask=`, `grid=`, `positions=`, `lines=`, etc. + + +```python +mask = ag.Mask2D.circular_annular( + shape_native=dataset.shape_native, + pixel_scales=dataset.pixel_scales, + inner_radius=0.3, + outer_radius=3.0, +) + +aplt.plot_array(array=dataset.data, title="Data") +``` + + + +![png](tutorial_0_visualization_files/tutorial_0_visualization_17_0.png) + + + +__Wrap Up__ + +Throughout lectures you'll see lots more visuals that are plotted on figures and subplots. + +Great! Hopefully, visualization in **PyAutoGalaxy** is displaying nicely for us to get on with the **HowToGalaxy** +lecture series. + + +```python + +``` diff --git a/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_11_0.png b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_11_0.png new file mode 100644 index 0000000..03042b8 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_11_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_13_0.png b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_13_0.png new file mode 100644 index 0000000..03042b8 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_13_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_15_0.png b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_15_0.png new file mode 100644 index 0000000..97e457b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_15_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_17_0.png b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_17_0.png new file mode 100644 index 0000000..03042b8 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_0_visualization_files/tutorial_0_visualization_17_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies.md b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies.md new file mode 100644 index 0000000..ca9f19c --- /dev/null +++ b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies.md @@ -0,0 +1,923 @@ +> ✏️ **This page is auto-generated from [`scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py`](../../scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py) — do not edit it directly.** +> It shows the example fully executed, with its real output images. +> Run it yourself via the [Python script](../../scripts/chapter_1_introduction/tutorial_1_grids_and_galaxies.py) or the [Jupyter notebook](../../notebooks/chapter_1_introduction/tutorial_1_grids_and_galaxies.ipynb). + +HowToGalaxy: Introduction +========================= + +Nearly a century ago, Edwin Hubble famously classified galaxies into three distinct groups: +ellipticals, spirals and irregulars. He produced a diagram of these galaxies, called the Hubble Tuning Fork, which +is shown below and still discussed by astronomers in the modern day: + +![HubbleTuning](https://github.com/PyAutoLabs/autogalaxy_workspace/blob/main/scripts/chapter_1_introduction/HubbleTuningFork.jpg) + +To make his diagram, Hubble looked at images of each galaxy in his sample, and subjectively judged by eye how +to classify it. Today, Astronomers use computer software, statistical algorithms and image processing techniques to +perform this task in a more quantifiable and objective way. + +The **HowToGalaxy** series of tutorials will teach you how to perform this analysis yourself, using the open-source +software package **PyAutoGalaxy**. By the end of the **HowToGalaxy** series, you'll be able to take an image of a +galaxy and study its morphology and structure using the same techniques that professional astronomers use today. + +Tutorial 1: Grids And Galaxies +============================== + +In this tutorial, we will introduce the fundamental concepts and quantities used to study galaxy morphology. +These concepts will enable us to create images of galaxies and analyze how their light is distributed across space. +Additionally, we will explore how adjusting various properties of galaxies can alter their appearance. For instance, +we can change the size of a galaxy, rotate it, or modify its brightness. + +To create these images, we first need to define 2D grids of \((y, x)\) coordinates. We will shift and rotate these +grids to manipulate the appearance of the galaxy in the generated images. The grid will serve as the input for light +profiles, which are analytic functions that describe the distribution of a galaxy's light. By evaluating these light +profiles on the grid, we can effectively generate images that represent the structure and characteristics of galaxies. + +__Contents__ + +- **Grids:** Create a uniform grid of (y,x) coordinates and show how it can be used to measure the light of a galaxy. +- **Geometry:** How to shift and rotate a grid, and convert it to elliptical coordinates. +- **Light Profiles:** Using light profiles, analytic functions that describe how a galaxy's light is distributed. +- **Galaxies:** Creating galaxies containing light profiles and computing the image of a galaxy. +- **One Dimension Projection:** Create projected 2D radial grids for 1D profile calculations. +- **Unit Conversion:** Converting angular distances to physical distances using cosmology. +- **Wrap Up:** Summary of the key concepts covered in this tutorial. + +The imports below are required to run the howtogalaxy tutorials in a Jupiter notebook. They also import the +`autogalaxy` package and the `autogalaxy.plot` module which are used throughout the tutorials. + + +```python + +from autoconf import setup_notebook; setup_notebook() + +import matplotlib.pyplot as plt +import numpy as np + +import autogalaxy as ag +import autogalaxy.plot as aplt +``` + + Working Directory has been set to `HowToGalaxy` + + +__Grids__ + +A `Grid2D` is a set of two-dimensional $(y,x)$ coordinates that represent points in space where we evaluate the +light emitted by a galaxy. + +Each coordinate on the grid is referred to as a 'pixel'. This is because we use the grid to create the image of a +galaxy at each of these coordinates, meaning that each coordinate maps to the centre of each pixel in this image. + +Grids are defined in units of 'arc-seconds' ("). An arc-second is a unit of angular measurement used by astronomers to +describe the apparent size of objects in the sky. + +The `pixel_scales` parameter sets how many arc-seconds each pixel represents. For example, if `pixel_scales=0.1`, +then each pixel covers 0.1" of the sky. + +We create a uniform 2D grid of 101 x 101 pixels with a pixel scale of 0.1", corresponding to an area +of 10.1" x 10.1", spanning from -5.05" to 5.05" in both the y and x directions. + + +```python +grid = ag.Grid2D.uniform( + shape_native=( + 101, + 101, + ), # The dimensions of the grid, which here is 101 x 101 pixels. + pixel_scales=0.1, # The conversion factor between pixel units and arc-seconds. +) +``` + +We can visualize this grid as a uniform grid of dots, each representing a coordinate where the light is measured. + + +```python +aplt.plot_grid(grid=grid, title="Uniform Grid of Coordinates") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_5_0.png) + + + +Each coordinate in the grid corresponds to an arc-second position. Below, we print a few of these coordinates to see +the values. + + +```python +print("(y,x) pixel 0:") +print(grid.native[0, 0]) # The coordinate of the first pixel. +print("(y,x) pixel 1:") +print(grid.native[0, 1]) # The coordinate of the second pixel. +print("(y,x) pixel 2:") +print(grid.native[0, 2]) # The coordinate of the third pixel. +print("(y,x) pixel 100:") +print(grid.native[1, 0]) # The coordinate of the 100th pixel. +print("...") +``` + + (y,x) pixel 0: + [ 5. -5.] + (y,x) pixel 1: + [ 5. -4.9] + (y,x) pixel 2: + [ 5. -4.8] + (y,x) pixel 100: + [ 4.9 -5. ] + ... + + +Grids have two internal representations, `native` and `slim`: + +- `native`: A 2D array with shape [total_y_pixels, total_x_pixels, 2], where the 2 corresponds to the (y,x) coordinates. +- `slim`: A 1D array with shape [total_y_pixels * total_x_pixels, 2], where the coordinates are 'flattened' into a single list. + +These formats are useful for different calculations and plotting. Here, we show the same coordinate using both formats. + + +```python +print("(y,x) pixel 0 (accessed via native):") +print(grid.native[0, 0]) +print("(y,x) pixel 0 (accessed via slim 1D):") +print(grid.slim[0]) +``` + + (y,x) pixel 0 (accessed via native): + [ 5. -5.] + (y,x) pixel 0 (accessed via slim 1D): + [ 5. -5.] + + +We can also check the shapes of the `Grid2D` object in both `native` and `slim` formats. For this grid, +the `native` shape is (101, 101, 2) and the `slim` shape is (10201, 2). + + +```python +print(grid.native.shape) +print(grid.slim.shape) +``` + + (101, 101, 2) + (10201, 2) + + +For the HowToGalaxy tutorials, you don't need to fully understand why grids have both native and slim representations. +Just note that both are used for calculations and plotting. + +*Exercise*: Try creating grids with different `shape_native` and `pixel_scales` using the `ag.Grid2D.uniform()` function +above. Observe how the grid coordinates change when you adjust `shape_native` and `pixel_scales`. + +__Geometry__ + +The above grid is centered on the origin (0.0", 0.0"). Sometimes, we need to shift the grid to be centered on a +specific point, like the center of a galaxy. + +We can shift the grid to a new center, (y_c, x_c), by subtracting this center from each coordinate. + + +```python +centre = (0.3, 0.5) # Shifting the grid to be centered at y=1.0", x=2.0". + +grid_shifted = grid +grid_shifted[:, 0] = grid_shifted[:, 0] - centre[0] # Shift in y-direction. +grid_shifted[:, 1] = grid_shifted[:, 1] - centre[1] # Shift in x-direction. + +print("(y,x) pixel 0 After Shift:") +print(grid_shifted.native[0, 0]) # The coordinate of the first pixel after shifting. +``` + + (y,x) pixel 0 After Shift: + [ 4.7 -5.5] + + +The grid is now centered around (0.3", 0.5"). We can plot the shifted grid to see this change. + +*Exercise*: Try shifting the grid to a different center, for example (0.0", 0.0") or (2.0", 3.0"). Observe how the +center of the grid changes when you adjust the `centre` variable. + + +```python +aplt.plot_grid(grid=grid_shifted, title="Grid Centered Around (0.3, 0.5)") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_15_0.png) + + + +Next, we can rotate the grid by an angle `phi` (in degrees). The rotation is counter-clockwise from the positive x-axis. + +To rotate the grid: + +1. Calculate the distance `radius` of each coordinate from the origin using $r = \sqrt{y^2 + x^2}$. +2. Determine the angle `theta` counter clockwise from the positive x-axis using $\theta = \arctan(y / x)$. +3. Adjust `theta` by the rotation angle and convert back to Cartesian coordinates via $y = r \sin(\theta)$ and $x = r \cos(\theta)$. + + +```python +angle_degrees = 60.0 + +y = grid_shifted[:, 0] +x = grid_shifted[:, 1] + +radius = np.sqrt(y**2 + x**2) +theta = np.arctan2(y, x) - np.radians(angle_degrees) + +grid_rotated = grid_shifted +grid_rotated[:, 0] = radius * np.sin(theta) +grid_rotated[:, 1] = radius * np.cos(theta) + +print("(y,x) pixel 0 After Rotation:") +print(grid_rotated.native[0, 0]) # The coordinate of the first pixel after rotation. +``` + + (y,x) pixel 0 After Rotation: + [7.11313972 1.3203194 ] + + +The grid has now been rotated 60 degrees counter-clockwise. We can plot it to see the change. + +*Exercise*: Try rotating the grid by a different angle, for example 30 degrees or 90 degrees. Observe how the grid +changes when you adjust the `angle_degrees` variable. + + +```python +aplt.plot_grid(grid=grid_rotated, title="Grid Rotated 60 Degrees") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_19_0.png) + + + +Next, we convert the rotated grid to elliptical coordinates using: + +$\eta = \sqrt{(x_r)^2 + (y_r)^2/q^2}$ + +Where `q` is the axis-ratio of the ellipse and `(x_r, y_r)` are the rotated coordinates. + +Elliptical coordinates are a system used to describe positions in relation to an ellipse rather than a circle. They +are particularly useful in astronomy when dealing with objects like galaxies, which often have elliptical shapes +due to their inclination or intrinsic shape. + +*Exercise*: Try converting the grid to elliptical coordinates using a different axis-ratio, for example 0.3 or 0.8. +What happens to the grid when you adjust the `axis_ratio` variable? + + +```python +axis_ratio = 0.5 +eta = np.sqrt((grid_rotated[:, 0]) ** 2 + (grid_rotated[:, 1]) ** 2 / axis_ratio**2) + +print("First Ten Elliptical Coordinates:") +print(eta[:10]) +``` + + First Ten Elliptical Coordinates: + [7.58747191 7.54210354 7.49879467 7.4575812 7.41849804 7.38157903 + 7.3468568 7.31436263 7.28412634 7.25617615] + + +Above, the angle $\phi$ (in degrees) was used to rotate the grid, and the axis-ratio $q$ was used to convert the grid +to elliptical coordinates. + +From now on, we'll describe ellipticity using "elliptical components" $\epsilon_{1}$ and $\epsilon_{2}$, calculated +from $\phi$ and $q$: + +$\epsilon_{1} = \frac{1 - q}{1 + q} \sin(2\phi)$ +$\epsilon_{2} = \frac{1 - q}{1 + q} \cos(2\phi)$ + +We'll refer to these as `ell_comps` in the code for brevity. + +Future tutorials will explain why $\epsilon_{1}$ and $\epsilon_{2}$ are preferred over $q$ and $\phi$. + +*Exercise*: Try computing the elliptical components from the axis-ratio and angle above. What happens to the elliptical +components when you adjust the `axis_ratio` and `angle_degrees` variables? + + +```python +fac = (1 - axis_ratio) / (1 + axis_ratio) +epsilon_y = fac * np.sin(2 * np.radians(angle_degrees)) +epsilon_x = fac * np.cos(2 * np.radians(angle_degrees)) + +ell_comps = (epsilon_y, epsilon_x) + +print("Elliptical Components:") +print(ell_comps) + +``` + + Elliptical Components: + (np.float64(0.28867513459481287), np.float64(-0.16666666666666657)) + + +__Light Profiles__ + +Galaxies are collections of stars, gas, dust, and other astronomical objects that emit light. Astronomers study this +light to understand various properties of galaxies. + +To model the light of a galaxy, we use light profiles, which are mathematical functions that describe how a galaxy's +light is distributed across space. By applying these light profiles to 2D grids of $(y, x)$ coordinates, we can +create images that represent a galaxy's luminous emission. + +A commonly used light profile is the `Sersic` profile, which is widely adopted in astronomy for representing galaxy +light. The `Sersic` profile is defined by the equation: + +$I_{\rm Ser} (\eta_{\rm l}) = I \exp \left\{ -k \left[ \left( \frac{\eta}{R} \right)^{\frac{1}{n}} - 1 \right] \right\}$ + +In this equation: + + - $\eta$ represents the elliptical coordinates of the profile in arc-seconds (refer to earlier sections for elliptical coordinates). + - $I$ is the intensity normalization of the profile, given in arbitrary units, which controls the overall brightness of the Sersic profile. + - $R$ is the effective radius in arc-seconds, which determines the size of the profile. + - $n$ is the Sersic index, which defines how 'steep' the profile is, influencing the concentration of light. + - $k$ is a constant that ensures half the light of the profile lies within the radius $R$, where $k = 2n - \frac{1}{3}$. + +We can evaluate this function using values for $(\eta, I, R, n)$ to calculate the intensity of the profile at +a particular elliptical coordinate. + + +```python +elliptical_coordinate = ( + 0.5 # The elliptical coordinate where we compute the intensity, in arc-seconds. +) +intensity = 1.0 # Intensity normalization of the profile in arbitrary units. +effective_radius = 2.0 # Effective radius of the profile in arc-seconds. +sersic_index = 1.0 # Sersic index of the profile. +k = 2 * sersic_index - ( + 1.0 / 3.0 +) # Calculating the constant k, note that this is an approximation. + +# Calculate the intensity of the Sersic profile at a specific elliptical coordinate. +sersic_value = np.exp( + -k * ((elliptical_coordinate / effective_radius) ** (1.0 / sersic_index) - 1.0) +) + +print("Intensity of Sersic Light Profile at Elliptical Coordinate 0.5:") +print(sersic_value) +``` + + Intensity of Sersic Light Profile at Elliptical Coordinate 0.5: + 3.4903429574618414 + + +The calculation above gives the intensity of the Sersic profile at an elliptical coordinate of 0.5. + +To create a complete image of the Sersic profile, we can evaluate the intensity at every point in our grid of +elliptical coordinates. + + +```python +sersic_image = np.exp(-k * ((eta / effective_radius) ** (1.0 / sersic_index) - 1.0)) +``` + +When we plot the resulting image, we can see how the properties of the grid affect its appearance: + + - The peak intensity is at the position (0.3", 0.5"), where we shifted the grid. + - The image is elongated along a 60° counter-clockwise angle, corresponding to the rotation of the grid. + - The image has an elliptical shape, consistent with the axis ratio of 0.5. + +This demonstrates how the geometry of the grid directly influences the appearance of the light profile. + +*Exercise*: Try changing the values of `centre`, `ell_comps`, `effective_radius`, and `sersic_index` above. +Observe how these adjustments change the Sersic profile image. + + +```python +aplt.plot_array( + array=ag.Array2D( + values=sersic_image, mask=grid.mask + ), # The `Array2D` object is discussed below. + title="Sersic Image", +) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_29_0.png) + + + +Instead of manually handling these transformations, we can use `LightProfile` objects from the `light_profile` +module (`lp`) for faster and more efficient calculations. + +Below, we define a `Sersic` light profile using the `Sersic` object. We can print the profile to display its parameters. + + +```python +sersic_light_profile = ag.lp.Sersic( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.1), + intensity=1.0, + effective_radius=2.0, + sersic_index=1.0, +) + +print(sersic_light_profile) +``` + + Sersic + centre: (0.0, 0.0) + ell_comps: (0.0, 0.1) + intensity: 1.0 + effective_radius: 2.0 + sersic_index: 1.0 + + +With this `Sersic` light profile, we can create an image by passing a grid to its `image_2d_from` method. + +The calculation will internally handle all the coordinate transformations and intensity evaluations we performed +manually earlier, making it much simpler. + +The `Sersic` profile we created just above is different from the one we used to manually compute the image, +so the image will look different. However, the process is the same. + + +```python +image = sersic_light_profile.image_2d_from(grid=grid) + +aplt.plot_array(array=image, title="Sersic Image via Light Profile") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_33_0.png) + + + +The `image` is returned as an `Array2D` object. Similar to a `Grid2D`, it has two forms: + + - `native`: A 2D array with shape [total_y_image_pixels, total_x_image_pixels]. + - `slim`: A 1D array that flattens this data into shape [total_y_image_pixels * total_x_image_pixels]. + +The `native` form is often used for visualizations, while the `slim` form can be useful for certain calculations. + + +```python +print("Intensity of pixel 0:") +print(image.native[0, 0]) +print("Intensity of pixel 1:") +print(image.slim[1]) +``` + + Intensity of pixel 0: + 0.01336649114209766 + Intensity of pixel 1: + 0.014020623587576737 + + +We can visualize the light profile's image. + +We provide it with the light profile and the grid, which are used to create and plot the image. + + +```python +aplt.plot_array( + array=sersic_light_profile.image_2d_from(grid=grid), title="Image via LightProfile" +) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_37_0.png) + + + +__One Dimension Projection__ + +We often want to calculative 1D quantities of a light profile, for example to plot how its light changes as +a function of radius. + +To do this, we must still input a 2D grid into the `image_2d_from` method, therefore we create a project 2D +radial grid as follows which has shape [Number_of_1d_coordinates, 2] and where all [:,0] entries are the same. + +A simple example of such a grid is as follows with 4 1D coordinates is: + + +```python +grid_2d_projected = ag.Grid2DIrregular( + [ + [1.000000e-06, 1.000000e-06], + [1.000000e-06, 1.000001e00], + [1.000000e-06, 2.000001e00], + [1.000000e-06, 3.000001e00], + ] +) +``` + +As in this example, we often already have a 2D grid we are using to calculate images of a light profile +and it would be convenient to simply create `grid_2d_projected` from that. + +For example, we may want the project grid which traces it major axis in uniform radial steps. + +This is easily computed using the `grid_2d_radial_project_from` function and passing the `centre` and `angle` +of a light profile we can make it align with the light profile itself. + +Note how in this example the two galaxy bulges are not rotationally aligned but we aligned the projected +grid with the first galaxy. The centres are aligned, but if they were not that would cause similar +issues. + + +```python +grid_2d_projected = grid.grid_2d_radial_projected_from( + centre=sersic_light_profile.centre, angle=sersic_light_profile.angle() +) + +image_1d = sersic_light_profile.image_2d_from(grid=grid_2d_projected) +``` + +We can now plot the 1D radial profile of the light profile. This profile shows how the intensity of the light +changes as a function of distance from the profile's center. This is a more informative way to visualize the light p +rofile's distribution. + +When we plot 1D quantities, we do not use built-in plotting functions as in 2D, but instead use standard +matplotlib functionality. + +The reason is partly that 1D plotting is simple, but also because 1D plots have many different decisions +about what is plotted and how they are computed, meaning its better to give the user full control. + +**Exercise**: Try plotting the 1D radial profile of Sersic profiles with different effective radii and Sersic indices. +Does the 1D representation show more clearly how the light distribution changes with these parameters? + + +```python +plt.plot(grid_2d_projected[:, 1], image_1d) +plt.xlabel("Radius (arcseconds)") +plt.ylabel("Luminosity") +plt.show() +plt.close() +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_43_0.png) + + + +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 `MatPlot2D` 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. + + +```python +aplt.plot_array( + array=sersic_light_profile.image_2d_from(grid=grid), + title="Sersic Image", + use_log10=True, +) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_45_0.png) + + + +__Galaxies__ + +Now, let's introduce `Galaxy` objects, which are key components in **PyAutoGalaxy**. + +A light profile represents a single feature of a galaxy, such as its bulge or disk. To model a complete galaxy, +we combine multiple `LightProfiles` 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.) + +Let's start by creating a galaxy with two `Sersic` light profiles, which notationally we will consider to represent +a bulge and disk component of the galaxy, the two most important structures seen in galaxies which drive the +Hubble tuning fork classification shown at the beginning of this tutorial. + + +```python +bulge = ag.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 = ag.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 = ag.Galaxy(redshift=0.5, bulge=bulge, disk=disk) + +print(galaxy) +``` + + Redshift: 0.5 + Light Profiles: + Sersic + centre: (0.0, 0.0) + ell_comps: (0.0, 0.111111) + intensity: 1.0 + effective_radius: 1.0 + sersic_index: 2.5 + Sersic + centre: (0.0, 0.0) + ell_comps: (0.0, 0.3) + intensity: 0.3 + effective_radius: 3.0 + sersic_index: 1.0 + + +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: + + +```python +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("...") + +aplt.plot_array(array=image, title="Bulge+Disk Image via Galaxy") +``` + + Intensity of `Grid2D` pixel 0: + 0.024894917164848044 + Intensity of `Grid2D` pixel 1: + 0.025428546280541572 + Intensity of `Grid2D` pixel 2: + 0.02596640780160061 + ... + + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_49_1.png) + + + +We can plot the galaxy's image, just like how we did for a light profile. + + +```python +aplt.plot_array(array=galaxy.image_2d_from(grid=grid), title="Galaxy Bulge+Disk Image") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_51_0.png) + + + +The bulge dominates the center of the image, and is pretty much the only luminous emission we see 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_of_light_profiles` method, which plots each individual light profile separately. + + +```python +aplt.subplot_galaxy_light_profiles(galaxy=galaxy, grid=grid) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_53_0.png) + + + +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. + + +```python +aplt.plot_array( + array=galaxy.image_2d_from(grid=grid), + title="Galaxy Bulge+Disk Image", + use_log10=True, +) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_55_0.png) + + + +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 if you should the +profiles offset from one another or plot them both from zero. + + +```python +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() +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_57_0.png) + + + +We can group multiple galaxies at the same redshift into a `Galaxies` object, which is created from a list of +individual galaxies. + +We create an additional galaxy and combine it with the original galaxy into a `Galaxies` object. This could +represent two galaxies merging or interacting with each other, which is commonly seen in studies of galaxy evolution. + + +```python +extra_galaxy = ag.Galaxy( + redshift=0.5, + bulge=ag.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 = ag.Galaxies(galaxies=[galaxy, extra_galaxy]) +``` + +The `Galaxies` object has similar methods as those for light profiles and individual galaxies. + +For example, `image_2d_from` sums the images of all the galaxies. + + +```python +image = galaxies.image_2d_from(grid=grid) +``` + +We can plot the combined image using a `Galaxies`, just like with other plotters. + + +```python +aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title="Image") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_63_0.png) + + + +A subplot of each individual galaxy image can also be created. + + +```python +aplt.subplot_galaxies(galaxies=galaxies, grid=grid) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_65_0.png) + + + +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. + + +```python +aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title="Image", use_log10=True) +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_67_0.png) + + + +__Unit Conversion__ + +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. + + +```python +cosmology = ag.cosmo.Planck15() + +kpc_per_arcsec = cosmology.kpc_per_arcsec_from(redshift=galaxy.redshift) + +print("Kiloparsecs per Arcsecond:") +print(kpc_per_arcsec) + +``` + + Kiloparsecs per Arcsecond: + 6.288247910157764 + + +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 in converted units of kiloparsecs. + +This passes the plotting modules `Units` object a `ticks_convert_factor` and manually specified the new units of the +plot ticks. + + +```python +aplt.plot_array(array=galaxy.image_2d_from(grid=grid), title="Image") +``` + + + +![png](tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_71_0.png) + + + +__Wrap Up__ + +You've learnt the basic quantities used to study galaxy morphology. + +Lets summarise what we've learnt: + +- **Grids**: We've learnt that a grid is a set of 2D coordinates that represent the positions where we measure the +light of a galaxy. + +- **Geometry**: We've learnt how to shift, rotate, and convert grids to elliptical coordinates. + +- **Light Profiles**: We've learnt that light profiles are mathematical functions that describe how a galaxy's light is +distributed in space. We've used the `Sersic` profile to create images of galaxies. + +- **Galaxies**: We've learnt that galaxies are collections of light profiles that represent a galaxy's light. We've +created galaxies with multiple light profiles and visualized their images. + +- **Unit Conversion**: We've learnt that by assuming redshifts for galaxies we can convert their quantities from +arcseconds to kiloparsecs. + +__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 galaxy morphology +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 from +to what Astronomers call an AB magnitude system, which is a system used to measure the brightness of galaxies. + +The `autogalaxy_workspace/*/guides/units` module contains many examples of unit conversions and how to use them, +but they will not be covered in the *HowToGalaxy* 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 which high accuracy across all use cases. + +Once you are more experienced, you should read up on over-sampling in more detail via +the `autogalaxy_workspace/*/guides/over_sampling.ipynb` notebook. + + +```python + +``` diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_15_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_15_0.png new file mode 100644 index 0000000..a51d3a4 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_15_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_19_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_19_0.png new file mode 100644 index 0000000..6524496 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_19_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_29_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_29_0.png new file mode 100644 index 0000000..d8d2c2c Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_29_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_33_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_33_0.png new file mode 100644 index 0000000..1047fe1 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_33_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_37_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_37_0.png new file mode 100644 index 0000000..c894ac5 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_37_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_43_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_43_0.png new file mode 100644 index 0000000..5945e1e Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_43_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_45_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_45_0.png new file mode 100644 index 0000000..1578cbc Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_45_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_49_1.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_49_1.png new file mode 100644 index 0000000..351e52b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_49_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_51_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_51_0.png new file mode 100644 index 0000000..1c45883 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_51_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_53_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_53_0.png new file mode 100644 index 0000000..0acaae2 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_53_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_55_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_55_0.png new file mode 100644 index 0000000..ae02510 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_55_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_57_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_57_0.png new file mode 100644 index 0000000..c4da721 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_57_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_5_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_5_0.png new file mode 100644 index 0000000..8487778 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_5_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_63_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_63_0.png new file mode 100644 index 0000000..394fd23 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_63_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_65_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_65_0.png new file mode 100644 index 0000000..140ae5b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_65_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_67_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_67_0.png new file mode 100644 index 0000000..29c3a75 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_67_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_71_0.png b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_71_0.png new file mode 100644 index 0000000..9ca518c Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_1_grids_and_galaxies_files/tutorial_1_grids_and_galaxies_71_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data.md b/markdown/chapter_1_introduction/tutorial_2_data.md new file mode 100644 index 0000000..4eca98d --- /dev/null +++ b/markdown/chapter_1_introduction/tutorial_2_data.md @@ -0,0 +1,519 @@ +> ✏️ **This page is auto-generated from [`scripts/chapter_1_introduction/tutorial_2_data.py`](../../scripts/chapter_1_introduction/tutorial_2_data.py) — do not edit it directly.** +> It shows the example fully executed, with its real output images. +> Run it yourself via the [Python script](../../scripts/chapter_1_introduction/tutorial_2_data.py) or the [Jupyter notebook](../../notebooks/chapter_1_introduction/tutorial_2_data.ipynb). + +Tutorial 2: Data +================ + +In the previous tutorial, we used light profiles to create images of galaxies. However, those images don't accurately +represent what we would observe through a telescope. + +Real telescope images, like those taken with the Charge Coupled Device (CCD) imaging detectors on the Hubble Space +Telescope (HST), include several factors that affect what we see: + +**Telescope Optics:** The optical components of the telescope can blur the light, influencing the image's sharpness. + +**Exposure Time:** The time the detector collects light, affecting the clarity of the image. Longer exposure times +gather more light, improving the signal-to-noise ratio and creating a clearer image. + +**Background Sky:** Light from a background sky, such as distant stars or zodiacal light, adds noise to the image. + +In this tutorial, we'll simulate a galaxy image by applying these real-world effects to the light profiles and images +we created earlier. + +__Contents__ + +- **Initial Setup:** Create a 2D grid and define galaxy light profiles for simulation. +- **Optics Blurring:** Simulate how the telescope optics blur the galaxy's light using PSF convolution. +- **Poisson Noise:** Add Poisson noise to the image, simulating CCD photon-to-electron randomness. +- **Background Sky:** Add background sky light that introduces noise across the entire image. +- **Simulator:** Use the SimulatorImaging object to simulate imaging data with all effects combined. +- **Output:** Save the simulated data to .fits files for use in future tutorials. +- **Wrap Up:** Summary of how CCD imaging data is simulated. + + +```python + +from autoconf import setup_notebook; setup_notebook() + +import numpy as np +from pathlib import Path +import autogalaxy as ag +import autogalaxy.plot as aplt +``` + + Working Directory has been set to `HowToGalaxy` + + +__Initial Setup__ + +To create our simulated galaxy image, we first need a 2D grid. This grid will represent the coordinate space over +which we will simulate the galaxy's light distribution. + + +```python +grid = ag.Grid2D.uniform( + shape_native=( + 101, + 101, + ), # The dimensions of the grid, which here is 101 x 101 pixels. + pixel_scales=0.1, # The conversion factor between pixel units and arc-seconds. +) +``` + +Next, we define the properties of our galaxy. In this tutorial, we’ll represent the galaxy with a bulge using a +Sersic light profile. + +In the previous tutorial, the units of `intensity` were arbitrary. However, for this tutorial, where we simulate +realistic imaging data, the intensity must have specific units. We’ll use units of electrons per second per pixel +($e- pix^-1 s^-1$), which is standard for CCD imaging data. + + +```python +galaxy = ag.Galaxy( + redshift=0.5, + bulge=ag.lp.Sersic( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.111111), + intensity=1.0, # in units of e- pix^-1 s^-1 + effective_radius=1.0, + sersic_index=2.5, + ), +) + +galaxies = ag.Galaxies(galaxies=[galaxy]) +``` + +To visualize the galaxy’s image, which we will use as the starting point for the simulations, we use the following code: + + +```python +aplt.plot_array( + array=galaxies.image_2d_from(grid=grid), title="Galaxy Image Before Simulating" +) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_7_0.png) + + + +__Optics Blurring__ + +All images captured using CCDs (like those on the Hubble Space Telescope) experience some level of blurring +due to the optics of the telescope. This blurring occurs because the optical system spreads out the light from each +point source (e.g., a star or a part of a galaxy). + +The Point Spread Function (PSF) describes how the telescope blurs the image. It can be thought of as a 2D representation +of how a single point of light would appear in the image, spread out by the optics. In practice, the PSF is a 2D +convolution kernel that we apply to the image to simulate this blurring effect. + + +```python +psf = ag.Convolver.from_gaussian( + shape_native=(11, 11), # The size of the PSF kernel, represented as an 11x11 grid. + sigma=0.1, # Controls the width of the Gaussian PSF, which determines the level of blurring. + pixel_scales=grid.pixel_scales, # Maintains consistency with the scale of the image grid. + normalize=True, # Normalizes the PSF kernel so that its values sum to 1. +) +``` + +We can visualize the PSF to better understand how it will blur the galaxy's image. The PSF is essentially a small +image that represents the spreading out of light from a single point source. This kernel will be used to blur the +entire galaxy image when we perform the convolution. + + +```python +aplt.plot_array(array=psf.kernel, title="PSF 2D Kernel") +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_11_0.png) + + + +The PSF is often more informative when plotted on a log10 scale. This approach allows us to clearly observe values +in its tail, which are much smaller than the central peak yet critical for many scientific analyses. The tail +values may significantly affect the spread and detail captured in the data. + + +```python +aplt.plot_array(array=psf.kernel, title="PSF 2D Kernel", use_log10=True) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_13_0.png) + + + +Next, we'll manually perform a 2D convolution of the PSF with the image of the galaxy. This convolution simulates the +blurring that occurs when the telescope optics spread out the galaxy's light. + +1. **Padding the Image**: Before convolution, we add padding (extra space with zero values) around the edges of the + image. This prevents unwanted edge effects when we perform the convolution, ensuring that the image's edges don't + become artificially altered by the process. + +2. **Convolution**: Using the `Convolver` object's `convolve` method, we apply the 2D PSF convolution to the padded + image. This step combines the PSF with the galaxy's light, simulating how the telescope spreads out the light. + +3. **Trimming the Image**: After convolution, we trim the padded areas back to their original size, obtaining a + convolved (blurred) image that matches the dimensions of the initial galaxy image. + + +```python +image = galaxies.image_2d_from(grid=grid) # The original unblurred image of the galaxy. +padded_image = galaxies.padded_image_2d_from( + grid=grid, + psf_shape_2d=psf.kernel.shape_native, # Adding padding based on the PSF size. +) +convolved_image = psf.convolved_image_from( + image=padded_image, blurring_image=None +) # Applying the PSF convolution. +blurred_image = convolved_image.trimmed_after_convolution_from( + kernel_shape=psf.kernel.shape_native +) # Trimming back to the original size. +``` + + .../PyAutoArray/autoarray/operators/convolver.py:1415: UserWarning: No blurring_image provided. Only the direct image will be convolved. This may change the correctness of the PSF convolution. + warnings.warn( + + +We can now plot the original and the blurred images side by side. This allows us to clearly see how the PSF +convolution affects the appearance of the galaxy, making the image appear softer and less sharp. + + +```python +aplt.plot_array(array=image, title="Galaxy Image Before PSF") +aplt.plot_array(array=blurred_image, title="Galaxy Image After PSF") + +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_17_0.png) + + + + + +![png](tutorial_2_data_files/tutorial_2_data_17_1.png) + + + +__Poisson Noise__ + +In addition to the blurring caused by telescope optics, we also need to consider Poisson noise when simulating imaging +data. + +When a telescope captures an image of a galaxy, photons from the galaxy are collected by the telescope's mirror and +directed onto a CCD (Charge-Coupled Device). The CCD is made up of a silicon lattice (or another material) that +converts incoming photons into electrons. These electrons are then gathered into discrete squares, which form the +pixels of the final image. + +The process of converting photons into electrons is inherently random, following a Poisson distribution. This randomness +means that the number of electrons in each pixel can vary, even if the same number of photons hits the CCD. Therefore, +the electron count per pixel becomes a Poisson random variable. For our simulation, this means that the recorded +number of photons in each pixel will differ slightly from the true number due to this randomness. + +To replicate this effect in our simulation, we can add Poisson noise to the galaxy image using NumPy’s random module, +which generates values from a Poisson distribution. + +It's important to note that the blurring caused by the telescope optics occurs before the photons reach the CCD. +Therefore, we need to add the Poisson noise after blurring the galaxy image. + +We also need to consider the units of our image data. Let’s assume that the galaxy image is measured in units of +electrons per second ($e^- s^{-1}$), which is standard for CCD imaging data. To simulate the number of electrons +actually detected in each pixel, we multiply the image by the observation’s exposure time. This conversion changes t +he units to the total number of electrons collected per pixel over the entire exposure time. + +Once the image is converted, we add Poisson noise, simulating the randomness in the photon-to-electron conversion +process. After adding the noise, we convert the image back to units of electrons per second for analysis, as +this is the preferred unit for astronomers when studying their data. + + +```python +exposure_time = 300.0 # Units of seconds +blurred_image_counts = ( + blurred_image * exposure_time +) # Convert to total electrons detected over the exposure time. +blurred_image_with_poisson_noise = ( + np.random.poisson(blurred_image_counts, blurred_image_counts.shape) / exposure_time +) # Add Poisson noise and convert back to electrons per second. +``` + +Here is what the blurred image with Poisson noise looks like. + + +```python +aplt.plot_array( + array=ag.Array2D(values=blurred_image_with_poisson_noise, mask=grid.mask), + title="Image With Poisson Noise", +) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_21_0.png) + + + +It is challenging to see the Poisson noise directly in the image above, as it is often subtle. To make the noise more +visible, we can subtract the blurred image without Poisson noise from the one with noise. + +This subtraction yields the "Poisson noise realization" which highlights the variation in each pixel due to the Poisson +distribution of photons hitting the CCD. It represents the noise values that were added to each pixel. We call +it the realization because it is one possible outcome of the Poisson process, and the noise will be different each time +we simulate the image. + + +```python +poisson_noise_realization = blurred_image_with_poisson_noise - blurred_image + +aplt.plot_array( + array=ag.Array2D(values=poisson_noise_realization, mask=grid.mask), + title="Poisson Noise Realization", +) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_23_0.png) + + + +__Background Sky__ + +The final effect we will consider when simulating imaging data is the background sky. + +In addition to light from the galaxy, the telescope also picks up light from the sky. This background sky light is +primarily due to two sources: zodiacal light, which is light scattered by interplanetary dust in the solar system, +and the unresolved emission from distant stars and galaxies. + +For our simulation, we'll assume that the background sky has a uniform brightness across the image, measured at +0.1 electrons per second per pixel. The background sky is added to the image before applying the PSF convolution +and adding Poisson noise. This is important because it means that the background contributes additional noise to the +image. + +The background sky introduces noise throughout the entire image, including areas where the galaxy is not present. +This is why CCD images often appear noisy, especially in regions far from where the galaxy signal is detected. +The sky noise can make it more challenging to observe faint details of the galaxy. + +To simulate this, we add a constant background sky to the galaxy image and then apply Poisson noise to create the +final simulated image as it would appear through a telescope. + + +```python +background_sky_level = 0.1 + +# Add background sky to the blurred galaxy image. +blurred_image_with_sky = blurred_image + background_sky_level +blurred_image_with_sky_counts = blurred_image_with_sky * exposure_time + +# Apply Poisson noise to the image with the background sky. +blurred_image_with_sky_poisson_noise = ( + np.random.poisson( + blurred_image_with_sky_counts, blurred_image_with_sky_counts.shape + ) + / exposure_time +) + +# Visualize the image with background sky and Poisson noise. +aplt.plot_array( + array=ag.Array2D(values=blurred_image_with_sky_poisson_noise, mask=grid.mask), + title="Image With Background Sky", +) + +# Create a noise map showing the differences between the blurred image with and without noise. +poisson_noise_realization = ( + blurred_image_with_sky_poisson_noise - blurred_image_with_sky +) + +aplt.plot_array( + array=ag.Array2D(values=poisson_noise_realization, mask=grid.mask), + title="Poisson Noise Realization", +) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_25_0.png) + + + + + +![png](tutorial_2_data_files/tutorial_2_data_25_1.png) + + + +__Simulator__ + +The `SimulatorImaging` object lets us create simulated imaging data while including the effects of PSF blurring, +Poisson noise, and background sky all at once: + + +```python +simulator = ag.SimulatorImaging( + exposure_time=300.0, + psf=psf, + background_sky_level=0.1, + add_poisson_noise_to_data=True, +) + +dataset = simulator.via_galaxies_from(galaxies=galaxies, grid=grid) +``` + + .../PyAutoArray/autoarray/operators/convolver.py:1415: UserWarning: No blurring_image provided. Only the direct image will be convolved. This may change the correctness of the PSF convolution. + warnings.warn( + + +By plotting the `data` from the dataset, we can see that it matches the image we simulated earlier. It includes +the effects of PSF blurring, Poisson noise, and noise from the background sky. This image is a realistic +approximation of what a telescope like the Hubble Space Telescope would capture. + + +```python +aplt.plot_array(array=dataset.data, title="Simulated Imaging Data") +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_29_0.png) + + + +The dataset also includes the `psf` (Point Spread Function) used to blur the galaxy image. + +For actual telescope data, the PSF is determined during data processing and is provided along with the observations. +It's crucial for accurately deconvolving the PSF from the galaxy image, allowing us to recover the true properties +of the galaxy. We'll explore this further in the next tutorial. + + +```python +aplt.plot_array(array=dataset.psf.kernel, title="Simulated PSF", use_log10=True) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_31_0.png) + + + +The dataset includes a `noise_map`, which represents the Root Mean Square (RMS) standard deviation of the noise +estimated for each pixel in the image. Higher noise values mean that the measurements in those pixels are +less certain, so those pixels are given less weight when analyzing the data. + +This `noise_map` is different from the Poisson noise arrays we plotted earlier. The Poisson noise arrays show the +actual noise added to the image due to the random nature of photon-to-electron conversion on the CCD, as calculated +using the numpy random module. These noise values are theoretical and cannot be directly measured in real telescope data. + +In contrast, the `noise_map` is our best estimate of the noise present in the image, derived from the data itself +and used in the fitting process. + + +```python +aplt.plot_array(array=dataset.noise_map, title="Simulated Noise Map") +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_33_0.png) + + + +The `signal-to-noise_map` shows the ratio of the signal in each pixel to the noise level in that pixel. It is +calculated by dividing the `data` by the `noise_map`. + +This ratio helps us understand how much of the observed signal is reliable compared to the noise, allowing us to +see where we can trust the detected signal from the galaxy and where the noise is more significant. + +In general, a signal-to-noise ratio greater than 3 indicates that the signal is likely real and not overwhelmed by +noise. For our datasets, the signal-to-noise ratio peaks at ~70, meaning we can trust the signal detected in the +image. + + +```python +aplt.plot_array(array=dataset.signal_to_noise_map, title="Signal-To-Noise Map") +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_35_0.png) + + + +The `Imaging` object can display all of these components together, making it a powerful tool for visualizing +simulated imaging data. + +It also shows the Data and PSF on a logarithmic (log10) scale, which helps highlight the faint details in these +components. + +The "Over Sampling" plots on the bottom of the figures display advanced features that can be ignored for now. + + +```python +aplt.subplot_imaging_dataset(dataset=dataset) +``` + + + +![png](tutorial_2_data_files/tutorial_2_data_37_0.png) + + + +__Output__ + +We will now save these simulated data to `.fits` files, the standard format used by astronomers for storing images. +Most imaging data from telescopes like the Hubble Space Telescope (HST) are stored in this format. + +The `dataset_path` specifies where the data will be saved, in this case, in the directory +`autogalaxy_workspace/dataset/imaging/howtogalaxy/`, which contains many example images distributed with +the `autogalaxy_workspace`. + +The files are named `data.fits`, `noise_map.fits`, and `psf.fits`, and will be used in the next tutorial. + + +```python +dataset_path = Path("dataset", "imaging", "howtogalaxy") +print("Dataset Path: ", dataset_path) + +aplt.fits_imaging( + dataset=dataset, + data_path=dataset_path / "data.fits", + noise_map_path=dataset_path / "noise_map.fits", + psf_path=dataset_path / "psf.fits", + overwrite=True, +) +``` + + Dataset Path: dataset/imaging/howtogalaxy + + +__Wrap Up__ + +In this tutorial, you learned how CCD imaging data of a galaxy is collected using real telescopes like the +Hubble Space Telescope, and how to simulate this data using the `SimulatorImaging` object. + +Let's summarise what we've covered: + +- **Optics Blurring**: The optics of a telescope blur the light from galaxies, reducing the clarity and sharpness of +the images. + +- **Poisson Noise**: The process of converting photons to electrons on a CCD introduces Poisson noise, which is random +variability in the number of electrons collected in each pixel. + +- **Background Sky**: Light from the sky is captured along with light from the galaxy, adding a layer of noise across +the entire image. + +- **Simulator**: The `SimulatorImaging` object enables us to simulate realistic imaging data by including all of +these effects together and contains the `data`, `psf`, and `noise_map` components. + +- **Output**: We saved the simulated data to `.fits` files, the standard format used by astronomers for storing images. + + +```python + +``` diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_11_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_11_0.png new file mode 100644 index 0000000..eb6a698 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_11_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_13_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_13_0.png new file mode 100644 index 0000000..509d47d Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_13_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_17_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_17_0.png new file mode 100644 index 0000000..6106735 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_17_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_17_1.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_17_1.png new file mode 100644 index 0000000..0721ecf Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_17_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_21_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_21_0.png new file mode 100644 index 0000000..d8848aa Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_21_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_23_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_23_0.png new file mode 100644 index 0000000..233b56f Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_23_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_25_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_25_0.png new file mode 100644 index 0000000..1f9ee35 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_25_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_25_1.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_25_1.png new file mode 100644 index 0000000..a7b745b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_25_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_29_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_29_0.png new file mode 100644 index 0000000..861f704 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_29_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_31_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_31_0.png new file mode 100644 index 0000000..10f1d4f Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_31_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_33_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_33_0.png new file mode 100644 index 0000000..69f69cd Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_33_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_35_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_35_0.png new file mode 100644 index 0000000..f3ca13f Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_35_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_37_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_37_0.png new file mode 100644 index 0000000..7600a53 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_37_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_7_0.png b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_7_0.png new file mode 100644 index 0000000..1a4e5fa Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_2_data_files/tutorial_2_data_7_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting.md b/markdown/chapter_1_introduction/tutorial_3_fitting.md new file mode 100644 index 0000000..e4ad110 --- /dev/null +++ b/markdown/chapter_1_introduction/tutorial_3_fitting.md @@ -0,0 +1,1005 @@ +> ✏️ **This page is auto-generated from [`scripts/chapter_1_introduction/tutorial_3_fitting.py`](../../scripts/chapter_1_introduction/tutorial_3_fitting.py) — do not edit it directly.** +> It shows the example fully executed, with its real output images. +> Run it yourself via the [Python script](../../scripts/chapter_1_introduction/tutorial_3_fitting.py) or the [Jupyter notebook](../../notebooks/chapter_1_introduction/tutorial_3_fitting.ipynb). + +Tutorial 3: Fitting +=================== + +In previous tutorials, we used light profiles to create simulated images of galaxies and visualized how these images +would appear when captured by a CCD detector on a telescope like the Hubble Space Telescope. + +However, this simulation process is the reverse of what astronomers typically do when analyzing real data. Usually, +astronomers start with an observation—an actual image of a galaxy—and aim to infer detailed information about the +galaxy’s properties, such as its shape, structure, formation, and evolutionary history. + +To achieve this, we must fit the observed image data with a model, identifying the combination of light profiles that +best matches the galaxy's appearance in the image. In this tutorial, we'll illustrate this process using the imaging +data simulated in the previous tutorial. Our goal is to demonstrate how we can recover the parameters of the light +profiles that we used to create the original simulation, as a proof of concept for the fitting procedure. + +The process of fitting data introduces essential statistical concepts like the `model`, `residual_map`, `chi-squared`, +`likelihood`, and `noise_map`. These terms are crucial for understanding how fitting works, not only in astronomy but +also in any scientific field that involves data modeling. This tutorial will provide a detailed introduction to these +concepts and show how they are applied in practice to analyze astronomical data. + +__Contents__ + +- **Dataset:** Load the imaging dataset previously simulated, consisting of the image, noise map, and PSF. +- **Mask:** Apply a mask to the data, excluding regions with low signal-to-noise ratios from the analysis. +- **Masked Grid:** Create a masked grid containing only coordinates of unmasked pixels. +- **Fitting:** Fit the data with a galaxy model, computing the model image, residuals, chi-squared, and log likelihood. +- **Incorrect Fit:** Demonstrate how small deviations from true parameters impact fit quality. +- **Model Fitting:** Perform a basic model fit, adjusting parameters to improve the fit. +- **Wrap Up:** Summary of the fitting process and key statistical concepts. + + +```python + +from autoconf import setup_notebook; setup_notebook() + +import numpy as np +from pathlib import Path +import autogalaxy as ag +import autogalaxy.plot as aplt +``` + + Working Directory has been set to `HowToGalaxy` + + +__Dataset__ + +We begin by loading the imaging dataset that we will use for fitting in this tutorial. This dataset is identical to the +one we simulated in the previous tutorial, representing how a galaxy would appear if captured by a CCD camera. + +In the previous tutorial, we saved this dataset as .fits files in the `autogalaxy_workspace/dataset/imaging/howtogalaxy` +folder. The `.fits` format is commonly used in astronomy for storing image data along with metadata, making it a +standard for CCD imaging. + +The `dataset_path` below specifies where these files are located: `autogalaxy_workspace/dataset/imaging/howtogalaxy/`. + + +```python +dataset_path = Path("dataset", "imaging", "howtogalaxy") +``` + +__Dataset Auto-Simulation__ + +If the dataset does not already exist on your system, it will be created by running the corresponding +simulator script. This ensures that all example scripts can be run without manually simulating data first. + + +```python +if not dataset_path.exists(): + import subprocess + import sys + + subprocess.run( + [sys.executable, "scripts/simulators/simple.py"], + check=True, + ) + + +dataset = ag.Imaging.from_fits( + data_path=dataset_path / "data.fits", + noise_map_path=dataset_path / "noise_map.fits", + psf_path=dataset_path / "psf.fits", + pixel_scales=0.1, +) +``` + +The `Imaging` object contains three key components: `data`, `noise_map`, and `psf`: + +- `data`: The actual image of the galaxy, which we will analyze. + +- `noise_map`: A map indicating the uncertainty or noise level in each pixel of the image, reflecting how much the + observed signal in each pixel might fluctuate due to instrumental or background noise. + +- `psf`: The Point Spread Function, which describes how a point source of light is spread out in the image by the + telescope's optics. It characterizes the blurring effect introduced by the instrument. + +Let's print some values from these components and plot a summary of the dataset to refresh our understanding of the +imaging data. + + +```python +print("Value of first pixel in imaging data:") +print(dataset.data.native[0, 0]) +print("Value of first pixel in noise map:") +print(dataset.noise_map.native[0, 0]) +print("Value of first pixel in PSF:") +print(dataset.psf.kernel.native[0, 0]) + +aplt.subplot_imaging_dataset(dataset=dataset) +``` + + Value of first pixel in imaging data: + -0.003333333333333341 + Value of first pixel in noise map: + 0.01795054935711501 + Value of first pixel in PSF: + 0.0 + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_7_1.png) + + + +__Mask__ + +The signal-to-noise map of the image highlights areas where the signal (light from the galaxy) is detected above the +background noise. Values above 3.0 indicate regions where the galaxy's light is detected with a signal-to-noise ratio +of at least 3, while values below 3.0 are dominated by noise, where the galaxy's light is not clearly distinguishable. + +To ensure the fitting process focuses only on meaningful data, we typically mask out regions with low signal-to-noise +ratios, removing areas dominated by noise from the analysis. This allows the fitting process to concentrate on the +regions where the galaxy is clearly detected. + +Here, we create a `Mask2D` to exclude certain regions of the image from the analysis. The mask defines which parts of +the image will be used during the fitting process. + +For our simulated image, a circular 3" mask centered at the center of the image is appropriate, since the simulated +galaxy was positioned at the center. + + +```python +mask = ag.Mask2D.circular( + shape_native=dataset.shape_native, + pixel_scales=dataset.pixel_scales, + radius=3.0, # The circular mask's radius in arc-seconds + centre=(0.0, 0.0), # center of the image which is also the center of the galaxy +) + +print(mask) # 1 = True, meaning the pixel is masked. Edge pixels are indeed masked. +print(mask[48:53, 48:53]) # Central pixels are `False` and therefore unmasked. +``` + + Mask2D([[False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False]]) + [] + + +We can visualize the mask over the galaxy image using an `Imaging`, which helps us adjust the mask as needed. +This is useful to ensure that the mask appropriately covers the galaxy's light and does not exclude important regions. + +To overlay objects like a mask onto a figure, we use the `Visuals2D` object. This tool allows us to add custom +visuals to any plot, providing flexibility in creating tailored visual representations. + + +```python +aplt.plot_array(array=dataset.data, title="Imaging Data With Mask") +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_11_0.png) + + + +Once we are satisfied with the mask, we apply it to the imaging data using the `apply_mask()` method. This ensures +that only the unmasked regions are considered during the analysis. + + +```python +dataset = dataset.apply_mask(mask=mask) +``` + + 2026-07-11 16:29:05,943 - autoarray.dataset.imaging.dataset - INFO - IMAGING - Data masked, contains a total of 225 image-pixels + + +When we plot the masked imaging data again, the mask is now automatically included in the plot, even though we did +not explicitly pass it using the `Visuals2D` object. The plot also zooms into the unmasked area, showing only the +region where we will focus our analysis. This is particularly helpful when working with large images, as it centers +the view on the regions where the galaxy's signal is detected. + + +```python +aplt.plot_array(array=dataset.data, title="Masked Imaging Data") +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_15_0.png) + + + +The mask is now stored as an additional attribute of the `Imaging` object, meaning it remains attached to the +dataset. This makes it readily available when we pass the dataset to a `FitImaging` object for the fitting process. + + +```python +print("Mask2D:") +print(dataset.mask) +``` + + Mask2D: + Mask2D([[False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False], + [False, False, False, False, False, False, False, False, False, + False, False, False, False, False, False]]) + + +In earlier tutorials, we discussed how grids and arrays have `native` and `slim` representations: + +- `native`: Represents the original 2D shape of the data, maintaining the full pixel array of the image. +- `slim`: Represents a 1D array containing only the values from unmasked pixels, allowing for more efficient + processing when working with large images. + +After applying the mask, the `native` and `slim` representations change as follows: + +- `native`: The 2D array keeps its original shape, [total_y_pixels, total_x_pixels], but masked pixels (those where + the mask is True) are set to 0.0. +- `slim`: This now only contains the unmasked pixel values, reducing the array size + from [total_y_pixels * total_x_pixels] to just the number of unmasked pixels. + +Let's verify this by checking the shape of the data in its `slim` representation. + + +```python +print("Number of unmasked pixels:") +print(dataset.data.native.shape) +print( + dataset.data.slim.shape +) # This should be lower than the total number of pixels, e.g., 100 x 100 = 10,000 +``` + + Number of unmasked pixels: + (15, 15) + (225,) + + +The `mask` object also has a `pixels_in_mask` attribute, which gives the number of unmasked pixels. This should +match the size of the `slim` data structure. + + +```python +print(dataset.data.mask.pixels_in_mask) +``` + + 225 + + +We can use the `slim` attribute to print the first unmasked values from the image and noise map: + + +```python +print("First unmasked image value:") +print(dataset.data.slim[0]) +print("First unmasked noise map value:") +print(dataset.noise_map.slim[0]) +``` + + First unmasked image value: + -0.003333333333333341 + First unmasked noise map value: + 0.01795054935711501 + + +Additionally, we can verify that the `native` data structure has zeros at the edges where the mask is applied and +retains non-zero values in the central unmasked regions. + + +```python +print("Example masked pixel in the image's native representation at its edge:") +print(dataset.data.native[0, 0]) +print("Example unmasked pixel in the image's native representation at its center:") +centre = tuple(s // 2 for s in dataset.data.shape_native) +print(dataset.data.native[centre]) +``` + + Example masked pixel in the image's native representation at its edge: + -0.003333333333333341 + Example unmasked pixel in the image's native representation at its center: + 8.75 + + +__Masked Grid__ + +In tutorial 1, we emphasized that the `Grid2D` object is crucial for evaluating a galaxy's light profile. This grid +contains (y, x) coordinates for each pixel in the image and is used to map out the positions where the galaxy's +light is calculated. + +From a `Mask2D`, we derive a `masked_grid`, which consists only of the coordinates of unmasked pixels. This ensures +that light profile calculations focus exclusively on regions where the galaxy's light is detected, saving computational +time and improving efficiency. + +Below, we plot the masked grid: + + +```python +masked_grid = mask.derive_grid.unmasked + +aplt.plot_grid(grid=masked_grid, title="Masked Grid2D") +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_27_0.png) + + + +By plotting this masked grid over the galaxy image, we can see that the grid aligns with the unmasked pixels of the +image. + +This alignment **is crucial** for accurate fitting because it ensures that when we evaluate a galaxy's light profile, +the calculations occur only at positions where we have real data from. + + +```python +aplt.plot_array(array=dataset.data, title="Image Data With 2D Grid Overlaid") +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_29_0.png) + + + +__Fitting__ + +Now that our data is masked, we are ready to proceed with the fitting process. + +Fitting the data is done using the `Galaxy` and `Galaxies objects that we introduced in tutorial 2. We will start by +setting up a `Galaxies`` object, using the same galaxy configuration that we previously used to simulate the +imaging data. This setup will give us what is known as a 'perfect' fit, as the simulated and fitted models are identical. + + +```python +galaxy = ag.Galaxy( + redshift=0.5, + bulge=ag.lp.Sersic( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.111111), + intensity=1.0, # in units of e- pix^-1 s^-1 + effective_radius=1.0, + sersic_index=2.5, + ), +) + +galaxies = ag.Galaxies(galaxies=[galaxy]) +``` + +Next, let's plot the image of the galaxies. This should look familiar, as it is the same image we saw in +previous tutorials. The difference now is that we use the dataset's `grid`, which corresponds to the `masked_grid` +we defined earlier. This means that the galaxy image is only evaluated in the unmasked region, skipping calculations +in masked regions. + + +```python +aplt.plot_array( + array=galaxies.image_2d_from(grid=dataset.grid), title="Galaxy Image To Be Fitted" +) +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_33_0.png) + + + +Now, we proceed to fit the image by passing both the `Imaging` and `Galaxies` objects to a `FitImaging` object. +This object will compute key quantities that describe the fit’s quality: + +`image`: Creates an image of the galaxies using their image_2d_from() method. +`model_data`: Convolves the galaxy image with the data's PSF to account for the effects of telescope optics. +`residual_map`: The difference between the model data and observed data. +`normalized_residual_map`: Residuals divided by noise values, giving units of noise. +`chi_squared_map`: Squares the normalized residuals. +`chi_squared` and `log_likelihood`: Sums the chi-squared values to compute chi_squared, and converts this into +a log_likelihood, which measures how well the model fits the data (higher values indicate a better fit). + +Let's create the fit and inspect each of these attributes: + + +```python +fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) +``` + +The `model_data` represents the galaxy's image after accounting for effects like PSF convolution. + +An important technical note is that when we mask data, we discussed above how the image of the galaxy is not evaluated +outside the mask and is set to zero. This is a problem for PSF convolution, as the PSF blurs light from these regions +outside the mask but at its edge into the mask. They must be correctly evaluated to ensure the model image accurately +represents the image data. + +The `FitImaging` object handles this internally, but evaluating the model image in the additional regions outside the mask +that are close enough to the mask edge to be blurred into the mask. + + +```python +print("First model image pixel:") +print(fit.model_data.slim[0]) +aplt.plot_array(array=fit.model_data, title="Model Image") +``` + + First model image pixel: + + + 0.9970308140413991 + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_37_2.png) + + + +Even before computing other fit quantities, we can normally assess if the fit is going to be good by visually comparing +the `data` and `model_data` and assessing if they look similar. + +In this example, the galaxies used to fit the data are the same as the galaxies used to simulate it, so the two +look very similar (the only difference is the noise in the image). + + +```python +aplt.plot_array(array=dataset.data, title="Data") +aplt.plot_array(array=fit.model_data, title="Model Image") +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_39_0.png) + + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_39_1.png) + + + +The `residual_map` is the different between the observed image and model image, showing where in the image the fit is +good (e.g. low residuals) and where it is bad (e.g. high residuals). + +The expression for the residual map is simply: + +\[ \text{residual} = \text{data} - \text{model\_data} \] + +The residual-map is plotted below, noting that all values are very close to zero because the fit is near perfect. +The only non-zero residuals are due to noise in the image. + + +```python +residual_map = dataset.data - fit.model_data +print("First residual-map pixel:") +print(residual_map.slim[0]) + +print("First residual-map pixel via fit:") +print(fit.residual_map.slim[0]) + +aplt.plot_array(array=fit.residual_map, title="Residual Map") +``` + + First residual-map pixel: + -1.0003641473747324 + First residual-map pixel via fit: + -1.0003641473747324 + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_41_1.png) + + + +Are these residuals indicative of a good fit to the data? Without considering the noise in the data, it's difficult +to ascertain. That is, its hard to ascenrtain if a residual value is large or small because this depends on the +amount of noise in that pixel. + +The `normalized_residual_map` divides the residual-map by the noise-map, giving the residual in units of the noise. +Its expression is: + +\[ \text{normalized\_residual} = \frac{\text{residual\_map}}{\text{noise\_map}} = \frac{\text{data} - \text{model\_data}}{\text{noise\_map}} \] + +If you're familiar with the concept of standard deviations (sigma) in statistics, the normalized residual map represents +how many standard deviations the residual is from zero. For instance, a normalized residual of 2.0 (corresponding +to a 95% confidence interval) means that the probability of the model underestimating the data by that amount is only 5%. + + +```python +normalized_residual_map = residual_map / dataset.noise_map + +print("First normalized residual-map pixel:") +print(normalized_residual_map.slim[0]) + +print("First normalized residual-map pixel via fit:") +print(fit.normalized_residual_map.slim[0]) + +aplt.plot_array(array=fit.normalized_residual_map, title="Normalized Residual Map") +``` + + First normalized residual-map pixel: + -55.72888759408473 + First normalized residual-map pixel via fit: + -55.72888759408473 + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_43_1.png) + + + +Next, we define the `chi_squared_map`, which is obtained by squaring the `normalized_residual_map` and serves as a +measure of goodness of fit. + +The chi-squared map is calculated as: + +\[ \chi^2 = \left(\frac{\text{data} - \text{model\_data}}{\text{noise\_map}}\right)^2 \] + +Squaring the normalized residual map ensures all values are positive. For instance, both a normalized residual of -0.2 +and 0.2 would square to 0.04, indicating the same quality of fit in terms of `chi_squared`. + +As seen from the normalized residual map, it's evident that the model provides a good fit to the data, in this +case because the chi-squared values are close to zero. + + +```python +chi_squared_map = (normalized_residual_map) ** 2 +print("First chi-squared pixel:") +print(chi_squared_map.slim[0]) + +print("First chi-squared pixel via fit:") +print(fit.chi_squared_map.slim[0]) + +aplt.plot_array(array=fit.chi_squared_map, title="Chi-Squared Map") +``` + + First chi-squared pixel: + 3105.708912474131 + First chi-squared pixel via fit: + 3105.708912474131 + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_45_1.png) + + + +Now, we consolidate all the information in our `chi_squared_map` into a single measure of goodness-of-fit +called `chi_squared`. + +It is defined as the sum of all values in the `chi_squared_map` and is computed as: + +\[ \chi^2 = \sum \left(\frac{\text{data} - \text{model\_data}}{\text{noise\_map}}\right)^2 \] + +This summing process highlights why ensuring all values in the chi-squared map are positive is crucial. If we +didn't square the values (making them positive), positive and negative residuals would cancel each other out, +leading to an inaccurate assessment of the model's fit to the data. + +The lower the `chi_squared`, the fewer residuals exist between the model's fit and the data, indicating a better +overall fit! + + +```python +chi_squared = np.sum(chi_squared_map) +print("Chi-squared = ", chi_squared) +print("Chi-squared via fit = ", fit.chi_squared) +``` + + Chi-squared = 2969344.8564160876 + Chi-squared via fit = 2969344.8564160876 + + +The reduced chi-squared is the `chi_squared` value divided by the number of data points (e.g., the number of pixels +in the mask). + +This quantity offers an intuitive measure of the goodness-of-fit, as it normalizes the `chi_squared` value by the +number of data points. That is, a reduced chi-squared of 1.0 indicates that the model provides a good fit to the data, +because every data point is fitted with a chi-squared value of 1.0. + +A reduced chi-squared value significantly greater than 1.0 indicates that the model is not a good fit to the data, +whereas a value significantly less than 1.0 suggests that the model is overfitting the data. + + +```python +reduced_chi_squared = chi_squared / dataset.mask.pixels_in_mask +print("Reduced Chi-squared = ", reduced_chi_squared) +``` + + Reduced Chi-squared = 13197.088250738167 + + +Another quantity that contributes to our final assessment of the goodness-of-fit is the `noise_normalization`. + +The `noise_normalization` is computed as the logarithm of the sum of squared noise values in our data: + +\[ +\text{{noise\_normalization}} = \sum \log(2 \pi \text{{noise\_map}}^2) +\] + +This quantity is fixed because the noise-map remains constant throughout the fitting process. Despite this, +including the `noise_normalization` is considered good practice due to its statistical significance. + +Understanding the exact meaning of `noise_normalization` isn't critical for our primary goal of successfully +fitting a model to a dataset. Essentially, it provides a measure of how well the noise properties of our data align +with a Gaussian distribution. + + +```python +noise_normalization = np.sum(np.log(2 * np.pi * dataset.noise_map**2)) +print("Noise Normalization = ", noise_normalization) +print("Noise Normalization via fit = ", fit.noise_normalization) +``` + + Noise Normalization = -1242.2701057123368 + Noise Normalization via fit = -1242.2701057123368 + + +From the `chi_squared` and `noise_normalization`, we can define a final goodness-of-fit measure known as +the `log_likelihood`. + +This measure is calculated by taking the sum of the `chi_squared` and `noise_normalization`, and then multiplying the +result by -0.5: + +\[ \text{log\_likelihood} = -0.5 \times \left( \chi^2 + \text{noise\_normalization} \right) \] + +Don't worry about why we multiply by -0.5; it's a standard practice in statistics to ensure the log likelihood is +defined correctly. + + +```python +log_likelihood = -0.5 * (chi_squared + noise_normalization) +print("Log Likelihood = ", log_likelihood) +print("Log Likelihood via fit = ", fit.log_likelihood) +``` + + Log Likelihood = -1484051.2931551877 + Log Likelihood via fit = -1484051.2931551877 + + +In the previous discussion, we noted that a lower \(\chi^2\) value indicates a better fit of the model to the +observed data. + +When we calculate the log likelihood, we take the \(\chi^2\) value and multiply it by -0.5. This means that a +higher log likelihood corresponds to a better model fit. Our goal when fitting models to data is to maximize the +log likelihood. + +The **reduced \(\chi^2\)** value provides an intuitive measure of goodness-of-fit. Values close to 1.0 suggest a +good fit, while values below or above 1.0 indicate potential underfitting or overfitting of the data, respectively. +In contrast, the log likelihood values can be less intuitive. For instance, a log likelihood value printed above +might be around 5300. + +However, log likelihoods become more meaningful when we compare them. For example, if we have two models, one with +a log likelihood of 5300 and the other with 5310 we can conclude that the first model fits the data better +because it has a higher log likelihood by 10.0. + +In fact, the difference in log likelihood between models can often be associated with a probability indicating how +much better one model fits the data compared to another. This can be expressed in terms of standard deviations (sigma). + +As a rule of thumb: + +- A difference in log likelihood of **2.5** suggests that one model is preferred at the **2.0 sigma** level. +- A difference in log likelihood of **5.0** indicates a preference at the **3.0 sigma** level. +- A difference in log likelihood of **10.0** suggests a preference at the **5.0 sigma** level. + +All these metrics can be visualized together using the `FitImaging` object, which offers a comprehensive +overview of the fit quality. + + +```python +fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) + +aplt.subplot_fit_imaging(fit=fit) +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_55_0.png) + + + +If you're familiar with model-fitting, you've likely encountered terms like 'residuals', 'chi-squared', +and 'log_likelihood' before. + +These metrics are standard ways to quantify the quality of a model fit. They are applicable not only to 1D data but +also to more complex data structures like 2D images, 3D data cubes, or any other multidimensional datasets. + +__Incorrect Fit___ + +In the previous section, we successfully created and fitted a galaxy model to the image data, resulting in an +excellent fit. The residual map and chi-squared map showed no significant discrepancies, indicating that the +galaxy's light was accurately captured by our model. This optimal solution translates to one of the highest log +likelihood values possible, reflecting a good match between the model and the observed data. + +Now, let's modify our galaxy model to create a fit that is close to the correct solution but slightly off. +Specifically, we will slightly offset the center of the galaxy by half a pixel (0.05") in both the x and y directions. +This change will allow us to observe how even small deviations from the true parameters can impact the quality of the fit. + + +```python +galaxy = ag.Galaxy( + redshift=0.5, + bulge=ag.lp.Sersic( + centre=(0.05, 0.05), # This is different from the previous center. + ell_comps=(0.0, 0.111111), + intensity=1.0, + effective_radius=1.0, + sersic_index=2.5, + ), +) + +galaxies = ag.Galaxies(galaxies=[galaxy]) +``` + +After implementing this slight adjustment, we can now plot the fit. In doing so, we observe that residuals have +emerged at the center of the galaxy, which indicates a mismatch between our model and the data. Consequently, +this discrepancy results in increased chi-squared values, which in turn affects our log likelihood. + + +```python +fit_bad = ag.FitImaging(dataset=dataset, galaxies=galaxies) + +aplt.subplot_fit_imaging(fit=fit_bad) +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_59_0.png) + + + +Next, we can compare the log likelihood of our current model to the log likelihood value we computed previously. + + +```python +print("Previous Likelihood:") +print(fit.log_likelihood) +print("New Likelihood:") +print(fit_bad.log_likelihood) +``` + + Previous Likelihood: + -1484051.2931551877 + New Likelihood: + -1564635.9594986455 + + +As expected, we observe that the log likelihood has decreased! This decline confirms that our new model is indeed a +worse fit to the data compared to the original model. + +Now, let’s change our galaxy model once more, this time setting it to a position that is far from the true parameters. +We will offset the galaxy's center significantly to see how this extreme deviation affects the fit quality. + + +```python +galaxy = ag.Galaxy( + redshift=0.5, + bulge=ag.lp.Sersic( + centre=( + 0.65, + 0.65, + ), # This position is significantly different from the previous one. + ell_comps=(0.0, 0.111111), + intensity=1.0, + effective_radius=1.0, + sersic_index=2.5, + ), +) + +galaxies = ag.Galaxies(galaxies=[galaxy]) + +fit_very_bad = ag.FitImaging(dataset=dataset, galaxies=galaxies) + +aplt.subplot_fit_imaging(fit=fit_very_bad) +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_63_0.png) + + + +It is now evident that this model provides a terrible fit to the data. The galaxies do not resemble a plausible +representation of our simulated galaxy dataset, which we already anticipated given that we generated the data ourselves! + +As expected, the log likelihood has dropped dramatically with this poorly fitting model. + + +```python +print("Previous Likelihoods:") +print(fit.log_likelihood) +print(fit_bad.log_likelihood) +print("New Likelihood:") +print(fit_very_bad.log_likelihood) +``` + + Previous Likelihoods: + -1484051.2931551877 + -1564635.9594986455 + New Likelihood: + -4260401.8922673315 + + +__Model Fitting__ + +In the previous sections, we used the true model to fit the data, which resulted in a high log likelihood and minimal +residuals. We also demonstrated how even small deviations from the true parameters can significantly degrade the fit +quality, reducing the log likelihood. + +In practice, however, we don't know the "true" model. For example, we might have an image of a galaxy observed with +the Hubble Space Telescope, but the values for parameters like its `effective_radius`, `sersic_index`, and others are +unknown. The process of determining the best-fit model is called model fitting, and it is the main topic of +Chapter 2 of *HowToGalaxy*. + +To conclude this section, let's perform a basic, hands-on model fit to develop some intuition about how we can find +the best-fit model. We'll start by loading a simple dataset that was simulated using a `Sersic` profile, where the +true parameters of this profile are unknown. + + +```python +dataset_name = "simple" +dataset_path = Path("dataset") / "imaging" / dataset_name + +dataset = ag.Imaging.from_fits( + data_path=dataset_path / "data.fits", + psf_path=dataset_path / "psf.fits", + noise_map_path=dataset_path / "noise_map.fits", + pixel_scales=0.1, +) + +mask = ag.Mask2D.circular( + shape_native=dataset.shape_native, + pixel_scales=dataset.pixel_scales, + radius=3.0, +) + +dataset = dataset.apply_mask(mask=mask) + +aplt.subplot_imaging_dataset(dataset=dataset) + +``` + + 2026-07-11 16:29:10,659 - autoarray.dataset.imaging.dataset - INFO - IMAGING - Data masked, contains a total of 2828 image-pixels + + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_67_1.png) + + + +Now, you'll try to determine the best-fit model for this image, corresponding to the parameters used to simulate the +dataset. + +We'll use the simplest possible approach: try different combinations of light profile parameters and adjust them +based on how well each model fits the data. You’ll quickly find that certain parameters produce a much better fit +than others. For example, determining the correct values of the `centre` should not take too long. + +Pay attention to the `log_likelihood` and the `residual_map` as you adjust the parameters. These will guide you in +determining if your model is providing a good fit to the data. Aim to increase the log likelihood and reduce the +residuals. + +Keep experimenting with different values for a while, seeing how small you can make the residuals and how high you +can push the log likelihood. Eventually, you’ll likely reach a point where further improvements become difficult, +even after trying many different parameter values. This is a good point to stop and reflect on your first experience +with model fitting, and then to scroll to the next cell to see a discussion of the exercise. + + +```python + +galaxy = ag.Galaxy( + redshift=0.5, + bulge=ag.lp.Sersic( + centre=(1.0, 10), # These are the parameters + ell_comps=(0.5, 0.9), # you need to adjust + intensity=1.0, # to try and improve + effective_radius=1.0, # the model's fit + sersic_index=1.0, # to the data! + ), +) +galaxies = ag.Galaxies(galaxies=[galaxy]) + +fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) + +aplt.subplot_fit_imaging(fit=fit) + +print("Log Likelihood:") +print(fit.log_likelihood) +``` + + + +![png](tutorial_3_fitting_files/tutorial_3_fitting_69_0.png) + + + + Log Likelihood: + -238826.9938543461 + + +Manually guessing model parameters repeatedly is a very inefficient and slow way to find the best fit. If the model +were more complex—say, if the `galaxy` had additional light profile components beyond just its `bulge` (like a +second `Sersic` profile representing a `disk`)—the model would become so intricate that this manual approach +would be practically impossible. This is definitely not how model fitting is done in practice. + +However, this exercise has given you a basic intuition for how model fitting works. The statistical inference tools +that are actually used for model fitting will be introduced in Chapter 2. Interestingly, these tools are not entirely +different from the approach you just tried. Essentially, they also involve iteratively testing models until those +with high log likelihoods are found. The key difference is that a computer can perform this process thousands of +times, and it does so in a much more efficient and strategic way. + +__Wrap Up__ + +In this tutorial, you have learned how to fit a galaxy model to imaging data, a fundamental process in astronomy +and statistical inference. + +Let's summarise what we have covered: + +- **Dataset**: We loaded the imaging dataset that we previously simulated, consisting of the galaxy image, noise map, + and PSF. + +- **Mask**: We applied a circular mask to the data, excluding regions with low signal-to-noise ratios from the analysis. + +- **Masked Grid**: We created a masked grid, which contains only the coordinates of unmasked pixels, to evaluate the + galaxy's light profile. + +- **Fitting**: We fitted the data with a galaxy model, computing key quantities like the model image, residuals, + chi-squared, and log likelihood to assess the quality of the fit. + +- **Bad Fits**: We demonstrated how even small deviations from the true parameters can significantly impact the fit + quality, leading to decreased log likelihood values. + +- **Model Fitting**: We performed a basic model fit on a simple dataset, adjusting the model parameters to improve the + fit quality. + + +```python + +``` diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_11_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_11_0.png new file mode 100644 index 0000000..a02ecce Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_11_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_15_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_15_0.png new file mode 100644 index 0000000..cace531 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_15_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_27_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_27_0.png new file mode 100644 index 0000000..e84c182 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_27_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_29_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_29_0.png new file mode 100644 index 0000000..ff2a534 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_29_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_33_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_33_0.png new file mode 100644 index 0000000..4471f32 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_33_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_37_2.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_37_2.png new file mode 100644 index 0000000..e0ed78b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_37_2.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_39_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_39_0.png new file mode 100644 index 0000000..f34457a Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_39_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_39_1.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_39_1.png new file mode 100644 index 0000000..e0ed78b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_39_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_41_1.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_41_1.png new file mode 100644 index 0000000..d071171 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_41_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_43_1.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_43_1.png new file mode 100644 index 0000000..a739035 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_43_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_45_1.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_45_1.png new file mode 100644 index 0000000..5d79446 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_45_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_55_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_55_0.png new file mode 100644 index 0000000..2584617 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_55_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_59_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_59_0.png new file mode 100644 index 0000000..a812859 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_59_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_63_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_63_0.png new file mode 100644 index 0000000..3a5ec6b Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_63_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_67_1.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_67_1.png new file mode 100644 index 0000000..20ba32a Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_67_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_69_0.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_69_0.png new file mode 100644 index 0000000..f5218f2 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_69_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_7_1.png b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_7_1.png new file mode 100644 index 0000000..b429b56 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_3_fitting_files/tutorial_3_fitting_7_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_4_methods.md b/markdown/chapter_1_introduction/tutorial_4_methods.md new file mode 100644 index 0000000..cb861ff --- /dev/null +++ b/markdown/chapter_1_introduction/tutorial_4_methods.md @@ -0,0 +1,7 @@ +> ✏️ **This page is auto-generated from [`scripts/chapter_1_introduction/tutorial_4_methods.py`](../../scripts/chapter_1_introduction/tutorial_4_methods.py) — do not edit it directly.** +> It shows the example fully executed, with its real output images. +> Run it yourself via the [Python script](../../scripts/chapter_1_introduction/tutorial_4_methods.py) or the [Jupyter notebook](../../notebooks/chapter_1_introduction/tutorial_4_methods.ipynb). + +```python + +``` diff --git a/markdown/chapter_1_introduction/tutorial_5_summary.md b/markdown/chapter_1_introduction/tutorial_5_summary.md new file mode 100644 index 0000000..a64883d --- /dev/null +++ b/markdown/chapter_1_introduction/tutorial_5_summary.md @@ -0,0 +1,266 @@ +> ✏️ **This page is auto-generated from [`scripts/chapter_1_introduction/tutorial_5_summary.py`](../../scripts/chapter_1_introduction/tutorial_5_summary.py) — do not edit it directly.** +> It shows the example fully executed, with its real output images. +> Run it yourself via the [Python script](../../scripts/chapter_1_introduction/tutorial_5_summary.py) or the [Jupyter notebook](../../notebooks/chapter_1_introduction/tutorial_5_summary.ipynb). + +Tutorial 9: Summary +=================== + +In this chapter, we have learnt that: + + 1) **PyAutoGalaxy** uses Cartesian `Grid2D`'s of $(y,x)$ coordinates to evaluate galaxy luminous emission. + 2) These grids are combined with light profiles to compute images and other quantities. + 3) Profiles are grouped together to make galaxies. + 4) Collections of galaxies (at the same redshift) can be made.. + 5) The Universe's cosmology can be input into this `Galaxies` to convert its units to kiloparsecs. + 6) The galaxies's image can be used to simulate galaxy `Imaging` like it was observed with a real telescope. + 7) This data can be fitted, so to as quantify how well a model galaxy system represents the observed image. + +In this summary, we'll go over all the different Python objects introduced throughout this chapter and consider how +they come together as one. + +__Contents__ + +- **Initial Setup:** Create profiles, galaxies and a Galaxies object for illustration. +- **Object Composition:** How Galaxies, Galaxy and Profile objects compose together. +- **Visualization:** Customize and visualize any aspect of galaxies using the plotting API. +- **Code Design:** Discussion of PyAutoGalaxy's object-oriented design philosophy. +- **Source Code:** Links to the source code repositories for PyAutoFit, PyAutoArray and PyAutoGalaxy. +- **Wrap Up:** Summary of chapter 1 and preview of the modeling chapter. + + +```python + +from autoconf import setup_notebook; setup_notebook() + +from pathlib import Path +import autogalaxy as ag +import autogalaxy.plot as aplt +``` + + Working Directory has been set to `HowToGalaxy` + + +__Initial Setup__ + +Below, we do all the steps we have learned this chapter, making profiles, galaxies, etc. + +Note that we use two galaxies, the first of which has a bulge and disk. + + +```python +grid = ag.Grid2D.uniform(shape_native=(100, 100), pixel_scales=0.05) + +galaxy_0 = ag.Galaxy( + redshift=0.5, + bulge=ag.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=ag.lp.Exponential( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.111111), + intensity=1.0, + effective_radius=1.0, + ), +) + +galaxy_1 = ag.Galaxy( + redshift=0.5, + bulge=ag.lp.Sersic( + centre=(1.0, 1.0), + ell_comps=(0.0, 0.111111), + intensity=1.0, + effective_radius=1.0, + sersic_index=2.5, + ), +) + +galaxies = ag.Galaxies(galaxies=[galaxy_0, galaxy_1]) +``` + +__Object Composition__ + +Lets now consider how all of the objects we've covered throughout this chapter (`LightProfile`'s, `MassProfile`'s, +`Galaxy`'s, `Galaxies`'s) come together. + +The `Galaxies` contain the `Galaxy`'s which contains the `Profile`'s: + + +```python +print(galaxies[0]) +print() +print(galaxies[0].bulge) +print() +print(galaxies[0].disk) +print() +print(galaxies[1].bulge) +print() +``` + + Redshift: 0.5 + Light Profiles: + Sersic + centre: (0.0, 0.0) + ell_comps: (0.0, 0.111111) + intensity: 1.0 + effective_radius: 1.0 + sersic_index: 2.5 + Exponential + centre: (0.0, 0.0) + ell_comps: (0.0, 0.111111) + intensity: 1.0 + effective_radius: 1.0 + sersic_index: 1.0 + + Sersic + centre: (0.0, 0.0) + ell_comps: (0.0, 0.111111) + intensity: 1.0 + effective_radius: 1.0 + sersic_index: 2.5 + + Exponential + centre: (0.0, 0.0) + ell_comps: (0.0, 0.111111) + intensity: 1.0 + effective_radius: 1.0 + sersic_index: 1.0 + + Sersic + centre: (1.0, 1.0) + ell_comps: (0.0, 0.111111) + intensity: 1.0 + effective_radius: 1.0 + sersic_index: 2.5 + + + +Once we have defined the galaxies, we can plot any quantity introduced throughout this chapter for a specific component, +a single galaxy, or multiple galaxies as needed. + +For example, if we want to plot the image of the first galaxy's bulge and disk, we can do this in a variety of +different ways. + + +```python +aplt.plot_array(array=galaxies.image_2d_from(grid=grid), title="Image") + +aplt.plot_array(array=galaxies[0].image_2d_from(grid=grid), title="Image") +``` + + + +![png](tutorial_5_summary_files/tutorial_5_summary_7_0.png) + + + + + +![png](tutorial_5_summary_files/tutorial_5_summary_7_1.png) + + + +Understanding how these objects decompose into the different components of a galaxy is important for general +**PyAutoGalaxy** use. + +As the galaxy systems that we analyse become more complex, it is useful to know how to decompose their light +profiles, galaxies and galaxies to extract different pieces of information about the galaxy. + +For example, we made our galaxy above with two light profiles, a `bulge` and `disk`. We can plot the image of +each component individually, now that we know how to break-up the different components of the galaxies. + + +```python +aplt.plot_array(array=galaxies[0].bulge.image_2d_from(grid=grid), title="Bulge Image") + +aplt.plot_array(array=galaxies[0].disk.image_2d_from(grid=grid), title="Disk Image") +``` + + + +![png](tutorial_5_summary_files/tutorial_5_summary_9_0.png) + + + + + +![png](tutorial_5_summary_files/tutorial_5_summary_9_1.png) + + + +__Visualization__ + +Furthermore, using the `MatPLot2D` and `Visuals2D` objects we can visualize any aspect we're interested +in and fully customize the figure. + +Before beginning chapter 2 of **HowToGalaxy**, you should checkout the package `autogalaxy_workspace/plot`. +This provides a full API reference of every plotting option in **PyAutoGalaxy**, allowing you to create your own +fully customized figures of galaxies with minimal effort! + + +```python +aplt.plot_array(array=galaxies[0].bulge.image_2d_from(grid=grid), title="Bulge Image") +``` + + + +![png](tutorial_5_summary_files/tutorial_5_summary_11_0.png) + + + +And, we're done, not just with the tutorial, but the chapter! + +__Code Design__ + +To end, I want to quickly talk about the **PyAutoGalaxy** code-design and structure, which was really the main topic of +this tutoriag. + +Throughout this chapter, we never talk about anything like it was code. We didn`t refer to 'variables', 'parameters`' +'functions' or 'dictionaries', did we? Instead, we talked about 'galaxies'. We discussed +the objects that we, as scientists, think about when we consider a galaxy system. + +Software that abstracts the underlying code in this way follows an `object-oriented design`, and it is our hope +with **PyAutoGalaxy** that we've made its interface (often called the API for short) very intuitive, whether you were +previous familiar with galaxy morphology or a complete newcomer! + +__Source Code__ + +If you do enjoy code, variables, functions, and parameters, you may want to dig deeper into the **PyAutoGalaxy** source +code at some point in the future. Firstly, you should note that all of the code we discuss throughout the **HowToGalaxy** +lectures is not contained in just one project (e.g. the **PyAutoGalaxy** GitHub repository) but in fact three repositories: + +**PyAutoFit** - Everything required for modeling (the topic of chapter 2): https://github.com/PyAutoLabs/PyAutoFit + +**PyAutoArray** - Handles all data structures and Astronomy dataset objects: https://github.com/PyAutoLabs/PyAutoArray + +**PyAutoGalaxy** - Contains the light profiles and galaxies: https://github.com/PyAutoLabs/PyAutoGalaxy + +Instructions on how to build these projects from source are provided here: + +https://pyautogalaxy.readthedocs.io/en/latest/installation/source.html + +We take a lot of pride in our source code, so I can promise you its well written, well documented and thoroughly +tested (check out the `test` directory if you're curious how to test code well!). + +__Wrap Up__ + +You`ve learn a lot in this chapter, but what you have not learnt is how to 'model' a real galaxy. + +In the real world, we have no idea what the 'correct' combination of light profiles are that will give a good fit to +a galaxy. Modeling is the process of finding the model which provides a good fit and it is the topic of chapter 2 +of **HowToGalaxy**. + +Finally, if you enjoyed doing the **HowToGalaxy** tutorials please git us a star on the **PyAutoGalaxy** GitHub +repository: + + https://github.com/PyAutoLabs/PyAutoGalaxy + +Even the smallest bit of exposure via a GitHub star can help our project grow! + + +```python + +``` diff --git a/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_11_0.png b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_11_0.png new file mode 100644 index 0000000..f862579 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_11_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_7_0.png b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_7_0.png new file mode 100644 index 0000000..678e22d Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_7_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_7_1.png b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_7_1.png new file mode 100644 index 0000000..7338c92 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_7_1.png differ diff --git a/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_9_0.png b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_9_0.png new file mode 100644 index 0000000..f862579 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_9_0.png differ diff --git a/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_9_1.png b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_9_1.png new file mode 100644 index 0000000..fa0c645 Binary files /dev/null and b/markdown/chapter_1_introduction/tutorial_5_summary_files/tutorial_5_summary_9_1.png differ diff --git a/notebooks/chapter_1_introduction/tutorial_3_fitting.ipynb b/notebooks/chapter_1_introduction/tutorial_3_fitting.ipynb index 0f9d136..e6cab7c 100644 --- a/notebooks/chapter_1_introduction/tutorial_3_fitting.ipynb +++ b/notebooks/chapter_1_introduction/tutorial_3_fitting.ipynb @@ -76,6 +76,8 @@ "cell_type": "code", "metadata": {}, "source": [ + "\n", + "from autoconf import setup_notebook; setup_notebook()\n", "\n", "import numpy as np\n", "from pathlib import Path\n", diff --git a/scripts/chapter_1_introduction/tutorial_3_fitting.py b/scripts/chapter_1_introduction/tutorial_3_fitting.py index 2883940..563bafe 100644 --- a/scripts/chapter_1_introduction/tutorial_3_fitting.py +++ b/scripts/chapter_1_introduction/tutorial_3_fitting.py @@ -30,6 +30,8 @@ - **Wrap Up:** Summary of the fitting process and key statistical concepts. """ +# from autoconf import setup_notebook; setup_notebook() + import numpy as np from pathlib import Path import autogalaxy as ag