diff --git a/scripts/group/features/scaling_relation/modeling.py b/scripts/group/features/scaling_relation/modeling.py index d12f61c5a..52664d267 100644 --- a/scripts/group/features/scaling_relation/modeling.py +++ b/scripts/group/features/scaling_relation/modeling.py @@ -122,34 +122,41 @@ """ __Centres__ -Three centre JSON files — one per tier. The same `al.from_json` pattern is used by `group/modeling.py` for -`main_lens_centres.json` and `extra_galaxies_centres.json`; we just add a third file for the scaling tier. +Centres for the main and extra tiers come from JSON files (one (y, x) tuple per galaxy each). The scaling tier loads +its centres AND luminosities from a single CSV via `al.galaxy_table_from_csv` — see the next section. """ main_lens_centres = al.from_json(file_path=dataset_path / "main_lens_centres.json") extra_galaxies_centres = al.from_json( file_path=dataset_path / "extra_galaxies_centres.json" ) -scaling_galaxies_centres = al.from_json( - file_path=dataset_path / "scaling_galaxies_centres.json" -) print(f"Main lens centres: {main_lens_centres}") print(f"Extra galaxies centres: {extra_galaxies_centres}") -print(f"Scaling galaxies centres: {scaling_galaxies_centres}") """ -__Scaling Galaxy Luminosities__ +__Scaling Galaxy Centres + Luminosities__ + +The scaling relation needs both centres AND a measured luminosity per scaling galaxy. There are two equally-supported +ways to provide them in PyAutoLens — both shown below so you can pick whichever fits your workflow. + +**Option A — CSV via `al.galaxy_table_from_csv` (recommended for non-trivial galaxy counts).** The simulator writes a +`scaling_galaxies.csv` with columns `y, x, luminosity` (and optional `redshift`) alongside the centre JSONs. We load it +in one call which returns a typed `GalaxyTable` with `.centres` (a `Grid2DIrregular`), `.luminosities`, and (optionally) +`.redshifts`. This scales naturally to populations of tens or hundreds of galaxies — the source of truth lives in a +single editable file. -The scaling relation needs a measured luminosity per scaling galaxy. In a production fit these would come from a -**prior light-only fit** rather than being hardcoded; this tutorial uses hardcoded numbers to keep the script a single -self-contained search. +**Option B — JSON centres + hardcoded luminosity list (the original API, fine for short, fixed-length tutorials).** +Load the centres from `scaling_galaxies_centres.json` with `al.from_json` (the same loader used for the main and +extras tiers above) and define the luminosities as a Python list. Concise and obvious for small populations; awkward +once you have more than a handful. -There are two production patterns for obtaining the luminosities: +In a real analysis the luminosities come from a prior light-only fit. Two production patterns for obtaining them: - **Standalone light-only fit.** Run a single-stage non-linear search whose model is just MGE bulges for every galaxy (no mass, no source). After the fit, compute total luminosity per galaxy from the bulge gaussian parameters: - `total_luminosity = sum(2 * pi * sigma**2 / axis_ratio * intensity) / pixel_scale**2`. See the worked example at - `scripts/group/features/scaling_relation/modeling_for_luminosities.py`. + `total_luminosity = sum(2 * pi * sigma**2 / axis_ratio * intensity) / pixel_scale**2`. The standalone example at + `scripts/group/features/scaling_relation/modeling_for_luminosities.py` writes its result as a `scaling_galaxies.csv` + in the dataset folder, which can then be loaded directly via Option A here. - **As the `source_lp[0]` stage of a SLaM pipeline.** Every group SLaM script defines a `source_lp_0` function whose sole purpose is to fit a light-only MGE model to the main lens, extra galaxies and scaling galaxies in one go. @@ -163,13 +170,27 @@ docstring. The luminosity computation lives in the `source_lp_1` function that consumes the `source_lp_result_0` result. -The order of the list below must match `scaling_galaxies_centres`. +We use Option A by default below. The Option B equivalent is shown commented out — uncomment it (and comment out +Option A) to switch. """ -scaling_galaxies_luminosity_list = [0.45, 0.45] - -assert len(scaling_galaxies_luminosity_list) == len(list(scaling_galaxies_centres)), ( - "Number of scaling-galaxy luminosities must match the number of scaling-galaxy centres." +# Option A: CSV (recommended) +scaling_galaxies_table = al.galaxy_table_from_csv( + file_path=dataset_path / "scaling_galaxies.csv" ) +scaling_galaxies_centres = scaling_galaxies_table.centres +scaling_galaxies_luminosity_list = scaling_galaxies_table.luminosities + +# Option B: JSON centres + hardcoded luminosities (uncomment to use instead) +# scaling_galaxies_centres = al.from_json( +# file_path=dataset_path / "scaling_galaxies_centres.json" +# ) +# scaling_galaxies_luminosity_list = [0.45, 0.45] +# assert len(scaling_galaxies_luminosity_list) == len(list(scaling_galaxies_centres)), ( +# "Number of scaling-galaxy luminosities must match the number of scaling-galaxy centres." +# ) + +print(f"Scaling galaxies centres: {scaling_galaxies_centres}") +print(f"Scaling galaxies luminosities: {scaling_galaxies_luminosity_list}") """ __Main Lens Galaxies__ diff --git a/scripts/group/features/scaling_relation/modeling_for_luminosities.py b/scripts/group/features/scaling_relation/modeling_for_luminosities.py index 551f5124e..d44cc1150 100644 --- a/scripts/group/features/scaling_relation/modeling_for_luminosities.py +++ b/scripts/group/features/scaling_relation/modeling_for_luminosities.py @@ -57,7 +57,6 @@ # from autoconf import setup_notebook; setup_notebook() import numpy as np -import json from pathlib import Path import autofit as af import autolens as al @@ -269,30 +268,34 @@ def total_luminosity_from(galaxy): """ __Output__ -Write the luminosities to JSON next to the centre files so the scaling-relation modeling script can load them directly -in a follow-up run. +Write the centres + luminosities to a `scaling_galaxies.csv` next to the centre JSONs. This is the same file the +scaling-relation modeling script consumes via `al.galaxy_table_from_csv`, so the result of this fit can be chained +into the next step with no manual copy/paste. """ -luminosities_path = dataset_path / "scaling_galaxies_luminosities.json" -with open(luminosities_path, "w") as f: - json.dump([float(l) for l in scaling_luminosities], f, indent=2) +csv_path = dataset_path / "scaling_galaxies.csv" -print(f"Wrote scaling-galaxy luminosities to: {luminosities_path}") +al.galaxy_table_to_csv( + centres=list(scaling_galaxies_centres), + luminosities=[float(l) for l in scaling_luminosities], + file_path=csv_path, +) + +print(f"Wrote scaling-galaxy centres + luminosities to: {csv_path}") """ __Wrap Up__ -The numbers printed above can be pasted into the `scaling_galaxies_luminosity_list` of `modeling.py` (or loaded from -`scaling_galaxies_luminosities.json`). - -Two ways to chain into the next step: +The CSV at `dataset_path / "scaling_galaxies.csv"` is the canonical chain-point. The downstream modeling script +loads it directly: - - **Manual:** copy the printed list into `scripts/group/features/scaling_relation/modeling.py`'s - `scaling_galaxies_luminosity_list = [...]`. + table = al.galaxy_table_from_csv(file_path=dataset_path / "scaling_galaxies.csv") + scaling_galaxies_centres = table.centres + scaling_galaxies_luminosity_list = table.luminosities - - **Programmatic:** load `scaling_galaxies_luminosities.json` at the top of `modeling.py` via - `json.load(open(dataset_path / "scaling_galaxies_luminosities.json"))`. This pattern matches what the SLaM pipelines - do when they chain `source_lp[0]` -> `source_lp[1]`. +Re-running this script overwrites the CSV in place, so iterating on the light fit and re-running the lens fit is +just two `python ...` invocations. -For the chained-pipeline alternative, see `scripts/group/slam.py` (section `__SOURCE LP PIPELINE — stage 0__`) and +For the chained-pipeline alternative — where the light fit is the `source_lp[0]` stage of a SLaM run instead of a +standalone search — see `scripts/group/slam.py` (`__SOURCE LP PIPELINE — stage 0__`) and `scripts/group/features/pixelization/slam.py`. """ diff --git a/scripts/group/features/scaling_relation/simulator.py b/scripts/group/features/scaling_relation/simulator.py index b191418e3..be63c6fdb 100644 --- a/scripts/group/features/scaling_relation/simulator.py +++ b/scripts/group/features/scaling_relation/simulator.py @@ -230,6 +230,31 @@ file_path=Path(dataset_path, "scaling_galaxies_centres.json"), ) +""" +__Galaxy Population CSVs__ + +The modeling script loads luminosities (and centres) for both the extras and the scaling tier from CSVs written here. +The simulator knows the truth values of the per-galaxy luminosities so we write them out alongside the centre JSONs. + +The CSV schema is `y, x, luminosity, redshift?` — see `al.galaxy_table_from_csv` / +`al.galaxy_table_to_csv` (`autogalaxy/galaxy/galaxy_table.py`). Centre JSONs above are kept for backward compatibility; +new consumers should prefer the CSVs. +""" +extra_galaxies_luminosities = [0.9, 0.9] +scaling_galaxies_luminosities = [0.45, 0.45] + +al.galaxy_table_to_csv( + centres=extra_galaxies_centres, + luminosities=extra_galaxies_luminosities, + file_path=Path(dataset_path, "extra_galaxies.csv"), +) + +al.galaxy_table_to_csv( + centres=scaling_galaxies_centres, + luminosities=scaling_galaxies_luminosities, + file_path=Path(dataset_path, "scaling_galaxies.csv"), +) + """ __Positions__ diff --git a/scripts/imaging/features/scaling_relation/modeling.py b/scripts/imaging/features/scaling_relation/modeling.py index d12d92314..397ecbde2 100644 --- a/scripts/imaging/features/scaling_relation/modeling.py +++ b/scripts/imaging/features/scaling_relation/modeling.py @@ -77,8 +77,10 @@ from that result to compute luminosities and bound / scale the mass models. See `scripts/group/slam.py`, `scripts/group/features/pixelization/slam.py`, and the other group `slam.py` variants for production examples. -This tutorial uses **hardcoded luminosity lists** for readability — they would be the *output* of one of the patterns -above in production code. +This tutorial loads the luminosities from a `scaling_galaxies.csv` written by the simulator (see +`al.galaxy_table_from_csv` further down). In a real analysis the same CSV would be the *output* of one of the patterns +above — `modeling_for_luminosities.py` already writes its result in this format, and the SLAM `source_lp[0]` stage can +similarly emit one. __Redshifts__ @@ -160,31 +162,57 @@ """ __Centres__ -Two JSON files. The naming convention used by the dataset (extras vs scaling) carries through here purely as a labelling -convenience — both populations end up in the same `extra_galaxies` collection in the model. +The individually-modelled tier loads its centres from a JSON file (a list of (y, x) tuples) — the centres are the only +input the modeling script needs for that tier. """ individual_extras_centres = al.from_json( file_path=dataset_path / "extra_galaxies_centres.json" ) -relational_extras_centres = al.from_json( - file_path=dataset_path / "scaling_galaxies_centres.json" -) print(f"Individually-modelled extras: {individual_extras_centres}") -print(f"Scaling-relation extras: {relational_extras_centres}") """ -__Luminosities__ +__Centres + Luminosities (scaling-relation tier)__ -The scaling-relation tier needs a measured luminosity per galaxy. As discussed in the header, in production these come -from a prior light-only fit (see `modeling_for_luminosities.py` or the SLAM `source_lp[0]` stage). The order of this list -must match `relational_extras_centres`. -""" -relational_extras_luminosity_list = [0.45, 0.45] +The scaling-relation tier needs both centres AND a measured luminosity per galaxy. There are two equally-supported +ways to provide them in PyAutoLens — both shown below so you can pick whichever fits your workflow. + +**Option A — CSV via `al.galaxy_table_from_csv` (recommended for non-trivial galaxy counts).** The simulator writes a +`scaling_galaxies.csv` with columns `y, x, luminosity` (and optional `redshift`) alongside the centre JSONs. We load it +in one call which returns a typed `GalaxyTable` with `.centres` (a `Grid2DIrregular`), `.luminosities`, and (optionally) +`.redshifts`. This scales naturally to populations of tens or hundreds of galaxies — the source of truth lives in a +single editable file. + +In a real analysis, a prior light-only fit produces this CSV — see +`scripts/group/features/scaling_relation/modeling_for_luminosities.py` for the standalone version of that fit, or the +SLAM `source_lp[0]` stage in `scripts/group/slam.py` for the chained-pipeline equivalent. -assert len(relational_extras_luminosity_list) == len(list(relational_extras_centres)), ( - "Number of luminosities must match number of scaling-relation extra galaxy centres." +**Option B — JSON centres + hardcoded luminosity list (the original API, fine for short, fixed-length tutorials).** +Load the centres from `scaling_galaxies_centres.json` with `al.from_json` (the same loader used for the +individually-modelled tier above) and define the luminosities as a Python list. Concise and obvious for small +populations; awkward once you have more than a handful. + +We use Option A by default below. The Option B equivalent is shown commented out — uncomment it (and comment out +Option A) to switch. +""" +# Option A: CSV (recommended) +relational_extras_table = al.galaxy_table_from_csv( + file_path=dataset_path / "scaling_galaxies.csv" ) +relational_extras_centres = relational_extras_table.centres +relational_extras_luminosity_list = relational_extras_table.luminosities + +# Option B: JSON centres + hardcoded luminosities (uncomment to use instead) +# relational_extras_centres = al.from_json( +# file_path=dataset_path / "scaling_galaxies_centres.json" +# ) +# relational_extras_luminosity_list = [0.45, 0.45] +# assert len(relational_extras_luminosity_list) == len(list(relational_extras_centres)), ( +# "Number of luminosities must match number of scaling-relation extra galaxy centres." +# ) + +print(f"Scaling-relation extras: {relational_extras_centres}") +print(f"Scaling-relation luminosities: {relational_extras_luminosity_list}") """ __Lens__ diff --git a/scripts/imaging/features/scaling_relation/simulator.py b/scripts/imaging/features/scaling_relation/simulator.py index 9fb85e996..29930a3fc 100644 --- a/scripts/imaging/features/scaling_relation/simulator.py +++ b/scripts/imaging/features/scaling_relation/simulator.py @@ -214,6 +214,32 @@ file_path=Path(dataset_path, "scaling_galaxies_centres.json"), ) +""" +__Galaxy Population CSVs__ + +The modeling script loads luminosities (and centres) for the scaling-relation tier from a CSV +written here. The simulator knows the truth values of the per-galaxy luminosities so we write +them out alongside the centre JSONs. + +The CSV schema is `y, x, luminosity, redshift?` -- see `al.galaxy_table_from_csv` / +`al.galaxy_table_to_csv` (`autogalaxy/galaxy/galaxy_table.py`). Centre JSONs above are kept for +backward compatibility; new consumers should prefer the CSV. +""" +extra_galaxies_luminosities = [0.9, 0.8] +scaling_galaxies_luminosities = [0.45, 0.45] + +al.galaxy_table_to_csv( + centres=extra_galaxies_centres, + luminosities=extra_galaxies_luminosities, + file_path=Path(dataset_path, "extra_galaxies.csv"), +) + +al.galaxy_table_to_csv( + centres=scaling_galaxies_centres, + luminosities=scaling_galaxies_luminosities, + file_path=Path(dataset_path, "scaling_galaxies.csv"), +) + """ Finished. """