solarmoonpy 0.1.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

solarmoonpy/__init__.py CHANGED
@@ -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",
solarmoonpy/moon.py CHANGED
@@ -4,41 +4,43 @@ 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)
39
21
 
40
- # Promedio ponderado: favorece cálculo preciso, pero usa aproximado como respaldo
41
- return round(0.9 * precise_phase + 0.1 * approx_phase, 6)
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
42
44
 
43
45
  def illuminated_percentage(date_utc: date) -> float:
44
46
  """
@@ -49,8 +51,8 @@ def illuminated_percentage(date_utc: date) -> float:
49
51
  Returns:
50
52
  float: Porcentaje de superficie lunar iluminada (0–100).
51
53
  """
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
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
55
+ jd = julianday(dt)
54
56
  jc = julianday_to_juliancentury(jd)
55
57
  lambda_m, beta_m, R, delta_psi = _moon_ecliptic_position(jd)
56
58
  lambda_s = sun_apparent_long(jc)
@@ -72,130 +74,60 @@ def _normalize_angle(angle: float) -> float:
72
74
 
73
75
  # Tabla completa 47.A para sigma_l y sigma_r (de PyMeeus/Meeus)
74
76
  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]
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]
135
107
  ]
136
108
 
137
109
  # Tabla completa 47.B para sigma_b (de PyMeeus/Meeus)
138
110
  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]
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]
199
131
  ]
200
132
 
201
133
  def _periodic_terms(D, M, Mp, F, T):
@@ -228,7 +160,6 @@ def _periodic_terms(D, M, Mp, F, T):
228
160
  sigma_l /= 1000000.0 # a grados
229
161
  sigma_r *= 0.001 # a km
230
162
  sigma_b /= 1000000.0 # a grados
231
-
232
163
  return sigma_l, sigma_r, sigma_b
233
164
 
234
165
  def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
@@ -248,7 +179,6 @@ def _moon_ecliptic_position(jd: float) -> Tuple[float, float, float, float]:
248
179
  R = 385000.529 + sigma_r
249
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
250
181
  lambda_ += delta_psi
251
-
252
182
  return lambda_, beta, R, delta_psi
253
183
 
254
184
  def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[float, float]:
@@ -272,7 +202,6 @@ def _calculate_position_and_alt(t: datetime, lat_r: float, lon: float) -> tuple[
272
202
  if ha < -math.pi: ha += 2 * math.pi
273
203
  elif ha > math.pi: ha -= 2 * math.pi
274
204
  alt = math.asin(math.sin(lat_r) * math.sin(dec) + math.cos(lat_r) * math.cos(dec) * math.cos(ha))
275
-
276
205
  return alt, h0
277
206
 
278
207
  def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[datetime], Optional[datetime]]:
@@ -324,7 +253,6 @@ def moon_rise_set(lat: float, lon: float, date_utc: date) -> Tuple[Optional[date
324
253
  if intervalos_set and set_ is None:
325
254
  start_t, end_t, start_alt, end_alt = intervalos_set[0]
326
255
  set_ = refine_event(start_t, end_t, start_alt, end_alt, False)
327
-
328
256
  return rise, set_
329
257
 
330
258
  def moon_distance(date_utc: date) -> float:
@@ -335,8 +263,8 @@ def moon_distance(date_utc: date) -> float:
335
263
  Returns:
336
264
  float: Distancia en kilómetros.
337
265
  """
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
266
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
267
+ jd = julianday(dt)
340
268
  _, _, R, _ = _moon_ecliptic_position(jd)
341
269
  return R
342
270
 
@@ -348,9 +276,102 @@ def moon_angular_diameter(date_utc: date) -> float:
348
276
  Returns:
349
277
  float: Diámetro angular en arcosegundos.
350
278
  """
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
279
+ dt = datetime(date_utc.year, date_utc.month, date_utc.day, 12, 0, 0, tzinfo=timezone.utc)
280
+ jd = julianday(dt)
353
281
  _, _, 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
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)
solarmoonpy/sun.py CHANGED
@@ -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,
solarmoonpy/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.0"
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.
@@ -0,0 +1,10 @@
1
+ solarmoonpy/__init__.py,sha256=cS0ZjePNX75usK8mgvbk_uguVpXRixqZU4zjrEtg42c,722
2
+ solarmoonpy/location.py,sha256=ELx1BVGMRwMPW6uhtyz2LUjxMyBNQM_RXgwRuLo1J7s,10386
3
+ solarmoonpy/moon.py,sha256=SSa4pwUX-r70NSg2Fv3TSAnoyMAdTq2vQ8JRZqDBONo,17587
4
+ solarmoonpy/sun.py,sha256=dleHQWXzjnqtPAZr9ONdXiZvTVWvdW6gW775A2ASsNk,8937
5
+ solarmoonpy/version.py,sha256=Pk3OGNvW6xVheWgr6AQzYbPTYigdhML663W3JHGsF6U,35
6
+ solarmoonpy-1.0.1.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
7
+ solarmoonpy-1.0.1.dist-info/METADATA,sha256=7Xwsrseo2d_crVrBPp7iya7m71r2ULM3-onQlewxgU8,14850
8
+ solarmoonpy-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ solarmoonpy-1.0.1.dist-info/top_level.txt,sha256=egsoDe9E0QEu5GfAA5jSvy_3qTCWMLMe_Kh3gnM6-dY,12
10
+ solarmoonpy-1.0.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- solarmoonpy/__init__.py,sha256=6oTXMLEtrwgFemMgPxNTrWSebn3_pZjBkFmyVGol6d0,695
2
- solarmoonpy/location.py,sha256=ELx1BVGMRwMPW6uhtyz2LUjxMyBNQM_RXgwRuLo1J7s,10386
3
- solarmoonpy/moon.py,sha256=eyrh1CHDjMAI1gFGoW4Ig_RYP5hAsHbQklKLjLGvqLc,14266
4
- solarmoonpy/sun.py,sha256=ai53vZEJ6k9fvb8JyaZNxKueqHXLnKZxFBz-QAAAHjc,8289
5
- solarmoonpy/version.py,sha256=XJYgmMWwwuGBlSVGAS1tn_sPe0VvcX2XS3T-VPrSZ4I,35
6
- solarmoonpy-0.1.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
7
- solarmoonpy-0.1.0.dist-info/METADATA,sha256=fyCsilbXyW-t99jPC1u9JdgSR_QOLYEIph1kdgQXcts,14084
8
- solarmoonpy-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- solarmoonpy-0.1.0.dist-info/top_level.txt,sha256=egsoDe9E0QEu5GfAA5jSvy_3qTCWMLMe_Kh3gnM6-dY,12
10
- solarmoonpy-0.1.0.dist-info/RECORD,,