starplot 0.20.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. starplot/__init__.py +59 -0
  2. starplot/callables.py +176 -0
  3. starplot/cli.py +29 -0
  4. starplot/config.py +80 -0
  5. starplot/coordinates.py +7 -0
  6. starplot/data/__init__.py +22 -0
  7. starplot/data/catalogs.py +356 -0
  8. starplot/data/constellations.py +71 -0
  9. starplot/data/db.py +33 -0
  10. starplot/data/dsos.py +85 -0
  11. starplot/data/ecliptic.py +201 -0
  12. starplot/data/library/constellation_names.parquet +0 -0
  13. starplot/data/library/dso_names.parquet +0 -0
  14. starplot/data/library/readme.md +1 -0
  15. starplot/data/library/star_designations.parquet +0 -0
  16. starplot/data/stars.py +79 -0
  17. starplot/data/translations.py +408 -0
  18. starplot/data/utils.py +35 -0
  19. starplot/geometry.py +319 -0
  20. starplot/mixins.py +360 -0
  21. starplot/models/__init__.py +21 -0
  22. starplot/models/base.py +77 -0
  23. starplot/models/comet.py +302 -0
  24. starplot/models/constellation.py +151 -0
  25. starplot/models/dso.py +317 -0
  26. starplot/models/milky_way.py +30 -0
  27. starplot/models/moon.py +130 -0
  28. starplot/models/objects.py +29 -0
  29. starplot/models/observer.py +125 -0
  30. starplot/models/optics.py +342 -0
  31. starplot/models/planet.py +137 -0
  32. starplot/models/satellite.py +138 -0
  33. starplot/models/star.py +253 -0
  34. starplot/models/sun.py +62 -0
  35. starplot/plots/__init__.py +7 -0
  36. starplot/plots/base.py +1023 -0
  37. starplot/plots/galaxy.py +372 -0
  38. starplot/plots/horizon.py +543 -0
  39. starplot/plots/map.py +515 -0
  40. starplot/plots/optic.py +469 -0
  41. starplot/plots/zenith.py +217 -0
  42. starplot/plotters/__init__.py +9 -0
  43. starplot/plotters/arrow.py +174 -0
  44. starplot/plotters/constellations.py +298 -0
  45. starplot/plotters/debug.py +21 -0
  46. starplot/plotters/dsos.py +294 -0
  47. starplot/plotters/experimental.py +722 -0
  48. starplot/plotters/gradients.py +153 -0
  49. starplot/plotters/legend.py +253 -0
  50. starplot/plotters/milkyway.py +51 -0
  51. starplot/plotters/stars.py +319 -0
  52. starplot/plotters/text.py +802 -0
  53. starplot/profile.py +16 -0
  54. starplot/projections.py +184 -0
  55. starplot/styles/__init__.py +6 -0
  56. starplot/styles/base.py +1344 -0
  57. starplot/styles/ext/antique.yml +175 -0
  58. starplot/styles/ext/blue_dark.yml +163 -0
  59. starplot/styles/ext/blue_gold.yml +147 -0
  60. starplot/styles/ext/blue_light.yml +123 -0
  61. starplot/styles/ext/blue_medium.yml +142 -0
  62. starplot/styles/ext/blue_night.yml +185 -0
  63. starplot/styles/ext/cb_wong.yml +124 -0
  64. starplot/styles/ext/color_print.yml +111 -0
  65. starplot/styles/ext/gradient_presets.yml +158 -0
  66. starplot/styles/ext/grayscale.yml +94 -0
  67. starplot/styles/ext/grayscale_dark.yml +136 -0
  68. starplot/styles/ext/map.yml +12 -0
  69. starplot/styles/ext/nord.yml +158 -0
  70. starplot/styles/ext/optic.yml +20 -0
  71. starplot/styles/ext/publication.yml +8 -0
  72. starplot/styles/extensions.py +129 -0
  73. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  74. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  75. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  76. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  77. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  78. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  79. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  80. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  81. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  82. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  83. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  84. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  85. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  86. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  87. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  88. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  89. starplot/styles/fonts.py +15 -0
  90. starplot/styles/helpers.py +93 -0
  91. starplot/styles/markers.py +308 -0
  92. starplot/utils.py +169 -0
  93. starplot/warnings.py +21 -0
  94. starplot-0.20.5.dist-info/METADATA +146 -0
  95. starplot-0.20.5.dist-info/RECORD +98 -0
  96. starplot-0.20.5.dist-info/WHEEL +4 -0
  97. starplot-0.20.5.dist-info/entry_points.txt +3 -0
  98. starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,21 @@
1
+ # ruff: noqa: F401,F403
2
+
3
+ from .constellation import Constellation, ConstellationBorder
4
+ from .comet import Comet
5
+ from .dso import DSO, DsoType
6
+ from .star import Star
7
+ from .planet import Planet
8
+ from .moon import Moon
9
+ from .sun import Sun
10
+ from .optics import (
11
+ Optic,
12
+ Scope,
13
+ Reflector,
14
+ Refractor,
15
+ Binoculars,
16
+ Camera,
17
+ )
18
+ from .objects import ObjectList
19
+ from .satellite import Satellite
20
+ from .observer import Observer
21
+ from .milky_way import MilkyWay
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass, fields
2
+ from functools import cache
3
+ from typing import Optional
4
+
5
+ import pyarrow as pa
6
+ from skyfield.api import position_of_radec, load_constellation_map
7
+
8
+ from starplot.mixins import CreateMapMixin, CreateOpticMixin
9
+
10
+
11
+ @cache
12
+ def constellation_at():
13
+ return load_constellation_map()
14
+
15
+
16
+ @dataclass(slots=True, kw_only=True)
17
+ class SkyObject(
18
+ CreateMapMixin,
19
+ CreateOpticMixin,
20
+ ):
21
+ """
22
+ Base class for sky objects.
23
+
24
+ All sky object classes inherit from this base class.
25
+ """
26
+
27
+ ra: float
28
+ """Right Ascension, in degrees (0 to 360)"""
29
+
30
+ dec: float
31
+ """Declination, in degrees (-90 to 90)"""
32
+
33
+ constellation_id: Optional[str] = None
34
+ """Three-letter IAU id of the constellation that contains this object"""
35
+
36
+ healpix_index: int = None
37
+ """[HEALPix](https://en.wikipedia.org/wiki/HEALPix) pixel index of this object's RA/DEC"""
38
+
39
+ def constellation(self):
40
+ """Returns an instance of the [`Constellation`][starplot.models.Constellation] that contains this object, or `None` if no constellation is found."""
41
+ from starplot.models import Constellation
42
+
43
+ return Constellation.get(iau_id=self.constellation_id)
44
+
45
+ def populate_constellation_id(self):
46
+ """Populates the constellation_id field based on the location of this object"""
47
+ pos = position_of_radec(self.ra / 15, self.dec)
48
+ self.constellation_id = constellation_at()(pos).lower()
49
+
50
+ @classmethod
51
+ @cache
52
+ def _dir(cls):
53
+ return dir(cls)
54
+
55
+ @classmethod
56
+ @cache
57
+ def _fields(cls):
58
+ return [f.name for f in fields(cls)]
59
+
60
+ @classmethod
61
+ @cache
62
+ def _pyarrow_schema(cls):
63
+ """Returns explicit schema"""
64
+ return pa.schema(
65
+ [
66
+ pa.field("ra", pa.float64(), nullable=False),
67
+ pa.field("dec", pa.float64(), nullable=False),
68
+ pa.field("constellation_id", pa.string()),
69
+ pa.field("healpix_index", pa.int64()),
70
+ ]
71
+ )
72
+
73
+
74
+ @dataclass(kw_only=True)
75
+ class CatalogObject:
76
+ pk: int
77
+ """Primary key of object in catalog. Needs to be unique across all objects in the catalog."""
@@ -0,0 +1,302 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Iterator
3
+ from functools import cache
4
+ from dataclasses import dataclass, fields
5
+
6
+ from skyfield.data import mpc
7
+ from skyfield.constants import GM_SUN_Pitjeva_2005_km3_s2 as GM_SUN
8
+ from shapely import Point
9
+
10
+ from starplot.data import load
11
+ from starplot.models.base import SkyObject
12
+ from starplot.models.observer import Observer
13
+
14
+
15
+ @dataclass
16
+ class SkyfieldComet:
17
+ designation: str
18
+ reference: str
19
+
20
+ perihelion_year: int
21
+ perihelion_month: int
22
+ perihelion_day: float
23
+ perihelion_distance_au: float
24
+ eccentricity: float
25
+ argument_of_perihelion_degrees: float
26
+ longitude_of_ascending_node_degrees: float
27
+ inclination_degrees: float
28
+
29
+ perturbed_epoch_year: int | None = None
30
+ perturbed_epoch_month: int | None = None
31
+ perturbed_epoch_day: int | None = None
32
+
33
+ number: int | None = None
34
+ designation_packed: str | None = None
35
+ orbit_type: str | None = None
36
+ magnitude_g: float | None = None
37
+ magnitude_k: float | None = None
38
+
39
+ # Skyfield columns
40
+ # ('number', (0, 4)),
41
+ # ('orbit_type', (4, 5)),
42
+ # ('designation_packed', (5, 12)),
43
+ # ('perihelion_year', (14, 18)),
44
+ # ('perihelion_month', (19, 21)),
45
+ # ('perihelion_day', (22, 29)),
46
+ # ('perihelion_distance_au', (30, 39)),
47
+ # ('eccentricity', (41, 49)),
48
+ # ('argument_of_perihelion_degrees', (51, 59)),
49
+ # ('longitude_of_ascending_node_degrees', (61, 69)),
50
+ # ('inclination_degrees', (71, 79)),
51
+ # ('perturbed_epoch_year', (81, 85)),
52
+ # ('perturbed_epoch_month', (85, 87)),
53
+ # ('perturbed_epoch_day', (87, 89)),
54
+ # ('magnitude_g', (91, 95)),
55
+ # ('magnitude_k', (96, 100)),
56
+ # ('designation', (102, 158)),
57
+ # ('reference', (159, 168)),
58
+
59
+ """
60
+ MPC JSON
61
+ {
62
+ "Comet_num": 483,
63
+ "Orbit_type": "P",
64
+ "Year_of_perihelion": 2027,
65
+ "Month_of_perihelion": 11,
66
+ "Day_of_perihelion": 11.7324,
67
+ "Perihelion_dist": 2.486784,
68
+ "e": 0.221733,
69
+ "Peri": 49.461,
70
+ "Node": 199.0602,
71
+ "i": 14.1756,
72
+ "Epoch_year": 2025,
73
+ "Epoch_month": 10,
74
+ "Epoch_day": 22,
75
+ "H": 17.0,
76
+ "G": 4.0,
77
+ "Designation_and_name": "483P-B/PANSTARRS",
78
+ "Ref": "MPC185666"
79
+ }
80
+ """
81
+
82
+ @classmethod
83
+ def from_mpc_json(cls, data: dict) -> "SkyfieldComet":
84
+ """Converts an MPC orbit JSON for a comet to a dataclass that Skyfield can work with"""
85
+
86
+ d = {k.lower(): v for k, v in data.items()}
87
+
88
+ return SkyfieldComet(
89
+ number=d.get("comet_num"),
90
+ orbit_type=d.get("orbit_type"),
91
+ designation_packed=d.get("provisional_packed_desig"),
92
+ perihelion_year=d.get("year_of_perihelion"),
93
+ perihelion_month=d.get("month_of_perihelion"),
94
+ perihelion_day=d.get("day_of_perihelion"),
95
+ perihelion_distance_au=d.get("perihelion_dist"),
96
+ eccentricity=d.get("e"),
97
+ argument_of_perihelion_degrees=d.get("peri"),
98
+ longitude_of_ascending_node_degrees=d.get("node"),
99
+ inclination_degrees=d.get("i"),
100
+ perturbed_epoch_year=d.get("epoch_year"),
101
+ perturbed_epoch_month=d.get("epoch_month"),
102
+ perturbed_epoch_day=d.get("epoch_day"),
103
+ magnitude_g=d.get("g"),
104
+ # magnitude_k=None, # no mapping?
105
+ designation=d.get("designation_and_name"),
106
+ reference=d.get("ref"),
107
+ )
108
+
109
+ @classmethod
110
+ def from_mpc_remote(cls, name: str, reload: bool) -> "SkyfieldComet":
111
+ """
112
+ Create a Skyfield comet by downloading the latest MPC comet data and finding the comet by name/designation
113
+
114
+ Args:
115
+ name: Name/designation of the comet
116
+ reload: If True, then the MPC data will be re-downloaded before finding the comet
117
+ """
118
+ comets = get_comets(reload)
119
+ row = comets.loc[name]
120
+ return SkyfieldComet(**row.to_dict())
121
+
122
+ def __getitem__(self, key):
123
+ """Allows accessing dataclass fields using dictionary-like syntax."""
124
+ if key in [f.name for f in fields(self)]:
125
+ return getattr(self, key)
126
+ else:
127
+ raise KeyError
128
+
129
+
130
+ @dataclass(slots=True, kw_only=True)
131
+ class Comet(SkyObject):
132
+ """
133
+ Comets can be created in three ways:
134
+
135
+ 1. [`get`][starplot.Comet.get] (designation/name)
136
+ 2. [`all`][starplot.Comet.all] (iterate through all comets available from MPC)
137
+ 3. [`from_json`][starplot.Comet.from_json] (IAU MPC JSON)
138
+
139
+ """
140
+
141
+ name: str = None
142
+ """
143
+ Name of the comet (as designated by IAU Minor Planet Center)
144
+ """
145
+
146
+ observer: Observer = None
147
+ """Observer of this comet instance"""
148
+
149
+ distance: float | None = None
150
+ """Distance to comet, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
151
+
152
+ ephemeris: str = None
153
+ """Ephemeris used when retrieving this instance"""
154
+
155
+ geometry: Point = None
156
+ """Shapely Point of the comet's position. Right ascension coordinates are in degrees (0...360)."""
157
+
158
+ data: SkyfieldComet = None
159
+
160
+ @classmethod
161
+ def from_json(
162
+ cls,
163
+ data: dict,
164
+ observer: Observer = None,
165
+ ephemeris: str = "de421.bsp",
166
+ ) -> "Comet":
167
+ """
168
+ Get a comet for a specific date/time/location from an IAU MPC JSON.
169
+
170
+ Args:
171
+ data: Dictionary of the IAU MPC JSON
172
+ observer: Observer instance that specifies a time and location
173
+ ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
174
+ """
175
+ comet = SkyfieldComet.from_mpc_json(data)
176
+ observer = observer or Observer()
177
+
178
+ return get_comet_at_date_location(comet, observer, ephemeris)
179
+
180
+ @classmethod
181
+ def all(
182
+ cls,
183
+ observer: Observer = None,
184
+ ephemeris: str = "de421.bsp",
185
+ reload: bool = False,
186
+ ) -> Iterator["Comet"]:
187
+ """
188
+ Iterator for getting all comets at a specific date/time and observing location.
189
+
190
+ Args:
191
+ observer: Observer instance that specifies a time and location
192
+ ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
193
+ reload: If True, then the comet data file will be re-downloaded. Otherwise, it'll use the existing file if available.
194
+ """
195
+ comets = get_comets(reload=reload)
196
+ observer = observer or Observer()
197
+ for name in comets.index.values:
198
+ row = comets.loc[name]
199
+ comet = SkyfieldComet(**row.to_dict())
200
+ yield get_comet_at_date_location(
201
+ comet=comet,
202
+ observer=observer,
203
+ ephemeris=ephemeris,
204
+ )
205
+
206
+ @classmethod
207
+ def get(
208
+ cls,
209
+ name: str,
210
+ observer: Observer = None,
211
+ ephemeris: str = "de421.bsp",
212
+ reload: bool = False,
213
+ ) -> "Comet":
214
+ """
215
+ Get a comet for a specific date/time.
216
+
217
+ Args:
218
+ name: Name of the comet you want to get (as designated by IAU Minor Planet Center)
219
+ observer: Observer instance that specifies a time and location
220
+ ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
221
+ reload: If True, then the comet data file will be re-downloaded. Otherwise, it'll use the existing file if available.
222
+ """
223
+ comet = SkyfieldComet.from_mpc_remote(name, reload)
224
+ observer = observer or Observer()
225
+ return get_comet_at_date_location(comet, observer, ephemeris)
226
+
227
+ def trajectory(
228
+ self, date_start: datetime, date_end: datetime, step: timedelta = None
229
+ ) -> Iterator["Comet"]:
230
+ """
231
+ Iterator for getting a trajectory of the comet.
232
+
233
+ Args:
234
+ date_start: Starting date/time for the trajectory (inclusive)
235
+ date_end: End date/time for the trajectory (exclusive)
236
+ step: Time-step for the trajectory. Defaults to 1-day
237
+
238
+ Returns:
239
+ Iterator that yields a Comet instance at each step in the date range
240
+ """
241
+ dt = date_start
242
+ step = step or timedelta(days=1)
243
+
244
+ while dt < date_end:
245
+ observer_kwargs = self.observer.model_dump()
246
+ observer_kwargs["dt"] = dt
247
+ new_observer = Observer(**observer_kwargs)
248
+ yield get_comet_at_date_location(
249
+ comet=self.data,
250
+ observer=new_observer,
251
+ ephemeris=self.ephemeris,
252
+ )
253
+ dt += step
254
+
255
+
256
+ @cache
257
+ def get_comets(reload=False):
258
+ """
259
+ Gets ALL comets currently tracked by IAU Minor Planet Center.
260
+
261
+ Args:
262
+ reload: If True, then redownload the comet data if it already exists
263
+
264
+ Returns:
265
+ DataFrame of all comets, indexed by name
266
+ """
267
+
268
+ with load.open(mpc.COMET_URL, reload=reload) as f:
269
+ comets = mpc.load_comets_dataframe(f)
270
+
271
+ # Keep only the most recent orbit for each comet, and index by designation for fast lookup.
272
+ comets = (
273
+ comets.sort_values("reference")
274
+ .groupby("designation", as_index=False)
275
+ .last()
276
+ .set_index("designation", drop=False)
277
+ )
278
+
279
+ return comets
280
+
281
+
282
+ def get_comet_at_date_location(
283
+ comet: SkyfieldComet, observer: Observer, ephemeris: str
284
+ ) -> Comet:
285
+ """
286
+ Creates a Comet instance for date and (optional) observing location.
287
+ """
288
+ ts = load.timescale()
289
+ eph = load(ephemeris)
290
+ c = eph["sun"] + mpc.comet_orbit(comet, ts, GM_SUN)
291
+ ra, dec, distance = observer._astrometric(c, ephemeris=ephemeris)
292
+
293
+ return Comet(
294
+ name=comet.designation,
295
+ ra=ra.hours * 15,
296
+ dec=dec.degrees,
297
+ observer=observer,
298
+ distance=distance.au,
299
+ ephemeris=ephemeris,
300
+ geometry=Point(ra.hours * 15, dec.degrees),
301
+ data=comet,
302
+ )
@@ -0,0 +1,151 @@
1
+ from typing import Iterator
2
+ from dataclasses import dataclass
3
+
4
+ from ibis import _
5
+ from shapely import Polygon, MultiPolygon, LineString
6
+
7
+ from starplot.models.base import SkyObject, CatalogObject
8
+ from starplot.data.catalogs import Catalog, CONSTELLATIONS_IAU
9
+ from starplot.data.constellations import load
10
+
11
+
12
+ @dataclass(slots=True, kw_only=True)
13
+ class Constellation(CatalogObject, SkyObject):
14
+ """
15
+ Constellation model.
16
+ """
17
+
18
+ boundary: Polygon | MultiPolygon
19
+ """
20
+ Shapely Polygon of the constellation's boundary. Right ascension coordinates are in degrees (0...360).
21
+
22
+ If the constellation's boundary crosses the 0-meridian, then this will be a MultiPolygon split at the meridian.
23
+ """
24
+
25
+ star_hip_ids: list[int]
26
+ """List of HIP ids for stars that are part of the _lines_ for this constellation."""
27
+
28
+ star_hip_lines: list[list[int, int]]
29
+ """Nested list of star HIP ids that represent the lines of this constellation. Each pair of HIP ids represents a line between those stars."""
30
+
31
+ name: str = None
32
+ """Name of constellation"""
33
+
34
+ iau_id: str = None
35
+ """
36
+ International Astronomical Union (IAU) three-letter designation, all lowercase.
37
+
38
+ **Important**: Starplot treats Serpens as two separate constellations to make them easier to work with programatically.
39
+ Serpens Caput has the `iau_id` of `ser1` and Serpens Cauda is `ser2`
40
+ """
41
+
42
+ border: LineString = None
43
+ """
44
+ Shapely LineString of the constellation's border. Right ascension coordinates are in degrees (0...360).
45
+
46
+ Coordinates in this geometry may extend past RA=360, if the border crosses the 0-meridian. This makes it ready to plot with the `line` function.
47
+ """
48
+
49
+ def __repr__(self) -> str:
50
+ return f"Constellation(iau_id={self.iau_id}, name={self.name}, ra={self.ra}, dec={self.dec})"
51
+
52
+ @classmethod
53
+ def all(cls, catalog: Catalog = CONSTELLATIONS_IAU) -> Iterator["Constellation"]:
54
+ """
55
+ Get all constellations from a catalog
56
+
57
+ Args:
58
+ catalog: Catalog you want to get constellation objects from
59
+
60
+ Returns:
61
+ Iterator of Constellation instances
62
+ """
63
+ df = load(catalog=catalog).to_pandas()
64
+
65
+ for c in df.itertuples():
66
+ yield from_tuple(c)
67
+
68
+ @classmethod
69
+ def get(
70
+ cls, catalog: Catalog = CONSTELLATIONS_IAU, sql: str = None, **kwargs
71
+ ) -> "Constellation":
72
+ """
73
+ Get a Constellation, by matching its attributes.
74
+
75
+ Example:
76
+
77
+ hercules = Constellation.get(name="Hercules")
78
+
79
+ Args:
80
+ catalog: The catalog of constellations to use
81
+ sql: SQL query for selecting constellation (table name is "_")
82
+ **kwargs: Attributes on the constellation you want to match
83
+
84
+ Raises: `ValueError` if more than one constellation is matched
85
+ """
86
+ filters = []
87
+
88
+ for k, v in kwargs.items():
89
+ filters.append(getattr(_, k) == v)
90
+
91
+ df = load(catalog=catalog, filters=filters, sql=sql).to_pandas()
92
+ results = [from_tuple(c) for c in df.itertuples()]
93
+
94
+ if len(results) == 1:
95
+ return results[0]
96
+
97
+ if len(results) > 1:
98
+ raise ValueError(
99
+ "More than one match. Use find() instead or narrow your search."
100
+ )
101
+
102
+ return None
103
+
104
+ @classmethod
105
+ def find(
106
+ cls,
107
+ catalog: Catalog = CONSTELLATIONS_IAU,
108
+ where: list = None,
109
+ sql: str = None,
110
+ ) -> list["Constellation"]:
111
+ """
112
+ Find Constellations
113
+
114
+ Args:
115
+ catalog: The catalog of constellations to use
116
+ where: A list of expressions that determine which constellations to find. See [Selecting Objects](/reference-selecting-objects/) for details.
117
+ sql: SQL query for selecting constellations (table name is "_")
118
+
119
+ Returns:
120
+ List of Constellations that match all `where` expressions
121
+
122
+ """
123
+ df = load(catalog=catalog, filters=where, sql=sql).to_pandas()
124
+
125
+ return [from_tuple(c) for c in df.itertuples()]
126
+
127
+ def constellation(self):
128
+ """Not applicable to Constellation model, raises `NotImplementedError`"""
129
+ raise NotImplementedError()
130
+
131
+ @classmethod
132
+ def get_label(cls, constellation):
133
+ """
134
+ Default function for determining the plotted label for a constellation
135
+
136
+ Returns the uppercase name of the constellation.
137
+
138
+ """
139
+ return constellation.name.upper().replace(" ", "\n")
140
+
141
+
142
+ def from_tuple(c: tuple) -> Constellation:
143
+ kwargs = {f: getattr(c, f) for f in Constellation._fields() if hasattr(c, f)}
144
+ c = Constellation(**kwargs)
145
+ return c
146
+
147
+
148
+ @dataclass(slots=True, kw_only=True)
149
+ class ConstellationBorder(CatalogObject, SkyObject):
150
+ geometry: LineString
151
+ """Shapely LineString of the border. Right ascension coordinates are in degrees (0...360)."""