From 06b62887d2ba9bc5f5594f68f93dc5de06b830cd Mon Sep 17 00:00:00 2001 From: Don Cross Date: Thu, 20 Oct 2022 17:31:21 -0400 Subject: [PATCH] Kotlin: obscuration for solar, lunar eclipses. --- demo/java/correct/lunar_eclipse.txt | 80 +++++----- demo/kotlin/correct/lunar_eclipse.txt | 80 +++++----- generate/dotnet/csharp_test/csharp_test.cs | 3 +- generate/template/astronomy.kt | 149 +++++++++++++++--- generate/test.js | 2 +- generate/test.py | 2 +- source/kotlin/README.md | 6 +- .../-global-solar-eclipse-info.md | 2 +- .../doc/-global-solar-eclipse-info/index.md | 7 +- .../-global-solar-eclipse-info/obscuration.md | 7 + .../-local-solar-eclipse-info.md | 2 +- .../doc/-local-solar-eclipse-info/index.md | 7 +- .../-local-solar-eclipse-info/obscuration.md | 7 + .../-lunar-eclipse-info.md | 2 +- .../kotlin/doc/-lunar-eclipse-info/index.md | 7 +- .../doc/-lunar-eclipse-info/obscuration.md | 7 + source/kotlin/doc/index.md | 6 +- .../github/cosinekitty/astronomy/astronomy.kt | 149 +++++++++++++++--- .../io/github/cosinekitty/astronomy/Tests.kt | 120 +++++++++++++- 19 files changed, 498 insertions(+), 147 deletions(-) create mode 100644 source/kotlin/doc/-global-solar-eclipse-info/obscuration.md create mode 100644 source/kotlin/doc/-local-solar-eclipse-info/obscuration.md create mode 100644 source/kotlin/doc/-lunar-eclipse-info/obscuration.md diff --git a/demo/java/correct/lunar_eclipse.txt b/demo/java/correct/lunar_eclipse.txt index 9cd4f41c..77a70ca6 100644 --- a/demo/java/correct/lunar_eclipse.txt +++ b/demo/java/correct/lunar_eclipse.txt @@ -1,50 +1,50 @@ -1988-03-03T16:04:42.484Z Partial eclipse begins. -1988-03-03T16:13:29.227Z Peak of partial eclipse. -1988-03-03T16:22:15.969Z Partial eclipse ends. +1988-03-03T16:05:01.550Z Partial eclipse begins. +1988-03-03T16:12:44.158Z Peak of partial eclipse. +1988-03-03T16:20:26.766Z Partial eclipse ends. -1988-08-27T10:07:57.337Z Partial eclipse begins. -1988-08-27T11:05:04.752Z Peak of partial eclipse. -1988-08-27T12:02:12.167Z Partial eclipse ends. +1988-08-27T10:07:28.657Z Partial eclipse begins. +1988-08-27T11:04:30.914Z Peak of partial eclipse. +1988-08-27T12:01:33.171Z Partial eclipse ends. -1989-02-20T13:44:07.826Z Partial eclipse begins. -1989-02-20T14:56:15.429Z Total eclipse begins. -1989-02-20T15:36:04.477Z Peak of total eclipse. -1989-02-20T16:15:53.525Z Total eclipse ends. -1989-02-20T17:28:01.129Z Partial eclipse ends. +1989-02-20T13:43:24.717Z Partial eclipse begins. +1989-02-20T14:55:34.838Z Total eclipse begins. +1989-02-20T15:35:19.955Z Peak of total eclipse. +1989-02-20T16:15:05.072Z Total eclipse ends. +1989-02-20T17:27:15.193Z Partial eclipse ends. -1989-08-17T01:21:19.201Z Partial eclipse begins. -1989-08-17T02:20:31.653Z Total eclipse begins. -1989-08-17T03:08:46.679Z Peak of total eclipse. -1989-08-17T03:57:01.705Z Total eclipse ends. -1989-08-17T04:56:14.157Z Partial eclipse ends. +1989-08-17T01:20:43.868Z Partial eclipse begins. +1989-08-17T02:19:56.975Z Total eclipse begins. +1989-08-17T03:08:10.847Z Peak of total eclipse. +1989-08-17T03:56:24.718Z Total eclipse ends. +1989-08-17T04:55:37.826Z Partial eclipse ends. -1990-02-09T17:29:19.371Z Partial eclipse begins. -1990-02-09T18:50:02.142Z Total eclipse begins. -1990-02-09T19:11:46.638Z Peak of total eclipse. -1990-02-09T19:33:31.134Z Total eclipse ends. -1990-02-09T20:54:13.906Z Partial eclipse ends. +1990-02-09T17:28:36.915Z Partial eclipse begins. +1990-02-09T18:49:12.803Z Total eclipse begins. +1990-02-09T19:11:06.009Z Peak of total eclipse. +1990-02-09T19:32:59.214Z Total eclipse ends. +1990-02-09T20:53:35.102Z Partial eclipse ends. -1990-08-06T12:44:54.289Z Partial eclipse begins. -1990-08-06T14:13:00.287Z Peak of partial eclipse. -1990-08-06T15:41:06.284Z Partial eclipse ends. +1990-08-06T12:44:10.958Z Partial eclipse begins. +1990-08-06T14:12:20.198Z Peak of partial eclipse. +1990-08-06T15:40:29.439Z Partial eclipse ends. -1991-12-21T10:00:27.418Z Partial eclipse begins. -1991-12-21T10:33:39.369Z Peak of partial eclipse. -1991-12-21T11:06:51.320Z Partial eclipse ends. +1991-12-21T10:00:02.660Z Partial eclipse begins. +1991-12-21T10:33:04.083Z Peak of partial eclipse. +1991-12-21T11:06:05.507Z Partial eclipse ends. -1992-06-15T03:27:15.070Z Partial eclipse begins. -1992-06-15T04:57:38.272Z Peak of partial eclipse. -1992-06-15T06:28:01.473Z Partial eclipse ends. +1992-06-15T03:26:36.541Z Partial eclipse begins. +1992-06-15T04:56:56.391Z Peak of partial eclipse. +1992-06-15T06:27:16.242Z Partial eclipse ends. -1992-12-09T21:59:59.280Z Partial eclipse begins. -1992-12-09T23:07:16.402Z Total eclipse begins. -1992-12-09T23:44:42.637Z Peak of total eclipse. -1992-12-10T00:22:08.872Z Total eclipse ends. -1992-12-10T01:29:25.994Z Partial eclipse ends. +1992-12-09T21:59:22.079Z Partial eclipse begins. +1992-12-09T23:06:41.497Z Total eclipse begins. +1992-12-09T23:44:04.198Z Peak of total eclipse. +1992-12-10T00:21:26.898Z Total eclipse ends. +1992-12-10T01:28:46.316Z Partial eclipse ends. -1993-06-04T11:11:48.333Z Partial eclipse begins. -1993-06-04T12:12:49.448Z Total eclipse begins. -1993-06-04T13:01:01.724Z Peak of total eclipse. -1993-06-04T13:49:14.000Z Total eclipse ends. -1993-06-04T14:50:15.116Z Partial eclipse ends. +1993-06-04T11:11:10.309Z Partial eclipse begins. +1993-06-04T12:12:10.636Z Total eclipse begins. +1993-06-04T13:00:24.283Z Peak of total eclipse. +1993-06-04T13:48:37.931Z Total eclipse ends. +1993-06-04T14:49:38.257Z Partial eclipse ends. diff --git a/demo/kotlin/correct/lunar_eclipse.txt b/demo/kotlin/correct/lunar_eclipse.txt index 9cd4f41c..77a70ca6 100644 --- a/demo/kotlin/correct/lunar_eclipse.txt +++ b/demo/kotlin/correct/lunar_eclipse.txt @@ -1,50 +1,50 @@ -1988-03-03T16:04:42.484Z Partial eclipse begins. -1988-03-03T16:13:29.227Z Peak of partial eclipse. -1988-03-03T16:22:15.969Z Partial eclipse ends. +1988-03-03T16:05:01.550Z Partial eclipse begins. +1988-03-03T16:12:44.158Z Peak of partial eclipse. +1988-03-03T16:20:26.766Z Partial eclipse ends. -1988-08-27T10:07:57.337Z Partial eclipse begins. -1988-08-27T11:05:04.752Z Peak of partial eclipse. -1988-08-27T12:02:12.167Z Partial eclipse ends. +1988-08-27T10:07:28.657Z Partial eclipse begins. +1988-08-27T11:04:30.914Z Peak of partial eclipse. +1988-08-27T12:01:33.171Z Partial eclipse ends. -1989-02-20T13:44:07.826Z Partial eclipse begins. -1989-02-20T14:56:15.429Z Total eclipse begins. -1989-02-20T15:36:04.477Z Peak of total eclipse. -1989-02-20T16:15:53.525Z Total eclipse ends. -1989-02-20T17:28:01.129Z Partial eclipse ends. +1989-02-20T13:43:24.717Z Partial eclipse begins. +1989-02-20T14:55:34.838Z Total eclipse begins. +1989-02-20T15:35:19.955Z Peak of total eclipse. +1989-02-20T16:15:05.072Z Total eclipse ends. +1989-02-20T17:27:15.193Z Partial eclipse ends. -1989-08-17T01:21:19.201Z Partial eclipse begins. -1989-08-17T02:20:31.653Z Total eclipse begins. -1989-08-17T03:08:46.679Z Peak of total eclipse. -1989-08-17T03:57:01.705Z Total eclipse ends. -1989-08-17T04:56:14.157Z Partial eclipse ends. +1989-08-17T01:20:43.868Z Partial eclipse begins. +1989-08-17T02:19:56.975Z Total eclipse begins. +1989-08-17T03:08:10.847Z Peak of total eclipse. +1989-08-17T03:56:24.718Z Total eclipse ends. +1989-08-17T04:55:37.826Z Partial eclipse ends. -1990-02-09T17:29:19.371Z Partial eclipse begins. -1990-02-09T18:50:02.142Z Total eclipse begins. -1990-02-09T19:11:46.638Z Peak of total eclipse. -1990-02-09T19:33:31.134Z Total eclipse ends. -1990-02-09T20:54:13.906Z Partial eclipse ends. +1990-02-09T17:28:36.915Z Partial eclipse begins. +1990-02-09T18:49:12.803Z Total eclipse begins. +1990-02-09T19:11:06.009Z Peak of total eclipse. +1990-02-09T19:32:59.214Z Total eclipse ends. +1990-02-09T20:53:35.102Z Partial eclipse ends. -1990-08-06T12:44:54.289Z Partial eclipse begins. -1990-08-06T14:13:00.287Z Peak of partial eclipse. -1990-08-06T15:41:06.284Z Partial eclipse ends. +1990-08-06T12:44:10.958Z Partial eclipse begins. +1990-08-06T14:12:20.198Z Peak of partial eclipse. +1990-08-06T15:40:29.439Z Partial eclipse ends. -1991-12-21T10:00:27.418Z Partial eclipse begins. -1991-12-21T10:33:39.369Z Peak of partial eclipse. -1991-12-21T11:06:51.320Z Partial eclipse ends. +1991-12-21T10:00:02.660Z Partial eclipse begins. +1991-12-21T10:33:04.083Z Peak of partial eclipse. +1991-12-21T11:06:05.507Z Partial eclipse ends. -1992-06-15T03:27:15.070Z Partial eclipse begins. -1992-06-15T04:57:38.272Z Peak of partial eclipse. -1992-06-15T06:28:01.473Z Partial eclipse ends. +1992-06-15T03:26:36.541Z Partial eclipse begins. +1992-06-15T04:56:56.391Z Peak of partial eclipse. +1992-06-15T06:27:16.242Z Partial eclipse ends. -1992-12-09T21:59:59.280Z Partial eclipse begins. -1992-12-09T23:07:16.402Z Total eclipse begins. -1992-12-09T23:44:42.637Z Peak of total eclipse. -1992-12-10T00:22:08.872Z Total eclipse ends. -1992-12-10T01:29:25.994Z Partial eclipse ends. +1992-12-09T21:59:22.079Z Partial eclipse begins. +1992-12-09T23:06:41.497Z Total eclipse begins. +1992-12-09T23:44:04.198Z Peak of total eclipse. +1992-12-10T00:21:26.898Z Total eclipse ends. +1992-12-10T01:28:46.316Z Partial eclipse ends. -1993-06-04T11:11:48.333Z Partial eclipse begins. -1993-06-04T12:12:49.448Z Total eclipse begins. -1993-06-04T13:01:01.724Z Peak of total eclipse. -1993-06-04T13:49:14.000Z Total eclipse ends. -1993-06-04T14:50:15.116Z Partial eclipse ends. +1993-06-04T11:11:10.309Z Partial eclipse begins. +1993-06-04T12:12:10.636Z Total eclipse begins. +1993-06-04T13:00:24.283Z Peak of total eclipse. +1993-06-04T13:48:37.931Z Total eclipse ends. +1993-06-04T14:49:38.257Z Partial eclipse ends. diff --git a/generate/dotnet/csharp_test/csharp_test.cs b/generate/dotnet/csharp_test/csharp_test.cs index c6ac9b30..3553ac79 100644 --- a/generate/dotnet/csharp_test/csharp_test.cs +++ b/generate/dotnet/csharp_test/csharp_test.cs @@ -2929,7 +2929,7 @@ namespace csharp_test } double diff = v(eclipse.obscuration - obscuration); - if (diff > tolerance) + if (Math.Abs(diff) > tolerance) { Console.WriteLine($"C# LocalSolarCase({year:0000}-{month:00}-{day:00}) FAIL: obscuration diff = {diff:F8}, expected = {obscuration:F8}, calculated = {eclipse.obscuration:F8}."); return 1; @@ -2943,7 +2943,6 @@ namespace csharp_test // Verify global solar eclipse obscurations for annular eclipses only. // This is because they are the only nontrivial values for global solar eclipses. // The trivial values are all validated exactly by GlobalSolarEclipseTest(). - if (0 != GlobalAnnularCase(2023, 10, 14, 0.90638)) return 1; // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2023Oct14Aprime.html if (0 != GlobalAnnularCase(2024, 10, 2, 0.86975)) return 1; // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Oct02Aprime.html if (0 != GlobalAnnularCase(2027, 2, 6, 0.86139)) return 1; // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2027Feb06Aprime.html diff --git a/generate/template/astronomy.kt b/generate/template/astronomy.kt index f75f50d7..813d4994 100644 --- a/generate/template/astronomy.kt +++ b/generate/template/astronomy.kt @@ -179,9 +179,10 @@ private const val EARTH_MEAN_RADIUS_KM = 6371.0 // mean radius of the Earth's private const val EARTH_ATMOSPHERE_KM = 88.0 // effective atmosphere thickness for lunar eclipses private const val EARTH_ECLIPSE_RADIUS_KM = EARTH_MEAN_RADIUS_KM + EARTH_ATMOSPHERE_KM private const val MOON_EQUATORIAL_RADIUS_KM = 1738.1 +private const val MOON_EQUATORIAL_RADIUS_AU = (MOON_EQUATORIAL_RADIUS_KM / KM_PER_AU) private const val MOON_MEAN_RADIUS_KM = 1737.4 private const val MOON_POLAR_RADIUS_KM = 1736.0 -private const val MOON_EQUATORIAL_RADIUS_AU = (MOON_EQUATORIAL_RADIUS_KM / KM_PER_AU) +private const val MOON_POLAR_RADIUS_AU = (MOON_POLAR_RADIUS_KM / KM_PER_AU) private const val ANGVEL = 7.2921150e-5 private const val SOLAR_DAYS_PER_SIDEREAL_DAY = 0.9972695717592592 private const val MEAN_SYNODIC_MONTH = 29.530588 // average number of days for Moon to return to the same phase @@ -1790,6 +1791,12 @@ enum class EclipseKind { * The `kind` field thus holds `EclipseKind.Penumbral`, `EclipseKind.Partial`, * or `EclipseKind.Total`, depending on the kind of lunar eclipse found. * + * The `obscuration` field holds a value in the range [0, 1] that indicates what fraction + * of the Moon's apparent disc area is covered by the Earth's umbra at the eclipse's peak. + * This indicates how dark the peak eclipse appears. For penumbral eclipses, the obscuration + * is 0, because the Moon does not pass through the Earth's umbra. For partial eclipses, + * the obscuration is somewhere between 0 and 1. For total lunar eclipses, the obscuration is 1. + * * Field `peak` holds the date and time of the center of the eclipse, when it is at its peak. * * Fields `sdPenum`, `sdPartial`, and `sdTotal` hold the semi-duration of each phase @@ -1804,6 +1811,11 @@ class LunarEclipseInfo( */ val kind: EclipseKind, + /** + * The peak fraction of the Moon's apparent disc that is covered by the Earth's umbra. + */ + val obscuration: Double, + /** * The time of the eclipse at its peak. */ @@ -1848,6 +1860,17 @@ class LunarEclipseInfo( * onto the daytime side of the Earth at the instant of the eclipse's peak. * If `kind` has any other value, `latitude` and `longitude` are undefined and should * not be used. + * + * For total or annular eclipses, the `obscuration` field holds the fraction (0, 1] + * of the Sun's apparent disc area that is blocked from view by the Moon's silhouette, + * as seen by an observer located at the geographic coordinates `latitude`, `longitude` + * at the darkest time `peak`. The value will always be 1 for total eclipses, and less than + * 1 for annular eclipses. + * For partial eclipses, `obscuration` is undefined and should not be used. + * This is because there is little practical use for an obscuration value of + * a partial eclipse without supplying a particular observation location. + * Developers who wish to find an obscuration value for partial solar eclipses should therefore use + * [searchLocalSolarEclipse] and provide the geographic coordinates of an observer. */ class GlobalSolarEclipseInfo( /** @@ -1855,6 +1878,11 @@ class GlobalSolarEclipseInfo( */ val kind: EclipseKind, + /** + * The peak fraction of the Sun's apparent disc area obscured by the Moon (total and annular eclipses only). + */ + val obscuration: Double, + /** * The date and time when the solar eclipse is darkest. * This is the instant when the axis of the Moon's shadow cone passes closest to the Earth's center. @@ -1922,6 +1950,13 @@ class EclipseEvent ( * A total eclipse occurs when the Moon is close enough to the Earth and aligned with the * Sun just right to completely block all sunlight from reaching the observer. * + * The `obscuration` field reports what fraction of the Sun's disc appears blocked + * by the Moon when viewed by the observer at the peak eclipse time. + * This is a value that ranges from 0 (no blockage) to 1 (total eclipse). + * The obscuration value will be between 0 and 1 for partial eclipses and annular eclipses. + * The value will be exactly 1 for total eclipses. Obscuration gives an indication + * of how dark the eclipse appears. + * * There are 5 "event" fields, each of which contains a time and a solar altitude. * Field `peak` holds the date and time of the center of the eclipse, when it is at its peak. * The fields `partialBegin` and `partialEnd` are always set, and indicate when @@ -1937,6 +1972,11 @@ class LocalSolarEclipseInfo ( */ val kind: EclipseKind, + /** + * The fraction of the Sun's apparent disc area obscured by the Moon at the eclipse peak. + */ + val obscuration: Double, + /** * The time and Sun altitude at the beginning of the eclipse. */ @@ -2054,44 +2094,42 @@ internal fun calcShadow( internal fun earthShadow(time: Time): ShadowInfo { // This function helps find when the Earth's shadow falls upon the Moon. - val e = helioEarthPos(time) + val s = geoVector(Body.Sun, time, Aberration.Corrected) val m = geoMoon(time) - return calcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, e) + return calcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, -s) } internal fun moonShadow(time: Time): ShadowInfo { // This function helps find when the Moon's shadow falls upon the Earth. - // This is a variation on the logic in EarthShadow(). - // Instead of a heliocentric Earth and a geocentric Moon, - // we want a heliocentric Moon and a lunacentric Earth. - - val e = helioEarthPos(time) + val s = geoVector(Body.Sun, time, Aberration.Corrected) val m = geoMoon(time) // -m = lunacentric Earth - // m+e = heliocentric Moon - return calcShadow(MOON_MEAN_RADIUS_KM, time, -m, m+e) + // m-s = heliocentric Moon + return calcShadow(MOON_MEAN_RADIUS_KM, time, -m, m-s) } internal fun localMoonShadow(time: Time, observer: Observer): ShadowInfo { // Calculate observer's geocentric position. - // For efficiency, do this first, to populate the earth rotation parameters in 'time'. - // That way they can be recycled instead of recalculated. val o = geoPos(time, observer) - val h = helioEarthPos(time) + + // Calculate light-travel and aberration corrected Sun. + val s = geoVector(Body.Sun, time, Aberration.Corrected) + + // Calculate geocentric Moon. val m = geoMoon(time) // o-m = lunacentric observer - // m+h = heliocentric Moon - return calcShadow(MOON_MEAN_RADIUS_KM, time, o-m, m+h) + // m-s = heliocentric Moon + return calcShadow(MOON_MEAN_RADIUS_KM, time, o-m, m-s) } internal fun planetShadow(body: Body, planetRadiusKm: Double, time: Time): ShadowInfo { // Calculate light-travel-corrected vector from Earth to planet. - val g = geoVector(body, time, Aberration.None) + val g = geoVector(body, time, Aberration.Corrected) // Calculate light-travel-corrected vector from Earth to Sun. - val e = geoVector(Body.Sun, time, Aberration.None) + val e = geoVector(Body.Sun, time, Aberration.Corrected) // -g = planetocentric Earth // g-e = heliocentric planet @@ -2137,7 +2175,6 @@ internal fun peakEarthShadow(searchCenterTime: Time): ShadowInfo { return earthShadow(tx) } - internal val moonShadowSlopeContext = SearchContext { time -> val dt = 1.0 / SECONDS_PER_DAY val t1 = time.addDays(-dt) @@ -2196,6 +2233,69 @@ internal fun planetTransitBoundary(body: Body, planetRadiusKm: Double, t1: Time, } ?: throw InternalError("Planet transit boundary search failed.") } +internal fun discObscuration(a: Double, b: Double, c: Double): Double { + // a = radius of first disc + // b = radius of second disc + // c = distance between centers of discs + if (a <= 0.0) throw InternalError("Radius of first disc must be positive.") + if (b <= 0.0) throw InternalError("Radius of second disc must be positive.") + if (c < 0.0) throw InternalError("Distance between discs is not allowed to be negative.") + + if (c >= a + b) { + // The discs are too far apart to have any overlapping area. + return 0.0 + } + + if (c == 0.0) { + // The discs have a common center. Therefore, one disc is inside the other. + return if (a <= b) 1.0 else (b*b)/(a*a) + } + + val x = (a*a - b*b + c*c) / (2*c) + val radicand = a*a - x*x + if (radicand <= 0.0) { + // The circumferences do not intersect, or are tangent. + // We already ruled out the case of non-overlapping discs. + // Therefore, one disc is inside the other. + return if (a <= b) 1.0 else (b*b)/(a*a) + } + + // The discs overlap fractionally in a pair of lens-shaped areas. + + val y = sqrt(radicand) + + // Return the overlapping fractional area. + // There are two lens-shaped areas, one to the left of x, the other to the right of x. + // Each part is calculated by subtracting a triangular area from a sector's area. + val lens1 = a*a*acos(x/a) - x*y + val lens2 = b*b*acos((c-x)/b) - (c-x)*y + + // Find the fractional area with respect to the first disc. + return (lens1 + lens2) / (PI*a*a) +} + + +internal fun solarEclipseObscuration(hm: Vector, lo: Vector): Double { + // Find heliocentric observer. + val ho = hm + lo + + // Calculate the apparent angular radius of the Sun for the observer. + val sunRadius = asin(SUN_RADIUS_AU / ho.length()) + + // Calculate the apparent angular radius of the Moon for the observer. + val moonRadius = asin(MOON_POLAR_RADIUS_AU / lo.length()) + + // Calculate the apparent angular separation between the Sun's center and the Moon's center. + val sunMoonSeparation = lo.angleWith(ho).degreesToRadians() + + // Find the fraction of the Sun's apparent disc area that is covered by the Moon. + val obscuration = discObscuration(sunRadius, moonRadius, sunMoonSeparation) + + // HACK: In marginal cases, we need to clamp obscuration to less than 1.0. + // This function is never called for total eclipses, so it should never return 1.0. + return min(0.9999, obscuration) +} + /** * Searches for a lunar eclipse. @@ -2234,6 +2334,7 @@ fun searchLunarEclipse(startTime: Time): LunarEclipseInfo { if (shadow.r < shadow.p + MOON_MEAN_RADIUS_KM) { // This is at least a penumbral eclipse. We will return a result. var kind = EclipseKind.Penumbral + var obscuration = 0.0 val sdPenum = shadowSemiDurationMinutes(shadow.time, shadow.p + MOON_MEAN_RADIUS_KM, 200.0) var sdPartial = 0.0 var sdTotal = 0.0 @@ -2246,11 +2347,14 @@ fun searchLunarEclipse(startTime: Time): LunarEclipseInfo { if (shadow.r + MOON_MEAN_RADIUS_KM < shadow.k) { // This is a total eclipse. kind = EclipseKind.Total + obscuration = 1.0 sdTotal = shadowSemiDurationMinutes(shadow.time, shadow.k - MOON_MEAN_RADIUS_KM, sdPartial) + } else { + obscuration = discObscuration(MOON_MEAN_RADIUS_KM, shadow.k, shadow.r) } } - return LunarEclipseInfo(kind, shadow.time, sdPenum, sdPartial, sdTotal) + return LunarEclipseInfo(kind, obscuration, shadow.time, sdPenum, sdPartial, sdTotal) } } @@ -2324,6 +2428,7 @@ internal fun eclipseKindFromUmbra(k: Double) = ( internal fun geoidIntersect(shadow: ShadowInfo): GlobalSolarEclipseInfo { var kind = EclipseKind.Partial + var obscuration = Double.NaN var latitude = Double.NaN var longitude = Double.NaN @@ -2385,9 +2490,10 @@ internal fun geoidIntersect(shadow: ShadowInfo): GlobalSolarEclipseInfo { throw InternalError("Invalid surface distance from intersection.") kind = eclipseKindFromUmbra(surface.k) + obscuration = if (kind == EclipseKind.Total) 1.0 else solarEclipseObscuration(shadow.dir, luna) } - return GlobalSolarEclipseInfo(kind, shadow.time, shadow.r, latitude, longitude) + return GlobalSolarEclipseInfo(kind, obscuration, shadow.time, shadow.r, latitude, longitude) } @@ -2549,7 +2655,8 @@ internal fun localEclipse(shadow: ShadowInfo, observer: Observer): LocalSolarEcl } else { kind = EclipseKind.Partial } - return LocalSolarEclipseInfo(kind, partialBegin, totalBegin, peak, totalEnd, partialEnd) + val obscuration = if (kind == EclipseKind.Total) 1.0 else solarEclipseObscuration(shadow.dir, shadow.target) + return LocalSolarEclipseInfo(kind, obscuration, partialBegin, totalBegin, peak, totalEnd, partialEnd) } diff --git a/generate/test.js b/generate/test.js index d2f82ec3..baf7230a 100644 --- a/generate/test.js +++ b/generate/test.js @@ -948,7 +948,7 @@ function LocalSolarCase(dateText, latitude, longitude, kind, obscuration, tolera } const diff = v(eclipse.obscuration - obscuration); - if (diff > tolerance) { + if (abs(diff) > tolerance) { console.error(`JS LocalSolarCase(${dateText}) FAIL: obscuration diff = ${diff}, expected = ${obscuration}, actual = ${eclipse.obscuration}.`); return 1; } diff --git a/generate/test.py b/generate/test.py index 4e1c68e2..ab6ae1fb 100755 --- a/generate/test.py +++ b/generate/test.py @@ -1796,7 +1796,7 @@ def LocalSolarCase(year, month, day, latitude, longitude, kind, obscuration, tol return Fail(funcname, 'expected {} eclipse, but found {}.'.format(kind, eclipse.kind)) diff = v(eclipse.obscuration - obscuration) - if diff > tolerance: + if abs(diff) > tolerance: return Fail(funcname, 'obscuration diff = {:0.8f}, expected = {:0.8f}, actual = {:0.8f}'.format(diff, obscuration, eclipse.obscuration)) Debug('{}: obscuration diff = {:11.8f}'.format(funcname, diff)) diff --git a/source/kotlin/README.md b/source/kotlin/README.md index 7313e432..23aa886b 100644 --- a/source/kotlin/README.md +++ b/source/kotlin/README.md @@ -89,7 +89,7 @@ movement through the Solar System. | [ElongationInfo](doc/-elongation-info/index.md)
class [ElongationInfo](doc/-elongation-info/index.md)(time: [Time](doc/-time/index.md), visibility: [Visibility](doc/-visibility/index.md), elongation: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), eclipticSeparation: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Contains information about the visibility of a celestial body at a given date and time. | | [EquatorEpoch](doc/-equator-epoch/index.md)
enum [EquatorEpoch](doc/-equator-epoch/index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[EquatorEpoch](doc/-equator-epoch/index.md)>
Selects the date for which the Earth's equator is to be used for representing equatorial coordinates. | | [Equatorial](doc/-equatorial/index.md)
class [Equatorial](doc/-equatorial/index.md)(ra: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), dec: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), dist: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), vec: [Vector](doc/-vector/index.md))
Equatorial angular and cartesian coordinates. | -| [GlobalSolarEclipseInfo](doc/-global-solar-eclipse-info/index.md)
class [GlobalSolarEclipseInfo](doc/-global-solar-eclipse-info/index.md)(kind: [EclipseKind](doc/-eclipse-kind/index.md), peak: [Time](doc/-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Reports the time and geographic location of the peak of a solar eclipse. | +| [GlobalSolarEclipseInfo](doc/-global-solar-eclipse-info/index.md)
class [GlobalSolarEclipseInfo](doc/-global-solar-eclipse-info/index.md)(kind: [EclipseKind](doc/-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](doc/-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Reports the time and geographic location of the peak of a solar eclipse. | | [GravitySimulator](doc/-gravity-simulator/index.md)
class [GravitySimulator](doc/-gravity-simulator/index.md)
A simulation of zero or more small bodies moving through the Solar System. | | [HourAngleInfo](doc/-hour-angle-info/index.md)
class [HourAngleInfo](doc/-hour-angle-info/index.md)(time: [Time](doc/-time/index.md), hor: [Topocentric](doc/-topocentric/index.md))
Information about a celestial body crossing a specific hour angle. | | [IlluminationInfo](doc/-illumination-info/index.md)
class [IlluminationInfo](doc/-illumination-info/index.md)(time: [Time](doc/-time/index.md), mag: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), phaseAngle: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), phaseFraction: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), helioDist: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), ringTilt: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Information about the brightness and illuminated shape of a celestial body. | @@ -97,8 +97,8 @@ movement through the Solar System. | [InvalidBodyException](doc/-invalid-body-exception/index.md)
class [InvalidBodyException](doc/-invalid-body-exception/index.md)(body: [Body](doc/-body/index.md)) : [Exception](https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html)
An invalid body was specified for the given function. | | [JupiterMoonsInfo](doc/-jupiter-moons-info/index.md)
class [JupiterMoonsInfo](doc/-jupiter-moons-info/index.md)(io: [StateVector](doc/-state-vector/index.md), europa: [StateVector](doc/-state-vector/index.md), ganymede: [StateVector](doc/-state-vector/index.md), callisto: [StateVector](doc/-state-vector/index.md))
Holds the positions and velocities of Jupiter's major 4 moons. | | [LibrationInfo](doc/-libration-info/index.md)
data class [LibrationInfo](doc/-libration-info/index.md)(elat: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), elon: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), mlat: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), mlon: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), distanceKm: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), diamDeg: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Lunar libration angles, returned by [libration](doc/libration.md). | -| [LocalSolarEclipseInfo](doc/-local-solar-eclipse-info/index.md)
class [LocalSolarEclipseInfo](doc/-local-solar-eclipse-info/index.md)(kind: [EclipseKind](doc/-eclipse-kind/index.md), partialBegin: [EclipseEvent](doc/-eclipse-event/index.md), totalBegin: [EclipseEvent](doc/-eclipse-event/index.md)?, peak: [EclipseEvent](doc/-eclipse-event/index.md), totalEnd: [EclipseEvent](doc/-eclipse-event/index.md)?, partialEnd: [EclipseEvent](doc/-eclipse-event/index.md))
Information about a solar eclipse as seen by an observer at a given time and geographic location. | -| [LunarEclipseInfo](doc/-lunar-eclipse-info/index.md)
class [LunarEclipseInfo](doc/-lunar-eclipse-info/index.md)(kind: [EclipseKind](doc/-eclipse-kind/index.md), peak: [Time](doc/-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Information about a lunar eclipse. | +| [LocalSolarEclipseInfo](doc/-local-solar-eclipse-info/index.md)
class [LocalSolarEclipseInfo](doc/-local-solar-eclipse-info/index.md)(kind: [EclipseKind](doc/-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), partialBegin: [EclipseEvent](doc/-eclipse-event/index.md), totalBegin: [EclipseEvent](doc/-eclipse-event/index.md)?, peak: [EclipseEvent](doc/-eclipse-event/index.md), totalEnd: [EclipseEvent](doc/-eclipse-event/index.md)?, partialEnd: [EclipseEvent](doc/-eclipse-event/index.md))
Information about a solar eclipse as seen by an observer at a given time and geographic location. | +| [LunarEclipseInfo](doc/-lunar-eclipse-info/index.md)
class [LunarEclipseInfo](doc/-lunar-eclipse-info/index.md)(kind: [EclipseKind](doc/-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](doc/-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Information about a lunar eclipse. | | [MoonQuarterInfo](doc/-moon-quarter-info/index.md)
class [MoonQuarterInfo](doc/-moon-quarter-info/index.md)(quarter: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html), time: [Time](doc/-time/index.md))
A lunar quarter event (new moon, first quarter, full moon, or third quarter) along with its date and time. | | [NodeEventInfo](doc/-node-event-info/index.md)
class [NodeEventInfo](doc/-node-event-info/index.md)(time: [Time](doc/-time/index.md), kind: [NodeEventKind](doc/-node-event-kind/index.md))
Information about an ascending or descending node of a body. | | [NodeEventKind](doc/-node-event-kind/index.md)
enum [NodeEventKind](doc/-node-event-kind/index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[NodeEventKind](doc/-node-event-kind/index.md)>
Indicates whether a crossing through the ecliptic plane is ascending or descending. | diff --git a/source/kotlin/doc/-global-solar-eclipse-info/-global-solar-eclipse-info.md b/source/kotlin/doc/-global-solar-eclipse-info/-global-solar-eclipse-info.md index 4e96fa53..835fac1a 100644 --- a/source/kotlin/doc/-global-solar-eclipse-info/-global-solar-eclipse-info.md +++ b/source/kotlin/doc/-global-solar-eclipse-info/-global-solar-eclipse-info.md @@ -2,4 +2,4 @@ # GlobalSolarEclipseInfo -fun [GlobalSolarEclipseInfo](-global-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), peak: [Time](../-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) +fun [GlobalSolarEclipseInfo](-global-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](../-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) diff --git a/source/kotlin/doc/-global-solar-eclipse-info/index.md b/source/kotlin/doc/-global-solar-eclipse-info/index.md index 8f800945..5d359723 100644 --- a/source/kotlin/doc/-global-solar-eclipse-info/index.md +++ b/source/kotlin/doc/-global-solar-eclipse-info/index.md @@ -2,7 +2,7 @@ # GlobalSolarEclipseInfo -class [GlobalSolarEclipseInfo](index.md)(kind: [EclipseKind](../-eclipse-kind/index.md), peak: [Time](../-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) +class [GlobalSolarEclipseInfo](index.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](../-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) Reports the time and geographic location of the peak of a solar eclipse. @@ -14,11 +14,13 @@ The kind field thus holds EclipseKind.Partial, EclipseKind.Annular, or EclipseKi If kind is EclipseKind.Total or EclipseKind.Annular, the latitude and longitude fields give the geographic coordinates of the center of the Moon's shadow projected onto the daytime side of the Earth at the instant of the eclipse's peak. If kind has any other value, latitude and longitude are undefined and should not be used. +For total or annular eclipses, the obscuration field holds the fraction (0, 1] of the Sun's apparent disc area that is blocked from view by the Moon's silhouette, as seen by an observer located at the geographic coordinates latitude, longitude at the darkest time peak. The value will always be 1 for total eclipses, and less than 1 for annular eclipses. For partial eclipses, obscuration is undefined and should not be used. This is because there is little practical use for an obscuration value of a partial eclipse without supplying a particular observation location. Developers who wish to find an obscuration value for partial solar eclipses should therefore use [searchLocalSolarEclipse](../search-local-solar-eclipse.md) and provide the geographic coordinates of an observer. + ## Constructors | | | |---|---| -| [GlobalSolarEclipseInfo](-global-solar-eclipse-info.md)
fun [GlobalSolarEclipseInfo](-global-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), peak: [Time](../-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) | +| [GlobalSolarEclipseInfo](-global-solar-eclipse-info.md)
fun [GlobalSolarEclipseInfo](-global-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](../-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) | ## Properties @@ -28,4 +30,5 @@ If kind is EclipseKind.Total or EclipseKind.Annular, the latitude and longitude | [kind](kind.md)
val [kind](kind.md): [EclipseKind](../-eclipse-kind/index.md)
The type of solar eclipse: EclipseKind.Partial, EclipseKind.Annular, or EclipseKind.Total. | | [latitude](latitude.md)
val [latitude](latitude.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The geographic latitude at the center of the peak eclipse shadow. | | [longitude](longitude.md)
val [longitude](longitude.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The geographic longitude at the center of the peak eclipse shadow. | +| [obscuration](obscuration.md)
val [obscuration](obscuration.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The peak fraction of the Sun's apparent disc area obscured by the Moon (total and annular eclipses only). | | [peak](peak.md)
val [peak](peak.md): [Time](../-time/index.md)
The date and time when the solar eclipse is darkest. This is the instant when the axis of the Moon's shadow cone passes closest to the Earth's center. | diff --git a/source/kotlin/doc/-global-solar-eclipse-info/obscuration.md b/source/kotlin/doc/-global-solar-eclipse-info/obscuration.md new file mode 100644 index 00000000..381bfd99 --- /dev/null +++ b/source/kotlin/doc/-global-solar-eclipse-info/obscuration.md @@ -0,0 +1,7 @@ +//[astronomy](../../../index.md)/[io.github.cosinekitty.astronomy](../index.md)/[GlobalSolarEclipseInfo](index.md)/[obscuration](obscuration.md) + +# obscuration + +val [obscuration](obscuration.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html) + +The peak fraction of the Sun's apparent disc area obscured by the Moon (total and annular eclipses only). diff --git a/source/kotlin/doc/-local-solar-eclipse-info/-local-solar-eclipse-info.md b/source/kotlin/doc/-local-solar-eclipse-info/-local-solar-eclipse-info.md index 38e3a902..da7a4e2b 100644 --- a/source/kotlin/doc/-local-solar-eclipse-info/-local-solar-eclipse-info.md +++ b/source/kotlin/doc/-local-solar-eclipse-info/-local-solar-eclipse-info.md @@ -2,4 +2,4 @@ # LocalSolarEclipseInfo -fun [LocalSolarEclipseInfo](-local-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), partialBegin: [EclipseEvent](../-eclipse-event/index.md), totalBegin: [EclipseEvent](../-eclipse-event/index.md)?, peak: [EclipseEvent](../-eclipse-event/index.md), totalEnd: [EclipseEvent](../-eclipse-event/index.md)?, partialEnd: [EclipseEvent](../-eclipse-event/index.md)) +fun [LocalSolarEclipseInfo](-local-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), partialBegin: [EclipseEvent](../-eclipse-event/index.md), totalBegin: [EclipseEvent](../-eclipse-event/index.md)?, peak: [EclipseEvent](../-eclipse-event/index.md), totalEnd: [EclipseEvent](../-eclipse-event/index.md)?, partialEnd: [EclipseEvent](../-eclipse-event/index.md)) diff --git a/source/kotlin/doc/-local-solar-eclipse-info/index.md b/source/kotlin/doc/-local-solar-eclipse-info/index.md index a8f85f9d..7d0060df 100644 --- a/source/kotlin/doc/-local-solar-eclipse-info/index.md +++ b/source/kotlin/doc/-local-solar-eclipse-info/index.md @@ -2,7 +2,7 @@ # LocalSolarEclipseInfo -class [LocalSolarEclipseInfo](index.md)(kind: [EclipseKind](../-eclipse-kind/index.md), partialBegin: [EclipseEvent](../-eclipse-event/index.md), totalBegin: [EclipseEvent](../-eclipse-event/index.md)?, peak: [EclipseEvent](../-eclipse-event/index.md), totalEnd: [EclipseEvent](../-eclipse-event/index.md)?, partialEnd: [EclipseEvent](../-eclipse-event/index.md)) +class [LocalSolarEclipseInfo](index.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), partialBegin: [EclipseEvent](../-eclipse-event/index.md), totalBegin: [EclipseEvent](../-eclipse-event/index.md)?, peak: [EclipseEvent](../-eclipse-event/index.md), totalEnd: [EclipseEvent](../-eclipse-event/index.md)?, partialEnd: [EclipseEvent](../-eclipse-event/index.md)) Information about a solar eclipse as seen by an observer at a given time and geographic location. @@ -10,19 +10,22 @@ Returned by [searchLocalSolarEclipse](../search-local-solar-eclipse.md) or [next When a solar eclipse is found, it is classified as partial, annular, or total. The kind field thus holds EclipseKind.Partial, EclipseKind.Annular, or EclipseKind.Total. A partial solar eclipse is when the Moon does not line up directly enough with the Sun to completely block the Sun's light from reaching the observer. An annular eclipse occurs when the Moon's disc is completely visible against the Sun but the Moon is too far away to completely block the Sun's light; this leaves the Sun with a ring-like appearance. A total eclipse occurs when the Moon is close enough to the Earth and aligned with the Sun just right to completely block all sunlight from reaching the observer. +The obscuration field reports what fraction of the Sun's disc appears blocked by the Moon when viewed by the observer at the peak eclipse time. This is a value that ranges from 0 (no blockage) to 1 (total eclipse). The obscuration value will be between 0 and 1 for partial eclipses and annular eclipses. The value will be exactly 1 for total eclipses. Obscuration gives an indication of how dark the eclipse appears. + There are 5 "event" fields, each of which contains a time and a solar altitude. Field peak holds the date and time of the center of the eclipse, when it is at its peak. The fields partialBegin and partialEnd are always set, and indicate when the eclipse begins/ends. If the eclipse reaches totality or becomes annular, totalBegin and totalEnd indicate when the total/annular phase begins/ends. When an event field is valid, the caller must also check its altitude field to see whether the Sun is above the horizon at the time indicated by the time field. ## Constructors | | | |---|---| -| [LocalSolarEclipseInfo](-local-solar-eclipse-info.md)
fun [LocalSolarEclipseInfo](-local-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), partialBegin: [EclipseEvent](../-eclipse-event/index.md), totalBegin: [EclipseEvent](../-eclipse-event/index.md)?, peak: [EclipseEvent](../-eclipse-event/index.md), totalEnd: [EclipseEvent](../-eclipse-event/index.md)?, partialEnd: [EclipseEvent](../-eclipse-event/index.md)) | +| [LocalSolarEclipseInfo](-local-solar-eclipse-info.md)
fun [LocalSolarEclipseInfo](-local-solar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), partialBegin: [EclipseEvent](../-eclipse-event/index.md), totalBegin: [EclipseEvent](../-eclipse-event/index.md)?, peak: [EclipseEvent](../-eclipse-event/index.md), totalEnd: [EclipseEvent](../-eclipse-event/index.md)?, partialEnd: [EclipseEvent](../-eclipse-event/index.md)) | ## Properties | Name | Summary | |---|---| | [kind](kind.md)
val [kind](kind.md): [EclipseKind](../-eclipse-kind/index.md)
The type of solar eclipse: EclipseKind.Partial, EclipseKind.Annular, or EclipseKind.Total. | +| [obscuration](obscuration.md)
val [obscuration](obscuration.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The fraction of the Sun's apparent disc area obscured by the Moon at the eclipse peak. | | [partialBegin](partial-begin.md)
val [partialBegin](partial-begin.md): [EclipseEvent](../-eclipse-event/index.md)
The time and Sun altitude at the beginning of the eclipse. | | [partialEnd](partial-end.md)
val [partialEnd](partial-end.md): [EclipseEvent](../-eclipse-event/index.md)
The time and Sun altitude at the end of the eclipse. | | [peak](peak.md)
val [peak](peak.md): [EclipseEvent](../-eclipse-event/index.md)
The time and Sun altitude when the eclipse reaches its peak. | diff --git a/source/kotlin/doc/-local-solar-eclipse-info/obscuration.md b/source/kotlin/doc/-local-solar-eclipse-info/obscuration.md new file mode 100644 index 00000000..f2d6ac4d --- /dev/null +++ b/source/kotlin/doc/-local-solar-eclipse-info/obscuration.md @@ -0,0 +1,7 @@ +//[astronomy](../../../index.md)/[io.github.cosinekitty.astronomy](../index.md)/[LocalSolarEclipseInfo](index.md)/[obscuration](obscuration.md) + +# obscuration + +val [obscuration](obscuration.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html) + +The fraction of the Sun's apparent disc area obscured by the Moon at the eclipse peak. diff --git a/source/kotlin/doc/-lunar-eclipse-info/-lunar-eclipse-info.md b/source/kotlin/doc/-lunar-eclipse-info/-lunar-eclipse-info.md index 69d6823f..ff46110f 100644 --- a/source/kotlin/doc/-lunar-eclipse-info/-lunar-eclipse-info.md +++ b/source/kotlin/doc/-lunar-eclipse-info/-lunar-eclipse-info.md @@ -2,4 +2,4 @@ # LunarEclipseInfo -fun [LunarEclipseInfo](-lunar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), peak: [Time](../-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) +fun [LunarEclipseInfo](-lunar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](../-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) diff --git a/source/kotlin/doc/-lunar-eclipse-info/index.md b/source/kotlin/doc/-lunar-eclipse-info/index.md index aba15803..001c5901 100644 --- a/source/kotlin/doc/-lunar-eclipse-info/index.md +++ b/source/kotlin/doc/-lunar-eclipse-info/index.md @@ -2,7 +2,7 @@ # LunarEclipseInfo -class [LunarEclipseInfo](index.md)(kind: [EclipseKind](../-eclipse-kind/index.md), peak: [Time](../-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) +class [LunarEclipseInfo](index.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](../-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) Information about a lunar eclipse. @@ -10,6 +10,8 @@ Returned by [searchLunarEclipse](../search-lunar-eclipse.md) or [nextLunarEclips The kind field thus holds EclipseKind.Penumbral, EclipseKind.Partial, or EclipseKind.Total, depending on the kind of lunar eclipse found. +The obscuration field holds a value in the range 0, 1 that indicates what fraction of the Moon's apparent disc area is covered by the Earth's umbra at the eclipse's peak. This indicates how dark the peak eclipse appears. For penumbral eclipses, the obscuration is 0, because the Moon does not pass through the Earth's umbra. For partial eclipses, the obscuration is somewhere between 0 and 1. For total lunar eclipses, the obscuration is 1. + Field peak holds the date and time of the center of the eclipse, when it is at its peak. Fields sdPenum, sdPartial, and sdTotal hold the semi-duration of each phase of the eclipse, which is half of the amount of time the eclipse spends in each phase (expressed in minutes), or 0.0 if the eclipse never reaches that phase. By converting from minutes to days, and subtracting/adding with peak, the caller may determine the date and time of the beginning/end of each eclipse phase. @@ -18,13 +20,14 @@ Fields sdPenum, sdPartial, and sdTotal hold the semi-duration of each phase of t | | | |---|---| -| [LunarEclipseInfo](-lunar-eclipse-info.md)
fun [LunarEclipseInfo](-lunar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), peak: [Time](../-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) | +| [LunarEclipseInfo](-lunar-eclipse-info.md)
fun [LunarEclipseInfo](-lunar-eclipse-info.md)(kind: [EclipseKind](../-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](../-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)) | ## Properties | Name | Summary | |---|---| | [kind](kind.md)
val [kind](kind.md): [EclipseKind](../-eclipse-kind/index.md)
The type of lunar eclipse found. | +| [obscuration](obscuration.md)
val [obscuration](obscuration.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The peak fraction of the Moon's apparent disc that is covered by the Earth's umbra. | | [peak](peak.md)
val [peak](peak.md): [Time](../-time/index.md)
The time of the eclipse at its peak. | | [sdPartial](sd-partial.md)
val [sdPartial](sd-partial.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The semi-duration of the partial phase in minutes, or 0.0 if none. | | [sdPenum](sd-penum.md)
val [sdPenum](sd-penum.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html)
The semi-duration of the penumbral phase in minutes. | diff --git a/source/kotlin/doc/-lunar-eclipse-info/obscuration.md b/source/kotlin/doc/-lunar-eclipse-info/obscuration.md new file mode 100644 index 00000000..64a37e1a --- /dev/null +++ b/source/kotlin/doc/-lunar-eclipse-info/obscuration.md @@ -0,0 +1,7 @@ +//[astronomy](../../../index.md)/[io.github.cosinekitty.astronomy](../index.md)/[LunarEclipseInfo](index.md)/[obscuration](obscuration.md) + +# obscuration + +val [obscuration](obscuration.md): [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html) + +The peak fraction of the Moon's apparent disc that is covered by the Earth's umbra. diff --git a/source/kotlin/doc/index.md b/source/kotlin/doc/index.md index 31333afb..e0a4bc53 100644 --- a/source/kotlin/doc/index.md +++ b/source/kotlin/doc/index.md @@ -21,7 +21,7 @@ | [ElongationInfo](-elongation-info/index.md)
class [ElongationInfo](-elongation-info/index.md)(time: [Time](-time/index.md), visibility: [Visibility](-visibility/index.md), elongation: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), eclipticSeparation: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Contains information about the visibility of a celestial body at a given date and time. | | [EquatorEpoch](-equator-epoch/index.md)
enum [EquatorEpoch](-equator-epoch/index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[EquatorEpoch](-equator-epoch/index.md)>
Selects the date for which the Earth's equator is to be used for representing equatorial coordinates. | | [Equatorial](-equatorial/index.md)
class [Equatorial](-equatorial/index.md)(ra: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), dec: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), dist: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), vec: [Vector](-vector/index.md))
Equatorial angular and cartesian coordinates. | -| [GlobalSolarEclipseInfo](-global-solar-eclipse-info/index.md)
class [GlobalSolarEclipseInfo](-global-solar-eclipse-info/index.md)(kind: [EclipseKind](-eclipse-kind/index.md), peak: [Time](-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Reports the time and geographic location of the peak of a solar eclipse. | +| [GlobalSolarEclipseInfo](-global-solar-eclipse-info/index.md)
class [GlobalSolarEclipseInfo](-global-solar-eclipse-info/index.md)(kind: [EclipseKind](-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](-time/index.md), distance: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), latitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), longitude: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Reports the time and geographic location of the peak of a solar eclipse. | | [GravitySimulator](-gravity-simulator/index.md)
class [GravitySimulator](-gravity-simulator/index.md)
A simulation of zero or more small bodies moving through the Solar System. | | [HourAngleInfo](-hour-angle-info/index.md)
class [HourAngleInfo](-hour-angle-info/index.md)(time: [Time](-time/index.md), hor: [Topocentric](-topocentric/index.md))
Information about a celestial body crossing a specific hour angle. | | [IlluminationInfo](-illumination-info/index.md)
class [IlluminationInfo](-illumination-info/index.md)(time: [Time](-time/index.md), mag: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), phaseAngle: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), phaseFraction: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), helioDist: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), ringTilt: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Information about the brightness and illuminated shape of a celestial body. | @@ -29,8 +29,8 @@ | [InvalidBodyException](-invalid-body-exception/index.md)
class [InvalidBodyException](-invalid-body-exception/index.md)(body: [Body](-body/index.md)) : [Exception](https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html)
An invalid body was specified for the given function. | | [JupiterMoonsInfo](-jupiter-moons-info/index.md)
class [JupiterMoonsInfo](-jupiter-moons-info/index.md)(io: [StateVector](-state-vector/index.md), europa: [StateVector](-state-vector/index.md), ganymede: [StateVector](-state-vector/index.md), callisto: [StateVector](-state-vector/index.md))
Holds the positions and velocities of Jupiter's major 4 moons. | | [LibrationInfo](-libration-info/index.md)
data class [LibrationInfo](-libration-info/index.md)(elat: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), elon: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), mlat: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), mlon: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), distanceKm: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), diamDeg: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Lunar libration angles, returned by [libration](libration.md). | -| [LocalSolarEclipseInfo](-local-solar-eclipse-info/index.md)
class [LocalSolarEclipseInfo](-local-solar-eclipse-info/index.md)(kind: [EclipseKind](-eclipse-kind/index.md), partialBegin: [EclipseEvent](-eclipse-event/index.md), totalBegin: [EclipseEvent](-eclipse-event/index.md)?, peak: [EclipseEvent](-eclipse-event/index.md), totalEnd: [EclipseEvent](-eclipse-event/index.md)?, partialEnd: [EclipseEvent](-eclipse-event/index.md))
Information about a solar eclipse as seen by an observer at a given time and geographic location. | -| [LunarEclipseInfo](-lunar-eclipse-info/index.md)
class [LunarEclipseInfo](-lunar-eclipse-info/index.md)(kind: [EclipseKind](-eclipse-kind/index.md), peak: [Time](-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Information about a lunar eclipse. | +| [LocalSolarEclipseInfo](-local-solar-eclipse-info/index.md)
class [LocalSolarEclipseInfo](-local-solar-eclipse-info/index.md)(kind: [EclipseKind](-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), partialBegin: [EclipseEvent](-eclipse-event/index.md), totalBegin: [EclipseEvent](-eclipse-event/index.md)?, peak: [EclipseEvent](-eclipse-event/index.md), totalEnd: [EclipseEvent](-eclipse-event/index.md)?, partialEnd: [EclipseEvent](-eclipse-event/index.md))
Information about a solar eclipse as seen by an observer at a given time and geographic location. | +| [LunarEclipseInfo](-lunar-eclipse-info/index.md)
class [LunarEclipseInfo](-lunar-eclipse-info/index.md)(kind: [EclipseKind](-eclipse-kind/index.md), obscuration: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), peak: [Time](-time/index.md), sdPenum: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdPartial: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html), sdTotal: [Double](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/index.html))
Information about a lunar eclipse. | | [MoonQuarterInfo](-moon-quarter-info/index.md)
class [MoonQuarterInfo](-moon-quarter-info/index.md)(quarter: [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html), time: [Time](-time/index.md))
A lunar quarter event (new moon, first quarter, full moon, or third quarter) along with its date and time. | | [NodeEventInfo](-node-event-info/index.md)
class [NodeEventInfo](-node-event-info/index.md)(time: [Time](-time/index.md), kind: [NodeEventKind](-node-event-kind/index.md))
Information about an ascending or descending node of a body. | | [NodeEventKind](-node-event-kind/index.md)
enum [NodeEventKind](-node-event-kind/index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[NodeEventKind](-node-event-kind/index.md)>
Indicates whether a crossing through the ecliptic plane is ascending or descending. | diff --git a/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt b/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt index c47984de..67d9eba2 100644 --- a/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt +++ b/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt @@ -179,9 +179,10 @@ private const val EARTH_MEAN_RADIUS_KM = 6371.0 // mean radius of the Earth's private const val EARTH_ATMOSPHERE_KM = 88.0 // effective atmosphere thickness for lunar eclipses private const val EARTH_ECLIPSE_RADIUS_KM = EARTH_MEAN_RADIUS_KM + EARTH_ATMOSPHERE_KM private const val MOON_EQUATORIAL_RADIUS_KM = 1738.1 +private const val MOON_EQUATORIAL_RADIUS_AU = (MOON_EQUATORIAL_RADIUS_KM / KM_PER_AU) private const val MOON_MEAN_RADIUS_KM = 1737.4 private const val MOON_POLAR_RADIUS_KM = 1736.0 -private const val MOON_EQUATORIAL_RADIUS_AU = (MOON_EQUATORIAL_RADIUS_KM / KM_PER_AU) +private const val MOON_POLAR_RADIUS_AU = (MOON_POLAR_RADIUS_KM / KM_PER_AU) private const val ANGVEL = 7.2921150e-5 private const val SOLAR_DAYS_PER_SIDEREAL_DAY = 0.9972695717592592 private const val MEAN_SYNODIC_MONTH = 29.530588 // average number of days for Moon to return to the same phase @@ -1790,6 +1791,12 @@ enum class EclipseKind { * The `kind` field thus holds `EclipseKind.Penumbral`, `EclipseKind.Partial`, * or `EclipseKind.Total`, depending on the kind of lunar eclipse found. * + * The `obscuration` field holds a value in the range [0, 1] that indicates what fraction + * of the Moon's apparent disc area is covered by the Earth's umbra at the eclipse's peak. + * This indicates how dark the peak eclipse appears. For penumbral eclipses, the obscuration + * is 0, because the Moon does not pass through the Earth's umbra. For partial eclipses, + * the obscuration is somewhere between 0 and 1. For total lunar eclipses, the obscuration is 1. + * * Field `peak` holds the date and time of the center of the eclipse, when it is at its peak. * * Fields `sdPenum`, `sdPartial`, and `sdTotal` hold the semi-duration of each phase @@ -1804,6 +1811,11 @@ class LunarEclipseInfo( */ val kind: EclipseKind, + /** + * The peak fraction of the Moon's apparent disc that is covered by the Earth's umbra. + */ + val obscuration: Double, + /** * The time of the eclipse at its peak. */ @@ -1848,6 +1860,17 @@ class LunarEclipseInfo( * onto the daytime side of the Earth at the instant of the eclipse's peak. * If `kind` has any other value, `latitude` and `longitude` are undefined and should * not be used. + * + * For total or annular eclipses, the `obscuration` field holds the fraction (0, 1] + * of the Sun's apparent disc area that is blocked from view by the Moon's silhouette, + * as seen by an observer located at the geographic coordinates `latitude`, `longitude` + * at the darkest time `peak`. The value will always be 1 for total eclipses, and less than + * 1 for annular eclipses. + * For partial eclipses, `obscuration` is undefined and should not be used. + * This is because there is little practical use for an obscuration value of + * a partial eclipse without supplying a particular observation location. + * Developers who wish to find an obscuration value for partial solar eclipses should therefore use + * [searchLocalSolarEclipse] and provide the geographic coordinates of an observer. */ class GlobalSolarEclipseInfo( /** @@ -1855,6 +1878,11 @@ class GlobalSolarEclipseInfo( */ val kind: EclipseKind, + /** + * The peak fraction of the Sun's apparent disc area obscured by the Moon (total and annular eclipses only). + */ + val obscuration: Double, + /** * The date and time when the solar eclipse is darkest. * This is the instant when the axis of the Moon's shadow cone passes closest to the Earth's center. @@ -1922,6 +1950,13 @@ class EclipseEvent ( * A total eclipse occurs when the Moon is close enough to the Earth and aligned with the * Sun just right to completely block all sunlight from reaching the observer. * + * The `obscuration` field reports what fraction of the Sun's disc appears blocked + * by the Moon when viewed by the observer at the peak eclipse time. + * This is a value that ranges from 0 (no blockage) to 1 (total eclipse). + * The obscuration value will be between 0 and 1 for partial eclipses and annular eclipses. + * The value will be exactly 1 for total eclipses. Obscuration gives an indication + * of how dark the eclipse appears. + * * There are 5 "event" fields, each of which contains a time and a solar altitude. * Field `peak` holds the date and time of the center of the eclipse, when it is at its peak. * The fields `partialBegin` and `partialEnd` are always set, and indicate when @@ -1937,6 +1972,11 @@ class LocalSolarEclipseInfo ( */ val kind: EclipseKind, + /** + * The fraction of the Sun's apparent disc area obscured by the Moon at the eclipse peak. + */ + val obscuration: Double, + /** * The time and Sun altitude at the beginning of the eclipse. */ @@ -2054,44 +2094,42 @@ internal fun calcShadow( internal fun earthShadow(time: Time): ShadowInfo { // This function helps find when the Earth's shadow falls upon the Moon. - val e = helioEarthPos(time) + val s = geoVector(Body.Sun, time, Aberration.Corrected) val m = geoMoon(time) - return calcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, e) + return calcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, -s) } internal fun moonShadow(time: Time): ShadowInfo { // This function helps find when the Moon's shadow falls upon the Earth. - // This is a variation on the logic in EarthShadow(). - // Instead of a heliocentric Earth and a geocentric Moon, - // we want a heliocentric Moon and a lunacentric Earth. - - val e = helioEarthPos(time) + val s = geoVector(Body.Sun, time, Aberration.Corrected) val m = geoMoon(time) // -m = lunacentric Earth - // m+e = heliocentric Moon - return calcShadow(MOON_MEAN_RADIUS_KM, time, -m, m+e) + // m-s = heliocentric Moon + return calcShadow(MOON_MEAN_RADIUS_KM, time, -m, m-s) } internal fun localMoonShadow(time: Time, observer: Observer): ShadowInfo { // Calculate observer's geocentric position. - // For efficiency, do this first, to populate the earth rotation parameters in 'time'. - // That way they can be recycled instead of recalculated. val o = geoPos(time, observer) - val h = helioEarthPos(time) + + // Calculate light-travel and aberration corrected Sun. + val s = geoVector(Body.Sun, time, Aberration.Corrected) + + // Calculate geocentric Moon. val m = geoMoon(time) // o-m = lunacentric observer - // m+h = heliocentric Moon - return calcShadow(MOON_MEAN_RADIUS_KM, time, o-m, m+h) + // m-s = heliocentric Moon + return calcShadow(MOON_MEAN_RADIUS_KM, time, o-m, m-s) } internal fun planetShadow(body: Body, planetRadiusKm: Double, time: Time): ShadowInfo { // Calculate light-travel-corrected vector from Earth to planet. - val g = geoVector(body, time, Aberration.None) + val g = geoVector(body, time, Aberration.Corrected) // Calculate light-travel-corrected vector from Earth to Sun. - val e = geoVector(Body.Sun, time, Aberration.None) + val e = geoVector(Body.Sun, time, Aberration.Corrected) // -g = planetocentric Earth // g-e = heliocentric planet @@ -2137,7 +2175,6 @@ internal fun peakEarthShadow(searchCenterTime: Time): ShadowInfo { return earthShadow(tx) } - internal val moonShadowSlopeContext = SearchContext { time -> val dt = 1.0 / SECONDS_PER_DAY val t1 = time.addDays(-dt) @@ -2196,6 +2233,69 @@ internal fun planetTransitBoundary(body: Body, planetRadiusKm: Double, t1: Time, } ?: throw InternalError("Planet transit boundary search failed.") } +internal fun discObscuration(a: Double, b: Double, c: Double): Double { + // a = radius of first disc + // b = radius of second disc + // c = distance between centers of discs + if (a <= 0.0) throw InternalError("Radius of first disc must be positive.") + if (b <= 0.0) throw InternalError("Radius of second disc must be positive.") + if (c < 0.0) throw InternalError("Distance between discs is not allowed to be negative.") + + if (c >= a + b) { + // The discs are too far apart to have any overlapping area. + return 0.0 + } + + if (c == 0.0) { + // The discs have a common center. Therefore, one disc is inside the other. + return if (a <= b) 1.0 else (b*b)/(a*a) + } + + val x = (a*a - b*b + c*c) / (2*c) + val radicand = a*a - x*x + if (radicand <= 0.0) { + // The circumferences do not intersect, or are tangent. + // We already ruled out the case of non-overlapping discs. + // Therefore, one disc is inside the other. + return if (a <= b) 1.0 else (b*b)/(a*a) + } + + // The discs overlap fractionally in a pair of lens-shaped areas. + + val y = sqrt(radicand) + + // Return the overlapping fractional area. + // There are two lens-shaped areas, one to the left of x, the other to the right of x. + // Each part is calculated by subtracting a triangular area from a sector's area. + val lens1 = a*a*acos(x/a) - x*y + val lens2 = b*b*acos((c-x)/b) - (c-x)*y + + // Find the fractional area with respect to the first disc. + return (lens1 + lens2) / (PI*a*a) +} + + +internal fun solarEclipseObscuration(hm: Vector, lo: Vector): Double { + // Find heliocentric observer. + val ho = hm + lo + + // Calculate the apparent angular radius of the Sun for the observer. + val sunRadius = asin(SUN_RADIUS_AU / ho.length()) + + // Calculate the apparent angular radius of the Moon for the observer. + val moonRadius = asin(MOON_POLAR_RADIUS_AU / lo.length()) + + // Calculate the apparent angular separation between the Sun's center and the Moon's center. + val sunMoonSeparation = lo.angleWith(ho).degreesToRadians() + + // Find the fraction of the Sun's apparent disc area that is covered by the Moon. + val obscuration = discObscuration(sunRadius, moonRadius, sunMoonSeparation) + + // HACK: In marginal cases, we need to clamp obscuration to less than 1.0. + // This function is never called for total eclipses, so it should never return 1.0. + return min(0.9999, obscuration) +} + /** * Searches for a lunar eclipse. @@ -2234,6 +2334,7 @@ fun searchLunarEclipse(startTime: Time): LunarEclipseInfo { if (shadow.r < shadow.p + MOON_MEAN_RADIUS_KM) { // This is at least a penumbral eclipse. We will return a result. var kind = EclipseKind.Penumbral + var obscuration = 0.0 val sdPenum = shadowSemiDurationMinutes(shadow.time, shadow.p + MOON_MEAN_RADIUS_KM, 200.0) var sdPartial = 0.0 var sdTotal = 0.0 @@ -2246,11 +2347,14 @@ fun searchLunarEclipse(startTime: Time): LunarEclipseInfo { if (shadow.r + MOON_MEAN_RADIUS_KM < shadow.k) { // This is a total eclipse. kind = EclipseKind.Total + obscuration = 1.0 sdTotal = shadowSemiDurationMinutes(shadow.time, shadow.k - MOON_MEAN_RADIUS_KM, sdPartial) + } else { + obscuration = discObscuration(MOON_MEAN_RADIUS_KM, shadow.k, shadow.r) } } - return LunarEclipseInfo(kind, shadow.time, sdPenum, sdPartial, sdTotal) + return LunarEclipseInfo(kind, obscuration, shadow.time, sdPenum, sdPartial, sdTotal) } } @@ -2324,6 +2428,7 @@ internal fun eclipseKindFromUmbra(k: Double) = ( internal fun geoidIntersect(shadow: ShadowInfo): GlobalSolarEclipseInfo { var kind = EclipseKind.Partial + var obscuration = Double.NaN var latitude = Double.NaN var longitude = Double.NaN @@ -2385,9 +2490,10 @@ internal fun geoidIntersect(shadow: ShadowInfo): GlobalSolarEclipseInfo { throw InternalError("Invalid surface distance from intersection.") kind = eclipseKindFromUmbra(surface.k) + obscuration = if (kind == EclipseKind.Total) 1.0 else solarEclipseObscuration(shadow.dir, luna) } - return GlobalSolarEclipseInfo(kind, shadow.time, shadow.r, latitude, longitude) + return GlobalSolarEclipseInfo(kind, obscuration, shadow.time, shadow.r, latitude, longitude) } @@ -2549,7 +2655,8 @@ internal fun localEclipse(shadow: ShadowInfo, observer: Observer): LocalSolarEcl } else { kind = EclipseKind.Partial } - return LocalSolarEclipseInfo(kind, partialBegin, totalBegin, peak, totalEnd, partialEnd) + val obscuration = if (kind == EclipseKind.Total) 1.0 else solarEclipseObscuration(shadow.dir, shadow.target) + return LocalSolarEclipseInfo(kind, obscuration, partialBegin, totalBegin, peak, totalEnd, partialEnd) } diff --git a/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt b/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt index 506a3902..f11bcb3e 100644 --- a/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt +++ b/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt @@ -1364,19 +1364,34 @@ class Tests { var eclipse: LunarEclipseInfo = searchLunarEclipse(Time(1701, 1, 1, 0, 0, 0.0)) for (line in infile.readLines()) { ++lnum + + assertTrue(eclipse.obscuration.isFinite()) + assertTrue(eclipse.sdPartial.isFinite()) + assertTrue(eclipse.sdPenum.isFinite()) + assertTrue(eclipse.sdTotal.isFinite()) + val tokens = tokenize(line, 3, filename, lnum) val peakTime = parseDate(tokens[0]) val partialMinutes = tokens[1].toDouble() val totalMinutes = tokens[2].toDouble() // Verify that the calculated semi-durations are consistent with the kind of eclipse. - val valid: Boolean = when (eclipse.kind) { + val sd_valid: Boolean = when (eclipse.kind) { EclipseKind.Penumbral -> (eclipse.sdPenum > 0.0) && (eclipse.sdPartial == 0.0) && (eclipse.sdTotal == 0.0) EclipseKind.Partial -> (eclipse.sdPenum > 0.0) && (eclipse.sdPartial > 0.0) && (eclipse.sdTotal == 0.0) EclipseKind.Total -> (eclipse.sdPenum > 0.0) && (eclipse.sdPartial > 0.0) && (eclipse.sdTotal > 0.0) else -> fail("Invalid lunar eclipse kind: ${eclipse.kind}") } - assertTrue(valid, "$filename line $lnum: invalid semiduration(s) for kind ${eclipse.kind}") + assertTrue(sd_valid, "$filename line $lnum: invalid semiduration(s) for kind ${eclipse.kind}") + + // Verify that obscurations make sense for the eclipse kind. + val frac_valid: Boolean = when (eclipse.kind) { + EclipseKind.Penumbral -> (eclipse.obscuration == 0.0) + EclipseKind.Partial -> (eclipse.obscuration > 0.0) && (eclipse.obscuration < 1.0) + EclipseKind.Total -> (eclipse.obscuration == 1.0) + else -> fail("Invalid lunar eclipse kind: ${eclipse.kind}") + } + assertTrue(frac_valid, "$filename line $lnum: invalid obscuration ${eclipse.obscuration} for kind ${eclipse.kind}") // Check eclipse peak time val peakDiffDays = eclipse.peak.ut - peakTime.ut @@ -1436,7 +1451,7 @@ class Tests { // Validate the eclipse prediction. val diffMinutes = MINUTES_PER_DAY * abs(diffDays) - assertTrue(diffMinutes < 6.93, "$filename line $lnum: excessive time error = $diffMinutes minutes; expected $peak, found ${eclipse.peak}") + assertTrue(diffMinutes < 7.56, "$filename line $lnum: excessive time error = $diffMinutes minutes; expected $peak, found ${eclipse.peak}") // Validate the eclipse kind, but only when it is not a "glancing" eclipse. if (eclipse.distance < 6360.0) @@ -1452,6 +1467,13 @@ class Tests { } } + when (eclipse.kind) { + EclipseKind.Partial -> assertTrue(eclipse.obscuration.isNaN(), "$filename line $lnum: expected NAN obscuration.") + EclipseKind.Annular -> assertTrue(eclipse.obscuration > 0.8 && eclipse.obscuration < 1.0, "$filename line $lnum: invalid obscuration ${eclipse.obscuration} for annular eclipse.") + EclipseKind.Total -> assertTrue(eclipse.obscuration == 1.0, "$filename line $lnum: invalid obscuration ${eclipse.obscuration} for total eclipse.") + else -> fail("$filename line $lnum: invalid eclipse kind ${eclipse.kind}") + } + eclipse = nextGlobalSolarEclipse(eclipse.peak) } @@ -1501,8 +1523,20 @@ class Tests { continue } + when (eclipse.kind) { + EclipseKind.Annular, + EclipseKind.Partial -> + assertTrue(eclipse.obscuration > 0.0 && eclipse.obscuration < 1.0, "$filename line $lnum: Invalid obscuration ${eclipse.obscuration} for ${eclipse.kind} eclipse.") + + EclipseKind.Total -> + assertTrue(eclipse.obscuration == 1.0, "$filename line $lnum: Invalid obscuration ${eclipse.obscuration} for {eclipse.kind} eclipse.") + + else -> + fail("$filename line $lnum: Invalid eclipse kind ${eclipse.kind}") + } + val diffMinutes = MINUTES_PER_DAY * abs(diffDays) - assertTrue(diffMinutes < 7.14, "$filename line $lnum: excessive time error = $diffMinutes minutes") + assertTrue(diffMinutes < 7.734, "$filename line $lnum: excessive time error = $diffMinutes minutes") } assertTrue(skipCount <= 6, "$filename: excessive skip count = $skipCount") @@ -1573,8 +1607,82 @@ class Tests { assertTrue(evt != null, "$filename line $lnum: eclipse $name was not supposed to be null.") val diffMinutes = MINUTES_PER_DAY * abs(expectedTime.ut - evt.time.ut) assertTrue(diffMinutes < 1.0, "$filename line $lnum: excessive time error for $name: $diffMinutes minutes.") - val diffAlt = abs(expectedAltitude - evt.altitude) - assertTrue(diffAlt < 0.5, "$filename line $lnum: excessive altitude error for $name: $diffAlt degrees.") + // Ignore discrepancies for negative altitudes, because of quirky and irrelevant differences in refraction models. + if (expectedAltitude >= 0.0) { + val diffAlt = abs(expectedAltitude - evt.altitude) + assertTrue(diffAlt < 0.5, "$filename line $lnum: excessive altitude error for $name: $diffAlt degrees.") + } + } + + //---------------------------------------------------------------------------------------- + + private fun globalAnnularCase(year: Int, month: Int, day: Int, obscuration: Double) { + // Search for the first solar eclipse that occurs after the given date. + val time = Time(year, month, day, 0, 0, 0.0) + val eclipse = searchGlobalSolarEclipse(time) + + // Verify the eclipse is within 1 day after the search basis time. + val dt = eclipse.peak.ut - time.ut + assertTrue(dt >= 0.0 && dt <= 1.0, "$time: found eclipse $dt days after search time.") + + // Verify we found an annular solar eclipse. + assertTrue(eclipse.kind == EclipseKind.Annular, "$time: expected annular eclipse but found ${eclipse.kind}") + + // Check how accurately we calculated obscuration. + val diff = eclipse.obscuration - obscuration + assertTrue(abs(diff) < 0.0000904, "$time: excessive obscuration error $diff") + } + + private fun localSolarCase( + year: Int, + month: Int, + day: Int, + latitude: Double, + longitude: Double, + kind: EclipseKind, + obscuration: Double, + tolerance: Double + ) { + val time = Time(year, month, day, 0, 0, 0.0) + val observer = Observer(latitude, longitude, 0.0) + val eclipse = searchLocalSolarEclipse(time, observer) + val dt = eclipse.peak.time.ut - time.ut + assertTrue(dt >= 0.0 && dt <= 1.0, "$time: found eclipse $dt days after search time.") + assertTrue(eclipse.kind == kind, "$time: expected $kind eclipse, but found ${eclipse.kind}") + val diff = eclipse.obscuration - obscuration + assertTrue(abs(diff) <= tolerance, "$time: excessive obscuration error $diff") + } + + @Test + fun `Solar eclipse obscuration`() { + // Verify global solar eclipse obscurations for annular eclipses only. + // This is because they are the only nontrivial values for global solar eclipses. + // The trivial values are all validated exactly by GlobalSolarEclipseTest(). + globalAnnularCase(2023, 10, 14, 0.90638) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2023Oct14Aprime.html + globalAnnularCase(2024, 10, 2, 0.86975) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Oct02Aprime.html + globalAnnularCase(2027, 2, 6, 0.86139) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2027Feb06Aprime.html + globalAnnularCase(2028, 1, 26, 0.84787) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2028Jan26Aprime.html + globalAnnularCase(2030, 6, 1, 0.89163) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2030Jun01Aprime.html + + // Verify obscuration values for specific locations on the Earth. + // Local solar eclipse calculations include obscuration for all types of eclipse, not just annular and total. + localSolarCase(2023, 10, 14, 11.3683, -83.1017, EclipseKind.Annular, 0.90638, 0.000080) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2023Oct14Aprime.html + localSolarCase(2023, 10, 14, 25.78, -80.22, EclipseKind.Partial, 0.578, 0.000023) // https://aa.usno.navy.mil/calculated/eclipse/solar?eclipse=22023&lat=25.78&lon=-80.22&label=Miami%2C+FL&height=0&submit=Get+Data + localSolarCase(2023, 10, 14, 30.2666, -97.7000, EclipseKind.Partial, 0.8867, 0.001016) // http://astro.ukho.gov.uk/eclipse/0332023/Austin_TX_United_States_2023Oct14.png + localSolarCase(2024, 4, 8, 25.2900, -104.1383, EclipseKind.Total, 1.0, 0.0 ) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Apr08Tprime.html + localSolarCase(2024, 4, 8, 37.76, -122.44, EclipseKind.Partial, 0.340, 0.000604) // https://aa.usno.navy.mil/calculated/eclipse/solar?eclipse=12024&lat=37.76&lon=-122.44&label=San+Francisco%2C+CA&height=0&submit=Get+Data + localSolarCase(2024, 10, 2, -21.9533, -114.5083, EclipseKind.Annular, 0.86975, 0.000061) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2024Oct02Aprime.html + localSolarCase(2024, 10, 2, -33.468, -70.636, EclipseKind.Partial, 0.436, 0.000980) // https://aa.usno.navy.mil/calculated/eclipse/solar?eclipse=22024&lat=-33.468&lon=-70.636&label=Santiago%2C+Chile&height=0&submit=Get+Data + localSolarCase(2030, 6, 1, 56.525, 80.0617, EclipseKind.Annular, 0.89163, 0.000067) // https://www.eclipsewise.com/solar/SEprime/2001-2100/SE2030Jun01Aprime.html + localSolarCase(2030, 6, 1, 40.388, 49.914, EclipseKind.Partial, 0.67240, 0.000599) // http://xjubier.free.fr/en/site_pages/SolarEclipseCalc_Diagram.html + localSolarCase(2030, 6, 1, 40.3667, 49.8333, EclipseKind.Partial, 0.6736, 0.001464) // http://astro.ukho.gov.uk/eclipse/0132030/Baku_Azerbaijan_2030Jun01.png + } + + //---------------------------------------------------------------------------------------- + + @Test + fun `Lunar eclipse obscuration`() { + } //----------------------------------------------------------------------------------------