Skip to content

Commit bae17ff

Browse files
committed
Add utility to compute the from Yi Tan et al. 2023
1 parent e39729b commit bae17ff

File tree

2 files changed

+95
-14
lines changed

2 files changed

+95
-14
lines changed

coolest/api/analysis.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def effective_einstein_radius(self, center=None, initial_guess=1, initial_delta_
8787
else:
8888
center_x, center_y = center
8989

90-
def loop_until_overshoot(r_Ein, delta, direction, runningtotal, total_pix):
90+
def _loop_until_overshoot(r_Ein, delta, direction, runningtotal, total_pix):
9191
"""
9292
this subfunction iteratively adjusts the mask radius by delta either inward or outward until the sign flips on mean_kappa-area
9393
"""
@@ -145,7 +145,7 @@ def only_between_r1r2(r1, r2, r_grid):
145145

146146
for n in range(n_iter):
147147
#overshoots, turn around and backtrack at higher precision
148-
r_Ein, delta, direction, runningtotal, total_pix = loop_until_overshoot(r_Ein, delta, direction, runningtotal, total_pix)
148+
r_Ein, delta, direction, runningtotal, total_pix = _loop_until_overshoot(r_Ein, delta, direction, runningtotal, total_pix)
149149
direction=direction*-1
150150
delta=delta/2
151151
accuracy=grid_res/2 #after testing, accuracy is about grid_res/2
@@ -216,10 +216,9 @@ def effective_radial_slope(self, r_eval=None, center=None, r_vec=np.linspace(0,
216216
if r_eval==None:
217217
return slope
218218
else:
219-
closest_r = self.find_nearest(r_vec,r_eval) #just takes closest r. Could rebuild it to interpolate.
219+
closest_r = self._find_nearest(r_vec,r_eval) #just takes closest r. Could rebuild it to interpolate.
220220
return slope[r_vec==closest_r]
221221

222-
223222
def effective_radius_light(self, outer_radius=10, center=None, coordinates=None,
224223
initial_guess=1, initial_delta_pix=10,
225224
n_iter=10, return_model=False, return_accuracy=False,
@@ -307,14 +306,6 @@ def effective_radius_light(self, outer_radius=10, center=None, coordinates=None,
307306
return r_eff, accuracy
308307
return r_eff
309308

310-
311-
def find_nearest(self, array, value):
312-
"""subfunction to find nearest closest element in array to value"""
313-
array = np.asarray(array)
314-
idx = (np.abs(array - value)).argmin()
315-
return array[idx]
316-
317-
318309
def two_point_correlation(self, Nbins=100, rmax=None, normalize=False,
319310
use_profile_coordinates=True, coordinates=None,
320311
min_flux=None, min_flux_frac=None,
@@ -508,7 +499,6 @@ def total_magnitude(self, outer_radius=10, center=None, coordinates=None,
508499
mag_tot = -2.5*np.log10(flux_tot) + mag_zero_point
509500

510501
return mag_tot
511-
512502

513503
def ellipticity_from_moments(self, center=None, coordinates=None, **kwargs_selection):
514504
"""Estimates the axis ratio and position angle of the model map
@@ -571,6 +561,27 @@ def ellipticity_from_moments(self, center=None, coordinates=None, **kwargs_selec
571561

572562
return q, phi
573563

564+
def lensing_information(self, a=16, b=0,
565+
noise_map=None, arc_mask=None, theta_E=None,
566+
entity_idx_theta_E=0, profile_idx_theta_E=0):
567+
"""
568+
Computes the 'lensing information' defined in Yi Tan et al. 2023, Equations (8) and (9).
569+
https://ui.adsabs.harvard.edu/abs/2023arXiv231109307T/abstract
570+
"""
571+
data = self.coolest.observation.pixels.get_pixels()
572+
# TODO: subtract the lens light
573+
print("WARNING: no lens light subtracted; assuming the data contains only the arcs.")
574+
if noise_map is None:
575+
raise NotImplementedError("Getting the noise map from the COOLEST instance is yet implemented.")
576+
if theta_E is None:
577+
mass_profile = self.coolest.lensing_entities[entity_idx_theta_E].mass_model[profile_idx_theta_E]
578+
theta_E = mass_profile.parameters['theta_E'].point_estimate.value
579+
x, y = self.coordinates.pixel_coordinates
580+
I, theta_E, phi_ref, mask = util.lensing_information(
581+
data, x, y, theta_E, noise_map, a=a, b=b, arc_mask=arc_mask
582+
)
583+
return I, theta_E, phi_ref, mask
584+
574585

575586
@staticmethod
576587
def effective_radius(light_map, x, y, outer_radius=10, initial_guess=1, initial_delta_pix=10, n_iter=10):
@@ -633,3 +644,10 @@ def effective_radius(light_map, x, y, outer_radius=10, initial_guess=1, initial_
633644

634645
return r_eff, grid_res
635646

647+
@staticmethod
648+
def _find_nearest(array, value):
649+
"""subfunction to find nearest closest element in array to value"""
650+
array = np.asarray(array)
651+
idx = (np.abs(array - value)).argmin()
652+
return array[idx]
653+

coolest/api/util.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,66 @@ def downsampling(image, factor=1):
135135
raise ValueError(f"Downscaling factor {factor} is not possible with shape ({nx}, {ny})")
136136

137137

138+
def lensing_information(data_lens_sub, x, y, theta_E, noise_map, center_x_lens=0, center_y_lens=0,
139+
a=16, b=0, arc_mask=None):
140+
"""
141+
Computes the 'lensing information' defined in Yi Tan et al. 2023, Equations (8) and (9).
142+
https://ui.adsabs.harvard.edu/abs/2023arXiv231109307T/abstract
143+
144+
Parameters
145+
----------
146+
data_lens_sub : np.ndarray
147+
Imaging data as a 2D array. It is assumed to contain no lens light.
148+
x : np.ndarray
149+
2D array of x coordinates, in arcsec.
150+
y : np.ndarray
151+
2D array of y coordinates, in arcsec.
152+
theta_E : float
153+
Einstein radius in arcsec, by default None
154+
noise_map : np.ndarray
155+
2D array with 1-sigma noise level per pixel (same units as `data_lens_sub`), by default None
156+
center_x_lens : int, optional
157+
x coordinates of the center of the lens, by default 0
158+
center_y_lens : int, optional
159+
y coordinates of the center of the lens, by default 0
160+
a : int, optional
161+
Exponent in Eq. (9) from Yi Tan et al. 2023, by default 16
162+
b : int, optional
163+
Exponent in Eq. (9) from Yi Tan et al. 2023, by default 0
164+
arc_mask : np.ndarray, optional
165+
Binary 2D array with 1s where there is are lensed arcs, by default None
166+
167+
Returns
168+
-------
169+
4-tuple
170+
Lensing information I, Einstein radius, reference azimuthal angle, total mask used for computing I
171+
"""
172+
if arc_mask is None:
173+
arc_mask = np.ones_like(data_lens_sub)
174+
# estimate background noise from one corner of the noise map
175+
sigma_bkg = np.mean(noise_map[:10, :10])
176+
# build a mask to only consider pixels at least 3 times the background noise level
177+
snr_mask = np.where(data_lens_sub > 3.*sigma_bkg, 1., 0.)
178+
# combine user mask and SNR mask
179+
arc_mask_tot = snr_mask * arc_mask
180+
# shift coordinates so that lens is at (0, 0)
181+
theta_x, theta_y = x - center_x_lens, y - center_y_lens
182+
# compute polar coordinates centered on the lens
183+
theta_r = np.hypot(theta_x, theta_y)
184+
phi = np.arctan2(theta_y, theta_x)
185+
# find index of the brightest pixel (within the arc mask)
186+
max_idx = np.where(data_lens_sub == (data_lens_sub*arc_mask_tot).max())
187+
# get azimuthal angle corresponding to the brightest pixel
188+
phi_ref = float(np.arctan2(theta_y[max_idx], theta_x[max_idx]))
189+
# compute the weights following Eq. (9) from Yi Tan et al. 2023
190+
weights = ( 1. + np.abs(theta_r - theta_E) / theta_E * (1 + np.abs(phi - phi_ref) / phi_ref)**b )**a
191+
# compute the weighted sum
192+
numerator = np.sum(arc_mask_tot*weights*data_lens_sub)
193+
denominator = np.sqrt(np.sum(arc_mask_tot*noise_map**2))
194+
lens_I = numerator / denominator
195+
return lens_I, theta_E, phi_ref, arc_mask_tot
196+
197+
138198
def split_lens_source_params(coolest_list, name_list, lens_light=False):
139199
"""
140200
Read several json files already containing a model with the results of this model fitting
@@ -147,7 +207,7 @@ def split_lens_source_params(coolest_list, name_list, lens_light=False):
147207
148208
OUTPUT
149209
------
150-
param_all_lens, param_all_source: organized dictionnaries readable by plotting function
210+
param_all_lens, param_all_source: organized dictionaries readable by plotting function
151211
"""
152212

153213
param_all_lens = {}
@@ -417,6 +477,7 @@ def read_sersic(light, param={}, prefix='Sersic_0_'):
417477

418478
return param
419479

480+
420481
def find_critical_lines(coordinates, mag_map):
421482
# invert and find contours corresponding to infinite magnification (i.e., changing sign)
422483
inv_mag = 1. / np.array(mag_map)
@@ -428,6 +489,7 @@ def find_critical_lines(coordinates, mag_map):
428489
lines.append((np.array(curve_x), np.array(curve_y)))
429490
return lines
430491

492+
431493
def find_caustics(crit_lines, composable_lens):
432494
"""`composable_lens` can be an instance of `ComposableLens` or `ComposableMass`"""
433495
lines = []
@@ -436,6 +498,7 @@ def find_caustics(crit_lines, composable_lens):
436498
lines.append((np.array(cl_src_x), np.array(cl_src_y)))
437499
return lines
438500

501+
439502
def find_all_lens_lines(coordinates, composable_lens):
440503
"""`composable_lens` can be an instance of `ComposableLens` or `ComposableMass`"""
441504
from coolest.api.composable_models import ComposableLensModel, ComposableMassModel # avoiding circular imports

0 commit comments

Comments
 (0)