Source code for slsim.FalsePositives.false_positive_pop

from slsim.FalsePositives.false_positive import FalsePositive
from slsim.Lenses.lens_pop import area_theta_e_infinity
from slsim.LOS.los_pop import LOSPop
import random
import numpy as np


[docs] class FalsePositivePopBase(object): """Base class for false positive population generation. This class provides common functionality and can be extended to create specific types of false positive populations. """ def __init__( self, central_galaxy_population, cosmo=None, los_pop=None, ): """ :param central_galaxy_population: Deflector population as a deflectors class instance. :param cosmo: astropy.cosmology instance :param los_pop: LOSPop instance which manages line-of-sight (LOS) effects and Gaussian mixture models in a simulation or analysis context. """ self.cosmo = cosmo self._lens_galaxies = central_galaxy_population self.los_config = los_pop or LOSPop()
[docs] def draw_deflector(self): """Draw and prepare a deflector (lens) with tolerance-based z_max. :return: a deflector instance and z_max for sources. """ deflector = self._lens_galaxies.draw_deflector() z_max = deflector.redshift + 0.002 # Adding tolerance to redshift return deflector, z_max
[docs] class FalsePositivePop(FalsePositivePopBase): """Class to perform samples of false positive population. This class generates configurations consisting of a central deflector and one or multiple sources drawn from provided populations (e.g., surrounding galaxies, stars, quasars). It combines the functionality of single-source and multi-source generation with flexible clustering modes. Note that these sources are not lensed by the deflector, but are positioned in a way that they could be misidentified as lensed images of a source due to the deflector, thus creating a "false positive" lensing configuration. """ def __init__( self, central_galaxy_population, intruder_populations, intruder_number_choices=[1, 2, 3], weights_for_intruder_number=None, cosmo=None, los_pop=None, test_area_factor=1, clustering_mode="area", include_central_galaxy_light=True, ): """ :param central_galaxy_population: Deflector population as a deflectors class instance. :param intruder_populations: A single intruder population or a list of intruder populations. These can be instances of SourcePopulation or any class that has a `draw_source` method which returns a Source instance. Each population represents a different type of "intruder" source (e.g., galaxies, stars, quasars) that can be drawn and positioned around the central deflector to create false positive configurations. If a single population is provided, it will be used for all intruder draws. :param intruder_number_choices: A list of integers (for a single population) or a list of lists containing integers (for multiple populations) representing the possible number of intruders to draw. Defaults to [1, 2, 3]. Can also provide a single integer for a fixed number of intruders. :param weights_for_intruder_number: Weights corresponding to probabilities for `intruder_number_choices`. Either a single list of weights (for a single population) or a list of lists of weights (for multiple populations). :param cosmo: astropy.cosmology instance :param los_pop: LOSPop instance. :param test_area_factor: A multiplicative factor for the test_area. :param clustering_mode: 'area' (default) places sources within the calculated test area, 'ring' for ring-like clustering around the deflector (places sources in a ring within a range of 0.5 to 2.5 times the Einstein radius), :param include_central_galaxy_light: Whether to include central galaxy light. """ super(FalsePositivePop, self).__init__( central_galaxy_population=central_galaxy_population, cosmo=cosmo, los_pop=los_pop, ) # Normalize populations and choices into lists for uniform handling if not isinstance(intruder_populations, list): self._intruder_populations = [intruder_populations] self._number_choices = [intruder_number_choices] self._weights = ( [weights_for_intruder_number] if weights_for_intruder_number else [None] ) else: self._intruder_populations = intruder_populations self._number_choices = intruder_number_choices self._weights = ( weights_for_intruder_number if weights_for_intruder_number else [None] * len(intruder_populations) ) # Basic validation if len(self._intruder_populations) != len(self._number_choices): raise ValueError( "The length of 'intruder_populations' must match the length of 'intruder_number_choices'." ) self._clustering_mode = clustering_mode self._test_area_factor = test_area_factor self._include_central_galaxy_light = include_central_galaxy_light
[docs] def draw_sources(self, z_max=None, test_area=None, theta_e=None): """Draws sources from all populations based on choices and positions them. :param z_max: maximum redshift for drawn source. :param test_area: area to draw source coordinates for 'area' clustering mode. :param theta_e: Einstein radius of the deflector, needed for 'ring'/'random' modes. :return: A Source instance or a list of Source instances. """ all_sources = [] # Iterate over each population and its corresponding number choices for pop, choices, weights in zip( self._intruder_populations, self._number_choices, self._weights ): if isinstance(choices, int): n_draw = choices else: n_draw = ( random.choices(choices, weights=weights)[0] if weights else random.choice(choices) ) for _ in range(n_draw): source = pop.draw_source(z_max=z_max) if source is None: return None all_sources.append(source) if not all_sources: return None total_sources = len(all_sources) for i, source in enumerate(all_sources): if self._clustering_mode == "ring": r = random.uniform(0.5 * theta_e, 2.5 * theta_e) phi = (2 * np.pi * i / total_sources) + random.uniform( -0.3, 0.3 ) # Adding some randomness to the angular position source.update_center(center_x=r * np.cos(phi), center_y=r * np.sin(phi)) else: # Default "area" positioning architecture if test_area is not None: source.update_center(area=test_area) return all_sources[0] if len(all_sources) == 1 else all_sources
[docs] def draw_false_positive(self, number=1): """Draw given number of false positive systems. :param number: number of false positive requested. The default value is 1. :return: list of FalsePositive() instance (or single instance if number=1). """ false_positive_population = [] for _ in range(number): successful = False while not successful: # Step 1: Draw deflector deflector, z_max = self.draw_deflector() # Step 2: Calculate areas and sizing for positioning theta_e_infinity = deflector.theta_e_infinity(cosmo=self.cosmo) test_area = self._test_area_factor * area_theta_e_infinity( theta_e_infinity=theta_e_infinity ) # Step 3: Draw and position sources sources = self.draw_sources( z_max=z_max, test_area=test_area, theta_e=theta_e_infinity ) if sources is None: continue # Retry if sources are invalid # Step 4: Create false positive false_positive = FalsePositive( deflector_class=deflector, source_class=sources, cosmo=self.cosmo, include_deflector_light=self._include_central_galaxy_light, los_class=self.los_config.draw_los( source_redshift=z_max, deflector_redshift=deflector.redshift ), ) false_positive_population.append(false_positive) successful = True return ( false_positive_population[0] if number == 1 else false_positive_population )