starplot 0.14.0__py2.py3-none-any.whl → 0.15.0__py2.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 (62) hide show
  1. starplot/__init__.py +3 -1
  2. starplot/base.py +149 -37
  3. starplot/cli.py +33 -0
  4. starplot/data/__init__.py +6 -24
  5. starplot/data/bigsky.py +58 -40
  6. starplot/data/constellation_lines.py +827 -0
  7. starplot/data/constellation_stars.py +1501 -0
  8. starplot/data/constellations.py +43 -32
  9. starplot/data/db.py +17 -0
  10. starplot/data/dsos.py +24 -141
  11. starplot/data/stars.py +45 -24
  12. starplot/geod.py +0 -6
  13. starplot/geometry.py +105 -6
  14. starplot/horizon.py +118 -107
  15. starplot/map.py +45 -96
  16. starplot/mixins.py +75 -14
  17. starplot/models/__init__.py +1 -1
  18. starplot/models/base.py +10 -128
  19. starplot/models/constellation.py +55 -32
  20. starplot/models/dso.py +132 -67
  21. starplot/models/moon.py +57 -78
  22. starplot/models/planet.py +44 -69
  23. starplot/models/star.py +91 -60
  24. starplot/models/sun.py +32 -53
  25. starplot/optic.py +14 -17
  26. starplot/plotters/constellations.py +81 -78
  27. starplot/plotters/dsos.py +49 -68
  28. starplot/plotters/experimental.py +1 -1
  29. starplot/plotters/milkyway.py +18 -20
  30. starplot/plotters/stars.py +91 -116
  31. starplot/profile.py +16 -0
  32. starplot/settings.py +26 -0
  33. starplot/styles/__init__.py +2 -0
  34. starplot/styles/base.py +7 -17
  35. starplot/styles/ext/blue_gold.yml +135 -0
  36. starplot/styles/ext/blue_light.yml +5 -4
  37. starplot/styles/ext/blue_medium.yml +11 -7
  38. starplot/styles/extensions.py +1 -0
  39. starplot/warnings.py +5 -0
  40. {starplot-0.14.0.dist-info → starplot-0.15.0.dist-info}/METADATA +11 -11
  41. {starplot-0.14.0.dist-info → starplot-0.15.0.dist-info}/RECORD +44 -54
  42. starplot-0.15.0.dist-info/entry_points.txt +3 -0
  43. starplot/data/bayer.py +0 -3499
  44. starplot/data/flamsteed.py +0 -2682
  45. starplot/data/library/constellation_borders_inv.gpkg +0 -0
  46. starplot/data/library/constellation_lines_hips.json +0 -709
  47. starplot/data/library/constellation_lines_inv.gpkg +0 -0
  48. starplot/data/library/constellations.gpkg +0 -0
  49. starplot/data/library/constellations_hip.fab +0 -88
  50. starplot/data/library/milkyway.gpkg +0 -0
  51. starplot/data/library/milkyway_inv.gpkg +0 -0
  52. starplot/data/library/ongc.gpkg.zip +0 -0
  53. starplot/data/library/stars.bigsky.mag11.parquet +0 -0
  54. starplot/data/library/stars.hipparcos.parquet +0 -0
  55. starplot/data/messier.py +0 -111
  56. starplot/data/prep/__init__.py +0 -0
  57. starplot/data/prep/constellations.py +0 -108
  58. starplot/data/prep/dsos.py +0 -299
  59. starplot/data/prep/utils.py +0 -16
  60. starplot/models/geometry.py +0 -44
  61. {starplot-0.14.0.dist-info → starplot-0.15.0.dist-info}/LICENSE +0 -0
  62. {starplot-0.14.0.dist-info → starplot-0.15.0.dist-info}/WHEEL +0 -0
starplot/mixins.py CHANGED
@@ -1,34 +1,95 @@
1
+ from functools import cache
2
+
1
3
  from shapely import Polygon, MultiPolygon
2
4
 
3
5
 
4
6
  class ExtentMaskMixin:
7
+ @cache
5
8
  def _extent_mask(self):
6
9
  """
7
10
  Returns shapely geometry objects of extent (RA = 0...360)
8
11
 
9
- If the extent crosses equinox, then two Polygons will be returned
12
+ If the extent crosses equinox, then a MultiPolygon will be returned
10
13
  """
11
- if self.ra_max < 24:
14
+ if self.ra_max <= 360:
12
15
  coords = [
13
- [self.ra_min * 15, self.dec_min],
14
- [self.ra_max * 15, self.dec_min],
15
- [self.ra_min * 15, self.dec_max],
16
- [self.ra_max * 15, self.dec_max],
16
+ [self.ra_min, self.dec_min],
17
+ [self.ra_max, self.dec_min],
18
+ [self.ra_max, self.dec_max],
19
+ [self.ra_min, self.dec_max],
20
+ [self.ra_min, self.dec_min],
17
21
  ]
18
22
  return Polygon(coords)
19
23
 
20
24
  else:
21
25
  coords_1 = [
22
- [self.ra_min * 15, self.dec_min],
26
+ [self.ra_min, self.dec_min],
23
27
  [360, self.dec_min],
24
- [self.ra_min * 15, self.dec_max],
25
28
  [360, self.dec_max],
29
+ [self.ra_min, self.dec_max],
30
+ [self.ra_min, self.dec_min],
26
31
  ]
27
32
  coords_2 = [
28
33
  [0, self.dec_min],
29
- [(self.ra_max - 24) * 15, self.dec_min],
34
+ [(self.ra_max - 360), self.dec_min],
35
+ [(self.ra_max - 360), self.dec_max],
30
36
  [0, self.dec_max],
31
- [(self.ra_max - 24) * 15, self.dec_max],
37
+ [0, self.dec_min],
38
+ ]
39
+
40
+ return MultiPolygon(
41
+ [
42
+ Polygon(coords_1),
43
+ Polygon(coords_2),
44
+ ]
45
+ )
46
+
47
+ @cache
48
+ def _extent_mask_altaz(self):
49
+ """
50
+ Returns shapely geometry objects of the alt/az extent
51
+
52
+ If the extent crosses North cardinal direction, then a MultiPolygon will be returned
53
+ """
54
+ extent = list(self.ax.get_extent(crs=self._plate_carree))
55
+ alt_min, alt_max = extent[2], extent[3]
56
+ az_min, az_max = extent[0], extent[1]
57
+
58
+ if az_min < 0:
59
+ az_min += 360
60
+ if az_max < 0:
61
+ az_max += 360
62
+
63
+ if az_min >= az_max:
64
+ az_max += 360
65
+
66
+ self.az = (az_min, az_max)
67
+ self.alt = (alt_min, alt_max)
68
+
69
+ if az_max <= 360:
70
+ coords = [
71
+ [az_min, alt_min],
72
+ [az_max, alt_min],
73
+ [az_max, alt_max],
74
+ [az_min, alt_max],
75
+ [az_min, alt_min],
76
+ ]
77
+ return Polygon(coords)
78
+
79
+ else:
80
+ coords_1 = [
81
+ [az_min, alt_min],
82
+ [360, alt_min],
83
+ [360, alt_max],
84
+ [az_min, alt_max],
85
+ [az_min, alt_min],
86
+ ]
87
+ coords_2 = [
88
+ [0, alt_min],
89
+ [az_max - 360, alt_min],
90
+ [az_max - 360, alt_max],
91
+ [0, alt_max],
92
+ [0, alt_min],
32
93
  ]
33
94
 
34
95
  return MultiPolygon(
@@ -43,7 +104,7 @@ class ExtentMaskMixin:
43
104
  return all(
44
105
  [
45
106
  self.ra_min == 0,
46
- self.ra_max == 24,
107
+ self.ra_max == 360,
47
108
  self.dec_min == -90,
48
109
  self.dec_max == 90,
49
110
  ]
@@ -71,14 +132,14 @@ class CreateMapMixin:
71
132
  height_degrees=height_degrees,
72
133
  width_degrees=width_degrees,
73
134
  )
74
- ra_min = ex[0][0] / 15
75
- ra_max = ex[2][0] / 15
135
+ ra_min = ex[0][0]
136
+ ra_max = ex[2][0]
76
137
  dec_min = ex[0][1]
77
138
  dec_max = ex[2][1]
78
139
 
79
140
  # handle wrapping
80
141
  if ra_max < ra_min:
81
- ra_max += 24
142
+ ra_max += 360
82
143
 
83
144
  p = MapPlot(
84
145
  ra_min=ra_min,
@@ -1,5 +1,5 @@
1
1
  from .constellation import Constellation # noqa: F401,F403
2
- from .dso import DSO # noqa: F401,F403
2
+ from .dso import DSO, DsoType # noqa: F401,F403
3
3
  from .star import Star # noqa: F401,F403
4
4
  from .planet import Planet # noqa: F401,F403
5
5
  from .moon import Moon # noqa: F401,F403
starplot/models/base.py CHANGED
@@ -1,114 +1,23 @@
1
+ from functools import cache
1
2
  from typing import Optional
2
- from abc import ABC, abstractmethod
3
3
 
4
4
  from skyfield.api import position_of_radec, load_constellation_map
5
5
 
6
6
  from starplot.mixins import CreateMapMixin, CreateOpticMixin
7
7
 
8
- constellation_at = load_constellation_map()
9
8
 
9
+ @cache
10
+ def constellation_at():
11
+ return load_constellation_map()
10
12
 
11
- class Expression:
12
- def __init__(self, func=None) -> None:
13
- self.func = func
14
13
 
15
- def evaluate(self, obj):
16
- return self.func(obj)
17
-
18
- def __or__(self, other):
19
- return Expression(func=lambda d: self.evaluate(d) or other.evaluate(d))
20
-
21
- def __and__(self, other):
22
- return Expression(func=lambda d: self.evaluate(d) and other.evaluate(d))
23
-
24
-
25
- class Term:
26
- def _factory(self, func):
27
- def exp(c):
28
- value = getattr(c, self.attr)
29
-
30
- if value is None:
31
- return False
32
-
33
- return func(value)
34
-
35
- return Expression(func=exp)
36
-
37
- def __init__(self, attr):
38
- self.attr = attr
39
-
40
- def __eq__(self, other):
41
- return Expression(
42
- func=lambda c: getattr(c, self.attr) == other
43
- if other is not None
44
- else getattr(c, self.attr) is None
45
- )
46
-
47
- def __ne__(self, other):
48
- return Expression(
49
- func=lambda c: getattr(c, self.attr) != other
50
- if other is not None
51
- else getattr(c, self.attr) is not None
52
- )
53
-
54
- def __lt__(self, other):
55
- return self._factory(lambda value: value < other)
56
-
57
- def __le__(self, other):
58
- return self._factory(lambda value: value <= other)
59
-
60
- def __gt__(self, other):
61
- return self._factory(lambda value: value > other)
62
-
63
- def __ge__(self, other):
64
- return self._factory(lambda value: value >= other)
65
-
66
- def is_in(self, other):
67
- """Returns `True` if the field's value is in `other`"""
68
- return Expression(func=lambda c: getattr(c, self.attr) in other)
69
-
70
- def is_not_in(self, other):
71
- """Returns `True` if the field's value is NOT in `other`"""
72
- return Expression(func=lambda c: getattr(c, self.attr) not in other)
73
-
74
- def is_null(self):
75
- """Returns `True` if the field value is `None`"""
76
- return Expression(func=lambda c: getattr(c, self.attr) is None)
77
-
78
- def is_not_null(self):
79
- """Returns `True` if the field value is NOT `None`"""
80
- return Expression(func=lambda c: getattr(c, self.attr) is not None)
81
-
82
- def intersects(self, other):
83
- """Returns `True` if the field's value intersects `other`. Only available for geometry-type fields."""
84
- return Expression(func=lambda c: getattr(c, self.attr).intersects(other))
85
-
86
-
87
- class Meta(type):
88
- managers = {}
89
-
90
- def __new__(cls, name, bases, attrs):
91
- new_cls = super().__new__(cls, name, bases, attrs)
92
-
93
- if "_manager" in attrs:
94
- Meta.managers[new_cls] = attrs["_manager"]
95
-
96
- return new_cls
97
-
98
- def __getattribute__(cls, attr):
99
- if attr in ["get", "find", "all"]:
100
- return getattr(Meta.managers[cls], attr)
101
-
102
- return Term(attr)
103
-
104
-
105
- class SkyObject(CreateMapMixin, CreateOpticMixin, metaclass=Meta):
14
+ class SkyObject(CreateMapMixin, CreateOpticMixin):
106
15
  """
107
16
  Basic sky object model.
108
17
  """
109
18
 
110
19
  ra: float
111
- """Right Ascension, in hours (0...24)"""
20
+ """Right Ascension, in degrees (0...360)"""
112
21
 
113
22
  dec: float
114
23
  """Declination, in degrees (-90...90)"""
@@ -118,16 +27,18 @@ class SkyObject(CreateMapMixin, CreateOpticMixin, metaclass=Meta):
118
27
  constellation_id: Optional[str] = None
119
28
  """Identifier of the constellation that contains this object. The ID is the three-letter (all lowercase) abbreviation from the International Astronomical Union (IAU)."""
120
29
 
121
- def __init__(self, ra: float, dec: float) -> None:
30
+ def __init__(self, ra: float, dec: float, constellation_id: str = None) -> None:
122
31
  self.ra = ra
123
32
  self.dec = dec
33
+ if constellation_id:
34
+ self._constellation_id = constellation_id
124
35
 
125
36
  @property
126
37
  def constellation_id(self):
127
38
  """Identifier of the constellation that contains this object. The ID is the three-letter (all lowercase) abbreviation from the International Astronomical Union (IAU)."""
128
39
  if not self._constellation_id:
129
40
  pos = position_of_radec(self.ra, self.dec)
130
- self._constellation_id = constellation_at(pos).lower()
41
+ self._constellation_id = constellation_at()(pos).lower()
131
42
  return self._constellation_id
132
43
 
133
44
  def constellation(self):
@@ -135,32 +46,3 @@ class SkyObject(CreateMapMixin, CreateOpticMixin, metaclass=Meta):
135
46
  from starplot.models import Constellation
136
47
 
137
48
  return Constellation.get(iau_id=self.constellation_id)
138
-
139
-
140
- class SkyObjectManager(ABC):
141
- @abstractmethod
142
- def all(cls, *args, **kwargs):
143
- raise NotImplementedError
144
-
145
- @classmethod
146
- def find(cls, where, **kwargs):
147
- all_objects = kwargs.pop("all_objects", None) or cls.all()
148
- return [s for s in all_objects if all([e.evaluate(s) for e in where])]
149
-
150
- @classmethod
151
- def get(cls, **kwargs):
152
- all_objects = kwargs.pop("all_objects", None) or cls.all()
153
- matches = [
154
- s
155
- for s in all_objects
156
- if all([getattr(s, kw) == value for kw, value in kwargs.items()])
157
- ]
158
-
159
- if len(matches) == 1:
160
- return matches[0]
161
- elif len(matches) > 1:
162
- raise ValueError(
163
- "More than one match. Use find() instead or narrow your search."
164
- )
165
- else:
166
- return None
@@ -1,16 +1,10 @@
1
- from shapely import Polygon
2
-
3
- from starplot.models.base import SkyObject, SkyObjectManager
4
- from starplot.models.geometry import to_24h
5
- from starplot.data import constellations
1
+ from typing import Union, Iterator
6
2
 
3
+ from ibis import _
4
+ from shapely import Polygon, MultiPolygon
7
5
 
8
- class ConstellationManager(SkyObjectManager):
9
- @classmethod
10
- def all(cls):
11
- all_constellations = constellations.load()
12
- for constellation in all_constellations.itertuples():
13
- yield from_tuple(constellation)
6
+ from starplot.models.base import SkyObject
7
+ from starplot.data import constellations
14
8
 
15
9
 
16
10
  class Constellation(SkyObject):
@@ -18,16 +12,22 @@ class Constellation(SkyObject):
18
12
  Constellation model.
19
13
  """
20
14
 
21
- _manager = ConstellationManager
22
-
23
15
  iau_id: str = None
24
- """International Astronomical Union (IAU) three-letter designation, all lowercase"""
16
+ """
17
+ International Astronomical Union (IAU) three-letter designation, all lowercase.
18
+
19
+ **Important**: Starplot treats Serpens as two separate constellations to make them easier to work with programatically.
20
+ Serpens Caput has the `iau_id` of `ser1` and Serpens Cauda is `ser2`
21
+ """
25
22
 
26
23
  name: str = None
27
24
  """Name"""
28
25
 
29
- boundary: Polygon = None
30
- """Shapely Polygon of the constellation's boundary. Right ascension coordinates are in 24H format."""
26
+ star_hip_ids: list[int] = None
27
+ """List of HIP ids for stars that are part of the _lines_ for this constellation."""
28
+
29
+ boundary: Union[Polygon, MultiPolygon] = None
30
+ """Shapely Polygon of the constellation's boundary. Right ascension coordinates are in degrees (0...360)."""
31
31
 
32
32
  def __init__(
33
33
  self,
@@ -35,33 +35,59 @@ class Constellation(SkyObject):
35
35
  dec: float,
36
36
  iau_id: str,
37
37
  name: str = None,
38
+ star_hip_ids: list[int] = None,
38
39
  boundary: Polygon = None,
39
40
  ) -> None:
40
- super().__init__(ra, dec)
41
+ super().__init__(ra, dec, constellation_id=iau_id.lower())
41
42
  self.iau_id = iau_id.lower()
42
- self._constellation_id = self.iau_id # override from super()
43
43
  self.name = name
44
+ self.star_hip_ids = star_hip_ids
44
45
  self.boundary = boundary
45
46
 
46
47
  def __repr__(self) -> str:
47
48
  return f"Constellation(iau_id={self.iau_id}, name={self.name}, ra={self.ra}, dec={self.dec})"
48
49
 
49
50
  @classmethod
50
- def get(**kwargs) -> "Constellation":
51
+ def all(cls) -> Iterator["Constellation"]:
52
+ df = constellations.load().to_pandas()
53
+
54
+ for c in df.itertuples():
55
+ yield from_tuple(c)
56
+
57
+ @classmethod
58
+ def get(cls, **kwargs) -> "Constellation":
51
59
  """
52
60
  Get a Constellation, by matching its attributes.
53
61
 
54
- Example: `hercules = Constellation.get(name="Hercules")`
62
+ Example:
63
+
64
+ hercules = Constellation.get(name="Hercules")
55
65
 
56
66
  Args:
57
67
  **kwargs: Attributes on the constellation you want to match
58
68
 
59
69
  Raises: `ValueError` if more than one constellation is matched
60
70
  """
61
- pass
71
+ filters = []
72
+
73
+ for k, v in kwargs.items():
74
+ filters.append(getattr(_, k) == v)
75
+
76
+ df = constellations.load(filters=filters).to_pandas()
77
+ results = [from_tuple(c) for c in df.itertuples()]
78
+
79
+ if len(results) == 1:
80
+ return results[0]
81
+
82
+ if len(results) > 1:
83
+ raise ValueError(
84
+ "More than one match. Use find() instead or narrow your search."
85
+ )
86
+
87
+ return None
62
88
 
63
89
  @classmethod
64
- def find(where: list) -> list["Constellation"]:
90
+ def find(cls, where: list) -> list["Constellation"]:
65
91
  """
66
92
  Find Constellations
67
93
 
@@ -72,7 +98,9 @@ class Constellation(SkyObject):
72
98
  List of Constellations that match all `where` expressions
73
99
 
74
100
  """
75
- pass
101
+ df = constellations.load(filters=where).to_pandas()
102
+
103
+ return [from_tuple(c) for c in df.itertuples()]
76
104
 
77
105
  def constellation(self):
78
106
  """Not applicable to Constellation model, raises `NotImplementedError`"""
@@ -80,16 +108,11 @@ class Constellation(SkyObject):
80
108
 
81
109
 
82
110
  def from_tuple(c: tuple) -> Constellation:
83
- geometry = c.geometry
84
- if len(c.geometry.geoms) == 1:
85
- geometry = c.geometry.geoms[0]
86
-
87
- geometry = to_24h(geometry)
88
-
89
111
  return Constellation(
90
- ra=c.center_ra / 15,
91
- dec=c.center_dec,
112
+ ra=c.ra,
113
+ dec=c.dec,
92
114
  iau_id=c.iau_id,
93
115
  name=c.name,
94
- boundary=geometry,
116
+ star_hip_ids=c.star_hip_ids,
117
+ boundary=c.geometry,
95
118
  )