¿Cómo determinar el día/la noche en función de la latitud, la longitud y la fecha/hora?

¿Existe un método simple para determinar, dada una fecha/hora UTC, si es de día o de noche en una determinada coordenada de latitud/longitud?

Actualmente estoy usando una fórmula basada en un algoritmo de amanecer/atardecer del Observatorio Naval de los EE. UU. , pero desafortunadamente tengo problemas para aplicar para llegar a un indicador simple de noche/día.

¿Debo basarme en esta fórmula o hay alguna forma más eficiente de hacerlo que me falta?

Descargo de responsabilidad: soy programador, no astrónomo, por lo que las fórmulas para los cálculos astronómicos me parecen un poco confusas.

Cosas extra: Idealmente, me gustaría acortar artificialmente el período de "noche" en dos horas, con el fin de eliminar algunos falsos positivos de los datos del sensor que ocurren durante el crepúsculo/amanecer. En otras palabras, si la puesta de sol se produce a las 7 p. m. en un lugar determinado, me gustaría tener la opción de aumentar el valor en 60 minutos para que la salida siga siendo "día" hasta las 8 p. m. para ese lugar. (Y aplique el mismo relleno antes del amanecer).

Actualizar:

Terminé calculando la altitud del sol para la ubicación usando una marca de tiempo UTC y las coordenadas de latitud y longitud de la ubicación. Usando cifras para varias cantidades de crepúsculo (6, 12, 18 o grados por debajo del horizonte), puedo seleccionar automáticamente la parte de la "noche" que es lo suficientemente oscura. (por ejemplo, 6 grados por debajo del horizonte no es tan oscuro como 12 grados). Este enfoque fue mucho más fácil que intentar determinar las horas de salida y puesta del sol para la zona horaria local (y todos los dolores de cabeza asociados a la zona horaria).

El enlace de tu pregunta está muerto. Parece haberse movido a edwilliams.org/sunrise_sunset_algorithm.htm a partir del 2019/08
@Waslap Gracias, actualicé el enlace.
¿Puedes compartir tu código?
@Ori Esto fue hace 10 años. Ya no tengo acceso al código base. Sin embargo, si puedo encontrar mis notas, actualizaré la pregunta.
@Ori Encontré parte del código original y lo publiqué como una respuesta adicional . No garantizo su funcionalidad ni su integridad, pero puede ser de alguna ayuda para otras personas que buscan ideas.

Respuestas (2)

Las fórmulas en esa página son tan simples como las que encontrará y deberían satisfacer sus necesidades. Si está confundido por algo específico, ¡solo pregunte!

Para acortar el período de la noche, debe ir por la distancia (en grados, por ejemplo) que el Sol está debajo del horizonte, en lugar de un tiempo fijo. Ese es el ajuste de la variable "cenit". Puede que te guste http://en.wikipedia.org/wiki/Twilight como fondo, entonces los valores "oficial", "civil", "náutico" y "astronómico" en la página a la que haces referencia deberían tener sentido. Sin siquiera saber el uso de su sensor, sugeriría comenzar con el valor del crepúsculo náutico, es decir, establecer el cenit variable en 102. (¡El crepúsculo náutico es bastante oscuro!)

UPD: Las matemáticas en la página se ven bien. Intenté codificarlo y me dio la respuesta correcta (para mi ubicación hoy de todos modos) en aproximadamente un minuto.

Gracias por señalar el uso de cenit, en realidad esto puede ser más útil que crear un búfer, ya que esencialmente significa "en qué punto consideramos que el sol se puso/salió". He estado usando oficial (90 °) hasta ahora, y esa es probablemente la razón por la que encuentro problemas: ¡todavía no está oscuro!

Según lo solicitado por @Ori, encontré el código que se usó para este propósito. Creo que una versión más nueva de este código eliminó mucha información adicional, ya que solo se necesitaba la Altitud para determinar la cantidad de grados por encima o por debajo del horizonte que tenía el sol para una ubicación determinada.

Hay muchos comentarios que, con suerte, deberían ayudar a cualquiera que sea nuevo en la codificación. Esto fue escrito en C# (.NET) pero debería ser relativamente fácil de adaptar a otros lenguajes. Creo que este código se puede simplificar y reducir mucho, pero lo comparto en su forma original porque puede ser útil para adaptarlo a otros propósitos.

Finalmente, se le da crédito donde se debe, a Paul Schlyter, cuya página web fue muy útil y también se menciona como un enlace en los comentarios.

public static class SunCalc
{
    public struct SunData
    {
        public double Azimuth;
        public double Altitude;
        public double RightAscension;
        public double Declination;
    }

    public static SunData GetSunPosition(DateTime utcTime, double locationLatitude, double locationLongitude)
    {
        var dayNumber = GetDayNumber(utcTime);
        var argumentOfPerihelion = Sun_ArgumentOfPerihelion(dayNumber);
        var eclipticObliquity = EclipticObliquity(dayNumber);
        /*
         * First, compute the eccentric anomaly E from the mean anomaly M and from the eccentricity e (degrees):
         * 
         *      E = M + e*(180/pi) * sin(M) * ( 1.0 + e * cos(M) )
         *      
         * or (if E and M are expressed in radians):
         * 
         *      E = M + e * sin(M) * ( 1.0 + e * cos(M) )
         *      
         */
        var meanAnomaly = Sun_MeanAnomaly(dayNumber);
        var eccentricity = Sun_Eccentricity(dayNumber);
        var eccentricAnomaly = meanAnomaly + (180 / Math.PI) * eccentricity * Math.Sin(Deg2Rad(meanAnomaly)) * (1.0 + eccentricity * Math.Cos(Deg2Rad(meanAnomaly)));

        /*
         * Then compute the Sun's distance r and its true anomaly v from:
         * 
         *      xv = r * cos(v) = cos(E) - e
         *      yv = r * sin(v) = sqrt(1.0 - e*e) * sin(E)
         * 
         *      v = atan2( yv, xv )
         *      r = sqrt( xv*xv + yv*yv )
         * 
         * (note that the r computed here is later used as rs)
         */
        var xv = Math.Cos(Deg2Rad(eccentricAnomaly)) - eccentricity;
        var yv = Math.Sqrt(1.0 - eccentricity * eccentricity) * Math.Sin(Deg2Rad(eccentricAnomaly));
        var v = Rad2Deg(Math.Atan2(yv, xv));
        var r = Math.Sqrt(xv * xv + yv * yv);

        /*
         * Now, compute the Sun's true longitude:
         * 
         *      lonsun = v + w
         */
        var sunTrueLongitude = Rev(v + argumentOfPerihelion);

        /*
         * Convert lonsun, r to ecliptic rectangular geocentric coordinates xs,ys:
         * 
         *     xs = r * cos(lonsun)
         *     ys = r * sin(lonsun)
         */
        var xs = r * Math.Cos(Deg2Rad(sunTrueLongitude));
        var ys = r * Math.Sin(Deg2Rad(sunTrueLongitude));

        /*
         * (since the Sun always is in the ecliptic plane, zs is of course zero).
         * xs,ys is the Sun's position in a coordinate system in the plane of the ecliptic.
         * To convert this to equatorial, rectangular, geocentric coordinates, compute:
         * 
         *     xe = xs
         *     ye = ys * cos(ecl)
         *     ze = ys * sin(ecl)
         */
        var xe = xs;
        var ye = ys * Math.Cos(Deg2Rad(eclipticObliquity));
        var ze = ys * Math.Sin(Deg2Rad(eclipticObliquity));

        /*
         * Finally, compute the Sun's Right Ascension (RA) and Declination (Dec):
         *     RA  = atan2( ye, xe )
         *     Dec = atan2( ze, sqrt(xe*xe+ye*ye) )
         */
        var rightAscension = Rad2Deg(Math.Atan2(ye, xe));
        var declination = Rad2Deg(Math.Atan2(ze, Math.Sqrt(xe * xe + ye * ye)));

        /*
         * Calculate Greenwich Sidereal Time, Sidereal Time and the Sun's Hour Angle
         */
        var sunMeanLongitude = Sun_MeanLongitude(dayNumber);
        var gmst0 = sunMeanLongitude / 15 + 12;

        var siderealTime = RevTime(gmst0 + utcTime.Hour + (utcTime.Minute / 60F) + locationLongitude / 15);
        var hourAngle = RevTime(siderealTime - rightAscension / 15);

        /*
         * Convert the Sun's Hour Angle and Declination to a rectangular coordinate system where the X
         * axis points to the celestial equator in the south, the Y axis to the horizon in the west,
         * and the Z axis to the north celestial pole.
         */
        var x = Math.Cos(Deg2Rad(hourAngle * 15)) * Math.Cos(Deg2Rad(declination));
        var y = Math.Sin(Deg2Rad(hourAngle * 15)) * Math.Cos(Deg2Rad(declination));
        var z = Math.Sin(Deg2Rad(declination));

        /*
         * Rotate this x,y,z axis system along an axis going east-west (Y axis) in such a way that the
         * Z axis will point to the zenith. At the North Pole, the angle of rotation will be zero since
         * there the north celestial pole already is in the zenith. At other latitudes the angle of
         * rotation becomes 90 - latitude.
         */
        var xhor = x * Math.Sin(Deg2Rad(locationLatitude)) - z * Math.Cos(Deg2Rad(locationLatitude));
        var yhor = y;
        var zhor = x * Math.Cos(Deg2Rad(locationLatitude)) + z * Math.Sin(Deg2Rad(locationLatitude));

        /*
         * Compute azimuth and altitude.
         */
        var azimuth = Rad2Deg(Math.Atan2(yhor, xhor)) + 180;
        var altitude = Rad2Deg(Math.Asin(zhor));

        return new SunData
        {
            RightAscension = rightAscension,
            Declination = declination,
            Azimuth = azimuth,
            Altitude = altitude
        };
    }

    private static double GetDayNumber(DateTime dt)
    {
        // http://www.stjarnhimlen.se/comp/ppcomp.html#5
        /*
         * The time scale in these formulae are counted in days. Hours, minutes, seconds are expressed as fractions of a day.
         * Day 0.0 occurs at 2000 Jan 0.0 UT (or 1999 Dec 31, 0:00 UT).
         * This "day number" d is computed as follows (y=year, m=month, D=date, UT=UT in hours+decimals):
         * 
         *      d = 367*y - 7 * ( y + (m+9)/12 ) / 4 + 275*m/9 + D - 730530
         *      
         * Note that ALL divisions here should be INTEGER divisions.
         * Finally, include the time of the day, by adding:
         * 
         *      d = d + UT/24.0        (this is a floating-point division)
         *      
         */

        var d = 367 * dt.Year - 7 * (dt.Year + (dt.Month + 9) / 12) / 4 + 275 * dt.Month / 9 + dt.Day - 730530;
        double hm = dt.Hour + (dt.Minute / 60F);
        return d + hm / 24;
    }

    /// <summary>
    /// Longitude of the Ascending Node (N)
    /// </summary>
    private static double Sun_LongitudeOfAscendingNode()
    {
        return 0.0D;
    }

    /// <summary>
    /// Inclination to the Ecliptic (i) (plane of the Earth's orbit)
    /// </summary>
    private static double Sun_InclinationToEcliptic()
    {
        return 0.0D;
    }

    /// <summary>
    /// Argument of Perihelion (w)
    /// </summary>
    private static double Sun_ArgumentOfPerihelion(double dayNumber)
    {
        return 282.9404 + 4.70935e-5 * dayNumber;
    }

    /// <summary>
    /// Semi-major Axis (a) or mean distance from sun, 1.000000 (AU)
    /// </summary>
    private static double Sun_SemiMajorAxis()
    {
        return 1.0D;
    }

    /// <summary>
    /// Eccentricity (e) where 0 = circle, 0-1 = ellipse, and 1 = parabola
    /// </summary>
    private static double Sun_Eccentricity(double dayNumber)
    {
        return 0.016709 - 1.151e-9 * dayNumber;
    }

    /// <summary>
    /// Mean anomaly (M) (0 at perihelion; increases uniformly with time)
    /// </summary>
    private static double Sun_MeanAnomaly(double dayNumber)
    {
        return Rev(356.0470 + 0.9856002585 * dayNumber);
    }

    /// <summary>
    /// Ecliptic Obliquity (ecl)
    /// </summary>
    private static double EclipticObliquity(double dayNumber)
    {
        return 23.4393 - 3.563e-7 * dayNumber;
    }

    /// <summary>
    /// Longitude of Perihelion (w1)
    /// </summary>
    private static double Sun_LongitudeOfPerihelion(double dayNumber)
    {
        var n = Sun_LongitudeOfAscendingNode();
        var w = Sun_ArgumentOfPerihelion(dayNumber);
        return Rev(n + w);
    }

    /// <summary>
    /// Mean Longitude (L)
    /// </summary>
    private static double Sun_MeanLongitude(double dayNumber)
    {
        var m = Sun_MeanAnomaly(dayNumber);
        var w1 = Sun_LongitudeOfPerihelion(dayNumber);
        return Rev(m + w1);
    }

    /// <summary>
    /// Perihelion Distance (q)
    /// </summary>
    private static double Sun_PerihelionDistance(double dayNumber)
    {
        var a = Sun_SemiMajorAxis();
        var e = Sun_Eccentricity(dayNumber);
        return a * (1 - e);
    }

    /// <summary>
    /// Aphelion Distance (Q)
    /// </summary>
    private static double Sun_AphelionDistance(double dayNumber)
    {
        var a = Sun_SemiMajorAxis();
        var e = Sun_Eccentricity(dayNumber);
        return a * (1 + e);
    }

    /// <summary>
    /// Convert degrees to radians
    /// </summary>
    private static double Deg2Rad(double angleDegrees)
    {
        return angleDegrees * (Math.PI / 180.0);
    }

    /// <summary>
    /// Convert radians to degrees
    /// </summary>
    private static double Rad2Deg(double angleRadians)
    {
        return angleRadians * (180.0 / Math.PI);
    }

    /// <summary>
    /// Revolution function, normalizes an angle to between 0 and 360 degrees by adding or subtracting even multiples of 360.
    /// </summary>
    private static double Rev(double x)
    {
        return x - Math.Floor(x / 360.0) * 360.0;
    }

    /// <summary>
    /// Revolution function, normalizes a time value (in hours) to between 0 and 24 by adding or subtracting even multiples of 24.
    /// </summary>
    private static double RevTime(double x)
    {
        return x - Math.Floor(x / 24.0) * 24.0;
    }
}