diff --git a/scripts/guides/results/_quick_fit.py b/scripts/guides/results/_quick_fit.py index 3b2ffae3..0e012e32 100644 --- a/scripts/guides/results/_quick_fit.py +++ b/scripts/guides/results/_quick_fit.py @@ -20,7 +20,9 @@ import sys from pathlib import Path -results_path = Path("output") / "results_folder" +from autoconf.test_mode import with_test_mode_segment + +results_path = with_test_mode_segment(Path("output")) / "results_folder" if results_path.exists(): sys.exit(0) diff --git a/scripts/guides/results/aggregator/data_fitting.py b/scripts/guides/results/aggregator/data_fitting.py index a49204fb..6b512436 100644 --- a/scripts/guides/results/aggregator/data_fitting.py +++ b/scripts/guides/results/aggregator/data_fitting.py @@ -1,222 +1,224 @@ -""" -Results: Data Fitting -===================== - -In this tutorial, we use the aggregator to load models and data from a non-linear search and use them to perform -fits to the data. - -We show how to use these tools to inspect the maximum log likelihood model of a fit to the data, customize things -like its visualization and also inspect fits randomly drawm from the PDF. - -__Contents__ - -- **Aggregator:** Load results using the Aggregator. -- **Fits via Aggregator:** Load datasets and maximum log likelihood fits using ImagingAgg and FitImagingAgg. -- **Modification:** Modify fit settings (e.g. over-sampling) when loading fits from the aggregator. -- **Visualization Customization:** Customize plot appearance when inspecting fits via the aggregator. -- **Errors (Random draws from PDF):** Inspect how fits vary within the PDF errors. - -__Interferometer__ - -This script can easily be adapted to analyse the results of charge injection imaging model-fits. - -The only entries that needs changing are: - - - `ImagingAgg` -> `InterferometerAgg`. - - `FitImagingAgg` -> `FitInterferometerAgg`. - - `Imaging` -> `Interferometer`. - - `FitImaging` -> `FitInterferometer`. - -Quantities specific to an interfometer, for example its uv-wavelengths real space mask, are accessed using the same API -(e.g. `values("dataset.uv_wavelengths")` and `.values{"dataset.real_space_mask")). - -__Database File__ - -The aggregator can also load results from a `.sqlite` database file. - -This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) -because it is optimized for fast querying of results. - -See the package `results/database` for a full description of how to set up the database and the benefits it provides, -especially if loading results from hard-disk is slow. -""" - -# from autoconf import setup_notebook; setup_notebook() - -import os -from pathlib import Path - -import autofit as af -import autogalaxy as ag -import autogalaxy.plot as aplt - -""" -__Aggregator__ - -Set up the aggregator as shown in `start_here.py`. -""" -from autofit.aggregator.aggregator import Aggregator - -results_path = Path("output") / "results_folder" -if not results_path.exists(): - import subprocess - import sys - - subprocess.run( - [sys.executable, "scripts/guides/results/_quick_fit.py"], - check=True, - ) - -agg = Aggregator.from_directory( - directory=Path("output") / "results_folder", -) - -""" -The masks we used to fit the galaxies is accessible via the aggregator. -""" -mask_gen = agg.values("dataset.mask") -print([mask for mask in mask_gen]) - -""" -The info dictionary we passed is also available. -""" -print("Info:") -info_gen = agg.values("info") -print([info for info in info_gen]) - -""" -__Fits via Aggregator__ - -Having performed a model-fit, we now want to interpret and visualize the results. In this example, we inspect -the `Imaging` objects that gave good fits to the data. - -Using the API shown in the `start_here.py` example this would require us to create a `Samples` object and manually -compose our own `Imaging` object. For large datasets, this would require us to use generators to ensure it is -memory-light, which are cumbersome to write. - -This example therefore uses the `ImagingAgg` object, which conveniently loads the `Imaging` objects of every fit via -generators for us. Explicit examples of how to do this via generators is given in the `advanced/manual_generator.py` -tutorial. - -We get a dataset generator via the `ag.agg.ImagingAgg` object, where this `dataset_gen` contains the maximum log -likelihood `Imaging `object of every model-fit. - -The `dataset_gen` returns a list of `Imaging` objects, as opposed to just a single `Imaging` object. This is because -only a single `Analysis` class was used in the model-fit, meaning there was only one `Imaging` dataset that was -fit. - -The `multi` package of the workspace illustrates model-fits which fit multiple datasets -simultaneously, (e.g. multi-wavelength imaging) by summing `Analysis` objects together, where the `dataset_list` -would contain multiple `Imaging` objects. -""" -dataset_agg = ag.agg.ImagingAgg(aggregator=agg) -dataset_gen = dataset_agg.dataset_gen_from() - -for dataset_list in dataset_gen: - # Only one `Analysis` so take first and only dataset. - dataset = dataset_list[0] - - aplt.subplot_imaging_dataset(dataset=dataset) - -""" -We now use the aggregator to load a generator containing the fit of the maximum log likelihood model (and therefore -galaxies) to each dataset. - -Analogous to the `dataset_gen` above returning a list with one `Imaging` object, the `fit_gen` returns a list of -`FitImaging` objects, because only one `Analysis` was used to perform the model-fit. -""" -fit_agg = ag.agg.FitImagingAgg(aggregator=agg) -fit_gen = fit_agg.max_log_likelihood_gen_from() - -for fit_list in fit_gen: - # Only one `Analysis` so take first and only dataset. - fit = fit_list[0] - - aplt.subplot_fit_imaging(fit=fit) - -""" -__Modification__ - -The `FitImagingAgg` allow us to modify the fit settings. By default, it uses the `Settings` that were used -during the model-fit. - -However, we can change these settings such that the fit is performed differently. For example, what if I wanted to see -how the fit looks where the `Grid2D`'s `over_sample_size` is 4 (instead of the value of 2 that was used)? - -You can do this by passing the settings objects, which overwrite the ones used by the analysis. -""" -fit_agg = ag.agg.FitImagingAgg( - aggregator=agg, - settings=ag.Settings(use_border_relocator=False), -) -fit_gen = fit_agg.max_log_likelihood_gen_from() - -for fit_list in fit_gen: - # Only one `Analysis` so take first and only dataset. - fit = fit_list[0] - - aplt.subplot_fit_imaging(fit=fit) - -""" -__Visualization Customization__ - -The benefit of inspecting fits using the aggregator, rather than the files outputs to the hard-disk, is that we can -customize the plots using the PyAutoGalaxy `mat_plot`. - -We create a new function to apply as a generator to do this. However, we use a convenience method available -in the aggregator package to set up the fit. -""" -fit_agg = ag.agg.FitImagingAgg(aggregator=agg) -fit_gen = fit_agg.max_log_likelihood_gen_from() - -for fit_list in fit_gen: - # Only one `Analysis` so take first and only dataset. - fit = fit_list[0] - - aplt.plot_array(array=fit.normalized_residual_map, title="Normalized Residual Map") - -""" -Making this plot for a paper? You can output it to hard disk. -""" -fit_agg = ag.agg.FitImagingAgg(aggregator=agg) -fit_gen = fit_agg.max_log_likelihood_gen_from() - -for fit_list in fit_gen: # Only Max LH sample so fit_list contains 1 lists of fits. - # Only one `Analysis` so take first and only dataset. - fit = fit_list[0] - - aplt.plot_array( - array=fit.normalized_residual_map, - title="Normalized Residual Map", - output_path=Path("output") / "path" / "of" / "file", - output_filename="publication", - output_format="png", - ) - -""" -__Errors (Random draws from PDF)__ - -In the `examples/models.py` example we showed how `Galaxies` objects could be randomly drawn form the Probability -Distribution Function, in order to quantity things such as errors. - -The same approach can be used with `FitImaging` objects, to investigate how the properties of the fit vary within -the errors (e.g. showing how the model galaxy appearances changes for different fits). -""" -fit_agg = ag.agg.FitImagingAgg(aggregator=agg) -fit_gen = fit_agg.randomly_drawn_via_pdf_gen_from(total_samples=2) - - -for fit_list_gen in fit_gen: # 1 Dataset so just one fit - for ( - fit_list - ) in ( - fit_list_gen - ): # Iterate over each total_samples=2, each with one fits for 1 analysis. - # Only one `Analysis` so take first and only dataset. - fit = fit_list[0] - - aplt.subplot_fit_imaging(fit=fit) - -""" -Finished. -""" +""" +Results: Data Fitting +===================== + +In this tutorial, we use the aggregator to load models and data from a non-linear search and use them to perform +fits to the data. + +We show how to use these tools to inspect the maximum log likelihood model of a fit to the data, customize things +like its visualization and also inspect fits randomly drawm from the PDF. + +__Contents__ + +- **Aggregator:** Load results using the Aggregator. +- **Fits via Aggregator:** Load datasets and maximum log likelihood fits using ImagingAgg and FitImagingAgg. +- **Modification:** Modify fit settings (e.g. over-sampling) when loading fits from the aggregator. +- **Visualization Customization:** Customize plot appearance when inspecting fits via the aggregator. +- **Errors (Random draws from PDF):** Inspect how fits vary within the PDF errors. + +__Interferometer__ + +This script can easily be adapted to analyse the results of charge injection imaging model-fits. + +The only entries that needs changing are: + + - `ImagingAgg` -> `InterferometerAgg`. + - `FitImagingAgg` -> `FitInterferometerAgg`. + - `Imaging` -> `Interferometer`. + - `FitImaging` -> `FitInterferometer`. + +Quantities specific to an interfometer, for example its uv-wavelengths real space mask, are accessed using the same API +(e.g. `values("dataset.uv_wavelengths")` and `.values{"dataset.real_space_mask")). + +__Database File__ + +The aggregator can also load results from a `.sqlite` database file. + +This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) +because it is optimized for fast querying of results. + +See the package `results/database` for a full description of how to set up the database and the benefits it provides, +especially if loading results from hard-disk is slow. +""" + +# from autoconf import setup_notebook; setup_notebook() + +import os +from pathlib import Path + +from autoconf.test_mode import with_test_mode_segment + +import autofit as af +import autogalaxy as ag +import autogalaxy.plot as aplt + +""" +__Aggregator__ + +Set up the aggregator as shown in `start_here.py`. +""" +from autofit.aggregator.aggregator import Aggregator + +results_path = with_test_mode_segment(Path("output")) / "results_folder" +if not results_path.exists(): + import subprocess + import sys + + subprocess.run( + [sys.executable, "scripts/guides/results/_quick_fit.py"], + check=True, + ) + +agg = Aggregator.from_directory( + directory=Path("output") / "results_folder", +) + +""" +The masks we used to fit the galaxies is accessible via the aggregator. +""" +mask_gen = agg.values("dataset.mask") +print([mask for mask in mask_gen]) + +""" +The info dictionary we passed is also available. +""" +print("Info:") +info_gen = agg.values("info") +print([info for info in info_gen]) + +""" +__Fits via Aggregator__ + +Having performed a model-fit, we now want to interpret and visualize the results. In this example, we inspect +the `Imaging` objects that gave good fits to the data. + +Using the API shown in the `start_here.py` example this would require us to create a `Samples` object and manually +compose our own `Imaging` object. For large datasets, this would require us to use generators to ensure it is +memory-light, which are cumbersome to write. + +This example therefore uses the `ImagingAgg` object, which conveniently loads the `Imaging` objects of every fit via +generators for us. Explicit examples of how to do this via generators is given in the `advanced/manual_generator.py` +tutorial. + +We get a dataset generator via the `ag.agg.ImagingAgg` object, where this `dataset_gen` contains the maximum log +likelihood `Imaging `object of every model-fit. + +The `dataset_gen` returns a list of `Imaging` objects, as opposed to just a single `Imaging` object. This is because +only a single `Analysis` class was used in the model-fit, meaning there was only one `Imaging` dataset that was +fit. + +The `multi` package of the workspace illustrates model-fits which fit multiple datasets +simultaneously, (e.g. multi-wavelength imaging) by summing `Analysis` objects together, where the `dataset_list` +would contain multiple `Imaging` objects. +""" +dataset_agg = ag.agg.ImagingAgg(aggregator=agg) +dataset_gen = dataset_agg.dataset_gen_from() + +for dataset_list in dataset_gen: + # Only one `Analysis` so take first and only dataset. + dataset = dataset_list[0] + + aplt.subplot_imaging_dataset(dataset=dataset) + +""" +We now use the aggregator to load a generator containing the fit of the maximum log likelihood model (and therefore +galaxies) to each dataset. + +Analogous to the `dataset_gen` above returning a list with one `Imaging` object, the `fit_gen` returns a list of +`FitImaging` objects, because only one `Analysis` was used to perform the model-fit. +""" +fit_agg = ag.agg.FitImagingAgg(aggregator=agg) +fit_gen = fit_agg.max_log_likelihood_gen_from() + +for fit_list in fit_gen: + # Only one `Analysis` so take first and only dataset. + fit = fit_list[0] + + aplt.subplot_fit_imaging(fit=fit) + +""" +__Modification__ + +The `FitImagingAgg` allow us to modify the fit settings. By default, it uses the `Settings` that were used +during the model-fit. + +However, we can change these settings such that the fit is performed differently. For example, what if I wanted to see +how the fit looks where the `Grid2D`'s `over_sample_size` is 4 (instead of the value of 2 that was used)? + +You can do this by passing the settings objects, which overwrite the ones used by the analysis. +""" +fit_agg = ag.agg.FitImagingAgg( + aggregator=agg, + settings=ag.Settings(use_border_relocator=False), +) +fit_gen = fit_agg.max_log_likelihood_gen_from() + +for fit_list in fit_gen: + # Only one `Analysis` so take first and only dataset. + fit = fit_list[0] + + aplt.subplot_fit_imaging(fit=fit) + +""" +__Visualization Customization__ + +The benefit of inspecting fits using the aggregator, rather than the files outputs to the hard-disk, is that we can +customize the plots using the PyAutoGalaxy `mat_plot`. + +We create a new function to apply as a generator to do this. However, we use a convenience method available +in the aggregator package to set up the fit. +""" +fit_agg = ag.agg.FitImagingAgg(aggregator=agg) +fit_gen = fit_agg.max_log_likelihood_gen_from() + +for fit_list in fit_gen: + # Only one `Analysis` so take first and only dataset. + fit = fit_list[0] + + aplt.plot_array(array=fit.normalized_residual_map, title="Normalized Residual Map") + +""" +Making this plot for a paper? You can output it to hard disk. +""" +fit_agg = ag.agg.FitImagingAgg(aggregator=agg) +fit_gen = fit_agg.max_log_likelihood_gen_from() + +for fit_list in fit_gen: # Only Max LH sample so fit_list contains 1 lists of fits. + # Only one `Analysis` so take first and only dataset. + fit = fit_list[0] + + aplt.plot_array( + array=fit.normalized_residual_map, + title="Normalized Residual Map", + output_path=Path("output") / "path" / "of" / "file", + output_filename="publication", + output_format="png", + ) + +""" +__Errors (Random draws from PDF)__ + +In the `examples/models.py` example we showed how `Galaxies` objects could be randomly drawn form the Probability +Distribution Function, in order to quantity things such as errors. + +The same approach can be used with `FitImaging` objects, to investigate how the properties of the fit vary within +the errors (e.g. showing how the model galaxy appearances changes for different fits). +""" +fit_agg = ag.agg.FitImagingAgg(aggregator=agg) +fit_gen = fit_agg.randomly_drawn_via_pdf_gen_from(total_samples=2) + + +for fit_list_gen in fit_gen: # 1 Dataset so just one fit + for ( + fit_list + ) in ( + fit_list_gen + ): # Iterate over each total_samples=2, each with one fits for 1 analysis. + # Only one `Analysis` so take first and only dataset. + fit = fit_list[0] + + aplt.subplot_fit_imaging(fit=fit) + +""" +Finished. +""" diff --git a/scripts/guides/results/aggregator/galaxies_fit.py b/scripts/guides/results/aggregator/galaxies_fit.py index f72b0aed..17af5fa3 100644 --- a/scripts/guides/results/aggregator/galaxies_fit.py +++ b/scripts/guides/results/aggregator/galaxies_fit.py @@ -48,6 +48,8 @@ # from autoconf import setup_notebook; setup_notebook() from pathlib import Path + +from autoconf.test_mode import with_test_mode_segment import autofit as af import autogalaxy as ag import autogalaxy.plot as aplt @@ -60,7 +62,7 @@ in this folder) have results to work with. When that folder already exists the helper exits immediately, so re-running this tutorial is cheap. """ -results_path = Path("output") / "results_folder" +results_path = with_test_mode_segment(Path("output")) / "results_folder" if not results_path.exists(): import subprocess import sys diff --git a/scripts/guides/results/aggregator/models.py b/scripts/guides/results/aggregator/models.py index 46422291..8f6ac64b 100644 --- a/scripts/guides/results/aggregator/models.py +++ b/scripts/guides/results/aggregator/models.py @@ -1,238 +1,240 @@ -""" -Results: Models -=============== - -Suppose we have the results of many fits and we only wanted to load and inspect a specific set -of model-fits (e.g. the results of `start_here.py`). We can use querying tools to only load the results we are -interested in. - -This includes support for advanced querying, so that specific model-fits (e.g., which fit a certain model or dataset) -can be loaded. - -__Contents__ - -- **Aggregator:** Load results using the Aggregator. -- **Galaxies via Aggregator:** Load maximum log likelihood Galaxies objects using GalaxiesAgg. -- **Luminosity Example:** Compute the luminosity of each galaxy from aggregated results. -- **Errors (PDF from samples):** Compute errors on derived quantities like axis ratio from the full PDF. -- **Errors (Random draws from PDF):** Estimate errors using random draws from the PDF. - -__Database File__ - -The aggregator can also load results from a `.sqlite` database file. - -This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) -because it is optimized for fast querying of results. - -See the package `results/database` for a full description of how to set up the database and the benefits it provides, -especially if loading results from hard-disk is slow. -""" - -import os - -# from autoconf import setup_notebook; setup_notebook() - -from pathlib import Path - -import autofit as af -import autogalaxy as ag -import autogalaxy.plot as aplt - - -""" -__Aggregator__ - -Set up the aggregator as shown in `start_here.py`. -""" -from autofit.aggregator.aggregator import Aggregator - -results_path = Path("output") / "results_folder" -if not results_path.exists(): - import subprocess - import sys - - subprocess.run( - [sys.executable, "scripts/guides/results/_quick_fit.py"], - check=True, - ) - -agg = Aggregator.from_directory( - directory=Path("output") / "results_folder", -) - -""" -__Galaxies via Aggregator__ - -Having performed a model-fit, we now want to interpret and visualize the results. In this example, we want to inspect -the `Galaxies` objects that gave good fits to the data. - -Using the API shown in the `start_here.py` example this would require us to create a `Samples` object and manually -compose our own `Galaxies` object. For large datasets, this would require us to use generators to ensure it is memory-light, -which are cumbersome to write. - -This example therefore uses the `GalaxiesAgg` object, which conveniently loads the `Galaxies` objects of every fit via -generators for us. Explicit examples of how to do this via generators is given in the `advanced/manual_generator.py` -tutorial. - -We get a galaxies generator via the `ag.agg.GalaxiesAgg` object, where this `galaxies_gen` contains the maximum log -likelihood `Galaxies `object of every model-fit. -""" -galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) -galaxies_gen = galaxies_agg.max_log_likelihood_gen_from() - -""" -We can now iterate over our galaxies generator to make the plots we desire. - -The `galaxies_gen` returns a list of `Galaxies` objects, as opposed to just a single `Galaxies` object. This is because -only a single `Analysis` class was used in the model-fit, meaning there was only one imaging dataset that was -fit. - -The `multi` package of the workspace illustrates model-fits which fit multiple datasets -simultaneously, (e.g. multi-wavelength imaging) by summing `Analysis` objects together, where the `galaxies_list` -would contain multiple `Galaxies` objects. - -The parameters of galaxies in the `Galaxies` may vary across the datasets (e.g. different light profile intensities -for different wavelengths), which would be reflected in the galaxies list. -""" -grid = ag.Grid2D.uniform(shape_native=(100, 100), pixel_scales=0.1) - -dataset_agg = ag.agg.ImagingAgg(aggregator=agg) -dataset_gen = dataset_agg.dataset_gen_from() - -for dataset_list, galaxies_list in zip(dataset_gen, galaxies_gen): - # Only one `Analysis` so take first and only dataset. - dataset = dataset_list[0] - - # Only one `Analysis` so take first and only galaxies. - galaxies = galaxies_list[0] - - # Input to FitImaging to solve for linear light profile intensities, see `start_here.py` for details. - fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) - galaxies = fit.galaxies_linear_light_profiles_to_light_profiles - - aplt.subplot_galaxies(galaxies=galaxies, grid=grid) - -""" -__Luminosity Example__ - -Each galaxies has the information we need to compute the luminosity of that model. Therefore, lets print -the luminosity of each of our most-likely galaxies. - -The model instance uses the model defined by a pipeline. In this pipeline, we called the galaxy `galaxy`. -""" -dataset_agg = ag.agg.ImagingAgg(aggregator=agg) -dataset_gen = dataset_agg.dataset_gen_from() - -galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) -galaxies_gen = galaxies_agg.max_log_likelihood_gen_from() - -print("Maximum Log Likelihood Luminosities:") - -for dataset_list, galaxies_list in zip(dataset_gen, galaxies_gen): - # Only one `Analysis` so take first and only dataset. - dataset = dataset_list[0] - - # Only one `Analysis` so take first and only tracer. - galaxies = galaxies_list[0] - - # Input to FitImaging to solve for linear light profile intensities, see `start_here.py` for details. - fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) - galaxies = fit.galaxies_linear_light_profiles_to_light_profiles - - luminosity = galaxies[0].luminosity_within_circle_from(radius=10.0) - - print("Luminosity (electrons per second) = ", luminosity) - - -""" -__Errors (PDF from samples)__ - -In this example, we will compute the errors on the axis ratio of a model. Computing the errors on a quantity -like the trap `density` is simple, because it is sampled by the non-linear search. The errors are therefore accessible -via the `Samples`, by marginalizing over all over parameters via the 1D Probability Density Function (PDF). - -Computing the errors on the axis ratio is more tricky, because it is a derived quantity. It is a parameter or -measurement that we want to calculate but was not sampled directly by the non-linear search. The `GalaxiesAgg` object -object has everything we need to compute the errors of derived quantities. - -Below, we compute the axis ratio of every model sampled by the non-linear search and use this determine the PDF -of the axis ratio. When combining each axis ratio we weight each value by its `weight`. For Nautilus, -the nested sampler used by the fit, this ensures models which gave a bad fit (and thus have a low weight) do not -contribute significantly to the axis ratio error estimate. - -We set `minimum_weight=`1e-4`, such that any sample with a weight below this value is discarded when computing the -error. This speeds up the error computation by only using a small fraction of the total number of samples. Computing -a axis ratio is cheap, and this is probably not necessary. However, certain quantities have a non-negligible -computational overhead is being calculated and setting a minimum weight can speed up the calculation without -significantly changing the inferred errors. - -Below, we use the `GalaxiesAgg` to get the `Plane` of every Nautilus sample in each model-fit. We extract from each -galaxies the model's axis-ratio, store them in a list and find the value via the PDF and quantile method. This again -uses generators, ensuring minimal memory use. - -In order to use these samples in the function `quantile`, we also need the weight list of the sample weights. We -compute this using the `GalaxiesAgg`'s function `weights_above_gen_from`, which computes generators of the weights of all -points above this minimum value. This again ensures memory use in minimag. -""" -galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) -galaxies_list_gen = galaxies_agg.all_above_weight_gen_from(minimum_weight=1e-4) -weight_list_gen = galaxies_agg.weights_above_gen_from(minimum_weight=1e-4) - -for galaxies_gen, weight_gen in zip(galaxies_list_gen, weight_list_gen): - axis_ratio_list = [] - - for galaxies_list in galaxies_gen: - # Only one `Analysis` so take first and only tracer. - galaxies = galaxies_list[0] - - axis_ratio = ag.convert.axis_ratio_from(ell_comps=galaxies[0].bulge.ell_comps) - - axis_ratio_list.append(axis_ratio) - - weight_list = [weight for weight in weight_gen] - - try: - median_axis_ratio, lower_axis_ratio, upper_axis_ratio = af.marginalize( - parameter_list=axis_ratio_list, sigma=3.0, weight_list=weight_list - ) - - print( - f"Axis-Ratio = {median_axis_ratio} ({upper_axis_ratio} {lower_axis_ratio}" - ) - except IndexError: - pass - -""" -__Errors (Random draws from PDF)__ - -An alternative approach to estimating the errors on a derived quantity is to randomly draw samples from the PDF -of the non-linear search. For a sufficiently high number of random draws, this should be as accurate and precise -as the method above. However, it can be difficult to be certain how many random draws are necessary. - -The weights of each sample are used to make every random draw. Therefore, when we compute the axis-ratio and its errors -we no longer need to pass the `weight_list` to the `quantile` function. -""" -galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) -galaxies_list_gen = galaxies_agg.randomly_drawn_via_pdf_gen_from(total_samples=2) - -for galaxies_gen in galaxies_list_gen: - axis_ratio_list = [] - - for galaxies_list in galaxies_gen: - # Only one `Analysis` so take first and only tracer. - galaxies = galaxies_list[0] - - axis_ratio = ag.convert.axis_ratio_from(ell_comps=galaxies[0].bulge.ell_comps) - - axis_ratio_list.append(axis_ratio) - - median_axis_ratio, lower_axis_ratio, upper_axis_ratio = af.marginalize( - parameter_list=axis_ratio_list, sigma=3.0 - ) - - print(f"Axis-Ratio = {median_axis_ratio} ({upper_axis_ratio} {lower_axis_ratio}") - - -""" -Finish. -""" +""" +Results: Models +=============== + +Suppose we have the results of many fits and we only wanted to load and inspect a specific set +of model-fits (e.g. the results of `start_here.py`). We can use querying tools to only load the results we are +interested in. + +This includes support for advanced querying, so that specific model-fits (e.g., which fit a certain model or dataset) +can be loaded. + +__Contents__ + +- **Aggregator:** Load results using the Aggregator. +- **Galaxies via Aggregator:** Load maximum log likelihood Galaxies objects using GalaxiesAgg. +- **Luminosity Example:** Compute the luminosity of each galaxy from aggregated results. +- **Errors (PDF from samples):** Compute errors on derived quantities like axis ratio from the full PDF. +- **Errors (Random draws from PDF):** Estimate errors using random draws from the PDF. + +__Database File__ + +The aggregator can also load results from a `.sqlite` database file. + +This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) +because it is optimized for fast querying of results. + +See the package `results/database` for a full description of how to set up the database and the benefits it provides, +especially if loading results from hard-disk is slow. +""" + +import os + +# from autoconf import setup_notebook; setup_notebook() + +from pathlib import Path + +from autoconf.test_mode import with_test_mode_segment + +import autofit as af +import autogalaxy as ag +import autogalaxy.plot as aplt + + +""" +__Aggregator__ + +Set up the aggregator as shown in `start_here.py`. +""" +from autofit.aggregator.aggregator import Aggregator + +results_path = with_test_mode_segment(Path("output")) / "results_folder" +if not results_path.exists(): + import subprocess + import sys + + subprocess.run( + [sys.executable, "scripts/guides/results/_quick_fit.py"], + check=True, + ) + +agg = Aggregator.from_directory( + directory=Path("output") / "results_folder", +) + +""" +__Galaxies via Aggregator__ + +Having performed a model-fit, we now want to interpret and visualize the results. In this example, we want to inspect +the `Galaxies` objects that gave good fits to the data. + +Using the API shown in the `start_here.py` example this would require us to create a `Samples` object and manually +compose our own `Galaxies` object. For large datasets, this would require us to use generators to ensure it is memory-light, +which are cumbersome to write. + +This example therefore uses the `GalaxiesAgg` object, which conveniently loads the `Galaxies` objects of every fit via +generators for us. Explicit examples of how to do this via generators is given in the `advanced/manual_generator.py` +tutorial. + +We get a galaxies generator via the `ag.agg.GalaxiesAgg` object, where this `galaxies_gen` contains the maximum log +likelihood `Galaxies `object of every model-fit. +""" +galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) +galaxies_gen = galaxies_agg.max_log_likelihood_gen_from() + +""" +We can now iterate over our galaxies generator to make the plots we desire. + +The `galaxies_gen` returns a list of `Galaxies` objects, as opposed to just a single `Galaxies` object. This is because +only a single `Analysis` class was used in the model-fit, meaning there was only one imaging dataset that was +fit. + +The `multi` package of the workspace illustrates model-fits which fit multiple datasets +simultaneously, (e.g. multi-wavelength imaging) by summing `Analysis` objects together, where the `galaxies_list` +would contain multiple `Galaxies` objects. + +The parameters of galaxies in the `Galaxies` may vary across the datasets (e.g. different light profile intensities +for different wavelengths), which would be reflected in the galaxies list. +""" +grid = ag.Grid2D.uniform(shape_native=(100, 100), pixel_scales=0.1) + +dataset_agg = ag.agg.ImagingAgg(aggregator=agg) +dataset_gen = dataset_agg.dataset_gen_from() + +for dataset_list, galaxies_list in zip(dataset_gen, galaxies_gen): + # Only one `Analysis` so take first and only dataset. + dataset = dataset_list[0] + + # Only one `Analysis` so take first and only galaxies. + galaxies = galaxies_list[0] + + # Input to FitImaging to solve for linear light profile intensities, see `start_here.py` for details. + fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) + galaxies = fit.galaxies_linear_light_profiles_to_light_profiles + + aplt.subplot_galaxies(galaxies=galaxies, grid=grid) + +""" +__Luminosity Example__ + +Each galaxies has the information we need to compute the luminosity of that model. Therefore, lets print +the luminosity of each of our most-likely galaxies. + +The model instance uses the model defined by a pipeline. In this pipeline, we called the galaxy `galaxy`. +""" +dataset_agg = ag.agg.ImagingAgg(aggregator=agg) +dataset_gen = dataset_agg.dataset_gen_from() + +galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) +galaxies_gen = galaxies_agg.max_log_likelihood_gen_from() + +print("Maximum Log Likelihood Luminosities:") + +for dataset_list, galaxies_list in zip(dataset_gen, galaxies_gen): + # Only one `Analysis` so take first and only dataset. + dataset = dataset_list[0] + + # Only one `Analysis` so take first and only tracer. + galaxies = galaxies_list[0] + + # Input to FitImaging to solve for linear light profile intensities, see `start_here.py` for details. + fit = ag.FitImaging(dataset=dataset, galaxies=galaxies) + galaxies = fit.galaxies_linear_light_profiles_to_light_profiles + + luminosity = galaxies[0].luminosity_within_circle_from(radius=10.0) + + print("Luminosity (electrons per second) = ", luminosity) + + +""" +__Errors (PDF from samples)__ + +In this example, we will compute the errors on the axis ratio of a model. Computing the errors on a quantity +like the trap `density` is simple, because it is sampled by the non-linear search. The errors are therefore accessible +via the `Samples`, by marginalizing over all over parameters via the 1D Probability Density Function (PDF). + +Computing the errors on the axis ratio is more tricky, because it is a derived quantity. It is a parameter or +measurement that we want to calculate but was not sampled directly by the non-linear search. The `GalaxiesAgg` object +object has everything we need to compute the errors of derived quantities. + +Below, we compute the axis ratio of every model sampled by the non-linear search and use this determine the PDF +of the axis ratio. When combining each axis ratio we weight each value by its `weight`. For Nautilus, +the nested sampler used by the fit, this ensures models which gave a bad fit (and thus have a low weight) do not +contribute significantly to the axis ratio error estimate. + +We set `minimum_weight=`1e-4`, such that any sample with a weight below this value is discarded when computing the +error. This speeds up the error computation by only using a small fraction of the total number of samples. Computing +a axis ratio is cheap, and this is probably not necessary. However, certain quantities have a non-negligible +computational overhead is being calculated and setting a minimum weight can speed up the calculation without +significantly changing the inferred errors. + +Below, we use the `GalaxiesAgg` to get the `Plane` of every Nautilus sample in each model-fit. We extract from each +galaxies the model's axis-ratio, store them in a list and find the value via the PDF and quantile method. This again +uses generators, ensuring minimal memory use. + +In order to use these samples in the function `quantile`, we also need the weight list of the sample weights. We +compute this using the `GalaxiesAgg`'s function `weights_above_gen_from`, which computes generators of the weights of all +points above this minimum value. This again ensures memory use in minimag. +""" +galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) +galaxies_list_gen = galaxies_agg.all_above_weight_gen_from(minimum_weight=1e-4) +weight_list_gen = galaxies_agg.weights_above_gen_from(minimum_weight=1e-4) + +for galaxies_gen, weight_gen in zip(galaxies_list_gen, weight_list_gen): + axis_ratio_list = [] + + for galaxies_list in galaxies_gen: + # Only one `Analysis` so take first and only tracer. + galaxies = galaxies_list[0] + + axis_ratio = ag.convert.axis_ratio_from(ell_comps=galaxies[0].bulge.ell_comps) + + axis_ratio_list.append(axis_ratio) + + weight_list = [weight for weight in weight_gen] + + try: + median_axis_ratio, lower_axis_ratio, upper_axis_ratio = af.marginalize( + parameter_list=axis_ratio_list, sigma=3.0, weight_list=weight_list + ) + + print( + f"Axis-Ratio = {median_axis_ratio} ({upper_axis_ratio} {lower_axis_ratio}" + ) + except IndexError: + pass + +""" +__Errors (Random draws from PDF)__ + +An alternative approach to estimating the errors on a derived quantity is to randomly draw samples from the PDF +of the non-linear search. For a sufficiently high number of random draws, this should be as accurate and precise +as the method above. However, it can be difficult to be certain how many random draws are necessary. + +The weights of each sample are used to make every random draw. Therefore, when we compute the axis-ratio and its errors +we no longer need to pass the `weight_list` to the `quantile` function. +""" +galaxies_agg = ag.agg.GalaxiesAgg(aggregator=agg) +galaxies_list_gen = galaxies_agg.randomly_drawn_via_pdf_gen_from(total_samples=2) + +for galaxies_gen in galaxies_list_gen: + axis_ratio_list = [] + + for galaxies_list in galaxies_gen: + # Only one `Analysis` so take first and only tracer. + galaxies = galaxies_list[0] + + axis_ratio = ag.convert.axis_ratio_from(ell_comps=galaxies[0].bulge.ell_comps) + + axis_ratio_list.append(axis_ratio) + + median_axis_ratio, lower_axis_ratio, upper_axis_ratio = af.marginalize( + parameter_list=axis_ratio_list, sigma=3.0 + ) + + print(f"Axis-Ratio = {median_axis_ratio} ({upper_axis_ratio} {lower_axis_ratio}") + + +""" +Finish. +""" diff --git a/scripts/guides/results/aggregator/queries.py b/scripts/guides/results/aggregator/queries.py index 761df449..cc0fd0bb 100644 --- a/scripts/guides/results/aggregator/queries.py +++ b/scripts/guides/results/aggregator/queries.py @@ -1,162 +1,164 @@ -""" -Database: Queries -================= - -Suppose we have the results of many fits and we only wanted to load and inspect a specific set -of model-fits (e.g. the results of `start_here.py`). We can use querying tools to only load the results we are -interested in. - -This includes support for advanced querying, so that specific model-fits (e.g., which fit a certain model or dataset) -can be loaded. - -__Contents__ - -- **Loading From Hard-disk:** Load results using the Aggregator from the output directory. -- **Unique Tag:** Query results by the unique_tag string used in the search. -- **Search Name:** Query results by the name of the non-linear search. -- **Model Queries:** Query results based on the model components fitted. -- **Logic:** Combine queries using AND (&) and OR (|) logical operators. - -__Database File__ - -The aggregator can also load results from a `.sqlite` database file. - -This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) -because it is optimized for fast querying of results. - -See the package `results/database` for a full description of how to set up the database and the benefits it provides, -especially if loading results from hard-disk is slow. -""" - -# from autoconf import setup_notebook; setup_notebook() - -from pathlib import Path -import autofit as af -import autogalaxy as ag - -""" -__Loading From Hard-disk__ - -Results can be loaded from hard disk using the `Aggregator` object (see the `start_here.py` script for a description of -what the `Aggregator` does if you have not seen it!). -""" -from autofit.aggregator.aggregator import Aggregator - -results_path = Path("output") / "results_folder" -if not results_path.exists(): - import subprocess - import sys - - subprocess.run( - [sys.executable, "scripts/guides/results/_quick_fit.py"], - check=True, - ) - -agg = Aggregator.from_directory( - directory=Path("output") / "results_folder", -) - -""" -__Database File__ - -The aggregator can also load results from a `.sqlite` database file. - -This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) -because it is optimized for fast querying of results. - -See the package `results/database` for a full description of how to set up the database and the benefits it provides, -especially if loading results from hard-disk is slow. - -__Unique Tag__ - -We can use the `Aggregator` to query the results and return only specific fits that we are interested in. We first -do this using the `unique_tag` which we can query to load the results of a specific `dataset_name` string we -input into the model-fit's search. - -By querying using the string `simple__1` the model-fit to only the second dataset is returned: -""" -unique_tag = agg.search.unique_tag -agg_query = agg.query(unique_tag == "simple") -samples_gen = agg_query.values("samples") - -""" -As expected, this list now has only 1 `SamplesNest` corresponding to the second dataset. -""" -print("Directory Filtered DynestySampler Samples: \n") -print("Total Samples Objects via unique tag = ", len(list(samples_gen)), "\n\n") - -""" -If we query using an incorrect dataset name we get no results: -""" -unique_tag = agg.search.unique_tag -agg_query = agg.query(unique_tag == "incorrect_name") -samples_gen = agg_query.values("samples") - -""" -__Search Name__ - -We can also use the `name` of the search used to fit to the model as a query. - -In this example, all three fits used the same search, which had the `name` `results`. Thus, using it as a -query in this example is somewhat pointless. However, querying based on the search name is very useful for model-fits -which use search chaining (see chapter 3 **HowToGalaxy**), where the results of a particular fit in the chain can be -instantly loaded. - -As expected, this query contains all 3 results. -""" -name = agg.search.name -agg_query = agg.query(name == "results") -print("Total Queried Results via search name = ", len(agg_query), "\n\n") - -""" -__Model Queries__ - -We can also query based on the model fitted. - -For example, we can load all results which fitted an `Sersic` model-component, which in this simple example is -all 3 model-fits. - -The ability to query via the model is extremely powerful. It enables a user to fit many models to large samples -of galaxies efficiently load and inspect the results. - -[Note: the code `agg.model.galaxies.galaxy.bulge` corresponds to the fact that in the `Model` we named the model -components `galaxies`, `galaxy` and `bulge`. If the `Model` had used a different name the code below would change -correspondingly. Models with multiple galaxies are therefore easily accessed.] -""" -galaxy = agg.model.galaxies.galaxy -agg_query = agg.query(galaxy.bulge == ag.lp_linear.Sersic) -print("Total Samples Objects via `Sersic` model query = ", len(agg_query), "\n") - -""" -Queries using the results of model-fitting are also supported. Below, we query to find all fits where the -inferred value of `sersic_index` for the `Sersic` of the source's bulge is less than 3.0 (which returns only -the first of the three model-fits). -""" -bulge = agg.model.galaxies.galaxy.bulge -agg_query = agg.query(bulge.sersic_index < 3.0) -print( - "Total Samples Objects In Query `galaxy.bulge.sersic_index < 3.0` = ", - len(agg_query), - "\n", -) - -""" -__Logic__ - -Advanced queries can be constructed using logic, for example we below we combine the two queries above to find all -results which fitted an `Sersic` bulge model AND (using the & symbol) inferred a value of effective radius of -greater than 3.0 for the bulge. - -The OR logical clause is also supported via the symbol |. -""" -bulge = agg.model.galaxies.galaxy.bulge -agg_query = agg.query((bulge == ag.lp_linear.Sersic) & (bulge.effective_radius > 3.0)) -print( - "Total Samples Objects In Query `Sersic and effective_radius > 3.0` = ", - len(agg_query), - "\n", -) - -""" -Finished. -""" +""" +Database: Queries +================= + +Suppose we have the results of many fits and we only wanted to load and inspect a specific set +of model-fits (e.g. the results of `start_here.py`). We can use querying tools to only load the results we are +interested in. + +This includes support for advanced querying, so that specific model-fits (e.g., which fit a certain model or dataset) +can be loaded. + +__Contents__ + +- **Loading From Hard-disk:** Load results using the Aggregator from the output directory. +- **Unique Tag:** Query results by the unique_tag string used in the search. +- **Search Name:** Query results by the name of the non-linear search. +- **Model Queries:** Query results based on the model components fitted. +- **Logic:** Combine queries using AND (&) and OR (|) logical operators. + +__Database File__ + +The aggregator can also load results from a `.sqlite` database file. + +This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) +because it is optimized for fast querying of results. + +See the package `results/database` for a full description of how to set up the database and the benefits it provides, +especially if loading results from hard-disk is slow. +""" + +# from autoconf import setup_notebook; setup_notebook() + +from pathlib import Path + +from autoconf.test_mode import with_test_mode_segment +import autofit as af +import autogalaxy as ag + +""" +__Loading From Hard-disk__ + +Results can be loaded from hard disk using the `Aggregator` object (see the `start_here.py` script for a description of +what the `Aggregator` does if you have not seen it!). +""" +from autofit.aggregator.aggregator import Aggregator + +results_path = with_test_mode_segment(Path("output")) / "results_folder" +if not results_path.exists(): + import subprocess + import sys + + subprocess.run( + [sys.executable, "scripts/guides/results/_quick_fit.py"], + check=True, + ) + +agg = Aggregator.from_directory( + directory=Path("output") / "results_folder", +) + +""" +__Database File__ + +The aggregator can also load results from a `.sqlite` database file. + +This is beneficial when loading results for large numbers of model-fits (e.g. more than hundreds) +because it is optimized for fast querying of results. + +See the package `results/database` for a full description of how to set up the database and the benefits it provides, +especially if loading results from hard-disk is slow. + +__Unique Tag__ + +We can use the `Aggregator` to query the results and return only specific fits that we are interested in. We first +do this using the `unique_tag` which we can query to load the results of a specific `dataset_name` string we +input into the model-fit's search. + +By querying using the string `simple__1` the model-fit to only the second dataset is returned: +""" +unique_tag = agg.search.unique_tag +agg_query = agg.query(unique_tag == "simple") +samples_gen = agg_query.values("samples") + +""" +As expected, this list now has only 1 `SamplesNest` corresponding to the second dataset. +""" +print("Directory Filtered DynestySampler Samples: \n") +print("Total Samples Objects via unique tag = ", len(list(samples_gen)), "\n\n") + +""" +If we query using an incorrect dataset name we get no results: +""" +unique_tag = agg.search.unique_tag +agg_query = agg.query(unique_tag == "incorrect_name") +samples_gen = agg_query.values("samples") + +""" +__Search Name__ + +We can also use the `name` of the search used to fit to the model as a query. + +In this example, all three fits used the same search, which had the `name` `results`. Thus, using it as a +query in this example is somewhat pointless. However, querying based on the search name is very useful for model-fits +which use search chaining (see chapter 3 **HowToGalaxy**), where the results of a particular fit in the chain can be +instantly loaded. + +As expected, this query contains all 3 results. +""" +name = agg.search.name +agg_query = agg.query(name == "results") +print("Total Queried Results via search name = ", len(agg_query), "\n\n") + +""" +__Model Queries__ + +We can also query based on the model fitted. + +For example, we can load all results which fitted an `Sersic` model-component, which in this simple example is +all 3 model-fits. + +The ability to query via the model is extremely powerful. It enables a user to fit many models to large samples +of galaxies efficiently load and inspect the results. + +[Note: the code `agg.model.galaxies.galaxy.bulge` corresponds to the fact that in the `Model` we named the model +components `galaxies`, `galaxy` and `bulge`. If the `Model` had used a different name the code below would change +correspondingly. Models with multiple galaxies are therefore easily accessed.] +""" +galaxy = agg.model.galaxies.galaxy +agg_query = agg.query(galaxy.bulge == ag.lp_linear.Sersic) +print("Total Samples Objects via `Sersic` model query = ", len(agg_query), "\n") + +""" +Queries using the results of model-fitting are also supported. Below, we query to find all fits where the +inferred value of `sersic_index` for the `Sersic` of the source's bulge is less than 3.0 (which returns only +the first of the three model-fits). +""" +bulge = agg.model.galaxies.galaxy.bulge +agg_query = agg.query(bulge.sersic_index < 3.0) +print( + "Total Samples Objects In Query `galaxy.bulge.sersic_index < 3.0` = ", + len(agg_query), + "\n", +) + +""" +__Logic__ + +Advanced queries can be constructed using logic, for example we below we combine the two queries above to find all +results which fitted an `Sersic` bulge model AND (using the & symbol) inferred a value of effective radius of +greater than 3.0 for the bulge. + +The OR logical clause is also supported via the symbol |. +""" +bulge = agg.model.galaxies.galaxy.bulge +agg_query = agg.query((bulge == ag.lp_linear.Sersic) & (bulge.effective_radius > 3.0)) +print( + "Total Samples Objects In Query `Sersic and effective_radius > 3.0` = ", + len(agg_query), + "\n", +) + +""" +Finished. +""" diff --git a/scripts/guides/results/aggregator/samples.py b/scripts/guides/results/aggregator/samples.py index 3320f2a2..56a14259 100644 --- a/scripts/guides/results/aggregator/samples.py +++ b/scripts/guides/results/aggregator/samples.py @@ -44,6 +44,8 @@ # from autoconf import setup_notebook; setup_notebook() from pathlib import Path + +from autoconf.test_mode import with_test_mode_segment import autofit as af import autogalaxy as ag import autogalaxy.plot as aplt @@ -56,7 +58,7 @@ in this folder) have results to work with. When that folder already exists the helper exits immediately, so re-running this tutorial is cheap. """ -results_path = Path("output") / "results_folder" +results_path = with_test_mode_segment(Path("output")) / "results_folder" if not results_path.exists(): import subprocess import sys diff --git a/scripts/guides/results/aggregator/samples_via_aggregator.py b/scripts/guides/results/aggregator/samples_via_aggregator.py index 04b2efb6..06bd7e7f 100644 --- a/scripts/guides/results/aggregator/samples_via_aggregator.py +++ b/scripts/guides/results/aggregator/samples_via_aggregator.py @@ -47,6 +47,8 @@ # from autoconf import setup_notebook; setup_notebook() from pathlib import Path + +from autoconf.test_mode import with_test_mode_segment import autofit as af import autogalaxy.plot as aplt @@ -82,7 +84,7 @@ """ from autofit.aggregator.aggregator import Aggregator -results_path = Path("output") / "results_folder" +results_path = with_test_mode_segment(Path("output")) / "results_folder" if not results_path.exists(): import subprocess import sys