solarmoonpy 1.0.0__tar.gz → 1.0.2__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.

Potentially problematic release.


This version of solarmoonpy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarmoonpy
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Precise solar and lunar calculations for astronomical applications
5
5
  Author-email: figorr <jdcuartero@yahoo.es>
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "solarmoonpy"
7
- version = "1.0.0"
7
+ version = "1.0.2"
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" }]
@@ -253,7 +253,6 @@ class Location:
253
253
  date = datetime.now(self.tzinfo if local else timezone.utc).date()
254
254
 
255
255
  return noon(
256
- latitude=self.latitude,
257
256
  longitude=self.longitude,
258
257
  date=date,
259
258
  timezone=self.tzinfo if local else timezone.utc
@@ -4,86 +4,36 @@ from typing import Optional, Tuple
4
4
  from .sun import julianday, julianday_to_juliancentury, sun_apparent_long, sun_distance
5
5
 
6
6
  _SYNODIC_MONTH = 29.530588853
7
- AU = 149597870.7 # km por unidad astronómica (valor estándar)
7
+ AU = 149597870.7
8
+ JD_REF_PHASE = 2423436.0 # para moon_phase
9
+ JD_REF_LUNATION = 2423407.51818 # para lunation_number
8
10
 
9
11
  def moon_phase(date_utc: date) -> float:
10
12
  """
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).
13
+ Devuelve los días transcurridos desde la última Luna Nueva.
14
+ Basado en el algoritmo de Chapront-Touzé (usado por timeanddate/NASA).
17
15
  """
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
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
39
24
 
40
- # Promedio ponderado: favorece cálculo preciso, pero usa aproximado como respaldo
41
- return round(0.9 * precise_phase + 0.1 * approx_phase, 6)
25
+ # Días desde la última luna nueva
26
+ return phase * _SYNODIC_MONTH
42
27
 
43
28
  def moon_day(date_utc: date, tol_hours: float = 0.01) -> int:
44
29
  """
45
30
  Calcula el día lunar 0–29 usando la última luna nueva exacta calculada por elongación.
46
- Usa el fin del día UTC para alinear con convenciones de timeanddate/NASA (etiqueta el día con la fase si ocurre en él).
47
- tol_hours: precisión en horas.
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).
48
33
  """
49
34
  dt = datetime(date_utc.year, date_utc.month, date_utc.day, 23, 59, tzinfo=timezone.utc)
50
-
51
- def elongation(t: datetime) -> float:
52
- jd = julianday(t.date()) + (t.hour + t.minute/60 + t.second/3600)/24
53
- jc = julianday_to_juliancentury(jd)
54
- lambda_m, _, _, _ = _moon_ecliptic_position(jd)
55
- lambda_s = sun_apparent_long(jc)
56
- diff = (lambda_m - lambda_s) % 360.0
57
- if diff > 180:
58
- diff -= 360
59
- return diff
60
-
61
- # Buscar la última luna nueva antes de la fecha
62
- step = timedelta(hours=6)
63
- t_back = dt
64
- prev_el = elongation(t_back)
65
- for _ in range(int(30*24/6)):
66
- t_back -= step
67
- el = elongation(t_back)
68
- if prev_el > 0 >= el: # cruce 0 hacia abajo indica luna nueva
69
- break
70
- prev_el = el
71
-
72
- # Refinar el momento exacto con bisección
73
- low = t_back
74
- high = t_back + step
75
- while (high - low).total_seconds() / 3600 > tol_hours:
76
- mid = low + (high - low)/2
77
- if elongation(mid) > 0:
78
- high = mid
79
- else:
80
- low = mid
81
- last_new_moon = low + (high - low)/2
82
-
83
- # Edad lunar en días
35
+ last_new_moon = find_last_phase_exact(date_utc, target=0.0)
84
36
  age_days = (dt - last_new_moon).total_seconds() / 86400.0
85
-
86
- # Día lunar: 0–29, redondeado correctamente
87
37
  if age_days < 0:
88
38
  day = 0
89
39
  else:
@@ -101,8 +51,8 @@ def illuminated_percentage(date_utc: date) -> float:
101
51
  Returns:
102
52
  float: Porcentaje de superficie lunar iluminada (0–100).
103
53
  """
104
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
105
- jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
54
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
55
+ jd = julianday(dt)
106
56
  jc = julianday_to_juliancentury(jd)
107
57
  lambda_m, beta_m, R, delta_psi = _moon_ecliptic_position(jd)
108
58
  lambda_s = sun_apparent_long(jc)
@@ -124,130 +74,60 @@ def _normalize_angle(angle: float) -> float:
124
74
 
125
75
  # Tabla completa 47.A para sigma_l y sigma_r (de PyMeeus/Meeus)
126
76
  SIGMA_LR_TABLE = [
127
- [0, 0, 1, 0, 6288774.0, -20905355.0],
128
- [2, 0, -1, 0, 1274027.0, -3699111.0],
129
- [2, 0, 0, 0, 658314.0, -2955968.0],
130
- [0, 0, 2, 0, 213618.0, -569925.0],
131
- [0, 1, 0, 0, -185116.0, 48888.0],
132
- [0, 0, 0, 2, -114332.0, -3149.0],
133
- [2, 0, -2, 0, 58793.0, 246158.0],
134
- [2, -1, -1, 0, 57066.0, -152138.0],
135
- [2, 0, 1, 0, 53322.0, -170733.0],
136
- [2, -1, 0, 0, 45758.0, -204586.0],
137
- [0, 1, -1, 0, -40923.0, -129620.0],
138
- [1, 0, 0, 0, -34720.0, 108743.0],
139
- [0, 1, 1, 0, -30383.0, 104755.0],
140
- [2, 0, 0, -2, 15327.0, 10321.0],
141
- [0, 0, 1, 2, -12528.0, 0.0],
142
- [0, 0, 1, -2, 10980.0, 79661.0],
143
- [4, 0, -1, 0, 10675.0, -34782.0],
144
- [0, 0, 3, 0, 10034.0, -23210.0],
145
- [4, 0, -2, 0, 8548.0, -21636.0],
146
- [2, 1, -1, 0, -7888.0, 24208.0],
147
- [2, 1, 0, 0, -6766.0, 30824.0],
148
- [1, 0, -1, 0, -5163.0, -8379.0],
149
- [1, 1, 0, 0, 4987.0, -16675.0],
150
- [2, -1, 1, 0, 4036.0, -12831.0],
151
- [2, 0, 2, 0, 3994.0, -10445.0],
152
- [4, 0, 0, 0, 3861.0, -11650.0],
153
- [2, 0, -3, 0, 3665.0, 14403.0],
154
- [0, 1, -2, 0, -2689.0, -7003.0],
155
- [2, 0, -1, 2, -2602.0, 0.0],
156
- [2, -1, -2, 0, 2390.0, 10056.0],
157
- [1, 0, 1, 0, -2348.0, 6322.0],
158
- [2, -2, 0, 0, 2236.0, -9884.0],
159
- [0, 1, 2, 0, -2120.0, 5751.0],
160
- [0, 2, 0, 0, -2069.0, 0.0],
161
- [2, -2, -1, 0, 2048.0, -4950.0],
162
- [2, 0, 1, -2, -1773.0, 4130.0],
163
- [2, 0, 0, 2, -1595.0, 0.0],
164
- [4, -1, -1, 0, 1215.0, -3958.0],
165
- [0, 0, 2, 2, -1110.0, 0.0],
166
- [3, 0, -1, 0, -892.0, 3258.0],
167
- [2, 1, 1, 0, -810.0, 2616.0],
168
- [4, -1, -2, 0, 759.0, -1897.0],
169
- [0, 2, -1, 0, -713.0, -2117.0],
170
- [2, 2, -1, 0, -700.0, 2354.0],
171
- [2, 1, -2, 0, 691.0, 0.0],
172
- [2, -1, 0, -2, 596.0, 0.0],
173
- [4, 0, 1, 0, 549.0, -1423.0],
174
- [0, 0, 4, 0, 537.0, -1117.0],
175
- [4, -1, 0, 0, 520.0, -1571.0],
176
- [1, 0, -2, 0, -487.0, -1739.0],
177
- [2, 1, 0, -2, -399.0, 0.0],
178
- [0, 0, 2, -2, -381.0, -4421.0],
179
- [1, 1, 1, 0, 351.0, 0.0],
180
- [3, 0, -2, 0, -340.0, 0.0],
181
- [4, 0, -3, 0, 330.0, 0.0],
182
- [2, -1, 2, 0, 327.0, 0.0],
183
- [0, 2, 1, 0, -323.0, 1165.0],
184
- [1, 1, -1, 0, 299.0, 0.0],
185
- [2, 0, 3, 0, 294.0, 0.0],
186
- [2, 0, -1, -2, 0.0, 8752.0]
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]
187
107
  ]
188
108
 
189
109
  # Tabla completa 47.B para sigma_b (de PyMeeus/Meeus)
190
110
  SIGMA_B_TABLE = [
191
- [0, 0, 0, 1, 5128122.0],
192
- [0, 0, 1, 1, 280602.0],
193
- [0, 0, 1, -1, 277693.0],
194
- [2, 0, 0, -1, 173237.0],
195
- [2, 0, -1, 1, 55413.0],
196
- [2, 0, -1, -1, 46271.0],
197
- [2, 0, 0, 1, 32573.0],
198
- [0, 0, 2, 1, 17198.0],
199
- [2, 0, 1, -1, 9266.0],
200
- [0, 0, 2, -1, 8822.0],
201
- [2, -1, 0, -1, 8216.0],
202
- [2, 0, -2, -1, 4324.0],
203
- [2, 0, 1, 1, 4200.0],
204
- [2, 1, 0, -1, -3359.0],
205
- [2, -1, -1, 1, 2463.0],
206
- [2, -1, 0, 1, 2211.0],
207
- [2, -1, -1, -1, 2065.0],
208
- [0, 1, -1, -1, -1870.0],
209
- [4, 0, -1, -1, 1828.0],
210
- [0, 1, 0, 1, -1794.0],
211
- [0, 0, 0, 3, -1749.0],
212
- [0, 1, -1, 1, -1565.0],
213
- [1, 0, 0, 1, -1491.0],
214
- [0, 1, 1, 1, -1475.0],
215
- [0, 1, 1, -1, -1410.0],
216
- [0, 1, 0, -1, -1344.0],
217
- [1, 0, 0, -1, -1335.0],
218
- [0, 0, 3, 1, 1107.0],
219
- [4, 0, 0, -1, 1021.0],
220
- [4, 0, -1, 1, 833.0],
221
- [0, 0, 1, -3, 777.0],
222
- [4, 0, -2, 1, 671.0],
223
- [2, 0, 0, -3, 607.0],
224
- [2, 0, 2, -1, 596.0],
225
- [2, -1, 1, -1, 491.0],
226
- [2, 0, -2, 1, -451.0],
227
- [0, 0, 3, -1, 439.0],
228
- [2, 0, 2, 1, 422.0],
229
- [2, 0, -3, -1, 421.0],
230
- [2, 1, -1, 1, -366.0],
231
- [2, 1, 0, 1, -351.0],
232
- [4, 0, 0, 1, 331.0],
233
- [2, -1, 1, 1, 315.0],
234
- [2, -2, 0, -1, 302.0],
235
- [0, 0, 1, 3, -283.0],
236
- [2, 1, 1, -1, -229.0],
237
- [1, 1, 0, -1, 223.0],
238
- [1, 1, 0, 1, 223.0],
239
- [0, 1, -2, -1, -220.0],
240
- [2, 1, -1, -1, -220.0],
241
- [1, 0, 1, 1, -185.0],
242
- [2, -1, -2, -1, 181.0],
243
- [0, 1, 2, 1, -177.0],
244
- [4, 0, -2, -1, 176.0],
245
- [4, -1, -1, -1, 166.0],
246
- [1, 0, 1, -1, -164.0],
247
- [4, 0, 1, -1, 132.0],
248
- [1, 0, -1, -1, -119.0],
249
- [4, -1, 0, -1, 115.0],
250
- [2, -2, 0, 1, 107.0]
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]
251
131
  ]
252
132
 
253
133
  def _periodic_terms(D, M, Mp, F, T):
@@ -280,7 +160,6 @@ def _periodic_terms(D, M, Mp, F, T):
280
160
  sigma_l /= 1000000.0 # a grados
281
161
  sigma_r *= 0.001 # a km
282
162
  sigma_b /= 1000000.0 # a grados
283
-
284
163
  return sigma_l, sigma_r, sigma_b
285
164
 
286
165
  def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
@@ -300,7 +179,6 @@ def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
300
179
  R = 385000.529 + sigma_r
301
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
302
181
  lambda_ += delta_psi
303
-
304
182
  return lambda_, beta, R, delta_psi
305
183
 
306
184
  def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[float, float]:
@@ -324,7 +202,6 @@ def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[
324
202
  if ha < -math.pi: ha += 2 * math.pi
325
203
  elif ha > math.pi: ha -= 2 * math.pi
326
204
  alt = math.asin(math.sin(lat_r) * math.sin(dec) + math.cos(lat_r) * math.cos(dec) * math.cos(ha))
327
-
328
205
  return alt, h0
329
206
 
330
207
  def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[datetime], Optional[datetime]]:
@@ -376,7 +253,6 @@ def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[date
376
253
  if intervalos_set and set_ is None:
377
254
  start_t, end_t, start_alt, end_alt = intervalos_set[0]
378
255
  set_ = refine_event(start_t, end_t, start_alt, end_alt, False)
379
-
380
256
  return rise, set_
381
257
 
382
258
  def moon_distance(date_utc: date) -> float:
@@ -387,8 +263,8 @@ def moon_distance(date_utc: date) -> float:
387
263
  Returns:
388
264
  float: Distancia en kilómetros.
389
265
  """
390
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
391
- jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
266
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
267
+ jd = julianday(dt)
392
268
  _, _, R, _ = _moon_ecliptic_position(jd)
393
269
  return R
394
270
 
@@ -400,9 +276,102 @@ def moon_angular_diameter(date_utc: date) -> float:
400
276
  Returns:
401
277
  float: Diámetro angular en arcosegundos.
402
278
  """
403
- dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc) # Mediodía UTC
404
- jd = julianday(dt.date()) + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
279
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
280
+ jd = julianday(dt)
405
281
  _, _, R, _ = _moon_ecliptic_position(jd)
406
- par = math.asin(6378.14 / R) # Paralaje horizontal
407
- sd = 0.2725076 * par # Semi-diámetro
408
- return 2 * math.degrees(sd) * 3600 # Diámetro angular en arcosegundos
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.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarmoonpy
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Precise solar and lunar calculations for astronomical applications
5
5
  Author-email: figorr <jdcuartero@yahoo.es>
6
6
  License: Apache License
@@ -1,2 +0,0 @@
1
- # version.py
2
- __version__ = "1.0.0"
File without changes
File without changes
File without changes