• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aymgal / COOLEST / 19457839561

18 Nov 2025 07:28AM UTC coverage: 45.353% (-0.06%) from 45.411%
19457839561

Pull #75

github

web-flow
Merge 63bb40ada into 52f3a30ef
Pull Request #75: Adding multi-source plane lens functionality

0 of 6 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

1420 of 3131 relevant lines covered (45.35%)

0.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.07
/coolest/api/composable_models.py
1
__author__ = 'aymgal'
1✔
2

3
import os
1✔
4
import numpy as np
1✔
5
import math
1✔
6
import logging
1✔
7
from scipy import signal
1✔
8
import pandas as pd
1✔
9
from functools import partial
1✔
10

11
from coolest.api import util
1✔
12

13

14
# logging settings
15
logging.getLogger().setLevel(logging.WARNING)
1✔
16

17

18
class BaseComposableModel(object):
1✔
19
    """Given a COOLEST object, evaluates a selection of mass or light profiles.
20
    This class serves as parent for more specific classes and should not be 
21
    instantiated by the user.
22

23
    Parameters
24
    ----------
25
    model_type : str
26
        Either 'light_model' or 'mass_model'
27
    coolest_object : COOLEST
28
        COOLEST instance
29
    coolest_directory : str, optional
30
        Directory which contains the COOLEST template, by default None
31
    load_posterior_samples : bool, optional
32
        If True, and if the COOLEST metadata provides it, the constructor will
33
        attempt to load the chain file containing posterior samples, in addition
34
        to point estimates for each profile parameters. Default is False.
35
    entity_selection : list, optional
36
        List of indices of the lensing entities to consider; If None, 
37
        selects the first entity which has a model of type model_type, by default None
38
    profile_selection : list, optional
39
        List of either lists of indices, or 'all', for selecting which (mass or light) profile 
40
        of a given lensing entity to consider. If None, selects all the 
41
        profiles of within the corresponding entity, by default None
42

43
    Raises
44
    ------
45
    ValueError
46
        No valid entity found or no profiles found.
47
    """
48
        
49
    _chain_key = "chain_file_name"
1✔
50
    _supported_eval_modes = ('point', 'posterior')
1✔
51

52
    def __init__(self, model_type, 
1✔
53
                 coolest_object, coolest_directory=None, 
54
                 load_posterior_samples=False,
55
                 entity_selection=None, profile_selection=None):
56
        if entity_selection is None:
1✔
57
            # finds the first entity that has a 'model_type' profile
58
            entity_selection = None
×
59
            for i, entity in enumerate(coolest_object.lensing_entities):
×
60
                if model_type == 'light_model' \
×
61
                    and entity.type == 'galaxy' \
62
                    and len(entity.light_model) > 0:
63
                    entity_selection = [i]
×
64
                    break
×
65
                elif model_type == 'mass_model' \
×
66
                    and len(entity.mass_model) > 0:
67
                    entity_selection = [i]
×
68
                    break
×
69
            if entity_selection is None:
×
70
                raise ValueError("No lensing entity with light profiles have been found")
×
71
            else:
72
                logging.warning(f"Found valid profile for lensing entity (index {i}) for model type '{model_type}'")
×
73
        if profile_selection is None:
1✔
74
            profile_selection = 'all'
×
75
        entities = coolest_object.lensing_entities
1✔
76
        self.directory = coolest_directory
1✔
77
        self._posterior_bool, self._csv_path = False, None
1✔
78
        if load_posterior_samples:
1✔
79
            metadata = coolest_object.meta
×
80
            if self._chain_key not in metadata:
×
81
                logging.warning(f"Metadata key '{self._chain_key}' is missing "
×
82
                                f"from COOLEST template, hence no posterior samples "
83
                                f"will be loaded.")
84
            else:
85
                self._posterior_bool = True
×
86
                self._csv_path = os.path.join(self.directory, metadata[self._chain_key])
×
87
        self.setup_profiles_and_params(model_type, entities, 
1✔
88
                                        entity_selection, profile_selection)
89
        self.num_profiles = len(self.profile_list)
1✔
90
        if self.num_profiles == 0:
1✔
91
            raise ValueError("No profile has been selected!")
×
92

93
    def setup_profiles_and_params(self, model_type, entities, 
1✔
94
                                  entity_selection, profile_selection):
95
        profile_list = []
1✔
96
        param_list, post_param_list = [], []
1✔
97
        info_list = []
1✔
98
        for i, entity in enumerate(entities):
1✔
99
            if self._selected(i, entity_selection):
1✔
100
                if model_type == 'light_model' and entity.type == 'external_shear':
1✔
101
                    raise ValueError(f"External shear (entity index {i}) has no light model")
×
102
                for j, profile in enumerate(getattr(entity, model_type)):
1✔
103
                    if self._selected(j, profile_selection):
1✔
104
                        if 'Grid' in profile.type:
1✔
105
                            if self.directory is None:
×
106
                                raise ValueError("The directory in which the COOLEST file is located "
×
107
                                                 "must be provided for loading FITS files.")
108
                            params, fixed_params = self._get_grid_params(profile, self.directory)
×
109
                            profile_list.append(self._get_api_profile(model_type, profile, *fixed_params))
×
110
                            post_params = None  # TODO: support samples for grid parameters
×
111
                        else:
112
                            params, post_params = self._get_regular_params(
1✔
113
                                profile, samples_file_path=self._csv_path
114
                            )
115
                            profile_list.append(self._get_api_profile(model_type, profile))
1✔
116
                        param_list.append(params)
1✔
117
                        post_param_list.append(post_params)
1✔
118
                        info_list.append((entity.name, entity.redshift))
1✔
119
        self.profile_list = profile_list
1✔
120
        self.param_list = param_list
1✔
121
        self.info_list = info_list
1✔
122
        if self._posterior_bool is True:
1✔
123
            post_param_list, post_weights = self._finalize_post_samples(post_param_list, self._csv_path)
×
124
            self.post_param_list = post_param_list
×
125
            self.post_weights = np.array(post_weights)
×
126
        else:
127
            self.post_param_list = None
1✔
128
            self.post_weights = None
1✔
129

130
    def estimate_center(self):
1✔
131
        # TODO: improve this (for now simply considers the first profile that has a center)
132
        for profile, params in zip(self.profile_list, self.param_list):
1✔
133
            if 'center_x' in params:
1✔
134
                center_x = params['center_x']
1✔
135
                center_y = params['center_y']
1✔
136
                logging.info(f"Picked center from profile '{profile.type}'")
1✔
137
                return center_x, center_y
1✔
138
        raise ValueError("Could not estimate a center from the composed model")
×
139

140
    @staticmethod
1✔
141
    def _get_api_profile(model_type, profile_in, *extra_profile_args):
1✔
142
        """
143
        Takes as input a light profile from the template submodule
144
        and instantites the corresponding profile from the API submodule
145
        """
146
        if model_type == 'light_model':
1✔
147
            from coolest.api.profiles import light
1✔
148
            ProfileClass = getattr(light, profile_in.type)
1✔
149
        elif model_type == 'mass_model':
1✔
150
            from coolest.api.profiles import mass
1✔
151
            ProfileClass = getattr(mass, profile_in.type)
1✔
152
        return ProfileClass(*extra_profile_args)
1✔
153

154
    @staticmethod
1✔
155
    def _get_regular_params(profile_in, samples_file_path=None):
1✔
156
        parameters = {}  # best-fit values
1✔
157
        samples = {} if samples_file_path else None  # posterior samples
1✔
158
        for name, param in profile_in.parameters.items():
1✔
159
            parameters[name] = param.point_estimate.value
1✔
160
            if samples is not None:
1✔
161
                # read just the column corresponding to the parameter ID
162
                column = pd.read_csv(
×
163
                    samples_file_path, 
164
                    usecols=[param.id], 
165
                    delimiter=',',
166
                )
167
                # TODO: take into account probability weights from nested sampling runs!
168
                samples[name] = list(column[param.id])
×
169
        return parameters, samples
1✔
170

171
    @staticmethod
1✔
172
    def _get_grid_params(profile_in, fits_dir):
1✔
173
        param_in = profile_in.parameters['pixels']
×
174
        if profile_in.type == 'PixelatedRegularGrid':
×
175
            data = param_in.get_pixels(directory=fits_dir)
×
176
            parameters = {'pixels': data}
×
177
            fov_x = param_in.field_of_view_x
×
178
            fov_y = param_in.field_of_view_y
×
179
            npix_x = param_in.num_pix_x
×
180
            npix_y = param_in.num_pix_y
×
181
            fixed_parameters = (fov_x, fov_y, npix_x, npix_y)
×
182

183
        elif profile_in.type == 'IrregularGrid':
×
184
            x, y, z = param_in.get_xyz(directory=fits_dir)
×
185
            parameters = {'x': x, 'y': y, 'z': z}
×
186
            fov_x = param_in.field_of_view_x
×
187
            fov_y = param_in.field_of_view_y
×
188
            npix = param_in.num_pix
×
189
            fixed_parameters = (fov_x, fov_y, npix)
×
190
        return parameters, fixed_parameters
×
191
    
192
    @staticmethod
1✔
193
    def _finalize_post_samples(param_list_of_samples, samples_file_path):
1✔
194
        """
195
        Takes as input the samples grouped at the leaves of the nested container structure,
196
        and returns a list of items each organized as self.param_list
197
        """
198
        num_profiles = len(param_list_of_samples)
×
199
        profile_0 = param_list_of_samples[0]
×
200
        num_samples = len(profile_0[list(profile_0.keys())[0]])
×
201
        samples_of_param_list = [
×
202
            [{} for _ in range(num_profiles)] for _ in range(num_samples)
203
        ]
204
        for i in range(num_samples):
×
205
            for k in range(num_profiles):
×
206
                for key in param_list_of_samples[k].keys():
×
207
                    samples_of_param_list[i][k][key] = param_list_of_samples[k][key][i]
×
208
        # also load and return the probability weights
209
        # read just the column corresponding to the parameter ID
210
        weights_key = 'probability_weights'
×
211
        column = pd.read_csv(
×
212
            samples_file_path, 
213
            usecols=[weights_key], 
214
            delimiter=',',
215
        )
216
        weights_list = list(column[weights_key])
×
217
        return samples_of_param_list, weights_list
×
218

219
    @staticmethod
1✔
220
    def _selected(index, selection):
1✔
221
        if isinstance(selection, str) and selection.lower() == 'all':
1✔
222
            return True
1✔
223
        elif isinstance(selection, (list, tuple, np.ndarray)) and index in selection:
1✔
224
            return True
1✔
225
        elif isinstance(selection, (int, float)) and int(selection) == index:
1✔
226
            return True
×
227
        return False
1✔
228

229
    def _check_eval_mode(self, mode):
1✔
230
        if mode not in self._supported_eval_modes:
×
231
            raise NotImplementedError(
×
232
                f"Only evaluation modes "
233
                f"{self._supported_eval_modes} are supported "
234
                f"(received '{mode}')."
235
        )
236

237

238
class ComposableLightModel(BaseComposableModel):
1✔
239
    """Given a COOLEST object, evaluates a selection of entity and their light profiles.
240

241
    Parameters
242
    ----------
243
    coolest_object : COOLEST
244
        COOLEST instance
245
    coolest_directory : str, optional
246
        Directory which contains the COOLEST template, by default None
247
    entity_selection : list, optional
248
        List of indices of the lensing entities to consider; If None, 
249
        selects the first entity that has a light model, by default None
250
    profile_selection : list, optional
251
        List of either lists of indices, or 'all', for selecting which light profile 
252
        of a given lensing entity to consider. If None, selects all the 
253
        profiles of within the corresponding entity, by default None
254

255
    Raises
256
    ------
257
    ValueError
258
        No valid entity found or no profiles found.
259
    """
260

261
    def __init__(self, coolest_object, coolest_directory=None, **kwargs_selection):
1✔
262
        super().__init__('light_model', coolest_object, 
1✔
263
                         coolest_directory=coolest_directory,
264
                         **kwargs_selection)
265
        pixel_size = coolest_object.instrument.pixel_size
1✔
266
        if pixel_size is None:
1✔
267
            self.pixel_area = 1.
×
268
        else:
269
            self.pixel_area = pixel_size**2
1✔
270

271
    def surface_brightness(self, return_extra=False):
1✔
272
        """Returns the surface brightness as stored in the COOLEST file"""
273
        if self.num_profiles > 1:
×
274
            logging.warning("When more than a single light profile has been selected, "
×
275
                            "the method `surface_brightness()` only considers the first profile")
276
        profile = self.profile_list[0]
×
277
        values = profile.surface_brightness(**self.param_list[0])
×
278
        if return_extra:
×
279
            extent = profile.get_extent()
×
280
            coordinates = profile.get_coordinates()
×
281
            return values, extent, coordinates
×
282
        return values
×
283

284
    def evaluate_surface_brightness(self, x, y):
1✔
285
        """Evaluates the surface brightness at given coordinates"""
286
        image = np.zeros_like(x)
1✔
287
        for k, (profile, params) in enumerate(zip(self.profile_list, self.param_list)):
1✔
288
            flux_k = profile.evaluate_surface_brightness(x, y, **params)
1✔
289
            if profile.units == 'per_ang':
1✔
290
                flux_k *= self.pixel_area
1✔
291
            image += flux_k
1✔
292
        return image
1✔
293

294

295
class ComposableMassModel(BaseComposableModel):
1✔
296
    """Given a COOLEST object, evaluates a selection of entity and their mass profiles.
297

298
    Parameters
299
    ----------
300
    coolest_object : COOLEST
301
        COOLEST instance
302
    coolest_directory : str, optional
303
        Directory which contains the COOLEST template, by default None
304
    entity_selection : list, optional
305
        List of indices of the lensing entities to consider; If None, 
306
        selects the first entity that has a mass model, by default None
307
    profile_selection : list, optional
308
        List of either lists of indices, or 'all', for selecting which mass profile 
309
        of a given lensing entity to consider. If None, selects all the 
310
        profiles of within the corresponding entity, by default None
311

312
    Raises
313
    ------
314
    ValueError
315
        No valid entity found or no profiles found.
316
    """
317

318
    def __init__(self, coolest_object, coolest_directory=None, 
1✔
319
                 load_posterior_samples=False,
320
                 **kwargs_selection):
321
        super().__init__('mass_model', coolest_object, 
1✔
322
                         coolest_directory=coolest_directory,
323
                         load_posterior_samples=load_posterior_samples,
324
                         **kwargs_selection)
325

326
    def evaluate_potential(self, x, y, mode='point', last_n_samples=None):
1✔
327
        """Evaluates the lensing potential field at given coordinates"""
328
        self._check_eval_mode(mode)
×
329
        if mode == 'point' or self._posterior_bool is False:
×
330
            return self._eval_pot_point(x, y, self.param_list)
×
331
        elif mode == 'posterior':
×
332
            return self._eval_pot_posterior(x, y, self.post_param_list, last_n_samples)
×
333

334
    def _eval_pot_point(self, x, y, param_list):
1✔
335
        psi = np.zeros_like(x)
×
336
        for k, profile in enumerate(self.profile_list):
×
337
            psi += profile.potential(x, y, **param_list[k])
×
338
        return psi
×
339
    
340
    def _eval_pot_posterior(self, x, y, param_list, last_n_samples):
1✔
341
        # map the point function at each sample
342
        use_all_samples = last_n_samples is None or last_n_samples <= 0
×
343
        val_list = param_list if use_all_samples else param_list[-last_n_samples:]
×
344
        mapped = map(partial(self._eval_pot_point, x, y), val_list)
×
345
        return np.array(list(mapped))
×
346
    
347
    def fermat_potential(self, x, y, x_src, y_src, mode='point', last_n_samples=None):
1✔
348
        """Computes the Fermat potential for image (x, y) and source position (x_src, y_src)
349
        """
350
        # gravitational term
351
        psi = self.evaluate_potential(x, y, mode=mode, last_n_samples=last_n_samples)
×
352
        # geometric term
353
        geo = ((x - x_src)**2 + (y - y_src)**2) / 2.
×
354
        geo = np.broadcast_to(geo, psi.shape)  # makes sure geo has same shape as psi
×
355
        return geo - psi
×
356
    
357
    def evaluate_deflection(self, x, y):
1✔
358
        """Evaluates the lensing deflection field at given coordinates"""
359
        alpha_x, alpha_y = np.zeros_like(x), np.zeros_like(x)
×
360
        for k, (profile, params) in enumerate(zip(self.profile_list, self.param_list)):
×
361
            a_x, a_y = profile.deflection(x, y, **params)
×
362
            alpha_x += a_x
×
363
            alpha_y += a_y
×
364
        return alpha_x, alpha_y
×
365

366
    def evaluate_convergence(self, x, y):
1✔
367
        """Evaluates the lensing convergence (i.e., 2D mass density) at given coordinates"""
368
        kappa = np.zeros_like(x)
1✔
369
        for k, (profile, params) in enumerate(zip(self.profile_list, self.param_list)):
1✔
370
            kappa += profile.convergence(x, y, **params)
1✔
371
        return kappa
1✔
372

373
    def evaluate_hessian(self, x, y):
1✔
374
        """Evaluates the lensing Hessian components at given coordinates"""
375
        H_xx_sum = np.zeros_like(x)
×
376
        H_xy_sum = np.zeros_like(x)
×
377
        H_yx_sum = np.zeros_like(x)
×
378
        H_yy_sum = np.zeros_like(x)
×
379
        for k, (profile, params) in enumerate(zip(self.profile_list, self.param_list)):
×
380
            H_xx, H_xy, H_yx, H_yy = profile.hessian(x, y, **params)
×
381
            H_xx_sum += H_xx
×
382
            H_xy_sum += H_xy
×
383
            H_yx_sum += H_yx
×
384
            H_yy_sum += H_yy
×
385
        return H_xx_sum, H_xy_sum, H_yx_sum, H_yy_sum
×
386
    
387
    def evaluate_jacobian(self, x, y):
1✔
388
        """Evaluates the lensing Jacobian (d beta / d theta)  at given coordinates"""
389
        H_xx, H_xy, H_yx, H_yy = self.evaluate_hessian(x, y)
×
390
        A = np.array([[1 - H_xx, -H_xy], [-H_yx, 1 - H_yy]])
×
391
        return A
×
392
    
393
    def evaluate_magnification(self, x, y):
1✔
394
        """Evaluates the lensing magnification at given coordinates"""
395
        H_xx, H_xy, H_yx, H_yy = self.evaluate_hessian(x, y)
×
396
        det_A = (1 - H_xx) * (1 - H_yy) - H_xy*H_yx
×
397
        mu = 1. / det_A
×
398
        return mu
×
399

400
    def ray_shooting(self, x, y):
1✔
401
        """evaluates the lens equation beta = theta - alpha(theta)"""
402
        alpha_x, alpha_y = self.evaluate_deflection(x, y)
×
403
        x_rs, y_rs = x - alpha_x, y - alpha_y
×
404
        return x_rs, y_rs
×
405

406

407
class ComposableLensModel(object):
1✔
408
    """Given a COOLEST object, evaluates a selection of entity and 
409
    their mass and light profiles, typically to construct an image of the lens.
410

411
    Parameters
412
    ----------
413
    coolest_object : COOLEST
414
        COOLEST instance
415
    coolest_directory : str, optional
416
        Directory which contains the COOLEST template, by default None
417
    entity_selection : list, optional
418
        List of indices of the lensing entities to consider; If None, 
419
        selects the first entity that has a light/mass model, by default None
420
    profile_selection : list, optional
421
        List of either lists of indices, or 'all', for selecting which light/mass profile 
422
        of a given lensing entity to consider. If None, selects all the 
423
        profiles of within the corresponding entity, by default None
424

425
    Raises
426
    ------
427
    ValueError
428
        No valid entity found or no profiles found.
429
    """
430

431
    def __init__(self, coolest_object, coolest_directory=None, 
1✔
432
                 kwargs_selection_source=None, kwargs_selection_lens_mass=None):
433
        self.coolest = coolest_object
×
434
        self.coord_obs = util.get_coordinates(self.coolest)
×
435
        self.directory = coolest_directory
×
436
        if kwargs_selection_source is None:
×
437
            kwargs_selection_source = {}
×
NEW
438
            self.lens_mass = [ComposableMassModel(coolest_object, 
×
439
                                             coolest_directory,
440
                                             **kwargs_selection_source)]
441
        else:
NEW
442
            self.lens_mass = [ComposableMassModel(coolest_object, 
×
443
                                             coolest_directory,
444
                                             dict(entity_selection = km)) for km in kwargs_selection_lens_mass['entity_selection']]
445
        if kwargs_selection_lens_mass is None:
×
446
            kwargs_selection_lens_mass = {}
×
NEW
447
            self.lens_mass = [ComposableMassModel(coolest_object, 
×
448
                                             coolest_directory,
449
                                             **kwargs_selection_source)]
450
        else:
NEW
451
            self.source = [ComposableLightModel(coolest_object, 
×
452
                                          coolest_directory,
453
                                          dict(entity_selection = ks)) for ks in kwargs_selection_source['entity_selection']]
454

455
        # TO DO----------------------------------------------------------------
456
        # Convert self.lens_mass and self.source into lists of ComposableModels
457
        # User will plot much like multiplotter: kwargs_lens_mass = dict(entity_selection = [[0,1], [3,4], [6]])
458
        # Where [0,1], [3,4], [6] are entity indexes for lens mass(/optional shear)
459
        # Will need to evaluate deflection for each source using all mass models at redshift < source
460
        # Maybe sort the mass models by redshift, make list of redshifts, and evaluate combined deflection for any mass
461
        # model at reshift lower than source.
462
        # ------------------------------------------------------------------------
463
        
464
        
NEW
465
        self.mass_redshifts = np.array([lm.info_list[0][1] for lm in self.lens_mass])
×
NEW
466
        self.source_redshifts = np.array([lm.info_list[0][1] for lm in self.source])
×
467

468
    def model_image(self, supersampling=5, convolved=True, super_convolution=True):
1✔
469
        """generates an image of the lens based on the selected model components"""
470
        obs = self.coolest.observation
×
471
        psf = self.coolest.instrument.psf
×
472
        if convolved is True and psf.type == 'PixelatedPSF':
×
473
            scale_factor = obs.pixels.pixel_size / psf.pixels.pixel_size
×
474
            supersampling_conv = int(round(scale_factor))
×
475
            if not math.isclose(scale_factor, supersampling_conv):
×
476
                raise ValueError(f"PSF supersampling ({scale_factor}) not close to an integer?")
×
477
            if supersampling_conv < 1:
×
478
                raise ValueError("PSF pixel size smaller than data pixel size")
×
479
        if supersampling < 1:
×
480
            raise ValueError("Supersampling must be >= 1")
×
481
        if convolved is True and supersampling_conv > supersampling:
×
482
            supersampling = supersampling_conv
×
483
            logging.warning(f"Supersampling adapted to the PSF pixel size ({supersampling})")
×
484
        coord_eval = self.coord_obs.create_new_coordinates(pixel_scale_factor=1./supersampling)
×
485
        x, y = coord_eval.pixel_coordinates
×
486
        image = self.evaluate_lensed_surface_brightness(x, y)
×
487
        if convolved is True:
×
488
            if psf.type != 'PixelatedPSF':
×
489
                raise NotImplementedError
×
490
            kernel = psf.pixels.get_pixels(directory=self.directory)
×
491
            kernel_sum = kernel.sum()
×
492
            if not math.isclose(kernel_sum, 1., abs_tol=1e-3):
×
493
                kernel /= kernel_sum
×
494
                logging.warning(f"PSF kernel is not normalized (sum={kernel_sum}), "
×
495
                                f"so it has been normalized before convolution")
496
            if np.isnan(image).any():
×
497
                np.nan_to_num(image, copy=False, nan=0., posinf=None, neginf=None)
×
498
                logging.warning("Found NaN values in image prior to convolution; "
×
499
                                "they have been replaced by zeros.")
500
            if super_convolution and supersampling_conv == supersampling:
×
501
                # first convolve then downscale
502
                image = signal.fftconvolve(image, kernel, mode='same')
×
503
                image = util.downsampling(image, factor=supersampling)
×
504
            else:
505
                # first downscale then convolve
506
                image = util.downsampling(image, factor=supersampling)
×
507
                image = signal.fftconvolve(image, kernel, mode='same')
×
508
        elif supersampling > 1:
×
509
            image = util.downsampling(image, factor=supersampling)
×
510
        return image, self.coord_obs
×
511

512
    def model_residuals(self, mask=None, **model_image_kwargs):
1✔
513
        """computes the normalized residuals map as (data - model) / sigma"""
514
        model, _ = self.model_image(**model_image_kwargs)
×
515
        data = self.coolest.observation.pixels.get_pixels(directory=self.directory)
×
516
        noise = self.coolest.observation.noise
×
517
        if noise.type != 'NoiseMap':
×
518
            raise NotImplementedError
×
519
        sigma = noise.noise_map.get_pixels(directory=self.directory)
×
520
        if mask is None:
×
521
            mask = np.ones_like(model)
×
522
        return ((data - model) / sigma) * mask, self.coord_obs
×
523

524
    def evaluate_lensed_surface_brightness(self, x, y):
1✔
525
        """Evaluates the surface brightness of all the lensed sources at given coordinates"""
526
        # Recursively apply ray shooting to each source and then find lensed image for each source.
527
        # For each source, finding total lensing deflection
UNCOV
528
        x_rs, y_rs = self.ray_shooting(x, y)
×
529
        # Evaluate source given individual total deflection
530
        lensed_image = self.source.evaluate_surface_brightness(x_rs, y_rs)
×
531
        # Returned sum sources
UNCOV
532
        return lensed_image
×
533

534
    def ray_shooting(self, x, y):
1✔
535
        """evaluates the lens equation beta = theta - alpha(theta)"""
536
        return self.lens_mass.ray_shooting(x, y)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc