solarmoonpy 0.1.0__tar.gz → 1.0.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarmoonpy
3
- Version: 0.1.0
3
+ Version: 1.0.1
4
4
  Summary: Precise solar and lunar calculations for astronomical applications
5
5
  Author-email: figorr <jdcuartero@yahoo.es>
6
6
  License: Apache License
@@ -220,5 +220,18 @@ Description-Content-Type: text/markdown
220
220
  License-File: LICENSE
221
221
  Dynamic: license-file
222
222
 
223
- # solarmoonpy
224
- solar and moon calculations
223
+ # ☀️🌙 solarmoonpy
224
+ Solar and moon calculations for Meteocat Home Assistant integration.
225
+
226
+ **solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
227
+
228
+ This project enables you to obtain information such as sunrise, sunset, moon phases, and more, for advanced automations in your home automation system.
229
+
230
+ ## 🚀 Features
231
+
232
+ - ☀️ Solar position calculations (sunrise, sunset, zenith, etc.).
233
+ - 🌙 Moon phase and position calculations.
234
+ - Integration with meteorological data from Meteocat.
235
+ - Compatible with Home Assistant for automations based on solar and lunar events.
236
+ - Lightweight, easy to use, and well-documented.
237
+ - Configurable for different geographic locations.
@@ -0,0 +1,15 @@
1
+ # ☀️🌙 solarmoonpy
2
+ Solar and moon calculations for Meteocat Home Assistant integration.
3
+
4
+ **solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
5
+
6
+ This project enables you to obtain information such as sunrise, sunset, moon phases, and more, for advanced automations in your home automation system.
7
+
8
+ ## 🚀 Features
9
+
10
+ - ☀️ Solar position calculations (sunrise, sunset, zenith, etc.).
11
+ - 🌙 Moon phase and position calculations.
12
+ - Integration with meteorological data from Meteocat.
13
+ - Compatible with Home Assistant for automations based on solar and lunar events.
14
+ - Lightweight, easy to use, and well-documented.
15
+ - Configurable for different geographic locations.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "solarmoonpy"
7
- version = "0.1.0"
7
+ version = "1.0.1"
8
8
  description = "Precise solar and lunar calculations for astronomical applications"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "figorr", email = "jdcuartero@yahoo.es" }]
@@ -9,11 +9,12 @@ https://github.com/figorr/solarmoonpy
9
9
 
10
10
  # solarmoonpy/__init__.py
11
11
  from .version import __version__
12
- from .moon import moon_phase, moon_rise_set, illuminated_percentage, moon_distance, moon_angular_diameter
12
+ from .moon import moon_phase, moon_day, moon_rise_set, illuminated_percentage, moon_distance, moon_angular_diameter
13
13
  from .location import Location, LocationInfo
14
14
 
15
15
  __all__ = [
16
- "moon_phase",
16
+ "moon_phase",
17
+ "moon_day",
17
18
  "moon_rise_set",
18
19
  "illuminated_percentage",
19
20
  "moon_distance",
@@ -0,0 +1,377 @@
1
+ from datetime import date, datetime, timedelta, timezone
2
+ import math
3
+ from typing import Optional, Tuple
4
+ from .sun import julianday, julianday_to_juliancentury, sun_apparent_long, sun_distance
5
+
6
+ _SYNODIC_MONTH = 29.530588853
7
+ AU = 149597870.7
8
+ JD_REF_PHASE = 2423436.0 # para moon_phase
9
+ JD_REF_LUNATION = 2423407.51818 # para lunation_number
10
+
11
+ def moon_phase(date_utc: date) -> float:
12
+ """
13
+ Devuelve los días transcurridos desde la última Luna Nueva.
14
+ Basado en el algoritmo de Chapront-Touzé (usado por timeanddate/NASA).
15
+ """
16
+ from datetime import datetime, timezone
17
+
18
+ # Convertimos a datetime UTC (mediodía del día solicitado)
19
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, tzinfo=timezone.utc)
20
+ jd = julianday(dt)
21
+
22
+ # Fracción del ciclo (0.0 = luna nueva, 0.5 = llena, 1.0 = nueva)
23
+ phase = ((jd - JD_REF_PHASE) / _SYNODIC_MONTH) % 1.0
24
+
25
+ # Días desde la última luna nueva
26
+ return phase * _SYNODIC_MONTH
27
+
28
+ def moon_day(date_utc: date, tol_hours: float = 0.01) -> int:
29
+ """
30
+ Calcula el día lunar 0–29 usando la última luna nueva exacta calculada por elongación.
31
+ Usa el fin del día UTC para alinear con convenciones de timeanddate/NASA.
32
+ tol_hours: precisión en horas (mantenido en 0.01 para consistencia).
33
+ """
34
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 23, 59, tzinfo=timezone.utc)
35
+ last_new_moon = find_last_phase_exact(date_utc, target=0.0)
36
+ age_days = (dt - last_new_moon).total_seconds() / 86400.0
37
+ if age_days < 0:
38
+ day = 0
39
+ else:
40
+ day = int(math.floor(age_days))
41
+ if day > 29:
42
+ day = 29
43
+ return day
44
+
45
+ def illuminated_percentage(date_utc: date) -> float:
46
+ """
47
+ Calcula el porcentaje de luna iluminada para una fecha UTC (versión precisa).
48
+ Usa posiciones eclípticas reales de Sol y Luna (Meeus) para la elongación y corrección por distancias.
49
+ Args:
50
+ date_utc (date): Fecha en UTC.
51
+ Returns:
52
+ float: Porcentaje de superficie lunar iluminada (0–100).
53
+ """
54
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
55
+ jd = julianday(dt)
56
+ jc = julianday_to_juliancentury(jd)
57
+ lambda_m, beta_m, R, delta_psi = _moon_ecliptic_position(jd)
58
+ lambda_s = sun_apparent_long(jc)
59
+ r_sun = sun_distance(jc)
60
+ diff = _normalize_angle(lambda_m - lambda_s)
61
+ elong_rad = math.radians(diff)
62
+ beta_rad = math.radians(beta_m)
63
+ cos_E = math.cos(beta_rad) * math.cos(elong_rad)
64
+ d = r_sun * AU
65
+ s2 = d**2 + R**2 - 2 * d * R * cos_E
66
+ s = math.sqrt(s2)
67
+ cos_i = (s**2 + R**2 - d**2) / (2 * s * R)
68
+ illum_fraction = (1 + cos_i) / 2
69
+ return illum_fraction * 100.0
70
+
71
+ def _normalize_angle(angle: float) -> float:
72
+ """Normaliza ángulo a 0-360 grados."""
73
+ return angle % 360.0
74
+
75
+ # Tabla completa 47.A para sigma_l y sigma_r (de PyMeeus/Meeus)
76
+ SIGMA_LR_TABLE = [
77
+ [0, 0, 1, 0, 6288774.0, -20905355.0], [2, 0, -1, 0, 1274027.0, -3699111.0],
78
+ [2, 0, 0, 0, 658314.0, -2955968.0], [0, 0, 2, 0, 213618.0, -569925.0],
79
+ [0, 1, 0, 0, -185116.0, 48888.0], [0, 0, 0, 2, -114332.0, -3149.0],
80
+ [2, 0, -2, 0, 58793.0, 246158.0], [2, -1, -1, 0, 57066.0, -152138.0],
81
+ [2, 0, 1, 0, 53322.0, -170733.0], [2, -1, 0, 0, 45758.0, -204586.0],
82
+ [0, 1, -1, 0, -40923.0, -129620.0], [1, 0, 0, 0, -34720.0, 108743.0],
83
+ [0, 1, 1, 0, -30383.0, 104755.0], [2, 0, 0, -2, 15327.0, 10321.0],
84
+ [0, 0, 1, 2, -12528.0, 0.0], [0, 0, 1, -2, 10980.0, 79661.0],
85
+ [4, 0, -1, 0, 10675.0, -34782.0], [0, 0, 3, 0, 10034.0, -23210.0],
86
+ [4, 0, -2, 0, 8548.0, -21636.0], [2, 1, -1, 0, -7888.0, 24208.0],
87
+ [2, 1, 0, 0, -6766.0, 30824.0], [1, 0, -1, 0, -5163.0, -8379.0],
88
+ [1, 1, 0, 0, 4987.0, -16675.0], [2, -1, 1, 0, 4036.0, -12831.0],
89
+ [2, 0, 2, 0, 3994.0, -10445.0], [4, 0, 0, 0, 3861.0, -11650.0],
90
+ [2, 0, -3, 0, 3665.0, 14403.0], [0, 1, -2, 0, -2689.0, -7003.0],
91
+ [2, 0, -1, 2, -2602.0, 0.0], [2, -1, -2, 0, 2390.0, 10056.0],
92
+ [1, 0, 1, 0, -2348.0, 6322.0], [2, -2, 0, 0, 2236.0, -9884.0],
93
+ [0, 1, 2, 0, -2120.0, 5751.0], [0, 2, 0, 0, -2069.0, 0.0],
94
+ [2, -2, -1, 0, 2048.0, -4950.0], [2, 0, 1, -2, -1773.0, 4130.0],
95
+ [2, 0, 0, 2, -1595.0, 0.0], [4, -1, -1, 0, 1215.0, -3958.0],
96
+ [0, 0, 2, 2, -1110.0, 0.0], [3, 0, -1, 0, -892.0, 3258.0],
97
+ [2, 1, 1, 0, -810.0, 2616.0], [4, -1, -2, 0, 759.0, -1897.0],
98
+ [0, 2, -1, 0, -713.0, -2117.0], [2, 2, -1, 0, -700.0, 2354.0],
99
+ [2, 1, -2, 0, 691.0, 0.0], [2, -1, 0, -2, 596.0, 0.0],
100
+ [4, 0, 1, 0, 549.0, -1423.0], [0, 0, 4, 0, 537.0, -1117.0],
101
+ [4, -1, 0, 0, 520.0, -1571.0], [1, 0, -2, 0, -487.0, -1739.0],
102
+ [2, 1, 0, -2, -399.0, 0.0], [0, 0, 2, -2, -381.0, -4421.0],
103
+ [1, 1, 1, 0, 351.0, 0.0], [3, 0, -2, 0, -340.0, 0.0],
104
+ [4, 0, -3, 0, 330.0, 0.0], [2, -1, 2, 0, 327.0, 0.0],
105
+ [0, 2, 1, 0, -323.0, 1165.0], [1, 1, -1, 0, 299.0, 0.0],
106
+ [2, 0, 3, 0, 294.0, 0.0], [2, 0, -1, -2, 0.0, 8752.0]
107
+ ]
108
+
109
+ # Tabla completa 47.B para sigma_b (de PyMeeus/Meeus)
110
+ SIGMA_B_TABLE = [
111
+ [0, 0, 0, 1, 5128122.0], [0, 0, 1, 1, 280602.0], [0, 0, 1, -1, 277693.0],
112
+ [2, 0, 0, -1, 173237.0], [2, 0, -1, 1, 55413.0], [2, 0, -1, -1, 46271.0],
113
+ [2, 0, 0, 1, 32573.0], [0, 0, 2, 1, 17198.0], [2, 0, 1, -1, 9266.0],
114
+ [0, 0, 2, -1, 8822.0], [2, -1, 0, -1, 8216.0], [2, 0, -2, -1, 4324.0],
115
+ [2, 0, 1, 1, 4200.0], [2, 1, 0, -1, -3359.0], [2, -1, -1, 1, 2463.0],
116
+ [2, -1, 0, 1, 2211.0], [2, -1, -1, -1, 2065.0], [0, 1, -1, -1, -1870.0],
117
+ [4, 0, -1, -1, 1828.0], [0, 1, 0, 1, -1794.0], [0, 0, 0, 3, -1749.0],
118
+ [0, 1, -1, 1, -1565.0], [1, 0, 0, 1, -1491.0], [0, 1, 1, 1, -1475.0],
119
+ [0, 1, 1, -1, -1410.0], [0, 1, 0, -1, -1344.0], [1, 0, 0, -1, -1335.0],
120
+ [0, 0, 3, 1, 1107.0], [4, 0, 0, -1, 1021.0], [4, 0, -1, 1, 833.0],
121
+ [0, 0, 1, -3, 777.0], [4, 0, -2, 1, 671.0], [2, 0, 0, -3, 607.0],
122
+ [2, 0, 2, -1, 596.0], [2, -1, 1, -1, 491.0], [2, 0, -2, 1, -451.0],
123
+ [0, 0, 3, -1, 439.0], [2, 0, 2, 1, 422.0], [2, 0, -3, -1, 421.0],
124
+ [2, 1, -1, 1, -366.0], [2, 1, 0, 1, -351.0], [4, 0, 0, 1, 331.0],
125
+ [2, -1, 1, 1, 315.0], [2, -2, 0, -1, 302.0], [0, 0, 1, 3, -283.0],
126
+ [2, 1, 1, -1, -229.0], [1, 1, 0, -1, 223.0], [1, 1, 0, 1, 223.0],
127
+ [0, 1, -2, -1, -220.0], [2, 1, -1, -1, -220.0], [1, 0, 1, 1, -185.0],
128
+ [2, -1, -2, -1, 181.0], [0, 1, 2, 1, -177.0], [4, 0, -2, -1, 176.0],
129
+ [4, -1, -1, -1, 166.0], [1, 0, 1, -1, -164.0], [4, 0, 1, -1, 132.0],
130
+ [1, 0, -1, -1, -119.0], [4, -1, 0, -1, 115.0], [2, -2, 0, 1, 107.0]
131
+ ]
132
+
133
+ def _periodic_terms(D, M, Mp, F, T):
134
+ """Calcula sigma_l, sigma_r, sigma_b con tablas completas y factor E."""
135
+ E = 1 - 0.002516 * T - 0.0000074 * T**2
136
+ E2 = E * E
137
+ sigma_l = 0.0
138
+ sigma_r = 0.0
139
+ sigma_b = 0.0
140
+
141
+ for d_coef, m, mp, f, l, r in SIGMA_LR_TABLE:
142
+ factor = 1.0
143
+ if abs(m) == 1:
144
+ factor = E
145
+ elif abs(m) == 2:
146
+ factor = E2
147
+ arg = d_coef * D + m * M + mp * Mp + f * F
148
+ sigma_l += l * math.sin(math.radians(arg)) * factor
149
+ sigma_r += r * math.cos(math.radians(arg)) * factor
150
+
151
+ for d_coef, m, mp, f, b in SIGMA_B_TABLE:
152
+ factor = 1.0
153
+ if abs(m) == 1:
154
+ factor = E
155
+ elif abs(m) == 2:
156
+ factor = E2
157
+ arg = d_coef * D + m * M + mp * Mp + f * F
158
+ sigma_b += b * math.sin(math.radians(arg)) * factor
159
+
160
+ sigma_l /= 1000000.0 # a grados
161
+ sigma_r *= 0.001 # a km
162
+ sigma_b /= 1000000.0 # a grados
163
+ return sigma_l, sigma_r, sigma_b
164
+
165
+ def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
166
+ """Calcula longitud eclíptica aparente, latitud, distancia y delta_psi de la Luna."""
167
+ T = (jd - 2451545.0) / 36525.0
168
+ Lp = _normalize_angle(218.3164477 + 481267.88123421 * T - 0.0015786 * T**2 + T**3 / 538841.0 - T**4 / 65194000.0)
169
+ D = _normalize_angle(297.8501921 + 445267.1114034 * T - 0.0018819 * T**2 + T**3 / 545868.0 - T**4 / 113065000.0)
170
+ M = _normalize_angle(357.5291092 + 35999.0502909 * T - 0.0001536 * T**2 + T**3 / 24490000.0)
171
+ Mp = _normalize_angle(134.9633964 + 477198.8675055 * T + 0.0087414 * T**2 + T**3 / 69699.0 - T**4 / 14712000.0)
172
+ F = _normalize_angle(93.2720950 + 483202.0175238 * T - 0.0036539 * T**2 - T**3 / 3526000.0 + T**4 / 863310000.0)
173
+ Omega = _normalize_angle(125.04452 - 1934.136261 * T + 0.0020708 * T**2 + T**3 / 450000.0)
174
+
175
+ sigma_l, sigma_r, sigma_b = _periodic_terms(D, M, Mp, F, T)
176
+
177
+ lambda_ = Lp + sigma_l + 0.003958 * math.sin(math.radians(119.75 + 131.849 * T)) + 0.000319 * math.sin(math.radians(53.09 + 479264.290 * T)) + 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T))
178
+ beta = sigma_b - 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T - 2 * F))
179
+ R = 385000.529 + sigma_r
180
+ delta_psi = (-17.20 * math.sin(math.radians(Omega)) - 1.32 * math.sin(math.radians(2 * (Lp - F))) + 0.23 * math.sin(math.radians(2 * Lp)) + 0.21 * math.sin(math.radians(2 * Omega))) / 3600.0
181
+ lambda_ += delta_psi
182
+ return lambda_, beta, R, delta_psi
183
+
184
+ def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[float, float]:
185
+ """Calcula alt, h0 para un tiempo dado t (ra no usado)."""
186
+ jd = julianday(t.date()) + (t.hour + t.minute / 60 + t.second / 3600) / 24
187
+ T = (jd - 2451545.0) / 36525.0
188
+ lambda_, beta, R, delta_psi = _moon_ecliptic_position(jd)
189
+
190
+ par = math.asin(6378.14 / R)
191
+ eps = math.radians(23.439281 - 0.0000004 * T)
192
+ lambda_r = math.radians(lambda_)
193
+ beta_r = math.radians(beta)
194
+ ra = math.atan2(math.sin(lambda_r) * math.cos(eps) - math.tan(beta_r) * math.sin(eps), math.cos(lambda_r))
195
+ dec = math.asin(math.sin(beta_r) * math.cos(eps) + math.cos(beta_r) * math.sin(eps) * math.sin(lambda_r))
196
+ sd = 0.2725076 * par
197
+ ref = math.radians(0.5667)
198
+ h0 = par - sd - ref
199
+ gmst = (280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * T**2 - T**3 / 38710000.0) % 360.0
200
+ lst = math.radians((gmst + lon) % 360.0)
201
+ ha = lst - ra
202
+ if ha < -math.pi: ha += 2 * math.pi
203
+ elif ha > math.pi: ha -= 2 * math.pi
204
+ alt = math.asin(math.sin(lat_r) * math.sin(dec) + math.cos(lat_r) * math.cos(dec) * math.cos(ha))
205
+ return alt, h0
206
+
207
+ def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[datetime], Optional[datetime]]:
208
+ """
209
+ Cálculo preciso de moonrise y moonset con series completas de Meeus (precisión ~1-2 min).
210
+ Devuelve tuplas UTC (datetime tz-aware) o None si no ocurre en ese día.
211
+ """
212
+ lat_r = math.radians(lat)
213
+ rise = None
214
+ set_ = None
215
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, tzinfo=timezone.utc)
216
+ prev_t = None
217
+ prev_alt = None
218
+
219
+ step_grueso = 1.0
220
+ intervalos_rise = []
221
+ intervalos_set = []
222
+ for i in range(int(24 / step_grueso) + 1):
223
+ hour = i * step_grueso
224
+ t = dt + timedelta(hours=hour)
225
+ alt, h0 = _calculate_position_and_alt(t, lat_r, lon)
226
+ if prev_alt is not None:
227
+ den = alt - prev_alt
228
+ if abs(den) < 1e-12:
229
+ continue
230
+ if prev_alt < h0 and alt >= h0:
231
+ intervalos_rise.append((prev_t, t, prev_alt, alt))
232
+ elif prev_alt >= h0 and alt < h0:
233
+ intervalos_set.append((prev_t, t, prev_alt, alt))
234
+ prev_t = t
235
+ prev_alt = alt
236
+
237
+ def refine_event(start_t: datetime, end_t: datetime, start_alt: float, end_alt: float, is_rise: bool) -> datetime:
238
+ for _ in range(10):
239
+ mid_t = start_t + (end_t - start_t) / 2
240
+ mid_alt, mid_h0 = _calculate_position_and_alt(mid_t, lat_r, lon)
241
+ den = mid_alt - start_alt if is_rise else mid_alt - end_alt
242
+ if abs(den) < 1e-12:
243
+ return mid_t
244
+ if (is_rise and mid_alt < mid_h0) or (not is_rise and mid_alt >= mid_h0):
245
+ start_t, start_alt = mid_t, mid_alt
246
+ else:
247
+ end_t, end_alt = mid_t, mid_alt
248
+ return start_t + (end_t - start_t) / 2
249
+
250
+ if intervalos_rise and rise is None:
251
+ start_t, end_t, start_alt, end_alt = intervalos_rise[0]
252
+ rise = refine_event(start_t, end_t, start_alt, end_alt, True)
253
+ if intervalos_set and set_ is None:
254
+ start_t, end_t, start_alt, end_alt = intervalos_set[0]
255
+ set_ = refine_event(start_t, end_t, start_alt, end_alt, False)
256
+ return rise, set_
257
+
258
+ def moon_distance(date_utc: date) -> float:
259
+ """
260
+ Calcula la distancia Tierra-Luna en kilómetros para una fecha UTC.
261
+ Args:
262
+ date_utc (date): Fecha en UTC.
263
+ Returns:
264
+ float: Distancia en kilómetros.
265
+ """
266
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
267
+ jd = julianday(dt)
268
+ _, _, R, _ = _moon_ecliptic_position(jd)
269
+ return R
270
+
271
+ def moon_angular_diameter(date_utc: date) -> float:
272
+ """
273
+ Calcula el diámetro angular de la Luna en arcosegundos para una fecha UTC.
274
+ Args:
275
+ date_utc (date): Fecha en UTC.
276
+ Returns:
277
+ float: Diámetro angular en arcosegundos.
278
+ """
279
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
280
+ jd = julianday(dt)
281
+ _, _, R, _ = _moon_ecliptic_position(jd)
282
+ par = math.asin(6378.14 / R)
283
+ sd = 0.2725076 * par
284
+ return 2 * math.degrees(sd) * 3600
285
+
286
+ def lunation_number(date_utc: date) -> int:
287
+ """
288
+ Calcula el número de lunación según el sistema de TimeandDate/NASA.
289
+ """
290
+ last_new_moon = find_last_phase_exact(date_utc, target=0.0)
291
+ jd_new_moon = julianday(last_new_moon)
292
+
293
+ lun = (jd_new_moon - JD_REF_LUNATION) / _SYNODIC_MONTH
294
+ return round(lun)
295
+
296
+ def find_last_phase_exact(date_utc: date, target: float = 0.0) -> datetime:
297
+ """
298
+ Devuelve la fase lunar exacta ANTERIOR o coincidente a date_utc.
299
+ target: 0.0 para new_moon, 90.0 para first_quarter, 180.0 para full_moon, 270.0 para last_quarter.
300
+ Método:
301
+ 1) Usa moon_phase(date_utc) para estimar la fecha/hora aproximada desde la última Luna Nueva, luego ajusta para target.
302
+ 2) Construye un intervalo alrededor de esa estimación y asegura que contiene un cruce
303
+ (elongation - target cambia de signo).
304
+ 3) Refina con bisección hasta ~1 segundo de precisión.
305
+ Este método evita saltos de ciclo porque la estimación inicial está basada en la elongación.
306
+ """
307
+ # punto central (mediodía UTC) y estimación inicial usando moon_phase para new_moon
308
+ dt_mid = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, tzinfo=timezone.utc)
309
+ # phase_days es el número de días desde la última Luna Nueva (según moon_phase)
310
+ phase_days = moon_phase(date_utc)
311
+ approx_from_new = (target / 360.0) * _SYNODIC_MONTH # Días aproximados desde new_moon al target
312
+ approx_dt = dt_mid - timedelta(days=phase_days) + timedelta(days=approx_from_new)
313
+
314
+ def elongation_dt(t: datetime) -> float:
315
+ jd = julianday(t)
316
+ jc = julianday_to_juliancentury(jd)
317
+ lambda_m, _, _, _ = _moon_ecliptic_position(jd)
318
+ lambda_s = sun_apparent_long(jc)
319
+ # normalizamos a -180..+180 alrededor de target
320
+ diff = (lambda_m - lambda_s - target + 180.0) % 360.0 - 180.0
321
+ return diff
322
+
323
+ # bracket inicial ±2 días alrededor de la estimación
324
+ low = approx_dt - timedelta(days=2)
325
+ high = approx_dt + timedelta(days=2)
326
+
327
+ # Intentamos asegurar que low..high contiene un cambio de signo.
328
+ # Si no, expandimos gradualmente hasta un máximo razonable (p.ej. ±60 días).
329
+ max_expand_days = 60
330
+ step_expand_days = 2
331
+ el_low = elongation_dt(low)
332
+ el_high = elongation_dt(high)
333
+
334
+ expand_attempts = 0
335
+ while el_low * el_high > 0: # mismo signo -> no hay cruce dentro del intervalo
336
+ expand_attempts += 1
337
+ if (high - low).days >= max_expand_days:
338
+ raise RuntimeError(f"No se encontró fase con target {target} en el rango de búsqueda (±60 días).")
339
+ low -= timedelta(days=step_expand_days)
340
+ high += timedelta(days=step_expand_days)
341
+ el_low = elongation_dt(low)
342
+ el_high = elongation_dt(high)
343
+
344
+ # Si exactamente en un extremo es cero, devuelvo ese instante
345
+ if abs(el_low) < 1e-12:
346
+ return low
347
+ if abs(el_high) < 1e-12:
348
+ return high
349
+
350
+ # Bisección: refinamos hasta ~1 segundo de precisión
351
+ # (puedes relajar a 30-60 s si prefieres).
352
+ while (high - low).total_seconds() > 1:
353
+ mid = low + (high - low) / 2
354
+ el_mid = elongation_dt(mid)
355
+ # si mid es exactamente 0 (raro), lo devolvemos
356
+ if abs(el_mid) < 1e-12:
357
+ return mid
358
+ # si el signo de low y mid difiere, el cruce está en low..mid; si no, en mid..high
359
+ if el_low * el_mid <= 0:
360
+ high = mid
361
+ el_high = el_mid
362
+ else:
363
+ low = mid
364
+ el_low = el_mid
365
+
366
+ return low + (high - low) / 2
367
+
368
+ def moon_elongation(date_utc: date) -> float:
369
+ """
370
+ Devuelve la elongación lunar (0-360°) para la fecha UTC al mediodía.
371
+ """
372
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
373
+ jd = julianday(dt)
374
+ jc = julianday_to_juliancentury(jd)
375
+ lambda_m, _, _, _ = _moon_ecliptic_position(jd)
376
+ lambda_s = sun_apparent_long(jc)
377
+ return _normalize_angle(lambda_m - lambda_s)
@@ -1,11 +1,23 @@
1
1
  import datetime
2
2
  from math import acos, asin, atan2, cos, degrees, radians, sin, tan, sqrt
3
3
 
4
- def julianday(date: datetime.date) -> float:
5
- """Calculate the Julian Day number for the specified date."""
6
- year = date.year
7
- month = date.month
8
- day = date.day
4
+ def julianday(date_or_dt: datetime.date | datetime.datetime) -> float:
5
+ """
6
+ Calculate the Julian Day number for a date or datetime (UTC).
7
+ Handles both date (midnight UTC) and datetime (exact time).
8
+ """
9
+ if isinstance(date_or_dt, datetime.datetime):
10
+ year = date_or_dt.year
11
+ month = date_or_dt.month
12
+ day = date_or_dt.day
13
+ hour = date_or_dt.hour
14
+ minute = date_or_dt.minute
15
+ second = date_or_dt.second
16
+ else:
17
+ year = date_or_dt.year
18
+ month = date_or_dt.month
19
+ day = date_or_dt.day
20
+ hour = minute = second = 0
9
21
 
10
22
  if month <= 2:
11
23
  year -= 1
@@ -14,7 +26,7 @@ def julianday(date: datetime.date) -> float:
14
26
  a = year // 100
15
27
  b = 2 - a + (a // 4)
16
28
  jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + b - 1524.5
17
- return jd
29
+ return jd + (hour + minute/60 + second/3600) / 24.0
18
30
 
19
31
  def julianday_to_juliancentury(jd: float) -> float:
20
32
  """Convert a Julian Day number to a Julian Century."""
@@ -52,10 +64,14 @@ def sun_true_long(jc: float) -> float:
52
64
  return l0 + c
53
65
 
54
66
  def sun_apparent_long(jc: float) -> float:
55
- """Calculate the sun's apparent longitude."""
56
- true_long = sun_true_long(jc)
67
+ """Calculate the sun's apparent longitude with nutation correction (Meeus Ch. 25)."""
68
+ l0 = geom_mean_long_sun(jc)
69
+ m = geom_mean_anomaly_sun(jc)
70
+ c = sun_eq_of_center(jc)
71
+ lambda_geo = l0 + c
57
72
  omega = 125.04 - 1934.136 * jc
58
- return true_long - 0.00569 - 0.00478 * sin(radians(omega))
73
+ nutation_long = -0.0048 * sin(radians(2 * omega)) # Nutation correction
74
+ return (lambda_geo + nutation_long - 0.00569) % 360.0
59
75
 
60
76
  def sun_distance(jc: float) -> float:
61
77
  """Calculate the distance to the Sun in astronomical units (AU)."""
@@ -108,10 +124,8 @@ def refraction_at_zenith(zenith: float) -> float:
108
124
  """Calculate the atmospheric refraction correction for the given zenith angle (in degrees)."""
109
125
  if zenith < 0 or zenith > 90:
110
126
  return 0.0
111
-
112
127
  # Convert zenith to radians
113
128
  zenith_rad = radians(zenith)
114
-
115
129
  # Refraction formula (Bennett 1982, used in Astral)
116
130
  tan_zenith = tan(zenith_rad)
117
131
  if zenith > 85.0:
@@ -123,14 +137,12 @@ def refraction_at_zenith(zenith: float) -> float:
123
137
  ) / 3600.0
124
138
  else:
125
139
  refraction = (58.276 / tan_zenith) / 3600.0
126
-
127
140
  return refraction
128
141
 
129
142
  def adjust_to_horizon(elevation: float) -> float:
130
143
  """Calculate the extra degrees of depression due to the observer's elevation."""
131
144
  if elevation <= 0:
132
145
  return 0.0
133
-
134
146
  r = 6356900 # Radius of the Earth in meters
135
147
  a1 = r
136
148
  h1 = r + elevation
@@ -213,11 +225,9 @@ def sunrise_sunset(
213
225
  # Convert to local timezone
214
226
  sunrise_local = sunrise_utc.astimezone(timezone)
215
227
  sunset_local = sunset_utc.astimezone(timezone)
216
-
217
228
  return sunrise_local, sunset_local
218
229
 
219
230
  def noon(
220
- latitude: float,
221
231
  longitude: float,
222
232
  date: datetime.date,
223
233
  timezone: datetime.timezone = datetime.timezone.utc,
@@ -0,0 +1,2 @@
1
+ # version.py
2
+ __version__ = "1.0.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarmoonpy
3
- Version: 0.1.0
3
+ Version: 1.0.1
4
4
  Summary: Precise solar and lunar calculations for astronomical applications
5
5
  Author-email: figorr <jdcuartero@yahoo.es>
6
6
  License: Apache License
@@ -220,5 +220,18 @@ Description-Content-Type: text/markdown
220
220
  License-File: LICENSE
221
221
  Dynamic: license-file
222
222
 
223
- # solarmoonpy
224
- solar and moon calculations
223
+ # ☀️🌙 solarmoonpy
224
+ Solar and moon calculations for Meteocat Home Assistant integration.
225
+
226
+ **solarmoonpy** is a Python library that provides accurate calculations of solar and lunar positions, specifically designed to integrate with Home Assistant and Meteocat data.
227
+
228
+ This project enables you to obtain information such as sunrise, sunset, moon phases, and more, for advanced automations in your home automation system.
229
+
230
+ ## 🚀 Features
231
+
232
+ - ☀️ Solar position calculations (sunrise, sunset, zenith, etc.).
233
+ - 🌙 Moon phase and position calculations.
234
+ - Integration with meteorological data from Meteocat.
235
+ - Compatible with Home Assistant for automations based on solar and lunar events.
236
+ - Lightweight, easy to use, and well-documented.
237
+ - Configurable for different geographic locations.
@@ -1,2 +0,0 @@
1
- # solarmoonpy
2
- solar and moon calculations
@@ -1,356 +0,0 @@
1
- from datetime import date, datetime, timedelta, timezone
2
- import math
3
- from typing import Optional, Tuple
4
- from .sun import julianday, julianday_to_juliancentury, sun_apparent_long, sun_distance
5
-
6
- _SYNODIC_MONTH = 29.530588853
7
- AU = 149597870.7 # km por unidad astronómica (valor estándar)
8
-
9
- def moon_phase(date_utc: date) -> float:
10
- """
11
- Devuelve la fase de la luna como valor fraccionario (0 a ~29.53, 0=nueva, ~14.77=llena).
12
- Usa una luna nueva reciente (21 Sep 2025) para mayor precisión y opcionalmente elongación eclíptica.
13
- Args:
14
- date_utc (date): Fecha en UTC.
15
- Returns:
16
- float: Días desde la última luna nueva (0 a ~29.53).
17
- """
18
- # Luna nueva de referencia: 21 de septiembre de 2025, 19:54 UTC
19
- known_new_moon = datetime(2025, 9, 21, 19, 54, tzinfo=timezone.utc)
20
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, tzinfo=timezone.utc)
21
- diff = dt - known_new_moon
22
- days = diff.days + (diff.seconds / 86400.0)
23
-
24
- # Cálculo aproximado con mes sinódico
25
- lunations = days / _SYNODIC_MONTH
26
- approx_phase = (lunations % 1) * _SYNODIC_MONTH
27
-
28
- # Cálculo preciso usando elongación eclíptica
29
- jd = julianday(dt.date()) + 0.5 # Mediodía UTC
30
- jc = julianday_to_juliancentury(jd)
31
- lambda_m, _, _, _ = _moon_ecliptic_position(jd)
32
- lambda_s = sun_apparent_long(jc)
33
- elong = _normalize_angle(lambda_m - lambda_s)
34
-
35
- # Convertir elongación a fase (0°=nueva, 180°=llena, 360°=nueva)
36
- precise_phase = (elong / 360.0) * _SYNODIC_MONTH
37
- if precise_phase > _SYNODIC_MONTH:
38
- precise_phase -= _SYNODIC_MONTH
39
-
40
- # Promedio ponderado: favorece cálculo preciso, pero usa aproximado como respaldo
41
- return round(0.9 * precise_phase + 0.1 * approx_phase, 6)
42
-
43
- def illuminated_percentage(date_utc: date) -> float:
44
- """
45
- Calcula el porcentaje de luna iluminada para una fecha UTC (versión precisa).
46
- Usa posiciones eclípticas reales de Sol y Luna (Meeus) para la elongación y corrección por distancias.
47
- Args:
48
- date_utc (date): Fecha en UTC.
49
- Returns:
50
- float: Porcentaje de superficie lunar iluminada (0–100).
51
- """
52
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
53
- jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
54
- jc = julianday_to_juliancentury(jd)
55
- lambda_m, beta_m, R, delta_psi = _moon_ecliptic_position(jd)
56
- lambda_s = sun_apparent_long(jc)
57
- r_sun = sun_distance(jc)
58
- diff = _normalize_angle(lambda_m - lambda_s)
59
- elong_rad = math.radians(diff)
60
- beta_rad = math.radians(beta_m)
61
- cos_E = math.cos(beta_rad) * math.cos(elong_rad)
62
- d = r_sun * AU
63
- s2 = d**2 + R**2 - 2 * d * R * cos_E
64
- s = math.sqrt(s2)
65
- cos_i = (s**2 + R**2 - d**2) / (2 * s * R)
66
- illum_fraction = (1 + cos_i) / 2
67
- return illum_fraction * 100.0
68
-
69
- def _normalize_angle(angle: float) -> float:
70
- """Normaliza ángulo a 0-360 grados."""
71
- return angle % 360.0
72
-
73
- # Tabla completa 47.A para sigma_l y sigma_r (de PyMeeus/Meeus)
74
- SIGMA_LR_TABLE = [
75
- [0, 0, 1, 0, 6288774.0, -20905355.0],
76
- [2, 0, -1, 0, 1274027.0, -3699111.0],
77
- [2, 0, 0, 0, 658314.0, -2955968.0],
78
- [0, 0, 2, 0, 213618.0, -569925.0],
79
- [0, 1, 0, 0, -185116.0, 48888.0],
80
- [0, 0, 0, 2, -114332.0, -3149.0],
81
- [2, 0, -2, 0, 58793.0, 246158.0],
82
- [2, -1, -1, 0, 57066.0, -152138.0],
83
- [2, 0, 1, 0, 53322.0, -170733.0],
84
- [2, -1, 0, 0, 45758.0, -204586.0],
85
- [0, 1, -1, 0, -40923.0, -129620.0],
86
- [1, 0, 0, 0, -34720.0, 108743.0],
87
- [0, 1, 1, 0, -30383.0, 104755.0],
88
- [2, 0, 0, -2, 15327.0, 10321.0],
89
- [0, 0, 1, 2, -12528.0, 0.0],
90
- [0, 0, 1, -2, 10980.0, 79661.0],
91
- [4, 0, -1, 0, 10675.0, -34782.0],
92
- [0, 0, 3, 0, 10034.0, -23210.0],
93
- [4, 0, -2, 0, 8548.0, -21636.0],
94
- [2, 1, -1, 0, -7888.0, 24208.0],
95
- [2, 1, 0, 0, -6766.0, 30824.0],
96
- [1, 0, -1, 0, -5163.0, -8379.0],
97
- [1, 1, 0, 0, 4987.0, -16675.0],
98
- [2, -1, 1, 0, 4036.0, -12831.0],
99
- [2, 0, 2, 0, 3994.0, -10445.0],
100
- [4, 0, 0, 0, 3861.0, -11650.0],
101
- [2, 0, -3, 0, 3665.0, 14403.0],
102
- [0, 1, -2, 0, -2689.0, -7003.0],
103
- [2, 0, -1, 2, -2602.0, 0.0],
104
- [2, -1, -2, 0, 2390.0, 10056.0],
105
- [1, 0, 1, 0, -2348.0, 6322.0],
106
- [2, -2, 0, 0, 2236.0, -9884.0],
107
- [0, 1, 2, 0, -2120.0, 5751.0],
108
- [0, 2, 0, 0, -2069.0, 0.0],
109
- [2, -2, -1, 0, 2048.0, -4950.0],
110
- [2, 0, 1, -2, -1773.0, 4130.0],
111
- [2, 0, 0, 2, -1595.0, 0.0],
112
- [4, -1, -1, 0, 1215.0, -3958.0],
113
- [0, 0, 2, 2, -1110.0, 0.0],
114
- [3, 0, -1, 0, -892.0, 3258.0],
115
- [2, 1, 1, 0, -810.0, 2616.0],
116
- [4, -1, -2, 0, 759.0, -1897.0],
117
- [0, 2, -1, 0, -713.0, -2117.0],
118
- [2, 2, -1, 0, -700.0, 2354.0],
119
- [2, 1, -2, 0, 691.0, 0.0],
120
- [2, -1, 0, -2, 596.0, 0.0],
121
- [4, 0, 1, 0, 549.0, -1423.0],
122
- [0, 0, 4, 0, 537.0, -1117.0],
123
- [4, -1, 0, 0, 520.0, -1571.0],
124
- [1, 0, -2, 0, -487.0, -1739.0],
125
- [2, 1, 0, -2, -399.0, 0.0],
126
- [0, 0, 2, -2, -381.0, -4421.0],
127
- [1, 1, 1, 0, 351.0, 0.0],
128
- [3, 0, -2, 0, -340.0, 0.0],
129
- [4, 0, -3, 0, 330.0, 0.0],
130
- [2, -1, 2, 0, 327.0, 0.0],
131
- [0, 2, 1, 0, -323.0, 1165.0],
132
- [1, 1, -1, 0, 299.0, 0.0],
133
- [2, 0, 3, 0, 294.0, 0.0],
134
- [2, 0, -1, -2, 0.0, 8752.0]
135
- ]
136
-
137
- # Tabla completa 47.B para sigma_b (de PyMeeus/Meeus)
138
- SIGMA_B_TABLE = [
139
- [0, 0, 0, 1, 5128122.0],
140
- [0, 0, 1, 1, 280602.0],
141
- [0, 0, 1, -1, 277693.0],
142
- [2, 0, 0, -1, 173237.0],
143
- [2, 0, -1, 1, 55413.0],
144
- [2, 0, -1, -1, 46271.0],
145
- [2, 0, 0, 1, 32573.0],
146
- [0, 0, 2, 1, 17198.0],
147
- [2, 0, 1, -1, 9266.0],
148
- [0, 0, 2, -1, 8822.0],
149
- [2, -1, 0, -1, 8216.0],
150
- [2, 0, -2, -1, 4324.0],
151
- [2, 0, 1, 1, 4200.0],
152
- [2, 1, 0, -1, -3359.0],
153
- [2, -1, -1, 1, 2463.0],
154
- [2, -1, 0, 1, 2211.0],
155
- [2, -1, -1, -1, 2065.0],
156
- [0, 1, -1, -1, -1870.0],
157
- [4, 0, -1, -1, 1828.0],
158
- [0, 1, 0, 1, -1794.0],
159
- [0, 0, 0, 3, -1749.0],
160
- [0, 1, -1, 1, -1565.0],
161
- [1, 0, 0, 1, -1491.0],
162
- [0, 1, 1, 1, -1475.0],
163
- [0, 1, 1, -1, -1410.0],
164
- [0, 1, 0, -1, -1344.0],
165
- [1, 0, 0, -1, -1335.0],
166
- [0, 0, 3, 1, 1107.0],
167
- [4, 0, 0, -1, 1021.0],
168
- [4, 0, -1, 1, 833.0],
169
- [0, 0, 1, -3, 777.0],
170
- [4, 0, -2, 1, 671.0],
171
- [2, 0, 0, -3, 607.0],
172
- [2, 0, 2, -1, 596.0],
173
- [2, -1, 1, -1, 491.0],
174
- [2, 0, -2, 1, -451.0],
175
- [0, 0, 3, -1, 439.0],
176
- [2, 0, 2, 1, 422.0],
177
- [2, 0, -3, -1, 421.0],
178
- [2, 1, -1, 1, -366.0],
179
- [2, 1, 0, 1, -351.0],
180
- [4, 0, 0, 1, 331.0],
181
- [2, -1, 1, 1, 315.0],
182
- [2, -2, 0, -1, 302.0],
183
- [0, 0, 1, 3, -283.0],
184
- [2, 1, 1, -1, -229.0],
185
- [1, 1, 0, -1, 223.0],
186
- [1, 1, 0, 1, 223.0],
187
- [0, 1, -2, -1, -220.0],
188
- [2, 1, -1, -1, -220.0],
189
- [1, 0, 1, 1, -185.0],
190
- [2, -1, -2, -1, 181.0],
191
- [0, 1, 2, 1, -177.0],
192
- [4, 0, -2, -1, 176.0],
193
- [4, -1, -1, -1, 166.0],
194
- [1, 0, 1, -1, -164.0],
195
- [4, 0, 1, -1, 132.0],
196
- [1, 0, -1, -1, -119.0],
197
- [4, -1, 0, -1, 115.0],
198
- [2, -2, 0, 1, 107.0]
199
- ]
200
-
201
- def _periodic_terms(D, M, Mp, F, T):
202
- """Calcula sigma_l, sigma_r, sigma_b con tablas completas y factor E."""
203
- E = 1 - 0.002516 * T - 0.0000074 * T**2
204
- E2 = E * E
205
- sigma_l = 0.0
206
- sigma_r = 0.0
207
- sigma_b = 0.0
208
-
209
- for d_coef, m, mp, f, l, r in SIGMA_LR_TABLE:
210
- factor = 1.0
211
- if abs(m) == 1:
212
- factor = E
213
- elif abs(m) == 2:
214
- factor = E2
215
- arg = d_coef * D + m * M + mp * Mp + f * F
216
- sigma_l += l * math.sin(math.radians(arg)) * factor
217
- sigma_r += r * math.cos(math.radians(arg)) * factor
218
-
219
- for d_coef, m, mp, f, b in SIGMA_B_TABLE:
220
- factor = 1.0
221
- if abs(m) == 1:
222
- factor = E
223
- elif abs(m) == 2:
224
- factor = E2
225
- arg = d_coef * D + m * M + mp * Mp + f * F
226
- sigma_b += b * math.sin(math.radians(arg)) * factor
227
-
228
- sigma_l /= 1000000.0 # a grados
229
- sigma_r *= 0.001 # a km
230
- sigma_b /= 1000000.0 # a grados
231
-
232
- return sigma_l, sigma_r, sigma_b
233
-
234
- def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
235
- """Calcula longitud eclíptica aparente, latitud, distancia y delta_psi de la Luna."""
236
- T = (jd - 2451545.0) / 36525.0
237
- Lp = _normalize_angle(218.3164477 + 481267.88123421 * T - 0.0015786 * T**2 + T**3 / 538841.0 - T**4 / 65194000.0)
238
- D = _normalize_angle(297.8501921 + 445267.1114034 * T - 0.0018819 * T**2 + T**3 / 545868.0 - T**4 / 113065000.0)
239
- M = _normalize_angle(357.5291092 + 35999.0502909 * T - 0.0001536 * T**2 + T**3 / 24490000.0)
240
- Mp = _normalize_angle(134.9633964 + 477198.8675055 * T + 0.0087414 * T**2 + T**3 / 69699.0 - T**4 / 14712000.0)
241
- F = _normalize_angle(93.2720950 + 483202.0175238 * T - 0.0036539 * T**2 - T**3 / 3526000.0 + T**4 / 863310000.0)
242
- Omega = _normalize_angle(125.04452 - 1934.136261 * T + 0.0020708 * T**2 + T**3 / 450000.0)
243
-
244
- sigma_l, sigma_r, sigma_b = _periodic_terms(D, M, Mp, F, T)
245
-
246
- lambda_ = Lp + sigma_l + 0.003958 * math.sin(math.radians(119.75 + 131.849 * T)) + 0.000319 * math.sin(math.radians(53.09 + 479264.290 * T)) + 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T))
247
- beta = sigma_b - 0.000024 * math.sin(math.radians(313.45 + 481266.484 * T - 2 * F))
248
- R = 385000.529 + sigma_r
249
- delta_psi = (-17.20 * math.sin(math.radians(Omega)) - 1.32 * math.sin(math.radians(2 * (Lp - F))) + 0.23 * math.sin(math.radians(2 * Lp)) + 0.21 * math.sin(math.radians(2 * Omega))) / 3600.0
250
- lambda_ += delta_psi
251
-
252
- return lambda_, beta, R, delta_psi
253
-
254
- def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[float, float]:
255
- """Calcula alt, h0 para un tiempo dado t (ra no usado)."""
256
- jd = julianday(t.date()) + (t.hour + t.minute / 60 + t.second / 3600) / 24
257
- T = (jd - 2451545.0) / 36525.0
258
- lambda_, beta, R, delta_psi = _moon_ecliptic_position(jd)
259
-
260
- par = math.asin(6378.14 / R)
261
- eps = math.radians(23.439281 - 0.0000004 * T)
262
- lambda_r = math.radians(lambda_)
263
- beta_r = math.radians(beta)
264
- ra = math.atan2(math.sin(lambda_r) * math.cos(eps) - math.tan(beta_r) * math.sin(eps), math.cos(lambda_r))
265
- dec = math.asin(math.sin(beta_r) * math.cos(eps) + math.cos(beta_r) * math.sin(eps) * math.sin(lambda_r))
266
- sd = 0.2725076 * par
267
- ref = math.radians(0.5667)
268
- h0 = par - sd - ref
269
- gmst = (280.46061837 + 360.98564736629 * (jd - 2451545.0) + 0.000387933 * T**2 - T**3 / 38710000.0) % 360.0
270
- lst = math.radians((gmst + lon) % 360.0)
271
- ha = lst - ra
272
- if ha < -math.pi: ha += 2 * math.pi
273
- elif ha > math.pi: ha -= 2 * math.pi
274
- alt = math.asin(math.sin(lat_r) * math.sin(dec) + math.cos(lat_r) * math.cos(dec) * math.cos(ha))
275
-
276
- return alt, h0
277
-
278
- def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[datetime], Optional[datetime]]:
279
- """
280
- Cálculo preciso de moonrise y moonset con series completas de Meeus (precisión ~1-2 min).
281
- Devuelve tuplas UTC (datetime tz-aware) o None si no ocurre en ese día.
282
- """
283
- lat_r = math.radians(lat)
284
- rise = None
285
- set_ = None
286
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, tzinfo=timezone.utc)
287
- prev_t = None
288
- prev_alt = None
289
-
290
- step_grueso = 1.0
291
- intervalos_rise = []
292
- intervalos_set = []
293
- for i in range(int(24 / step_grueso) + 1):
294
- hour = i * step_grueso
295
- t = dt + timedelta(hours=hour)
296
- alt, h0 = _calculate_position_and_alt(t, lat_r, lon)
297
- if prev_alt is not None:
298
- den = alt - prev_alt
299
- if abs(den) < 1e-12:
300
- continue
301
- if prev_alt < h0 and alt >= h0:
302
- intervalos_rise.append((prev_t, t, prev_alt, alt))
303
- elif prev_alt >= h0 and alt < h0:
304
- intervalos_set.append((prev_t, t, prev_alt, alt))
305
- prev_t = t
306
- prev_alt = alt
307
-
308
- def refine_event(start_t: datetime, end_t: datetime, start_alt: float, end_alt: float, is_rise: bool) -> datetime:
309
- for _ in range(10):
310
- mid_t = start_t + (end_t - start_t) / 2
311
- mid_alt, mid_h0 = _calculate_position_and_alt(mid_t, lat_r, lon)
312
- den = mid_alt - start_alt if is_rise else mid_alt - end_alt
313
- if abs(den) < 1e-12:
314
- return mid_t
315
- if (is_rise and mid_alt < mid_h0) or (not is_rise and mid_alt >= mid_h0):
316
- start_t, start_alt = mid_t, mid_alt
317
- else:
318
- end_t, end_alt = mid_t, mid_alt
319
- return start_t + (end_t - start_t) / 2
320
-
321
- if intervalos_rise and rise is None:
322
- start_t, end_t, start_alt, end_alt = intervalos_rise[0]
323
- rise = refine_event(start_t, end_t, start_alt, end_alt, True)
324
- if intervalos_set and set_ is None:
325
- start_t, end_t, start_alt, end_alt = intervalos_set[0]
326
- set_ = refine_event(start_t, end_t, start_alt, end_alt, False)
327
-
328
- return rise, set_
329
-
330
- def moon_distance(date_utc: date) -> float:
331
- """
332
- Calcula la distancia Tierra-Luna en kilómetros para una fecha UTC.
333
- Args:
334
- date_utc (date): Fecha en UTC.
335
- Returns:
336
- float: Distancia en kilómetros.
337
- """
338
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
339
- jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
340
- _, _, R, _ = _moon_ecliptic_position(jd)
341
- return R
342
-
343
- def moon_angular_diameter(date_utc: date) -> float:
344
- """
345
- Calcula el diámetro angular de la Luna en arcosegundos para una fecha UTC.
346
- Args:
347
- date_utc (date): Fecha en UTC.
348
- Returns:
349
- float: Diámetro angular en arcosegundos.
350
- """
351
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
352
- jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
353
- _, _, R, _ = _moon_ecliptic_position(jd)
354
- par = math.asin(6378.14 / R) # Paralaje horizontal
355
- sd = 0.2725076 * par # Semi-diámetro
356
- return 2 * math.degrees(sd) * 3600 # Diámetro angular en arcosegundos
@@ -1,2 +0,0 @@
1
- # version.py
2
- __version__ = "0.1.0"
File without changes
File without changes