.. _spherical_bounding_regions:
Bounding Regions for On-Sky Spatial Searches
============================================
The `~regions.SphericalSkyRegion` classes also provide
`~regions.SphericalSkyRegion.bounding_circle` and
`~regions.SphericalSkyRegion.bounding_lonlat` properties,
yielding the spherical circle and longitude and latitude ranges
enclosing the region, respectively.
This bounding information can be helpful when querying databases
to select objects with tools such as
`Astroquery `_
or `PyVO's TAP service `_.
For many regions of interest, the spherical region properties can
directly provide the query search boundary
(e.g., the center and radius for a circle for a cone search,
or the vertices for a polygon region search).
However, this depends on what query shapes are supported by
the database.
When a particular region shape is not supported by a
database, these bounding properties can be used to define a
"padded" search, and the results can then be downselected to only
those within the region using the `~regions.SphericalSkyRegion.contains()`.
(In particular, the bounding properties may be helpful when a
coordinate frame transformation is necessary to match the database
coordinate system; see :ref:`spherical_frame_transform`.)
As an example, let's create two spherical regions, a circle and range,
and determine their bounding longitude/latitude spans and circle:
.. code-block:: python
>>> from astropy.coordinates import SkyCoord
>>> from astropy import units as u
>>> from regions import CircleSphericalSkyRegion, RangeSphericalSkyRegion
>>> sph_circ = CircleSphericalSkyRegion(SkyCoord(100,-40,unit=u.deg,
... frame="galactic"),
... 30*u.deg)
>>> print(sph_circ.bounding_lonlat) # doctest: +FLOAT_CMP
(, )
>>> sph_range = RangeSphericalSkyRegion(frame="galactic",
... longitude_range=[-45,45]*u.deg,
... latitude_range=[0,45]*u.deg)
>>> print(sph_range.bounding_circle) # doctest: +FLOAT_CMP
Region: CircleSphericalSkyRegion
center:
radius: 47.2657903991002 deg
(The bounding circle of a circle is always itself, and in the original
coordinate frame, the bounding longitude/latitude of a
spherical range is equivalent to the region.)
Let's now transform these regions into the ICRS frame,
and obtain the new bounds, including the new frame longitude/latitude
bounds on the transformed Range region:
.. code-block:: python
>>> sph_circ_transf = sph_circ.transform_to("icrs")
>>> print(sph_circ_transf.bounding_lonlat) # doctest: +FLOAT_CMP
(, )
>>> sph_range_transf = sph_range.transform_to("icrs")
>>> print(sph_range_transf.bounding_lonlat) # doctest: +FLOAT_CMP
(, )
>>> print(sph_range_transf.bounding_circle) # doctest: +FLOAT_CMP
Region: CircleSphericalSkyRegion
center:
radius: 47.26579039910021 deg
These bounding circles and longitude/latitude are visualized below for both
the original and transformed coordinate frames.
.. plot::
:include-source: false
from astropy.coordinates import SkyCoord, Angle
from astropy.visualization.wcsaxes.frame import EllipticalFrame
from astropy import units as u
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from regions import (CircleSphericalSkyRegion,
RangeSphericalSkyRegion,
make_example_dataset)
dataset = make_example_dataset(data='simulated')
wcs = dataset.wcs
dataset_icrs = make_example_dataset(data='simulated', config={'ctype':
('RA---AIT',
'DEC--AIT')})
wcs_icrs = dataset_icrs.wcs
sph_circ = CircleSphericalSkyRegion(SkyCoord(100,-40,
unit=u.deg,
frame="galactic"),
30*u.deg)
sph_range = RangeSphericalSkyRegion(frame="galactic",
longitude_range=[-45,45]*u.deg,
latitude_range=[0,45]*u.deg)
sph_circ_transf = sph_circ.transform_to("icrs")
sph_range_transf = sph_range.transform_to("icrs")
fig = plt.figure()
fig.set_size_inches(7,7)
axes = []
axes.append(fig.add_axes([0.15, 0.575, 0.8, 0.425],
projection=wcs,
frame_class=EllipticalFrame))
axes.append(fig.add_axes([0.15, 0.05, 0.8, 0.425],
projection=wcs_icrs,
frame_class=EllipticalFrame))
ax = axes[0]
ax.coords.grid(color='gray')
ax.set_xlabel(r"Galactic $\ell$", labelpad=10)
ax.set_ylabel(r"Galactic $b$")
ax.set_title("Galactic coordinates", pad=5)
patch = sph_circ.to_pixel(
wcs=wcs,
include_boundary_distortions=True,
discretize_kwargs={"n_points":1000}
).plot(ax=ax, color='tab:blue', lw=3)
sph_range.to_pixel(
wcs=wcs,
include_boundary_distortions=True,
discretize_kwargs={"n_points":250}
).plot(ax=ax, color='tab:red', lw=3)
bound_color = 'tab:blue'
bound_lw = 0.75
bound_zord = 2
bound_lon, bound_lat = sph_circ.bounding_lonlat
for i in range(2):
# Need to manually "super sample" to get correct distortion.
# Unfortunately axv/hline works for just "aitoff" projection,
# not doing a WCS it seems.
npt = 250
xarr = np.repeat(bound_lon[i].degree, npt)
yarr = np.linspace(-90,90,num=npt,endpoint=True)
l1 = Line2D(xarr, yarr, ls='--', color=bound_color,
lw=bound_lw, zorder=bound_zord,
transform=ax.get_transform('galactic'))
xarr = np.linspace(-180,180,num=npt,endpoint=True)
yarr = np.repeat(bound_lat[i].degree, npt)
l2 = Line2D(xarr, yarr, ls='--', color=bound_color,
lw=bound_lw, zorder=bound_zord,
transform=ax.get_transform('galactic'))
ax.add_artist(l1)
ax.add_artist(l2)
bound_color = 'tab:red'
sph_range.bounding_circle.to_pixel(
wcs=wcs,
include_boundary_distortions=True,
discretize_kwargs={"n_points":1000}
).plot(ax=ax, color='tab:red', lw=bound_lw, ls='--', zorder=bound_zord)
patch.set_clip_path(ax.coords.frame.patch)
ax.set_xlim(20,340)
ax.set_ylim(10,170)
ax = axes[1]
ax.coords.grid(color='gray')
ax.set_xlabel("RA", labelpad=10)
ax.set_ylabel("Dec")
ax.set_title("ICRS coordinates", pad=5)
patch = sph_circ_transf.to_pixel(
wcs=wcs_icrs,
include_boundary_distortions=True,
discretize_kwargs={"n_points":1000}
).plot(ax=ax, color='tab:blue', lw=3)
sph_range_transf.to_pixel(
wcs=wcs_icrs,
include_boundary_distortions=True,
discretize_kwargs={"n_points":250}
).plot(ax=ax, color='tab:red', lw=3)
bound_color = 'tab:blue'
bound_lw = 0.75
bound_zord = 2
bound_lon, bound_lat = sph_circ_transf.bounding_lonlat
for i in range(2):
# Need to manually "super sample" to get correct distortion.
# Unfortunately axv/hline works for just "aitoff" projection,
# not doing a WCS it seems.
npt = 250
xarr = np.repeat(bound_lon[i].degree, npt)
yarr = np.linspace(-90,90,num=npt,endpoint=True)
l1 = Line2D(xarr, yarr, ls='--', color=bound_color,
lw=bound_lw, zorder=bound_zord,
transform=ax.get_transform('icrs'))
xarr = np.linspace(-180,180,num=npt,endpoint=True)
yarr = np.repeat(bound_lat[i].degree, npt)
l2 = Line2D(xarr, yarr, ls='--', color=bound_color,
lw=bound_lw, zorder=bound_zord,
transform=ax.get_transform('icrs'))
ax.add_artist(l1)
ax.add_artist(l2)
bound_color = 'tab:red'
bound_lon, bound_lat = sph_range_transf.bounding_lonlat
for i in range(2):
# Need to manually "super sample" to get correct distortion.
# Unfortunately axv/hline works for just "aitoff" projection,
# not doing a WCS it seems.
npt = 250
xarr = np.repeat(bound_lon[i].degree, npt)
yarr = np.linspace(-90,90,num=npt,endpoint=True)
l1 = Line2D(xarr, yarr, ls='--', color=bound_color,
lw=bound_lw, zorder=bound_zord,
transform=ax.get_transform('icrs'))
xarr = np.linspace(-180,180,num=npt,endpoint=True)
yarr = np.repeat(bound_lat[i].degree, npt)
l2 = Line2D(xarr, yarr, ls='--', color=bound_color,
lw=bound_lw, zorder=bound_zord,
transform=ax.get_transform('icrs'))
ax.add_artist(l1)
ax.add_artist(l2)
sph_range_transf.bounding_circle.to_pixel(
wcs=wcs_icrs,
include_boundary_distortions=True,
discretize_kwargs={"n_points":1000}
).plot(ax=ax, color='tab:red', lw=bound_lw, ls='--', zorder=bound_zord)
patch.set_clip_path(ax.coords.frame.patch)
ax.set_xlim(20,340)
ax.set_ylim(10,170)
ax.coords[0].set_format_unit(u.deg)