pylunar 0.7.1__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.
pylunar/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ # This file is part of pylunar.
2
+ #
3
+ # Developed by Michael Reuter.
4
+ #
5
+ # See the LICENSE file at the top-level directory of this distribution
6
+ # for details of code ownership.
7
+ #
8
+ # Use of this source code is governed by a 3-clause BSD-style
9
+ # license that can be found in the LICENSE file.
10
+
11
+ __all__ = [
12
+ "__author__",
13
+ "__email__",
14
+ "__version__",
15
+ "LunarFeature",
16
+ "LunarFeatureContainer",
17
+ "mjd_to_date_tuple",
18
+ "MoonInfo",
19
+ "tuple_to_string",
20
+ "version_info",
21
+ ]
22
+
23
+ from importlib.metadata import PackageNotFoundError, version
24
+
25
+ __author__ = "Michael Reuter"
26
+ __email__ = "mareuternh@gmail.com"
27
+ try:
28
+ __version__ = version("pylunar")
29
+ except PackageNotFoundError:
30
+ # package is not installed
31
+ __version__ = "0.0.0"
32
+
33
+ version_info = __version__.split(".")
34
+ """The decomposed version, split across "``.``."
35
+
36
+ Use this for version comparison.
37
+ """
38
+
39
+ from .helpers import mjd_to_date_tuple, tuple_to_string
40
+ from .lunar_feature import LunarFeature
41
+ from .lunar_feature_container import LunarFeatureContainer
42
+ from .moon_info import MoonInfo
pylunar/data/lunar.db ADDED
Binary file
pylunar/helpers.py ADDED
@@ -0,0 +1,55 @@
1
+ # This file is part of pylunar.
2
+ #
3
+ # Developed by Michael Reuter.
4
+ #
5
+ # See the LICENSE file at the top-level directory of this distribution
6
+ # for details of code ownership.
7
+ #
8
+ # Use of this source code is governed by a 3-clause BSD-style
9
+ # license that can be found in the LICENSE file.
10
+
11
+ """Module for helper functions."""
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = ["mjd_to_date_tuple", "tuple_to_string"]
16
+
17
+ import ephem
18
+
19
+ from .types import DateTimeTuple, DmsCoordinate
20
+
21
+
22
+ def mjd_to_date_tuple(mjd: float, round_off: bool = False) -> DateTimeTuple:
23
+ """Convert a Modified Julian date to a UTC time tuple.
24
+
25
+ Parameters
26
+ ----------
27
+ mjd : float
28
+ The Modified Julian Date to convert.
29
+ round_off : bool, optional
30
+ Flag to round the seconds.
31
+
32
+ Returns
33
+ -------
34
+ tuple
35
+ The UTC time for the MJD.
36
+ """
37
+ date_tuple: DateTimeTuple
38
+ date_tuple = tuple(int(x) for x in ephem.Date(mjd).tuple()) if round_off else ephem.Date(mjd).tuple()
39
+ return date_tuple
40
+
41
+
42
+ def tuple_to_string(coord: DmsCoordinate) -> str:
43
+ """Return a colon-delimited string.
44
+
45
+ Parameters
46
+ ----------
47
+ coord : tuple of 3 ints
48
+ The coordinate to transform.
49
+
50
+ Returns
51
+ -------
52
+ str
53
+ The colon-delimited coordinate string.
54
+ """
55
+ return ":".join([str(x) for x in coord])
@@ -0,0 +1,179 @@
1
+ # This file is part of pylunar.
2
+ #
3
+ # Developed by Michael Reuter.
4
+ #
5
+ # See the LICENSE file at the top-level directory of this distribution
6
+ # for details of code ownership.
7
+ #
8
+ # Use of this source code is governed by a 3-clause BSD-style
9
+ # license that can be found in the LICENSE file.
10
+
11
+ """Module for the LunarFeature class."""
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = ["LunarFeature"]
16
+
17
+ import math
18
+ import os
19
+
20
+ from .types import FeatureRow, Range
21
+
22
+
23
+ class LunarFeature:
24
+ """Class keeping all the information for a given Lunar feature.
25
+
26
+ Parameters
27
+ ----------
28
+ name : str
29
+ The name of the Lunar feature (no unicode).
30
+ diameter : float
31
+ The diameter (km) of the Lunar feature.
32
+ latitude : float
33
+ The latitude (degrees) of the Lunar feature. Negative is South,
34
+ positive is North.
35
+ longitude : float
36
+ The longitude (degrees) of the Lunar feature. Negative is West,
37
+ positive is East.
38
+ delta_latitude : float
39
+ The size (degrees) in latitude of the Lunar feature.
40
+ delta_longitude : float
41
+ The size (degrees) in longitude of the Lunar feature.
42
+ feature_type : str
43
+ The classification of the Lunar feature: i.e. Crater, Mons.
44
+ quad_name : str
45
+ Name of lunar quadrant containing feature's center point as
46
+ determined by the International Astronomical Union (IAU) Working
47
+ Group for Planetary System Nomenclature (WGPSN).
48
+ quad_code : str
49
+ Specific lunar quadrant containing feature's center point as
50
+ determined by the IAU WGPSN.
51
+ code_name : str
52
+ The AstroLeague club name for the Lunar feature. Can be: Lunar,
53
+ LunarII or Both.
54
+ lunar_club_type : str or None
55
+ The Lunar Club classification of the feature: Naked Eye, Binocular,
56
+ Telescope. For a LunarII only feature this is None.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ name: str,
62
+ diameter: float,
63
+ latitude: float,
64
+ longitude: float,
65
+ delta_latitude: float,
66
+ delta_longitude: float,
67
+ feature_type: str,
68
+ quad_name: str,
69
+ quad_code: str,
70
+ code_name: str,
71
+ lunar_club_type: str | None,
72
+ ):
73
+ self.name = name
74
+ self.diameter = diameter
75
+ self.latitude = latitude
76
+ self.longitude = longitude
77
+ self.delta_latitude = delta_latitude
78
+ self.delta_longitude = delta_longitude
79
+ self.feature_type = feature_type
80
+ self.quad_name = quad_name
81
+ self.quad_code = quad_code
82
+ self.code_name = code_name
83
+ self.lunar_club_type = str(lunar_club_type)
84
+
85
+ def __str__(self) -> str:
86
+ """Class string representation.
87
+
88
+ Returns
89
+ -------
90
+ str
91
+ The string representation.
92
+ """
93
+ result = []
94
+ result.append(f"Name = {self.name}")
95
+ result.append(f"Lat/Long = ({self.latitude:.2f}, {self.longitude:.2f})")
96
+ result.append(f"Type = {self.feature_type}")
97
+ result.append(f"Delta Lat/Long = ({self.delta_latitude:.2f}, {self.delta_longitude:.2f})")
98
+ return os.linesep.join(result)
99
+
100
+ @classmethod
101
+ def from_row(cls: type[LunarFeature], row: FeatureRow) -> LunarFeature:
102
+ """Initialize from a database row.
103
+
104
+ Parameters
105
+ ----------
106
+ row : list
107
+ The database row containing the information.
108
+
109
+ Returns
110
+ -------
111
+ :class:`.LunarFeature`
112
+ Class initialized from database row.
113
+ """
114
+ return cls(*row[1:])
115
+
116
+ def feature_angle(self) -> float:
117
+ """Get the angle of the feature on the lunar face relative to North.
118
+
119
+ The feature angle is determined by calculating atan2(lon, lat) and
120
+ then adding 360 degrees if the result is less than zero. This makes
121
+ North zero degrees, East 90 degrees, South 180 degrees and West 270
122
+ degrees.
123
+
124
+ Returns
125
+ -------
126
+ float
127
+ The feature angle in degrees.
128
+ """
129
+ lat_rad = math.radians(self.latitude)
130
+ lon_rad = math.radians(self.longitude)
131
+ fa = math.degrees(math.atan2(lon_rad, lat_rad))
132
+ fa += 360.0 if fa < 0 else 0.0
133
+ return fa
134
+
135
+ def latitude_range(self) -> Range:
136
+ """Get the latitude range of the feature.
137
+
138
+ Returns
139
+ -------
140
+ tuple(float, float)
141
+ The (minimum, maximum) latitude values for the feature.
142
+ """
143
+ min_lat = self.latitude - (self.delta_latitude / 2.0)
144
+ max_lat = self.latitude + (self.delta_latitude / 2.0)
145
+ return (min_lat, max_lat)
146
+
147
+ def list_from_feature(self) -> list[object]:
148
+ """Convert the feature information into a list.
149
+
150
+ Returns
151
+ -------
152
+ list
153
+ The list of lunar features.
154
+ """
155
+ return [
156
+ self.name,
157
+ self.diameter,
158
+ self.latitude,
159
+ self.longitude,
160
+ self.delta_latitude,
161
+ self.delta_longitude,
162
+ self.feature_type,
163
+ self.quad_name,
164
+ self.quad_code,
165
+ self.code_name,
166
+ self.lunar_club_type,
167
+ ]
168
+
169
+ def longitude_range(self) -> Range:
170
+ """Get the longitude range of the feature.
171
+
172
+ Returns
173
+ -------
174
+ tuple(float, float)
175
+ The (minimum, maximum) longitude values for the feature.
176
+ """
177
+ min_lon = self.longitude - (self.delta_longitude / 2.0)
178
+ max_lon = self.longitude + (self.delta_longitude / 2.0)
179
+ return (min_lon, max_lon)
@@ -0,0 +1,97 @@
1
+ # This file is part of pylunar.
2
+ #
3
+ # Developed by Michael Reuter.
4
+ #
5
+ # See the LICENSE file at the top-level directory of this distribution
6
+ # for details of code ownership.
7
+ #
8
+ # Use of this source code is governed by a 3-clause BSD-style
9
+ # license that can be found in the LICENSE file.
10
+
11
+ """Module for the LunarFeatureContainer class."""
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = ["LunarFeatureContainer"]
16
+
17
+ import collections
18
+ import sys
19
+ from typing import Generator
20
+
21
+ if sys.version_info >= (3, 10):
22
+ from importlib.resources import files
23
+ else:
24
+ from importlib_resources import files
25
+
26
+ import sqlite3
27
+
28
+ from .lunar_feature import LunarFeature
29
+ from .moon_info import MoonInfo
30
+
31
+
32
+ class LunarFeatureContainer:
33
+ """Collection of Lunar features available from the database.
34
+
35
+ Parameters
36
+ ----------
37
+ club_name : str
38
+ The name of the observing club to sort on. Values are Lunar and
39
+ LunarII.
40
+ """
41
+
42
+ def __init__(self, club_name: str):
43
+ dbname = str(files("pylunar.data").joinpath("lunar.db"))
44
+ self.conn = sqlite3.connect(dbname)
45
+ self.club_name = club_name
46
+ self.features: dict[int, LunarFeature] = collections.OrderedDict()
47
+ self.club_type: set[str] = set()
48
+ self.feature_type: set[str] = set()
49
+
50
+ def __iter__(self) -> Generator[LunarFeature, None, None]:
51
+ """Create iterator for container.
52
+
53
+ Yields
54
+ ------
55
+ :class:`.LunarFeature`
56
+ The current lunar feature.
57
+ """
58
+ yield from self.features.values()
59
+
60
+ def __len__(self) -> int:
61
+ """Length of the container.
62
+
63
+ Returns
64
+ -------
65
+ int
66
+ The length of the container.
67
+ """
68
+ return len(self.features)
69
+
70
+ def load(self, moon_info: MoonInfo | None = None, limit: int | None = None) -> None:
71
+ """Read the Lunar features from the database.
72
+
73
+ Parameters
74
+ ----------
75
+ moon_info : :class:`.MoonInfo`, optional
76
+ Instance of the Lunar information class.
77
+ limit : int, optional
78
+ Restrict the number of features read to the given value.
79
+ """
80
+ if len(self.features) != 0:
81
+ self.features = collections.OrderedDict()
82
+
83
+ cur = self.conn.cursor()
84
+ sql = f'select * from Features where Lunar_Code = "{self.club_name}" or ' 'Lunar_Code = "Both"'
85
+ if limit is not None:
86
+ sql += f" limit {limit}"
87
+ cur.execute(sql)
88
+
89
+ for row in cur:
90
+ feature = LunarFeature.from_row(row)
91
+ is_visible = True if moon_info is None else moon_info.is_visible(feature)
92
+ if is_visible:
93
+ self.features[id(feature)] = feature
94
+ self.club_type.add(row[11])
95
+ self.feature_type.add(row[7])
96
+
97
+ cur.close()
pylunar/moon_info.py ADDED
@@ -0,0 +1,656 @@
1
+ # This file is part of pylunar.
2
+ #
3
+ # Developed by Michael Reuter.
4
+ #
5
+ # See the LICENSE file at the top-level directory of this distribution
6
+ # for details of code ownership.
7
+ #
8
+ # Use of this source code is governed by a 3-clause BSD-style
9
+ # license that can be found in the LICENSE file.
10
+
11
+ """Module for the MoonInfo class."""
12
+
13
+ from __future__ import annotations
14
+
15
+ __all__ = ["MoonInfo"]
16
+
17
+ from datetime import datetime
18
+ from enum import Enum
19
+ import math
20
+ from operator import itemgetter
21
+
22
+ import ephem
23
+ import pytz
24
+
25
+ from .helpers import mjd_to_date_tuple, tuple_to_string
26
+ from .lunar_feature import LunarFeature
27
+ from .types import DateTimeTuple, DmsCoordinate, MoonPhases
28
+
29
+
30
+ class PhaseName(Enum):
31
+ """Phase names for the lunar cycle."""
32
+
33
+ NEW_MOON = 0
34
+ WAXING_CRESCENT = 1
35
+ FIRST_QUARTER = 2
36
+ WAXING_GIBBOUS = 3
37
+ FULL_MOON = 4
38
+ WANING_GIBBOUS = 5
39
+ LAST_QUARTER = 6
40
+ WANING_CRESCENT = 7
41
+
42
+
43
+ class TimeOfDay(Enum):
44
+ """Time of day from the lunar terminator."""
45
+
46
+ MORNING = 0
47
+ EVENING = 1
48
+
49
+
50
+ class MoonInfo:
51
+ """Handle all moon information.
52
+
53
+ Attributes
54
+ ----------
55
+ observer : ephem.Observer instance.
56
+ The instance containing the observer's location information.
57
+ moon : ephem.Moon instance
58
+ The instance of the moon object.
59
+
60
+ Parameters
61
+ ----------
62
+ latitude : tuple of 3 ints
63
+ The latitude of the observer in GPS DMS(Degrees, Minutes and
64
+ Seconds) format.
65
+ longitude : tuple of 3 ints
66
+ The longitude of the observer in GPS DMS(Degrees, Minutes and
67
+ Seconds) format.
68
+ name : str, optional
69
+ A name for the observer's location.
70
+ """
71
+
72
+ DAYS_TO_HOURS = 24.0
73
+ MAIN_PHASE_CUTOFF = 2.0
74
+ # Time cutoff (hours) around the NM, FQ, FM, and LQ phases
75
+ FEATURE_CUTOFF = 15.0
76
+ # The offset (degrees) from the colongitude used for visibility check
77
+ NO_CUTOFF_TYPE = ("Landing Site", "Mare", "Oceanus")
78
+ # Feature types that are not subject to longitude cutoffs
79
+ LIBRATION_ZONE = 80.0
80
+ # Latitude and/or longitude where librations have a big effect
81
+ MAXIMUM_LIBRATION_PHASE_ANGLE_CUTOFF = 65.0
82
+ # The maximum value of the libration phase angle difference for a feature
83
+
84
+ reverse_phase_lookup = {
85
+ "new_moon": (ephem.previous_last_quarter_moon, "last_quarter"),
86
+ "first_quarter": (ephem.previous_new_moon, "new_moon"),
87
+ "full_moon": (ephem.previous_first_quarter_moon, "first_quarter"),
88
+ "last_quarter": (ephem.previous_full_moon, "full_moon"),
89
+ }
90
+
91
+ def __init__(self, latitude: DmsCoordinate, longitude: DmsCoordinate, name: str | None = None):
92
+ self.observer = ephem.Observer()
93
+ self.observer.lat = tuple_to_string(latitude)
94
+ self.observer.long = tuple_to_string(longitude)
95
+ self.moon = ephem.Moon()
96
+
97
+ def age(self) -> float:
98
+ """Lunar age in days.
99
+
100
+ Returns
101
+ -------
102
+ float
103
+ The lunar age.
104
+ """
105
+ prev_new = ephem.previous_new_moon(self.observer.date)
106
+ return float(self.observer.date - prev_new)
107
+
108
+ def fractional_age(self) -> float:
109
+ """Lunar fractional age which is always less than 1.0.
110
+
111
+ Returns
112
+ -------
113
+ float
114
+ The fractional lunar age.
115
+ """
116
+ prev_new = ephem.previous_new_moon(self.observer.date)
117
+ next_new = ephem.next_new_moon(self.observer.date)
118
+ return float((self.observer.date - prev_new) / (next_new - prev_new))
119
+
120
+ def altitude(self) -> float:
121
+ """Lunar altitude in degrees.
122
+
123
+ Returns
124
+ -------
125
+ float
126
+ The lunar altitiude.
127
+ """
128
+ return math.degrees(self.moon.alt)
129
+
130
+ def angular_size(self) -> float:
131
+ """Lunar current angular size in degrees.
132
+
133
+ Returns
134
+ -------
135
+ float
136
+ The lunar angular size.
137
+ """
138
+ moon_size: float = self.moon.size
139
+ return moon_size / 3600.0
140
+
141
+ def azimuth(self) -> float:
142
+ """Lunar azimuth in degrees.
143
+
144
+ Returns
145
+ -------
146
+ float
147
+ The lunar azimuth.
148
+ """
149
+ return math.degrees(self.moon.az)
150
+
151
+ def colong(self) -> float:
152
+ """Lunar selenographic colongitude in degrees.
153
+
154
+ Returns
155
+ -------
156
+ float
157
+ The lunar seleographic colongitude.
158
+ """
159
+ return math.degrees(self.moon.colong)
160
+
161
+ def dec(self) -> float:
162
+ """Lunar current declination in degrees.
163
+
164
+ Returns
165
+ -------
166
+ float
167
+ The lunar declination.
168
+ """
169
+ return math.degrees(self.moon.dec)
170
+
171
+ def earth_distance(self) -> float:
172
+ """Lunar current distance from the earth in km.
173
+
174
+ Returns
175
+ -------
176
+ float
177
+ THe earth-moon distance.
178
+ """
179
+ return float(self.moon.earth_distance * ephem.meters_per_au / 1000.0)
180
+
181
+ def elongation(self) -> float:
182
+ """Lunar elongation from the sun in degrees.
183
+
184
+ Returns
185
+ -------
186
+ float
187
+ The lunar solar elongation.
188
+ """
189
+ elongation = math.degrees(self.moon.elong)
190
+ if elongation < 0:
191
+ elongation += 360.0
192
+ return elongation
193
+
194
+ def fractional_phase(self) -> float:
195
+ """Lunar fractional illumination which is always less than 1.0.
196
+
197
+ Returns
198
+ -------
199
+ float
200
+ The lunar fractional phase.
201
+ """
202
+ return float(self.moon.moon_phase)
203
+
204
+ def libration_lat(self) -> float:
205
+ """Lunar current latitudinal libration in degrees.
206
+
207
+ Returns
208
+ -------
209
+ float
210
+ The lunar libration latitude.
211
+ """
212
+ return math.degrees(self.moon.libration_lat)
213
+
214
+ def libration_lon(self) -> float:
215
+ """Lunar current longitudinal libration in degrees.
216
+
217
+ Returns
218
+ -------
219
+ float
220
+ The lunar libration longitude.
221
+ """
222
+ return math.degrees(self.moon.libration_long)
223
+
224
+ def libration_phase_angle(self) -> float:
225
+ """Phase angle of lunar current libration in degrees.
226
+
227
+ Returns
228
+ -------
229
+ float
230
+ The lunar libration phase angle.
231
+ """
232
+ phase_angle = math.atan2(self.moon.libration_long, self.moon.libration_lat)
233
+ phase_angle += 2.0 * math.pi if phase_angle < 0 else 0.0
234
+ return math.degrees(phase_angle)
235
+
236
+ def magnitude(self) -> float:
237
+ """Lunar current magnitude.
238
+
239
+ Returns
240
+ -------
241
+ float
242
+ The lunar magnitude.
243
+ """
244
+ return float(self.moon.mag)
245
+
246
+ def colong_to_long(self) -> float:
247
+ """Selenographic longitude in degrees based on the terminator.
248
+
249
+ Returns
250
+ -------
251
+ float
252
+ The lunar seleographic longitude.
253
+ """
254
+ colong: float = self.colong()
255
+ if 90.0 <= colong < 270.0:
256
+ longitude = 180.0 - colong
257
+ elif 270.0 <= colong < 360.0:
258
+ longitude = 360.0 - colong
259
+ else:
260
+ longitude = -colong
261
+
262
+ return longitude
263
+
264
+ def is_libration_ok(self, feature: LunarFeature) -> bool:
265
+ """Determine if lunar feature is visible due to libration effect.
266
+
267
+ Parameters
268
+ ----------
269
+ feature : :class:`.LunarFeature`
270
+ The lunar feature instance to check.
271
+
272
+ Returns
273
+ -------
274
+ bool
275
+ True if visible, False if not.
276
+ """
277
+ is_lon_in_zone = math.fabs(feature.longitude) > self.LIBRATION_ZONE
278
+ is_lat_in_zone = math.fabs(feature.latitude) > self.LIBRATION_ZONE
279
+ if is_lat_in_zone or is_lon_in_zone:
280
+ feature_angle = feature.feature_angle()
281
+ libration_phase_angle = self.libration_phase_angle()
282
+ delta_phase_angle = libration_phase_angle - feature_angle
283
+ delta_phase_angle -= 360.0 if delta_phase_angle > 180.0 else 0.0
284
+
285
+ return math.fabs(delta_phase_angle) <= self.MAXIMUM_LIBRATION_PHASE_ANGLE_CUTOFF
286
+
287
+ return True
288
+
289
+ def is_visible(self, feature: LunarFeature) -> bool:
290
+ """Determine if lunar feature is visible.
291
+
292
+ Parameters
293
+ ----------
294
+ feature : :class:`.LunarFeature`
295
+ The lunar feature instance to check.
296
+
297
+ Returns
298
+ -------
299
+ bool
300
+ True if visible, False if not.
301
+ """
302
+ selco_lon = self.colong_to_long()
303
+ current_tod = self.time_of_day()
304
+
305
+ min_lon = feature.longitude - feature.delta_longitude / 2
306
+ max_lon = feature.longitude + feature.delta_longitude / 2
307
+
308
+ if min_lon > max_lon:
309
+ min_lon, max_lon = max_lon, min_lon
310
+
311
+ is_visible = False
312
+ latitude_scaling = math.cos(math.radians(feature.latitude))
313
+ if feature.feature_type not in MoonInfo.NO_CUTOFF_TYPE:
314
+ cutoff = MoonInfo.FEATURE_CUTOFF / latitude_scaling
315
+ else:
316
+ cutoff = MoonInfo.FEATURE_CUTOFF
317
+
318
+ if current_tod == TimeOfDay.MORNING.name:
319
+ # Minimum longitude for morning visibility
320
+ lon_cutoff = min_lon - cutoff
321
+ if feature.feature_type in MoonInfo.NO_CUTOFF_TYPE:
322
+ is_visible = selco_lon <= min_lon
323
+ else:
324
+ is_visible = lon_cutoff <= selco_lon <= min_lon
325
+ else:
326
+ # Maximum longitude for evening visibility
327
+ lon_cutoff = max_lon + cutoff
328
+ if feature.feature_type in MoonInfo.NO_CUTOFF_TYPE:
329
+ is_visible = max_lon <= selco_lon
330
+ else:
331
+ is_visible = max_lon <= selco_lon <= lon_cutoff
332
+
333
+ return is_visible and self.is_libration_ok(feature)
334
+
335
+ def next_four_phases(self) -> MoonPhases:
336
+ """Next four phases in date sorted order (closest phase first).
337
+
338
+ Returns
339
+ -------
340
+ list[(str, float)]
341
+ Set of lunar phases specified by an abbreviated phase name and
342
+ Modified Julian Date.
343
+ """
344
+ phases = {}
345
+ phases["new_moon"] = ephem.next_new_moon(self.observer.date)
346
+ phases["first_quarter"] = ephem.next_first_quarter_moon(self.observer.date)
347
+ phases["full_moon"] = ephem.next_full_moon(self.observer.date)
348
+ phases["last_quarter"] = ephem.next_last_quarter_moon(self.observer.date)
349
+
350
+ sorted_phases = sorted(phases.items(), key=itemgetter(1))
351
+ sorted_phases = [(phase[0], mjd_to_date_tuple(phase[1])) for phase in sorted_phases]
352
+
353
+ return sorted_phases
354
+
355
+ def phase_name(self) -> str:
356
+ """Return standard name of lunar phase, i.e. Waxing Cresent.
357
+
358
+ This function returns a standard name for lunar phase based on the
359
+ current selenographic colongitude.
360
+
361
+ Returns
362
+ -------
363
+ str
364
+ The lunar phase name.
365
+ """
366
+ next_phase_name = self.next_four_phases()[0][0]
367
+ try:
368
+ next_phase_time = getattr(ephem, f"next_{next_phase_name}")(self.observer.date)
369
+ except AttributeError:
370
+ next_phase_time = getattr(ephem, f"next_{next_phase_name}_moon")(self.observer.date)
371
+ previous_phase = self.reverse_phase_lookup[next_phase_name]
372
+ time_to_next_phase = math.fabs(next_phase_time - self.observer.date) * self.DAYS_TO_HOURS
373
+ time_to_previous_phase = (
374
+ math.fabs(self.observer.date - previous_phase[0](self.observer.date)) * self.DAYS_TO_HOURS
375
+ )
376
+ previous_phase_name = previous_phase[1]
377
+
378
+ phase_name = ""
379
+ if time_to_previous_phase < self.MAIN_PHASE_CUTOFF:
380
+ phase_name = getattr(PhaseName, previous_phase_name.upper()).name
381
+ elif time_to_next_phase < self.MAIN_PHASE_CUTOFF:
382
+ phase_name = getattr(PhaseName, next_phase_name.upper()).name
383
+ else:
384
+ if previous_phase_name == "new_moon" and next_phase_name == "first_quarter":
385
+ phase_name = PhaseName.WAXING_CRESCENT.name
386
+ elif previous_phase_name == "first_quarter" and next_phase_name == "full_moon":
387
+ phase_name = PhaseName.WAXING_GIBBOUS.name
388
+ elif previous_phase_name == "full_moon" and next_phase_name == "last_quarter":
389
+ phase_name = PhaseName.WANING_GIBBOUS.name
390
+ elif previous_phase_name == "last_quarter" and next_phase_name == "new_moon":
391
+ phase_name = PhaseName.WANING_CRESCENT.name
392
+ return phase_name
393
+
394
+ def phase_shape_in_ascii(self) -> str:
395
+ """Display lunar phase shape in ASCII art.
396
+
397
+ This function returns a multi-line string demonstrate current lunar
398
+ shape in ASCII format.
399
+
400
+ Returns
401
+ -------
402
+ str
403
+ The lunar phase shape.
404
+ """
405
+ phase = self.phase_name()
406
+
407
+ if phase == PhaseName.NEW_MOON.name:
408
+ return """ _..._
409
+ .:::::::.
410
+ :::::::::::
411
+ :::::::::::
412
+ `:::::::::'
413
+ `':::'' """
414
+ elif phase == PhaseName.WAXING_CRESCENT.name:
415
+ return """ _..._
416
+ .::::. `.
417
+ :::::::. :
418
+ :::::::: :
419
+ `::::::' .'
420
+ `'::'-' """
421
+ elif phase == PhaseName.FIRST_QUARTER.name:
422
+ return """ _..._
423
+ .:::: `.
424
+ :::::: :
425
+ :::::: :
426
+ `::::: .'
427
+ `'::.-' """
428
+ elif phase == PhaseName.WAXING_GIBBOUS.name:
429
+ return """ _..._
430
+ .::' `.
431
+ ::: :
432
+ ::: :
433
+ `::. .'
434
+ `':..-' """
435
+ elif phase == PhaseName.FULL_MOON.name:
436
+ return """ _..._
437
+ .' `.
438
+ : :
439
+ : :
440
+ `. .'
441
+ `-...-' """
442
+ elif phase == PhaseName.WANING_GIBBOUS.name:
443
+ return """ _..._
444
+ .' `::.
445
+ : :::
446
+ : :::
447
+ `. .::'
448
+ `-..:'' """
449
+ elif phase == PhaseName.LAST_QUARTER.name:
450
+ return """ _..._
451
+ .' ::::.
452
+ : ::::::
453
+ : ::::::
454
+ `. :::::'
455
+ `-.::'' """
456
+ elif phase == PhaseName.WAXING_CRESCENT.name:
457
+ return """ _..._
458
+ .' .::::.
459
+ : ::::::::
460
+ : ::::::::
461
+ `. '::::::'
462
+ `-.::'' """
463
+ else:
464
+ return phase
465
+
466
+ def phase_emoji(self) -> str:
467
+ """Return standard emoji of lunar phase, i.e. '🌒'.
468
+
469
+ This function returns a standard emoji for lunar phase based on the
470
+ current selenographic colongitude.
471
+
472
+ Returns
473
+ -------
474
+ str
475
+ The lunar phase emoji.
476
+ """
477
+ return {
478
+ "NEW_MOON": "🌑",
479
+ "WAXING_CRESCENT": "🌒",
480
+ "FIRST_QUARTER": "🌓",
481
+ "WAXING_GIBBOUS": "🌔",
482
+ "FULL_MOON": "🌕",
483
+ "WANING_GIBBOUS": "🌖",
484
+ "LAST_QUARTER": "🌗",
485
+ "WANING_CRESCENT": "🌘",
486
+ }[self.phase_name()]
487
+
488
+ def ra(self) -> float:
489
+ """Lunar current right ascension in degrees.
490
+
491
+ Returns
492
+ -------
493
+ float
494
+ The lunar right ascension.
495
+ """
496
+ return math.degrees(self.moon.ra)
497
+
498
+ def rise_set_times(self, timezone: str) -> MoonPhases:
499
+ """Calculate the rise, set and transit times in the local time system.
500
+
501
+ Parameters
502
+ ----------
503
+ timezone : str
504
+ The timezone identifier for the calculations.
505
+
506
+ Returns
507
+ -------
508
+ list[(str, tuple)]
509
+ Set of rise, set, and transit times in the local time system. If
510
+ event does not happen, 'Does not xxx' is tuple value.
511
+ """
512
+ utc = pytz.utc
513
+ try:
514
+ tz = pytz.timezone(timezone)
515
+ except pytz.UnknownTimeZoneError:
516
+ tz = utc
517
+
518
+ func_map = {"rise": "rising", "transit": "transit", "set": "setting"}
519
+
520
+ # Need to set observer's horizon and pressure to get times
521
+ old_pressure = self.observer.pressure
522
+ old_horizon = self.observer.horizon
523
+
524
+ self.observer.pressure = 0
525
+ self.observer.horizon = "-0:34"
526
+
527
+ current_date_utc = datetime(*mjd_to_date_tuple(self.observer.date, round_off=True), tzinfo=utc) # type: ignore
528
+ current_date = current_date_utc.astimezone(tz)
529
+ current_day = current_date.day
530
+ times = {}
531
+ does_not = None
532
+ for time_type in ("rise", "transit", "set"):
533
+ mjd_time = getattr(self.observer, "{}_{}".format("next", func_map[time_type]))(self.moon)
534
+ utc_time = datetime(*mjd_to_date_tuple(mjd_time, round_off=True), tzinfo=utc) # type: ignore
535
+ local_date = utc_time.astimezone(tz)
536
+ if local_date.day == current_day:
537
+ times[time_type] = local_date
538
+ else:
539
+ mjd_time = getattr(self.observer, "{}_{}".format("previous", func_map[time_type]))(self.moon)
540
+ utc_time = datetime(*mjd_to_date_tuple(mjd_time, round_off=True), tzinfo=utc) # type: ignore
541
+ local_date = utc_time.astimezone(tz)
542
+ if local_date.day == current_day:
543
+ times[time_type] = local_date
544
+ else:
545
+ does_not = (time_type, f"Does not {time_type}")
546
+
547
+ # Return observer and moon to previous state
548
+ self.observer.pressure = old_pressure
549
+ self.observer.horizon = old_horizon
550
+ self.moon.compute(self.observer)
551
+
552
+ original_sorted_times = sorted(times.items(), key=itemgetter(1))
553
+ sorted_times: MoonPhases = [(xtime[0], xtime[1].timetuple()[:6]) for xtime in original_sorted_times]
554
+ if does_not is not None:
555
+ sorted_times.insert(0, does_not)
556
+
557
+ return sorted_times
558
+
559
+ def subsolar_lat(self) -> float:
560
+ """Latitude in degress on the moon where the sun is overhead.
561
+
562
+ Returns
563
+ -------
564
+ float
565
+ The lunar subsolar latitude.
566
+ """
567
+ return math.degrees(self.moon.subsolar_lat)
568
+
569
+ def time_of_day(self) -> str:
570
+ """Terminator time of day.
571
+
572
+ This function determines if the terminator is sunrise (morning) or
573
+ sunset (evening).
574
+
575
+ Returns
576
+ -------
577
+ str
578
+ The lunar time of day.
579
+ """
580
+ colong = self.colong()
581
+ if 90.0 <= colong < 270.0:
582
+ return TimeOfDay.EVENING.name
583
+ else:
584
+ return TimeOfDay.MORNING.name
585
+
586
+ def time_from_new_moon(self) -> float:
587
+ """Time (hours) from the previous new moon.
588
+
589
+ This function calculates the time from the previous new moon.
590
+
591
+ Returns
592
+ -------
593
+ float
594
+ The time from new moon.
595
+ """
596
+ previous_new_moon = ephem.previous_new_moon(self.observer.date)
597
+ return float(MoonInfo.DAYS_TO_HOURS * (self.observer.date - previous_new_moon))
598
+
599
+ def time_to_full_moon(self) -> float:
600
+ """Time (days) to the next full moon.
601
+
602
+ This function calculates the time to the next full moon.
603
+
604
+ Returns
605
+ -------
606
+ float
607
+ The time to full moon.
608
+ """
609
+ next_full_moon = ephem.next_full_moon(self.observer.date)
610
+ return float(next_full_moon - self.observer.date)
611
+
612
+ def time_to_new_moon(self) -> float:
613
+ """Time (hours) to the next new moon.
614
+
615
+ This function calculates the time to the next new moon.
616
+
617
+ Returns
618
+ -------
619
+ float
620
+ The time to new moon.
621
+ """
622
+ next_new_moon = ephem.next_new_moon(self.observer.date)
623
+ return float(MoonInfo.DAYS_TO_HOURS * (next_new_moon - self.observer.date))
624
+
625
+ def update(self, datetime: DateTimeTuple) -> None:
626
+ """Update the moon information based on time.
627
+
628
+ This fuction updates the Observer instance's datetime setting. The
629
+ incoming datetime tuple should be in UTC with the following placement
630
+ of values: (YYYY, m, d, H, M, S) as defined below::
631
+
632
+ YYYY
633
+ Four digit year
634
+
635
+ m
636
+ month (1-12)
637
+
638
+ d
639
+ day (1-31)
640
+
641
+ H
642
+ hours (0-23)
643
+
644
+ M
645
+ minutes (0-59)
646
+
647
+ S
648
+ seconds (0-59)
649
+
650
+ Parameters
651
+ ----------
652
+ datetime : tuple
653
+ The current UTC time in a tuple of numbers.
654
+ """
655
+ self.observer.date = datetime
656
+ self.moon.compute(self.observer)
pylunar/types.py ADDED
@@ -0,0 +1,31 @@
1
+ # This file is part of pylunar.
2
+ #
3
+ # Developed by Michael Reuter.
4
+ #
5
+ # See the LICENSE file at the top-level directory of this distribution
6
+ # for details of code ownership.
7
+ #
8
+ # Use of this source code is governed by a 3-clause BSD-style
9
+ # license that can be found in the LICENSE file.
10
+
11
+ """Module for holding types."""
12
+
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+
17
+ if sys.version_info >= (3, 10):
18
+ from typing import TypeAlias
19
+ else:
20
+ from typing_extensions import TypeAlias
21
+
22
+ from typing import List, Tuple, Union
23
+
24
+ DateTimeTuple: TypeAlias = Tuple[int, int, int, int, int, Union[int, float]]
25
+ MoonPhases: TypeAlias = List[Tuple[str, Union[DateTimeTuple, str]]]
26
+ DmsCoordinate: TypeAlias = Tuple[int, int, int]
27
+ Range: TypeAlias = Tuple[float, float]
28
+ LunarFeatureList: TypeAlias = Tuple[
29
+ str, float, float, float, float, float, str, str, str, str, Union[str, None]
30
+ ]
31
+ FeatureRow: TypeAlias = Tuple[int, str, float, float, float, float, float, str, str, str, str, str]
@@ -0,0 +1,16 @@
1
+ =======
2
+ Credits
3
+ =======
4
+
5
+ Development Lead
6
+ ----------------
7
+
8
+ * Michael Reuter <mareuternh@gmail.com>
9
+
10
+ Contributors
11
+ ------------
12
+
13
+ * `1kastner <https://github.com/1kastner>`_
14
+ * `Louis Knapp <https://github.com/lknapp>`_
15
+ * `Bryan Neal Garrison <https://github.com/noblecloud>`_
16
+ * `Peyman Majidi Moein <https://github.com/peymanmajidi>`_
@@ -0,0 +1,26 @@
1
+ Copyright 2016-2024 Michael Reuter
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this
7
+ list of conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ 3. Neither the name of the copyright holder nor the names of its contributors
14
+ may be used to endorse or promote products derived from this software
15
+ without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.1
2
+ Name: pylunar
3
+ Version: 0.7.1
4
+ Summary: Information for completing the Astronomical League's Lunar and Lunar II observing programs.
5
+ Author-email: Michael Reuter <mareuternh@gmail.com>
6
+ Project-URL: Documentation, http://pylunar.readthedocs.io
7
+ Project-URL: Repository, https://github.com/mareuter/pylunar
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Natural Language :: English
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Description-Content-Type: text/x-rst
19
+ License-File: LICENSE
20
+ License-File: AUTHORS.rst
21
+ Requires-Dist: ephem ==4.1.5
22
+ Requires-Dist: pytz ==2024.1
23
+ Requires-Dist: importlib-resources ==6.1.1 ; python_version < "3.10"
24
+ Requires-Dist: typing-extensions ==4.9.0 ; python_version < "3.10"
25
+ Provides-Extra: build
26
+ Requires-Dist: build ==1.0.3 ; extra == 'build'
27
+ Requires-Dist: twine ==4.0.2 ; extra == 'build'
28
+ Provides-Extra: dev
29
+ Requires-Dist: pylunar[build,docs,lint,test] ; extra == 'dev'
30
+ Requires-Dist: tox ==4.11.4 ; extra == 'dev'
31
+ Provides-Extra: docs
32
+ Requires-Dist: sphinx ~=7.1 ; extra == 'docs'
33
+ Requires-Dist: sphinx-rtd-theme ==2.0.0 ; extra == 'docs'
34
+ Provides-Extra: lint
35
+ Requires-Dist: pre-commit ~=3.5.0 ; extra == 'lint'
36
+ Provides-Extra: test
37
+ Requires-Dist: coverage[toml] ==7.4.0 ; extra == 'test'
38
+ Requires-Dist: pytest ==7.4.4 ; extra == 'test'
39
+
40
+ =============================
41
+ Python Lunar
42
+ =============================
43
+
44
+ .. |license| image:: https://img.shields.io/pypi/l/pylunar.svg
45
+ :target: http://opensource.org/licenses/BSD
46
+ :alt: BSD License
47
+
48
+ .. |version| image:: http://img.shields.io/pypi/v/pylunar.svg
49
+ :target: https://pypi.python.org/pypi/pylunar
50
+ :alt: Software Version
51
+
52
+ .. |github| image:: https://github.com/mareuter/pylunar/actions/workflows/ci.yaml/badge.svg
53
+ :target: https://github.com/mareuter/pylunar
54
+ :alt: Github build status
55
+
56
+ .. |python| image:: https://img.shields.io/pypi/pyversions/pylunar.svg
57
+ :target: https://pypi.python.org/pypi/pylunar
58
+ :alt: Supported Python
59
+
60
+ .. |docs| image:: https://readthedocs.org/projects/pylunar/badge/?version=latest
61
+ :target: https://pylunar.readthedocs.io
62
+ :alt: Readthedocs status
63
+
64
+ .. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit
65
+ :target: https://github.com/pre-commit/pre-commit
66
+ :alt: Uses pre-commit
67
+
68
+ |license| |python| |version| |github| |docs| |pre-commit|
69
+
70
+ Information for completing the Astronomical League's Lunar and Lunar II observing programs. Uses the `pyephem <https://rhodesmill.org/pyephem>`_ package to calculate lunar information.
71
+
72
+
73
+ Features
74
+ --------
75
+
76
+ * Offer moon information based on location and date/time.
77
+ * Offer lunar targets for AL observing clubs based on terminator location.
78
+
@@ -0,0 +1,13 @@
1
+ pylunar/__init__.py,sha256=-I-kK_YCtnOkwYPs2Qe9mCaanU5XJLnXxhHzNbKJpAk,1056
2
+ pylunar/helpers.py,sha256=Ip1koGrhbay318QLOhQCMwBLDA_Q1bKXHDkU_Un3EZE,1323
3
+ pylunar/lunar_feature.py,sha256=oK-ALplg29DtGzKXqPUbssri3K0_1Ftz97SPzNhP8v8,5521
4
+ pylunar/lunar_feature_container.py,sha256=ud8-B_CqQ_0x9FSbkj2eHMPRT5PAwSai6D6tU9dGkQY,2796
5
+ pylunar/moon_info.py,sha256=AdYdfgKgwnL2nRs9QF9x28_pflCaxsX10VKFvqaXLGY,19958
6
+ pylunar/types.py,sha256=DbPCc9_kzRuSOxOfeKaLJdWfykdbzRPnOTY-EN2xrsc,973
7
+ pylunar/data/lunar.db,sha256=y2u5wR_DpHjPYH7fuR-T_q0nbPzN1GuP8_OXorjVr14,28672
8
+ pylunar-0.7.1.dist-info/AUTHORS.rst,sha256=mqGQzrJPFGm44DZxZFs0d080QDWXdtfFlv1i_fdJerg,332
9
+ pylunar-0.7.1.dist-info/LICENSE,sha256=EF_CKfNhkKjEynpn9Msb6HaGNIBN9VfeGyrJFJQ5FYk,1479
10
+ pylunar-0.7.1.dist-info/METADATA,sha256=Jkl2_uVETgCknLpOeWwUhE4bBH0P1kReYzSbldNXxOA,3035
11
+ pylunar-0.7.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
12
+ pylunar-0.7.1.dist-info/top_level.txt,sha256=vEQZCgYlUuoq6D4q99Kj5fSpNs2Mo-jW8vKV3dnpI-U,8
13
+ pylunar-0.7.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pylunar