diff --git a/docs/sacc_usage.rst b/docs/sacc_usage.rst index 21c1e03c7..1227d18df 100644 --- a/docs/sacc_usage.rst +++ b/docs/sacc_usage.rst @@ -704,7 +704,7 @@ For more information about SACC: For Firecrown-specific questions: - :py:func:`firecrown.metadata_functions.extract_all_measured_types` -- :py:func:`firecrown.metadata_functions.extract_all_tracers_inferred_galaxy_zdists` +- :py:func:`firecrown.metadata_functions.extract_all_tracers_tomographic_bins` - :py:class:`firecrown.app.sacc.View` - CLI view command - :py:class:`firecrown.app.sacc.Transform` - CLI transform command - Firecrown GitHub Issues and Discussions diff --git a/firecrown/app/analysis/_analysis_builder.py b/firecrown/app/analysis/_analysis_builder.py index e967def57..00d028dba 100644 --- a/firecrown/app/analysis/_analysis_builder.py +++ b/firecrown/app/analysis/_analysis_builder.py @@ -135,6 +135,7 @@ def add_row(label, value): self.use_absolute_path, self.cosmology_analysis_spec(), self.required_cosmology(), + self.required_distance_max_z(), ) self._proceed_generation(generator) @@ -244,6 +245,18 @@ def required_cosmology(self) -> FrameworkCosmology: :return: True if the analysis requires a cosmology, False otherwise """ + def required_distance_max_z(self) -> float: + """Return the maximum redshift the background/distance splines must reach. + + Frameworks compute background quantities (comoving distance, H(z)) only + out to this redshift. Analyses using a tracer that needs values beyond + the default (e.g. CMB lensing, with its source at z~1100) must override + this method to return a large enough value. + + :return: Maximum redshift for background/distance computations + """ + return 4.0 + def cosmology_analysis_spec(self) -> CCLCosmologySpec: """Return the cosmology analysis specification. diff --git a/firecrown/app/analysis/_cobaya.py b/firecrown/app/analysis/_cobaya.py index 17686141f..3bf44ef73 100644 --- a/firecrown/app/analysis/_cobaya.py +++ b/firecrown/app/analysis/_cobaya.py @@ -55,6 +55,7 @@ def create_config( cosmo_spec: CCLCosmologySpec, use_absolute_path: bool = False, required_cosmology: FrameworkCosmology = FrameworkCosmology.NONLINEAR, + distance_max_z: float = 4.0, ) -> dict[str, Any]: """Create Cobaya configuration dictionary. @@ -68,6 +69,7 @@ def create_config( :param cosmo_spec: Cosmology specification :param use_absolute_path: Use absolute paths in configuration :param required_cosmology: Level of cosmology computation + :param distance_max_z: Maximum redshift for background/distance splines :return: Configuration dictionary ready for YAML serialization """ factory_source_str = get_path_str(factory_source, use_absolute_path) @@ -101,6 +103,7 @@ def create_config( if use_cosmology: config["likelihood"][likelihood_name]["input_style"] = "CAMB" + config["likelihood"][likelihood_name]["distance_max_z"] = distance_max_z config.update( { @@ -255,6 +258,7 @@ def write_config(self) -> None: likelihood_name=likelihood_name, cosmo_spec=self.cosmo_spec, required_cosmology=self.required_cosmology, + distance_max_z=self.distance_max_z, ) add_models(cfg, self.models) write_config(cfg, cobaya_yaml) diff --git a/firecrown/app/analysis/_config_generator.py b/firecrown/app/analysis/_config_generator.py index 8148cc1c9..2222dd0d4 100644 --- a/firecrown/app/analysis/_config_generator.py +++ b/firecrown/app/analysis/_config_generator.py @@ -26,6 +26,7 @@ def get_generator( use_absolute_path: bool, cosmo_spec: CCLCosmologySpec, required_cosmology: FrameworkCosmology = FrameworkCosmology.NONLINEAR, + distance_max_z: float = 4.0, ) -> ConfigGenerator: """Factory function to create framework-specific configuration generator. @@ -38,6 +39,7 @@ def get_generator( :param cosmo_spec: Cosmology specification with parameters and priors :param required_cosmology: Level of cosmology computation (none/background/linear/nonlinear) + :param distance_max_z: Maximum redshift for background/distance splines :return: Initialized configuration generator ready for component addition :raises ValueError: If framework is not supported """ @@ -50,6 +52,7 @@ def get_generator( use_absolute_path=use_absolute_path, cosmo_spec=cosmo_spec, required_cosmology=required_cosmology, + distance_max_z=distance_max_z, ) case Frameworks.COBAYA: return CobayaConfigGenerator( @@ -58,6 +61,7 @@ def get_generator( use_absolute_path=use_absolute_path, cosmo_spec=cosmo_spec, required_cosmology=required_cosmology, + distance_max_z=distance_max_z, ) case Frameworks.NUMCOSMO: return NumCosmoConfigGenerator( @@ -66,6 +70,7 @@ def get_generator( use_absolute_path=use_absolute_path, cosmo_spec=cosmo_spec, required_cosmology=required_cosmology, + distance_max_z=distance_max_z, ) case _: raise ValueError(f"Unsupported framework: {framework}") diff --git a/firecrown/app/analysis/_cosmosis.py b/firecrown/app/analysis/_cosmosis.py index 976521b64..6931e3c54 100644 --- a/firecrown/app/analysis/_cosmosis.py +++ b/firecrown/app/analysis/_cosmosis.py @@ -80,10 +80,19 @@ def add_comment_block( config.set(section, comment_line) +#: Default upper bound of the matter-power/background redshift grid. Analyses +#: needing background quantities beyond this (e.g. CMB lensing) get the extra +#: reach through CAMB's own log-spaced background extension (n_logz/zmax_logz) +#: instead of widening this grid, since that would coarsen the P(k,z) sampling +#: over the whole range for no benefit. +_DEFAULT_ZMAX = 4.0 + + def _add_cosmology_modules( cfg: configparser.ConfigParser, required_cosmology: FrameworkCosmology, cosmo_spec: CCLCosmologySpec, + distance_max_z: float = _DEFAULT_ZMAX, ) -> None: """Add CAMB and consistency modules to pipeline configuration.""" cfg["consistency"] = { @@ -101,10 +110,15 @@ def _add_cosmology_modules( "mode": "power", "feedback": "0", "zmin": "0.0", - "zmax": "4.0", + "zmax": str(_DEFAULT_ZMAX), "nz": "100", "kmax": "50.0", "nk": "1000", + **( + {"n_logz": "50", "zmax_logz": str(distance_max_z)} + if distance_max_z > _DEFAULT_ZMAX + else {} + ), **( cosmo_spec.extra_parameters.get_dict() if cosmo_spec.extra_parameters @@ -147,6 +161,7 @@ def create_config( *, use_absolute_path: bool = True, required_cosmology: FrameworkCosmology = FrameworkCosmology.NONLINEAR, + distance_max_z: float = _DEFAULT_ZMAX, ) -> configparser.ConfigParser: """Create CosmoSIS pipeline configuration (main INI file). @@ -162,6 +177,7 @@ def create_config( :param cosmo_spec: Cosmology specification :param use_absolute_path: Use absolute paths in configuration :param required_cosmology: Level of cosmology computation + :param distance_max_z: Maximum redshift for background/distance splines :return: Configured ConfigParser ready to write """ cfg = configparser.ConfigParser( @@ -196,7 +212,7 @@ def create_config( cfg["pipeline"]["priors"] = get_path_str(priors_path, use_absolute_path) if use_cosmology: - _add_cosmology_modules(cfg, required_cosmology, cosmo_spec) + _add_cosmology_modules(cfg, required_cosmology, cosmo_spec, distance_max_z) _add_firecrown_likelihood( cfg, get_path_str(factory_source, use_absolute_path), build_parameters @@ -447,6 +463,7 @@ def write_config(self) -> None: cosmo_spec=self.cosmo_spec, use_absolute_path=self.use_absolute_path, required_cosmology=self.required_cosmology, + distance_max_z=self.distance_max_z, ) add_models(cfg, self.models) diff --git a/firecrown/app/analysis/_numcosmo.py b/firecrown/app/analysis/_numcosmo.py index aab39fac2..dc57e78c5 100644 --- a/firecrown/app/analysis/_numcosmo.py +++ b/firecrown/app/analysis/_numcosmo.py @@ -643,6 +643,7 @@ def write_config(self) -> None: self.use_absolute_path, self.required_cosmology, self.prefix, + distance_max_z=self.distance_max_z, ) ctx = mp.get_context("spawn") proc = ctx.Process(target=_write_config_worker, args=(config_options,)) diff --git a/firecrown/app/analysis/_types.py b/firecrown/app/analysis/_types.py index 47dcb6175..c58359785 100644 --- a/firecrown/app/analysis/_types.py +++ b/firecrown/app/analysis/_types.py @@ -354,6 +354,7 @@ class ConfigGenerator(ABC): use_absolute_path: bool cosmo_spec: CCLCosmologySpec required_cosmology: FrameworkCosmology = FrameworkCosmology.NONLINEAR + distance_max_z: float = 4.0 sacc_path: Path | None = None factory_source: str | Path | None = None diff --git a/firecrown/app/examples/__init__.py b/firecrown/app/examples/__init__.py index 1c773ae71..a40f7ce70 100644 --- a/firecrown/app/examples/__init__.py +++ b/firecrown/app/examples/__init__.py @@ -6,6 +6,7 @@ """ from ..analysis import AnalysisBuilder +from ._cmb_cross import ExampleCMBCross from ._cosmic_shear import ExampleCosmicShear from ._des_y1_3x2pt import DESY1FactoryType, ExampleDESY13x2pt from ._sn_srd import ExampleSupernovaSRD @@ -13,6 +14,7 @@ __all__ = [ "EXAMPLES_LIST", "ExampleCosmicShear", + "ExampleCMBCross", "ExampleSupernovaSRD", "ExampleDESY13x2pt", "DESY1FactoryType", @@ -20,6 +22,7 @@ EXAMPLES_LIST: dict[str, type[AnalysisBuilder]] = { "cosmic_shear": ExampleCosmicShear, + "cmb_cross": ExampleCMBCross, "sn_srd": ExampleSupernovaSRD, "des_y1_3x2pt": ExampleDESY13x2pt, } diff --git a/firecrown/app/examples/_cmb_cross.py b/firecrown/app/examples/_cmb_cross.py new file mode 100644 index 000000000..e2ab7e5e8 --- /dev/null +++ b/firecrown/app/examples/_cmb_cross.py @@ -0,0 +1,548 @@ +"""CMB lensing cross-correlation analysis example generator. + +Generates synthetic weak lensing cosmic shear data cross-correlated with a +CMB lensing convergence map, with realistic noise and covariance for testing +and demonstration purposes. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Annotated, ClassVar, Sequence + +import numpy as np +import pyccl +import sacc +import typer +from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.table import Table + +from firecrown.likelihood import NamedParameters + +from ...utils import upper_triangle_indices +from ..analysis import AnalysisBuilder, FrameworkCosmology, Model, Parameter + +# pylint: disable=duplicate-code + +CMB_TRACER_NAME = "cmb_convergence" +BUILD_LIKELIHOOD_FACTORY = "firecrown.likelihood.factories.build_two_point_likelihood" + + +@dataclass +class ExampleCMBCross(AnalysisBuilder): + """CMB lensing cross-correlation analysis example with synthetic data. + + Generates synthetic weak lensing data for configurable tomographic bins + together with a single CMB lensing convergence tracer, including the + galaxy-galaxy and CMB-galaxy cross-correlation power spectra. + """ + + description: ClassVar[str] = ( + "Weak lensing cosmic shear cross-correlated with CMB lensing convergence" + ) + + prefix: Annotated[ + str, + typer.Option( + help=( + "Prefix for generated filenames (e.g., 'cmb_cross' creates " + "'cmb_cross.sacc')" + ), + show_default=True, + ), + ] = "cmb_cross" + + seed: Annotated[ + int, + typer.Option( + help="Random seed for reproducible synthetic data generation", + show_default=True, + ), + ] = 42 + + n_bins: Annotated[ + int, + typer.Option( + help="Number of galaxy tomographic redshift bins", + show_default=True, + ), + ] = 2 + + z_max: Annotated[ + float, + typer.Option( + help="Maximum redshift for n(z) distributions", + show_default=True, + ), + ] = 2.0 + + n_z_points: Annotated[ + int, + typer.Option( + help="Number of redshift points for n(z) sampling", + show_default=True, + ), + ] = 600 + + ell_min: Annotated[ + float, + typer.Option( + help="Minimum multipole for power spectrum", + show_default=True, + ), + ] = 10.0 + + ell_max: Annotated[ + float, + typer.Option( + help="Maximum multipole for power spectrum", + show_default=True, + ), + ] = 1000.0 + + n_ell_points: Annotated[ + int, + typer.Option( + help="Number of multipole points for power spectrum", + show_default=True, + ), + ] = 10 + + noise_level: Annotated[ + float, + typer.Option( + help="Relative noise level for synthetic data", + show_default=True, + ), + ] = 0.01 + + sigma_z: Annotated[ + float, + typer.Option( + help="Width of Gaussian redshift distributions", + show_default=True, + ), + ] = 0.25 + + z_source: Annotated[ + float, + typer.Option( + help="Source redshift for the CMB lensing convergence tracer", + show_default=True, + ), + ] = 1100.0 + + include_cmb_auto: Annotated[ + bool, + typer.Option( + help="Include the CMB convergence auto-correlation", + show_default=True, + ), + ] = True + + def _setup_phase(self, progress, summary, _output_path): + """Setup phase of SACC generation.""" + task1 = progress.add_task("Setting up cosmology and coordinates...", total=None) + cosmo = self.cosmology_analysis_spec().to_ccl_cosmology() + summary.add_row("[b]Cosmology[/b]", "") + self._show_cosmology_config(cosmo, summary) + + z_range, ell_range = self._create_coordinate_arrays() + summary.add_section() + summary.add_row("[b]Coordinates[/b]", "") + self._show_coordinate_config(z_range, ell_range, summary) + progress.update(task1, completed=True) + + return cosmo, z_range, ell_range + + def _tracer_phase(self, progress, summary, cosmo, z_range): + """Tracer generation phase.""" + task2 = progress.add_task("Creating tomographic and CMB tracers...", total=None) + np.random.seed(self.seed) + sacc_data = sacc.Sacc() + bin_centers, galaxy_tracers = self._create_galaxy_tracers( + sacc_data, cosmo, z_range + ) + cmb_tracer = self._create_cmb_tracer(sacc_data, cosmo) + + summary.add_section() + summary.add_row("[b]Tracers[/b]", "") + self._show_tracer_config(bin_centers, summary) + progress.update(task2, completed=True) + + return sacc_data, galaxy_tracers, cmb_tracer + + def _spectra_phase( + self, progress, summary, sacc_data, cosmo, galaxy_tracers, cmb_tracer, ell_range + ): + """Power spectra computation phase.""" + task3 = progress.add_task("Computing power spectra...", total=None) + theory_cls = self._generate_power_spectra( + sacc_data, cosmo, galaxy_tracers, cmb_tracer, ell_range + ) + summary.add_section() + summary.add_row("[b]Power Spectra[/b]", "") + self._show_power_spectrum_config(summary) + progress.update(task3, completed=True) + return theory_cls + + def _covariance_phase(self, progress, summary, sacc_data, theory_cls): + """Covariance matrix generation phase.""" + task4 = progress.add_task("Adding covariance matrix...", total=None) + summary.add_section() + summary.add_row("[b]Covariance Matrix[/b]", "") + self._show_covariance_config(summary) + self._add_covariance_matrix(sacc_data, theory_cls) + progress.update(task4, completed=True) + + def _save_phase(self, progress, sacc_data, output_path): + """Save SACC file phase.""" + sacc_full_file = output_path / f"{self.prefix}.sacc" + task5 = progress.add_task("Saving SACC file...", total=None) + sacc_data.save_fits(sacc_full_file, overwrite=True) + progress.update(task5, completed=True) + return sacc_full_file + + def generate_sacc(self, output_path: Path) -> Path: + """Generate synthetic CMB cross-correlation data in SACC format. + + Creates a SACC file with galaxy tomographic bins, a CMB lensing + convergence tracer, their power spectra, and covariance. + + :param output_path: Output directory + :return: Path to generated SACC file + """ + summary = Table( + title="CMB Lensing Cross-Correlation Example", + border_style="blue", + show_header=False, + ) + summary.add_column("Parameter", style="cyan", no_wrap=True) + summary.add_column("Value", style="green") + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=self.console, + ) as progress: + + # Phase 1: Setup + cosmo, z_range, ell_range = self._setup_phase( + progress, summary, output_path + ) + + # Phase 2: Tracers + sacc_data, galaxy_tracers, cmb_tracer = self._tracer_phase( + progress, summary, cosmo, z_range + ) + + # Phase 3: Power spectra + theory_cls = self._spectra_phase( + progress, + summary, + sacc_data, + cosmo, + galaxy_tracers, + cmb_tracer, + ell_range, + ) + + # Phase 4: Covariance + self._covariance_phase(progress, summary, sacc_data, theory_cls) + + # Phase 5: Save + sacc_full_file = self._save_phase(progress, sacc_data, output_path) + + progress.console.print( + f"[dim]Generated {len(theory_cls)} power spectra[/dim]" + ) + + summary.add_section() + summary.add_row("[b]Output File[/b]", sacc_full_file.as_posix()) + + self.console.print(summary) + + return sacc_full_file + + def _create_coordinate_arrays(self) -> tuple[np.ndarray, np.ndarray]: + """Create coordinate arrays for redshift and multipole sampling. + + :return: Tuple of (z_range, ell_range) arrays for sampling + """ + z_range = np.linspace(0, self.z_max, self.n_z_points) + 0.05 + ell_range = np.logspace( + np.log10(self.ell_min), np.log10(self.ell_max), self.n_ell_points + ) + return z_range, ell_range + + def _show_cosmology_config(self, cosmo: pyccl.Cosmology, table: Table) -> None: + """Display cosmology configuration.""" + params = [ + ("Omega_c", ".3f"), + ("Omega_b", ".3f"), + ("h", ".2f"), + ("Omega_k", ".3f"), + ("T_CMB", ".2f"), + ("Neff", ".2f"), + ("w0", ".1f"), + ("wa", ".1f"), + ("A_s", ".1e"), + ("sigma8", ".2f"), + ("n_s", ".2f"), + ] + + for param, fmt in params: + if cosmo[param] is not None and not np.isnan(cosmo[param]): + table.add_row(param, f"{cosmo[param]:{fmt}}") + + def _show_coordinate_config( + self, z_range: np.ndarray, ell_range: np.ndarray, table: Table + ) -> None: + """Display coordinate configuration.""" + table.add_row("Redshift", f"{z_range.min():.2f} - {z_range.max():.2f}") + table.add_row("Multipoles", f"{ell_range.min():.0f} - {ell_range.max():.0f}") + + def _show_tracer_config(self, bin_centers: np.ndarray, table: Table) -> None: + """Display tracer configuration.""" + table.add_row("Number of galaxy bins", f"{self.n_bins}") + table.add_row("Redshift width", f"{self.sigma_z:.2f}") + table.add_row("Random seed", f"{self.seed}") + table.add_row("Bin centers", ", ".join(f"{x:.3f}" for x in bin_centers)) + table.add_row("CMB source redshift", f"{self.z_source:.1f}") + + def _show_power_spectrum_config(self, table: Table) -> None: + """Display power spectrum configuration.""" + n_galaxy_correlations = self.n_bins * (self.n_bins + 1) // 2 + n_cmb_cross = self.n_bins + n_cmb_auto = 1 if self.include_cmb_auto else 0 + + table.add_row("Galaxy correlations", f"{n_galaxy_correlations} (auto + cross)") + table.add_row("CMB-galaxy cross-correlations", f"{n_cmb_cross}") + table.add_row("CMB auto-correlation", f"{n_cmb_auto}") + table.add_row("Noise level", f"{self.noise_level:.3f}") + + def _show_covariance_config(self, table: Table) -> None: + """Display covariance configuration.""" + n_galaxy = self.n_ell_points * self.n_bins * (self.n_bins + 1) // 2 + n_cmb_cross = self.n_ell_points * self.n_bins + n_cmb_auto = self.n_ell_points if self.include_cmb_auto else 0 + + table.add_row("Number of points", f"{n_galaxy + n_cmb_cross + n_cmb_auto}") + table.add_row("Format", "diagonal") + + def _create_galaxy_tracers( + self, sacc_data: sacc.Sacc, cosmo: pyccl.Cosmology, z_range: np.ndarray + ) -> tuple[np.ndarray, list[pyccl.WeakLensingTracer]]: + """Create tomographic redshift bins and weak lensing tracers. + + :param sacc_data: SACC data object to populate with tracer metadata + :param cosmo: CCL cosmology object for tracer calculations + :param z_range: Redshift sampling array + :return: Bin centers and CCL WeakLensingTracer objects + """ + tracers = [] + bin_centers = np.linspace(0.2, self.z_max * 0.8, self.n_bins) + + for i, z_mean in enumerate(bin_centers): + nz = np.exp(-0.5 * (z_range - z_mean) ** 2 / self.sigma_z**2) + + tracer_name = f"trc{i}" + sacc_data.add_tracer("NZ", tracer_name, z_range, nz) + + tracers.append(pyccl.WeakLensingTracer(cosmo, dndz=(z_range, nz))) + + return bin_centers, tracers + + def _create_cmb_tracer( + self, sacc_data: sacc.Sacc, cosmo: pyccl.Cosmology + ) -> pyccl.CMBLensingTracer: + """Create the CMB lensing convergence tracer. + + The tracer is stored in the SACC file as a ``MapTracer`` (spin-0, + unit beam) and the source redshift is stored in its metadata under + the ``z_lss`` key. + + :param sacc_data: SACC data object to populate with tracer metadata + :param cosmo: CCL cosmology object for tracer calculations + :return: CCL CMBLensingTracer object for theory calculations + """ + beam_ells = np.array([0.0, self.ell_max]) + sacc_data.add_tracer( + "Map", + CMB_TRACER_NAME, + 0, + beam_ells, + np.ones_like(beam_ells), + metadata={"z_lss": self.z_source}, + ) + return pyccl.CMBLensingTracer(cosmo, z_source=self.z_source) + + def _generate_power_spectra( + self, + sacc_data: sacc.Sacc, + cosmo: pyccl.Cosmology, + galaxy_tracers: list[pyccl.WeakLensingTracer], + cmb_tracer: pyccl.CMBLensingTracer, + ell_range: np.ndarray, + ) -> list[np.ndarray]: + """Generate galaxy and CMB-galaxy power spectra with realistic noise. + + Computes theoretical C_ell for all galaxy auto/cross-correlations, + all CMB-galaxy cross-correlations, and (optionally) the CMB + auto-correlation, adds Gaussian noise, and stores in SACC format. + + :param sacc_data: SACC data object to populate with measurements + :param cosmo: CCL cosmology for theoretical predictions + :param galaxy_tracers: List of weak lensing tracers for each bin + :param cmb_tracer: CMB lensing convergence tracer + :param ell_range: Multipole sampling array + :return: List of noise-free theoretical power spectra for covariance + """ + theory_cls = [] + + def add_noisy_cl( + data_type: str, tracer1: str, tracer2: str, cl_theory: np.ndarray + ) -> None: + noise = np.random.normal(size=len(cl_theory)) * self.noise_level * cl_theory + cl_noisy = cl_theory + noise + sacc_data.add_ell_cl(data_type, tracer1, tracer2, ell_range, cl_noisy) + theory_cls.append(cl_theory) + + # Galaxy-galaxy auto and cross-correlations. + for i, j in upper_triangle_indices(len(galaxy_tracers)): + cl_theory = pyccl.angular_cl( + cosmo, galaxy_tracers[i], galaxy_tracers[j], ell_range + ) + add_noisy_cl("galaxy_shear_cl_ee", f"trc{i}", f"trc{j}", cl_theory) + + # CMB-galaxy cross-correlations. + for i, galaxy_tracer in enumerate(galaxy_tracers): + cl_theory = pyccl.angular_cl(cosmo, cmb_tracer, galaxy_tracer, ell_range) + add_noisy_cl( + "cmbGalaxy_convergenceShear_cl_e", + CMB_TRACER_NAME, + f"trc{i}", + cl_theory, + ) + + # CMB auto-correlation. + if self.include_cmb_auto: + cl_theory = pyccl.angular_cl(cosmo, cmb_tracer, cmb_tracer, ell_range) + add_noisy_cl( + "cmb_convergence_cl", CMB_TRACER_NAME, CMB_TRACER_NAME, cl_theory + ) + + return theory_cls + + def _add_covariance_matrix( + self, sacc_data: sacc.Sacc, theory_cls: list[np.ndarray] + ) -> None: + """Add diagonal covariance matrix based on theoretical predictions. + + :param sacc_data: SACC data object to populate with covariance + :param theory_cls: Theoretical power spectra for uncertainty estimation + """ + all_theory = np.concatenate(theory_cls) + cov_diag = (self.noise_level * all_theory) ** 2 + covariance = np.diag(cov_diag) + sacc_data.add_covariance(covariance) + + def generate_factory(self, output_path: Path, _sacc: Path) -> str: + """Generate a YAML likelihood configuration for the two-point factory. + + The configuration drives Firecrown's ``TwoPointFactory`` system, + which discovers sources and statistics automatically from the SACC + file's tracers and data, dispatching each tracer to the appropriate + ``WeakLensingFactory`` or ``CMBConvergenceFactory``. + + :param output_path: Output directory + :param _sacc: SACC file path + :return: Fully qualified name of the likelihood factory function + """ + output_file = output_path / f"{self.prefix}_experiment.yaml" + output_file.write_text(self._get_yaml_config(_sacc)) + return BUILD_LIKELIHOOD_FACTORY + + def _get_yaml_config(self, sacc_path: Path) -> str: + """Generate the YAML likelihood configuration content. + + :param sacc_path: SACC file path + :return: YAML configuration as a string + """ + sacc_path_str = self.get_sacc_file(sacc_path) + return f"""--- + +data_source: + sacc_data_file: {sacc_path_str} + +two_point_factory: + correlation_space: harmonic + weak_lensing_factories: + - type_source: default + global_systematics: [] + per_bin_systematics: + - type: PhotoZShiftFactory + cmb_factories: + - type_source: default + z_source: {self.z_source} + +ccl_factory: + require_nonlinear_pk: true +""" + + def get_build_parameters(self, sacc_path: Path) -> NamedParameters: + """Return the likelihood configuration path for likelihood construction.""" + experiment_path = sacc_path.parent / f"{self.prefix}_experiment.yaml" + return NamedParameters( + {"likelihood_config": experiment_path.absolute().as_posix()} + ) + + def get_models(self) -> list[Model]: + """Define photo-z shift parameters for each tomographic bin. + + :return: Model with delta_z parameters for all galaxy bins + """ + parameters: list[tuple[str, str, float, float, float, bool]] = [ + ( + f"trc{bin_index}_delta_z", + rf"\delta_{{z{bin_index}}}", + -5.0, + 5.0, + 0.5, + True, + ) + for bin_index in range(self.n_bins) + ] + return [ + Model( + name=f"firecrown_{self.prefix}", + description="Model parameters for CMB cross-correlation analysis", + parameters=[Parameter.from_tuple(*param) for param in parameters], + ) + ] + + def required_cosmology(self): + """Return cosmology requirement level.""" + return FrameworkCosmology.NONLINEAR + + def required_distance_max_z(self) -> float: + """Return a redshift past the CMB lensing source redshift. + + The background/distance splines must reach at least z_source for the + CMB lensing tracer to be usable. + """ + return self.z_source * 1.1 + + def get_options_desc(self) -> Sequence[tuple[str, str]]: + """Return description of CMB Cross-Correlation options.""" + return [ + ("Number of Galaxy Bins", f"{self.n_bins}"), + ("Include CMB Auto-Correlation", f"{self.include_cmb_auto}"), + ("CMB Source Redshift", f"{self.z_source}"), + ("Noise Level", f"{self.noise_level}"), + ("Redshift Width", f"{self.sigma_z}"), + ("Redshift Max", f"{self.z_max}"), + ("Ell Min", f"{self.ell_min}"), + ("Ell Max", f"{self.ell_max}"), + ("Number of Ell Points", f"{self.n_ell_points}"), + ("Number of Z Points", f"{self.n_z_points}"), + ] diff --git a/firecrown/app/sacc/_utils.py b/firecrown/app/sacc/_utils.py index 80f1079c0..2f16fe311 100644 --- a/firecrown/app/sacc/_utils.py +++ b/firecrown/app/sacc/_utils.py @@ -18,7 +18,7 @@ ) -def mean_std_tracer(tracer: mdt.InferredGalaxyZDist): +def mean_std_tracer(tracer: mdt.TomographicBin): """Compute the mean and standard deviation of a tracer. :param tracer: The galaxy redshift distribution tracer to analyze. diff --git a/firecrown/app/sacc/_view.py b/firecrown/app/sacc/_view.py index 3fe1902db..a7eba1faf 100644 --- a/firecrown/app/sacc/_view.py +++ b/firecrown/app/sacc/_view.py @@ -127,7 +127,7 @@ def _show_sacc_summary(self) -> None: n_cov_elements = 0 else: n_cov_elements = self.sacc_data.covariance.dense.shape[0] - self.all_tracers = mdf.extract_all_tracers_inferred_galaxy_zdists( + self.all_tracers = mdf.extract_all_tracers_projected_fields( self.sacc_data, self.allow_mixed_types ) self.all_tracers.sort(key=lambda t: t.bin_name) @@ -156,9 +156,16 @@ def _show_tracers(self) -> None: measurements_str = ", ".join( [m.name for m in sorted(tracer.measurements)] ) - z_str = f"{tracer.z.min():5.3f}-{tracer.z.max():5.3f}" - mean, std = mean_std_tracer(tracer) - dndz_str = f"{mean:5.3f} +/- {std:5.3f}" + if isinstance(tracer, mdt.CMBLensing): + z_str = f"z_lss={tracer.z_lss:5.3f}" + dndz_str = "N/A" + elif isinstance(tracer, mdt.TomographicBin): + z_str = f"{tracer.z.min():5.3f}-{tracer.z.max():5.3f}" + mean, std = mean_std_tracer(tracer) + dndz_str = f"{mean:5.3f} +/- {std:5.3f}" + else: # pragma: no cover - defensive, no other ProjectedField today + z_str = "N/A" + dndz_str = "N/A" table.add_row( tracer.bin_name, tracer.type_source, @@ -549,7 +556,7 @@ def _capture_sacc_operations( sacc_data_raw = factories.load_sacc_data(str(self.sacc_file)) # Extract tracers (this may trigger warnings about naming conventions) - _ = mdf.extract_all_tracers_inferred_galaxy_zdists( + _ = mdf.extract_all_tracers_projected_fields( sacc_data_raw, allow_mixed_types=False ) captured_warnings = list(w) diff --git a/firecrown/connector/cobaya/likelihood.py b/firecrown/connector/cobaya/likelihood.py index fd8c7e1f1..e8da1d767 100644 --- a/firecrown/connector/cobaya/likelihood.py +++ b/firecrown/connector/cobaya/likelihood.py @@ -36,9 +36,14 @@ handle_unused_params, ) +#: Number of extra log-spaced points used to extend the background/distance +#: array beyond spl.A_SPLINE_MIN when distance_max_z requires it (see below). +_N_LOGZ_EXTRA = 50 + def compute_pyccl_args_options( ccl_cosmo: pyccl.Cosmology, + distance_max_z: float = 4.0, ) -> tuple[ npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64], float ]: @@ -46,6 +51,10 @@ def compute_pyccl_args_options( This method uses the CCLFactory to create a pyccl object and returns the dictionary of precision options for the pyccl object. + + :param ccl_cosmo: The CCL cosmology object. + :param distance_max_z: Maximum redshift the background/distance arrays must + reach (e.g. for a CMB lensing tracer, this must be at least z_source). """ # Here we follow the pyccl convention. spl = ccl_cosmo.cosmo.spline_params @@ -75,6 +84,21 @@ def compute_pyccl_args_options( z_bg = (1.0 / a_bg - 1.0).astype(np.float64) + # spl.A_SPLINE_MIN limits the background array to z <= z_spline_min. If a + # tracer needs background quantities beyond that (e.g. CMB lensing, with + # its source at z~1100), extend z_bg/a_bg with a handful of coarse, + # log-spaced points reaching distance_max_z. These are background-only + # (comoving distance, H(z) are cheap); the matter power spectrum grid + # below is left untouched. We prepend points strictly beyond a_bg's + # current minimum, so the array's new minimum a is never equal to + # spl.A_SPLINE_MINLOG, avoiding the degenerate case described above. + z_spline_min = 1.0 / spl.A_SPLINE_MIN - 1.0 + if distance_max_z > z_spline_min: + z_extra = np.geomspace(distance_max_z, z_spline_min, num=_N_LOGZ_EXTRA)[:-1] + a_extra = (1.0 / (1.0 + z_extra)).astype(np.float64) + z_bg = np.concatenate([z_extra, z_bg]) + a_bg = np.concatenate([a_extra, a_bg]) + # We inspect the linear power spectrum from pyccl to get the maximum k and the # redshift grid. psp: Pk2D = ccl_cosmo.get_linear_power() @@ -93,6 +117,7 @@ class LikelihoodConnector(Likelihood): "input_style", "build_parameters", "derived_parameters", + "distance_max_z", ] input_style: str | None = None @@ -100,6 +125,7 @@ class LikelihoodConnector(Likelihood): firecrownIni: str = "" derived_parameters: list[str] = [] build_parameters: NamedParameters = NamedParameters() + distance_max_z: float = 4.0 def initialize(self): """Initialize the likelihood object by loading its Firecrown configuration.""" @@ -140,7 +166,7 @@ def initialize(self): self.tools.prepare() ccl_cosmo = self.tools.ccl_factory.get() self.a_bg, self.z_bg, z_array, Pk_kmax = compute_pyccl_args_options( - ccl_cosmo + ccl_cosmo, self.distance_max_z ) # We need to request external Boltzmann code for power spectra, if we want diff --git a/firecrown/data_functions/_extraction.py b/firecrown/data_functions/_extraction.py index 7322ad2ad..7c3a10850 100644 --- a/firecrown/data_functions/_extraction.py +++ b/firecrown/data_functions/_extraction.py @@ -7,7 +7,7 @@ from firecrown.metadata_functions import ( extract_all_harmonic_metadata_indices, extract_all_real_metadata_indices, - extract_all_tracers_inferred_galaxy_zdists, + extract_all_tracers_projected_fields, make_two_point_xy, maybe_enforce_window, ) @@ -31,9 +31,9 @@ def extract_all_harmonic_data( if sacc_data.covariance is None or sacc_data.covariance.dense is None: raise ValueError("The SACC object does not have a dense covariance matrix.") - inferred_galaxy_zdists_dict = { - igz.bin_name: igz - for igz in extract_all_tracers_inferred_galaxy_zdists( + projected_fields_dict = { + projected_field.bin_name: projected_field + for projected_field in extract_all_tracers_projected_fields( sacc_data, allow_mixed_types ) } @@ -60,7 +60,7 @@ def extract_all_harmonic_data( covariance_name=cov_hash(sacc_data), metadata=TwoPointHarmonic( XY=make_two_point_xy( - inferred_galaxy_zdists_dict, cell_index["tracer_names"], dt + projected_fields_dict, cell_index["tracer_names"], dt ), window=weights, window_ells=window_ells, @@ -81,9 +81,9 @@ def extract_all_real_data( if sacc_data.covariance is None or sacc_data.covariance.dense is None: raise ValueError("The SACC object does not have a dense covariance matrix.") - inferred_galaxy_zdists_dict = { - igz.bin_name: igz - for igz in extract_all_tracers_inferred_galaxy_zdists(sacc_data) + projected_fields_dict = { + projected_field.bin_name: projected_field + for projected_field in extract_all_tracers_projected_fields(sacc_data) } result: list[TwoPointMeasurement] = [] @@ -104,7 +104,7 @@ def extract_all_real_data( covariance_name=cov_hash(sacc_data), metadata=TwoPointReal( XY=make_two_point_xy( - inferred_galaxy_zdists_dict, real_index["tracer_names"], dt + projected_fields_dict, real_index["tracer_names"], dt ), thetas=thetas, ), diff --git a/firecrown/generators/_inferred_galaxy_zdist.py b/firecrown/generators/_inferred_galaxy_zdist.py index 56a91b985..d5a7e5606 100644 --- a/firecrown/generators/_inferred_galaxy_zdist.py +++ b/firecrown/generators/_inferred_galaxy_zdist.py @@ -12,7 +12,7 @@ from scipy.special import erf, erfc, gamma # pylint: disable=no-name-in-module from firecrown.metadata_functions import make_measurements, make_measurements_dict -from firecrown.metadata_types import Galaxies, InferredGalaxyZDist, Measurement +from firecrown.metadata_types import Galaxies, TomographicBin, Measurement class BinsType(TypedDict): @@ -360,7 +360,7 @@ def binned_distribution( z: npt.NDArray, name: str, measurements: set[Measurement], - ) -> InferredGalaxyZDist: + ) -> TomographicBin: """Generate the inferred galaxy redshift distribution in bins. :param zpl: The lower bound of the integration @@ -411,7 +411,7 @@ def _P(z, _): / norma ) - return InferredGalaxyZDist( + return TomographicBin( bin_name=name, z=z_knots, dndz=dndz, measurements=measurements ) @@ -463,7 +463,7 @@ def serialize_measurements(cls, value: set[Measurement]) -> list[dict]: """Serialize the Measurement.""" return make_measurements_dict(value) - def generate(self, zdist: ZDistLSSTSRD) -> InferredGalaxyZDist: + def generate(self, zdist: ZDistLSSTSRD) -> TomographicBin: """Generate the inferred galaxy redshift distribution in bins.""" return zdist.binned_distribution( zpl=self.zpl, @@ -489,7 +489,7 @@ class ZDistLSSTSRDBinCollection(BaseModel): autoknots_reltol: float = 1.0e-4 autoknots_abstol: float = 1.0e-15 - def generate(self) -> list[InferredGalaxyZDist]: + def generate(self) -> list[TomographicBin]: """Generate the inferred galaxy redshift distributions in bins.""" zdist = ZDistLSSTSRD( alpha=self.alpha, diff --git a/firecrown/likelihood/__init__.py b/firecrown/likelihood/__init__.py index 9cfa84d13..5363b32f2 100644 --- a/firecrown/likelihood/__init__.py +++ b/firecrown/likelihood/__init__.py @@ -35,7 +35,11 @@ from firecrown.likelihood._binned_cluster_number_counts_shear import ( BinnedClusterShearProfile, ) -from firecrown.likelihood._cmb import CMBConvergence, CMBConvergenceArgs +from firecrown.likelihood._cmb import ( + CMBConvergence, + CMBConvergenceArgs, + CMBConvergenceFactory, +) from firecrown.likelihood._gaussfamily import GaussFamily, State from firecrown.likelihood._gaussian import ConstGaussian from firecrown.likelihood._gaussian_pointmass import ConstGaussianPM, PointMassData @@ -85,6 +89,7 @@ "SourceGalaxyArgs", "CMBConvergence", "CMBConvergenceArgs", + "CMBConvergenceFactory", "SourceGalaxySystematic", "Tracer", "SourceSystematic", diff --git a/firecrown/likelihood/_cmb.py b/firecrown/likelihood/_cmb.py index 85a7ffe1e..9d4fb422c 100644 --- a/firecrown/likelihood/_cmb.py +++ b/firecrown/likelihood/_cmb.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict, PrivateAttr from firecrown.likelihood_base import Source, Tracer -from firecrown.metadata_types import InferredGalaxyZDist, TypeSource +from firecrown.metadata_types import ProjectedField, CMBLensing, TypeSource from firecrown.modeling_tools import ModelingTools @@ -113,14 +113,15 @@ def model_post_init(self, _, /) -> None: """Initialize the CMBConvergenceFactory.""" self._cache: dict[int, CMBConvergence] = {} - def create(self, inferred_galaxy_zdist: InferredGalaxyZDist) -> CMBConvergence: + def create(self, cmb_tracer: ProjectedField) -> CMBConvergence: """Create a CMBConvergence object with the given inferred galaxy z distribution. - :param inferred_galaxy_zdist: the inferred galaxy redshift distribution + :param cmb_tracer: the inferred galaxy redshift distribution :return: a fully initialized CMBConvergence object """ + assert isinstance(cmb_tracer, CMBLensing) # Use the bin_name as the tracer identifier - sacc_tracer = inferred_galaxy_zdist.bin_name + sacc_tracer = cmb_tracer.bin_name tracer_id = hash(sacc_tracer) if tracer_id in self._cache: diff --git a/firecrown/likelihood/_two_point.py b/firecrown/likelihood/_two_point.py index cb82f8052..99aab6b91 100644 --- a/firecrown/likelihood/_two_point.py +++ b/firecrown/likelihood/_two_point.py @@ -37,7 +37,6 @@ CMB_TYPES, GALAXY_LENS_TYPES, GALAXY_SOURCE_TYPES, - InferredGalaxyZDist, Measurement, TracerNames, TwoPointCorrelationSpace, @@ -280,6 +279,11 @@ def _from_metadata_single_base( :return: A TwoPoint statistic. """ + # metadata.XY.x/Y are typed as Tracer (protocol). In this code path we + # expect concrete TomographicBin instances (with redshift arrays). Use + # runtime isinstance checks and raise an informative error if this is + # not the case — this both documents the assumption and narrows the + # type for the type checker. source0 = use_source_factory( metadata.XY.x, metadata.XY.x_measurement, tp_factory ) @@ -863,21 +867,19 @@ def from_metadata( def use_source_factory( - inferred_galaxy_zdist: InferredGalaxyZDist, + tomographic_bin: mdt.ProjectedField, measurement: Measurement, tp_factory: TwoPointFactory, ) -> WeakLensing | NumberCounts | CMBConvergence: """Apply the factory to the inferred galaxy redshift distribution.""" - if measurement not in inferred_galaxy_zdist.measurements: + if measurement not in tomographic_bin.measurements: raise ValueError( f"Measurement {measurement} not found in inferred galaxy redshift " - f"distribution {inferred_galaxy_zdist.bin_name}!" + f"distribution {tomographic_bin.bin_name}!" ) - source_factory = tp_factory.get_factory( - measurement, inferred_galaxy_zdist.type_source - ) - source = source_factory.create(inferred_galaxy_zdist) + source_factory = tp_factory.get_factory(measurement, tomographic_bin.type_source) + source = source_factory.create(tomographic_bin) return source diff --git a/firecrown/likelihood/number_counts/_factories.py b/firecrown/likelihood/number_counts/_factories.py index 3345fb345..26e00a570 100644 --- a/firecrown/likelihood/number_counts/_factories.py +++ b/firecrown/likelihood/number_counts/_factories.py @@ -20,7 +20,7 @@ PhotoZShiftFactory, SourceGalaxySystematic, ) -from firecrown.metadata_types import InferredGalaxyZDist, TypeSource +from firecrown.metadata_types import ProjectedField, TomographicBin, TypeSource class LinearBiasSystematicFactory(BaseModel): @@ -155,24 +155,25 @@ def model_post_init(self, _, /) -> None: for nc_systematic_factory in self.global_systematics ] - def create(self, inferred_zdist: InferredGalaxyZDist) -> NumberCounts: + def create(self, tomographic_bin: ProjectedField) -> NumberCounts: """Create a NumberCounts object with the given tracer name and scale. - :param inferred_zdist: the inferred redshift distribution + :param tomographic_bin: the inferred redshift distribution :return: a fully initialized NumberCounts object """ - inferred_zdist_id = id(inferred_zdist) + assert isinstance(tomographic_bin, TomographicBin) + inferred_zdist_id = id(tomographic_bin) if inferred_zdist_id in self._cache: return self._cache[inferred_zdist_id] systematics: list[SourceGalaxySystematic[NumberCountsArgs]] = [ - systematic_factory.create(inferred_zdist.bin_name) + systematic_factory.create(tomographic_bin.bin_name) for systematic_factory in self.per_bin_systematics ] systematics.extend(self._global_systematics_instances) nc = NumberCounts.create_ready( - inferred_zdist, systematics=systematics, has_rsd=self.include_rsd + tomographic_bin, systematics=systematics, has_rsd=self.include_rsd ) self._cache[inferred_zdist_id] = nc diff --git a/firecrown/likelihood/number_counts/_source.py b/firecrown/likelihood/number_counts/_source.py index ff373afa2..5c2818b8a 100644 --- a/firecrown/likelihood/number_counts/_source.py +++ b/firecrown/likelihood/number_counts/_source.py @@ -15,7 +15,7 @@ SourceGalaxySystematic, Tracer, ) -from firecrown.metadata_types import InferredGalaxyZDist +from firecrown.metadata_types import TomographicBin from firecrown.modeling_tools import ModelingTools from firecrown.updatable import ( DerivedParameter, @@ -69,7 +69,7 @@ def __init__( @classmethod def create_ready( cls, - inferred_zdist: InferredGalaxyZDist, + tomographic_bin: TomographicBin, has_rsd: bool = False, derived_scale: bool = False, scale: float = 1.0, @@ -80,7 +80,7 @@ def create_ready( This is the recommended way to create a NumberCounts object. It creates a fully initialized object. - :param inferred_zdist: the inferred redshift distribution + :param tomographic_bin: the inferred redshift distribution :param has_rsd: whether to include RSD in the tracer :param derived_scale: whether to include a derived parameter for the scale of the tracer @@ -89,7 +89,7 @@ def create_ready( :return: a fully initialized NumberCounts object """ obj = cls( - sacc_tracer=inferred_zdist.bin_name, + sacc_tracer=tomographic_bin.bin_name, systematics=systematics, has_rsd=has_rsd, derived_scale=derived_scale, @@ -98,8 +98,8 @@ def create_ready( # pylint: disable=unexpected-keyword-arg obj.tracer_args = NumberCountsArgs( scale=obj.scale, - z=inferred_zdist.z, - dndz=inferred_zdist.dndz, + z=tomographic_bin.z, + dndz=tomographic_bin.dndz, bias=None, mag_bias=None, ) diff --git a/firecrown/likelihood/weak_lensing/_factories.py b/firecrown/likelihood/weak_lensing/_factories.py index adc1cdcd8..76fa4bfaa 100644 --- a/firecrown/likelihood/weak_lensing/_factories.py +++ b/firecrown/likelihood/weak_lensing/_factories.py @@ -19,7 +19,7 @@ PhotoZShiftFactory, SourceGalaxySystematic, ) -from firecrown.metadata_types import InferredGalaxyZDist, TypeSource +from firecrown.metadata_types import ProjectedField, TomographicBin, TypeSource class MultiplicativeShearBiasFactory(BaseModel): @@ -35,7 +35,7 @@ class MultiplicativeShearBiasFactory(BaseModel): def create(self, bin_name: str) -> MultiplicativeShearBias: """Create a MultiplicativeShearBias object. - :param inferred_zdist: The inferred galaxy redshift distribution for + :param tomographic_bin: The inferred galaxy redshift distribution for the created MultiplicativeShearBias object. :return: The created MultiplicativeShearBias object. """ @@ -64,7 +64,7 @@ class LinearAlignmentSystematicFactory(BaseModel): def create(self, bin_name: str) -> LinearAlignmentSystematic: """Create a LinearAlignmentSystematic object. - :param inferred_zdist: The inferred galaxy redshift distribution for + :param tomographic_bin: The inferred galaxy redshift distribution for the created LinearAlignmentSystematic object. :return: The created LinearAlignmentSystematic object. """ @@ -92,7 +92,7 @@ class TattAlignmentSystematicFactory(BaseModel): def create(self, bin_name: str) -> TattAlignmentSystematic: """Create a TattAlignmentSystematic object. - :param inferred_zdist: The inferred galaxy redshift distribution for + :param tomographic_bin: The inferred galaxy redshift distribution for the created TattAlignmentSystematic object. :return: The created TattAlignmentSystematic object. """ @@ -142,19 +142,20 @@ def model_post_init(self, _, /) -> None: for wl_systematic_factory in self.global_systematics ] - def create(self, inferred_zdist: InferredGalaxyZDist) -> WeakLensing: + def create(self, tomographic_bin: ProjectedField) -> WeakLensing: """Create a WeakLensing object with the given tracer name and scale.""" - inferred_zdist_id = id(inferred_zdist) + assert isinstance(tomographic_bin, TomographicBin) + inferred_zdist_id = id(tomographic_bin) if inferred_zdist_id in self._cache: return self._cache[inferred_zdist_id] systematics: list[SourceGalaxySystematic[WeakLensingArgs]] = [ - systematic_factory.create(inferred_zdist.bin_name) + systematic_factory.create(tomographic_bin.bin_name) for systematic_factory in self.per_bin_systematics ] systematics.extend(self._global_systematics_instances) - wl = WeakLensing.create_ready(inferred_zdist, systematics) + wl = WeakLensing.create_ready(tomographic_bin, systematics) self._cache[inferred_zdist_id] = wl return wl diff --git a/firecrown/likelihood/weak_lensing/_source.py b/firecrown/likelihood/weak_lensing/_source.py index 92f856866..c0a024872 100644 --- a/firecrown/likelihood/weak_lensing/_source.py +++ b/firecrown/likelihood/weak_lensing/_source.py @@ -16,7 +16,7 @@ SourceGalaxySystematic, Tracer, ) -from firecrown.metadata_types import InferredGalaxyZDist +from firecrown.metadata_types import TomographicBin from firecrown.modeling_tools import ModelingTools from firecrown.updatable import ParamsMap @@ -51,14 +51,17 @@ def __init__( @classmethod def create_ready( cls, - inferred_zdist: InferredGalaxyZDist, + tomographic_bin: TomographicBin, systematics: None | list[SourceGalaxySystematic[WeakLensingArgs]] = None, ) -> WeakLensing: """Create a WeakLensing object with the given tracer name and scale.""" - obj = cls(sacc_tracer=inferred_zdist.bin_name, systematics=systematics) + obj = cls(sacc_tracer=tomographic_bin.bin_name, systematics=systematics) # pylint: disable=unexpected-keyword-arg obj.tracer_args = WeakLensingArgs( - scale=obj.scale, z=inferred_zdist.z, dndz=inferred_zdist.dndz, ia_bias=None + scale=obj.scale, + z=tomographic_bin.z, + dndz=tomographic_bin.dndz, + ia_bias=None, ) # pylint: enable=unexpected-keyword-arg diff --git a/firecrown/metadata_functions/__init__.py b/firecrown/metadata_functions/__init__.py index da020476b..3e9df9cd9 100644 --- a/firecrown/metadata_functions/__init__.py +++ b/firecrown/metadata_functions/__init__.py @@ -19,6 +19,8 @@ extract_all_real_metadata, extract_all_real_metadata_indices, extract_all_tracers_inferred_galaxy_zdists, + extract_all_tracers_projected_fields, + extract_all_tracers_tomographic_bins, extract_window_function, maybe_enforce_window, ) @@ -46,6 +48,9 @@ "make_measurement_dict", "make_measurements_dict", "make_correlation_space", + "extract_all_tracers_tomographic_bins", + "extract_all_tracers_projected_fields", + # Backwards compatibility: expose the legacy name "extract_all_tracers_inferred_galaxy_zdists", "extract_all_measured_types", "extract_all_real_metadata_indices", diff --git a/firecrown/metadata_functions/_combination_utils.py b/firecrown/metadata_functions/_combination_utils.py index cb8570a52..567ec3175 100644 --- a/firecrown/metadata_functions/_combination_utils.py +++ b/firecrown/metadata_functions/_combination_utils.py @@ -7,30 +7,29 @@ - Filtered combinations based on bin pair selectors """ +from collections.abc import Sequence from itertools import chain, product -import numpy as np - import firecrown.metadata_types as mdt -def _validate_list_of_inferred_galaxy_zdists( - inferred_galaxy_zdists: list[mdt.InferredGalaxyZDist], +def _validate_list_of_tomographic_bins( + tomographic_bins: Sequence[mdt.ProjectedField], ) -> None: """Validate that tomographic bin names are unique. - :param inferred_galaxy_zdists: List of tomographic bins to validate. + :param tomographic_bins: List of tomographic bins to validate. :raises ValueError: If any bin names appear more than once in the list. """ bin_names_set = set() # Produce a list of duplicates bin_names = [] - for igz in inferred_galaxy_zdists: - if igz.bin_name in bin_names_set: - bin_names.append(igz.bin_name) + for tomographic_bin in tomographic_bins: + if tomographic_bin.bin_name in bin_names_set: + bin_names.append(tomographic_bin.bin_name) else: - bin_names_set.add(igz.bin_name) + bin_names_set.add(tomographic_bin.bin_name) if bin_names: raise ValueError( @@ -39,7 +38,7 @@ def _validate_list_of_inferred_galaxy_zdists( def make_all_photoz_bin_combinations( - inferred_galaxy_zdists: list[mdt.InferredGalaxyZDist], + tomographic_bins: Sequence[mdt.ProjectedField], ) -> list[mdt.TwoPointXY]: """Create all possible two-point correlation combinations for galaxy bins. @@ -48,25 +47,27 @@ def make_all_photoz_bin_combinations( (same measurement type), only unique pairs are kept to avoid duplicates (e.g., only bin0-bin1, not both bin0-bin1 and bin1-bin0). - :param inferred_galaxy_zdists: List of tomographic redshift bins with their + :param tomographic_bins: List of tomographic redshift bins with their associated measurement types. :return: List of all valid TwoPointXY combinations. - :raises ValueError: If duplicate bin names are found in inferred_galaxy_zdists. + :raises ValueError: If duplicate bin names are found in tomographic_bins. """ - _validate_list_of_inferred_galaxy_zdists(inferred_galaxy_zdists) + _validate_list_of_tomographic_bins(tomographic_bins) expanded = [ - (igz, m) for igz in inferred_galaxy_zdists for m in igz.measurement_list + (tomographic_bin, m) + for tomographic_bin in tomographic_bins + for m in tomographic_bin.measurement_list ] # Create all combinations of the expanded list, keeping only compatible ones # and avoiding duplicates in the case of correlations of the same type all_xy = [ - mdt.TwoPointXY(x=igz1, y=igz2, x_measurement=m1, y_measurement=m2) - for (igz1, m1), (igz2, m2) in product(expanded, repeat=2) + mdt.TwoPointXY(x=bin1, y=bin2, x_measurement=m1, y_measurement=m2) + for (bin1, m1), (bin2, m2) in product(expanded, repeat=2) if mdt.measurement_is_compatible(m1, m2) - and ((m1 != m2) or (igz2.bin_name >= igz1.bin_name)) + and ((m1 != m2) or (bin2.bin_name >= bin1.bin_name)) ] # Reorder expanded to have alphabetical order considering first measurements, then @@ -83,7 +84,7 @@ def make_all_photoz_bin_combinations( def make_all_photoz_bin_combinations_with_cmb( - inferred_galaxy_zdists: list[mdt.InferredGalaxyZDist], + tomographic_bins: list[mdt.TomographicBin], cmb_tracer_name: str = "cmb_convergence", include_cmb_auto: bool = False, ) -> list[mdt.TwoPointXY]: @@ -92,7 +93,7 @@ def make_all_photoz_bin_combinations_with_cmb( This function generates all possible two-point correlations including both galaxy-galaxy auto/cross-correlations and CMB-galaxy cross-correlations. - :param inferred_galaxy_zdists: List of galaxy redshift bins with their associated + :param tomographic_bins: List of galaxy redshift bins with their associated measurement types. :param cmb_tracer_name: Name to assign to the CMB tracer (default: "cmb_convergence"). @@ -100,20 +101,20 @@ def make_all_photoz_bin_combinations_with_cmb( :return: Combined list of galaxy-galaxy and CMB-galaxy correlation combinations. - :raises ValueError: If duplicate bin names are found in inferred_galaxy_zdists. + :raises ValueError: If duplicate bin names are found in tomographic_bins. """ - _validate_list_of_inferred_galaxy_zdists(inferred_galaxy_zdists) + _validate_list_of_tomographic_bins(tomographic_bins) # Get all galaxy-galaxy combinations first - galaxy_combinations = make_all_photoz_bin_combinations(inferred_galaxy_zdists) + galaxy_combinations = make_all_photoz_bin_combinations(tomographic_bins) all_combinations = galaxy_combinations + make_cmb_galaxy_combinations_only( - inferred_galaxy_zdists, cmb_tracer_name, include_cmb_auto + tomographic_bins, cmb_tracer_name, include_cmb_auto ) return all_combinations def make_cmb_galaxy_combinations_only( - inferred_galaxy_zdists: list[mdt.InferredGalaxyZDist], + tomographic_bins: list[mdt.TomographicBin], cmb_tracer_name: str = "cmb_convergence", include_cmb_auto: bool = False, ) -> list[mdt.TwoPointXY]: @@ -123,7 +124,7 @@ def make_cmb_galaxy_combinations_only( measurements, optionally including the CMB auto-correlation. It does NOT include any galaxy-galaxy correlations. - :param inferred_galaxy_zdists: List of galaxy redshift bins with their + :param tomographic_bins: List of galaxy redshift bins with their associated measurement types. :param cmb_tracer_name: Name to assign to the CMB tracer (default: "cmb_convergence"). @@ -132,23 +133,28 @@ def make_cmb_galaxy_combinations_only( :return: List of CMB-galaxy cross-correlation combinations (and optionally CMB auto). - :raises ValueError: If duplicate bin names are found in inferred_galaxy_zdists. + :raises ValueError: If duplicate bin names are found in tomographic_bins. """ - _validate_list_of_inferred_galaxy_zdists(inferred_galaxy_zdists) + _validate_list_of_tomographic_bins(tomographic_bins) # Create a mock mdt.CMB "bin" - cmb_bin = mdt.InferredGalaxyZDist( + cmb_bin = mdt.CMBLensing( bin_name=cmb_tracer_name, - z=np.array([1100.0]), - dndz=np.array([1.0]), + z_lss=1100.0, measurements={mdt.CMB.CONVERGENCE}, type_source=mdt.TypeSource.DEFAULT, ) cmb_galaxy_combinations = [] - for galaxy_bin in inferred_galaxy_zdists: - galaxy_bin_type = list(product([galaxy_bin], list(galaxy_bin.measurements))) - cmb_bin_type = list(product([cmb_bin], list(cmb_bin.measurements))) + x: mdt.ProjectedField + y: mdt.ProjectedField + for galaxy_bin in tomographic_bins: + galaxy_bin_type: list[tuple[mdt.ProjectedField, mdt.Measurement]] = list( + product([galaxy_bin], list(galaxy_bin.measurements)) + ) + cmb_bin_type: list[tuple[mdt.ProjectedField, mdt.Measurement]] = list( + product([cmb_bin], list(cmb_bin.measurements)) + ) for (x, m1), (y, m2) in chain( product(galaxy_bin_type, cmb_bin_type), product(cmb_bin_type, galaxy_bin_type), @@ -174,7 +180,7 @@ def make_cmb_galaxy_combinations_only( def make_binned_two_point_filtered( - inferred_galaxy_zdists: list[mdt.InferredGalaxyZDist], + tomographic_bins: Sequence[mdt.ProjectedField], bin_pair_selector: mdt.BinPairSelector, ) -> list[mdt.TwoPointXY]: """Create two-point correlations filtered by a bin pair selector. @@ -183,21 +189,21 @@ def make_binned_two_point_filtered( the provided selector, keeping only pairs that satisfy the selection criteria (e.g., auto-correlations only, specific measurements, neighboring bins). - :param inferred_galaxy_zdists: List of tomographic redshift bins with their + :param tomographic_bins: List of tomographic redshift bins with their associated measurement types. :param bin_pair_selector: Selector defining which bin pairs to include. :return: List of TwoPointXY combinations that pass the selector's criteria. - :raises ValueError: If duplicate bin names are found in inferred_galaxy_zdists. + :raises ValueError: If duplicate bin names are found in tomographic_bins. Example: # Get only auto-correlations of source measurements selector = AutoNameBinPairSelector() & SourceBinPairSelector() combinations = make_binned_two_point_filtered(bins, selector) """ - _validate_list_of_inferred_galaxy_zdists(inferred_galaxy_zdists) - all_bin_combinations = make_all_photoz_bin_combinations(inferred_galaxy_zdists) + _validate_list_of_tomographic_bins(tomographic_bins) + all_bin_combinations = make_all_photoz_bin_combinations(tomographic_bins) return filter_two_point_combinations(all_bin_combinations, bin_pair_selector) diff --git a/firecrown/metadata_functions/_extraction.py b/firecrown/metadata_functions/_extraction.py index 5eecccdc5..acad6a6e8 100644 --- a/firecrown/metadata_functions/_extraction.py +++ b/firecrown/metadata_functions/_extraction.py @@ -18,10 +18,10 @@ ) -def extract_all_tracers_inferred_galaxy_zdists( +def extract_all_tracers_tomographic_bins( sacc_data: sacc.Sacc, allow_mixed_types: bool = False, -) -> list[mdt.InferredGalaxyZDist]: +) -> list[mdt.TomographicBin]: """Extracts the two-point function metadata from a Sacc object. The Sacc object contains a set of tracers (one-dimensional bins) and data @@ -45,7 +45,7 @@ def extract_all_tracers_inferred_galaxy_zdists( ) return [ - mdt.InferredGalaxyZDist( + mdt.TomographicBin( bin_name=tracer.name, z=tracer.z, dndz=tracer.nz, @@ -56,6 +56,63 @@ def extract_all_tracers_inferred_galaxy_zdists( ] +# Backwards compatibility: expose the legacy name +extract_all_tracers_inferred_galaxy_zdists = extract_all_tracers_tomographic_bins + +_DEFAULT_CMB_Z_LSS = 1100.0 + + +def extract_all_tracers_projected_fields( + sacc_data: sacc.Sacc, + allow_mixed_types: bool = False, +) -> list[mdt.ProjectedField]: + """Extracts the two-point function metadata from a Sacc object. + + Each SACC ``NZTracer`` is returned as a ``TomographicBin`` and each SACC + ``MapTracer`` is returned as a ``CMBLensing``. The ``z_lss`` value for a + ``CMBLensing`` is read from the tracer's ``metadata`` dictionary (key + ``"z_lss"``), falling back to the standard last-scattering redshift + (1100.0) if not present. Any other tracer type is ignored. + + See also: :func:`extract_all_tracers_tomographic_bins`. + """ + tracers: list[sacc.tracers.BaseTracer] = sacc_data.tracers.values() + tracer_types, _ = extract_all_measured_types( + sacc_data, allow_mixed_types=allow_mixed_types + ) + for tracer in tracers: + if isinstance(tracer, (sacc.tracers.NZTracer, sacc.tracers.MapTracer)): + if (tracer.name not in tracer_types) or ( + len(tracer_types[tracer.name]) == 0 + ): + raise ValueError( + f"Tracer {tracer.name} does not have data points " + f"associated with it. Inconsistent SACC object." + ) + + projected_fields: list[mdt.ProjectedField] = [] + for tracer in tracers: + if isinstance(tracer, sacc.tracers.NZTracer): + projected_fields.append( + mdt.TomographicBin( + bin_name=tracer.name, + z=tracer.z, + dndz=tracer.nz, + measurements=tracer_types[tracer.name], + ) + ) + elif isinstance(tracer, sacc.tracers.MapTracer): + projected_fields.append( + mdt.CMBLensing( + bin_name=tracer.name, + z_lss=tracer.metadata.get("z_lss", _DEFAULT_CMB_Z_LSS), + measurements=tracer_types[tracer.name], + ) + ) + + return projected_fields + + def _sacc_convention_warning( tracer1: str, tracer2: str, data_type: str, a: mdt.Measurement, b: mdt.Measurement ) -> str: @@ -476,9 +533,9 @@ def extract_all_harmonic_metadata( :param normalize: If True, normalize the window function weights to sum to 1. :return: List of TwoPointHarmonic objects with metadata and ell values. """ - inferred_galaxy_zdists_dict = { - igz.bin_name: igz - for igz in extract_all_tracers_inferred_galaxy_zdists( + projected_fields_dict = { + projected_field.bin_name: projected_field + for projected_field in extract_all_tracers_projected_fields( sacc_data, allow_mixed_types ) } @@ -490,7 +547,7 @@ def extract_all_harmonic_metadata( tracer_names = cell_index["tracer_names"] dt = cell_index["data_type"] - XY = make_two_point_xy(inferred_galaxy_zdists_dict, tracer_names, dt) + XY = make_two_point_xy(projected_fields_dict, tracer_names, dt) # Apply bin pair selector if provided if bin_pair_selector is not None: @@ -537,9 +594,9 @@ def extract_all_real_metadata( If None, all valid bin pairs are returned. :return: List of TwoPointReal objects with metadata and theta values. """ - inferred_galaxy_zdists_dict = { - igz.bin_name: igz - for igz in extract_all_tracers_inferred_galaxy_zdists( + projected_fields_dict = { + projected_field.bin_name: projected_field + for projected_field in extract_all_tracers_projected_fields( sacc_data, allow_mixed_types ) } @@ -551,7 +608,7 @@ def extract_all_real_metadata( tracer_names = real_index["tracer_names"] dt = real_index["data_type"] - XY = make_two_point_xy(inferred_galaxy_zdists_dict, tracer_names, dt) + XY = make_two_point_xy(projected_fields_dict, tracer_names, dt) # Apply bin pair selector if provided if bin_pair_selector is not None: @@ -582,10 +639,10 @@ def extract_all_photoz_bin_combinations( If None, all valid bin pairs are returned. :return: List of TwoPointXY objects representing valid bin pair combinations. """ - inferred_galaxy_zdists = extract_all_tracers_inferred_galaxy_zdists( + projected_fields = extract_all_tracers_projected_fields( sacc_data, allow_mixed_types ) - bin_combinations = make_all_photoz_bin_combinations(inferred_galaxy_zdists) + bin_combinations = make_all_photoz_bin_combinations(projected_fields) if bin_pair_selector is not None: bin_combinations = [ diff --git a/firecrown/metadata_functions/_matching.py b/firecrown/metadata_functions/_matching.py index 7c76931e5..849000fed 100644 --- a/firecrown/metadata_functions/_matching.py +++ b/firecrown/metadata_functions/_matching.py @@ -1,5 +1,7 @@ """Utilities for matching tracer names and measurements.""" +from typing import Mapping + import firecrown.metadata_types as mdt from firecrown.metadata_functions._type_defs import ( TwoPointHarmonicIndex, @@ -12,8 +14,8 @@ def _make_two_point_xy_error_message( a: mdt.Measurement, b: mdt.Measurement, tracer_names: mdt.TracerNames, - igz1: mdt.InferredGalaxyZDist, - igz2: mdt.InferredGalaxyZDist, + pf1: mdt.ProjectedField, + pf2: mdt.ProjectedField, ) -> str: """Generate a detailed error message for SACC naming convention violations. @@ -21,8 +23,8 @@ def _make_two_point_xy_error_message( :param a: The first expected measurement type. :param b: The second expected measurement type. :param tracer_names: The tracer names that were provided. - :param igz1: The inferred galaxy z distribution for the first tracer. - :param igz2: The inferred galaxy z distribution for the second tracer. + :param pf1: The projected field for the first tracer. + :param pf2: The projected field for the second tracer. :return: A formatted error message explaining the violation. """ return f""" @@ -30,8 +32,8 @@ def _make_two_point_xy_error_message( Data type: {data_type} Expected measurements: ({a}, {b}) - Tracer '{tracer_names[0]}' has measurements: {igz1.measurements} - Tracer '{tracer_names[1]}' has measurements: {igz2.measurements} + Tracer '{tracer_names[0]}' has measurements: {pf1.measurements} + Tracer '{tracer_names[1]}' has measurements: {pf2.measurements} According to the SACC convention, the order of measurement types in the data type string must match the order of tracers. The measurement type '{a}' should be associated @@ -44,7 +46,7 @@ def _make_two_point_xy_error_message( def make_two_point_xy( - inferred_galaxy_zdists_dict: dict[str, mdt.InferredGalaxyZDist], + projected_fields_dict: Mapping[str, mdt.ProjectedField], tracer_names: mdt.TracerNames, data_type: str, ) -> mdt.TwoPointXY: @@ -53,8 +55,7 @@ def make_two_point_xy( The mdt.TwoPointXY object is built from the inferred galaxy z distributions, the data type, and the tracer names. - :param inferred_galaxy_zdists_dict: a dictionary of inferred galaxy z - distributions. + :param projected_fields_dict: a dictionary of projected fields. :param tracer_names: a tuple of tracer names. :param data_type: the data type. @@ -65,25 +66,25 @@ def make_two_point_xy( a, b = mdt.MEASURED_TYPE_STRING_MAP[data_type] try: - igz1 = inferred_galaxy_zdists_dict[tracer_names[0]] - igz2 = inferred_galaxy_zdists_dict[tracer_names[1]] + pf1 = projected_fields_dict[tracer_names[0]] + pf2 = projected_fields_dict[tracer_names[1]] except KeyError as e: raise ValueError( f"Tracer '{e.args[0]}' not found in inferred galaxy z distributions." ) from e - if (a not in igz1.measurements) or (b not in igz2.measurements): - if (a in igz2.measurements) and (b in igz1.measurements): + if (a not in pf1.measurements) or (b not in pf2.measurements): + if (a in pf2.measurements) and (b in pf1.measurements): # swap the order of the tracers, this is a temporary fix following # _should_swap_tracers_for_convention in _extraction.py - igz1, igz2 = igz2, igz1 + pf1, pf2 = pf2, pf1 else: raise ValueError( _make_two_point_xy_error_message( - data_type, a, b, tracer_names, igz1, igz2 + data_type, a, b, tracer_names, pf1, pf2 ) ) - return mdt.TwoPointXY(x=igz1, y=igz2, x_measurement=a, y_measurement=b) + return mdt.TwoPointXY(x=pf1, y=pf2, x_measurement=a, y_measurement=b) def measurements_from_index( diff --git a/firecrown/metadata_types/__init__.py b/firecrown/metadata_types/__init__.py index 281ac084f..1e1f52896 100644 --- a/firecrown/metadata_types/__init__.py +++ b/firecrown/metadata_types/__init__.py @@ -12,7 +12,14 @@ measurement_supports_real, measurements_types, ) -from firecrown.metadata_types._inferred_galaxy_zdist import InferredGalaxyZDist +from firecrown.metadata_types._two_point_tracers import ( + TomographicBin, + ProjectedField, + CMBLensing, + InferredGalaxyZDist, +) + +# Backwards compatibility: expose the legacy name from firecrown.metadata_types._measurements import ( ALL_MEASUREMENT_TYPES, ALL_MEASUREMENTS, @@ -49,8 +56,8 @@ SourceBinPairSelector, SourceLensBinPairSelector, ThreeTwoBinPairSelector, - TomographicBinPair, TypeSourceBinPairSelector, + ProjectedFieldPair, register_bin_pair_selector, ) from firecrown.metadata_types._sacc_type_string import MEASURED_TYPE_STRING_MAP @@ -86,8 +93,11 @@ "TracerNames", "TRACER_NAMES_TOTAL", "TypeSource", - # Inferred galaxy zdist + # Tomographic bin / legacy name + "TomographicBin", "InferredGalaxyZDist", + "ProjectedField", + "CMBLensing", # Two-point types "TwoPointXY", "TwoPointHarmonic", @@ -126,7 +136,7 @@ "SourceLensBinPairSelector", "ThreeTwoBinPairSelector", "TypeSourceBinPairSelector", - "TomographicBinPair", + "ProjectedFieldPair", "MeasurementPair", "register_bin_pair_selector", ] diff --git a/firecrown/metadata_types/_inferred_galaxy_zdist.py b/firecrown/metadata_types/_inferred_galaxy_zdist.py deleted file mode 100644 index 249a117b3..000000000 --- a/firecrown/metadata_types/_inferred_galaxy_zdist.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Inferred galaxy redshift distribution types.""" - -from dataclasses import dataclass - -import numpy as np - -from firecrown.metadata_types._measurements import ALL_MEASUREMENT_TYPES, Measurement -from firecrown.metadata_types._utils import TypeSource -from firecrown.utils import YAMLSerializable - - -@dataclass(frozen=True, kw_only=True) -class InferredGalaxyZDist(YAMLSerializable): - """The class used to store the redshift resolution data for a sacc file. - - The sacc file is a complicated set of tracers (bins) and surveys. This class is - used to store the redshift resolution data for a single photometric bin. - """ - - bin_name: str - z: np.ndarray - dndz: np.ndarray - measurements: set[Measurement] - type_source: TypeSource = TypeSource.DEFAULT - - def __post_init__(self) -> None: - """Validate the redshift resolution data. - - - Make sure the z and dndz arrays have the same shape; - - The measurement must be of type Measurement. - - The bin_name should not be empty. - """ - if self.z.shape != self.dndz.shape: - raise ValueError("The z and dndz arrays should have the same shape.") - - for measurement in self.measurements: - if not isinstance(measurement, ALL_MEASUREMENT_TYPES): - raise ValueError("The measurement should be a Measurement.") - - if self.bin_name == "": - raise ValueError("The bin_name should not be empty.") - - def __eq__(self, other): - """Equality test for InferredGalaxyZDist. - - Two InferredGalaxyZDist are equal if they have equal bin_name, z, dndz, and - measurement. - """ - assert isinstance(other, InferredGalaxyZDist) - return ( - self.bin_name == other.bin_name - and np.array_equal(self.z, other.z) - and np.array_equal(self.dndz, other.dndz) - and self.measurements == other.measurements - ) - - @property - def measurement_list(self) -> list[Measurement]: - """Get the measurements as a sorted list.""" - return sorted(self.measurements) diff --git a/firecrown/metadata_types/_pair_selector.py b/firecrown/metadata_types/_pair_selector.py index 6d9a6ec34..4996c9947 100644 --- a/firecrown/metadata_types/_pair_selector.py +++ b/firecrown/metadata_types/_pair_selector.py @@ -29,7 +29,7 @@ ) from pydantic_core import PydanticUndefined, core_schema -from firecrown.metadata_types._inferred_galaxy_zdist import InferredGalaxyZDist +from firecrown.metadata_types._two_point_tracers import ProjectedField from firecrown.metadata_types._measurements import ( GALAXY_LENS_TYPES, GALAXY_SOURCE_TYPES, @@ -67,7 +67,7 @@ def register_bin_pair_selector(cls: type["BinPairSelector"]) -> type["BinPairSel # Type aliases for clarity -TomographicBinPair = tuple[InferredGalaxyZDist, InferredGalaxyZDist] +ProjectedFieldPair = tuple[ProjectedField, ProjectedField] """A pair of tomographic bin distributions to be correlated.""" MeasurementPair = tuple[Measurement, Measurement] @@ -77,7 +77,7 @@ def register_bin_pair_selector(cls: type["BinPairSelector"]) -> type["BinPairSel class BinPairSelector(BaseModel): """Base class for filtering pairs of tomographic bins in two-point measurements. - A BinPairSelector determines which pairs of `InferredGalaxyZDist` bins should be + A BinPairSelector determines which pairs of `ProjectedField` bins should be included when constructing `TwoPointXY` objects. Concrete implementations define specific selection criteria (e.g., auto-correlations only, specific bin names, measurement types, etc.). @@ -91,10 +91,10 @@ class BinPairSelector(BaseModel): kind: str @abstractmethod - def keep(self, zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if the pair should be kept. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param m: Pair of Measurement objects. :return: True if the pair should be kept, False otherwise. @@ -177,16 +177,16 @@ class AndBinPairSelector(BinPairSelector): kind: str = "and" pair_selectors: list[SerializeAsAny[BinPairSelector]] - def keep(self, zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if all of the bin pair selectors pass. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param m: Pair of Measurement objects. :return: True if all bin pair selectors pass, False otherwise. """ return all( - pair_selector.keep(zdist, m) for pair_selector in self.pair_selectors + pair_selector.keep(field_pair, m) for pair_selector in self.pair_selectors ) def model_post_init(self, _, /) -> None: @@ -215,16 +215,16 @@ class OrBinPairSelector(BinPairSelector): kind: str = "or" pair_selectors: list[SerializeAsAny[BinPairSelector]] - def keep(self, zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if any of the bin pair selectors pass. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param m: Pair of Measurement objects. :return: True if any bin pair selector passes, False otherwise. """ return any( - pair_selector.keep(zdist, m) for pair_selector in self.pair_selectors + pair_selector.keep(field_pair, m) for pair_selector in self.pair_selectors ) def model_post_init(self, _, /) -> None: @@ -253,15 +253,15 @@ class NotBinPairSelector(BinPairSelector): kind: str = "not" pair_selector: SerializeAsAny[BinPairSelector] - def keep(self, zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return the negation of the contained pair selector's result. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param m: Pair of Measurement objects. :return: True if the contained pair selector returns False, False otherwise. """ - return not self.pair_selector.keep(zdist, m) + return not self.pair_selector.keep(field_pair, m) class BadSelector(BinPairSelector): @@ -269,7 +269,7 @@ class BadSelector(BinPairSelector): kind: str = "bad-selector" - def keep(self, _zdist: TomographicBinPair, _m: MeasurementPair) -> bool: + def keep(self, _field_pair: ProjectedFieldPair, _m: MeasurementPair) -> bool: """Raise NotImplementedError always. :raise NotImplementedError: Always raised since this selector should not be @@ -283,10 +283,10 @@ class CompositeSelector(BinPairSelector): _impl: BinPairSelector = BadSelector() - def keep(self, zdist: TomographicBinPair, m: MeasurementPair): + def keep(self, field_pair: ProjectedFieldPair, m: MeasurementPair): """Delegate to the underlying selector implementation.""" assert isinstance(self._impl, BinPairSelector) - return self._impl.keep(zdist, m) + return self._impl.keep(field_pair, m) @register_bin_pair_selector @@ -327,18 +327,18 @@ def validate_names(cls, value: list[list[str]]) -> list[tuple[str, str]]: assert len(names) == 2 return [(names[0], names[1]) for names in value] - def keep(self, zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if the bin name pair matches any configured pair. Note: Matching is currently order-dependent. To achieve symmetric matching, include both (name1, name2) and (name2, name1) in the names list. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param m: Pair of Measurement objects (unused). :return: True if the bin name pair matches any configured name pair. """ - return (zdist[0].bin_name, zdist[1].bin_name) in self.names + return (field_pair[0].bin_name, field_pair[1].bin_name) in self.names @register_bin_pair_selector @@ -351,15 +351,15 @@ class AutoNameBinPairSelector(BinPairSelector): kind: str = "auto-name" - def keep(self, zdist: TomographicBinPair, _m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, _m: MeasurementPair) -> bool: """Return True if both bins have the same bin name. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param _m: Pair of Measurement objects (unused). :return: True if both bins have the same name, False otherwise. """ - return zdist[0].bin_name == zdist[1].bin_name + return field_pair[0].bin_name == field_pair[1].bin_name @register_bin_pair_selector @@ -386,10 +386,10 @@ class AutoMeasurementBinPairSelector(BinPairSelector): kind: str = "auto-measurement" - def keep(self, _zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, _field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if both measurements are the same. - :param _zdist: Pair of InferredGalaxyZDist objects (unused). + :param _field_pair: Pair of TomographicBin objects (unused). :param m: Pair of Measurement objects. :return: True if both measurements are identical, False otherwise. @@ -457,10 +457,10 @@ class LeftMeasurementBinPairSelector(BinPairSelector): kind: str = "left-measurement" measurement_set: set[Measurement] - def keep(self, _zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, _field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if the left measurement is in the configured set. - :param _zdist: Pair of InferredGalaxyZDist objects (unused). + :param _field_pair: Pair of TomographicBin objects (unused). :param m: Pair of Measurement objects. :return: True if the left measurement is in the set, False otherwise. @@ -483,10 +483,10 @@ class RightMeasurementBinPairSelector(BinPairSelector): kind: str = "right-measurement" measurement_set: set[Measurement] - def keep(self, _zdist: TomographicBinPair, m: MeasurementPair) -> bool: + def keep(self, _field_pair: ProjectedFieldPair, m: MeasurementPair) -> bool: """Return True if the right measurement is in the configured set. - :param _zdist: Pair of InferredGalaxyZDist objects (unused). + :param _field_pair: Pair of TomographicBin objects (unused). :param m: Pair of Measurement objects. :return: True if the right measurement is in the set, False otherwise. @@ -597,14 +597,14 @@ class NameDiffBinPairSelector(BinPairSelector): same_name_prefix: bool = True neighbors_diff: int | list[int] = 1 - def keep(self, zdist: TomographicBinPair, _m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, _m: MeasurementPair) -> bool: """Return True if bin name indices differ by an allowed amount. Both bin names must match the pattern . The numeric parts are extracted and their difference is checked against the allowed values. If same_name_prefix is set, the text parts must also satisfy the constraint. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param _m: Pair of Measurement objects (unused). :return: True if bins are neighbors, False otherwise. @@ -616,8 +616,8 @@ def keep(self, zdist: TomographicBinPair, _m: MeasurementPair) -> bool: else self.neighbors_diff ) if not ( - (match1 := pattern.match(zdist[0].bin_name)) - and (match2 := pattern.match(zdist[1].bin_name)) + (match1 := pattern.match(field_pair[0].bin_name)) + and (match2 := pattern.match(field_pair[1].bin_name)) ): return False if self.same_name_prefix and (match1["text"] != match2["text"]): @@ -768,14 +768,14 @@ class TypeSourceBinPairSelector(BinPairSelector): kind: str = "type-source" type_source: TypeSource - def keep(self, zdist: TomographicBinPair, _m: MeasurementPair) -> bool: + def keep(self, field_pair: ProjectedFieldPair, _m: MeasurementPair) -> bool: """Return True if both bins have the same matching type-source. - :param zdist: Pair of InferredGalaxyZDist objects. + :param field_pair: Pair of TomographicBin objects. :param _m: Pair of Measurement objects (unused). :return: True if both bins have matching type-sources, False otherwise. """ - return (zdist[0].type_source == zdist[1].type_source) and ( - self.type_source == zdist[0].type_source + return (field_pair[0].type_source == field_pair[1].type_source) and ( + self.type_source == field_pair[0].type_source ) diff --git a/firecrown/metadata_types/_two_point_tracers.py b/firecrown/metadata_types/_two_point_tracers.py new file mode 100644 index 000000000..bfb34b2e3 --- /dev/null +++ b/firecrown/metadata_types/_two_point_tracers.py @@ -0,0 +1,183 @@ +"""Tomographic bin / tracer types. + +This module defines a Protocol for generic tracers and a concrete +implementation previously known as ``TomographicBin``. The concrete +class has been renamed to ``TomographicBin``; an alias ``TomographicBin`` +is provided for backwards compatibility. +""" + +from dataclasses import dataclass, field +from typing import Protocol, runtime_checkable + +import numpy as np + +from firecrown.metadata_types._measurements import Galaxies, CMB, Measurement +from firecrown.metadata_types._utils import TypeSource +from firecrown.utils import YAMLSerializable + + +@runtime_checkable +class ProjectedField(Protocol): + """Protocol describing the minimal tracer interface used across Firecrown. + + Implementations must provide these attributes so that code can operate on + different kinds of tracers (not only galaxy redshift distributions). + + The protocol is intentionally defined in terms of instance attributes, + which makes it directly compatible with frozen dataclasses such as + ``TomographicBin`` and other simple data containers. + + :var bin_name: String identifier for the tracer or tomographic bin. + :var measurements: Set of Measurement values supported by the tracer. + :var type_source: TypeSource describing how the tracer was obtained. + """ + + @property + def bin_name(self) -> str: # pragma: no cover - Protocol stub + """The name of the tracer or tomographic bin.""" + + @property + def measurements(self) -> set[Measurement]: # pragma: no cover - Protocol stub + """The set of Measurement types supported by the tracer.""" + + @property + def measurement_list(self) -> list[Measurement]: # pragma: no cover - Protocol stub + """The measurements supported by the tracer, as a sorted list.""" + + @property + def type_source(self) -> TypeSource: # pragma: no cover - Protocol stub + """The source of the tracer type information.""" + + +@dataclass(frozen=True, kw_only=True) +class TomographicBin(YAMLSerializable): + """Concrete tomographic bin holding redshift distribution arrays. + + This class is the renamed replacement for the legacy + ``TomographicBin`` type. It is intentionally frozen and lightweight. + """ + + bin_name: str + z: np.ndarray + dndz: np.ndarray + measurements: set[Measurement] + type_source: TypeSource = TypeSource.DEFAULT + + def __post_init__(self) -> None: + """Validate the redshift resolution data. + + - Make sure the z and dndz arrays have the same shape; + - The measurement entries must be members of Measurement enum; + - The bin_name should not be empty. + """ + if self.z.shape != self.dndz.shape: + raise ValueError("The z and dndz arrays should have the same shape.") + + for measurement in self.measurements: + if not isinstance(measurement, Galaxies): + raise ValueError("The measurement should be a Galaxies Measurement.") + + if self.bin_name == "": + raise ValueError("The bin_name should not be empty.") + + def __eq__(self, other: object) -> bool: + """Equality test for TomographicBin. + + Two TomographicBin objects are equal if they have equal bin_name, z, + dndz, and measurements. + + If ``other`` is another concrete ``TomographicBin`` instance we perform + the full array and set comparisons. If ``other`` implements the + :class:`Tracer` protocol but is not a ``TomographicBin``, return False + (they are distinct implementations). For unrelated types return + ``NotImplemented`` so Python can try the reflected operation. + """ + if isinstance(other, TomographicBin): + return ( + self.bin_name == other.bin_name + and np.array_equal(self.z, other.z) + and np.array_equal(self.dndz, other.dndz) + and self.measurements == other.measurements + ) + + # Another object that satisfies the Tracer protocol: intentionally + # considered not equal to this concrete TomographicBin implementation. + if isinstance(other, ProjectedField): + return False + + # Allow Python to attempt other.__eq__(self) + return NotImplemented + + @property + def measurement_list(self) -> list[Measurement]: + """Get the measurements as a sorted list.""" + return sorted(self.measurements) + # Exact same concrete type: compare contents + + +# Backwards compatibility: keep the old name available +InferredGalaxyZDist = TomographicBin + + +@dataclass(frozen=True, kw_only=True) +class CMBLensing(YAMLSerializable): + """Tracer type describing CMB lensing. + + This minimal tracer exposes the same public interface as other tracers + (``bin_name``, ``measurements``, ``type_source``) and additionally + stores the redshift of the last-scattering surface as ``z_lss``. + + The only measurement supported by this tracer by default is + :data:`firecrown.metadata_types._measurements.CMB.CONVERGENCE`. + """ + + bin_name: str + z_lss: float + measurements: set[Measurement] = field(default_factory=lambda: {CMB.CONVERGENCE}) + type_source: TypeSource = TypeSource.DEFAULT + + def __post_init__(self) -> None: + """Basic validation for the CMB lensing tracer. + + - Ensure bin_name is not empty; + - Ensure z_lss is a finite non-negative number; and + - Ensure measurements contain valid Measurement members. + """ + if self.bin_name == "": + raise ValueError("The bin_name should not be empty.") + + if not isinstance(self.z_lss, (int, float)) or not np.isfinite(self.z_lss): + raise ValueError("z_lss must be a finite float value.") + + if self.z_lss <= 0: + raise ValueError("z_lss must be positive.") + + for measurement in self.measurements: + if not isinstance(measurement, CMB): + raise ValueError("The measurement should be a CMB Measurement.") + + def __eq__(self, other: object) -> bool: + """Equality semantics for CMBLensing. + + Two CMBLensing instances are equal if they have the same bin_name, + z_lss, measurements and type_source. If ``other`` implements the + Tracer protocol but is not a CMBLensing instance, we return False. + For unrelated types, return NotImplemented. + """ + if isinstance(other, CMBLensing): + return ( + self.bin_name == other.bin_name + and self.z_lss == other.z_lss + and self.measurements == other.measurements + and self.type_source == other.type_source + ) + + if isinstance(other, ProjectedField): + return False + + return NotImplemented + + @property + def measurement_list(self) -> list[Measurement]: + """Get the measurements as a sorted list.""" + return sorted(self.measurements) diff --git a/firecrown/metadata_types/_two_point_types.py b/firecrown/metadata_types/_two_point_types.py index 3702b9968..2072b57d1 100644 --- a/firecrown/metadata_types/_two_point_types.py +++ b/firecrown/metadata_types/_two_point_types.py @@ -9,7 +9,7 @@ from pydantic_core import core_schema from firecrown.metadata_types._compatibility import measurement_is_compatible -from firecrown.metadata_types._inferred_galaxy_zdist import InferredGalaxyZDist +from firecrown.metadata_types._two_point_tracers import ProjectedField from firecrown.metadata_types._measurements import ( HARMONIC_ONLY_MEASUREMENTS, REAL_ONLY_MEASUREMENTS, @@ -43,8 +43,10 @@ class TwoPointXY(YAMLSerializable): the Measurement enum (e.g., for Galaxies: shape measurements before counts). """ - x: InferredGalaxyZDist - y: InferredGalaxyZDist + # Use the generic ProjectedField protocol so other fields implementations can be + # used + x: ProjectedField + y: ProjectedField x_measurement: Measurement y_measurement: Measurement diff --git a/pytest.ini b/pytest.ini index afd647582..acc5590fa 100644 --- a/pytest.ini +++ b/pytest.ini @@ -23,3 +23,4 @@ filterwarnings = ignore:.*`trapz` is deprecated.*:DeprecationWarning:fastpt.* ignore:.*Empty index selected.*:UserWarning:sacc.* ignore:.*Conversion of an array with ndim > 0 to a scalar is deprecated.*:DeprecationWarning:scipy.* + ignore:.*Attribute \`SACCNAME\` of type \ cannot be written to HDF5 files.* diff --git a/tests/app/examples/test_cmb_cross.py b/tests/app/examples/test_cmb_cross.py new file mode 100644 index 000000000..9e12dca62 --- /dev/null +++ b/tests/app/examples/test_cmb_cross.py @@ -0,0 +1,297 @@ +"""Unit tests for firecrown.app.examples._cmb_cross module. + +Tests ExampleCMBCross example generator and the resulting likelihood, built +through Firecrown's YAML-driven TwoPointFactory system. +""" + +from pathlib import Path +from unittest.mock import patch + +import sacc + +from firecrown.app.analysis import ( + FrameworkCosmology, + Frameworks, +) +from firecrown.app.examples._cmb_cross import ( + BUILD_LIKELIHOOD_FACTORY, + ExampleCMBCross, +) +from firecrown.likelihood import ConstGaussian, NamedParameters +from firecrown.likelihood.factories import build_two_point_likelihood +from firecrown.metadata_types import CMBLensing, TomographicBin +from firecrown.modeling_tools import ModelingTools + + +class TestExampleCMBCross: + """Tests for ExampleCMBCross class.""" + + def test_class_attributes(self) -> None: + """Test that class has required attributes.""" + assert hasattr(ExampleCMBCross, "description") + assert "CMB" in ExampleCMBCross.description + + def test_default_parameters(self, tmp_path: Path) -> None: + """Test that default parameters are set correctly.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + target_framework=Frameworks.COSMOSIS, + ) + + assert builder.prefix == "cmb_cross" + assert builder.seed == 42 + assert builder.n_bins == 2 + assert builder.z_max == 2.0 + assert builder.z_source == 1100.0 + assert builder.include_cmb_auto is True + + def test_custom_parameters(self, tmp_path: Path) -> None: + """Test that custom parameters are applied.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="custom_cmb", + seed=123, + n_bins=3, + z_max=3.0, + z_source=1090.0, + include_cmb_auto=False, + target_framework=Frameworks.COBAYA, + ) + + assert builder.prefix == "custom_cmb" + assert builder.seed == 123 + assert builder.n_bins == 3 + assert builder.z_max == 3.0 + assert builder.z_source == 1090.0 + assert builder.include_cmb_auto is False + + def test_generate_sacc_returns_path(self, tmp_path: Path) -> None: + """Test that generate_sacc returns expected path.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_ell_points=5, + n_z_points=100, + target_framework=Frameworks.COSMOSIS, + ) + + result = builder.generate_sacc(tmp_path) + assert result == tmp_path / "test_cmb.sacc" + + def test_generate_sacc_tracers_and_data_types(self, tmp_path: Path) -> None: + """Test that generate_sacc produces the expected tracers and data types.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=2, + n_ell_points=5, + n_z_points=100, + target_framework=Frameworks.COSMOSIS, + ) + + sacc_file = builder.generate_sacc(tmp_path) + sacc_data = sacc.Sacc.load_fits(str(sacc_file)) + + assert set(sacc_data.tracers) == {"trc0", "trc1", "cmb_convergence"} + assert isinstance(sacc_data.tracers["trc0"], sacc.tracers.NZTracer) + assert isinstance(sacc_data.tracers["cmb_convergence"], sacc.tracers.MapTracer) + assert sacc_data.tracers["cmb_convergence"].metadata["z_lss"] == 1100.0 + + data_types = set(sacc_data.get_data_types()) + assert data_types == { + "galaxy_shear_cl_ee", + "cmbGalaxy_convergenceShear_cl_e", + "cmb_convergence_cl", + } + + def test_generate_sacc_without_cmb_auto(self, tmp_path: Path) -> None: + """Test that include_cmb_auto=False omits the CMB auto-correlation.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=2, + n_ell_points=5, + n_z_points=100, + include_cmb_auto=False, + target_framework=Frameworks.COSMOSIS, + ) + + sacc_file = builder.generate_sacc(tmp_path) + sacc_data = sacc.Sacc.load_fits(str(sacc_file)) + + data_types = set(sacc_data.get_data_types()) + assert "cmb_convergence_cl" not in data_types + + def test_generate_sacc_extracts_as_projected_fields(self, tmp_path: Path) -> None: + """The generated SACC file must round-trip through the generalized + extraction function as a mix of TomographicBin and CMBLensing.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=2, + n_ell_points=5, + n_z_points=100, + target_framework=Frameworks.COSMOSIS, + ) + + sacc_file = builder.generate_sacc(tmp_path) + sacc_data = sacc.Sacc.load_fits(str(sacc_file)) + + # pylint: disable-next=import-outside-toplevel + from firecrown.metadata_functions import extract_all_tracers_projected_fields + + all_fields = extract_all_tracers_projected_fields(sacc_data) + fields_by_name = {f.bin_name: f for f in all_fields} + + assert isinstance(fields_by_name["trc0"], TomographicBin) + assert isinstance(fields_by_name["cmb_convergence"], CMBLensing) + + def test_generate_factory_writes_yaml_config(self, tmp_path: Path) -> None: + """Test that generate_factory writes a YAML likelihood configuration + and returns the YAML-driven factory function reference.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + z_source=1090.0, + target_framework=Frameworks.COSMOSIS, + ) + + sacc_path = tmp_path / "test_cmb.sacc" + result = builder.generate_factory(tmp_path, sacc_path) + + assert result == BUILD_LIKELIHOOD_FACTORY + assert result == "firecrown.likelihood.factories.build_two_point_likelihood" + + yaml_file = tmp_path / "test_cmb_experiment.yaml" + assert yaml_file.is_file() + content = yaml_file.read_text() + assert "cmb_factories" in content + assert "weak_lensing_factories" in content + assert "z_source: 1090.0" in content + assert "correlation_space: harmonic" in content + + def test_get_build_parameters(self, tmp_path: Path) -> None: + """Test that get_build_parameters returns the likelihood_config path.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + target_framework=Frameworks.COSMOSIS, + ) + + sacc_path = tmp_path / "test_cmb.sacc" + params = builder.get_build_parameters(sacc_path) + + assert isinstance(params, NamedParameters) + param_dict = params.convert_to_basic_dict() + assert param_dict["likelihood_config"] == str( + (tmp_path / "test_cmb_experiment.yaml").absolute() + ) + + def test_get_models_returns_delta_z_params(self, tmp_path: Path) -> None: + """Test that get_models returns delta_z parameters for photo-z shifts.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=2, + target_framework=Frameworks.COSMOSIS, + ) + + models = builder.get_models() + + assert isinstance(models, list) + assert len(models) == 1 + param_names = {p.name for p in models[0].parameters} + assert any("delta_z" in name for name in param_names) + + def test_required_cosmology(self, tmp_path: Path) -> None: + """Test that required_cosmology returns NONLINEAR.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + target_framework=Frameworks.COSMOSIS, + ) + + result = builder.required_cosmology() + assert result == FrameworkCosmology.NONLINEAR + + def test_get_options_desc(self, tmp_path: Path) -> None: + """Test that get_options_desc returns configuration info.""" + with patch.object(ExampleCMBCross, "_proceed_generation"): + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=3, + seed=100, + target_framework=Frameworks.COSMOSIS, + ) + + options = builder.get_options_desc() + + assert isinstance(options, list) + assert len(options) > 0 + option_names = [name for name, _ in options] + assert any("bin" in name.lower() for name in option_names) + assert any("cmb" in name.lower() for name in option_names) + + def test_build_likelihood_execution(self, tmp_path: Path) -> None: + """Test that the auto-discovered likelihood is built successfully, + picking up both the galaxy and CMB lensing tracers via the + WeakLensingFactory/CMBConvergenceFactory dispatch.""" + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=2, + seed=42, + n_ell_points=5, + n_z_points=100, + target_framework=Frameworks.COSMOSIS, + ) + + sacc_file = builder.generate_sacc(tmp_path) + factory_ref = builder.generate_factory(tmp_path, sacc_file) + assert factory_ref == BUILD_LIKELIHOOD_FACTORY + params = builder.get_build_parameters(sacc_file) + + likelihood, modeling_tools = build_two_point_likelihood(params) + + assert isinstance(likelihood, ConstGaussian) + assert isinstance(modeling_tools, ModelingTools) + + # n_bins galaxy auto/cross + n_bins CMB-galaxy cross + 1 CMB auto + expected_n_stats = 2 * (2 + 1) // 2 + 2 + 1 + assert len(likelihood.statistics) == expected_n_stats + + def test_build_likelihood_without_cmb_auto(self, tmp_path: Path) -> None: + """Test that the auto-discovered likelihood correctly omits the CMB + auto-correlation when it is not present in the SACC file.""" + builder = ExampleCMBCross( + output_path=tmp_path, + prefix="test_cmb", + n_bins=2, + seed=42, + n_ell_points=5, + n_z_points=100, + include_cmb_auto=False, + target_framework=Frameworks.COSMOSIS, + ) + + sacc_file = builder.generate_sacc(tmp_path) + builder.generate_factory(tmp_path, sacc_file) + params = builder.get_build_parameters(sacc_file) + + likelihood, _ = build_two_point_likelihood(params) + assert isinstance(likelihood, ConstGaussian) + + # n_bins galaxy auto/cross + n_bins CMB-galaxy cross, no CMB auto + expected_n_stats = 2 * (2 + 1) // 2 + 2 + assert len(likelihood.statistics) == expected_n_stats diff --git a/tests/app/sacc/test_utils.py b/tests/app/sacc/test_utils.py index 463776320..b273d3075 100644 --- a/tests/app/sacc/test_utils.py +++ b/tests/app/sacc/test_utils.py @@ -11,7 +11,7 @@ @pytest.fixture(name="mock_tracer") -def fixture_mock_tracer() -> mdt.InferredGalaxyZDist: +def fixture_mock_tracer() -> mdt.TomographicBin: """Create mock tracer with Gaussian distribution.""" z = np.linspace(0.0, 2.0, 100) mean = 1.0 @@ -19,7 +19,7 @@ def fixture_mock_tracer() -> mdt.InferredGalaxyZDist: dndz = np.exp(-0.5 * ((z - mean) / sigma) ** 2) dndz /= np.trapezoid(dndz, z) - return mdt.InferredGalaxyZDist( + return mdt.TomographicBin( bin_name="bin0", z=z, dndz=dndz, @@ -31,9 +31,7 @@ def fixture_mock_tracer() -> mdt.InferredGalaxyZDist: class TestMeanStdTracer: """Tests for mean_std_tracer function.""" - def test_mean_std_tracer_gaussian( - self, mock_tracer: mdt.InferredGalaxyZDist - ) -> None: + def test_mean_std_tracer_gaussian(self, mock_tracer: mdt.TomographicBin) -> None: """Test mean_std_tracer with Gaussian distribution.""" mean, std = mean_std_tracer(mock_tracer) @@ -48,7 +46,7 @@ def test_mean_std_tracer_uniform(self) -> None: dndz = np.ones_like(z) dndz /= np.trapezoid(dndz, z) - tracer = mdt.InferredGalaxyZDist( + tracer = mdt.TomographicBin( bin_name="uniform", z=z, dndz=dndz, @@ -69,7 +67,7 @@ def test_mean_std_tracer_delta_function(self) -> None: dndz[center_idx] = 1.0 dndz /= np.trapezoid(dndz, z) - tracer = mdt.InferredGalaxyZDist( + tracer = mdt.TomographicBin( bin_name="delta", z=z, dndz=dndz, @@ -91,7 +89,7 @@ def test_mean_std_tracer_skewed_distribution(self) -> None: dndz = np.exp(-0.5 * ((z - 1.3) / 0.3) ** 2) dndz /= np.trapezoid(dndz, z) - tracer = mdt.InferredGalaxyZDist( + tracer = mdt.TomographicBin( bin_name="skewed", z=z, dndz=dndz, diff --git a/tests/app/sacc/test_view.py b/tests/app/sacc/test_view.py index a72889afc..2fd495e65 100644 --- a/tests/app/sacc/test_view.py +++ b/tests/app/sacc/test_view.py @@ -376,6 +376,33 @@ def test_view_multiple_tracers_sorted(self, tmp_path: Path) -> None: assert view.all_tracers[1].bin_name == "m_middle" assert view.all_tracers[2].bin_name == "z_last" + def test_view_shows_cmb_lensing_tracer(self, tmp_path: Path) -> None: + """Test that View displays a CMB MapTracer without crashing. + + CMBLensing tracers have no z/dndz arrays, so the tracer table must + display them differently from TomographicBin (NZTracer) rows. + """ + s = sacc.Sacc() + s.add_tracer("Map", "cmb_convergence", 0, [10, 100, 1000], [1.0, 1.0, 1.0]) + ells = np.array([10, 20, 30]) + for ell in ells: + s.add_data_point( + "cmb_convergence_cl", + ("cmb_convergence", "cmb_convergence"), + 1.0, + ell=int(ell), + ) + cov = np.eye(len(ells)) * 0.1 + s.add_covariance(cov) + + sacc_path = tmp_path / "test_cmb.sacc" + s.save_fits(str(sacc_path)) + + view = View(sacc_file=sacc_path, plot_covariance=False) + + assert len(view.all_tracers) == 1 + assert view.all_tracers[0].bin_name == "cmb_convergence" + def test_view_with_plot_covariance_true(self, sacc_file: Path) -> None: """Test View with plot_covariance=True (line 118).""" with patch("matplotlib.pyplot.show"): diff --git a/tests/conftest.py b/tests/conftest.py index ad3422328..3c064922a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ import ast import sys from itertools import product +from typing import assert_never import numpy as np import numpy.typing as npt @@ -26,9 +27,10 @@ ALL_MEASUREMENTS, CMB, Clusters, + CMBLensing, Galaxies, - InferredGalaxyZDist, Measurement, + TomographicBin, TracerNames, TwoPointHarmonic, TwoPointReal, @@ -226,19 +228,19 @@ def fixture_tools_with_vanilla_cosmology() -> ModelingTools: @pytest.fixture(name="harmonic_bin_1") -def fixture_harmonic_bin_1(request) -> InferredGalaxyZDist: - """Generate an InferredGalaxyZDist object with 5 bins.""" +def fixture_harmonic_bin_1(request) -> TomographicBin: + """Generate an TomographicBin object with 5 bins.""" return request.param -def _make_harmonic_bin(name: str, z_mean: float, m: Measurement) -> InferredGalaxyZDist: - """Generate an InferredGalaxyZDist object with 5 bins.""" +def _make_harmonic_bin(name: str, z_mean: float, m: Measurement) -> TomographicBin: + """Generate an TomographicBin object with 5 bins.""" z = np.linspace(0.0, 1.0, 256) # Necessary to match the default lensing kernel size z_sigma = 0.05 dndz = np.exp(-0.5 * (z - z_mean) ** 2 / z_sigma**2) / ( np.sqrt(2 * np.pi) * z_sigma ) - x = InferredGalaxyZDist(bin_name=name, z=z, dndz=dndz, measurements={m}) + x = TomographicBin(bin_name=name, z=z, dndz=dndz, measurements={m}) return x @@ -278,36 +280,34 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: @pytest.fixture( name="all_harmonic_bins", ) -def fixture_all_harmonic_bins() -> list[InferredGalaxyZDist]: - """Generate a list of InferredGalaxyZDist objects with 4 bins.""" +def fixture_all_harmonic_bins() -> list[TomographicBin]: + """Generate a list of TomographicBin objects with 5 bins.""" z = np.linspace(0.0, 1.0, 256) dndzs = [ np.exp(-0.5 * (z - 0.5) ** 2 / 0.05**2) / (np.sqrt(2 * np.pi) * 0.05), np.exp(-0.5 * (z - 0.6) ** 2 / 0.05**2) / (np.sqrt(2 * np.pi) * 0.05), ] return [ - InferredGalaxyZDist( - bin_name=f"bin_{i + 1}", - z=z, - dndz=dndzs[i], - measurements={Galaxies.COUNTS, Galaxies.SHEAR_E}, + TomographicBin( + bin_name=f"bin_{int(m)}_{i + 1}", z=z, dndz=dndzs[i], measurements={m} ) for i in range(2) + for m in [Galaxies.COUNTS, Galaxies.SHEAR_E] ] @pytest.fixture( name="many_harmonic_bins", ) -def make_many_harmonic_bins() -> list[InferredGalaxyZDist]: - """Generate a list of InferredGalaxyZDist objects with 5 bins.""" +def make_many_harmonic_bins() -> list[TomographicBin]: + """Generate a list of TomographicBin objects with 5 bins.""" z = np.linspace(0.0, 1.0, 256) dndzs = [ np.exp(-0.5 * (z - mu) ** 2 / 0.05**2) / (np.sqrt(2 * np.pi) * 0.05) for mu in np.linspace(0.5, 0.9, 5) ] return [ - InferredGalaxyZDist( + TomographicBin( bin_name=f"bin_{i + 1}", z=z, dndz=dndzs[i], @@ -327,9 +327,9 @@ def make_many_harmonic_bins() -> list[InferredGalaxyZDist]: ], ids=["counts", "shear_t", "shear_minus", "shear_plus"], ) -def fixture_real_bin_1(request) -> InferredGalaxyZDist: - """Generate an InferredGalaxyZDist object with 5 bins.""" - x = InferredGalaxyZDist( +def fixture_real_bin_1(request) -> TomographicBin: + """Generate an TomographicBin object with 5 bins.""" + x = TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 5), dndz=np.array([0.1, 0.5, 0.2, 0.3, 0.4]), @@ -348,9 +348,9 @@ def fixture_real_bin_1(request) -> InferredGalaxyZDist: ], ids=["counts", "shear_t", "shear_minus", "shear_plus"], ) -def fixture_real_bin_2(request) -> InferredGalaxyZDist: - """Generate an InferredGalaxyZDist object with 3 bins.""" - x = InferredGalaxyZDist( +def fixture_real_bin_2(request) -> TomographicBin: + """Generate an TomographicBin object with 3 bins.""" + x = TomographicBin( bin_name="bin_2", z=np.linspace(0, 1, 3), dndz=np.array([0.1, 0.5, 0.4]), @@ -362,10 +362,10 @@ def fixture_real_bin_2(request) -> InferredGalaxyZDist: @pytest.fixture( name="all_real_bins", ) -def fixture_all_real_bins() -> list[InferredGalaxyZDist]: - """Generate a list of InferredGalaxyZDist objects with 5 bins.""" +def fixture_all_real_bins() -> list[TomographicBin]: + """Generate a list of TomographicBin objects with 5 bins.""" return [ - InferredGalaxyZDist( + TomographicBin( bin_name=f"bin_{i + 1}", z=np.linspace(0, 1, 5), dndz=np.array([0.1, 0.5, 0.2, 0.3, 0.4]), @@ -394,8 +394,8 @@ def fixture_window_1() -> ( @pytest.fixture(name="harmonic_two_point_xy") def fixture_harmonic_two_point_xy( - harmonic_bin_1: InferredGalaxyZDist, - harmonic_bin_2: InferredGalaxyZDist, + harmonic_bin_1: TomographicBin, + harmonic_bin_2: TomographicBin, ) -> TwoPointXY: """Generate a TwoPointXY object with 100 ells.""" m1 = list(harmonic_bin_1.measurements)[0] @@ -410,8 +410,8 @@ def fixture_harmonic_two_point_xy( @pytest.fixture(name="real_two_point_xy") def fixture_real_two_point_xy( - real_bin_1: InferredGalaxyZDist, - real_bin_2: InferredGalaxyZDist, + real_bin_1: TomographicBin, + real_bin_2: TomographicBin, ) -> TwoPointXY: """Generate a TwoPointXY object with 100 ells.""" m1 = list(real_bin_1.measurements)[0] @@ -1299,6 +1299,19 @@ def fixture_tp_factory( ) +@pytest.fixture(name="tp_factory_missing_counts") +def fixture_tp_factory_missing_counts( + wl_factory: wl.WeakLensingFactory, +) -> tp.TwoPointFactory: + """Generate a TwoPointFactory object.""" + return tp.TwoPointFactory( + correlation_space=tp.TwoPointCorrelationSpace.REAL, + weak_lensing_factories=[wl_factory], + number_counts_factories=[], + cmb_factories=[cmb.CMBConvergenceFactory()], + ) + + # Optimized fixtures that eliminate "incompatible measurements" skips @@ -1401,14 +1414,14 @@ def fixture_optimized_real_two_point_xy(optimized_real_measurement_pair) -> TwoP """ m1, m2 = optimized_real_measurement_pair - bin_1 = InferredGalaxyZDist( + bin_1 = TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 5), dndz=np.array([0.1, 0.5, 0.2, 0.3, 0.4]), measurements={m1}, ) - bin_2 = InferredGalaxyZDist( + bin_2 = TomographicBin( bin_name="bin_2", z=np.linspace(0, 1, 3), dndz=np.array([0.1, 0.5, 0.4]), @@ -1433,19 +1446,44 @@ def fixture_optimized_harmonic_two_point_xy( # Use different z-distribution for harmonic space z = np.linspace(0.0, 1.0, 256) # Match default lensing kernel size - bin_1 = InferredGalaxyZDist( - bin_name="bin_1", - z=z, - dndz=np.exp(-0.5 * (z - 0.5) ** 2 / 0.05**2) / (np.sqrt(2 * np.pi) * 0.05), - measurements={m1}, - ) - - bin_2 = InferredGalaxyZDist( - bin_name="bin_2", - z=z, - dndz=np.exp(-0.5 * (z - 0.6) ** 2 / 0.05**2) / (np.sqrt(2 * np.pi) * 0.05), - measurements={m2}, - ) + bin_1: TomographicBin | CMBLensing + bin_2: TomographicBin | CMBLensing + + match m1: + case Galaxies(): + bin_1 = TomographicBin( + bin_name="bin_1", + z=z, + dndz=np.exp(-0.5 * (z - 0.5) ** 2 / 0.05**2) + / (np.sqrt(2 * np.pi) * 0.05), + measurements={m1}, + ) + case CMB(): + bin_1 = CMBLensing( + bin_name="bin_1", + z_lss=1100.0, # CMB lensing source redshift + measurements={m1}, + ) + case _ as unreachable: + assert_never(unreachable) + + match m2: + case Galaxies(): + bin_2 = TomographicBin( + bin_name="bin_2", + z=z, + dndz=np.exp(-0.5 * (z - 0.6) ** 2 / 0.05**2) + / (np.sqrt(2 * np.pi) * 0.05), + measurements={m2}, + ) + case CMB(): + bin_2 = CMBLensing( + bin_name="bin_2", + z_lss=1100.0, # CMB lensing source redshift + measurements={m2}, + ) + case _ as unreachable: + assert_never(unreachable) return TwoPointXY(x=bin_1, y=bin_2, x_measurement=m1, y_measurement=m2) diff --git a/tests/connector/cobaya/test_model_likelihood.py b/tests/connector/cobaya/test_model_likelihood.py index 951d054ab..663b6dfc7 100644 --- a/tests/connector/cobaya/test_model_likelihood.py +++ b/tests/connector/cobaya/test_model_likelihood.py @@ -5,13 +5,18 @@ from unittest import mock import numpy as np +import pyccl import pytest from cobaya.log import LoggedError from cobaya.model import Model, get_model +from numpy.testing import assert_allclose import firecrown.likelihood._statistic as stat import firecrown.modeling_tools as ccl_factory -from firecrown.connector.cobaya.likelihood import LikelihoodConnector +from firecrown.connector.cobaya.likelihood import ( + LikelihoodConnector, + compute_pyccl_args_options, +) from firecrown.likelihood._gaussian import ConstGaussian from firecrown.likelihood._likelihood import NamedParameters from firecrown.modeling_tools import ModelingTools @@ -27,6 +32,19 @@ def test_cobaya_ccl_initialize(): assert isinstance(ccl_connector, LikelihoodConnector) assert ccl_connector.input_style == "CAMB" + assert ccl_connector.distance_max_z == 4.0 + + +def test_cobaya_ccl_initialize_with_distance_max_z(): + ccl_connector = LikelihoodConnector( + info={ + "firecrownIni": "tests/likelihood/lkdir/lkscript.py", + "input_style": "CAMB", + "distance_max_z": 1210.0, + } + ) + + assert ccl_connector.distance_max_z == 1210.0 def test_cobaya_ccl_initialize_with_params(): @@ -43,6 +61,50 @@ def test_cobaya_ccl_initialize_with_params(): assert ccl_connector.input_style == "CAMB" +def test_compute_pyccl_args_options_default_no_extension(): + """With the default distance_max_z (4.0), no background extension is + needed since it does not exceed the redshift already covered by + spl.A_SPLINE_MIN (z~9).""" + cosmo = pyccl.CosmologyVanillaLCDM() + cosmo.compute_linear_power() + + a_bg, z_bg, _, _ = compute_pyccl_args_options(cosmo) + + spl = cosmo.cosmo.spline_params + assert len(a_bg) == spl.A_SPLINE_NA + assert len(z_bg) == spl.A_SPLINE_NA + assert z_bg.max() == pytest.approx(1.0 / spl.A_SPLINE_MIN - 1.0) + + +def test_compute_pyccl_args_options_extends_for_cmb_lensing(): + """A distance_max_z beyond spl.A_SPLINE_MIN's implicit z_max extends the + background arrays out to that redshift, without the array's minimum + scale factor landing exactly on spl.A_SPLINE_MINLOG (see the note in + compute_pyccl_args_options about the resulting degenerate array).""" + cosmo = pyccl.CosmologyVanillaLCDM() + cosmo.compute_linear_power() + + spl = cosmo.cosmo.spline_params + z_spline_min = 1.0 / spl.A_SPLINE_MIN - 1.0 + + a_bg_no_ext, z_bg_no_ext, _, _ = compute_pyccl_args_options(cosmo) + a_bg, z_bg, _, _ = compute_pyccl_args_options(cosmo, distance_max_z=1210.0) + + assert len(a_bg) == len(z_bg) + assert len(z_bg) > len(z_bg_no_ext) + assert z_bg.max() == pytest.approx(1210.0) + assert z_bg.min() == pytest.approx(0.0, abs=1e-6) + assert a_bg.min() != pytest.approx(spl.A_SPLINE_MINLOG) + # z_bg descending / a_bg ascending, as required by downstream consumers. + assert np.all(np.diff(z_bg) <= 0) + assert np.all(np.diff(a_bg) >= 0) + # The fine-grained, existing part of the array is left untouched. + n_orig = len(z_bg_no_ext) + assert_allclose(z_bg[-n_orig:], z_bg_no_ext) + assert_allclose(a_bg[-n_orig:], a_bg_no_ext) + assert z_spline_min == pytest.approx(z_bg_no_ext.max()) + + def test_cobaya_likelihood_initialize(): lk_connector = LikelihoodConnector( info={ diff --git a/tests/example/test_cmb_cross.py b/tests/example/test_cmb_cross.py new file mode 100644 index 000000000..09f032a04 --- /dev/null +++ b/tests/example/test_cmb_cross.py @@ -0,0 +1,75 @@ +"""Integration tests for the CMB lensing cross-correlation example.""" + +import subprocess +from pathlib import Path + +import pytest + +from firecrown.app.analysis import Frameworks +from firecrown.app.examples import ExampleCMBCross + + +@pytest.fixture(name="target_framework", params=Frameworks) +def fixture_target_framework(request) -> Frameworks: + """Generate a target framework for all frameworks.""" + return request.param + + +@pytest.fixture(name="cmb_cross_example") +def fixture_cmb_cross_example( + target_framework: Frameworks, tmp_path: Path +) -> tuple[Path, Frameworks]: + """Generate the CMB cross-correlation example for all frameworks.""" + ExampleCMBCross( + output_path=tmp_path, + prefix="cmb_cross", + n_bins=2, + n_ell_points=5, + n_z_points=100, + target_framework=target_framework, + ) + + return tmp_path, target_framework + + +@pytest.mark.example +def test_cmb_cross_run(cmb_cross_example): + """Run each framework's generated configuration end-to-end. + + This is what actually exercises the background/distance splines out to + the CMB lensing source redshift (z~1100); a framework whose pipeline + doesn't reach far enough would fail here with a ValueError, even though + the SACC/likelihood-construction unit tests pass. + """ + output_path, framework = cmb_cross_example + match framework: + case Frameworks.COSMOSIS: + result = subprocess.run( + ["cosmosis", "cosmosis_cmb_cross.ini"], + cwd=output_path, + capture_output=True, + text=True, + check=True, + ) + + assert result.returncode == 0 + case Frameworks.COBAYA: + result = subprocess.run( + ["cobaya-run", "-f", "cobaya_cmb_cross.yaml"], + cwd=output_path, + capture_output=True, + text=True, + check=True, + ) + + assert result.returncode == 0 + case Frameworks.NUMCOSMO: + result = subprocess.run( + ["numcosmo", "run", "test", "numcosmo_cmb_cross.yaml"], + cwd=output_path, + capture_output=True, + text=True, + check=True, + ) + + assert result.returncode == 0 diff --git a/tests/fctools/test_generate_symbol_map.py b/tests/fctools/test_generate_symbol_map.py index 686e54d5d..f8575bbda 100644 --- a/tests/fctools/test_generate_symbol_map.py +++ b/tests/fctools/test_generate_symbol_map.py @@ -7,6 +7,7 @@ import json from types import ModuleType +import pytest from typer.testing import CliRunner from firecrown.fctools.generate_symbol_map import ( @@ -390,6 +391,12 @@ def test_get_all_symbols_with_generators(): runner = CliRunner() +@pytest.mark.filterwarnings( + r"ignore:.*firecrown\.ccl_factory is deprecated.*:DeprecationWarning" +) +@pytest.mark.filterwarnings( + r"ignore:.*firecrown\.parameters module is deprecated.*:DeprecationWarning" +) def test_main_outputs_valid_json(): """Test that main command outputs valid JSON to stdout.""" result = runner.invoke(app, []) diff --git a/tests/generators/test_inferred_galaxy_zdist.py b/tests/generators/test_inferred_galaxy_zdist.py index e053e9cd1..6c524b58f 100644 --- a/tests/generators/test_inferred_galaxy_zdist.py +++ b/tests/generators/test_inferred_galaxy_zdist.py @@ -1,4 +1,4 @@ -"""Tests for the module firecrown.generators.inferred_galaxy_zdist.""" +"""Tests for the module firecrown.generators.tomographic_bin.""" import copy import re diff --git a/tests/likelihood/gauss_family/statistic/source/test_source.py b/tests/likelihood/gauss_family/statistic/source/test_source.py index f96244c2c..3f287b1eb 100644 --- a/tests/likelihood/gauss_family/statistic/source/test_source.py +++ b/tests/likelihood/gauss_family/statistic/source/test_source.py @@ -29,7 +29,7 @@ ) from firecrown.likelihood.number_counts._args import NumberCountsArgs from firecrown.likelihood.weak_lensing import WeakLensingArgs -from firecrown.metadata_functions import extract_all_tracers_inferred_galaxy_zdists +from firecrown.metadata_functions import extract_all_tracers_tomographic_bins from firecrown.modeling_tools import ModelingTools from firecrown.updatable import ParamsMap, get_default_params @@ -201,11 +201,11 @@ def test_weak_lensing_source_init( def test_weak_lensing_source_create_ready(sacc_galaxy_cells_src0_src0): sacc_data, _, _ = sacc_galaxy_cells_src0_src0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) src0 = next((obj for obj in all_tracers if obj.bin_name == "src0"), None) assert src0 is not None - source_ready = wl.WeakLensing.create_ready(inferred_zdist=src0) + source_ready = wl.WeakLensing.create_ready(tomographic_bin=src0) source_read = wl.WeakLensing(sacc_tracer="src0") source_read.read(sacc_data) @@ -217,12 +217,12 @@ def test_weak_lensing_source_create_ready(sacc_galaxy_cells_src0_src0): def test_weak_lensing_source_factory(sacc_galaxy_cells_src0_src0): sacc_data, _, _ = sacc_galaxy_cells_src0_src0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) src0 = next((obj for obj in all_tracers if obj.bin_name == "src0"), None) assert src0 is not None wl_factory = wl.WeakLensingFactory(per_bin_systematics=[], global_systematics=[]) - source_ready = wl_factory.create(inferred_zdist=src0) + source_ready = wl_factory.create(tomographic_bin=src0) source_read = wl.WeakLensing(sacc_tracer="src0") source_read.read(sacc_data) @@ -234,14 +234,14 @@ def test_weak_lensing_source_factory(sacc_galaxy_cells_src0_src0): def test_weak_lensing_source_factory_cache(sacc_galaxy_cells_src0_src0): sacc_data, _, _ = sacc_galaxy_cells_src0_src0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) src0 = next((obj for obj in all_tracers if obj.bin_name == "src0"), None) assert src0 is not None wl_factory = wl.WeakLensingFactory(per_bin_systematics=[], global_systematics=[]) - source_ready = wl_factory.create(inferred_zdist=src0) + source_ready = wl_factory.create(tomographic_bin=src0) - assert source_ready is wl_factory.create(inferred_zdist=src0) + assert source_ready is wl_factory.create(tomographic_bin=src0) @pytest.mark.parametrize("include_z_dependence", [True, False]) @@ -250,7 +250,7 @@ def test_weak_lensing_source_factory_global_systematics( ): sacc_data, _, _ = sacc_galaxy_cells_src0_src0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) src0 = next((obj for obj in all_tracers if obj.bin_name == "src0"), None) assert src0 is not None @@ -263,7 +263,7 @@ def test_weak_lensing_source_factory_global_systematics( wl_factory = wl.WeakLensingFactory( per_bin_systematics=[], global_systematics=global_systematics ) - source_ready = wl_factory.create(inferred_zdist=src0) + source_ready = wl_factory.create(tomographic_bin=src0) # pylint: disable=protected-access source_read = wl.WeakLensing( @@ -307,11 +307,11 @@ def test_number_counts_source_init( def test_number_counts_source_create_ready(sacc_galaxy_cells_lens0_lens0): sacc_data, _, _ = sacc_galaxy_cells_lens0_lens0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) lens0 = next((obj for obj in all_tracers if obj.bin_name == "lens0"), None) assert lens0 is not None - source_ready = nc.NumberCounts.create_ready(inferred_zdist=lens0) + source_ready = nc.NumberCounts.create_ready(tomographic_bin=lens0) source_read = nc.NumberCounts(sacc_tracer="lens0") source_read.read(sacc_data) @@ -323,12 +323,12 @@ def test_number_counts_source_create_ready(sacc_galaxy_cells_lens0_lens0): def test_number_counts_source_factory(sacc_galaxy_cells_lens0_lens0): sacc_data, _, _ = sacc_galaxy_cells_lens0_lens0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) lens0 = next((obj for obj in all_tracers if obj.bin_name == "lens0"), None) assert lens0 is not None nc_factory = nc.NumberCountsFactory(per_bin_systematics=[], global_systematics=[]) - source_ready = nc_factory.create(inferred_zdist=lens0) + source_ready = nc_factory.create(tomographic_bin=lens0) source_read = nc.NumberCounts(sacc_tracer="lens0") source_read.read(sacc_data) @@ -340,20 +340,20 @@ def test_number_counts_source_factory(sacc_galaxy_cells_lens0_lens0): def test_number_counts_source_factory_cache(sacc_galaxy_cells_lens0_lens0): sacc_data, _, _ = sacc_galaxy_cells_lens0_lens0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) lens0 = next((obj for obj in all_tracers if obj.bin_name == "lens0"), None) assert lens0 is not None nc_factory = nc.NumberCountsFactory(per_bin_systematics=[], global_systematics=[]) - source_ready = nc_factory.create(inferred_zdist=lens0) + source_ready = nc_factory.create(tomographic_bin=lens0) - assert source_ready is nc_factory.create(inferred_zdist=lens0) + assert source_ready is nc_factory.create(tomographic_bin=lens0) def test_number_counts_source_factory_global_systematics(sacc_galaxy_cells_lens0_lens0): sacc_data, _, _ = sacc_galaxy_cells_lens0_lens0 - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) lens0 = next((obj for obj in all_tracers if obj.bin_name == "lens0"), None) assert lens0 is not None @@ -362,7 +362,7 @@ def test_number_counts_source_factory_global_systematics(sacc_galaxy_cells_lens0 per_bin_systematics=[], global_systematics=global_systematics, ) - source_ready = nc_factory.create(inferred_zdist=lens0) + source_ready = nc_factory.create(tomographic_bin=lens0) # pylint: disable=protected-access source_read = nc.NumberCounts( diff --git a/tests/likelihood/gauss_family/statistic/test_two_point.py b/tests/likelihood/gauss_family/statistic/test_two_point.py index 4e5e65423..c04f4c6e6 100644 --- a/tests/likelihood/gauss_family/statistic/test_two_point.py +++ b/tests/likelihood/gauss_family/statistic/test_two_point.py @@ -42,7 +42,7 @@ GALAXY_LENS_TYPES, GALAXY_SOURCE_TYPES, Galaxies, - InferredGalaxyZDist, + TomographicBin, ) from firecrown.modeling_tools import ModelingTools from firecrown.models.two_point import TwoPointTheory @@ -448,7 +448,7 @@ def test_two_point_lens0_lens0_data_and_conf_warn(sacc_galaxy_xis_lens0_lens0) - def test_use_source_factory( - harmonic_bin_1: InferredGalaxyZDist, tp_factory: TwoPointFactory + harmonic_bin_1: TomographicBin, tp_factory: TwoPointFactory ) -> None: measurement = list(harmonic_bin_1.measurements)[0] source = use_source_factory(harmonic_bin_1, measurement, tp_factory) @@ -465,7 +465,7 @@ def test_use_source_factory( def test_use_source_factory_invalid_measurement( - harmonic_bin_1: InferredGalaxyZDist, tp_factory: TwoPointFactory + harmonic_bin_1: TomographicBin, tp_factory: TwoPointFactory ) -> None: with pytest.raises( ValueError, diff --git a/tests/likelihood/lkdir/lkmodule.py b/tests/likelihood/lkdir/lkmodule.py index 5ca522762..f8ba95fab 100644 --- a/tests/likelihood/lkdir/lkmodule.py +++ b/tests/likelihood/lkdir/lkmodule.py @@ -4,9 +4,9 @@ import sacc -from firecrown import parameters from firecrown.likelihood._likelihood import Likelihood, NamedParameters from firecrown.modeling_tools import ModelingTools +from firecrown import updatable from firecrown.updatable import DerivedParameter, DerivedParameterCollection @@ -59,7 +59,7 @@ def __init__(self, params: NamedParameters): parameter_prefix value and creates a sampler parameter called "sampler_param0". """ super().__init__(parameter_prefix=params.get_string("parameter_prefix")) - self.sampler_param0 = parameters.register_new_updatable_parameter( + self.sampler_param0 = updatable.register_new_updatable_parameter( default_value=1.0 ) diff --git a/tests/likelihood/test_weak_lensing.py b/tests/likelihood/test_weak_lensing.py index ce1d74c12..efc84555a 100644 --- a/tests/likelihood/test_weak_lensing.py +++ b/tests/likelihood/test_weak_lensing.py @@ -9,7 +9,7 @@ import sacc import firecrown.likelihood.weak_lensing as wl -from firecrown.metadata_types import Galaxies, InferredGalaxyZDist +from firecrown.metadata_types import Galaxies, TomographicBin from firecrown.modeling_tools import ModelingTools from firecrown.updatable import ParamsMap @@ -258,7 +258,7 @@ def test_init(self): def test_create_ready(self): """Test WeakLensing.create_ready class method.""" - zdist = InferredGalaxyZDist( + zdist = TomographicBin( bin_name="test_bin", z=np.linspace(0.1, 2.0, 50), dndz=np.exp(-(((np.linspace(0.1, 2.0, 50) - 0.5) / 0.2) ** 2)), @@ -457,7 +457,7 @@ def test_weak_lensing_factory(): global_systematics=[wl.LinearAlignmentSystematicFactory()], ) - zdist = InferredGalaxyZDist( + zdist = TomographicBin( bin_name="test_bin", z=np.linspace(0.1, 2.0, 50), dndz=np.exp(-(((np.linspace(0.1, 2.0, 50) - 0.5) / 0.2) ** 2)), diff --git a/tests/metadata/test_bin_pair_selector.py b/tests/metadata/test_bin_pair_selector.py index fae058b30..2dcc11ac4 100644 --- a/tests/metadata/test_bin_pair_selector.py +++ b/tests/metadata/test_bin_pair_selector.py @@ -25,7 +25,7 @@ class MissingBinPairSelector(mt.BinPairSelector): """BinPairSelector with missing kind.""" def keep( - self, _zdist: mt.TomographicBinPair, _m: mt.MeasurementPair + self, _field_pair: mt.ProjectedFieldPair, _m: mt.MeasurementPair ) -> bool: return True @@ -42,7 +42,7 @@ class Duplicate1BinPairSelector(mt.BinPairSelector): kind: str = "foo" def keep( - self, _zdist: mt.TomographicBinPair, _m: mt.MeasurementPair + self, _field_pair: mt.ProjectedFieldPair, _m: mt.MeasurementPair ) -> bool: return True @@ -53,7 +53,7 @@ class Duplicate2BinPairSelector(mt.BinPairSelector): kind: str = "foo" def keep( - self, _zdist: mt.TomographicBinPair, _m: mt.MeasurementPair + self, _field_pair: mt.ProjectedFieldPair, _m: mt.MeasurementPair ) -> bool: return True @@ -70,8 +70,8 @@ def test_pair_selector_auto(all_harmonic_bins): all_harmonic_bins, auto_pair_selector ) # AutoBinPairSelector should create all auto-combinations - # There is two measurements per bin in all_harmonic_bins - assert len(two_point_xy_combinations) == 2 * len(all_harmonic_bins) + # There is one measurement per bin in all_harmonic_bins + assert len(two_point_xy_combinations) == len(all_harmonic_bins) for two_point_xy in two_point_xy_combinations: assert two_point_xy.x == two_point_xy.y assert two_point_xy.x_measurement == two_point_xy.y_measurement @@ -132,15 +132,18 @@ def test_pair_selector_auto_source_lens(all_harmonic_bins): def test_pair_selector_named(all_harmonic_bins): - named_pair_selector = mt.NamedBinPairSelector(names=[("bin_1", "bin_2")]) + named_pair_selector = mt.NamedBinPairSelector(names=[("bin_1_1", "bin_1_2")]) two_point_xy_combinations = make_binned_two_point_filtered( all_harmonic_bins, named_pair_selector ) # NamedBinPairSelector should create all named combinations - assert len(two_point_xy_combinations) == 3 + assert len(two_point_xy_combinations) == 1 for two_point_xy in two_point_xy_combinations: - assert {two_point_xy.x.bin_name, two_point_xy.y.bin_name} == {"bin_1", "bin_2"} + assert {two_point_xy.x.bin_name, two_point_xy.y.bin_name} == { + "bin_1_1", + "bin_1_2", + } def test_pair_selector_type_source(all_harmonic_bins): @@ -154,7 +157,7 @@ def test_pair_selector_type_source(all_harmonic_bins): # TypeSourceBinPairSelector should create all type-source combinations assert len(two_point_xy_combinations) == 10 - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="extra_src1", z=np.array([0.1]), dndz=np.array([1.0]), @@ -174,15 +177,18 @@ def test_pair_selector_type_source(all_harmonic_bins): def test_pair_selector_not_named(all_harmonic_bins): - named_pair_selector = mt.NamedBinPairSelector(names=[("bin_1", "bin_2")]) + named_pair_selector = mt.NamedBinPairSelector(names=[("bin_1_1", "bin_1_2")]) two_point_xy_combinations = make_binned_two_point_filtered( all_harmonic_bins, ~named_pair_selector ) # NamedBinPairSelector should create all named combinations - assert len(two_point_xy_combinations) == 7 + assert len(two_point_xy_combinations) == 9 for two_point_xy in two_point_xy_combinations: - assert [two_point_xy.x.bin_name, two_point_xy.y.bin_name] != ["bin_1", "bin_2"] + assert [two_point_xy.x.bin_name, two_point_xy.y.bin_name] != [ + "bin_1_1", + "bin_1_2", + ] def test_pair_selector_first_neighbor(many_harmonic_bins): @@ -190,13 +196,13 @@ def test_pair_selector_first_neighbor(many_harmonic_bins): neighbors_diff=[0, 1, -1] ) - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="extra_src_a", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="extra_src_b", z=np.array([0.2]), dndz=np.array([1.0]), @@ -238,13 +244,13 @@ def test_pair_selector_first_neighbor_no_auto(many_harmonic_bins): def test_pair_selector_auto_name_keep(): - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -256,13 +262,13 @@ def test_pair_selector_auto_name_keep(): def test_pair_selector_auto_measurement_keep(): - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -274,13 +280,13 @@ def test_pair_selector_auto_measurement_keep(): def test_pair_selector_named_keep(): - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -291,13 +297,13 @@ def test_pair_selector_named_keep(): def test_pair_selector_lens_keep(): - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -309,13 +315,13 @@ def test_pair_selector_lens_keep(): def test_pair_selector_source_keep(): - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="src1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="src1", z=np.array([0.2]), dndz=np.array([1.0]), @@ -326,19 +332,19 @@ def test_pair_selector_source_keep(): def test_pair_selector_first_neighbor_mixed(): - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="extra_src_a", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="extra_src_b", z=np.array([0.2]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - z3 = mt.InferredGalaxyZDist( + z3 = mt.TomographicBin( bin_name="extra_src_1", z=np.array([0.3]), dndz=np.array([1.0]), @@ -634,13 +640,13 @@ def test_pair_selector_deserialization_invalid_kind(): def test_composite_selector_bad_selector_not_initialized(): """Test that CompositeSelector raises NotImplementedError when _impl is not set.""" - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -673,8 +679,8 @@ def test_auto_bin_pair_selector(all_harmonic_bins): all_harmonic_bins, auto_pair_selector ) # AutoBinPairSelector should create all auto-combinations - # There are two measurements per bin in all_harmonic_bins - assert len(two_point_xy_combinations) == 2 * len(all_harmonic_bins) + # There are one measurement per bin in all_harmonic_bins + assert len(two_point_xy_combinations) == len(all_harmonic_bins) for two_point_xy in two_point_xy_combinations: assert two_point_xy.x == two_point_xy.y assert two_point_xy.x_measurement == two_point_xy.y_measurement @@ -682,13 +688,13 @@ def test_auto_bin_pair_selector(all_harmonic_bins): def test_auto_bin_pair_selector_keep(): """Test AutoBinPairSelector keep method.""" - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -708,13 +714,13 @@ def test_source_lens_pair_selector(): source_lens_pair_selector = mt.SourceLensBinPairSelector() # Create a mixed measurement bin for testing - z_source = mt.InferredGalaxyZDist( + z_source = mt.TomographicBin( bin_name="src0", z=np.linspace(0, 1.0, 50) + 0.05, dndz=np.exp(-0.5 * (np.linspace(0, 1.0, 50) + 0.05 - 0.5) ** 2 / 0.05 / 0.05), measurements={mt.Galaxies.SHEAR_E}, ) - z_lens = mt.InferredGalaxyZDist( + z_lens = mt.TomographicBin( bin_name="lens0", z=np.linspace(0, 1.0, 50) + 0.05, dndz=np.exp(-0.5 * (np.linspace(0, 1.0, 50) + 0.05 - 0.5) ** 2 / 0.05 / 0.05), @@ -738,13 +744,13 @@ def test_source_lens_pair_selector(): def test_source_lens_pair_selector_keep(): """Test SourceLensBinPairSelector keep method with fixed order.""" - z_source = mt.InferredGalaxyZDist( + z_source = mt.TomographicBin( bin_name="src1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - z_lens = mt.InferredGalaxyZDist( + z_lens = mt.TomographicBin( bin_name="lens1", z=np.array([0.2]), dndz=np.array([1.0]), @@ -807,13 +813,13 @@ def test_source_lens_pair_selector_deserialization(): def test_cross_name_bin_pair_selector_keep(): """Test CrossNameBinPairSelector keep method.""" - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -843,13 +849,13 @@ def test_cross_name_bin_pair_selector(all_harmonic_bins): def test_cross_measurement_bin_pair_selector_keep(): """Test CrossMeasurementBinPairSelector keep method.""" - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -879,13 +885,13 @@ def test_cross_measurement_bin_pair_selector(all_harmonic_bins): def test_cross_bin_pair_selector_keep(): """Test CrossBinPairSelector keep method.""" - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -975,13 +981,13 @@ def test_cross_bin_pair_selector_deserialization(): def test_cross_selectors_are_inverses(): """Test that Cross selectors are exact inverses of Auto selectors.""" - z1 = mt.InferredGalaxyZDist( + z1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - z2 = mt.InferredGalaxyZDist( + z2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -999,24 +1005,26 @@ def test_cross_selectors_are_inverses(): # CrossName vs AutoName auto_name = mt.AutoNameBinPairSelector() cross_name = mt.CrossNameBinPairSelector() - for zdist, measurements in test_cases: - assert auto_name.keep(zdist, measurements) != cross_name.keep( - zdist, measurements + for field_pair, measurements in test_cases: + assert auto_name.keep(field_pair, measurements) != cross_name.keep( + field_pair, measurements ) # CrossMeasurement vs AutoMeasurement auto_measurement = mt.AutoMeasurementBinPairSelector() cross_measurement = mt.CrossMeasurementBinPairSelector() - for zdist, measurements in test_cases: - assert auto_measurement.keep(zdist, measurements) != cross_measurement.keep( - zdist, measurements - ) + for field_pair, measurements in test_cases: + assert auto_measurement.keep( + field_pair, measurements + ) != cross_measurement.keep(field_pair, measurements) # CrossBin vs AutoBin auto_bin = mt.AutoBinPairSelector() cross_bin = mt.CrossBinPairSelector() - for zdist, measurements in test_cases: - assert auto_bin.keep(zdist, measurements) != cross_bin.keep(zdist, measurements) + for field_pair, measurements in test_cases: + assert auto_bin.keep(field_pair, measurements) != cross_bin.keep( + field_pair, measurements + ) # ============================================================================ @@ -1029,25 +1037,25 @@ def test_three_two_bin_pair_selector_keep(): selector = mt.ThreeTwoBinPairSelector() # Create test bins - src1 = mt.InferredGalaxyZDist( + src1 = mt.TomographicBin( bin_name="src_0", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - src2 = mt.InferredGalaxyZDist( + src2 = mt.TomographicBin( bin_name="src_1", z=np.array([0.2]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - lens1 = mt.InferredGalaxyZDist( + lens1 = mt.TomographicBin( bin_name="lens_0", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - lens2 = mt.InferredGalaxyZDist( + lens2 = mt.TomographicBin( bin_name="lens_2", z=np.array([0.2]), dndz=np.array([1.0]), @@ -1067,7 +1075,7 @@ def test_three_two_bin_pair_selector_keep(): # Test 4: Source-lens with SAME name - should NOT be kept (excluded by # CrossNameDiff) - src_lens_same = mt.InferredGalaxyZDist( + src_lens_same = mt.TomographicBin( bin_name="bin_0", z=np.array([0.1]), dndz=np.array([1.0]), @@ -1084,13 +1092,13 @@ def test_three_two_bin_pair_selector_integration(all_harmonic_bins): # Ensure we have distinct prefixes for cross source-lens pairs extra_bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name="srcX1", z=np.array([0.3]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ), - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name="lensX0", z=np.array([0.1]), dndz=np.array([1.0]), @@ -1139,7 +1147,7 @@ def test_three_two_bin_pair_selector_components(): # Explicitly build source-only and lens-only bins with distinct prefixes sources = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"src{i}", z=np.array([0.1 * (i + 1)]), dndz=np.array([1.0]), @@ -1148,7 +1156,7 @@ def test_three_two_bin_pair_selector_components(): for i in range(2) ] lenses = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"lens{i}", z=np.array([0.2 * (i + 1)]), dndz=np.array([1.0]), @@ -1214,25 +1222,25 @@ def test_three_two_bin_pair_selector_distance_parameters(): ) # Source bins with numeric suffixes - src0 = mt.InferredGalaxyZDist( + src0 = mt.TomographicBin( bin_name="src0", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - src1 = mt.InferredGalaxyZDist( + src1 = mt.TomographicBin( bin_name="src1", z=np.array([0.2]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - src2 = mt.InferredGalaxyZDist( + src2 = mt.TomographicBin( bin_name="src2", z=np.array([0.3]), dndz=np.array([1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - src3 = mt.InferredGalaxyZDist( + src3 = mt.TomographicBin( bin_name="src3", z=np.array([0.4]), dndz=np.array([1.0]), @@ -1240,19 +1248,19 @@ def test_three_two_bin_pair_selector_distance_parameters(): ) # Lens bins - lens0 = mt.InferredGalaxyZDist( + lens0 = mt.TomographicBin( bin_name="lens0", z=np.array([0.1]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - lens1 = mt.InferredGalaxyZDist( + lens1 = mt.TomographicBin( bin_name="lens1", z=np.array([0.2]), dndz=np.array([1.0]), measurements={mt.Galaxies.COUNTS}, ) - lens3 = mt.InferredGalaxyZDist( + lens3 = mt.TomographicBin( bin_name="lens3", z=np.array([0.4]), dndz=np.array([1.0]), diff --git a/tests/metadata/test_cmb_tracer.py b/tests/metadata/test_cmb_tracer.py new file mode 100644 index 000000000..2a572bb1e --- /dev/null +++ b/tests/metadata/test_cmb_tracer.py @@ -0,0 +1,67 @@ +import numpy as np +import pytest + +from firecrown.metadata_types import CMBLensing, TomographicBin +from firecrown.metadata_types import CMB, Galaxies + + +def make_tomobin() -> TomographicBin: + z = np.array([0.0, 0.5, 1.0]) + dndz = np.array([0.1, 0.2, 0.7]) + return TomographicBin( + bin_name="src0", z=z, dndz=dndz, measurements={Galaxies.COUNTS} + ) + + +def test_cmblensing_basic(): + cmb = CMBLensing(bin_name="cmb0", z_lss=1090.0) + assert cmb.bin_name == "cmb0" + assert cmb.z_lss == 1090.0 + assert cmb.measurements == {CMB.CONVERGENCE} + assert cmb.measurement_list == [CMB.CONVERGENCE] + + +def test_cmblensing_validation(): + with pytest.raises(ValueError, match="bin_name should not be empty"): + CMBLensing(bin_name="", z_lss=1090.0) + + with pytest.raises(ValueError, match="z_lss must be a finite float value."): + CMBLensing(bin_name="cmb0", z_lss=float("nan")) + + with pytest.raises(ValueError, match="z_lss must be positive"): + CMBLensing(bin_name="cmb0", z_lss=-1.0) + + with pytest.raises( + ValueError, match="The measurement should be a CMB Measurement." + ): + CMBLensing(bin_name="cmb0", z_lss=1100.0, measurements={Galaxies.COUNTS}) + + +def test_tomographicbin_equality_and_cmb_mismatch(): + tb1 = make_tomobin() + tb2 = TomographicBin( + bin_name="src0", + z=np.array([0.0, 0.5, 1.0]), + dndz=np.array([0.1, 0.2, 0.7]), + measurements={Galaxies.COUNTS}, + ) + + assert tb1 == tb2 + + cmb = CMBLensing(bin_name="cmb0", z_lss=1090.0) + # Different tracer implementations should not be equal + assert (tb1 == cmb) is False + assert (cmb == tb1) is False + + # Unrelated types should not be considered equal + assert (tb1 == object()) is False + assert (cmb == object()) is False + + +def test_cmblensing_equality(): + c1 = CMBLensing(bin_name="cmb0", z_lss=1090.0) + c2 = CMBLensing(bin_name="cmb0", z_lss=1090.0) + assert c1 == c2 + + c3 = CMBLensing(bin_name="cmb1", z_lss=1090.0) + assert c1 != c3 diff --git a/tests/metadata/test_combination_utils.py b/tests/metadata/test_combination_utils.py index 6dd0e4696..da7e27ea0 100644 --- a/tests/metadata/test_combination_utils.py +++ b/tests/metadata/test_combination_utils.py @@ -16,7 +16,7 @@ @pytest.fixture(name="sample_combinations") def fixture_sample_combinations( - all_harmonic_bins: list[mt.InferredGalaxyZDist], + all_harmonic_bins: list[mt.TomographicBin], ) -> list[mt.TwoPointXY]: """Create a sample list of TwoPointXY combinations for testing.""" return make_all_photoz_bin_combinations(all_harmonic_bins) @@ -185,19 +185,19 @@ def test_filter_two_point_combinations_not_selector( def test_filter_two_point_combinations_named_selector(): """Test filtering with NamedBinPairSelector.""" # Create specific bins for this test - bin1 = mt.InferredGalaxyZDist( + bin1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - bin2 = mt.InferredGalaxyZDist( + bin2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - bin3 = mt.InferredGalaxyZDist( + bin3 = mt.TomographicBin( bin_name="bin_3", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -221,7 +221,7 @@ def test_filter_two_point_combinations_preserves_order(): """Test that filtering preserves the original order of combinations.""" # Create bins with specific names to control ordering bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"bin_{i}", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -250,13 +250,13 @@ def test_filter_two_point_combinations_preserves_order(): def test_filter_two_point_combinations_auto_measurement(): """Test filtering with AutoMeasurementBinPairSelector.""" - bin1 = mt.InferredGalaxyZDist( + bin1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), measurements={mt.Galaxies.SHEAR_E, mt.Galaxies.COUNTS}, ) - bin2 = mt.InferredGalaxyZDist( + bin2 = mt.TomographicBin( bin_name="bin_2", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -275,7 +275,7 @@ def test_filter_two_point_combinations_auto_measurement(): def test_filter_two_point_combinations_cross_measurement(): """Test filtering with CrossMeasurementBinPairSelector.""" - bin1 = mt.InferredGalaxyZDist( + bin1 = mt.TomographicBin( bin_name="bin_1", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -295,7 +295,7 @@ def test_filter_two_point_combinations_cross_measurement(): def test_filter_two_point_combinations_complex_selector(): """Test filtering with a complex nested selector.""" bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"bin_{i}", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -332,7 +332,7 @@ def test_filter_two_point_combinations_complex_selector(): def test_filter_two_point_combinations_triple_negation(): """Test filtering with triple negation selector.""" bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"bin_{i}", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -359,7 +359,7 @@ def test_filter_two_point_combinations_with_real_vs_harmonic(): types. """ # Create bins with measurements that support different spaces - harmonic_bin = mt.InferredGalaxyZDist( + harmonic_bin = mt.TomographicBin( bin_name="harmonic", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -380,13 +380,13 @@ def test_filter_two_point_combinations_with_real_vs_harmonic(): def test_filter_two_point_combinations_left_right_measurement_selectors(): """Test filtering with SourceLensBinPairSelector.""" - bin1 = mt.InferredGalaxyZDist( + bin1 = mt.TomographicBin( bin_name="src", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), measurements={mt.Galaxies.SHEAR_E}, ) - bin2 = mt.InferredGalaxyZDist( + bin2 = mt.TomographicBin( bin_name="lens", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -411,7 +411,7 @@ def test_filter_two_point_combinations_left_right_measurement_selectors(): def test_filter_two_point_combinations_idempotent(): """Test that applying the same filter twice gives the same result.""" bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"bin_{i}", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -433,7 +433,7 @@ def test_filter_two_point_combinations_idempotent(): def test_filter_two_point_combinations_commutative_and(): """Test that AND selector is commutative.""" bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"bin_{i}", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), @@ -456,7 +456,7 @@ def test_filter_two_point_combinations_commutative_and(): def test_filter_two_point_combinations_commutative_or(): """Test that OR selector is commutative.""" bins = [ - mt.InferredGalaxyZDist( + mt.TomographicBin( bin_name=f"bin_{i}", z=np.array([0.1, 0.2, 0.3]), dndz=np.array([1.0, 2.0, 1.0]), diff --git a/tests/metadata/test_data_functions.py b/tests/metadata/test_data_functions.py index 02a572705..6bb960c8f 100644 --- a/tests/metadata/test_data_functions.py +++ b/tests/metadata/test_data_functions.py @@ -19,7 +19,7 @@ from firecrown.metadata_functions import make_all_photoz_bin_combinations from firecrown.metadata_types import ( Galaxies, - InferredGalaxyZDist, + TomographicBin, TwoPointFilterMethod, TwoPointHarmonic, TwoPointReal, @@ -29,7 +29,7 @@ @pytest.fixture(name="harmonic_bins") def fixture_harmonic_bins( - all_harmonic_bins: list[InferredGalaxyZDist], + all_harmonic_bins: list[TomographicBin], ) -> list[TwoPointMeasurement]: """Create a list of TwoPointMeasurement with harmonic metadata.""" all_xy = make_all_photoz_bin_combinations(all_harmonic_bins) @@ -49,7 +49,7 @@ def fixture_harmonic_bins( @pytest.fixture(name="harmonic_window_bins") def fixture_harmonic_window_bins( - all_harmonic_bins: list[InferredGalaxyZDist], + all_harmonic_bins: list[TomographicBin], ) -> list[TwoPointMeasurement]: """Create a list of TwoPointMeasurement with harmonic metadata.""" all_xy = make_all_photoz_bin_combinations(all_harmonic_bins) @@ -77,7 +77,7 @@ def fixture_harmonic_window_bins( @pytest.fixture(name="real_bins") def fixture_real_bins( - all_real_bins: list[InferredGalaxyZDist], + all_real_bins: list[TomographicBin], ) -> list[TwoPointMeasurement]: """Create a list of TwoPointMeasurement with real metadata.""" all_xy = make_all_photoz_bin_combinations(all_real_bins) @@ -406,7 +406,7 @@ def test_two_point_harmonic_window_bin_filter_collection_call( def test_two_point_harmonic_bin_filter_collection_call_require( - harmonic_bin_1: InferredGalaxyZDist, + harmonic_bin_1: TomographicBin, ) -> None: harmonic_filter_collection_no_empty = TwoPointBinFilterCollection( filters=[ @@ -432,7 +432,7 @@ def test_two_point_harmonic_bin_filter_collection_call_require( def test_two_point_harmonic_bin_filter_collection_call_no_empty( - harmonic_bin_1: InferredGalaxyZDist, + harmonic_bin_1: TomographicBin, ) -> None: cm = list(harmonic_bin_1.measurements)[0] harmonic_filter_collection_no_empty = TwoPointBinFilterCollection( @@ -461,7 +461,7 @@ def test_two_point_harmonic_bin_filter_collection_call_no_empty( def test_two_point_harmonic_bin_filter_collection_call_empty( - harmonic_bin_1: InferredGalaxyZDist, + harmonic_bin_1: TomographicBin, ) -> None: cm = list(harmonic_bin_1.measurements)[0] harmonic_filter_collection_no_empty = TwoPointBinFilterCollection( @@ -520,7 +520,7 @@ def test_two_point_real_bin_filter_collection_call( def test_two_point_real_bin_filter_collection_call_require( - real_bin_1: InferredGalaxyZDist, + real_bin_1: TomographicBin, ) -> None: cm = list(real_bin_1.measurements)[0] real_filter_collection_no_empty = TwoPointBinFilterCollection( @@ -545,7 +545,7 @@ def test_two_point_real_bin_filter_collection_call_require( def test_two_point_real_bin_filter_collection_call_no_empty( - real_bin_1: InferredGalaxyZDist, + real_bin_1: TomographicBin, ) -> None: cm = list(real_bin_1.measurements)[0] real_filter_collection_no_empty = TwoPointBinFilterCollection( @@ -576,7 +576,7 @@ def test_two_point_real_bin_filter_collection_call_no_empty( def test_two_point_real_bin_filter_collection_call_empty( - real_bin_1: InferredGalaxyZDist, + real_bin_1: TomographicBin, ) -> None: cm = list(real_bin_1.measurements)[0] real_filter_collection_no_empty = TwoPointBinFilterCollection( @@ -648,7 +648,7 @@ def test_bin_filter_methods( bin_col = TwoPointBinFilterCollection( filters=[ TwoPointBinFilter.from_args_auto( - "bin_1", Galaxies.COUNTS, 5, 10, method=method + "bin_5_1", Galaxies.COUNTS, 5, 10, method=method ) ] ) @@ -659,19 +659,19 @@ def test_bin_filter_methods( def test_raise_with_two_bins_same_name(): # Here we test if make_all_photoz_bin_combinations raises an error when # there are two bins with the same name and measurement. - igz1 = InferredGalaxyZDist( + bin1 = TomographicBin( bin_name="bin_1", dndz=np.linspace(0.0, 2.0, 100), z=np.linspace(0.0, 2.0, 100), measurements={Galaxies.COUNTS}, ) - igz2 = InferredGalaxyZDist( + bin2 = TomographicBin( bin_name="bin_2", dndz=np.linspace(0.0, 2.0, 100), z=np.linspace(0.0, 2.0, 100), measurements={Galaxies.COUNTS}, ) - igz3 = InferredGalaxyZDist( + bin3 = TomographicBin( bin_name="bin_3", dndz=np.linspace(0.0, 2.0, 100), z=np.linspace(0.0, 2.0, 100), @@ -684,4 +684,4 @@ def test_raise_with_two_bins_same_name(): "names found: ['bin_1', 'bin_2']" ), ): - _ = make_all_photoz_bin_combinations([igz1, igz1, igz2, igz2, igz3]) + _ = make_all_photoz_bin_combinations([bin1, bin1, bin2, bin2, bin3]) diff --git a/tests/metadata/test_metadata_two_point.py b/tests/metadata/test_metadata_two_point.py index 96307a63b..5dd956673 100644 --- a/tests/metadata/test_metadata_two_point.py +++ b/tests/metadata/test_metadata_two_point.py @@ -2,6 +2,8 @@ Tests for the module firecrown.metadata_types and firecrown.metadata_functions. """ +import dataclasses + import numpy as np import pytest import sacc @@ -25,13 +27,17 @@ from firecrown.metadata_types import ( ALL_MEASUREMENTS, CMB, + CMBLensing, Clusters, Galaxies, - InferredGalaxyZDist, + Measurement, + ProjectedField, + TomographicBin, TracerNames, TwoPointHarmonic, TwoPointReal, TwoPointXY, + TypeSource, ) from firecrown.metadata_types._sacc_type_string import ( _type_to_sacc_string_harmonic as harmonic, @@ -42,7 +48,7 @@ def test_inferred_galaxy_z_dist(): - z_dist = InferredGalaxyZDist( + z_dist = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -60,7 +66,7 @@ def test_inferred_galaxy_z_dist_bad_shape(): with pytest.raises( ValueError, match="The z and dndz arrays should have the same shape." ): - InferredGalaxyZDist( + TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(101), @@ -69,8 +75,10 @@ def test_inferred_galaxy_z_dist_bad_shape(): def test_inferred_galaxy_z_dist_bad_type(): - with pytest.raises(ValueError, match="The measurement should be a Measurement."): - InferredGalaxyZDist( + with pytest.raises( + ValueError, match="The measurement should be a Galaxies Measurement." + ): + TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -80,7 +88,7 @@ def test_inferred_galaxy_z_dist_bad_type(): def test_inferred_galaxy_z_dist_bad_name(): with pytest.raises(ValueError, match="The bin_name should not be empty."): - InferredGalaxyZDist( + TomographicBin( bin_name="", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -89,13 +97,13 @@ def test_inferred_galaxy_z_dist_bad_name(): def test_two_point_xy_gal_gal(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -111,13 +119,13 @@ def test_two_point_xy_gal_gal(): def test_two_point_xy_gal_gal_invalid_x_measurement(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -133,13 +141,13 @@ def test_two_point_xy_gal_gal_invalid_x_measurement(): def test_two_point_xy_gal_gal_invalid_y_measurement(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -155,13 +163,12 @@ def test_two_point_xy_gal_gal_invalid_y_measurement(): def test_two_point_xy_cmb_gal(): - x = InferredGalaxyZDist( + x = CMBLensing( bin_name="b_name1", - z=np.linspace(0, 1, 100), - dndz=np.ones(100), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -177,13 +184,13 @@ def test_two_point_xy_cmb_gal(): def test_two_point_xy_invalid(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -199,13 +206,13 @@ def test_two_point_xy_invalid(): def test_two_point_harmonic(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -224,13 +231,13 @@ def test_two_point_harmonic(): def test_two_point_harmonic_invalid_ells(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -248,13 +255,13 @@ def test_two_point_harmonic_invalid_ells(): def test_two_point_harmonic_invalid_type(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -353,13 +360,13 @@ def test_two_point_cwindow_invalid(): weights = np.ones(400).reshape(-1, 4) window_ells = np.array([0, 1, 2, 3], dtype=np.float64) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -377,13 +384,13 @@ def test_two_point_cwindow_invalid(): def test_two_point_cwindow_invalid_window(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -405,13 +412,13 @@ def test_two_point_cwindow_invalid_window_shape(): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) weights = np.ones(400, dtype=np.float64) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -431,13 +438,13 @@ def test_two_point_cwindow_window_ell_not_match(): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) weights = np.ones(400).reshape(-1, 4) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -457,13 +464,13 @@ def test_two_point_cwindow_missing_window_ells(): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) weights = np.ones(400).reshape(-1, 4) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -483,13 +490,13 @@ def test_two_point_cwindow_window_ells_wrong_shape(): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) weights = np.ones(400).reshape(-1, 4) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -515,13 +522,13 @@ def test_two_point_cwindow_window_ells_wrong_len(): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) weights = np.ones(400).reshape(-1, 4) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -546,13 +553,13 @@ def test_two_point_cwindow_window_ells_wrong_len(): def test_two_point_cwindow_no_window_with_window_ells(): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -570,13 +577,13 @@ def test_two_point_cwindow_no_window_with_window_ells(): def test_two_point_real(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -595,13 +602,13 @@ def test_two_point_real(): def test_two_point_real_invalid(): - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="b_name1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -646,11 +653,11 @@ def test_measurement_serialization(): assert t == recovered -def test_inferred_galaxy_zdist_serialization(harmonic_bin_1: InferredGalaxyZDist): +def test_inferred_galaxy_zdist_serialization(harmonic_bin_1: TomographicBin): s = harmonic_bin_1.to_yaml() # Take a look at how hideous the generated string # is. - recovered = InferredGalaxyZDist.from_yaml(s) + recovered = TomographicBin.from_yaml(s) assert harmonic_bin_1 == recovered @@ -804,13 +811,25 @@ def test_two_point_from_metadata_xi_theta(optimized_real_two_point_xy, tp_factor def test_two_point_from_metadata_cells_unsupported_type(tp_factory): ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) - x = InferredGalaxyZDist( + + @dataclasses.dataclass(frozen=True, kw_only=True) + class DummyClusterBin: + """A dummy cluster bin for testing unsupported types.""" + + bin_name: str + measurements: set[Measurement] + type_source: TypeSource = TypeSource("cluster") + + @property + def measurement_list(self) -> list[Measurement]: + """Get the measurements as a sorted list.""" + return sorted(self.measurements) + + x = DummyClusterBin( bin_name="b_name1", - z=np.linspace(0, 1, 100), - dndz=np.ones(100), measurements={Clusters.COUNTS}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -840,13 +859,12 @@ def fixture_tp_factory_with_cmb(): def test_two_point_from_metadata_cmb_supported(tp_factory_with_cmb): """Test that CMB measurements work when CMB factory is provided.""" ells = np.array(np.linspace(0, 100, 100), dtype=np.int64) - x = InferredGalaxyZDist( + x = CMBLensing( bin_name="b_name1", - z=np.linspace(0, 1, 100), - dndz=np.ones(100), measurements={CMB.CONVERGENCE}, + z_lss=1100.0, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="b_name2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -869,23 +887,23 @@ def test_two_point_from_metadata_cmb_supported(tp_factory_with_cmb): def test_make_two_point_xy_valid_galaxies(): """Test make_two_point_xy with valid galaxy measurements.""" - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="shear_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="shear_bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - inferred_dict = {"shear_bin_0": x, "shear_bin_1": y} + tomographic_dict: dict[str, ProjectedField] = {"shear_bin_0": x, "shear_bin_1": y} tracer_names = TracerNames("shear_bin_0", "shear_bin_1") data_type = "galaxy_shear_cl_ee" - xy = make_two_point_xy(inferred_dict, tracer_names, data_type) + xy = make_two_point_xy(tomographic_dict, tracer_names, data_type) assert xy.x == x assert xy.y == y @@ -895,23 +913,25 @@ def test_make_two_point_xy_valid_galaxies(): def test_make_two_point_xy_valid_cmb_galaxy(): """Test make_two_point_xy with CMB-galaxy measurements.""" - cmb = InferredGalaxyZDist( + cmb = CMBLensing( bin_name="cmb_convergence", - z=np.array([1100.0]), - dndz=np.array([1.0]), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) - galaxy = InferredGalaxyZDist( + galaxy = TomographicBin( bin_name="galaxy_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - inferred_dict = {"cmb_convergence": cmb, "galaxy_bin_0": galaxy} + tomographic_dict: dict[str, ProjectedField] = { + "cmb_convergence": cmb, + "galaxy_bin_0": galaxy, + } tracer_names = TracerNames("cmb_convergence", "galaxy_bin_0") data_type = harmonic(CMB.CONVERGENCE, Galaxies.COUNTS) - xy = make_two_point_xy(inferred_dict, tracer_names, data_type) + xy = make_two_point_xy(tomographic_dict, tracer_names, data_type) assert xy.x == cmb assert xy.y == galaxy @@ -921,25 +941,27 @@ def test_make_two_point_xy_valid_cmb_galaxy(): def test_make_two_point_xy_valid_cmb_galaxy_needs_swap(): """Test make_two_point_xy with CMB-galaxy measurements.""" - cmb = InferredGalaxyZDist( + cmb = CMBLensing( bin_name="cmb_convergence", - z=np.array([1100.0]), - dndz=np.array([1.0]), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) - galaxy = InferredGalaxyZDist( + galaxy = TomographicBin( bin_name="galaxy_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ) - inferred_dict = {"cmb_convergence": cmb, "galaxy_bin_0": galaxy} + tomographic_dict: dict[str, ProjectedField] = { + "cmb_convergence": cmb, + "galaxy_bin_0": galaxy, + } tracer_names = TracerNames("galaxy_bin_0", "cmb_convergence") data_type = harmonic(CMB.CONVERGENCE, Galaxies.COUNTS) # Even though the order is swapped, this should still work this behavior will be # removed in the future. It is kept for backwards compatibility and to avoid # breaking existing data files. - xy = make_two_point_xy(inferred_dict, tracer_names, data_type) + xy = make_two_point_xy(tomographic_dict, tracer_names, data_type) assert xy.x == cmb assert xy.y == galaxy @@ -954,20 +976,20 @@ def test_make_two_point_xy_missing_tracer_zdist(): when a requested tracer name is not in the inferred galaxy z distributions dictionary. """ - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="shear_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) # Only provide one tracer in the dictionary - inferred_dict = {"shear_bin_0": x} + tomographic_dict: dict[str, ProjectedField] = {"shear_bin_0": x} # But request two tracers (second one doesn't exist) tracer_names = TracerNames("shear_bin_0", "shear_bin_1") data_type = harmonic(Galaxies.SHEAR_E, Galaxies.SHEAR_E) with pytest.raises(ValueError) as exc_info: - make_two_point_xy(inferred_dict, tracer_names, data_type) + make_two_point_xy(tomographic_dict, tracer_names, data_type) error_msg = str(exc_info.value) assert "shear_bin_1" in error_msg @@ -976,24 +998,24 @@ def test_make_two_point_xy_missing_tracer_zdist(): def test_make_two_point_xy_missing_x_measurement(): """Test make_two_point_xy when first tracer lacks required measurement.""" - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="shear_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, # Has SHEAR_T but needs SHEAR_E ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="shear_bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - inferred_dict = {"shear_bin_0": x, "shear_bin_1": y} + tomographic_dict: dict[str, ProjectedField] = {"shear_bin_0": x, "shear_bin_1": y} tracer_names = TracerNames("shear_bin_0", "shear_bin_1") data_type = harmonic(Galaxies.SHEAR_E, Galaxies.SHEAR_E) with pytest.raises(ValueError) as exc_info: - make_two_point_xy(inferred_dict, tracer_names, data_type) + make_two_point_xy(tomographic_dict, tracer_names, data_type) error_msg = str(exc_info.value) assert "Tracer measurements do not match the SACC naming convention" in error_msg @@ -1007,24 +1029,24 @@ def test_make_two_point_xy_missing_x_measurement(): def test_make_two_point_xy_missing_y_measurement(): """Test make_two_point_xy when second tracer lacks required measurement.""" - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="shear_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="shear_bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, # Has SHEAR_T but needs SHEAR_E ) - inferred_dict = {"shear_bin_0": x, "shear_bin_1": y} + tomographic_dict: dict[str, ProjectedField] = {"shear_bin_0": x, "shear_bin_1": y} tracer_names = TracerNames("shear_bin_0", "shear_bin_1") data_type = harmonic(Galaxies.SHEAR_E, Galaxies.SHEAR_E) with pytest.raises(ValueError) as exc_info: - make_two_point_xy(inferred_dict, tracer_names, data_type) + make_two_point_xy(tomographic_dict, tracer_names, data_type) error_msg = str(exc_info.value) assert "Tracer measurements do not match the SACC naming convention" in error_msg @@ -1037,24 +1059,24 @@ def test_make_two_point_xy_missing_y_measurement(): def test_make_two_point_xy_both_measurements_missing(): """Test make_two_point_xy when both tracers lack required measurements.""" - x = InferredGalaxyZDist( + x = TomographicBin( bin_name="counts_bin_0", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, # Has COUNTS, needs SHEAR_E ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="shear_bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, # Has SHEAR_T, needs SHEAR_E ) - inferred_dict = {"counts_bin_0": x, "shear_bin_1": y} + tomographic_dict: dict[str, ProjectedField] = {"counts_bin_0": x, "shear_bin_1": y} tracer_names = TracerNames("counts_bin_0", "shear_bin_1") data_type = harmonic(Galaxies.SHEAR_E, Galaxies.SHEAR_E) with pytest.raises(ValueError) as exc_info: - make_two_point_xy(inferred_dict, tracer_names, data_type) + make_two_point_xy(tomographic_dict, tracer_names, data_type) error_msg = str(exc_info.value) assert "Tracer measurements do not match the SACC naming convention" in error_msg @@ -1065,24 +1087,23 @@ def test_make_two_point_xy_both_measurements_missing(): def test_make_two_point_xy_sacc_convention_explanation(): """Test that error message includes SACC convention explanation.""" - x = InferredGalaxyZDist( + x = CMBLensing( bin_name="cmb_bin", - z=np.array([1100.0]), - dndz=np.array([1.0]), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) - y = InferredGalaxyZDist( + y = TomographicBin( bin_name="galaxy_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, # Has SHEAR_E but needs COUNTS ) - inferred_dict = {"cmb_bin": x, "galaxy_bin": y} + tomographic_dict: dict[str, ProjectedField] = {"cmb_bin": x, "galaxy_bin": y} tracer_names = TracerNames("cmb_bin", "galaxy_bin") data_type = harmonic(CMB.CONVERGENCE, Galaxies.COUNTS) with pytest.raises(ValueError) as exc_info: - make_two_point_xy(inferred_dict, tracer_names, data_type) + make_two_point_xy(tomographic_dict, tracer_names, data_type) error_msg = str(exc_info.value) # Check for convention explanation diff --git a/tests/metadata/test_metadata_two_point_sacc.py b/tests/metadata/test_metadata_two_point_sacc.py index 7f2aa1a30..b2473b875 100644 --- a/tests/metadata/test_metadata_two_point_sacc.py +++ b/tests/metadata/test_metadata_two_point_sacc.py @@ -28,7 +28,8 @@ extract_all_photoz_bin_combinations, extract_all_real_metadata, extract_all_real_metadata_indices, - extract_all_tracers_inferred_galaxy_zdists, + extract_all_tracers_projected_fields, + extract_all_tracers_tomographic_bins, extract_window_function, make_all_photoz_bin_combinations, make_all_photoz_bin_combinations_with_cmb, @@ -40,13 +41,14 @@ AutoMeasurementBinPairSelector, AutoNameBinPairSelector, Galaxies, - InferredGalaxyZDist, LensBinPairSelector, NamedBinPairSelector, SourceBinPairSelector, TracerNames, TwoPointHarmonic, TwoPointReal, + TomographicBin, + CMBLensing, TypeSource, ) from firecrown.metadata_types._sacc_type_string import ( @@ -155,7 +157,7 @@ def fixture_sacc_galaxy_cells_three_tracers() -> ( def test_extract_all_tracers_cells(sacc_galaxy_cells) -> None: sacc_data, tracers, _ = sacc_galaxy_cells assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) for tracer in all_tracers: orig_tracer = tracers[tracer.bin_name] @@ -166,7 +168,7 @@ def test_extract_all_tracers_cells(sacc_galaxy_cells) -> None: def test_extract_all_tracers_xis(sacc_galaxy_xis): sacc_data, tracers, _ = sacc_galaxy_xis assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) for tracer in all_tracers: orig_tracer = tracers[tracer.bin_name] @@ -177,7 +179,7 @@ def test_extract_all_tracers_xis(sacc_galaxy_xis): def test_extract_all_tracers_cells_src0_src0(sacc_galaxy_cells_src0_src0): sacc_data, z, dndz = sacc_galaxy_cells_src0_src0 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 1 @@ -191,7 +193,7 @@ def test_extract_all_tracers_cells_src0_src0(sacc_galaxy_cells_src0_src0): def test_extract_all_tracers_cells_src0_src1(sacc_galaxy_cells_src0_src1): sacc_data, z, dndz0, dndz1 = sacc_galaxy_cells_src0_src1 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 2 @@ -207,7 +209,7 @@ def test_extract_all_tracers_cells_src0_src1(sacc_galaxy_cells_src0_src1): def test_extract_all_tracers_cells_lens0_lens0(sacc_galaxy_cells_lens0_lens0): sacc_data, z, dndz = sacc_galaxy_cells_lens0_lens0 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 1 @@ -221,7 +223,7 @@ def test_extract_all_tracers_cells_lens0_lens0(sacc_galaxy_cells_lens0_lens0): def test_extract_all_tracers_cells_lens0_lens1(sacc_galaxy_cells_lens0_lens1): sacc_data, z, dndz0, dndz1 = sacc_galaxy_cells_lens0_lens1 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 2 @@ -237,7 +239,7 @@ def test_extract_all_tracers_cells_lens0_lens1(sacc_galaxy_cells_lens0_lens1): def test_extract_all_tracers_xis_lens0_lens0(sacc_galaxy_xis_lens0_lens0): sacc_data, z, dndz = sacc_galaxy_xis_lens0_lens0 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 1 @@ -251,7 +253,7 @@ def test_extract_all_tracers_xis_lens0_lens0(sacc_galaxy_xis_lens0_lens0): def test_extract_all_tracers_xis_lens0_lens1(sacc_galaxy_xis_lens0_lens1): sacc_data, z, dndz0, dndz1 = sacc_galaxy_xis_lens0_lens1 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 2 @@ -267,7 +269,7 @@ def test_extract_all_tracers_xis_lens0_lens1(sacc_galaxy_xis_lens0_lens1): def test_extract_all_trace_cells_src0_lens0(sacc_galaxy_cells_src0_lens0): sacc_data, z, dndz0, dndz1 = sacc_galaxy_cells_src0_lens0 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 2 @@ -285,7 +287,7 @@ def test_extract_all_trace_cells_src0_lens0(sacc_galaxy_cells_src0_lens0): def test_extract_all_trace_xis_src0_lens0(sacc_galaxy_xis_src0_lens0): sacc_data, z, dndz0, dndz1 = sacc_galaxy_xis_src0_lens0 assert sacc_data is not None - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) assert len(all_tracers) == 2 @@ -308,16 +310,16 @@ def test_extract_all_tracers_invalid_data_type( with pytest.raises( ValueError, match="Tracer src0 does not have data points associated with it." ): - _ = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + _ = extract_all_tracers_tomographic_bins(sacc_data) def test_extract_all_tracers_skips_non_nztracer() -> None: - """Test that extract_all_tracers_inferred_galaxy_zdists skips non-NZTracer types. + """Test that extract_all_tracers_tomographic_bins skips non-NZTracer types. Verifies that when a SACC object contains both NZTracer and non-NZTracer types (e.g., WeakLensingTracer, DeltaFunctionTracer), only the NZTracer instances are extracted and returned. This tests the filtering logic on line 37-45 in - extract_all_tracers_inferred_galaxy_zdists. + extract_all_tracers_tomographic_bins. """ sacc_data = sacc.Sacc() @@ -339,7 +341,7 @@ def test_extract_all_tracers_skips_non_nztracer() -> None: sacc_data.add_covariance(cov) # Extract tracers - should only get the NZTracer, skipping DeltaFunctionTracer - all_tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) # Verify only the NZTracer was extracted assert len(all_tracers) == 1 @@ -349,6 +351,147 @@ def test_extract_all_tracers_skips_non_nztracer() -> None: assert all_tracers[0].measurements == {Galaxies.SHEAR_E} +def test_extract_all_tracers_tomographic_bins_still_skips_map_tracer() -> None: + """extract_all_tracers_tomographic_bins must keep ignoring MapTracer. + + It is the legacy, galaxy-only extraction function; CMB support is only + added to extract_all_tracers_projected_fields. + """ + sacc_data = sacc.Sacc() + sacc_data.add_tracer("Map", "cmb_convergence", 0, [10, 100, 1000], [1.0, 1.0, 1.0]) + sacc_data.add_data_point( + "cmb_convergence_cl", ("cmb_convergence", "cmb_convergence"), 1.0, ell=10 + ) + + all_tracers = extract_all_tracers_tomographic_bins(sacc_data) + assert all_tracers == [] + + +def test_extract_all_tracers_projected_fields_cmb_map_tracer() -> None: + """extract_all_tracers_projected_fields turns a CMB MapTracer into CMBLensing.""" + sacc_data = sacc.Sacc() + sacc_data.add_tracer( + "Map", + "cmb_convergence", + 0, + [10, 100, 1000], + [1.0, 1.0, 1.0], + metadata={"z_lss": 1090.0}, + ) + sacc_data.add_data_point( + "cmb_convergence_cl", ("cmb_convergence", "cmb_convergence"), 1.0, ell=10 + ) + + all_fields = extract_all_tracers_projected_fields(sacc_data) + + assert len(all_fields) == 1 + cmb_field = all_fields[0] + assert isinstance(cmb_field, CMBLensing) + assert cmb_field.bin_name == "cmb_convergence" + assert cmb_field.z_lss == 1090.0 + assert cmb_field.measurements == {CMB.CONVERGENCE} + + +def test_extract_all_tracers_projected_fields_cmb_default_z_lss() -> None: + """z_lss defaults to 1100.0 when absent from the tracer's metadata.""" + sacc_data = sacc.Sacc() + sacc_data.add_tracer("Map", "cmb_convergence", 0, [10, 100], [1.0, 1.0]) + sacc_data.add_data_point( + "cmb_convergence_cl", ("cmb_convergence", "cmb_convergence"), 1.0, ell=10 + ) + + all_fields = extract_all_tracers_projected_fields(sacc_data) + + assert len(all_fields) == 1 + cmb_field = all_fields[0] + assert isinstance(cmb_field, CMBLensing) + assert cmb_field.z_lss == 1100.0 + + +def test_extract_all_tracers_projected_fields_skips_other_tracer_types() -> None: + """A non-NZ, non-Map tracer (e.g. DeltaFunctionTracer) is ignored, even when + NZTracer and MapTracer instances are also present.""" + sacc_data = sacc.Sacc() + z = np.linspace(0.0, 2.0, 50) + 0.01 + dndz = np.exp(-0.5 * (z - 0.5) ** 2 / 0.05 / 0.05) + sacc_data.add_tracer("NZ", "src0", z, dndz) + sacc_data.add_tracer("Map", "cmb_convergence", 0, [10, 100, 1000], [1.0, 1.0, 1.0]) + sacc_data.add_tracer("misc", "sample") + + sacc_data.add_data_point("galaxy_shear_cl_ee", ("src0", "src0"), 1.0, ell=10) + sacc_data.add_data_point( + "cmb_convergence_cl", ("cmb_convergence", "cmb_convergence"), 1.0, ell=10 + ) + + all_fields = extract_all_tracers_projected_fields(sacc_data) + + assert len(all_fields) == 2 + assert {f.bin_name for f in all_fields} == {"src0", "cmb_convergence"} + + +def test_extract_all_tracers_projected_fields_mixed_nz_and_map() -> None: + """A file with both galaxy and CMB tracers yields both TomographicBin and + CMBLensing instances.""" + sacc_data = sacc.Sacc() + z = np.linspace(0.0, 2.0, 50) + 0.01 + dndz = np.exp(-0.5 * (z - 0.5) ** 2 / 0.05 / 0.05) + sacc_data.add_tracer("NZ", "src0", z, dndz) + sacc_data.add_tracer("Map", "cmb_convergence", 0, [10, 100, 1000], [1.0, 1.0, 1.0]) + + sacc_data.add_data_point("galaxy_shear_cl_ee", ("src0", "src0"), 1.0, ell=10) + sacc_data.add_data_point( + "cmb_convergence_cl", ("cmb_convergence", "cmb_convergence"), 1.0, ell=10 + ) + sacc_data.add_data_point( + "cmbGalaxy_convergenceShear_cl_e", ("cmb_convergence", "src0"), 1.0, ell=10 + ) + + all_fields = extract_all_tracers_projected_fields(sacc_data) + + assert len(all_fields) == 2 + fields_by_name = {f.bin_name: f for f in all_fields} + assert isinstance(fields_by_name["src0"], TomographicBin) + assert isinstance(fields_by_name["cmb_convergence"], CMBLensing) + + # And the combination/extraction pipeline should handle the mix without error. + combinations = extract_all_photoz_bin_combinations(sacc_data) + pairs = { + (xy.x.bin_name, xy.y.bin_name, xy.x_measurement, xy.y_measurement) + for xy in combinations + } + assert ("src0", "src0", Galaxies.SHEAR_E, Galaxies.SHEAR_E) in pairs + assert ( + "cmb_convergence", + "cmb_convergence", + CMB.CONVERGENCE, + CMB.CONVERGENCE, + ) in pairs + assert ( + "cmb_convergence", + "src0", + CMB.CONVERGENCE, + Galaxies.SHEAR_E, + ) in pairs + + harmonic_metadata = extract_all_harmonic_metadata(sacc_data) + harmonic_pairs = {(h.XY.x.bin_name, h.XY.y.bin_name) for h in harmonic_metadata} + assert ("src0", "src0") in harmonic_pairs + assert ("cmb_convergence", "cmb_convergence") in harmonic_pairs + assert ("cmb_convergence", "src0") in harmonic_pairs + + +def test_extract_all_tracers_projected_fields_map_tracer_without_data() -> None: + """A MapTracer with no associated data points is an inconsistent SACC object.""" + sacc_data = sacc.Sacc() + sacc_data.add_tracer("Map", "cmb_convergence", 0, [10, 100, 1000], [1.0, 1.0, 1.0]) + + with pytest.raises( + ValueError, + match="Tracer cmb_convergence does not have data points associated with it.", + ): + _ = extract_all_tracers_projected_fields(sacc_data) + + def test_extract_all_metadata_index_harmonics(sacc_galaxy_cells): sacc_data, _, tracer_pairs = sacc_galaxy_cells @@ -1071,13 +1214,13 @@ def test_make_all_photoz_bin_combinations_with_cmb_basic(): """Test basic functionality of make_all_photoz_bin_combinations_with_cmb.""" # Create test galaxy bins galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ), - InferredGalaxyZDist( + TomographicBin( bin_name="bin_2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1113,7 +1256,7 @@ def test_make_all_photoz_bin_combinations_with_cmb_basic(): def test_make_all_photoz_bin_combinations_with_cmb_with_auto(): """Test make_all_photoz_bin_combinations_with_cmb with CMB auto-correlation.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1147,7 +1290,7 @@ def test_make_all_photoz_bin_combinations_with_cmb_with_auto(): def test_make_all_photoz_bin_combinations_with_cmb_custom_tracer_name(): """Test make_all_photoz_bin_combinations_with_cmb with custom CMB tracer name.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1181,13 +1324,13 @@ def test_make_all_photoz_bin_combinations_with_cmb_custom_tracer_name(): def test_make_all_photoz_bin_combinations_with_cmb_measurement_compatibility(): """Test that only compatible measurements create CMB-galaxy cross-correlations.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="counts_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, # Compatible with CMB.CONVERGENCE ), - InferredGalaxyZDist( + TomographicBin( bin_name="shear_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1240,7 +1383,7 @@ def test_make_all_photoz_bin_combinations_with_cmb_empty_input(): def test_make_all_photoz_bin_combinations_with_cmb_cmb_bin_properties(): """Test that the created CMB bin has correct properties.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1257,8 +1400,7 @@ def test_make_all_photoz_bin_combinations_with_cmb_cmb_bin_properties(): cmb_bin = cmb_combo.x assert cmb_bin.bin_name == "cmb_convergence" - assert np.array_equal(cmb_bin.z, np.array([1100.0])) - assert np.array_equal(cmb_bin.dndz, np.array([1.0])) + assert isinstance(cmb_bin, CMBLensing) assert cmb_bin.measurements == {CMB.CONVERGENCE} assert cmb_bin.type_source == TypeSource.DEFAULT @@ -1267,7 +1409,7 @@ def test_make_all_photoz_bin_combinations_with_cmb_cmb_bin_properties(): def test_make_all_photoz_bin_combinations_with_cmb_parametrized(include_auto: bool): """Parametrized test for CMB auto-correlation inclusion.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1298,7 +1440,7 @@ def test_make_all_photoz_bin_combinations_with_cmb_parametrized(include_auto: bo def test_make_all_photoz_bin_combinations_with_cmb_multiple_measurements(): """Test with galaxy bins that have multiple measurement types.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="multi_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1343,13 +1485,13 @@ def test_make_cmb_galaxy_combinations_only_basic(): """Test basic functionality of make_cmb_galaxy_combinations_only.""" # Create test galaxy bins galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ), - InferredGalaxyZDist( + TomographicBin( bin_name="bin_2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1397,7 +1539,7 @@ def test_make_cmb_galaxy_combinations_only_basic(): def test_make_cmb_galaxy_combinations_only_custom_tracer_name(): """Test make_cmb_galaxy_combinations_only with custom CMB tracer name.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1431,13 +1573,13 @@ def test_make_cmb_galaxy_combinations_only_custom_tracer_name(): def test_make_cmb_galaxy_combinations_only_measurement_compatibility(): """Test that only compatible measurements create CMB-galaxy cross-correlations.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="counts_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, # Compatible with CMB.CONVERGENCE ), - InferredGalaxyZDist( + TomographicBin( bin_name="shear_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1478,7 +1620,7 @@ def test_make_cmb_galaxy_combinations_only_incompatible_measurements_0(): # Create a galaxy bin with a measurement that might not be compatible # (This test depends on what measurements are actually incompatible) galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="test_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1502,7 +1644,7 @@ def test_make_cmb_galaxy_combinations_only_incompatible_measurements_0(): def test_make_cmb_galaxy_combinations_only_cmb_bin_properties(): """Test that the created CMB bin has correct properties.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1519,8 +1661,7 @@ def test_make_cmb_galaxy_combinations_only_cmb_bin_properties(): cmb_bin = cmb_combo.x assert cmb_bin.bin_name == "cmb_convergence" - assert np.array_equal(cmb_bin.z, np.array([1100.0])) - assert np.array_equal(cmb_bin.dndz, np.array([1.0])) + assert isinstance(cmb_bin, CMBLensing) assert cmb_bin.measurements == {CMB.CONVERGENCE} assert cmb_bin.type_source == TypeSource.DEFAULT @@ -1528,7 +1669,7 @@ def test_make_cmb_galaxy_combinations_only_cmb_bin_properties(): def test_make_cmb_galaxy_combinations_only_multiple_measurements(): """Test with galaxy bins that have multiple measurement types.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="multi_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1559,7 +1700,7 @@ def test_make_cmb_galaxy_combinations_only_multiple_measurements(): def test_make_cmb_galaxy_combinations_only_symmetric_pairs(): """Test that symmetric pairs are created for each compatible measurement.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="test_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1591,7 +1732,7 @@ def test_make_cmb_galaxy_combinations_only_symmetric_pairs(): def test_make_cmb_galaxy_combinations_only_single_galaxy_bin(): """Test with a single galaxy bin.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="single_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1615,7 +1756,7 @@ def test_make_cmb_galaxy_combinations_only_single_galaxy_bin(): def test_make_cmb_galaxy_combinations_only_parametrized_names(cmb_name: str): """Parametrized test for different CMB tracer names.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1640,13 +1781,13 @@ def test_make_cmb_galaxy_combinations_only_vs_with_cmb(): make_all_photoz_bin_combinations_with_cmb without galaxy combinations.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="bin_1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, ), - InferredGalaxyZDist( + TomographicBin( bin_name="bin_2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1691,13 +1832,13 @@ def test_make_cmb_galaxy_combinations_only_vs_with_cmb(): def test_make_all_photoz_bin_combinations_with_cmb_incompatible_measurements(): """Test that incompatible measurements are skipped in CMB-galaxy combinations.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="compatible_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.COUNTS}, # Compatible with CMB.CONVERGENCE ), - InferredGalaxyZDist( + TomographicBin( bin_name="another_compatible_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1744,13 +1885,13 @@ def test_make_all_photoz_bin_combinations_with_cmb_incompatible_measurements(): def test_make_cmb_galaxy_combinations_only_incompatible_measurements(): """Test that incompatible measurements are skipped in CMB-galaxy combinations.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="compatible_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_E}, # Compatible with CMB.CONVERGENCE ), - InferredGalaxyZDist( + TomographicBin( bin_name="another_compatible_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1785,13 +1926,13 @@ def test_make_all_photoz_bin_combinations_with_cmb_all_incompatible(): # Since we can't find truly incompatible measurements, let's test with # measurements that we know ARE compatible and adjust expectations galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="compatible_bin1", z=np.linspace(0, 1, 100), dndz=np.ones(100), measurements={Galaxies.SHEAR_T}, # Actually compatible with CMB.CONVERGENCE ), - InferredGalaxyZDist( + TomographicBin( bin_name="compatible_bin2", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1825,7 +1966,7 @@ def test_make_cmb_galaxy_combinations_only_all_incompatible(): # Since we can't find truly incompatible measurements, let's test with # measurements that we know ARE compatible and adjust expectations galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="compatible_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -1842,7 +1983,7 @@ def test_make_cmb_galaxy_combinations_only_all_incompatible(): def test_make_all_photoz_bin_combinations_with_cmb_empty(): """Test behavior when given an empty list of galaxy bins.""" galaxy_bins = [ - InferredGalaxyZDist( + TomographicBin( bin_name="compatible_bin", z=np.linspace(0, 1, 100), dndz=np.ones(100), @@ -2072,7 +2213,7 @@ def test_extract_all_real_metadata_with_or_selector(sacc_galaxy_xis): sacc_data, _, _ = sacc_galaxy_xis # Get all tracers to find specific bin names - tracers = extract_all_tracers_inferred_galaxy_zdists(sacc_data) + tracers = extract_all_tracers_tomographic_bins(sacc_data) if len(tracers) < 2: pytest.skip("Need at least 2 tracers for this test") diff --git a/tests/test_ccl_factory_deprecated.py b/tests/test_ccl_factory_deprecated.py index ce7b592bc..6b5e24a4d 100644 --- a/tests/test_ccl_factory_deprecated.py +++ b/tests/test_ccl_factory_deprecated.py @@ -31,6 +31,8 @@ def test_ccl_factory_deprecation_warning(): # pylint: disable=import-outside-toplevel,unused-import import firecrown.ccl_factory # noqa: F401 + assert firecrown.ccl_factory is not None # Verify import succeeded + def test_ccl_factory_deprecation_warning_content(): """Test the deprecation warning mentions modeling_tools. @@ -41,6 +43,8 @@ def test_ccl_factory_deprecation_warning_content(): # pylint: disable=import-outside-toplevel,unused-import import firecrown.ccl_factory # noqa: F401 + assert firecrown.ccl_factory is not None # Verify import succeeded + # If we get here without error, the import worked @@ -74,6 +78,9 @@ def test_all_items_importable(): # pylint: disable=too-many-locals +@pytest.mark.filterwarnings( + r"ignore:firecrown\.ccl_factory is deprecated.*:DeprecationWarning" +) def test_items_identical_to_new_location(): """Test that imported items are the same objects as in modeling_tools.""" # pylint: disable=import-outside-toplevel diff --git a/tests/test_cmb_factories.py b/tests/test_cmb_factories.py index 5ca492e61..ddb500b2c 100644 --- a/tests/test_cmb_factories.py +++ b/tests/test_cmb_factories.py @@ -2,7 +2,6 @@ from unittest import mock -import numpy as np import pytest import sacc @@ -11,7 +10,7 @@ CMBConvergenceArgs, CMBConvergenceFactory, ) -from firecrown.metadata_types import CMB, InferredGalaxyZDist +from firecrown.metadata_types import CMB, CMBLensing from firecrown.modeling_tools import ModelingTools from firecrown.updatable import ParamsMap @@ -136,11 +135,10 @@ def test_cmb_convergence_factory_create(): """Test CMBConvergenceFactory create method.""" factory = CMBConvergenceFactory(z_source=1090.0, scale=1.5) - # Create a mock InferredGalaxyZDist with CMB measurements - mock_zdist = InferredGalaxyZDist( + # Create a mock TomographicBin with CMB measurements + mock_zdist = CMBLensing( bin_name="cmb_bin", - z=np.linspace(0, 2, 100), - dndz=np.ones(100), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) @@ -155,10 +153,9 @@ def test_cmb_convergence_factory_create_caching(): """Test that CMBConvergenceFactory caches created objects.""" factory = CMBConvergenceFactory() - mock_zdist = InferredGalaxyZDist( + mock_zdist = CMBLensing( bin_name="cmb_bin", - z=np.linspace(0, 2, 100), - dndz=np.ones(100), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) @@ -223,10 +220,9 @@ def test_cmb_convergence_factory_different_params(): factory1 = CMBConvergenceFactory(z_source=1090.0, scale=1.0) factory2 = CMBConvergenceFactory(z_source=1100.0, scale=2.0) - mock_zdist = InferredGalaxyZDist( + mock_zdist = CMBLensing( bin_name="cmb_bin", - z=np.linspace(0, 2, 100), - dndz=np.ones(100), + z_lss=1100.0, measurements={CMB.CONVERGENCE}, ) diff --git a/tests/test_number_counts_source.py b/tests/test_number_counts_source.py index 0b51f9637..f85c2257c 100644 --- a/tests/test_number_counts_source.py +++ b/tests/test_number_counts_source.py @@ -3,15 +3,15 @@ import firecrown.likelihood.number_counts as nc import firecrown.metadata_types as mt import firecrown.modeling_tools as mtools -from firecrown import parameters +from firecrown import updatable def test_get_derived_parameters( - harmonic_bin_1: mt.InferredGalaxyZDist, + harmonic_bin_1: mt.TomographicBin, tools_with_vanilla_cosmology: mtools.ModelingTools, ): ncs = nc.NumberCounts.create_ready(harmonic_bin_1, derived_scale=True) - ncs.update(parameters.ParamsMap({"bin_1_bias": 1.0})) + ncs.update(updatable.ParamsMap({"bin_1_bias": 1.0})) ncs.create_tracers(tools_with_vanilla_cosmology) params = ncs.get_derived_parameters() assert params is not None diff --git a/tests/test_parameters_deprecated.py b/tests/test_parameters_deprecated.py index 7b1d1159c..54f18ab8e 100644 --- a/tests/test_parameters_deprecated.py +++ b/tests/test_parameters_deprecated.py @@ -7,6 +7,7 @@ import sys import warnings +import pytest def test_parameters_module_emits_deprecation_warning(): @@ -22,6 +23,7 @@ def test_parameters_module_emits_deprecation_warning(): # pylint: disable=unused-import,import-outside-toplevel import firecrown.parameters # noqa: F401 + assert firecrown.parameters is not None # Verify import succeeded # Should have at least one warning (there might be multiple due to # imports within the module) assert len(w) >= 1 @@ -89,6 +91,9 @@ def test_params_map_from_deprecated_module(): assert params.get("b") == 2.0 +@pytest.mark.filterwarnings( + r"ignore:.*firecrown\.parameters module is deprecated.*:DeprecationWarning" +) def test_required_parameters_from_deprecated_module(): """Ensure RequiredParameters imported from deprecated module works correctly.""" # pylint: disable=import-outside-toplevel diff --git a/tests/test_updatable.py b/tests/test_updatable.py index d2a596703..31f6ae2b3 100644 --- a/tests/test_updatable.py +++ b/tests/test_updatable.py @@ -7,7 +7,6 @@ import numpy as np import pytest -from firecrown import parameters from firecrown.updatable import ( DerivedParameter, DerivedParameterCollection, @@ -20,6 +19,7 @@ get_default_params, get_default_params_map, ) +from firecrown import updatable class MinimalUpdatable(Updatable): @@ -28,7 +28,7 @@ class MinimalUpdatable(Updatable): def __init__(self, prefix: str | None = None): """Initialize object with defaulted value.""" super().__init__(prefix) - self.a = parameters.register_new_updatable_parameter(default_value=1.0) + self.a = updatable.register_new_updatable_parameter(default_value=1.0) class SimpleUpdatable(Updatable): # pylint: disable=too-many-instance-attributes @@ -38,8 +38,8 @@ def __init__(self, prefix: str | None = None): """Initialize object with defaulted values.""" super().__init__(prefix) - self.x = parameters.register_new_updatable_parameter(default_value=2.0) - self.y = parameters.register_new_updatable_parameter(default_value=3.0) + self.x = updatable.register_new_updatable_parameter(default_value=2.0) + self.y = updatable.register_new_updatable_parameter(default_value=3.0) class UpdatableWithDerived(Updatable): @@ -49,8 +49,8 @@ def __init__(self): """Initialize object with defaulted values.""" super().__init__() - self.A = parameters.register_new_updatable_parameter(default_value=2.0) - self.B = parameters.register_new_updatable_parameter(default_value=1.0) + self.A = updatable.register_new_updatable_parameter(default_value=2.0) + self.B = updatable.register_new_updatable_parameter(default_value=1.0) def _get_derived_parameters(self) -> DerivedParameterCollection: derived_scale = DerivedParameter("Section", "Name", self.A + self.B) @@ -99,11 +99,11 @@ def test_updatable_record_with_internal_params(): obj = SimpleUpdatable("test") obj.set_internal_parameter( "internal1", - parameters.register_new_updatable_parameter(value=1.0, default_value=1.0), + updatable.register_new_updatable_parameter(value=1.0, default_value=1.0), ) obj.set_internal_parameter( "internal2", - parameters.register_new_updatable_parameter(value=2.0, default_value=2.0), + updatable.register_new_updatable_parameter(value=2.0, default_value=2.0), ) params = ParamsMap({"test_x": 1.0, "test_y": 2.0}) @@ -337,7 +337,7 @@ def test_updatable_collection_insertion(): def test_set_sampler_parameter(): my_updatable = MinimalUpdatable() - my_param = parameters.register_new_updatable_parameter(default_value=42.0) + my_param = updatable.register_new_updatable_parameter(default_value=42.0) my_param.set_fullname(prefix=None, name="the_meaning_of_life") my_updatable.set_sampler_parameter(my_param) @@ -347,7 +347,7 @@ def test_set_sampler_parameter(): def test_set_sampler_parameter_rejects_internal_parameter(): my_updatable = MinimalUpdatable() - my_param = parameters.register_new_updatable_parameter( + my_param = updatable.register_new_updatable_parameter( value=42.0, default_value=41.0 ) @@ -357,9 +357,9 @@ def test_set_sampler_parameter_rejects_internal_parameter(): def test_set_sampler_parameter_rejects_duplicates(): my_updatable = MinimalUpdatable() - my_param = parameters.register_new_updatable_parameter(default_value=42.0) + my_param = updatable.register_new_updatable_parameter(default_value=42.0) my_param.set_fullname(prefix=None, name="the_meaning_of_life") - my_param_same = parameters.register_new_updatable_parameter(default_value=42.0) + my_param_same = updatable.register_new_updatable_parameter(default_value=42.0) my_param_same.set_fullname(prefix=None, name="the_meaning_of_life") my_updatable.set_sampler_parameter(my_param) @@ -372,7 +372,7 @@ def test_set_internal_parameter(): my_updatable = MinimalUpdatable() my_updatable.set_internal_parameter( "the_meaning_of_life", - parameters.register_new_updatable_parameter(value=1.0, default_value=42.0), + updatable.register_new_updatable_parameter(value=1.0, default_value=42.0), ) assert hasattr(my_updatable, "the_meaning_of_life") @@ -381,7 +381,7 @@ def test_set_internal_parameter(): def test_set_parameter_using_internal_parameter(): my_updatable = MinimalUpdatable() - ip = parameters.InternalParameter(2112) + ip = updatable.InternalParameter(2112) my_updatable.set_parameter("epic_Rush_album", ip) assert hasattr(my_updatable, "epic_Rush_album") @@ -393,7 +393,7 @@ def test_set_internal_parameter_rejects_sampler_parameter(): with pytest.raises(TypeError): my_updatable.set_internal_parameter( "sampler_param", - parameters.register_new_updatable_parameter(default_value=1.0), + updatable.register_new_updatable_parameter(default_value=1.0), ) @@ -401,13 +401,13 @@ def test_set_internal_parameter_rejects_duplicates(): my_updatable = MinimalUpdatable() my_updatable.set_internal_parameter( "the_meaning_of_life", - parameters.register_new_updatable_parameter(value=1.0, default_value=42.0), + updatable.register_new_updatable_parameter(value=1.0, default_value=42.0), ) with pytest.raises(ValueError): my_updatable.set_internal_parameter( "the_meaning_of_life", - parameters.register_new_updatable_parameter(value=1.0, default_value=42.0), + updatable.register_new_updatable_parameter(value=1.0, default_value=42.0), ) @@ -415,11 +415,11 @@ def test_set_parameter(): my_updatable = MinimalUpdatable() my_updatable.set_parameter( "the_meaning_of_life", - parameters.register_new_updatable_parameter(value=1.0, default_value=42.0), + updatable.register_new_updatable_parameter(value=1.0, default_value=42.0), ) my_updatable.set_parameter( "no_meaning_of_life", - parameters.register_new_updatable_parameter(default_value=42.0), + updatable.register_new_updatable_parameter(default_value=42.0), ) assert hasattr(my_updatable, "the_meaning_of_life") @@ -433,7 +433,7 @@ def test_update_rejects_internal_parameters(): my_updatable = MinimalUpdatable() my_updatable.set_internal_parameter( "the_meaning_of_life", - parameters.register_new_updatable_parameter(value=2.0, default_value=42.0), + updatable.register_new_updatable_parameter(value=2.0, default_value=42.0), ) assert hasattr(my_updatable, "the_meaning_of_life") @@ -529,8 +529,8 @@ def test_nesting_updatables_missing_parameters(nested_updatables): base.update(params) - for updatable in nested_updatables: - assert updatable.is_updated() + for my_updatable in nested_updatables: + assert my_updatable.is_updated() def test_nesting_updatables_required_parameters(nested_updatables): diff --git a/tutorial/_quarto.yml b/tutorial/_quarto.yml index fa3ceccc7..5d14cc387 100644 --- a/tutorial/_quarto.yml +++ b/tutorial/_quarto.yml @@ -7,7 +7,7 @@ project: - development_example.qmd - intro_article.qmd - two_point_framework.qmd - - inferred_zdist.qmd + - tomographic_bin.qmd - inferred_zdist_generators.qmd - inferred_zdist_serialization.qmd - two_point_generators.qmd @@ -40,12 +40,12 @@ website: contents: - section: "Tomographic Bins" contents: - - href: inferred_zdist.qmd - text: "Distributions" + - href: tomographic_bin.qmd + text: "Inferred Redshift Distributions" - href: inferred_zdist_generators.qmd - text: "Distribution Generators" + text: "Redshift Distribution Generators" - href: inferred_zdist_serialization.qmd - text: "Serialization" + text: "Redshift Distribution Serialization" - section: "Two-Point" contents: - href: two_point_framework.qmd diff --git a/tutorial/inferred_zdist_generators.qmd b/tutorial/inferred_zdist_generators.qmd index a487d3a25..c47b3e457 100644 --- a/tutorial/inferred_zdist_generators.qmd +++ b/tutorial/inferred_zdist_generators.qmd @@ -8,7 +8,7 @@ format: html ## Purpose of this document -In the previous [tutorial](inferred_zdist.qmd), we explored how to use `InferredGalaxyZDist` objects to represent redshift distributions for galaxies. +In the previous [tutorial](tomographic_bin.qmd), we explored how to use `TomographicBin` objects to represent redshift distributions for galaxies. These distributions are necessary for modeling galaxy redshift distributions and can be created using various methods. In this tutorial, we will focus on generating these distributions using the `ZDistLSSTSRD` class in Firecrown. This class generates redshift distributions in accordance with the LSST Science Requirements Document (SRD). @@ -19,7 +19,7 @@ P(z|B_i, \theta) \equiv \frac{\mathrm{d}n}{\mathrm{d}z}(z;B_i, \theta), $${#eq-Pi} where $B_i$ is the $i$-th bin of the photometric redshifts, and $\theta$ is a set of parameters that describe the model. -In Firecrown this distribution is represented as an object of type `InferredGalaxyZDist`[^1]. +In Firecrown this distribution is represented as an object of type `TomographicBin`[^1]. An object of this type contains the redshifts, the corresponding probabilities, and the data-type of the measurements. Firecrown also provides facilities to generate these distributions, given a chosen model. In `firecrown.generators`, we have the `ZDistLSSTSRD` class, which can be used to generate redshift distributions according to the LSST SRD. @@ -132,7 +132,7 @@ d_y_all = pd.concat( ) ``` -Next, using the same SRD prescriptions, we want to generate the `InferredGalaxyZDist` objects representing @eq-Pi for a specific binning, and using a specific resolution parameter $\sigma_z$. +Next, using the same SRD prescriptions, we want to generate the `TomographicBin` objects representing @eq-Pi for a specific binning, and using a specific resolution parameter $\sigma_z$. Here we show the first bin for the lens and source samples for both Year 1 and Year 10, the functions are evaluated at $100$ points equally spaced between $0$ and $0.6$.[^3] [^3]: Note that we are making use of several module-level constants in the `firecrown.generators` module, namely `Y1_LENS_BINS`, `Y10_LENS_BINS`, `Y1_SOURCE_BINS`, and `Y10_SOURCE_BINS`, which are dictionaries that contain the bin edges and the resolution parameter $\sigma_z$ for the bins. @@ -140,7 +140,7 @@ Here we show the first bin for the lens and source samples for both Year 1 and Y ```{python} import numpy as np import firecrown -from firecrown.metadata_types import InferredGalaxyZDist +from firecrown.metadata_types import TomographicBin from firecrown.generators import ( ZDistLSSTSRD, Y1_LENS_BINS, @@ -155,7 +155,7 @@ z = np.linspace(0.0, 0.6, 100) # We use the same zdist_y1_* and zdist_y10_* that were created above. -# We create two InferredGalaxyZDist objects, one for Y1 and one for Y10. +# We create two TomographicBin objects, one for Y1 and one for Y10. Pz_lens0_y1 = zdist_y1_lens.binned_distribution( zpl=Y1_LENS_BINS["edges"][0], zpu=Y1_LENS_BINS["edges"][1], @@ -190,13 +190,13 @@ Pz_source0_y10 = zdist_y10_source.binned_distribution( ) # Next we check that the objects we created are of the expected type. -assert isinstance(Pz_lens0_y1, InferredGalaxyZDist) -assert isinstance(Pz_lens0_y10, InferredGalaxyZDist) -assert isinstance(Pz_source0_y1, InferredGalaxyZDist) -assert isinstance(Pz_source0_y10, InferredGalaxyZDist) +assert isinstance(Pz_lens0_y1, TomographicBin) +assert isinstance(Pz_lens0_y10, TomographicBin) +assert isinstance(Pz_source0_y1, TomographicBin) +assert isinstance(Pz_source0_y10, TomographicBin) ``` -The plot of the $\textrm{d}N/\textrm{d}z$ distributions in these `InferredGalaxyZDist` objects is shown in @fig-inferred-dist. +The plot of the $\textrm{d}N/\textrm{d}z$ distributions in these `TomographicBin` objects is shown in @fig-inferred-dist. ```{python} # | label: fig-inferred-dist @@ -257,7 +257,7 @@ The following code block demonstrates how to do this. from itertools import pairwise # We use the same zdist_y1 and zdist_y10 that were created above. -# We create two InferredGalaxyZDist objects, one for Y1 and one for Y10. +# We create two TomographicBin objects, one for Y1 and one for Y10. z = np.linspace(0.0, 3.5, 1000) all_y1_bins = [ zdist_y1_lens.binned_distribution( @@ -282,7 +282,7 @@ all_y1_bins = [ ] ``` -The plot of the $\textrm{d}N/\textrm{d}z$ distributions in these `InferredGalaxyZDist` objects is shown in @fig-inferred-dist-all. +The plot of the $\textrm{d}N/\textrm{d}z$ distributions in these `TomographicBin` objects is shown in @fig-inferred-dist-all. ```{python} # | label: fig-inferred-dist-all @@ -433,5 +433,5 @@ percent_diff = 100.0 * (N_gal - N_gal_phot) / N_gal ## End note Note that these facilities for creating redshift distributions are not limitations on the use of the distributions. -Any facility that can create the $z$ and $\textrm{d}N/\textrm{d}z$ arrays can be used to create an `InferredGalaxyZDist` object. -Code that uses the `InferredGalaxyZDist` objects does not depend on how they were created. +Any facility that can create the $z$ and $\textrm{d}N/\textrm{d}z$ arrays can be used to create an `TomographicBin` object. +Code that uses the `TomographicBin` objects does not depend on how they were created. diff --git a/tutorial/inferred_zdist_serialization.qmd b/tutorial/inferred_zdist_serialization.qmd index bcf09fed1..ab9811f54 100644 --- a/tutorial/inferred_zdist_serialization.qmd +++ b/tutorial/inferred_zdist_serialization.qmd @@ -10,7 +10,7 @@ format: html In the previous [tutorial](inferred_zdist_generators.qmd), we discussed using the `ZDistLSSTSRD` class in Firecrown to generate redshift distributions for galaxies. In this tutorial, we will cover how to serialize these distributions to disk and read them back in. -While `InferredGalaxyZDist` objects can be serialized, the resulting files are not human-readable because they contain the final redshift distribution. +While `TomographicBin` objects can be serialized, the resulting files are not human-readable because they contain the final redshift distribution. To achieve a human-readable format, we need to serialize the parameters used to generate the distribution. Firecrown addresses this by introducing the `ZDistLSSTSRDBin` and `ZDistLSSTSRDBinCollection` dataclasses.[^1] @@ -116,7 +116,7 @@ assert bin_collection.bins[0].measurements == bin_collection_read.bins[0].measur assert bin_collection.bins[0].z == bin_collection_read.bins[0].z ``` -Calling `generate()` on the read object will generate the `InferredGalaxyZDist` redshift distribution dataclasses. +Calling `generate()` on the read object will generate the redshift distribution dataclasses `TomographicBin`. ```{python} from pprint import pprint diff --git a/tutorial/inferred_zdist.qmd b/tutorial/tomographic_bin.qmd similarity index 88% rename from tutorial/inferred_zdist.qmd rename to tutorial/tomographic_bin.qmd index 410ec29bd..27d4dd5b2 100644 --- a/tutorial/inferred_zdist.qmd +++ b/tutorial/tomographic_bin.qmd @@ -1,5 +1,5 @@ --- -title: "Using Firecrown InferredGalaxyZDist objects" +title: "Using Firecrown TomographicBin objects" subtitle: "Version {{< env FIRECROWN_VERSION >}}" format: html --- @@ -13,24 +13,24 @@ The galaxy redshift distributions play a crucial role in modeling the distributi ## Overview -Firecrown employs `InferredGalaxyZDist` objects to encapsulate these distributions. +Firecrown employs `TomographicBin` objects to encapsulate these distributions. These objects are essential for representing the redshift distribution of galaxies within predefined photometric redshift bins. Additionally, Firecrown utilizes metadata dataclasses to handle metadata and calibration data pertinent to cosmological analyses. These dataclasses can be constructed directly, extracted from SACC objects, or generated through Firecrown's provided functionalities. -This document outlines the process of creating `InferredGalaxyZDist` objects directly and demonstrates their utilization in cosmological analyses. +This document outlines the process of creating `TomographicBin` objects directly and demonstrates their utilization in cosmological analyses. -## Creating an `InferredGalaxyZDist` Object +## Creating an `TomographicBin` Object -The `InferredGalaxyZDist` object serves as the cornerstone for representing the redshift distribution of galaxies within a designated photometric redshift bin. Each bin is identified by a string identifier, `bin_name`, utilized by theoretical models to specify parameters specific to the corresponding redshift bin. +The `TomographicBin` object serves as the cornerstone for representing the redshift distribution of galaxies within a designated photometric redshift bin. Each bin is identified by a string identifier, `bin_name`, utilized by theoretical models to specify parameters specific to the corresponding redshift bin. -The `InferredGalaxyZDist` object can be created by providing the following parameters: +The `TomographicBin` object can be created by providing the following parameters: ```{python} -from firecrown.metadata_types import Galaxies, InferredGalaxyZDist +from firecrown.metadata_types import Galaxies, TomographicBin import numpy as np z = np.linspace(0.0, 1.0, 200) -lens0 = InferredGalaxyZDist( +lens0 = TomographicBin( bin_name="lens0", z=z, dndz=np.exp(-0.5 * ((z - 0.5) / 0.02) ** 2) / np.sqrt(2 * np.pi) / 0.02, @@ -183,7 +183,7 @@ data = pd.DataFrame( ## Conclusion This document has provided an overview of Firecrown's capabilities in representing galaxy redshift distributions and utilizing them in cosmological analyses. -The `InferredGalaxyZDist` object encapsulates the redshift distribution of galaxies within predefined photometric redshift bins. +The `TomographicBin` object encapsulates the redshift distribution of galaxies within predefined photometric redshift bins. The `TwoPointHarmonic` object is used to encapsulate the two-point correlation function in harmonic space, while the `TwoPoint` object encapsulates the theoretical model for the two-point correlation function. These objects are essential for modeling the distribution of galaxies within specific photometric redshift bins and are crucial for cosmological analyses. diff --git a/tutorial/two_point_framework.qmd b/tutorial/two_point_framework.qmd index 857e01adc..81ee01ce7 100644 --- a/tutorial/two_point_framework.qmd +++ b/tutorial/two_point_framework.qmd @@ -18,7 +18,7 @@ If you are looking for practical instructions on how to use the system, you may ## Introduction Firecrown’s two-point likelihood framework is built around a structured hierarchy of metadata types that describe the configuration and organization of cosmological measurements. -At the foundation are basic bin descriptors, such as [[metadata_types|InferredGalaxyZDist]], which define properties of individual observables or selections. +At the foundation are basic bin descriptors, such as [[metadata_types|TomographicBin]], which define properties of individual observables or selections. These descriptors are not directly tied to specific measurements but are shared across multiple components in a complex analysis. This allows Firecrown to ensure that components that should have identical descriptors have exactly identical descriptors, by construction. @@ -47,7 +47,7 @@ The complete hierarchy is visualized in @fig-framework-hierarchy. %%| fig-cap-location: margin %%| fig-cap: "Hierarchy of types in the Firecrown two-point statistic framework." graph TD - A["Basic Bin Description
e.g. InferredGalaxyZDist"] + A["Basic Bin Description
e.g. TomographicBin"] A --> B["TwoPointXY
(bin combination and
measured types)"] B --> C1["TwoPointHarmonic
(ell layout)"] B --> C2["TwoPointReal
(theta layout)"] @@ -65,8 +65,8 @@ graph TD The foundation of Firecrown’s two-point framework begins with a description of individual bins, represented as dataclasses. These classes encapsulate the metadata required to define a bin used in cosmological measurements. -A key example is the [[metadata_types|InferredGalaxyZDist]] class, which describes the inferred redshift distribution of a single bin. -Each bin is labeled using a [[metadata_types|InferredGalaxyZDist.bin_name]] and includes arrays [[metadata_types|InferredGalaxyZDist.z]] and [[metadata_types|InferredGalaxyZDist.dndz]] representing the redshift and corresponding distribution. +A key example is the [[metadata_types|TomographicBin]] class, which describes the inferred redshift distribution of a single bin. +Each bin is labeled using a [[metadata_types|TomographicBin.bin_name]] and includes arrays [[metadata_types|TomographicBin.z]] and [[metadata_types|TomographicBin.dndz]] representing the redshift and corresponding distribution. Additionally, it contains a set of [[metadata_types|Measurement]] types (e.g., [[metadata_types|Galaxies.COUNTS]], [[metadata_types|Galaxies.SHEAR_T]], [[metadata_types|CMB.CONVERGENCE]])[^enumerations] indicating which observables were measured in this bin. It is valid to include multiple measurement types for the same bin, such as galaxy number counts and shear measured on the same galaxy subsample. @@ -77,10 +77,10 @@ It is valid to include multiple measurement types for the same bin, such as gala Such additions are made in `firecrown/metadata_types.py`. Firecrown's use of type checking should ensure that any code that would be invalidated by adding a new type, or value, is identified. -To further differentiate among subpopulations, each bin may be tagged with a [[metadata_types|InferredGalaxyZDist.type_source]]. +To further differentiate among subpopulations, each bin may be tagged with a [[metadata_types|TomographicBin.type_source]]. This identifier, while typically a simple string[^type_source], distinguishes between subsets within the same measurement category. -For example, in galaxy surveys, [[metadata_types|InferredGalaxyZDist.type_source]] might refer to red vs. blue galaxies, or in CMB lensing, to Planck vs. SPT data. -This tag allows the modeling framework to apply different theoretical treatments or nuisance parametrization to distinct subcomponents while keeping the overall bin structure unified. +For example, in galaxy surveys, [[metadata_types|TomographicBin.type_source]] might refer to red vs. blue galaxies, or in CMB lensing, to Planck vs. SPT data. +This tag allows the modeling framework to apply different theoretical treatments or nuisance parameterizations to distinct subcomponents while keeping the overall bin structure unified. [^type_source]: More precisely, `type_source` is actually of type `TypeSource`, which is a subclass of `str`. This type has some additional functionality used by the metadata system for validation, and which is usually transparent to users of Firecrown. @@ -91,21 +91,21 @@ The implementation is as follows: #| echo: false #| output: asis from firecrown.fctools.print_code import display_class_attributes -from firecrown.metadata_types import InferredGalaxyZDist -display_class_attributes(InferredGalaxyZDist) +from firecrown.metadata_types import TomographicBin +display_class_attributes(TomographicBin) ``` Subclassing [[utils|YAMLSerializable]] enables these objects to be saved to and loaded from YAML files, facilitating configuration and reproducibility. -The dataclass is *frozen*, indicating that `InferredGalaxyZDist` objects cannot be modified after creation. -Modifying an `InferredGalaxyZDist` object that may be shared between multiple `TwoPoint` objects could lead to unexpected behavior. +The dataclass is *frozen*, indicating that `TomographicBin` objects cannot be modified after creation. +Modifying an `TomographicBin` object that may be shared between multiple `TwoPoint` objects could lead to unexpected behavior. It is also marked with *kw_only*, ensuring that only keyword arguments are accepted during construction, thus ensuring that use of the dataclass is mostly self-documenting. In other tutorials, we explore the following topics: -1. **[Inferred Redshift Distributions](inferred_zdist.qmd)**: A guide to utilizing Firecrown's capabilities to describe galaxy redshift distributions for cosmological analyses. +1. **[Inferred Redshift Distributions](tomographic_bin.qmd)**: A guide to utilizing Firecrown's capabilities to describe galaxy redshift distributions for cosmological analyses. -2. **[Redshift Distribution Generators](inferred_zdist_generators.qmd)**: Demonstrates how to generate [[metadata_types|InferredGalaxyZDist]] objects from LSST SRD redshift distributions. -3. **[Redshift Distribution Serialization](inferred_zdist_serialization.qmd)**: Explains how to serialize and deserialize [[metadata_types|InferredGalaxyZDist]] objects and use the [[generators|ZDistLSSTSRDBin]] and [[generators|ZDistLSSTSRDBinCollection]] generators dataclasses. +2. **[Redshift Distribution Generators](inferred_zdist_generators.qmd)**: Demonstrates how to generate [[metadata_types|TomographicBin]] objects from LSST SRD redshift distributions. +3. **[Redshift Distribution Serialization](inferred_zdist_serialization.qmd)**: Explains how to serialize and deserialize [[metadata_types|TomographicBin]] objects and use the [[generators|ZDistLSSTSRDBin]] and [[generators|ZDistLSSTSRDBinCollection]] generators dataclasses. # Bin Combinations: [[metadata_types|TwoPointXY]] @@ -120,11 +120,11 @@ In practice, the same pair of bins may appear in multiple combinations, each cor ```{python} #| eval: false -bin1 = InferredGalaxyZDist( +bin1 = TomographicBin( z=..., dndz=..., measurements={Galaxies.COUNTS, Galaxies.SHEAR_E} ) -bin2 = InferredGalaxyZDist( +bin2 = TomographicBin( z=..., dndz=..., measurements={Galaxies.COUNTS, Galaxies.SHEAR_E} ) @@ -191,7 +191,7 @@ It also enables use cases such as theoretical predictions or forecasts where dat # Two-Point Data Container: [[data_types|TwoPointMeasurement]] -The previous types ([[metadata_types|InferredGalaxyZDist]], [[metadata_types|TwoPointXY]], [[metadata_types|TwoPointHarmonic]], [[metadata_types|TwoPointReal]]) define the structure and layout of a two-point measurement, what is being measured and how, but do not include actual data. +The previous types ([[metadata_types|TomographicBin]], [[metadata_types|TwoPointXY]], [[metadata_types|TwoPointHarmonic]], [[metadata_types|TwoPointReal]]) define the structure and layout of a two-point measurement, what is being measured and how, but do not include actual data. These layout descriptions can be used independently of any measurement data, for example, in computing theoretical predictions, plotting expectations, or running forecasts. To represent an actual measurement, either from observational data or a simulated dataset, the framework introduces the [[data_types|TwoPointMeasurement]] class. @@ -221,11 +221,11 @@ This framework provides a structured approach for managing two-point correlation It separates concerns into different components, each with a specific role, and supports both theoretical predictions and actual observational data. All dataclasses below are frozen for immutability and support YAML serialization. -1. **[[metadata_types|InferredGalaxyZDist]]**: +1. **[[metadata_types|TomographicBin]]**: - Represents the metadata for a particular bin, which includes redshift distributions, measurement types (e.g., galaxy counts, shear), and a [[metadata_types|TypeSource]] to distinguish subpopulations (e.g., red or blue galaxies). 2. **[[metadata_types|TwoPointXY]]**: - - Combines two [[metadata_types|InferredGalaxyZDist]] bins and specifies which measurements (e.g., galaxy counts, shear) are being cross-correlated. + - Combines two [[metadata_types|TomographicBin]] bins and specifies which measurements (e.g., galaxy counts, shear) are being cross-correlated. - This object encapsulates the logic for validating that the selected measurements match the bins involved, avoiding duplication of bin metadata. 3. **[[metadata_types|TwoPointHarmonic]] and [[metadata_types|TwoPointReal]]**: diff --git a/tutorial/two_point_generators.qmd b/tutorial/two_point_generators.qmd index 21d359099..96194b059 100644 --- a/tutorial/two_point_generators.qmd +++ b/tutorial/two_point_generators.qmd @@ -10,7 +10,7 @@ format: html In the tutorial [redshift distributions](inferred_zdist_generators.qmd) we illustrate the process of using Firecrown to create redshift distributions based on specified parameters describing the distributions. Additionally, in [serializable redshift distributions](inferred_zdist_serialization.qmd) we demonstrate how to generate redshift distributions using serializable objects. -This document outlines how to use `InferredGalaxyZDist` objects to derive the two-point statistics pertinent to the Large Synoptic Survey Telescope (LSST), employing the redshift distribution outlined in the LSST Science Requirements Document (SRD). +This document outlines how to use `TomographicBin` objects to derive the two-point statistics pertinent to the Large Synoptic Survey Telescope (LSST), employing the redshift distribution outlined in the LSST Science Requirements Document (SRD). This tutorial focuses on generating two-point statistics from scratch using metadata. For loading existing data from SACC files and applying scale cuts, see: - [Two-Point Factory Basics](two_point_factory_basics.qmd)