Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+83433b07ee,g16d25e1f1b+23bc9e47ac,g1ec0fe41b4+3ea9d11450,g1fd858c14a+9be2b0f3b9,g2440f9efcc+8c5ae1fdc5,g35bb328faa+8c5ae1fdc5,g4a4af6cd76+d25431c27e,g4d2262a081+c74e83464e,g53246c7159+8c5ae1fdc5,g55585698de+1e04e59700,g56a49b3a55+92a7603e7a,g60b5630c4e+1e04e59700,g67b6fd64d1+3fc8cb0b9e,g78460c75b0+7e33a9eb6d,g786e29fd12+668abc6043,g8352419a5c+8c5ae1fdc5,g8852436030+60e38ee5ff,g89139ef638+3fc8cb0b9e,g94187f82dc+1e04e59700,g989de1cb63+3fc8cb0b9e,g9d31334357+1e04e59700,g9f33ca652e+0a83e03614,gabe3b4be73+8856018cbb,gabf8522325+977d9fabaf,gb1101e3267+8b4b9c8ed7,gb89ab40317+3fc8cb0b9e,gc0af124501+57ccba3ad1,gcf25f946ba+60e38ee5ff,gd6cbbdb0b4+1cc2750d2e,gd794735e4e+7be992507c,gdb1c4ca869+be65c9c1d7,gde0f65d7ad+c7f52e58fe,ge278dab8ac+6b863515ed,ge410e46f29+3fc8cb0b9e,gf35d7ec915+97dd712d81,gf5e32f922b+8c5ae1fdc5,gf618743f1b+747388abfa,gf67bdafdda+3fc8cb0b9e,w.2025.18
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
_continue_class.py
Go to the documentation of this file.
1# This file is part of sphgeom.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27#
28
29"""Extend any of the C++ Python classes by adding additional methods."""
30
31# Nothing to export.
32__all__ = []
33
34import math
35import sys
36import typing
37
38from ._sphgeom import Angle, Box, Circle, ConvexPolygon, LonLat, Region, UnitVector3d
39
40# Copy and paste from lsst.utils.wrappers:
41# * INTRINSIC_SPECIAL_ATTRIBUTES
42# * isAttributeSafeToTransfer
43# * continueClass
44_INTRINSIC_SPECIAL_ATTRIBUTES = frozenset(
45 (
46 "__qualname__",
47 "__module__",
48 "__metaclass__",
49 "__dict__",
50 "__weakref__",
51 "__class__",
52 "__subclasshook__",
53 "__name__",
54 "__doc__",
55 )
56)
57
58
59def _isAttributeSafeToTransfer(name: str, value: typing.Any) -> bool:
60 if name.startswith("__") and (
61 value is getattr(object, name, None) or name in _INTRINSIC_SPECIAL_ATTRIBUTES
62 ):
63 return False
64 return True
65
66
68 orig = getattr(sys.modules[cls.__module__], cls.__name__)
69 for name in dir(cls):
70 # Common descriptors like classmethod and staticmethod can only be
71 # accessed without invoking their magic if we use __dict__; if we use
72 # getattr on those we'll get e.g. a bound method instance on the dummy
73 # class rather than a classmethod instance we can put on the target
74 # class.
75 attr = cls.__dict__.get(name, None) or getattr(cls, name)
76 if _isAttributeSafeToTransfer(name, attr):
77 setattr(orig, name, attr)
78 return orig
79
80
81def _inf_to_limit(value: float, min: float, max: float) -> float:
82 """Map a value to a fixed range if infinite."""
83 if not math.isinf(value):
84 return value
85 if value > 0.0:
86 return max
87 return min
88
89
90def _inf_to_lat(lat: float) -> float:
91 """Map latitude +Inf to +90 and -Inf to -90 degrees."""
92 return _inf_to_limit(lat, -90.0, 90.0)
93
94
95def _inf_to_lon(lat: float) -> float:
96 """Map longitude +Inf to +360 and -Inf to 0 degrees."""
97 return _inf_to_limit(lat, 0.0, 360.0)
98
99
100@_continueClass
101class Region:
102 """A minimal interface for 2-dimensional regions on the unit sphere."""
103
104 @classmethod
105 def from_ivoa_pos(cls, pos: str) -> Region:
106 """Create a Region from an IVOA POS string.
107
108 Parameters
109 ----------
110 pos : `str`
111 A string using the IVOA SIAv2 POS syntax.
112
113 Returns
114 -------
115 region : `Region`
116 A region equivalent to the POS string.
117
118 Notes
119 -----
120 See
121 https://ivoa.net/documents/SIA/20151223/REC-SIA-2.0-20151223.html#toc12
122 for a description of the POS parameter but in summary the options are:
123
124 * ``CIRCLE <longitude> <latitude> <radius>``
125 * ``RANGE <longitude1> <longitude2> <latitude1> <latitude2>``
126 * ``POLYGON <longitude1> <latitude1> ... (at least 3 pairs)``
127
128 Units are degrees in all coordinates.
129 """
130 shape, *coordinates = pos.split()
131 coordinates = tuple(float(c) for c in coordinates)
132 n_floats = len(coordinates)
133 if shape == "CIRCLE":
134 if n_floats != 3:
135 raise ValueError(f"CIRCLE requires 3 numbers but got {n_floats} in '{pos}'.")
136 center = LonLat.fromDegrees(coordinates[0], coordinates[1])
137 radius = Angle.fromDegrees(coordinates[2])
138 return Circle(UnitVector3d(center), radius)
139
140 if shape == "RANGE":
141 if n_floats != 4:
142 raise ValueError(f"RANGE requires 4 numbers but got {n_floats} in '{pos}'.")
143 # POS allows +Inf and -Inf in ranges. These are not allowed by
144 # Box and so must be converted.
145 return Box(
146 LonLat.fromDegrees(_inf_to_lon(coordinates[0]), _inf_to_lat(coordinates[2])),
147 LonLat.fromDegrees(_inf_to_lon(coordinates[1]), _inf_to_lat(coordinates[3])),
148 )
149
150 if shape == "POLYGON":
151 if n_floats % 2 != 0:
152 raise ValueError(f"POLYGON requires even number of floats but got {n_floats} in '{pos}'.")
153 if n_floats < 6:
154 raise ValueError(
155 f"POLYGON specification requires at least 3 coordinates, got {n_floats // 2} in '{pos}'"
156 )
157 # Coordinates are x1, y1, x2, y2, x3, y3...
158 # Get pairs by skipping every other value.
159 pairs = list(zip(coordinates[0::2], coordinates[1::2], strict=True))
160 vertices = [LonLat.fromDegrees(lon, lat) for lon, lat in pairs]
161 return ConvexPolygon([UnitVector3d(c) for c in vertices])
162
163 raise ValueError(f"Unrecognized shape in POS string '{pos}'")
164
165 def to_ivoa_pos(self) -> str:
166 """Represent the region as an IVOA POS string.
167
168 Returns
169 -------
170 pos : `str`
171 The region in ``POS`` format.
172 """
173 raise NotImplementedError("This region can not be converted to an IVOA POS string.")
174
175
176@_continueClass
177class Circle: # noqa: F811
178 """A circular region on the unit sphere that contains its boundary."""
179
180 def to_ivoa_pos(self) -> str:
181 # Docstring inherited.
182 center = LonLat(self.getCenter())
183 lon = center.getLon().asDegrees()
184 lat = center.getLat().asDegrees()
185 rad = self.getOpeningAngle().asDegrees()
186 return f"CIRCLE {lon} {lat} {rad}"
187
188
189@_continueClass
190class Box: # noqa: F811
191 """A rectangle in spherical coordinate space that contains its boundary."""
192
193 def to_ivoa_pos(self) -> str:
194 # Docstring inherited.
195 lon_range = self.getLon()
196 lat_range = self.getLat()
197
198 lon1 = lon_range.getA().asDegrees()
199 lon2 = lon_range.getB().asDegrees()
200 lat1 = lat_range.getA().asDegrees()
201 lat2 = lat_range.getB().asDegrees()
202
203 # Do not attempt to map to +/- Inf -- there is no way to know if
204 # that is any better than 0. -> 360.
205 return f"RANGE {lon1} {lon2} {lat1} {lat2}"
206
207
208@_continueClass
209class ConvexPolygon: # noqa: F811
210 """A rectangle in spherical coordinate space that contains its boundary."""
211
212 def to_ivoa_pos(self) -> str:
213 # Docstring inherited.
214 coords = (LonLat(v) for v in self.getVertices())
215 coord_strings = [f"{c.getLon().asDegrees()} {c.getLat().asDegrees()}" for c in coords]
216
217 return f"POLYGON {' '.join(coord_strings)}"
LonLat represents a spherical coordinate (longitude/latitude angle) pair.
Definition LonLat.h:55
UnitVector3d is a unit vector in ℝ³ with components stored in double precision.
bool _isAttributeSafeToTransfer(str name, typing.Any value)
float _inf_to_limit(float value, float min, float max)