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.
- starplot/__init__.py +59 -0
- starplot/callables.py +176 -0
- starplot/cli.py +29 -0
- starplot/config.py +80 -0
- starplot/coordinates.py +7 -0
- starplot/data/__init__.py +22 -0
- starplot/data/catalogs.py +356 -0
- starplot/data/constellations.py +71 -0
- starplot/data/db.py +33 -0
- starplot/data/dsos.py +85 -0
- starplot/data/ecliptic.py +201 -0
- starplot/data/library/constellation_names.parquet +0 -0
- starplot/data/library/dso_names.parquet +0 -0
- starplot/data/library/readme.md +1 -0
- starplot/data/library/star_designations.parquet +0 -0
- starplot/data/stars.py +79 -0
- starplot/data/translations.py +408 -0
- starplot/data/utils.py +35 -0
- starplot/geometry.py +319 -0
- starplot/mixins.py +360 -0
- starplot/models/__init__.py +21 -0
- starplot/models/base.py +77 -0
- starplot/models/comet.py +302 -0
- starplot/models/constellation.py +151 -0
- starplot/models/dso.py +317 -0
- starplot/models/milky_way.py +30 -0
- starplot/models/moon.py +130 -0
- starplot/models/objects.py +29 -0
- starplot/models/observer.py +125 -0
- starplot/models/optics.py +342 -0
- starplot/models/planet.py +137 -0
- starplot/models/satellite.py +138 -0
- starplot/models/star.py +253 -0
- starplot/models/sun.py +62 -0
- starplot/plots/__init__.py +7 -0
- starplot/plots/base.py +1023 -0
- starplot/plots/galaxy.py +372 -0
- starplot/plots/horizon.py +543 -0
- starplot/plots/map.py +515 -0
- starplot/plots/optic.py +469 -0
- starplot/plots/zenith.py +217 -0
- starplot/plotters/__init__.py +9 -0
- starplot/plotters/arrow.py +174 -0
- starplot/plotters/constellations.py +298 -0
- starplot/plotters/debug.py +21 -0
- starplot/plotters/dsos.py +294 -0
- starplot/plotters/experimental.py +722 -0
- starplot/plotters/gradients.py +153 -0
- starplot/plotters/legend.py +253 -0
- starplot/plotters/milkyway.py +51 -0
- starplot/plotters/stars.py +319 -0
- starplot/plotters/text.py +802 -0
- starplot/profile.py +16 -0
- starplot/projections.py +184 -0
- starplot/styles/__init__.py +6 -0
- starplot/styles/base.py +1344 -0
- starplot/styles/ext/antique.yml +175 -0
- starplot/styles/ext/blue_dark.yml +163 -0
- starplot/styles/ext/blue_gold.yml +147 -0
- starplot/styles/ext/blue_light.yml +123 -0
- starplot/styles/ext/blue_medium.yml +142 -0
- starplot/styles/ext/blue_night.yml +185 -0
- starplot/styles/ext/cb_wong.yml +124 -0
- starplot/styles/ext/color_print.yml +111 -0
- starplot/styles/ext/gradient_presets.yml +158 -0
- starplot/styles/ext/grayscale.yml +94 -0
- starplot/styles/ext/grayscale_dark.yml +136 -0
- starplot/styles/ext/map.yml +12 -0
- starplot/styles/ext/nord.yml +158 -0
- starplot/styles/ext/optic.yml +20 -0
- starplot/styles/ext/publication.yml +8 -0
- starplot/styles/extensions.py +129 -0
- starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
- starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
- starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
- starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
- starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/helpers.py +93 -0
- starplot/styles/markers.py +308 -0
- starplot/utils.py +169 -0
- starplot/warnings.py +21 -0
- starplot-0.20.5.dist-info/METADATA +146 -0
- starplot-0.20.5.dist-info/RECORD +98 -0
- starplot-0.20.5.dist-info/WHEEL +4 -0
- starplot-0.20.5.dist-info/entry_points.txt +3 -0
- starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
import pyproj
|
|
6
|
+
from matplotlib import patches
|
|
7
|
+
from pydantic import BaseModel, computed_field
|
|
8
|
+
|
|
9
|
+
from starplot.utils import in_circle
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Optic(BaseModel, ABC):
|
|
13
|
+
"""Abstract class for defining Optics."""
|
|
14
|
+
|
|
15
|
+
def __str__(self):
|
|
16
|
+
return "Optic"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def xlim(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def ylim(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def label(self):
|
|
31
|
+
return "Abstract Optic"
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def patch(self, center_x, center_y) -> patches.Patch:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def transform(self, axis) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def in_bounds(self, x, y, scale: float = 1) -> bool:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def _compute_radius(self, radius_degrees: float, x: float = 0, y: float = 0):
|
|
45
|
+
geod = pyproj.Geod("+a=6378137 +f=0.0", sphere=True)
|
|
46
|
+
_, _, distance = geod.inv(x, y, x + radius_degrees, y)
|
|
47
|
+
return distance
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Scope(Optic):
|
|
51
|
+
"""Creates a new generic Scope optic.
|
|
52
|
+
|
|
53
|
+
Use this class to create custom scope optics or use it as a generic optic that does NOT apply any transforms to the view.
|
|
54
|
+
|
|
55
|
+
See subclasses of this optic for more specific use cases:
|
|
56
|
+
|
|
57
|
+
- [`Refractor`][starplot.models.Refractor] - automatically inverts the view (i.e. assumes a star diagonal is used)
|
|
58
|
+
|
|
59
|
+
- [`Reflector`][starplot.models.Reflector] - automatically rotates the view so it's upside-down
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
focal_length: Focal length (mm) of the telescope
|
|
63
|
+
eyepiece_focal_length: Focal length (mm) of the eyepiece
|
|
64
|
+
eyepiece_fov: Field of view (degrees) of the eyepiece
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Scope: A new instance of a Scope optic
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
focal_length: float
|
|
71
|
+
"""Focal length (mm) of the telescope"""
|
|
72
|
+
|
|
73
|
+
eyepiece_focal_length: float
|
|
74
|
+
"""Focal length (mm) of the eyepiece"""
|
|
75
|
+
|
|
76
|
+
eyepiece_fov: float
|
|
77
|
+
"""Field of view (degrees) of the eyepiece"""
|
|
78
|
+
|
|
79
|
+
@computed_field
|
|
80
|
+
@property
|
|
81
|
+
def magnification(self) -> float:
|
|
82
|
+
"""Magnification calculated from the telescope's focal length and eyepiece focal length"""
|
|
83
|
+
return self.focal_length / self.eyepiece_focal_length
|
|
84
|
+
|
|
85
|
+
@computed_field
|
|
86
|
+
@property
|
|
87
|
+
def true_fov(self) -> float:
|
|
88
|
+
"""True field of view of telescope"""
|
|
89
|
+
return self.eyepiece_fov / self.magnification
|
|
90
|
+
|
|
91
|
+
@computed_field
|
|
92
|
+
@property
|
|
93
|
+
def radius(self) -> float:
|
|
94
|
+
return self._compute_radius(self.true_fov / 2)
|
|
95
|
+
|
|
96
|
+
def __str__(self):
|
|
97
|
+
return f"{self.focal_length:.0f}mm w/ {self.eyepiece_focal_length:.0f}mm ({self.magnification:.0f}x) @ {self.eyepiece_fov:.0f}\N{DEGREE SIGN} = {self.true_fov:.2f}\N{DEGREE SIGN} TFOV"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def xlim(self):
|
|
101
|
+
return self.radius
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def ylim(self):
|
|
105
|
+
return self.radius
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def label(self):
|
|
109
|
+
return "Scope"
|
|
110
|
+
|
|
111
|
+
def patch(self, center_x, center_y, **kwargs):
|
|
112
|
+
padding = kwargs.pop("padding", 0)
|
|
113
|
+
return patches.Circle(
|
|
114
|
+
(center_x, center_y),
|
|
115
|
+
radius=self.radius + padding,
|
|
116
|
+
**kwargs,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def in_bounds(self, x, y, scale: float = 1) -> bool:
|
|
120
|
+
return in_circle(x, y, 0, 0, self.radius * scale)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Refractor(Scope):
|
|
124
|
+
"""Creates a new Refractor Telescope optic
|
|
125
|
+
|
|
126
|
+
Warning:
|
|
127
|
+
This optic assumes a star diagonal is used, so it applies a transform that inverts the image.
|
|
128
|
+
|
|
129
|
+
If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
focal_length: Focal length (mm) of the telescope
|
|
133
|
+
eyepiece_focal_length: Focal length (mm) of the eyepiece
|
|
134
|
+
eyepiece_fov: Field of view (degrees) of the eyepiece
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Refractor: A new instance of a Refractor optic
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def label(self):
|
|
143
|
+
return "Refractor"
|
|
144
|
+
|
|
145
|
+
def transform(self, axis) -> None:
|
|
146
|
+
axis.invert_xaxis()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Reflector(Scope):
|
|
150
|
+
"""Creates a new Reflector Telescope optic
|
|
151
|
+
|
|
152
|
+
Warning:
|
|
153
|
+
This optic applies a transform that produces an "upside-down" image.
|
|
154
|
+
|
|
155
|
+
If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
focal_length: Focal length (mm) of the telescope
|
|
159
|
+
eyepiece_focal_length: Focal length (mm) of the eyepiece
|
|
160
|
+
eyepiece_fov: Field of view (degrees) of the eyepiece
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Reflector: A new instance of a Reflector optic
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def label(self):
|
|
169
|
+
return "Reflector"
|
|
170
|
+
|
|
171
|
+
def transform(self, axis) -> None:
|
|
172
|
+
axis.invert_xaxis()
|
|
173
|
+
axis.invert_yaxis()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Binoculars(Optic):
|
|
177
|
+
"""Creates a new Binoculars optic
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
magnification: Magnification of the binoculars
|
|
181
|
+
fov: Apparent field of view (FOV) of the binoculars in degrees. This isn't always easy to find for binoculars, so if you can't find it in your binocular's specs, then try using `60`.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Binoculars: A new instance of a Binoculars optic
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
magnification: float
|
|
189
|
+
"""Magnification of the binoculars"""
|
|
190
|
+
|
|
191
|
+
fov: float
|
|
192
|
+
"""Apparent field of view of the binoculars"""
|
|
193
|
+
|
|
194
|
+
@computed_field
|
|
195
|
+
@property
|
|
196
|
+
def true_fov(self) -> float:
|
|
197
|
+
"""True field of view of binoculars"""
|
|
198
|
+
return self.fov / self.magnification
|
|
199
|
+
|
|
200
|
+
@computed_field
|
|
201
|
+
@property
|
|
202
|
+
def radius(self) -> float:
|
|
203
|
+
return self._compute_radius(self.true_fov / 2)
|
|
204
|
+
|
|
205
|
+
def __str__(self):
|
|
206
|
+
return f"{self.magnification:.0f}x @ {self.fov:.0f}\N{DEGREE SIGN} = {self.true_fov}\N{DEGREE SIGN}"
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def xlim(self):
|
|
210
|
+
return self.radius
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def ylim(self):
|
|
214
|
+
return self.radius
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def label(self):
|
|
218
|
+
return "Binoculars"
|
|
219
|
+
|
|
220
|
+
def patch(self, center_x, center_y, **kwargs):
|
|
221
|
+
padding = kwargs.pop("padding", 0)
|
|
222
|
+
return patches.Circle(
|
|
223
|
+
(center_x, center_y),
|
|
224
|
+
radius=self.radius + padding,
|
|
225
|
+
**kwargs,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def in_bounds(self, x, y, scale: float = 1) -> bool:
|
|
229
|
+
return in_circle(x, y, 0, 0, self.radius * scale)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class Camera(Optic):
|
|
233
|
+
"""Creates a new Camera optic
|
|
234
|
+
|
|
235
|
+
Note:
|
|
236
|
+
Field of view for each dimension is calculated using the following formula:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
TFOV = 2 * arctan( d / (2 * f) )
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
_Where_:
|
|
243
|
+
|
|
244
|
+
d = sensor size (height or width)
|
|
245
|
+
|
|
246
|
+
f = focal length of lens
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
sensor_height: Height of camera sensor (mm)
|
|
250
|
+
sensor_width: Width of camera sensor (mm)
|
|
251
|
+
lens_focal_length: Focal length of camera lens (mm)
|
|
252
|
+
rotation: Angle (degrees) to rotate camera, defaults to 0
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Camera: A new instance of a Camera optic
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
sensor_height: float
|
|
260
|
+
"""Height (mm) of camera's sensor"""
|
|
261
|
+
|
|
262
|
+
sensor_width: float
|
|
263
|
+
"""Width (mm) of camera's sensor"""
|
|
264
|
+
|
|
265
|
+
lens_focal_length: float
|
|
266
|
+
"""Focal length (mm) of the camera's lens"""
|
|
267
|
+
|
|
268
|
+
rotation: float = 0
|
|
269
|
+
"""Angle (degrees) to rotate the camera"""
|
|
270
|
+
|
|
271
|
+
@computed_field
|
|
272
|
+
@property
|
|
273
|
+
def true_fov_x(self) -> float:
|
|
274
|
+
return 2 * math.degrees(
|
|
275
|
+
math.atan(self.sensor_width / (2 * self.lens_focal_length))
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
@computed_field
|
|
279
|
+
@property
|
|
280
|
+
def true_fov_y(self) -> float:
|
|
281
|
+
return 2 * math.degrees(
|
|
282
|
+
math.atan(self.sensor_height / (2 * self.lens_focal_length))
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
@computed_field
|
|
286
|
+
@property
|
|
287
|
+
def true_fov(self) -> float:
|
|
288
|
+
return max(self.true_fov_x, self.true_fov_y)
|
|
289
|
+
|
|
290
|
+
@computed_field
|
|
291
|
+
@property
|
|
292
|
+
def radius_x(self) -> float:
|
|
293
|
+
return self._compute_radius(self.true_fov_x / 2)
|
|
294
|
+
|
|
295
|
+
@computed_field
|
|
296
|
+
@property
|
|
297
|
+
def radius_y(self) -> float:
|
|
298
|
+
return self._compute_radius(self.true_fov_y / 2)
|
|
299
|
+
|
|
300
|
+
def __str__(self):
|
|
301
|
+
return f"{self.sensor_width}x{self.sensor_height} w/ {self.lens_focal_length:.0f}mm lens = {self.true_fov_x:.2f}\N{DEGREE SIGN} x {self.true_fov_y:.2f}\N{DEGREE SIGN}"
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def xlim(self):
|
|
305
|
+
x_offset = self.radius_x * self.rotation / 180
|
|
306
|
+
if self.rotation:
|
|
307
|
+
x_offset *= 1.1
|
|
308
|
+
return self.radius_x + x_offset
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def ylim(self):
|
|
312
|
+
y_offset = self.radius_y * math.sin(math.radians(self.rotation))
|
|
313
|
+
if self.rotation:
|
|
314
|
+
y_offset *= 1.2
|
|
315
|
+
return self.radius_y + y_offset
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def label(self):
|
|
319
|
+
return "Camera"
|
|
320
|
+
|
|
321
|
+
def patch(self, center_x, center_y, **kwargs):
|
|
322
|
+
padding = kwargs.pop("padding", 0)
|
|
323
|
+
x = center_x - self.radius_x - padding
|
|
324
|
+
y = center_y - self.radius_y - padding
|
|
325
|
+
return patches.Rectangle(
|
|
326
|
+
(x, y),
|
|
327
|
+
self.radius_x * 2 + padding,
|
|
328
|
+
self.radius_y * 2 + padding,
|
|
329
|
+
angle=self.rotation,
|
|
330
|
+
rotation_point="center",
|
|
331
|
+
**kwargs,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def in_bounds(self, x, y, scale: float = 1) -> bool:
|
|
335
|
+
radians = math.radians(180 - self.rotation)
|
|
336
|
+
|
|
337
|
+
px = x * math.cos(radians) - y * math.sin(radians)
|
|
338
|
+
py = x * math.sin(radians) + y * math.cos(radians)
|
|
339
|
+
|
|
340
|
+
in_bounds_x = px < self.radius_x * scale and px > -1 * self.radius_x * scale
|
|
341
|
+
in_bounds_y = py < self.radius_y * scale and py > -1 * self.radius_y * scale
|
|
342
|
+
return in_bounds_x and in_bounds_y
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Iterator
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from shapely import Polygon
|
|
9
|
+
from skyfield.api import Angle
|
|
10
|
+
|
|
11
|
+
from starplot.data import load
|
|
12
|
+
from starplot.models.base import SkyObject
|
|
13
|
+
from starplot.models.observer import Observer
|
|
14
|
+
from starplot.geometry import circle
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlanetName(str, Enum):
|
|
18
|
+
"""Planet names"""
|
|
19
|
+
|
|
20
|
+
MERCURY = "mercury"
|
|
21
|
+
VENUS = "venus"
|
|
22
|
+
MARS = "mars"
|
|
23
|
+
JUPITER = "jupiter"
|
|
24
|
+
SATURN = "saturn"
|
|
25
|
+
URANUS = "uranus"
|
|
26
|
+
NEPTUNE = "neptune"
|
|
27
|
+
PLUTO = "pluto"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
PLANET_LABELS_DEFAULT = {p: p.value.upper() for p in PlanetName}
|
|
31
|
+
|
|
32
|
+
PLANET_RADIUS_KM = {
|
|
33
|
+
PlanetName.MERCURY: 2_440,
|
|
34
|
+
PlanetName.VENUS: 6_052,
|
|
35
|
+
PlanetName.MARS: 3_390,
|
|
36
|
+
PlanetName.JUPITER: 69_911,
|
|
37
|
+
PlanetName.SATURN: 58_232,
|
|
38
|
+
PlanetName.URANUS: 25_362,
|
|
39
|
+
PlanetName.NEPTUNE: 24_622,
|
|
40
|
+
PlanetName.PLUTO: 1_151,
|
|
41
|
+
}
|
|
42
|
+
"""
|
|
43
|
+
Planet radii in kilometers, via NASA:
|
|
44
|
+
|
|
45
|
+
- https://science.nasa.gov/resource/solar-system-sizes/
|
|
46
|
+
- https://science.nasa.gov/dwarf-planets/pluto/facts/
|
|
47
|
+
|
|
48
|
+
Retrieved on 18-APR-2024
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True, kw_only=True)
|
|
53
|
+
class Planet(SkyObject):
|
|
54
|
+
"""Planet model."""
|
|
55
|
+
|
|
56
|
+
name: str = None
|
|
57
|
+
"""
|
|
58
|
+
Name of the planet:
|
|
59
|
+
|
|
60
|
+
- Mercury
|
|
61
|
+
- Venus
|
|
62
|
+
- Mars
|
|
63
|
+
- Jupiter
|
|
64
|
+
- Saturn
|
|
65
|
+
- Uranus
|
|
66
|
+
- Neptune
|
|
67
|
+
- Pluto
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
dt: datetime = None
|
|
72
|
+
"""Date/time of planet's position"""
|
|
73
|
+
|
|
74
|
+
apparent_size: float = 0
|
|
75
|
+
"""Apparent diameter (degrees)"""
|
|
76
|
+
|
|
77
|
+
geometry: Polygon = None
|
|
78
|
+
"""Shapely Polygon of the planet's extent. Right ascension coordinates are in degrees (0...360)."""
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def all(
|
|
82
|
+
cls,
|
|
83
|
+
observer: Observer = None,
|
|
84
|
+
ephemeris: str = "de421.bsp",
|
|
85
|
+
) -> Iterator["Planet"]:
|
|
86
|
+
"""
|
|
87
|
+
Iterator for getting all planets at a specific date/time and observing location.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
observer: Observer instance that specifies a time and location
|
|
91
|
+
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
observer = observer or Observer(lat=None, lon=None)
|
|
95
|
+
eph = load(ephemeris)
|
|
96
|
+
|
|
97
|
+
for p in PlanetName:
|
|
98
|
+
planet = eph[f"{p.value} barycenter"]
|
|
99
|
+
ra, dec, distance = observer._astrometric(planet, ephemeris=ephemeris)
|
|
100
|
+
|
|
101
|
+
# angular diameter:
|
|
102
|
+
# https://rhodesmill.org/skyfield/examples.html#what-is-the-angular-diameter-of-a-planet-given-its-radius
|
|
103
|
+
apparent_diameter_degrees = Angle(
|
|
104
|
+
radians=np.arcsin(PLANET_RADIUS_KM[p] / distance.km) * 2.0
|
|
105
|
+
).degrees
|
|
106
|
+
|
|
107
|
+
yield Planet(
|
|
108
|
+
ra=ra.hours * 15,
|
|
109
|
+
dec=dec.degrees,
|
|
110
|
+
name=p,
|
|
111
|
+
dt=observer.dt,
|
|
112
|
+
apparent_size=apparent_diameter_degrees,
|
|
113
|
+
geometry=circle(
|
|
114
|
+
(ra.hours * 15, dec.degrees), apparent_diameter_degrees
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get(
|
|
120
|
+
cls,
|
|
121
|
+
name: str,
|
|
122
|
+
observer: Observer = None,
|
|
123
|
+
ephemeris: str = "de421.bsp",
|
|
124
|
+
) -> "Planet":
|
|
125
|
+
"""
|
|
126
|
+
Get a planet for a specific date/time.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Name of the planet you want to get (see [`Planet.name`][starplot.Planet.name] for options). Case insensitive.
|
|
130
|
+
observer: Observer instance that specifies a time and location
|
|
131
|
+
ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
132
|
+
"""
|
|
133
|
+
for p in cls.all(observer, ephemeris):
|
|
134
|
+
if p.name.lower() == name.lower():
|
|
135
|
+
return p
|
|
136
|
+
|
|
137
|
+
return None
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Iterator
|
|
4
|
+
|
|
5
|
+
from shapely import Point
|
|
6
|
+
from skyfield.api import wgs84, EarthSatellite
|
|
7
|
+
|
|
8
|
+
from starplot.data import load
|
|
9
|
+
from starplot.models.base import SkyObject
|
|
10
|
+
from starplot.models.observer import Observer
|
|
11
|
+
|
|
12
|
+
ts = load.timescale()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True, kw_only=True)
|
|
16
|
+
class Satellite(SkyObject):
|
|
17
|
+
"""
|
|
18
|
+
Satellites can be created in two ways:
|
|
19
|
+
|
|
20
|
+
1. [`from_tle`][starplot.Satellite.from_tle] (two-line element set)
|
|
21
|
+
2. [`from_json`][starplot.Satellite.from_json] (CelesTrak JSON)
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name: str = None
|
|
26
|
+
"""Name of the satellite"""
|
|
27
|
+
|
|
28
|
+
observer: Observer = None
|
|
29
|
+
"""Observer of this satellite instance"""
|
|
30
|
+
|
|
31
|
+
distance: float | None = None
|
|
32
|
+
"""Distance to satellite, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
|
|
33
|
+
|
|
34
|
+
geometry: Point = None
|
|
35
|
+
"""Shapely Point of the satellite's position. Right ascension coordinates are in degrees (0...360)."""
|
|
36
|
+
|
|
37
|
+
_satellite: EarthSatellite = None
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_json(
|
|
41
|
+
cls,
|
|
42
|
+
data: dict,
|
|
43
|
+
observer: Observer = None,
|
|
44
|
+
) -> "Satellite":
|
|
45
|
+
"""
|
|
46
|
+
Get a satellite for a specific date/time/location from a CelesTrak JSON.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
data: Dictionary of the CelesTrak JSON
|
|
50
|
+
observer: Observer instance that specifies a time and location
|
|
51
|
+
"""
|
|
52
|
+
observer = observer or Observer()
|
|
53
|
+
return get_satellite_at_date_location(
|
|
54
|
+
satellite=EarthSatellite.from_omm(ts, data),
|
|
55
|
+
observer=observer,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_tle(
|
|
60
|
+
cls,
|
|
61
|
+
name: str,
|
|
62
|
+
line1: str,
|
|
63
|
+
line2: str,
|
|
64
|
+
observer: Observer = None,
|
|
65
|
+
) -> "Satellite":
|
|
66
|
+
"""
|
|
67
|
+
Get a satellite for a specific date/time/location from a two-line element set (TLE).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: Name of the satellite
|
|
71
|
+
line1: Line 1 of the two-line element set (TLE)
|
|
72
|
+
line2: Line 2 of the two-line element set (TLE)
|
|
73
|
+
observer: Observer instance that specifies a time and location
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
observer = observer or Observer()
|
|
77
|
+
return get_satellite_at_date_location(
|
|
78
|
+
satellite=EarthSatellite(
|
|
79
|
+
line1,
|
|
80
|
+
line2,
|
|
81
|
+
name,
|
|
82
|
+
ts,
|
|
83
|
+
),
|
|
84
|
+
observer=observer,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def trajectory(
|
|
88
|
+
self, date_start: datetime, date_end: datetime, step: timedelta = None
|
|
89
|
+
) -> Iterator["Satellite"]:
|
|
90
|
+
"""
|
|
91
|
+
Iterator for getting a trajectory of the satellite.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
date_start: Starting date/time for the trajectory (inclusive)
|
|
95
|
+
date_end: End date/time for the trajectory (exclusive)
|
|
96
|
+
step: Time-step for the trajectory. Defaults to 1-day
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Iterator that yields a Satellite instance at each step in the date range
|
|
100
|
+
"""
|
|
101
|
+
dt = date_start
|
|
102
|
+
step = step or timedelta(hours=1)
|
|
103
|
+
|
|
104
|
+
while dt < date_end:
|
|
105
|
+
observer_kwargs = self.observer.model_dump()
|
|
106
|
+
observer_kwargs["dt"] = dt
|
|
107
|
+
new_observer = Observer(**observer_kwargs)
|
|
108
|
+
yield get_satellite_at_date_location(
|
|
109
|
+
satellite=self._satellite, observer=new_observer
|
|
110
|
+
)
|
|
111
|
+
dt += step
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_satellite_at_date_location(
|
|
115
|
+
satellite: EarthSatellite, observer: Observer
|
|
116
|
+
) -> Satellite:
|
|
117
|
+
t = observer.timescale
|
|
118
|
+
lat = observer.lat
|
|
119
|
+
lon = observer.lon
|
|
120
|
+
|
|
121
|
+
if lat is not None and lon is not None:
|
|
122
|
+
position = wgs84.latlon(lat, lon, observer.elevation)
|
|
123
|
+
difference = satellite - position
|
|
124
|
+
topocentric = difference.at(t)
|
|
125
|
+
ra, dec, distance = topocentric.radec()
|
|
126
|
+
else:
|
|
127
|
+
ra, dec, distance = satellite.at(t).radec()
|
|
128
|
+
|
|
129
|
+
result = Satellite(
|
|
130
|
+
name=satellite.name,
|
|
131
|
+
ra=ra.hours * 15,
|
|
132
|
+
dec=dec.degrees,
|
|
133
|
+
observer=observer,
|
|
134
|
+
distance=distance.au,
|
|
135
|
+
geometry=Point(ra.hours * 15, dec.degrees),
|
|
136
|
+
)
|
|
137
|
+
setattr(result, "_satellite", satellite)
|
|
138
|
+
return result
|