celestial-calendar 0.6.0__py3-none-win_amd64.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.
- celestial_calendar/__init__.py +631 -0
- celestial_calendar/_binding.py +245 -0
- celestial_calendar/_native/_celestial_calendar.dll +0 -0
- celestial_calendar/_version.py +1 -0
- celestial_calendar-0.6.0.dist-info/METADATA +106 -0
- celestial_calendar-0.6.0.dist-info/RECORD +8 -0
- celestial_calendar-0.6.0.dist-info/WHEEL +5 -0
- celestial_calendar-0.6.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# CelestialCalendar:
|
|
2
|
+
# A C++23-style library that performs astronomical calculations and date conversions among various calendars,
|
|
3
|
+
# including Gregorian, Lunar, and Chinese Ganzhi calendars.
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2026 Ningqi Wang (0xf3cd)
|
|
6
|
+
# Email: nq.maigre@gmail.com
|
|
7
|
+
# Repo : https://github.com/0xf3cd/celestial-calendar
|
|
8
|
+
#
|
|
9
|
+
# This project is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# This project is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with this project. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
"""Astronomical calculations and Chinese calendar conversion."""
|
|
23
|
+
|
|
24
|
+
import ctypes as _ctypes
|
|
25
|
+
import math as _math
|
|
26
|
+
from dataclasses import dataclass as _dataclass
|
|
27
|
+
from enum import IntEnum as _IntEnum
|
|
28
|
+
from enum import StrEnum as _StrEnum
|
|
29
|
+
from typing import TypeVar as _TypeVar
|
|
30
|
+
|
|
31
|
+
from . import _binding
|
|
32
|
+
from ._version import VERSION as __version__
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LogVerbosity(_StrEnum):
|
|
36
|
+
"""Native logging verbosity."""
|
|
37
|
+
|
|
38
|
+
NONE = "none"
|
|
39
|
+
INFO = "info"
|
|
40
|
+
DEBUG = "debug"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MoonPhase(_StrEnum):
|
|
44
|
+
"""One of the four principal Moon phases."""
|
|
45
|
+
|
|
46
|
+
NEW = "new"
|
|
47
|
+
FIRST_QUARTER = "first_quarter"
|
|
48
|
+
FULL = "full"
|
|
49
|
+
LAST_QUARTER = "last_quarter"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeltaTModel(_StrEnum):
|
|
53
|
+
"""A selectable ΔT model."""
|
|
54
|
+
|
|
55
|
+
DEFAULT = "default"
|
|
56
|
+
ALGO1 = "algo1"
|
|
57
|
+
ALGO2 = "algo2"
|
|
58
|
+
ALGO3 = "algo3"
|
|
59
|
+
ALGO4 = "algo4"
|
|
60
|
+
ALGO5 = "algo5"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class LunarAlgorithm(_StrEnum):
|
|
64
|
+
"""A Chinese Lunar calendar conversion algorithm."""
|
|
65
|
+
|
|
66
|
+
ALGO1 = "algo1"
|
|
67
|
+
ALGO2 = "algo2"
|
|
68
|
+
ALGO3 = "algo3"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Jieqi(_IntEnum):
|
|
72
|
+
"""The 24 Chinese Jieqi (节气), indexed from Lichun (立春)."""
|
|
73
|
+
|
|
74
|
+
LICHUN = 0
|
|
75
|
+
YUSHUI = 1
|
|
76
|
+
JINGZHE = 2
|
|
77
|
+
CHUNFEN = 3
|
|
78
|
+
QINGMING = 4
|
|
79
|
+
GUYU = 5
|
|
80
|
+
LIXIA = 6
|
|
81
|
+
XIAOMAN = 7
|
|
82
|
+
MANGZHONG = 8
|
|
83
|
+
XIAZHI = 9
|
|
84
|
+
XIAOSHU = 10
|
|
85
|
+
DASHU = 11
|
|
86
|
+
LIQIU = 12
|
|
87
|
+
CHUSHU = 13
|
|
88
|
+
BAILU = 14
|
|
89
|
+
QIUFEN = 15
|
|
90
|
+
HANLU = 16
|
|
91
|
+
SHUANGJIANG = 17
|
|
92
|
+
LIDONG = 18
|
|
93
|
+
XIAOXUE = 19
|
|
94
|
+
DAXUE = 20
|
|
95
|
+
DONGZHI = 21
|
|
96
|
+
XIAOHAN = 22
|
|
97
|
+
DAHAN = 23
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@_dataclass(frozen=True)
|
|
101
|
+
class CivilDateTime:
|
|
102
|
+
"""A civil date and fraction of its day; the function names identify its time scale."""
|
|
103
|
+
|
|
104
|
+
year: int
|
|
105
|
+
month: int
|
|
106
|
+
day: int
|
|
107
|
+
fraction: float
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@_dataclass(frozen=True)
|
|
111
|
+
class GregorianDate:
|
|
112
|
+
"""A proleptic Gregorian calendar date."""
|
|
113
|
+
|
|
114
|
+
year: int
|
|
115
|
+
month: int
|
|
116
|
+
day: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@_dataclass(frozen=True)
|
|
120
|
+
class LunarDate:
|
|
121
|
+
"""A Chinese Lunar date using traditional month numbering."""
|
|
122
|
+
|
|
123
|
+
year: int
|
|
124
|
+
month: int
|
|
125
|
+
day: int
|
|
126
|
+
is_leap: bool
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@_dataclass(frozen=True)
|
|
130
|
+
class SunCoordinate:
|
|
131
|
+
"""Apparent geocentric ecliptic coordinates of the Sun."""
|
|
132
|
+
|
|
133
|
+
longitude_deg: float
|
|
134
|
+
latitude_deg: float
|
|
135
|
+
radius_au: float
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@_dataclass(frozen=True)
|
|
139
|
+
class MoonCoordinate:
|
|
140
|
+
"""Apparent geocentric ecliptic coordinates of the Moon."""
|
|
141
|
+
|
|
142
|
+
longitude_deg: float
|
|
143
|
+
latitude_deg: float
|
|
144
|
+
distance_km: float
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@_dataclass(frozen=True)
|
|
148
|
+
class MoonIllumination:
|
|
149
|
+
"""The illuminated fraction and Moon-Sun elongation."""
|
|
150
|
+
|
|
151
|
+
fraction: float
|
|
152
|
+
elongation_deg: float
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@_dataclass(frozen=True)
|
|
156
|
+
class JieqiMoment:
|
|
157
|
+
"""The UT1 civil moment of a Jieqi."""
|
|
158
|
+
|
|
159
|
+
jieqi: Jieqi
|
|
160
|
+
moment_ut1: CivilDateTime
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@_dataclass(frozen=True)
|
|
164
|
+
class LunarYearRange:
|
|
165
|
+
"""Inclusive Lunar year range supported by an algorithm."""
|
|
166
|
+
|
|
167
|
+
start: int
|
|
168
|
+
end: int
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@_dataclass(frozen=True)
|
|
172
|
+
class LunarYearInfo:
|
|
173
|
+
"""The Gregorian first day and month structure of a Lunar year."""
|
|
174
|
+
|
|
175
|
+
first_day: GregorianDate
|
|
176
|
+
leap_month: int | None
|
|
177
|
+
month_lengths: tuple[int, ...]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class CelestialError(RuntimeError):
|
|
181
|
+
"""A failure reported by the native CelestialCalendar boundary.
|
|
182
|
+
|
|
183
|
+
Attributes:
|
|
184
|
+
operation: The public function that failed.
|
|
185
|
+
recorded: Whether the message came from the native error channel.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, operation: str, message: str, *, recorded: bool) -> None:
|
|
189
|
+
super().__init__(message)
|
|
190
|
+
self.operation = operation
|
|
191
|
+
self.recorded = recorded
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
_EnumT = _TypeVar("_EnumT", bound=_IntEnum | _StrEnum)
|
|
195
|
+
_MAX_CIVIL_YEAR = 32_767
|
|
196
|
+
_MAX_CALENDAR_YEAR = 32_766
|
|
197
|
+
_MIN_JIEQI_YEAR = 401
|
|
198
|
+
_MAX_NEW_MOON_COUNT = 4_096
|
|
199
|
+
_JIEQI_NAME_BYTES = 16
|
|
200
|
+
_LOG_VERBOSITY = {LogVerbosity.NONE: 0, LogVerbosity.INFO: 1, LogVerbosity.DEBUG: 2}
|
|
201
|
+
_MOON_PHASE = {MoonPhase.NEW: 0, MoonPhase.FIRST_QUARTER: 1, MoonPhase.FULL: 2, MoonPhase.LAST_QUARTER: 3}
|
|
202
|
+
_LUNAR_ALGORITHM = {LunarAlgorithm.ALGO1: 1, LunarAlgorithm.ALGO2: 2, LunarAlgorithm.ALGO3: 3}
|
|
203
|
+
_LUNAR_YEAR_RANGE = {
|
|
204
|
+
LunarAlgorithm.ALGO1: (1901, 2099),
|
|
205
|
+
LunarAlgorithm.ALGO2: (410, 2500),
|
|
206
|
+
LunarAlgorithm.ALGO3: (1600, 2199),
|
|
207
|
+
}
|
|
208
|
+
_DELTA_T_EXPORT = {
|
|
209
|
+
DeltaTModel.DEFAULT: "delta_t",
|
|
210
|
+
DeltaTModel.ALGO1: "delta_t_algo1",
|
|
211
|
+
DeltaTModel.ALGO2: "delta_t_algo2",
|
|
212
|
+
DeltaTModel.ALGO3: "delta_t_algo3",
|
|
213
|
+
DeltaTModel.ALGO4: "delta_t_algo4",
|
|
214
|
+
DeltaTModel.ALGO5: "delta_t_algo5",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _enum(value: object, enum_type: type[_EnumT], name: str) -> _EnumT:
|
|
219
|
+
if not isinstance(value, enum_type):
|
|
220
|
+
raise TypeError(f"{name} must be a {enum_type.__name__} member")
|
|
221
|
+
return value
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _integer(value: object, name: str, minimum: int, maximum: int) -> int:
|
|
225
|
+
if not isinstance(value, int) or isinstance(value, (bool, _IntEnum)):
|
|
226
|
+
raise TypeError(f"{name} must be an integer")
|
|
227
|
+
if value < minimum or value > maximum:
|
|
228
|
+
raise ValueError(f"{name} must be in [{minimum}, {maximum}]")
|
|
229
|
+
return value
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _finite(value: object, name: str) -> float:
|
|
233
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
234
|
+
raise TypeError(f"{name} must be a real number")
|
|
235
|
+
result = float(value)
|
|
236
|
+
if not _math.isfinite(result):
|
|
237
|
+
raise ValueError(f"{name} must be finite")
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _ranged_float(value: object, name: str, minimum: float, maximum: float, *, include_maximum: bool = True) -> float:
|
|
242
|
+
result = _finite(value, name)
|
|
243
|
+
outside = result < minimum or (result > maximum if include_maximum else result >= maximum)
|
|
244
|
+
if outside:
|
|
245
|
+
closing = "]" if include_maximum else ")"
|
|
246
|
+
raise ValueError(f"{name} must be in [{minimum}, {maximum}{closing}")
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _gregorian_month_length(year: int, month: int) -> int:
|
|
251
|
+
if month == 2:
|
|
252
|
+
return 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28
|
|
253
|
+
return 30 if month in (4, 6, 9, 11) else 31
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _civil_date(value: object, name: str, *, maximum_year: int = _MAX_CIVIL_YEAR) -> tuple[int, int, int]:
|
|
257
|
+
if not isinstance(value, GregorianDate):
|
|
258
|
+
raise TypeError(f"{name} must be a GregorianDate")
|
|
259
|
+
year = _integer(value.year, f"{name}.year", 1, maximum_year)
|
|
260
|
+
month = _integer(value.month, f"{name}.month", 1, 12)
|
|
261
|
+
day = _integer(value.day, f"{name}.day", 1, _gregorian_month_length(year, month))
|
|
262
|
+
return year, month, day
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _civil_datetime(value: object, name: str) -> tuple[int, int, int, float]:
|
|
266
|
+
if not isinstance(value, CivilDateTime):
|
|
267
|
+
raise TypeError(f"{name} must be a CivilDateTime")
|
|
268
|
+
year = _integer(value.year, f"{name}.year", 1, _MAX_CIVIL_YEAR)
|
|
269
|
+
month = _integer(value.month, f"{name}.month", 1, 12)
|
|
270
|
+
day = _integer(value.day, f"{name}.day", 1, _gregorian_month_length(year, month))
|
|
271
|
+
fraction = _ranged_float(value.fraction, f"{name}.fraction", 0.0, 1.0, include_maximum=False)
|
|
272
|
+
return year, month, day, fraction
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _lunar_date(value: object, algorithm: LunarAlgorithm, name: str) -> tuple[int, int, bool, int]:
|
|
276
|
+
if not isinstance(value, LunarDate):
|
|
277
|
+
raise TypeError(f"{name} must be a LunarDate")
|
|
278
|
+
start, end = _LUNAR_YEAR_RANGE[algorithm]
|
|
279
|
+
year = _integer(value.year, f"{name}.year", start, end)
|
|
280
|
+
month = _integer(value.month, f"{name}.month", 1, 12)
|
|
281
|
+
day = _integer(value.day, f"{name}.day", 1, 30)
|
|
282
|
+
if not isinstance(value.is_leap, bool):
|
|
283
|
+
raise TypeError(f"{name}.is_leap must be a bool")
|
|
284
|
+
return year, month, value.is_leap, day
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _failure(operation: str, *, recording: bool) -> CelestialError:
|
|
288
|
+
detail = _binding.last_error_text() if recording else ""
|
|
289
|
+
message = detail or f"{operation} failed"
|
|
290
|
+
return CelestialError(operation, message, recorded=bool(detail))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _valid(result: object, operation: str, *, recording: bool = True) -> object:
|
|
294
|
+
if not result.valid:
|
|
295
|
+
raise _failure(operation, recording=recording)
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _civil_result(result: object) -> CivilDateTime:
|
|
300
|
+
return CivilDateTime(result.year, result.month, result.day, result.fraction)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def set_log_verbosity(level: LogVerbosity) -> None:
|
|
304
|
+
"""Set process-wide native log verbosity."""
|
|
305
|
+
checked = _enum(level, LogVerbosity, "level")
|
|
306
|
+
if not _binding.call("set_log_verbosity", _LOG_VERBOSITY[checked]):
|
|
307
|
+
raise _failure("set_log_verbosity", recording=True)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def ut1_to_jd(ut1: CivilDateTime) -> float:
|
|
311
|
+
"""Convert a UT1 civil moment to Julian Day."""
|
|
312
|
+
value = _civil_datetime(ut1, "ut1")
|
|
313
|
+
result = _valid(_binding.call("ut1_to_jd", *value), "ut1_to_jd", recording=True)
|
|
314
|
+
return result.value
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def ut1_to_jde(ut1: CivilDateTime) -> float:
|
|
318
|
+
"""Convert a UT1 civil moment to TT-based Julian Ephemeris Day."""
|
|
319
|
+
value = _civil_datetime(ut1, "ut1")
|
|
320
|
+
result = _valid(_binding.call("ut1_to_jde", *value), "ut1_to_jde", recording=True)
|
|
321
|
+
return result.value
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def jde_to_ut1(jde: float) -> CivilDateTime:
|
|
325
|
+
"""Convert TT-based Julian Ephemeris Day to a UT1 civil moment."""
|
|
326
|
+
result = _valid(_binding.call("jde_to_ut1", _finite(jde, "jde")), "jde_to_ut1", recording=True)
|
|
327
|
+
return _civil_result(result)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def sun_apparent_geocentric_coordinate(jde: float) -> SunCoordinate:
|
|
331
|
+
"""Return the Sun's apparent geocentric ecliptic coordinate at JDE."""
|
|
332
|
+
result = _valid(
|
|
333
|
+
_binding.call("sun_apparent_geocentric_coord", _finite(jde, "jde")),
|
|
334
|
+
"sun_apparent_geocentric_coordinate",
|
|
335
|
+
)
|
|
336
|
+
return SunCoordinate(result.lon, result.lat, result.r)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def moon_apparent_geocentric_coordinate(jde: float) -> MoonCoordinate:
|
|
340
|
+
"""Return the Moon's apparent geocentric ecliptic coordinate at JDE."""
|
|
341
|
+
result = _valid(
|
|
342
|
+
_binding.call("moon_apparent_geocentric_coord", _finite(jde, "jde")),
|
|
343
|
+
"moon_apparent_geocentric_coordinate",
|
|
344
|
+
)
|
|
345
|
+
return MoonCoordinate(result.lon, result.lat, result.r)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def moon_illumination(jde: float) -> MoonIllumination:
|
|
349
|
+
"""Return the Moon's illuminated fraction and elongation at JDE."""
|
|
350
|
+
result = _valid(
|
|
351
|
+
_binding.call("moon_illumination", _finite(jde, "jde")),
|
|
352
|
+
"moon_illumination",
|
|
353
|
+
recording=True,
|
|
354
|
+
)
|
|
355
|
+
return MoonIllumination(result.illumination, result.elongation_deg)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def moon_bright_limb_position_angle(jde: float) -> float:
|
|
359
|
+
"""Return the Moon's bright-limb position angle in degrees."""
|
|
360
|
+
result = _valid(
|
|
361
|
+
_binding.call("moon_position_angle", _finite(jde, "jde")),
|
|
362
|
+
"moon_bright_limb_position_angle",
|
|
363
|
+
recording=True,
|
|
364
|
+
)
|
|
365
|
+
return result.angle_deg
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def moon_phase_moments(year: int, phase: MoonPhase) -> tuple[float, ...]:
|
|
369
|
+
"""Return the JDE moments of a principal Moon phase in a Gregorian year.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
ValueError: If year is outside [1, 32766].
|
|
373
|
+
CelestialError: If the native calculation cannot produce the phase moments.
|
|
374
|
+
"""
|
|
375
|
+
checked_year = _integer(year, "year", 1, _MAX_CALENDAR_YEAR)
|
|
376
|
+
checked_phase = _enum(phase, MoonPhase, "phase")
|
|
377
|
+
total = _ctypes.c_uint32()
|
|
378
|
+
written = _binding.call(
|
|
379
|
+
"moon_phase_moments",
|
|
380
|
+
checked_year,
|
|
381
|
+
_MOON_PHASE[checked_phase],
|
|
382
|
+
_ctypes.byref(total),
|
|
383
|
+
None,
|
|
384
|
+
0,
|
|
385
|
+
)
|
|
386
|
+
if written != 0 or total.value == 0:
|
|
387
|
+
raise _failure("moon_phase_moments", recording=True)
|
|
388
|
+
expected = total.value
|
|
389
|
+
slots = (_ctypes.c_double * expected)()
|
|
390
|
+
written = _binding.call(
|
|
391
|
+
"moon_phase_moments",
|
|
392
|
+
checked_year,
|
|
393
|
+
_MOON_PHASE[checked_phase],
|
|
394
|
+
_ctypes.byref(total),
|
|
395
|
+
slots,
|
|
396
|
+
expected,
|
|
397
|
+
)
|
|
398
|
+
if written != expected:
|
|
399
|
+
raise _failure("moon_phase_moments", recording=True)
|
|
400
|
+
return tuple(slots[:written])
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def solar_longitude_roots(year: int, longitude_deg: float) -> tuple[float, ...]:
|
|
404
|
+
"""Return JDEs when the Sun reaches an apparent geocentric longitude.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
ValueError: If year is outside [1, 32766].
|
|
408
|
+
CelestialError: If the native calculation cannot produce the roots.
|
|
409
|
+
"""
|
|
410
|
+
checked_year = _integer(year, "year", 1, _MAX_CALENDAR_YEAR)
|
|
411
|
+
longitude = _ranged_float(longitude_deg, "longitude_deg", 0.0, 360.0, include_maximum=False)
|
|
412
|
+
discriminant = _valid(
|
|
413
|
+
_binding.call("solar_lon_root_discriminant", checked_year, longitude),
|
|
414
|
+
"solar_longitude_roots",
|
|
415
|
+
)
|
|
416
|
+
if discriminant.count == 0:
|
|
417
|
+
return ()
|
|
418
|
+
slots = (_ctypes.c_double * discriminant.count)()
|
|
419
|
+
written = _binding.call("solar_lon_roots", checked_year, longitude, slots, discriminant.count)
|
|
420
|
+
if written != discriminant.count:
|
|
421
|
+
raise _failure("solar_longitude_roots", recording=True)
|
|
422
|
+
return tuple(slots[:written])
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def new_moons_after(jde: float, count: int) -> tuple[float, ...]:
|
|
426
|
+
"""Return the requested number of new-moon JDEs after a starting JDE.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ValueError: If count is outside [0, 4096].
|
|
430
|
+
CelestialError: If the native calculation cannot produce the requested moments.
|
|
431
|
+
"""
|
|
432
|
+
start = _finite(jde, "jde")
|
|
433
|
+
checked_count = _integer(count, "count", 0, _MAX_NEW_MOON_COUNT)
|
|
434
|
+
if checked_count == 0:
|
|
435
|
+
return ()
|
|
436
|
+
slots = (_ctypes.c_double * checked_count)()
|
|
437
|
+
written = _binding.call("new_moons_after_jde", start, slots, checked_count)
|
|
438
|
+
if written != checked_count:
|
|
439
|
+
raise _failure("new_moons_after", recording=True)
|
|
440
|
+
return tuple(slots[:written])
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def new_moons_in_year(year: int) -> tuple[float, ...]:
|
|
444
|
+
"""Return all new-moon JDEs in a Gregorian year.
|
|
445
|
+
|
|
446
|
+
Raises:
|
|
447
|
+
ValueError: If year is outside [1, 32766].
|
|
448
|
+
CelestialError: If the native calculation cannot produce the moments.
|
|
449
|
+
"""
|
|
450
|
+
checked_year = _integer(year, "year", 1, _MAX_CALENDAR_YEAR)
|
|
451
|
+
total = _ctypes.c_uint32()
|
|
452
|
+
written = _binding.call("new_moons_in_year", checked_year, _ctypes.byref(total), None, 0)
|
|
453
|
+
if written != 0 or total.value == 0:
|
|
454
|
+
raise _failure("new_moons_in_year", recording=True)
|
|
455
|
+
expected = total.value
|
|
456
|
+
slots = (_ctypes.c_double * expected)()
|
|
457
|
+
written = _binding.call("new_moons_in_year", checked_year, _ctypes.byref(total), slots, expected)
|
|
458
|
+
if written != expected:
|
|
459
|
+
raise _failure("new_moons_in_year", recording=True)
|
|
460
|
+
return tuple(slots[:written])
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def equation_of_time(jde: float) -> float:
|
|
464
|
+
"""Return apparent minus mean solar time in degrees of hour angle."""
|
|
465
|
+
result = _valid(_binding.call("equation_of_time", _finite(jde, "jde")), "equation_of_time")
|
|
466
|
+
return result.value
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def apparent_solar_time(utc: CivilDateTime, longitude_deg: float) -> CivilDateTime:
|
|
470
|
+
"""Convert civil UTC to local apparent solar time at an east-positive longitude in degrees."""
|
|
471
|
+
value = _civil_datetime(utc, "utc")
|
|
472
|
+
longitude = _ranged_float(longitude_deg, "longitude_deg", -180.0, 180.0)
|
|
473
|
+
result = _valid(_binding.call("apparent_solar_time", *value, longitude), "apparent_solar_time")
|
|
474
|
+
return _civil_result(result)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def local_apparent_sidereal_time(jd_ut1: float, longitude_deg: float) -> float:
|
|
478
|
+
"""Return local apparent sidereal time in degrees at an east-positive longitude.
|
|
479
|
+
|
|
480
|
+
Raises:
|
|
481
|
+
CelestialError: If the native calculation cannot produce sidereal time.
|
|
482
|
+
"""
|
|
483
|
+
jd = _finite(jd_ut1, "jd_ut1")
|
|
484
|
+
longitude = _ranged_float(longitude_deg, "longitude_deg", -180.0, 180.0)
|
|
485
|
+
result = _valid(
|
|
486
|
+
_binding.call("local_apparent_sidereal_time", jd, longitude),
|
|
487
|
+
"local_apparent_sidereal_time",
|
|
488
|
+
recording=True,
|
|
489
|
+
)
|
|
490
|
+
return result.value
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def jieqi_moment(year: int, jieqi: Jieqi) -> JieqiMoment:
|
|
494
|
+
"""Return the UT1 civil moment of a Jieqi in a Gregorian year.
|
|
495
|
+
|
|
496
|
+
Raises:
|
|
497
|
+
ValueError: If year is outside [401, 32766].
|
|
498
|
+
CelestialError: If the native calculation cannot produce the moment.
|
|
499
|
+
"""
|
|
500
|
+
checked_year = _integer(year, "year", _MIN_JIEQI_YEAR, _MAX_CALENDAR_YEAR)
|
|
501
|
+
checked_jieqi = _enum(jieqi, Jieqi, "jieqi")
|
|
502
|
+
result = _valid(_binding.call("query_jieqi_moment", checked_year, checked_jieqi.value), "jieqi_moment")
|
|
503
|
+
return JieqiMoment(Jieqi(result.jq_idx), CivilDateTime(result.y, result.m, result.d, result.frac))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def jieqi_name(jieqi: Jieqi) -> str:
|
|
507
|
+
"""Return the Chinese name of a Jieqi."""
|
|
508
|
+
checked_jieqi = _enum(jieqi, Jieqi, "jieqi")
|
|
509
|
+
buffer = _ctypes.create_string_buffer(_JIEQI_NAME_BYTES)
|
|
510
|
+
if not _binding.call("get_jieqi_name", checked_jieqi.value, buffer, len(buffer)):
|
|
511
|
+
raise _failure("jieqi_name", recording=True)
|
|
512
|
+
return buffer.value.decode("utf-8")
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def supported_lunar_year_range(algorithm: LunarAlgorithm) -> LunarYearRange:
|
|
516
|
+
"""Return the inclusive Lunar year range supported by an algorithm."""
|
|
517
|
+
checked = _enum(algorithm, LunarAlgorithm, "algorithm")
|
|
518
|
+
result = _valid(
|
|
519
|
+
_binding.call("get_supported_lunar_year_range", _LUNAR_ALGORITHM[checked]),
|
|
520
|
+
"supported_lunar_year_range",
|
|
521
|
+
)
|
|
522
|
+
return LunarYearRange(result.start, result.end)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def lunar_year_info(algorithm: LunarAlgorithm, year: int) -> LunarYearInfo:
|
|
526
|
+
"""Return the first day and month lengths of a Lunar year.
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
ValueError: If year is outside the selected algorithm's supported range.
|
|
530
|
+
CelestialError: If the native calculation cannot produce the year information.
|
|
531
|
+
"""
|
|
532
|
+
checked = _enum(algorithm, LunarAlgorithm, "algorithm")
|
|
533
|
+
start, end = _LUNAR_YEAR_RANGE[checked]
|
|
534
|
+
checked_year = _integer(year, "year", start, end)
|
|
535
|
+
result = _valid(_binding.call("get_lunar_year_info", _LUNAR_ALGORITHM[checked], checked_year), "lunar_year_info")
|
|
536
|
+
month_count = 12 if result.leap_month == 0 else 13
|
|
537
|
+
month_lengths = tuple(30 if result.month_len & (1 << index) else 29 for index in range(month_count))
|
|
538
|
+
leap_month = None if result.leap_month == 0 else result.leap_month
|
|
539
|
+
return LunarYearInfo(GregorianDate(result.year, result.month, result.day), leap_month, month_lengths)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def gregorian_to_lunar(algorithm: LunarAlgorithm, date: GregorianDate) -> LunarDate:
|
|
543
|
+
"""Convert a Gregorian date to a Chinese Lunar date.
|
|
544
|
+
|
|
545
|
+
Raises:
|
|
546
|
+
CelestialError: If date cannot be represented by the selected algorithm.
|
|
547
|
+
"""
|
|
548
|
+
checked = _enum(algorithm, LunarAlgorithm, "algorithm")
|
|
549
|
+
value = _civil_date(date, "date")
|
|
550
|
+
result = _valid(_binding.call("gregorian_to_lunar", _LUNAR_ALGORITHM[checked], *value), "gregorian_to_lunar")
|
|
551
|
+
return LunarDate(result.year, result.month, result.day, result.is_leap)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def lunar_to_gregorian(algorithm: LunarAlgorithm, date: LunarDate) -> GregorianDate:
|
|
555
|
+
"""Convert a Chinese Lunar date to a Gregorian date.
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
ValueError: If date.year is outside the selected algorithm's supported range.
|
|
559
|
+
CelestialError: If date does not exist in the selected algorithm.
|
|
560
|
+
"""
|
|
561
|
+
checked = _enum(algorithm, LunarAlgorithm, "algorithm")
|
|
562
|
+
year, month, is_leap, day = _lunar_date(date, checked, "date")
|
|
563
|
+
result = _valid(
|
|
564
|
+
_binding.call("lunar_to_gregorian", _LUNAR_ALGORITHM[checked], year, month, is_leap, day),
|
|
565
|
+
"lunar_to_gregorian",
|
|
566
|
+
)
|
|
567
|
+
return GregorianDate(result.year, result.month, result.day)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def delta_t(year: float, model: DeltaTModel = DeltaTModel.DEFAULT) -> float:
|
|
571
|
+
"""Return ΔT in seconds for a decimal Gregorian year.
|
|
572
|
+
|
|
573
|
+
ALGO1 requires year >= -4000, ALGO3 requires year < 3000, and ALGO4 requires year < 2035. The other models have no
|
|
574
|
+
model-specific year bound.
|
|
575
|
+
|
|
576
|
+
Raises:
|
|
577
|
+
ValueError: If year is outside the selected model's bounds above.
|
|
578
|
+
CelestialError: If the native calculation fails.
|
|
579
|
+
"""
|
|
580
|
+
decimal_year = _finite(year, "year")
|
|
581
|
+
checked_model = _enum(model, DeltaTModel, "model")
|
|
582
|
+
if checked_model is DeltaTModel.ALGO1 and decimal_year < -4000.0:
|
|
583
|
+
raise ValueError("year must be at least -4000 for DeltaTModel.ALGO1")
|
|
584
|
+
if checked_model is DeltaTModel.ALGO3 and decimal_year >= 3000.0:
|
|
585
|
+
raise ValueError("year must be less than 3000 for DeltaTModel.ALGO3")
|
|
586
|
+
if checked_model is DeltaTModel.ALGO4 and decimal_year >= 2035.0:
|
|
587
|
+
raise ValueError("year must be less than 2035 for DeltaTModel.ALGO4")
|
|
588
|
+
result = _valid(_binding.call(_DELTA_T_EXPORT[checked_model], decimal_year), "delta_t")
|
|
589
|
+
return result.value
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
__all__ = [
|
|
593
|
+
"CelestialError",
|
|
594
|
+
"CivilDateTime",
|
|
595
|
+
"DeltaTModel",
|
|
596
|
+
"GregorianDate",
|
|
597
|
+
"Jieqi",
|
|
598
|
+
"JieqiMoment",
|
|
599
|
+
"LogVerbosity",
|
|
600
|
+
"LunarAlgorithm",
|
|
601
|
+
"LunarDate",
|
|
602
|
+
"LunarYearInfo",
|
|
603
|
+
"LunarYearRange",
|
|
604
|
+
"MoonCoordinate",
|
|
605
|
+
"MoonIllumination",
|
|
606
|
+
"MoonPhase",
|
|
607
|
+
"SunCoordinate",
|
|
608
|
+
"__version__",
|
|
609
|
+
"apparent_solar_time",
|
|
610
|
+
"delta_t",
|
|
611
|
+
"equation_of_time",
|
|
612
|
+
"gregorian_to_lunar",
|
|
613
|
+
"jde_to_ut1",
|
|
614
|
+
"jieqi_moment",
|
|
615
|
+
"jieqi_name",
|
|
616
|
+
"local_apparent_sidereal_time",
|
|
617
|
+
"lunar_to_gregorian",
|
|
618
|
+
"lunar_year_info",
|
|
619
|
+
"moon_apparent_geocentric_coordinate",
|
|
620
|
+
"moon_bright_limb_position_angle",
|
|
621
|
+
"moon_illumination",
|
|
622
|
+
"moon_phase_moments",
|
|
623
|
+
"new_moons_after",
|
|
624
|
+
"new_moons_in_year",
|
|
625
|
+
"set_log_verbosity",
|
|
626
|
+
"solar_longitude_roots",
|
|
627
|
+
"sun_apparent_geocentric_coordinate",
|
|
628
|
+
"supported_lunar_year_range",
|
|
629
|
+
"ut1_to_jd",
|
|
630
|
+
"ut1_to_jde",
|
|
631
|
+
]
|