Skip to content

Commit

Permalink
Merge branch 'main' into PS_mod
Browse files Browse the repository at this point in the history
  • Loading branch information
aymgal committed Nov 16, 2023
2 parents 6301197 + 29aa4ec commit b2ba2dc
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 108 deletions.
298 changes: 243 additions & 55 deletions coolest/api/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import numpy as np
from astropy.coordinates import SkyCoord
import warnings
import logging
from skimage import measure

from coolest.api.composable_models import *
from coolest.api import util

# logging settings
logging.getLogger().setLevel(logging.INFO)

class Analysis(object):
"""Handles computation of model-independent quantities
Expand Down Expand Up @@ -63,7 +66,7 @@ def effective_einstein_radius(self, center=None, initial_guess=1, initial_delta_
Raises
------
Warning
RuntimeError
If the algorithm is running for more than 100 loops.
"""
if kwargs_selection is None:
Expand Down Expand Up @@ -93,8 +96,7 @@ def loop_until_overshoot(r_Ein, delta, direction, runningtotal, total_pix):
return np.nan, np.nan, 0, runningtotal, total_pix
while (runningtotal-total_pix)*direction>0:
if loopcount > max_loopcount:
raise Warning('Stuck in very long (possibly infinite) loop')
break
raise RuntimeError('Stuck in very long (possibly infinite) loop')
if direction > 0:
mask=only_between_r1r2(r_Ein, r_Ein+delta,r_grid)
else:
Expand All @@ -112,7 +114,7 @@ def loop_until_overshoot(r_Ein, delta, direction, runningtotal, total_pix):
runningtotal=np.sum(kappa_image[mask])
total_pix=np.sum(mask)
if runningtotal-total_pix < 0:
print('WARNING: kappa is sub-critical, Einstein radius undefined.')
logging.warning('kappa is sub-critical, Einstein radius undefined.')
return np.nan, np.nan, 0, runningtotal, total_pix
loopcount+=1
return r_Ein, delta, direction, runningtotal, total_pix
Expand Down Expand Up @@ -220,7 +222,8 @@ def effective_radial_slope(self, r_eval=None, center=None, r_vec=np.linspace(0,

def effective_radius_light(self, outer_radius=10, center=None, coordinates=None,
initial_guess=1, initial_delta_pix=10,
n_iter=10, return_model=False, **kwargs_selection):
n_iter=10, return_model=False, return_accuracy=False,
**kwargs_selection):
"""Computes the effective radius of the 2D surface brightness profile,
based on a definition similar to the half-light radius.
Expand All @@ -234,13 +237,15 @@ def effective_radius_light(self, outer_radius=10, center=None, coordinates=None,
Instance of a Coordinates object to be used for the computation.
If None, will use an instance based on the Instrument, by default None
initial_guess : int, optional
Initial guess for effective radius, by default 1
Initial guess for effective radius in arcsecond, by default 1
initial_delta_pix : int, optional
Initial step size before shrinking in future iterations, by default 10
Initial step size in pixels before shrinking in future iterations, by default 10
n_iter : int, optional
Number of iterations, by default 5
return_model : bool, optional
If True, also returns the surface brightness map used to comouted the radius. By default False.
return_accuracy : bool, optional
if True, return a rough estimate of accuracy as well, by default False
Returns
-------
Expand All @@ -254,64 +259,53 @@ def effective_radius_light(self, outer_radius=10, center=None, coordinates=None,
"""
if kwargs_selection is None:
kwargs_selection = {}
light_model = ComposableLightModel(self.coolest, self.coolest_dir, **kwargs_selection)
# get an image of the convergence
if coordinates is None:
x, y = self.coordinates.pixel_coordinates
else:
x, y = coordinates.pixel_coordinates
light_image = light_model.evaluate_surface_brightness(x, y)
light_image[np.isnan(light_image)] = 0.

# import matplotlib.pyplot as plt
# plt.imshow(np.log10(light_image))
# plt.show()
light_model = ComposableLightModel(self.coolest, self.coolest_dir, **kwargs_selection)

# select a center
if center is None:
center_x, center_y = light_model.estimate_center()
else:
center_x, center_y = center

# get an image of the convergence
if coordinates is None:
x, y = self.coordinates.pixel_coordinates
else:
x, y = coordinates.pixel_coordinates
# make sure to evaluate the profile such that it is centered on the image
x_ = x + center_x
y_ = y + center_y
light_image = light_model.evaluate_surface_brightness(x_, y_)
light_image[np.isnan(light_image)] = 0.

#if limit of integration exceeds FoV, raise warning
x_FoV=self.coolest.observation.pixels.field_of_view_x
y_FoV=self.coolest.observation.pixels.field_of_view_y
if coordinates is None:
x_FoV = self.coolest.observation.pixels.field_of_view_x
y_FoV = self.coolest.observation.pixels.field_of_view_y
else:
x_FoV = (coordinates.extent[0], coordinates.extent[1])
y_FoV = (coordinates.extent[2], coordinates.extent[3])
out_of_FoV=False
if center_x - outer_radius < x_FoV[0] or center_x + outer_radius > x_FoV[1]:
if outer_radius - center_x < x_FoV[0] or outer_radius + center_x > x_FoV[1]:
out_of_FoV=True
if center_y - outer_radius < y_FoV[0] or center_y + outer_radius > y_FoV[1]:
if outer_radius - center_y < y_FoV[0] or outer_radius + center_y > y_FoV[1]:
out_of_FoV=True
if out_of_FoV is True:
warnings.warn("Warning: Outer limit of integration exceeds FoV; effective radius may not be accurate.")

#initialize
grid_res=np.abs(x[0,0]-x[0,1])
initial_delta=grid_res*initial_delta_pix #default inital step size is 10 pixels
r_grid=np.sqrt((x-center_x)**2+(y-center_y)**2)
total_light=np.sum(light_image[r_grid<outer_radius])
cumulative_light=np.sum(light_image[r_grid<initial_guess])
if cumulative_light < total_light/2: #move outward
direction=1
elif cumulative_light > total_light/2: #move inward
direction=-1
else:
return initial_guess
r_eff=initial_guess
delta=initial_delta
loopcount=0

for n in range(n_iter): #overshoots, turn around and backtrack at higher precision
while (total_light/2-cumulative_light)*direction>0:
if loopcount > 100:
raise Warning('Stuck in very long (possibly infinite) loop')
break
r_eff=r_eff+delta*direction
cumulative_light=np.sum(light_image[r_grid<r_eff])
loopcount+=1
direction=direction*-1
delta=delta/2

return r_eff if not return_model else r_eff, light_image
logging.warning("Outer limit of integration exceeds FoV; effective radius may not be accurate.")

r_eff, accuracy = self.effective_radius(
light_image, x, y, outer_radius=outer_radius, initial_guess=initial_guess,
initial_delta_pix=initial_delta_pix, n_iter=n_iter
)

if return_model and return_accuracy:
return r_eff, accuracy, light_image
elif return_model:
return r_eff, light_image
elif return_accuracy:
return r_eff, accuracy
return r_eff


def find_nearest(self, array, value):
Expand Down Expand Up @@ -386,12 +380,12 @@ def two_point_correlation(self, Nbins=100, rmax=None, normalize=False,
light_image = np.nan_to_num(light_image, nan=0.)
cov_mask = np.ones_like(light_image)
if min_flux is not None:
print(f"Setting to zero any flux below {min_flux}.")
logging.info(f"Setting to zero any flux below {min_flux}.")
light_image[light_image < min_flux] = 0.
cov_mask[light_image < min_flux] = 0.
elif min_flux_frac is not None:
min_flux = min_flux_frac*light_image.max()
print(f"Setting to zero any flux below {min_flux}.")
logging.info(f"Setting to zero any flux below {min_flux}.")
light_image[light_image < min_flux] = 0.
cov_mask[light_image < min_flux] = 0.
cov_mask = cov_mask.astype(bool)
Expand Down Expand Up @@ -443,5 +437,199 @@ def two_point_correlation(self, Nbins=100, rmax=None, normalize=False,
elif return_map:
return bins, means, sdevs, light_image
return bins, means, sdevs


def total_magnitude(self, outer_radius=10, center=None, coordinates=None,
no_re_eval=False, flux_factor=None, mag_zero_point=None, **kwargs_selection):
"""Computes the effective radius of the 2D surface brightness profile,
based on a definition similar to the half-light radius.
Parameters
----------
outer_radius : int, optional
outer limit of integration within which half the light is calculated to estimate the effective radius, by default 10
no_re_eval : bool, option
If True, do re-evaluate the light profile (only relevant for pixelated profiles). Default is False.
center : (float, float), optional
(x, y)-coordinates of the center from which to calculate Einstein radius; if None, use the value from create_kappa_image, by default None
coordinates : Coordinates, optional
Instance of a Coordinates object to be used for the computation.
If None, will use an instance based on the Instrument, by default None
mag_zero_point : float, optional
Magnitude zero-point corresponding to 1 electron per second.
Must be given when no mag_zero_point has been found in the self.coolest object.
TODO: flux_factor is temporary, this will be removed in the future.
Returns
-------
float
Total magnitude from the flux integrated over the field of view.
"""
if kwargs_selection is None:
kwargs_selection = {}

light_model = ComposableLightModel(self.coolest, self.coolest_dir, **kwargs_selection)

# get an image of the convergence
if no_re_eval:
light_image = light_model.surface_brightness()
light_image[np.isnan(light_image)] = 0.
else:
# select a center
if center is None:
center_x, center_y = light_model.estimate_center()
else:
center_x, center_y = center
if coordinates is None:
x, y = self.coordinates.pixel_coordinates
else:
x, y = coordinates.pixel_coordinates
# make sure to evaluate the profile such that it is centered on the image
x_ = x + center_x
y_ = y + center_y
light_image = light_model.evaluate_surface_brightness(x_, y_)
light_image[np.isnan(light_image)] = 0.

# retrieve the zero-point from the
if self.coolest.observation.mag_zero_point is not None:
logging.info(f"Using magnitude zero-point from self.coolest object ({mag_zero_point}).")
mag_zero_point = self.coolest.observation.mag_zero_point
elif mag_zero_point is None:
raise ValueError("No `mag_zero_point` has been found in the COOLEST object, "
"hence `mag_zero_point` must be provided.")
else:
logging.info(f"Using the magnitude zero-point ({mag_zero_point}.)")

# compute the magnitude
flux_tot = light_image.sum()
if flux_factor is not None:
flux_tot *= flux_factor # temporary feature
mag_tot = -2.5*np.log10(flux_tot) + mag_zero_point

return mag_tot


def ellipticity_from_moments(self, center=None, coordinates=None, **kwargs_selection):
"""Estimates the axis ratio and position angle of the model map
based on central moments of the image.
Parameters
----------
center : (float, float), optional
(x, y)-coordinates of the center from which to calculate Einstein radius; if None, use the value from create_kappa_image, by default None
coordinates : Coordinates, optional
Instance of a Coordinates object to be used for the computation.
If None, will use an instance based on the Instrument, by default None
Returns
-------
float
Ellipticity measurement (axis)
Raises
------
Warning
If integration loop exceeds outer bound before convergence.
"""
if kwargs_selection is None:
kwargs_selection = {}

light_model = ComposableLightModel(self.coolest, self.coolest_dir, **kwargs_selection)

# select a center
if center is None:
center_x, center_y = light_model.estimate_center()
else:
center_x, center_y = center

# get an image of the convergence
if coordinates is None:
x, y = self.coordinates.pixel_coordinates
spacing = self.coordinates.pixel_size
else:
x, y = coordinates.pixel_coordinates
spacing = coordinates.pixel_size
# make sure to evaluate the profile such that it is centered on the image
x_ = x + center_x
y_ = y + center_y
light_image = light_model.evaluate_surface_brightness(x_, y_)
light_image[np.isnan(light_image)] = 0.

# compute central momoments
mu = measure.moments_central(light_image, order=2, spacing=spacing)

# use the moments to estimate orientation and ellipticity (https://en.wikipedia.org/wiki/Image_moment)
mu_20_ = mu[2, 0] / mu[0, 0]
mu_02_ = mu[0, 2] / mu[0, 0]
mu_11_ = mu[1, 1] / mu[0, 0]
lambda_1 = (mu_20_ + mu_02_) / 2. + np.sqrt(4*mu_11_**2 + (mu_20_ - mu_02_)**2) / 2.
lambda_2 = (mu_20_ + mu_02_) / 2. - np.sqrt(4*mu_11_**2 + (mu_20_ - mu_02_)**2) / 2.
q = np.sqrt(lambda_2 / lambda_1) # b/a, axis ratio
phi_rad = np.arctan(2. * mu_11_ / (mu_20_ - mu_02_)) / 2. # position angle
phi = phi_rad * 180./np.pi + 90. # conversion to COOLEST conventions

return q, phi


@staticmethod
def effective_radius(light_map, x, y, outer_radius=10, initial_guess=1, initial_delta_pix=10, n_iter=10):
"""Computes the effective radius of the 2D surface brightness profile,
based on a definition similar to the half-light radius.
NOTE: This functions assumes that the profile is centered on the grid.
Parameters
----------
light_map : ndarray
2D array of the light model
x : ndarray
x-coordinates associated to the light model
y : ndarray
y-coordinates associated to the light model
outer_radius : int, optional
outer limit of integration within which half the light is calculated to estimate the effective radius, by default 10
initial_guess : int, optional
Initial guess for effective radius in arcsecond, by default 1
initial_delta_pix : int, optional
Initial step size in pixels before shrinking in future iterations, by default 10
n_iter : int, optional
Number of iterations, by default 5
Returns
-------
(float, float)
Effective radius and spacing of the coordinates grid (approximate accuracy)
Raises
------
RuntimeError
If integration loop exceeds outer bound before convergence.
"""
#initialize
grid_res=np.abs(x[0,0]-x[0,1])
initial_delta=grid_res*initial_delta_pix #default inital step size is 10 pixels
r_grid=np.hypot(x, y)
total_light=np.sum(light_map[r_grid<outer_radius])
cumulative_light=np.sum(light_map[r_grid<initial_guess])
if cumulative_light < total_light/2: #move outward
direction=1
elif cumulative_light > total_light/2: #move inward
direction=-1
else:
return initial_guess
r_eff=initial_guess
delta=initial_delta
loopcount=0

for n in range(n_iter): #overshoots, turn around and backtrack at higher precision
while (total_light/2.-cumulative_light)*direction>0:
if loopcount > 100:
raise RuntimeError('Stuck in very long (possibly infinite) loop')
r_eff=r_eff+delta*direction
cumulative_light=np.sum(light_map[r_grid<r_eff])
loopcount+=1
direction=direction*-1
delta=delta/2.

return r_eff, grid_res

Loading

0 comments on commit b2ba2dc

Please sign in to comment.