starplot 0.16.1__py2.py3-none-any.whl → 0.17.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.
- starplot/__init__.py +28 -2
- starplot/base.py +42 -60
- starplot/config.py +41 -9
- starplot/data/bigsky.py +1 -1
- starplot/data/constellations.py +9 -681
- starplot/data/db.py +2 -2
- starplot/data/dsos.py +11 -28
- starplot/data/library/bigsky.0.4.0.stars.mag11.parquet +0 -0
- starplot/data/library/sky.db +0 -0
- starplot/data/stars.py +15 -433
- starplot/data/translations.py +161 -0
- starplot/geometry.py +52 -6
- starplot/horizon.py +18 -12
- starplot/map.py +1 -5
- starplot/mixins.py +283 -0
- starplot/models/__init__.py +19 -7
- starplot/models/base.py +1 -1
- starplot/models/comet.py +332 -0
- starplot/models/constellation.py +10 -0
- starplot/models/dso.py +24 -0
- starplot/{optics.py → models/optics.py} +4 -4
- starplot/models/satellite.py +158 -0
- starplot/models/star.py +10 -0
- starplot/optic.py +24 -13
- starplot/plotters/__init__.py +1 -0
- starplot/plotters/arrow.py +162 -0
- starplot/plotters/constellations.py +34 -56
- starplot/plotters/dsos.py +8 -14
- starplot/plotters/experimental.py +560 -8
- starplot/plotters/legend.py +5 -0
- starplot/plotters/stars.py +7 -16
- starplot/styles/base.py +20 -1
- starplot/styles/extensions.py +10 -1
- starplot/zenith.py +4 -1
- {starplot-0.16.1.dist-info → starplot-0.17.0.dist-info}/METADATA +20 -17
- {starplot-0.16.1.dist-info → starplot-0.17.0.dist-info}/RECORD +40 -36
- /starplot/{observer.py → models/observer.py} +0 -0
- {starplot-0.16.1.dist-info → starplot-0.17.0.dist-info}/WHEEL +0 -0
- {starplot-0.16.1.dist-info → starplot-0.17.0.dist-info}/entry_points.txt +0 -0
- {starplot-0.16.1.dist-info → starplot-0.17.0.dist-info}/licenses/LICENSE +0 -0
starplot/models/base.py
CHANGED
|
@@ -83,7 +83,7 @@ class SkyObject(BaseModel, CreateMapMixin, CreateOpticMixin):
|
|
|
83
83
|
def constellation_id(self) -> str | None:
|
|
84
84
|
"""Identifier of the constellation that contains this object. The ID is the three-letter (all lowercase) abbreviation from the International Astronomical Union (IAU)."""
|
|
85
85
|
if not self._constellation_id:
|
|
86
|
-
pos = position_of_radec(self.ra, self.dec)
|
|
86
|
+
pos = position_of_radec(self.ra / 15, self.dec)
|
|
87
87
|
self._constellation_id = constellation_at()(pos).lower()
|
|
88
88
|
return self._constellation_id
|
|
89
89
|
|
starplot/models/comet.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
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.api import wgs84
|
|
7
|
+
from skyfield.data import mpc
|
|
8
|
+
from skyfield.constants import GM_SUN_Pitjeva_2005_km3_s2 as GM_SUN
|
|
9
|
+
|
|
10
|
+
from starplot.data import load
|
|
11
|
+
from starplot.models.base import SkyObject, ShapelyPoint
|
|
12
|
+
from starplot.utils import dt_or_now
|
|
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
|
+
class Comet(SkyObject):
|
|
131
|
+
"""
|
|
132
|
+
Comets can be created in three ways:
|
|
133
|
+
|
|
134
|
+
1. [`get`][starplot.Comet.get] (designation/name)
|
|
135
|
+
2. [`all`][starplot.Comet.all] (iterate through all comets available from MPC)
|
|
136
|
+
3. [`from_json`][starplot.Comet.from_json] (IAU MPC JSON)
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
name: str
|
|
141
|
+
"""
|
|
142
|
+
Name of the comet (as designated by IAU Minor Planet Center)
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
dt: datetime
|
|
146
|
+
"""Date/time of comet's position"""
|
|
147
|
+
|
|
148
|
+
lat: float | None = None
|
|
149
|
+
"""Latitude of observing location"""
|
|
150
|
+
|
|
151
|
+
lon: float | None = None
|
|
152
|
+
"""Longitude of observing location"""
|
|
153
|
+
|
|
154
|
+
distance: float | None = None
|
|
155
|
+
"""Distance to comet, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
|
|
156
|
+
|
|
157
|
+
ephemeris: str = None
|
|
158
|
+
"""Ephemeris used when retrieving this instance"""
|
|
159
|
+
|
|
160
|
+
geometry: ShapelyPoint = None
|
|
161
|
+
"""Shapely Point of the comet's position. Right ascension coordinates are in degrees (0...360)."""
|
|
162
|
+
|
|
163
|
+
data: SkyfieldComet = None
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_json(
|
|
167
|
+
cls,
|
|
168
|
+
data: dict,
|
|
169
|
+
dt: datetime = None,
|
|
170
|
+
lat: float = None,
|
|
171
|
+
lon: float = None,
|
|
172
|
+
ephemeris: str = "de421_2001.bsp",
|
|
173
|
+
) -> "Comet":
|
|
174
|
+
"""
|
|
175
|
+
Get a comet for a specific date/time/location from an IAU MPC JSON.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data: Dictionary of the IAU MPC JSON
|
|
179
|
+
dt: Datetime you want the comet for (must be timezone aware!). _Defaults to current UTC time_.
|
|
180
|
+
lat: Latitude of observing location. If you set this (and longitude), then the comet's _apparent_ RA/DEC will be calculated.
|
|
181
|
+
lon: Longitude of observing location
|
|
182
|
+
ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
183
|
+
"""
|
|
184
|
+
dt = dt_or_now(dt)
|
|
185
|
+
comet = SkyfieldComet.from_mpc_json(data)
|
|
186
|
+
|
|
187
|
+
return get_comet_at_date_location(comet, dt, lat, lon, ephemeris)
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def all(
|
|
191
|
+
cls,
|
|
192
|
+
dt: datetime = None,
|
|
193
|
+
lat: float = None,
|
|
194
|
+
lon: float = None,
|
|
195
|
+
ephemeris: str = "de421_2001.bsp",
|
|
196
|
+
reload: bool = False,
|
|
197
|
+
) -> Iterator["Comet"]:
|
|
198
|
+
"""
|
|
199
|
+
Iterator for getting all comets at a specific date/time and observing location.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
dt: Datetime you want the comets for (must be timezone aware!). _Defaults to current UTC time_.
|
|
203
|
+
lat: Latitude of observing location. If you set this (and longitude), then the comet's _apparent_ RA/DEC will be calculated.
|
|
204
|
+
lon: Longitude of observing location
|
|
205
|
+
ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
206
|
+
reload: If True, then the comet data file will be re-downloaded. Otherwise, it'll use the existing file if available.
|
|
207
|
+
"""
|
|
208
|
+
dt = dt_or_now(dt)
|
|
209
|
+
comets = get_comets(reload=reload)
|
|
210
|
+
for name in comets.index.values:
|
|
211
|
+
row = comets.loc[name]
|
|
212
|
+
comet = SkyfieldComet(**row.to_dict())
|
|
213
|
+
yield get_comet_at_date_location(
|
|
214
|
+
comet=comet,
|
|
215
|
+
dt=dt,
|
|
216
|
+
lat=lat,
|
|
217
|
+
lon=lon,
|
|
218
|
+
ephemeris=ephemeris,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def get(
|
|
223
|
+
cls,
|
|
224
|
+
name: str,
|
|
225
|
+
dt: datetime = None,
|
|
226
|
+
lat: float = None,
|
|
227
|
+
lon: float = None,
|
|
228
|
+
ephemeris: str = "de421_2001.bsp",
|
|
229
|
+
reload: bool = False,
|
|
230
|
+
) -> "Comet":
|
|
231
|
+
"""
|
|
232
|
+
Get a comet for a specific date/time.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: Name of the comet you want to get (as designated by IAU Minor Planet Center)
|
|
236
|
+
dt: Datetime you want the comet for (must be timezone aware!). _Defaults to current UTC time_.
|
|
237
|
+
lat: Latitude of observing location. If you set this (and longitude), then the comet's _apparent_ RA/DEC will be calculated.
|
|
238
|
+
lon: Longitude of observing location
|
|
239
|
+
ephemeris: Ephemeris to use for calculating comet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
240
|
+
reload: If True, then the comet data file will be re-downloaded. Otherwise, it'll use the existing file if available.
|
|
241
|
+
"""
|
|
242
|
+
dt = dt_or_now(dt)
|
|
243
|
+
comet = SkyfieldComet.from_mpc_remote(name, reload)
|
|
244
|
+
return get_comet_at_date_location(comet, dt, lat, lon, ephemeris)
|
|
245
|
+
|
|
246
|
+
def trajectory(
|
|
247
|
+
self, date_start: datetime, date_end: datetime, step: timedelta = None
|
|
248
|
+
) -> Iterator["Comet"]:
|
|
249
|
+
"""
|
|
250
|
+
Iterator for getting a trajectory of the comet.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
date_start: Starting date/time for the trajectory (inclusive)
|
|
254
|
+
date_end: End date/time for the trajectory (exclusive)
|
|
255
|
+
step: Time-step for the trajectory. Defaults to 1-day
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Iterator that yields a Comet instance at each step in the date range
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
step = step or timedelta(days=1)
|
|
262
|
+
dt = date_start
|
|
263
|
+
|
|
264
|
+
while dt < date_end:
|
|
265
|
+
yield get_comet_at_date_location(
|
|
266
|
+
comet=self.data,
|
|
267
|
+
dt=dt,
|
|
268
|
+
lat=self.lat,
|
|
269
|
+
lon=self.lon,
|
|
270
|
+
ephemeris=self.ephemeris,
|
|
271
|
+
)
|
|
272
|
+
dt += step
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@cache
|
|
276
|
+
def get_comets(reload=False):
|
|
277
|
+
"""
|
|
278
|
+
Gets ALL comets currently tracked by IAU Minor Planet Center.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
reload: If True, then redownload the comet data if it already exists
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
DataFrame of all comets, indexed by name
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
with load.open(mpc.COMET_URL, reload=reload) as f:
|
|
288
|
+
comets = mpc.load_comets_dataframe(f)
|
|
289
|
+
|
|
290
|
+
# Keep only the most recent orbit for each comet, and index by designation for fast lookup.
|
|
291
|
+
comets = (
|
|
292
|
+
comets.sort_values("reference")
|
|
293
|
+
.groupby("designation", as_index=False)
|
|
294
|
+
.last()
|
|
295
|
+
.set_index("designation", drop=False)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return comets
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_comet_at_date_location(
|
|
302
|
+
comet: SkyfieldComet, dt: datetime, lat: float, lon: float, ephemeris: str
|
|
303
|
+
) -> Comet:
|
|
304
|
+
"""
|
|
305
|
+
Creates a Comet instance for date and (optional) observing location.
|
|
306
|
+
"""
|
|
307
|
+
ts = load.timescale()
|
|
308
|
+
eph = load(ephemeris)
|
|
309
|
+
sun, earth = eph["sun"], eph["earth"]
|
|
310
|
+
c = sun + mpc.comet_orbit(comet, ts, GM_SUN)
|
|
311
|
+
t = ts.from_datetime(dt)
|
|
312
|
+
|
|
313
|
+
if lat is not None and lon is not None:
|
|
314
|
+
position = earth + wgs84.latlon(lat, lon)
|
|
315
|
+
astrometric = position.at(t).observe(c)
|
|
316
|
+
apparent = astrometric.apparent()
|
|
317
|
+
ra, dec, distance = apparent.radec()
|
|
318
|
+
else:
|
|
319
|
+
ra, dec, distance = earth.at(t).observe(c).radec()
|
|
320
|
+
|
|
321
|
+
return Comet(
|
|
322
|
+
name=comet.designation,
|
|
323
|
+
ra=ra.hours * 15,
|
|
324
|
+
dec=dec.degrees,
|
|
325
|
+
dt=dt,
|
|
326
|
+
lat=lat,
|
|
327
|
+
lon=lon,
|
|
328
|
+
distance=distance.au,
|
|
329
|
+
ephemeris=ephemeris,
|
|
330
|
+
geometry=ShapelyPoint(ra.hours * 15, dec.degrees),
|
|
331
|
+
data=comet,
|
|
332
|
+
)
|
starplot/models/constellation.py
CHANGED
|
@@ -92,6 +92,16 @@ class Constellation(SkyObject):
|
|
|
92
92
|
"""Not applicable to Constellation model, raises `NotImplementedError`"""
|
|
93
93
|
raise NotImplementedError()
|
|
94
94
|
|
|
95
|
+
@classmethod
|
|
96
|
+
def get_label(cls, constellation):
|
|
97
|
+
"""
|
|
98
|
+
Default function for determining the plotted label for a constellation
|
|
99
|
+
|
|
100
|
+
Returns the uppercase name of the constellation.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
return constellation.name.upper()
|
|
104
|
+
|
|
95
105
|
|
|
96
106
|
def from_tuple(c: tuple) -> Constellation:
|
|
97
107
|
c = Constellation(
|
starplot/models/dso.py
CHANGED
|
@@ -189,6 +189,30 @@ class DSO(SkyObject, CreateMapMixin, CreateOpticMixin):
|
|
|
189
189
|
df = load(filters=where, sql=sql).to_pandas()
|
|
190
190
|
return [from_tuple(d) for d in df.itertuples()]
|
|
191
191
|
|
|
192
|
+
@classmethod
|
|
193
|
+
def get_label(cls, dso) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Default function for determining the plotted label for a DSO.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
|
|
199
|
+
1. `"M13"` if DSO is a Messier object
|
|
200
|
+
2. `"6456"` if DSO is an NGC object
|
|
201
|
+
3. `"IC1920"` if DSO is an IC object
|
|
202
|
+
4. Empty string otherwise
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
if dso.m:
|
|
206
|
+
return f"M{dso.m}"
|
|
207
|
+
|
|
208
|
+
if dso.ngc:
|
|
209
|
+
return f"{dso.ngc}"
|
|
210
|
+
|
|
211
|
+
if dso.ic:
|
|
212
|
+
return f"IC{dso.ic}"
|
|
213
|
+
|
|
214
|
+
return ""
|
|
215
|
+
|
|
192
216
|
|
|
193
217
|
def from_tuple(d: tuple) -> DSO:
|
|
194
218
|
dso = DSO(
|
|
@@ -54,9 +54,9 @@ class Scope(Optic):
|
|
|
54
54
|
|
|
55
55
|
See subclasses of this optic for more specific use cases:
|
|
56
56
|
|
|
57
|
-
- [`Refractor`][starplot.
|
|
57
|
+
- [`Refractor`][starplot.models.Refractor] - automatically inverts the view (i.e. assumes a star diagonal is used)
|
|
58
58
|
|
|
59
|
-
- [`Reflector`][starplot.
|
|
59
|
+
- [`Reflector`][starplot.models.Reflector] - automatically rotates the view so it's upside-down
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
62
|
focal_length: Focal length (mm) of the telescope
|
|
@@ -126,7 +126,7 @@ class Refractor(Scope):
|
|
|
126
126
|
Warning:
|
|
127
127
|
This optic assumes a star diagonal is used, so it applies a transform that inverts the image.
|
|
128
128
|
|
|
129
|
-
If you don't want this transform applied, then use the generic [`Scope`][starplot.
|
|
129
|
+
If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
|
|
130
130
|
|
|
131
131
|
Args:
|
|
132
132
|
focal_length: Focal length (mm) of the telescope
|
|
@@ -152,7 +152,7 @@ class Reflector(Scope):
|
|
|
152
152
|
Warning:
|
|
153
153
|
This optic applies a transform that produces an "upside-down" image.
|
|
154
154
|
|
|
155
|
-
If you don't want this transform applied, then use the generic [`Scope`][starplot.
|
|
155
|
+
If you don't want this transform applied, then use the generic [`Scope`][starplot.models.Scope] optic instead.
|
|
156
156
|
|
|
157
157
|
Args:
|
|
158
158
|
focal_length: Focal length (mm) of the telescope
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Iterator
|
|
3
|
+
|
|
4
|
+
from skyfield.api import wgs84, EarthSatellite
|
|
5
|
+
from skyfield.timelib import Timescale
|
|
6
|
+
|
|
7
|
+
from starplot.data import load
|
|
8
|
+
from starplot.models.base import SkyObject, ShapelyPoint
|
|
9
|
+
from starplot.utils import dt_or_now
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Satellite(SkyObject):
|
|
13
|
+
"""
|
|
14
|
+
Satellites can be created in two ways:
|
|
15
|
+
|
|
16
|
+
1. [`from_tle`][starplot.Satellite.from_tle] (two-line element set)
|
|
17
|
+
2. [`from_json`][starplot.Satellite.from_json] (CelesTrak JSON)
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
"""
|
|
23
|
+
Name of the satellite
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
dt: datetime
|
|
27
|
+
"""Date/time of satellite's position"""
|
|
28
|
+
|
|
29
|
+
lat: float | None = None
|
|
30
|
+
"""Latitude of observing location"""
|
|
31
|
+
|
|
32
|
+
lon: float | None = None
|
|
33
|
+
"""Longitude of observing location"""
|
|
34
|
+
|
|
35
|
+
distance: float | None = None
|
|
36
|
+
"""Distance to satellite, in Astronomical units (the Earth-Sun distance of 149,597,870,700 m)"""
|
|
37
|
+
|
|
38
|
+
ephemeris: str = None
|
|
39
|
+
"""Ephemeris used when retrieving this instance"""
|
|
40
|
+
|
|
41
|
+
geometry: ShapelyPoint = None
|
|
42
|
+
"""Shapely Point of the satellite's position. Right ascension coordinates are in degrees (0...360)."""
|
|
43
|
+
|
|
44
|
+
_satellite: EarthSatellite
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_json(
|
|
48
|
+
cls,
|
|
49
|
+
data: dict,
|
|
50
|
+
dt: datetime = None,
|
|
51
|
+
lat: float = None,
|
|
52
|
+
lon: float = None,
|
|
53
|
+
ephemeris: str = "de421_2001.bsp",
|
|
54
|
+
) -> "Satellite":
|
|
55
|
+
"""
|
|
56
|
+
Get a satellite for a specific date/time/location from a CelesTrak JSON.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
data: Dictionary of the CelesTrak JSON
|
|
60
|
+
dt: Datetime you want the satellite for (must be timezone aware!). _Defaults to current UTC time_.
|
|
61
|
+
lat: Latitude of observing location. If you set this (and longitude), then the satellite's _apparent_ RA/DEC will be calculated.
|
|
62
|
+
lon: Longitude of observing location
|
|
63
|
+
ephemeris: Ephemeris to use for calculating satellite positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
64
|
+
"""
|
|
65
|
+
dt = dt_or_now(dt)
|
|
66
|
+
ts = load.timescale()
|
|
67
|
+
satellite = EarthSatellite.from_omm(ts, data)
|
|
68
|
+
|
|
69
|
+
return get_satellite_at_date_location(satellite, dt, lat, lon, ts)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_tle(
|
|
73
|
+
cls,
|
|
74
|
+
name: str,
|
|
75
|
+
line1: str,
|
|
76
|
+
line2: str,
|
|
77
|
+
dt: datetime = None,
|
|
78
|
+
lat: float = None,
|
|
79
|
+
lon: float = None,
|
|
80
|
+
ephemeris: str = "de421_2001.bsp",
|
|
81
|
+
) -> "Satellite":
|
|
82
|
+
"""
|
|
83
|
+
Get a satellite for a specific date/time/location from a two-line element set (TLE).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Name of the satellite
|
|
87
|
+
line1: Line 1 of the two-line element set (TLE)
|
|
88
|
+
line2: Line 2 of the two-line element set (TLE)
|
|
89
|
+
dt: Datetime you want the satellite for (must be timezone aware!). _Defaults to current UTC time_.
|
|
90
|
+
lat: Latitude of observing location. If you set this (and longitude), then the satellite's _apparent_ RA/DEC will be calculated.
|
|
91
|
+
lon: Longitude of observing location
|
|
92
|
+
ephemeris: Ephemeris to use for calculating satellite positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
|
|
93
|
+
"""
|
|
94
|
+
dt = dt_or_now(dt)
|
|
95
|
+
ts = load.timescale()
|
|
96
|
+
satellite = EarthSatellite(
|
|
97
|
+
line1,
|
|
98
|
+
line2,
|
|
99
|
+
name,
|
|
100
|
+
ts,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return get_satellite_at_date_location(satellite, dt, lat, lon, ts)
|
|
104
|
+
|
|
105
|
+
def trajectory(
|
|
106
|
+
self, date_start: datetime, date_end: datetime, step: timedelta = None
|
|
107
|
+
) -> Iterator["Satellite"]:
|
|
108
|
+
"""
|
|
109
|
+
Iterator for getting a trajectory of the satellite.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
date_start: Starting date/time for the trajectory (inclusive)
|
|
113
|
+
date_end: End date/time for the trajectory (exclusive)
|
|
114
|
+
step: Time-step for the trajectory. Defaults to 1-day
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Iterator that yields a Satellite instance at each step in the date range
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
step = step or timedelta(hours=1)
|
|
121
|
+
dt = date_start
|
|
122
|
+
ts = load.timescale()
|
|
123
|
+
|
|
124
|
+
while dt < date_end:
|
|
125
|
+
yield get_satellite_at_date_location(
|
|
126
|
+
self._satellite, dt, self.lat, self.lon, ts
|
|
127
|
+
)
|
|
128
|
+
dt += step
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_satellite_at_date_location(
|
|
132
|
+
satellite: EarthSatellite, dt: datetime, lat: float, lon: float, ts: Timescale
|
|
133
|
+
) -> Satellite:
|
|
134
|
+
t = ts.from_datetime(dt)
|
|
135
|
+
|
|
136
|
+
if lat is not None and lon is not None:
|
|
137
|
+
position = wgs84.latlon(lat, lon)
|
|
138
|
+
difference = satellite - position
|
|
139
|
+
topocentric = difference.at(t)
|
|
140
|
+
ra, dec, distance = topocentric.radec()
|
|
141
|
+
# alt, az, distance = topocentric.altaz()
|
|
142
|
+
# print(alt, az)
|
|
143
|
+
else:
|
|
144
|
+
ra, dec, distance = satellite.at(t).radec()
|
|
145
|
+
|
|
146
|
+
result = Satellite(
|
|
147
|
+
name=satellite.name,
|
|
148
|
+
ra=ra.hours * 15,
|
|
149
|
+
dec=dec.degrees,
|
|
150
|
+
dt=dt,
|
|
151
|
+
lat=lat,
|
|
152
|
+
lon=lon,
|
|
153
|
+
distance=distance.au,
|
|
154
|
+
ephemeris="na",
|
|
155
|
+
geometry=ShapelyPoint(ra.hours * 15, dec.degrees),
|
|
156
|
+
)
|
|
157
|
+
setattr(result, "_satellite", satellite)
|
|
158
|
+
return result
|
starplot/models/star.py
CHANGED
|
@@ -134,6 +134,16 @@ class Star(SkyObject):
|
|
|
134
134
|
|
|
135
135
|
return [from_tuple(s) for s in df.itertuples()]
|
|
136
136
|
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_label(cls, star) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Default function for determining the plotted label for a Star.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The star's name
|
|
144
|
+
"""
|
|
145
|
+
return star.name
|
|
146
|
+
|
|
137
147
|
|
|
138
148
|
def from_tuple(star: tuple) -> Star:
|
|
139
149
|
s = Star(
|