diff --git a/README.en.md b/README.en.md index 399bace..e5a5d8f 100644 --- a/README.en.md +++ b/README.en.md @@ -8,7 +8,9 @@ A personal astronomy library developed over years for calendrical work, amateur > This project is mainly for learning and validating astronomical algorithms. The results are intended for serious amateur use. -The implementation follows *Astronomical Algorithms* and provides calendar conversion, Sun/Moon/planet positions, eclipses, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP/MPP02 DE405-style analytical series. No external JPL ephemeris files are required. +The implementation follows *Astronomical Algorithms*. It covers calendar conversion, Sun/Moon/planet positions, eclipses, lunar occultations, rise/set/transit times, lunar phases, stars, coordinate transforms, physical ephemerides, research formulas, and generic small-body orbit propagation. + +The Sun and planets use built-in VSOP87-style analytical terms, while the Moon uses a built-in ELP/MPP02 DE405-style analytical series. No external JPL ephemeris files are required. Unless noted otherwise, coordinates are apparent-of-date coordinates. Angles are in degrees, apparent diameters and semidiameters are in arcseconds, and distances use the unit implied by the function name, usually `AU` or `km`. @@ -24,6 +26,8 @@ Unless noted otherwise, coordinates are apparent-of-date coordinates. Angles are - [Lite Sun And Moon](#lite-sun-and-moon) - [Solar Eclipse](#solar-eclipse) - [Lunar Eclipse](#lunar-eclipse) + - [Lunar Occultations](#lunar-occultations) + - [Event Maps And GeoJSON](#event-maps-and-geojson) - [Planets](#planets) - [Stars](#stars) - [Coordinate Tools](#coordinate-tools) @@ -45,7 +49,9 @@ go get b612.me/astro - Solar position, rise/set, Earth distance, apparent solar time, apparent altitude, parallactic angle, solar `P/B0/L0`, apparent diameter - Lunar position, rise/set, Earth distance, phase, new/full/quarter times, apparent diameter, bright-limb angle, parallactic angle, geocentric/topocentric libration, apsides, nodes, maximum declination - `lite/sun` and `lite/moon` lightweight approximation chains for watches, frontends, mini programs, and other resource-constrained environments -- Global and local solar/lunar eclipses, solar central paths, partial footprints, visible local lunar eclipses, Saros metadata, and SVG diagrams +- Global and local solar/lunar eclipses, solar central paths, partial footprints, visible local lunar eclipses, Saros metadata, local diagrams, and global visibility-map SVGs +- Point-source stellar and finite-disk planetary lunar occultations, with fixed-site contacts, global paths, geometric greatest points, and SVG output +- GeoJSON for solar eclipses, lunar eclipses, and lunar occultations, including timed paths and optional time-marker points; border-free global SVG maps support equirectangular and polar projections - Seven major planets with positions, rise/set, conjunction/opposition/station events, quadratures, elongations, Mercury/Venus geocentric transits, nodes, phase, apparent magnitude, apparent diameter, parallactic angle, and physical ephemerides - 9100+ star catalog entries, constellation lookup, proper-motion propagation, rise/set, parallactic angle, and apparent altitude - Coordinate transforms, topocentric coordinates, sidereal time, precession, nutation, angular distance, refraction, airmass, parallactic angle, and Galactic coordinates @@ -60,9 +66,11 @@ go get b612.me/astro | `calendar` | Gregorian/lunisolar conversion, solar terms, historical era names, old-calendar metadata | | `coord` | Ecliptic/equatorial/horizontal transforms, sidereal time, precession, nutation, topocentric helpers, refraction, airmass, parallactic angle, Galactic coordinates, and research helpers with manual obliquity/hour angle | | `sun` | Solar position, rise/set, twilight, equation of time, apparent solar time, apparent altitude, parallactic angle, diameter, solar `P/B0/L0` | -| `moon` | Lunar position, rise/set, phases, new/full/quarter times, apparent altitude, parallactic angle, diameter, bright-limb angle, geocentric/topocentric libration, apsides, nodes, maximum declination | +| `moon` | Lunar position, rise/set, phases, new/full/quarter times, apparent altitude, parallactic angle, diameter, bright-limb angle, geocentric/topocentric libration, apsides, nodes, maximum declination, and stellar/planetary lunar occultations with global paths | | `lite/sun` / `lite/moon` | Lightweight Sun/Moon approximation chains for minute-level rise/set, lightweight sky position, and lunar-phase work | -| `eclipse` / `eclipse/svg` | Global/local solar and lunar eclipses, solar central paths, partial footprints, local visibility filtering, Saros metadata, SVG output | +| `eclipse` / `eclipse/svg` | Global/local solar and lunar eclipses, solar central paths, partial footprints, local visibility filtering, Saros metadata, local diagrams, and global visibility-map SVGs | +| `moon/svg` | Fixed-site stellar/planetary lunar-occultation disk charts and projected global maps with bands, center lines, and time labels | +| `geojson` | RFC 7946 encoding for existing solar-eclipse, lunar-eclipse, and lunar-occultation geographic results; projection and styling remain application concerns | | `mercury` / `venus` | Positions, rise/set, conjunctions, stations, elongations, geocentric transits, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides | | `mars` / `jupiter` / `saturn` / `uranus` / `neptune` | Positions, rise/set, conjunction/opposition, stations, quadratures, phase, parallactic angle, magnitude, diameter, nodes, physical ephemerides | | `earth` | Earth orbital eccentricity, perihelion, aphelion | @@ -103,7 +111,9 @@ This is suitable for ordinary calendrical work, observing support, outreach, and ### Moon -The Moon uses a built-in truncated ELP/MPP02 DE405-style analytical series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris. +The Moon uses a built-in truncated ELP/MPP02 DE405-style analytical series retaining the major periodic terms. The package stays lightweight and does not require external ephemeris files. + +It is suitable for Chinese-calendar new moons, lunar phases, rise/set, lunar eclipses, amateur occultation prediction, and ordinary positional work. For lunar laser ranging, long-term physical libration, or professional occultation work, use JPL or a dedicated lunar ephemeris. The four principal phases keep the historical pinyin names and also expose English aliases: @@ -162,6 +172,10 @@ The following areas have been checked against JPL Horizons, NASA GSFC, IMCCE, an - apparent diameters of the Sun, planets, and Moon - solar physical ephemerides `P/B0/L0` - planetary rise, transit, and set events +- Moon rise/set: `aero=true` uses dynamic standard refraction and the instantaneous lunar semidiameter for an upper-limb crossing. Across 14 sea-level events at 7 sites, the current mean/maximum differences against JPL Horizons DE441 are about `0.30s / 0.75s`. +- Moon rise/set with other conventions: mean/maximum differences are about `38.77s / 76.22s` against MET Norway's fixed `-0.8333°` convention. Against IMCCE Miriade, whose horizon convention is not exposed, the mean is about `2m13.46s`; the grazing `61°N` sample reaches about `6m41.82s`. +- Global stellar-occultation path: for the `2025-06-05` occultation of HR 4799, start/greatest/end differences from the project's recorded reference are about `+2.30s / -1.37s / -3.12s`. +- HR 4799 path geometry: greatest-point longitude/latitude differ by about `+0.0122° / +0.0105°`. The computed `3582.4 km` band width differs from the `3571.9 km` reference by about `10.5 km`. - Earth perihelion and aphelion - main-chain lunar position: the current algorithm is a truncated ELP/MPP02 DE405-style analytical series; across four JPL/Horizons `JDTT` samples in year `-2000`, the maximum difference from JPL/Horizons is about `219.6"` in longitude, `25.8"` in latitude, and `34.3 km` in distance - Moon perigee and apogee @@ -169,6 +183,10 @@ The following areas have been checked against JPL Horizons, NASA GSFC, IMCCE, an - solar and lunar eclipses - Galilean satellite events +Impact boundary of the topocentric hour-angle fix: Moon rise/set, fixed-site lunar occultations, topocentric lunar coordinates, and the greatest-event Moon altitude/visibility fields of local lunar eclipses use the corrected UT hour-angle chain. + +Global solar/lunar eclipse contacts, greatest times, and magnitudes use their own geocentric or Besselian geometry. Their existing NASA accuracy figures therefore do not change because of this fix. + The README examples are illustrative. The repository tests contain the exact baselines. ## Quick Start @@ -398,20 +416,20 @@ func main() { // Civil morning twilight begins when the Sun is 6 degrees below the horizon. fmt.Println(sun.MorningTwilight(date, lon, lat, -6)) - // Sunrise in Xi'an on this date, with atmospheric refraction. + // Sunrise: dynamic standard refraction and instantaneous solar semidiameter, upper limb. fmt.Println(sun.RiseTime(date, lon, lat, height, true)) // Upper culmination of the Sun in Xi'an. fmt.Println(sun.CulminationTime(date, lon)) - // Sunset in Xi'an on this date, with atmospheric refraction. + // Sunset: dynamic standard refraction and instantaneous solar semidiameter, upper limb. fmt.Println(sun.SetTime(date, lon, lat, height, true)) // Civil evening twilight ends when the Sun is 6 degrees below the horizon. fmt.Println(sun.EveningTwilight(date, lon, lat, -6)) - // Moonrise in Xi'an on this date, with atmospheric refraction. + // Moonrise: dynamic standard refraction and instantaneous lunar semidiameter, upper limb. fmt.Println(moon.RiseTime(date, lon, lat, height, true)) // Upper culmination of the Moon in Xi'an. fmt.Println(moon.CulminationTime(date, lon, lat)) - // Moonset in Xi'an on this date, with atmospheric refraction. + // Moonset: dynamic standard refraction and instantaneous lunar semidiameter, upper limb. fmt.Println(moon.SetTime(date, lon, lat, height, true)) } ``` @@ -420,13 +438,13 @@ Output: ```text 2020-01-01 07:22:27.960488498 +0800 CST // civil morning twilight begins -2020-01-01 07:50:14.530648291 +0800 CST // sunrise +2020-01-01 07:49:52.413689196 +0800 CST // sunrise 2020-01-01 12:47:35.933117866 +0800 CST // solar upper culmination -2020-01-01 17:44:47.070974707 +0800 CST // sunset +2020-01-01 17:45:09.188657999 +0800 CST // sunset 2020-01-01 18:12:33.624035418 +0800 CST // civil evening twilight ends -2020-01-01 11:52:45.157297253 +0800 CST // moonrise +2020-01-01 11:52:49.860912859 +0800 CST // moonrise 2020-01-01 17:38:02.510787248 +0800 CST // lunar upper culmination -2020-01-01 23:26:51.580328643 +0800 CST // moonset +2020-01-01 23:26:49.313593804 +0800 CST // moonset ``` #### Sun and Moon position @@ -695,9 +713,13 @@ Common entry points: - `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`: search locally visible total solar eclipses, returning `(info, ok)` - `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`: search locally visible annular solar eclipses, returning `(info, ok)` - `SolarEclipseCentralPath`: compute central line, northern/southern limits, and greatest-eclipse point -- `SolarEclipsePartialFootprints`: compute the partial-eclipse penumbral footprint on Earth +- `SolarEclipsePartialFootprints`: compute the partial-eclipse penumbral footprint on Earth, with optional sampled umbral/antumbral outlines - `eclipse/svg.LocalSolarEclipseSVG`: render a local solar-disk SVG +`SolarEclipsePartialFootprintsInfo` also reports global shadow contacts. `P1/P4` are the external penumbral contacts and `P2/P3` are the internal penumbral contacts; `U1/U4` are the external umbral or antumbral contacts and `U2/U3` are the internal contacts. Contacts that do not occur remain zero `time.Time` values. `CentralBeginOnEarth` / `CentralEndOnEarth` retain their existing meaning of the shadow axis entering and leaving Earth; they are not aliases for `U1/U4`. + +Set `CentralShadowStep` in `SolarEclipsePartialFootprintOptions` when structured instantaneous central-shadow outlines are needed; samples are returned in `CentralShadowFootprints`. Zero disables this extra calculation in the data API, while the SVG entry point samples it every 10 minutes by default. + `SolarEclipseInfo`, `LocalSolarEclipseInfo`, and the embedded `Eclipse` field in `SolarEclipsePath` / `SolarEclipsePartialFootprintsInfo` include Saros metadata: - `HasSaros`: whether a Saros series was matched @@ -1121,6 +1143,334 @@ Rendered example: - NASA 2024-03-25 penumbral lunar eclipse diagram: - NASA lunar eclipse algorithm and history notes: +### Lunar Occultations + +Lunar-occultation APIs live in `moon` and search only the target supplied by the caller; they never enumerate the star catalog. Fixed-site APIs take `start`, `end`, longitude, latitude, and elevation directly. + +Global-path results contain WGS84 samples suitable for `moon/svg` or `geojson`. + +Targets use two distinct contact models: + +- **Stars** are point sources. Results contain immersion, greatest occultation, and emersion. +- **Planets** are finite disks. C1/C4 are external contacts; a fully covered disk also has C2/C3 internal contacts. Partial and grazing events have no C2/C3. +- The planetary model uses the equatorial body radius and excludes rings, atmospheric extensions, and oblateness. +- `FindBestStarOccultations` and `FindBestPlanetOccultations` return the global sea-level geometric greatest point. They do not score horizon visibility, lunar altitude, duration, or magnitude. +- `VisibleAtGreatest` only reports visibility at the selected point. +- The query window selects events by their greatest instant. Once selected, complete contacts or a complete global path are returned rather than clipped at the query endpoints. +- Contact times solve topocentric geometry between the target and lunar limb without atmospheric refraction. +- `MoonAltitudeAtGreatest` is the true altitude of the lunar center. `VisibleAtGreatest` reports whether it is at or above the geometric horizon. + +#### Stellar occultations + +Callers supply a `StarCoordinate`. `RA` and `Dec` are degrees; `Epoch` and `Frame` are required. Proper motions use `mas/year`; `ProperMotionRACosDecMasPerYear` follows the usual catalog convention `dRA*cos(Dec)`. + +A coordinate can be constructed directly: + +```go +target := moon.StarCoordinate{ + ID: "HR 4799", + RA: 189.1975, + Dec: -5.831944444444, + Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), + Frame: moon.CoordinateFrameJ2000, + ProperMotionRACosDecMasPerYear: -28, + ProperMotionDecMasPerYear: -18, +} +``` + +Alternatively, explicitly load the embedded 9100-star catalog and convert a `StarData` value with `StarCoordinateFromStarData`. + +The occultation search itself does not load the catalog; calls such as `star.InitStarDatabase`, `StarDataByName`, and `StarDataByHR` do. + +```go +package main + +import ( + "fmt" + "time" + + "b612.me/astro/moon" + "b612.me/astro/star" +) + +func main() { + cst := time.FixedZone("CST", 8*3600) + start := time.Date(2025, 6, 5, 0, 0, 0, 0, cst) + end := start.Add(24 * time.Hour) + + _ = star.InitStarDatabase() + data, _ := star.StarDataByHR(4799) + target, _ := moon.StarCoordinateFromStarData(data) + target.ID = "HR 4799" // Optional display label. + + events, _ := moon.FindStarOccultations( + start, end, target, + 121.56601, 6.80706, 0, + moon.OccultationSearchOptions{}, + ) + for _, event := range events { + fmt.Println(event.TargetID, event.Type) + fmt.Println( + event.Immersion.Format("2006-01-02 15:04:05.000 MST"), + event.Greatest.Format("2006-01-02 15:04:05.000 MST"), + event.Emersion.Format("2006-01-02 15:04:05.000 MST"), + ) + fmt.Printf("altitude=%.3f visible=%v\n", event.MoonAltitudeAtGreatest, event.VisibleAtGreatest) + } + + paths, _ := moon.FindStarOccultationPaths( + start, end, target, + moon.OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200}, + ) + for _, path := range paths { + fmt.Println( + path.Start.Time.Format("2006-01-02 15:04:05.000 MST"), + path.Greatest.Time.Format("2006-01-02 15:04:05.000 MST"), + path.End.Time.Format("2006-01-02 15:04:05.000 MST"), + ) + fmt.Printf("greatest=%.6f %.6f width=%.1fkm center=%d\n", + path.Greatest.Longitude, path.Greatest.Latitude, + path.Greatest.WidthKM, len(path.CenterLine)) + } +} +``` + +Output: + +```text +HR 4799 total +2025-06-05 19:14:01.095 CST 2025-06-05 20:02:06.357 CST 2025-06-05 20:50:10.740 CST +altitude=75.561 visible=true +2025-06-05 17:45:28.498 CST 2025-06-05 20:02:06.332 CST 2025-06-05 22:18:49.977 CST +greatest=121.566021 6.807046 width=3582.4km center=108 +``` + +Zero-valued `OccultationSearchOptions` select the default search step and safety margin; `MaxEvents > 0` limits output. + +`OccultationPathOptions.Step` controls base time sampling, while `TargetSpacingKM` adaptively refines the center line. Requests exceeding the deterministic budget return `ErrOccultationPathSamplingLimit`. + +#### Planetary occultations + +Planet targets use the constants from `OccultationMercury` through `OccultationNeptune`. This example solves C1-C4 for the `2025-02-01` occultation of Saturn at a site near the global geometric greatest point: + +```go +package main + +import ( + "fmt" + "time" + + "b612.me/astro/moon" +) + +func main() { + cst := time.FixedZone("CST", 8*3600) + start := time.Date(2025, 2, 1, 0, 0, 0, 0, cst) + events, _ := moon.FindPlanetOccultations( + start, start.Add(24*time.Hour), moon.OccultationSaturn, + 104.52219613, 55.25401991, 0, + moon.OccultationSearchOptions{}, + ) + for _, event := range events { + fmt.Println(event.TargetID, event.Type, event.HasInternalContacts) + fmt.Println(event.ExternalImmersion.Format("2006-01-02 15:04:05.000 MST")) // C1 + fmt.Println(event.InternalImmersion.Format("2006-01-02 15:04:05.000 MST")) // C2 + fmt.Println(event.Greatest.Format("2006-01-02 15:04:05.000 MST")) + fmt.Println(event.InternalEmersion.Format("2006-01-02 15:04:05.000 MST")) // C3 + fmt.Println(event.ExternalEmersion.Format("2006-01-02 15:04:05.000 MST")) // C4 + } +} +``` + +Output: + +```text +Saturn total true +2025-02-01 11:29:09.692 CST +2025-02-01 11:29:40.042 CST +2025-02-01 12:00:48.729 CST +2025-02-01 12:32:46.388 CST +2025-02-01 12:33:18.285 CST +``` + +Global `FindPlanetOccultationPaths` results contain the region where any part of the planetary disk overlaps the Moon and, when present, the narrower full-coverage region. + +`HasTotalBand` reports the latter, and `GreatestTotalWidthKM` is its width at greatest occultation. Center lines, limits, and instantaneous footprints all carry sample times. + +#### Lunar-occultation SVG + +`moon/svg` provides both search-and-render and render-an-existing-result entry points: + +- `FindLocalStarOccultationSVGs` / `FindLocalPlanetOccultationSVGs`: fixed-site apparent tracks, the lunar path, and contact-stage panels. +- `FindStarOccultationSVGs` / `FindPlanetOccultationSVGs`: global bands, center lines, event points, and time labels. +- `LocalStarOccultationSVG` / `LocalPlanetOccultationSVG`: render an existing fixed-site event. +- `StarOccultationPathSVG` / `PlanetOccultationPathSVG`: render an existing global path. + +```go +localSVGs, err := moonsvg.FindLocalStarOccultationSVGs( + start, end, target, + 121.56601, 6.80706, 0, + moon.OccultationSearchOptions{}, + moonsvg.LocalStarOccultationSVGOptions{Width: 920, Height: 700, Location: cst}, +) +fmt.Println(err, len(localSVGs)) + +globalSVGs, err := moonsvg.FindStarOccultationSVGs( + start, end, target, + moon.OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200}, + moonsvg.StarOccultationSVGOptions{ + Width: 1200, Height: 800, Location: cst, + TimeLabelStep: 30 * time.Minute, + }, +) +fmt.Println(err, len(globalSVGs)) +``` + +Local charts use the selected observer's topocentric geometry. Global maps use Natural Earth `1:50m` coastlines without administrative boundaries. + +Center-line labels default to 30-minute intervals, and high-latitude events can automatically switch to a polar projection. + +The following diagrams reuse the `2025-06-05` occultation of HR 4799 from the preceding example. The fixed site is `121.56601°E, 6.80706°N`, near the global geometric greatest point. Its immersion, greatest, and emersion times are the actual topocentric contacts at that site. The diagram also shows lunar orientation, the lunar path, Moon altitude, azimuth, and horizon visibility. + +![2025 fixed-site occultation of HR 4799](doc/lunar-occultation-hr4799-2025-06-05-local-en.svg) + +The global diagram shows the band limits, visible/geometric center line, global event points, and 30-minute center-line labels. Global start and end are the instants when the lunar shadow first touches and finally leaves Earth; they are not the fixed site's contact times. + +![2025 global path of the HR 4799 lunar occultation](doc/lunar-occultation-hr4799-2025-06-05-global-en.svg) + +### Event Maps And GeoJSON + +#### Global visibility-map SVG + +`eclipse/svg` can directly render global solar- and lunar-eclipse maps. A solar map includes the partial-visibility sweep, total/annular central band, center line, global stages, and center-line time labels. It also shows the greatest-eclipse day/night terminator and subsolar point, shadow-axis entry/exit, `P1-P4/U1-U4` contacts, sampled penumbral outlines, and sampled umbral/antumbral outlines. + +A lunar map shows P1/P4 visible hemispheres, moonrise/moonset transition regions, and the region that sees the entire event. + +```go +package main + +import ( + "os" + "time" + + eclipsesvg "b612.me/astro/eclipse/svg" +) + +func main() { + cst := time.FixedZone("CST", 8*3600) + + solar, ok := eclipsesvg.SolarEclipseMapSVG( + time.Date(2009, 7, 22, 12, 0, 0, 0, cst), + eclipsesvg.SolarEclipseMapSVGOptions{ + Width: 1200, Height: 800, Location: cst, + Language: "en", + TimeLabelStep: 30 * time.Minute, + PenumbralOutlineStep: 60 * time.Minute, + CentralShadowStep: 10 * time.Minute, + }, + ) + if ok { + _ = os.WriteFile("doc/solar-eclipse-yangshan-2009-global-en.svg", []byte(solar), 0o644) + } + + lunar, ok := eclipsesvg.LunarEclipseMapSVG( + time.Date(2029, 1, 1, 0, 0, 0, 0, cst), + eclipsesvg.LunarEclipseMapSVGOptions{Width: 1200, Height: 800, Language: "en", Location: cst}, + ) + if ok { + _ = os.WriteFile("doc/lunar-eclipse-2029-01-01-global-en.svg", []byte(lunar), 0o644) + } +} +``` + +The right-hand event table lists available `P1-P4/U1-U4` contacts, shadow-axis entry/exit, and greatest eclipse in time order; contact coordinates are included when the layout has enough room. The summary also includes the Saros series, path width, Sun altitude/azimuth at the greatest point, and central-phase duration. Orange dashed lines are instantaneous penumbral boundaries with `HH:MM` labels, the gray dashed line is the day/night terminator at greatest eclipse, and solid brown lines are instantaneous umbral or antumbral outlines. + +Zero `PenumbralOutlineStep` and `CentralShadowStep` values use 60-minute and 10-minute defaults respectively; negative values disable the corresponding outlines. Explicit values such as 30 minutes remain supported. A zero `TimeLabelStep` uses 30 minutes, while a negative value disables center-line time labels. + +Solar-eclipse and occultation automatic projection can select a north- or south-polar map when appropriate. Lunar-eclipse maps default to equirectangular. Projection affects SVG presentation only, not the underlying WGS84 result. + +Eclipse maps use `EclipseMapProjectionEquirectangular`, `EclipseMapProjectionNorthPolar`, or `EclipseMapProjectionSouthPolar` to force a projection. Lunar-occultation maps use the corresponding `MapProjection...` constants. + +The following global maps reuse the dates from the earlier local SVG examples. The 2009 Yangtze River total eclipse, 2012 Xiamen annular eclipse, and 2035 Beijing total eclipse use the equirectangular projection: + +![2009 Yangtze River total solar eclipse global visibility](doc/solar-eclipse-yangshan-2009-global-en.svg) + +![2012 Xiamen annular solar eclipse global visibility](doc/solar-eclipse-xiamen-2012-global-en.svg) + +![2035 Beijing total solar eclipse global visibility](doc/solar-eclipse-beijing-2035-global-en.svg) + +The partial-eclipse visibility region of the `2012-05-21` annular eclipse includes the North Pole. The same event is therefore shown again with a forced north-polar azimuthal-equidistant projection, making its antimeridian-crossing Arctic visibility easier to inspect. The circular outline is the projection boundary, not an administrative or political boundary: + +![2012 annular solar eclipse north-polar global visibility](doc/solar-eclipse-arctic-2012-global-en.svg) + +The lunar-eclipse example reuses the cross-year total eclipse on `2029-01-01`, separating entire-event visibility, moonrise during eclipse, moonset during eclipse, and unavailable regions: + +![2029 cross-year total lunar eclipse global visibility](doc/lunar-eclipse-2029-01-01-global-en.svg) + +#### GeoJSON + +`geojson` accepts an already computed solar-eclipse, lunar-eclipse, or lunar-occultation result and returns `[]byte`. Those bytes are one complete UTF-8 RFC 7946 `FeatureCollection`, not an image or compressed payload. + +They can be written to `.geojson`, passed to `encoding/json`, or served directly to a map client. + +```go +package main + +import ( + "encoding/json" + "fmt" + "time" + + "b612.me/astro/eclipse" + "b612.me/astro/geojson" +) + +func main() { + date := time.Date(2024, 4, 8, 0, 0, 0, 0, time.UTC) + partial, ok := eclipse.SolarEclipsePartialFootprints( + date, + eclipse.SolarEclipsePartialFootprintOptions{ + Step: 10 * time.Minute, BoundaryPoints: 180, + }, + ) + if !ok { + return + } + central, hasCentral := eclipse.SolarEclipseCentralPath( + date, + eclipse.SolarEclipsePathOptions{Step: time.Minute, TargetSpacingKM: 20}, + ) + var centralPath *eclipse.SolarEclipsePath + if hasCentral { + centralPath = ¢ral + } + + data, err := geojson.MarshalSolarEclipseWithTimeMarkers( + partial, centralPath, + geojson.TimeMarkerOptions{ + Step: 30 * time.Minute, + Location: time.FixedZone("CST", 8*3600), + }, + ) + fmt.Println(err, json.Valid(data)) +} +``` + +Each event type has plain and time-marker variants: + +- `MarshalSolarEclipse` / `MarshalSolarEclipseWithTimeMarkers` +- `MarshalLunarEclipse` / `MarshalLunarEclipseWithTimeMarkers` +- `MarshalStarOccultation` / `MarshalStarOccultationWithTimeMarkers` +- `MarshalPlanetOccultation` / `MarshalPlanetOccultationWithTimeMarkers` + +Coordinates are WGS84 longitude and latitude; lines and polygons are split at the antimeridian. Timed paths have `times` arrays aligned point-for-point with coordinate segments. + +`WithTimeMarkers` adds Point Features with `role=time-marker`. Localized `label` values are for display, while `time` remains UTC RFC 3339. + +A zero `TimeMarkerOptions.Step` means 30 minutes. A positive step must be at least one minute, and one export is limited to 1440 markers. + +GeoJSON contains no basemap, national boundaries, styling, or projection. Web Mercator, polar views, tile selection, and political-boundary policy belong to the application. + ### Planets #### Inner planets @@ -1614,7 +1964,7 @@ Output: 143.99353431082105 18.7404068044953 // topocentric RA and Dec manual az=281.869347 alt=24.489608 zen=65.510392 ha=73.866900 // manual-LST horizontal result and hour angle gal lon=0.000047 lat=-0.000079 // Galactic longitude and latitude -apparent alt=10.092644 // apparent altitude after refraction estimate +apparent alt=10.093429 // apparent altitude after refraction estimate ``` Research-style `coord` helpers do not automatically substitute the current obliquity or sidereal time. They are useful for experiments with custom axial tilts or manually specified hour angles. For ordinary observing calculations, use the `time.Time` based APIs such as `EclipticToEquatorial` and `EquatorialToHorizontal`. @@ -1845,7 +2195,8 @@ Notes: ## Implemented - Sun position, altitude, zenith distance, azimuth, culmination, twilight, rise/set, solar terms, solar eclipses, solar physical ephemerides -- Moon position, altitude, zenith distance, azimuth, culmination, rise/set, phases, lunar eclipses, libration, apsides, maximum declination +- Moon position, altitude, zenith distance, azimuth, culmination, rise/set, phases, lunar eclipses, libration, apsides, maximum declination, and stellar/planetary lunar occultations +- Global projected SVG maps for solar eclipses, lunar eclipses, and lunar occultations; fixed-site occultation charts; GeoJSON with optional time markers - `lite/sun` and `lite/moon` lightweight Sun/Moon chains for minute-level rise/set, lightweight position, and lunar-phase work - Earth eccentricity, Sun-Earth distance, perihelion, aphelion - Apparent/mean sidereal time, constellation lookup, common coordinate transforms, refraction, airmass, parallactic angle, Galactic coordinates diff --git a/README.md b/README.md index 26c950b..5bc6afc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ >📚 本项目主要用于天文算法学习与验证,计算结果满足业余爱好级别需求。 -基于《天文算法》(Astronomical Algorithms)一书实现,提供历法转换、太阳/月亮/行星位置、日月食、升落、中天、月相、恒星、坐标变换、物理星历、研究公式和通用小天体轨道传播等功能。太阳和行星部分使用内置 VSOP87 解析项,月球部分使用内置 ELP2000/82 解析级数,不依赖外部 JPL 星历文件。 +基于《天文算法》(Astronomical Algorithms)一书实现,提供历法转换、太阳/月亮/行星位置、日月食、月掩、升落、中天、月相、恒星、坐标变换、物理星历、研究公式和通用小天体轨道传播等功能。太阳和行星部分使用内置 VSOP87 解析项,月球部分使用内置 ELP2000/82 解析级数,不依赖外部 JPL 星历文件。 没有特殊标注时,本程序所提供的坐标均为瞬时天球坐标;角度单位默认是度,视直径/视半径单位是角秒,距离单位按函数名使用 AU 或 km。 @@ -23,12 +23,15 @@ - [历法转换与节气](#历法转换与节气) - [太阳与月亮](#太阳与月亮) - [Lite 轻量太阳与月亮](#lite-轻量太阳与月亮) + - [月掩](#月掩) + - [天象地图与 GeoJSON](#天象地图与-geojson) - [行星](#行星) - [恒星](#恒星) - [坐标工具](#坐标工具) - [研究公式](#研究公式) - [通用小天体轨道](#通用小天体轨道) - [日晷与真太阳时](#日晷与真太阳时) +- [已实现](#已实现) - [TODO](#todo) ## 安装 @@ -43,7 +46,9 @@ go get b612.me/astro - 🌞 **太阳计算**:天球位置、日出日落、日地距离、真太阳时、视高度角、视差角、日面物理参数(`P/B0/L0`)、视直径等 - 🌙 **月亮计算**:天球位置、月出月落、地月距离、月相、朔望时间、视直径、亮边位置角、视差角、地心/站心天平动、近远地点、交点、最大赤纬等 - 🪶 **轻量链路**:`lite/sun` 与 `lite/moon` 提供面向手表、前端、小程序和其它资源受限环境的轻量近似太阳/月亮算法,覆盖天球位置、升落和月相 -- 🌗 **日月食**:全局日食、站心日食、中心线/偏食足迹、月食、地方可见月食与 SVG 示意图 +- 🌗 **日月食**:全局日食、站心日食、中心线/偏食足迹、月食、地方可见月食,以及局地示意图和全球见食图 SVG +- 🌘 **月掩**:按指定赤经赤纬搜索恒星月掩,按有限圆盘计算行星月掩,支持指定地点接触时刻、全球掩带、几何掩甚点和 SVG +- 🗺️ **地理输出**:日食、月食和月掩结果可编码为带时间数据与可选时间标记的 GeoJSON;全球 SVG 使用无行政边界海岸线,并支持等经纬和南北极投影 - 🪐 **行星计算**:七大行星天球位置、升落时间、合冲留、大距、水星/金星地心凌日等特殊天象时间、升交点/降交点、视直径/视半径、相位、视差角、节点、视星等与物理星历 - ⭐ **恒星计算**:指定天球坐标所属星座;同时包含9100颗恒星数据库,可计算升降时间、视差角和视高度角,获取指定日期的恒星坐标信息 - 🧭 **坐标工具**:黄道/赤道/地平坐标转换、站心坐标、恒星时、岁差、章动、角距离、大气折射、大气质量、视差角、银道坐标 @@ -58,9 +63,11 @@ go get b612.me/astro | `calendar` | 公历/农历互转、节气、历史朝代年号、古代历法信息 | | `coord` | 黄道/赤道/地平互转、恒星时、岁差、章动、站心坐标、大气折射、大气质量、视差角、银道坐标、手动黄赤交角和手动时角的研究接口 | | `sun` | 太阳位置、日出日落、晨昏朦影、均时差、真太阳时、视高度角、视差角、视直径、日面 `P/B0/L0` | -| `moon` | 月亮位置、月出月落、月相、朔望弦、视高度角、视差角、视直径、亮边位置角、地心/站心天平动、近远地点、交点、最大赤纬 | +| `moon` | 月亮位置、月出月落、月相、朔望弦、视高度角、视差角、视直径、亮边位置角、地心/站心天平动、近远地点、交点、最大赤纬,以及恒星/行星月掩和全球掩带 | | `lite/sun` / `lite/moon` | 轻量太阳/月亮近似链路,面向分钟级升落、轻量天球位置和月相计算 | -| `eclipse` / `eclipse/svg` | 全局/局地日月食、日食中心线与偏食足迹、局地可见性筛选、日月食 SVG | +| `eclipse` / `eclipse/svg` | 全局/局地日月食、日食中心线与偏食足迹、局地可见性筛选、局地示意图与全球见食图 SVG | +| `moon/svg` | 指定地点恒星/行星月掩视圆图,以及带掩带、中心线和时间标记的全球投影 SVG | +| `geojson` | 将日食、月食和月掩的既有地理结果编码为 RFC 7946 GeoJSON,投影与样式由应用负责 | | `mercury` / `venus` | 水星、金星位置、升落、合日、留、大距、地心凌日、相位、视差角、视星等、视直径、节点和物理星历 | | `mars` / `jupiter` / `saturn` / `uranus` / `neptune` | 外行星位置、升落、合冲、留、方照、相位、视差角、视星等、视直径、节点和物理星历 | | `earth` | 地球轨道偏心率、近日点、远日点 | @@ -101,7 +108,7 @@ go get b612.me/astro ### 月球 -月球使用内置的 ELP/MPP02 DE405 解析级数(截断版,保留主要周期项),库体积轻,不需要外部星历文件。它适合农历定朔、月相、升落、月食和常规位置计算;若需要极高精度月球测距、长期物理天平动或专业掩星,请以 JPL 星历或专门月球星历为准。 +月球使用内置的 ELP/MPP02 DE405 解析级数(截断版,保留主要周期项),库体积轻,不需要外部星历文件。它适合农历定朔、月相、升落、月食、业余月掩预报和常规位置计算;若需要极高精度月球测距、长期物理天平动或专业掩星,请以 JPL 星历或专门月球星历为准。 ### Lite 轻量链路 @@ -153,11 +160,16 @@ go get b612.me/astro - 太阳/行星/月亮视直径:与外部基线最大差异从 `0.000002"` 到 `0.194598"` 不等,月亮因视差和距离变化更敏感 - 太阳物理星历 `P/B0/L0`:最大差异约 `0.003349° / 0.003986° / 0.047394°` - 行星升/中天/落:已用 JPL Horizons 电视事件(TVH, Time-Varying Hourly)做对比校验;该基线按 1 分钟步长生成,当前结果与 Horizons 事件时间在分钟级上对齐 +- 月出/月落:`aero=true` 按动态标准折射和实时月球视半径计算上缘过地平线。7 个地点、14 个海平面事件相对 JPL Horizons DE441 的平均/最大差异约 `0.30s / 0.75s` +- 月出/月落的其他口径:相对固定 `-0.8333°` 的 MET Norway(Skyfield 1.53 + DE440s)约 `38.77s / 76.22s`;相对未公开地平线口径的 IMCCE Miriade 平均约 `2m13.46s`,`61°N` 低仰角样本最大约 `6m41.82s` +- 月掩恒星全球路径:`2025-06-05` 月掩进贤增九(HR 4799)样例相对项目记录的参考值,掩始/掩甚/掩终差异约 `+2.30s / -1.37s / -3.12s`;掩甚经纬度差异约 `+0.0122° / +0.0105°`,掩带宽为 `3582.4 km`,与参考 `3571.9 km` 相差约 `10.5 km` - 地球近日点/远日点:时刻最大差异约 `1m28.84s`,距离最大差异约 `0.000000039837 AU` - 月球主链位置:当前算法为 ELP/MPP02 DE405 解析级数截断版;在 `-2000` 年四个 JPL/Horizons `JDTT` 样本上,相对 JPL/Horizons 的最大差异约为黄经 `219.6"`、黄纬 `25.8"`、距离 `34.3 km` - 月球近地点/远地点:时刻最大差异约 `15m53.45s`,距离最大差异约 `39.758 km` - 月球最大赤纬:时刻最大差异约 `2.43s`,赤纬最大差异约 `0.00006431°` +站心时角修复后的影响边界:月出/月落、指定地点月掩、月球站心坐标,以及地方月食结果中的食甚月高和可见性判断使用修复后的 UT 时角链。日食和月食的全球接触时刻、食甚、食分等主体结果来自各自的地心/贝塞尔几何,不经过该站心转换,因此现有 NASA 精度数据无需随之改写。 + ## 快速开始 ### 历法转换与节气 @@ -469,20 +481,20 @@ func main() { // 西安市2020年1月1日民用晨朦影开始时间 // 民用朦影,太阳位于地平线下6度,航海朦影=地平线下12度,天文朦影=地平线下18度 fmt.Println(sun.MorningTwilight(date, lon, lat, -6)) - // 西安市2020年1月1日日出时间,计算大气影响 + // 西安市2020年1月1日日出时间,按动态标准折射和实时太阳视半径计算上缘过地平线 fmt.Println(sun.RiseTime(date, lon, lat, height, true)) // 西安市2020年1月1日太阳上中天时间 fmt.Println(sun.CulminationTime(date, lon)) - // 西安市2020年1月1日日落时间,计算大气影响 + // 西安市2020年1月1日日落时间,按动态标准折射和实时太阳视半径计算上缘过地平线 fmt.Println(sun.SetTime(date, lon, lat, height, true)) // 西安市2020年1月1日民用昏朦影结束时间 fmt.Println(sun.EveningTwilight(date, lon, lat, -6)) - // 西安市2020年1月1日月出时间,计算大气影响 + // 西安市2020年1月1日月出时间,按动态标准折射和实时月球视半径计算上缘过地平线 fmt.Println(moon.RiseTime(date, lon, lat, height, true)) // 西安市2020年1月1日月亮上中天时间 fmt.Println(moon.CulminationTime(date, lon, lat)) - // 西安市2020年1月1日月落时间,计算大气影响 + // 西安市2020年1月1日月落时间,按动态标准折射和实时月球视半径计算上缘过地平线 fmt.Println(moon.SetTime(date, lon, lat, height, true)) } ``` @@ -492,13 +504,13 @@ func main() { ``` 2020-01-01 07:22:27.960488498 +0800 CST -2020-01-01 07:50:14.530648291 +0800 CST +2020-01-01 07:49:52.413689196 +0800 CST 2020-01-01 12:47:35.933117866 +0800 CST -2020-01-01 17:44:47.070974707 +0800 CST +2020-01-01 17:45:09.188657999 +0800 CST 2020-01-01 18:12:33.624035418 +0800 CST -2020-01-01 11:52:45.157297253 +0800 CST +2020-01-01 11:52:49.860912859 +0800 CST 2020-01-01 17:38:02.510787248 +0800 CST -2020-01-01 23:26:51.580328643 +0800 CST +2020-01-01 23:26:49.313593804 +0800 CST ``` @@ -761,9 +773,13 @@ func main() { - `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`:搜索某地可见的日全食,返回 `(info, ok)` - `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`:搜索某地可见的日环食,返回 `(info, ok)` - `SolarEclipseCentralPath`:计算中心线、南北界和食甚点 -- `SolarEclipsePartialFootprints`:计算偏食半影在地球表面的足迹 +- `SolarEclipsePartialFootprints`:计算偏食半影在地球表面的足迹;可选采样本影/反本影瞬时轮廓 - `eclipse/svg.LocalSolarEclipseSVG`:生成某地的日面视圆 SVG +`SolarEclipsePartialFootprintsInfo` 还给出影锥与地球的全球接触:`P1/P4` 是半影外切,`P2/P3` 是半影内切;`U1/U4` 是本影或反本影外切,`U2/U3` 是内切。某次日食不存在的接触保持 `time.Time` 零值。`CentralBeginOnEarth` / `CentralEndOnEarth` 仍表示影轴进入和离开地球,不等同于 `U1/U4`。 + +需要结构化的瞬时中心影轮廓时,可在 `SolarEclipsePartialFootprintOptions` 中设置 `CentralShadowStep`;结果写入 `CentralShadowFootprints`。零值关闭该额外计算,SVG 入口则默认按 10 分钟采样。 + 日食结果 `SolarEclipseInfo`、`LocalSolarEclipseInfo`,以及 `SolarEclipsePath` / `SolarEclipsePartialFootprintsInfo` 里的 `Eclipse` 字段还会附带沙罗序列信息: - `HasSaros`:是否成功匹配到沙罗序列 @@ -1197,6 +1213,311 @@ true 19671 +### 月掩 + +月掩接口位于 `moon`,按用户给定的目标搜索,不会遍历恒星表。固定地点接口直接接收 `start`、`end`、经度、纬度和海拔;全球路径接口返回 WGS84 经纬度采样,可继续交给 `moon/svg` 或 `geojson`。 + +目标和接触语义分为两类: + +- **恒星**按点光源处理,返回掩始 `Immersion`、掩甚 `Greatest` 和掩终 `Emersion`。 +- **行星**按有限圆盘处理,外切为 C1/C4,完全被月面覆盖时另有内切 C2/C3;偏掩和擦掩没有 C2/C3。行星半径取赤道本体半径,不含行星环、大气延伸和扁率。 +- `FindBestStarOccultations` / `FindBestPlanetOccultations` 返回全球海平面几何掩甚点,不按地平线、月高、可见时长或食分评分;`VisibleAtGreatest` 仅报告该点的可见性。 +- 搜索时间窗按掩甚时刻选择事件。命中后会返回完整接触时刻或完整全球路径,不会把结果裁剪到查询端点。 +- 接触时刻按目标与月面边缘的站心几何求解,不加入大气折射。`MoonAltitudeAtGreatest` 是月心真高度,`VisibleAtGreatest` 表示它是否不低于几何地平线。 + +#### 恒星月掩 + +恒星由调用者传入 `StarCoordinate`。`RA` / `Dec` 单位为度,`Epoch` 和 `Frame` 必填;自行单位为 `mas/year`,其中 `ProperMotionRACosDecMasPerYear` 使用星表常见的 `dRA*cos(Dec)` 口径。 + +可以直接构造坐标: + +```go +target := moon.StarCoordinate{ + ID: "HR 4799", + RA: 189.1975, + Dec: -5.831944444444, + Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), + Frame: moon.CoordinateFrameJ2000, + ProperMotionRACosDecMasPerYear: -28, + ProperMotionDecMasPerYear: -18, +} +``` + +也可以显式加载内置 9100 星表,再用 `StarCoordinateFromStarData` 转换。月掩搜索本身不会加载星表;只有调用 `star.InitStarDatabase`、`StarDataByName`、`StarDataByHR` 等星表接口时才会加载。 + +```go +package main + +import ( + "fmt" + "time" + + "b612.me/astro/moon" + "b612.me/astro/star" +) + +func main() { + cst := time.FixedZone("CST", 8*3600) + start := time.Date(2025, 6, 5, 0, 0, 0, 0, cst) + end := start.Add(24 * time.Hour) + + _ = star.InitStarDatabase() + data, _ := star.StarDataByName("进贤增九") + target, _ := moon.StarCoordinateFromStarData(data) + + events, _ := moon.FindStarOccultations( + start, end, target, + 121.56601, 6.80706, 0, + moon.OccultationSearchOptions{}, + ) + for _, event := range events { + fmt.Println(event.TargetID, event.Type) + fmt.Println( + event.Immersion.Format("2006-01-02 15:04:05.000 MST"), + event.Greatest.Format("2006-01-02 15:04:05.000 MST"), + event.Emersion.Format("2006-01-02 15:04:05.000 MST"), + ) + fmt.Printf("altitude=%.3f visible=%v\n", event.MoonAltitudeAtGreatest, event.VisibleAtGreatest) + } + + paths, _ := moon.FindStarOccultationPaths( + start, end, target, + moon.OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200}, + ) + for _, path := range paths { + fmt.Println( + path.Start.Time.Format("2006-01-02 15:04:05.000 MST"), + path.Greatest.Time.Format("2006-01-02 15:04:05.000 MST"), + path.End.Time.Format("2006-01-02 15:04:05.000 MST"), + ) + fmt.Printf("greatest=%.6f %.6f width=%.1fkm center=%d\n", + path.Greatest.Longitude, path.Greatest.Latitude, + path.Greatest.WidthKM, len(path.CenterLine)) + } +} +``` + +输出结果: + +```text +进贤增九 total +2025-06-05 19:14:01.095 CST 2025-06-05 20:02:06.357 CST 2025-06-05 20:50:10.740 CST +altitude=75.561 visible=true +2025-06-05 17:45:28.498 CST 2025-06-05 20:02:06.332 CST 2025-06-05 22:18:49.977 CST +greatest=121.566021 6.807046 width=3582.4km center=108 +``` + +`OccultationSearchOptions` 的零值使用默认搜索步长和安全余量;`MaxEvents > 0` 限制返回数量。`OccultationPathOptions.Step` 控制基础时间采样,`TargetSpacingKM` 按地面距离自适应加密中心线;过密请求超出确定性预算时返回 `ErrOccultationPathSamplingLimit`。 + +#### 行星月掩 + +行星目标使用 `OccultationMercury` 到 `OccultationNeptune` 常量。下面以 `2025-02-01` 月掩土星为例,在靠近全球几何掩甚点的位置求 C1-C4: + +```go +package main + +import ( + "fmt" + "time" + + "b612.me/astro/moon" +) + +func main() { + cst := time.FixedZone("CST", 8*3600) + start := time.Date(2025, 2, 1, 0, 0, 0, 0, cst) + events, _ := moon.FindPlanetOccultations( + start, start.Add(24*time.Hour), moon.OccultationSaturn, + 104.52219613, 55.25401991, 0, + moon.OccultationSearchOptions{}, + ) + for _, event := range events { + fmt.Println(event.TargetID, event.Type, event.HasInternalContacts) + fmt.Println(event.ExternalImmersion.Format("2006-01-02 15:04:05.000 MST")) // C1 + fmt.Println(event.InternalImmersion.Format("2006-01-02 15:04:05.000 MST")) // C2 + fmt.Println(event.Greatest.Format("2006-01-02 15:04:05.000 MST")) + fmt.Println(event.InternalEmersion.Format("2006-01-02 15:04:05.000 MST")) // C3 + fmt.Println(event.ExternalEmersion.Format("2006-01-02 15:04:05.000 MST")) // C4 + } +} +``` + +输出结果: + +```text +Saturn total true +2025-02-01 11:29:09.692 CST +2025-02-01 11:29:40.042 CST +2025-02-01 12:00:48.729 CST +2025-02-01 12:32:46.388 CST +2025-02-01 12:33:18.285 CST +``` + +`FindPlanetOccultationPaths` 的全球结果同时包含任意圆盘重叠的部分掩区域和整颗行星被遮住的全掩区域。`HasTotalBand` 表示是否存在全掩带,`GreatestTotalWidthKM` 是掩甚处全掩带宽;中心线、边界和瞬时足迹都带采样时刻。 + +#### 月掩 SVG + +`moon/svg` 同时提供“搜索并渲染”和“渲染已计算结果”两组入口: + +- `FindLocalStarOccultationSVGs` / `FindLocalPlanetOccultationSVGs`:指定地点的实际月面轨迹、白道和接触阶段图。 +- `FindStarOccultationSVGs` / `FindPlanetOccultationSVGs`:全球掩带、中心线、阶段点和时间标记图。 +- `LocalStarOccultationSVG` / `LocalPlanetOccultationSVG`:渲染已有的固定地点事件。 +- `StarOccultationPathSVG` / `PlanetOccultationPathSVG`:渲染已有的全球路径。 + +```go +localSVGs, err := moonsvg.FindLocalStarOccultationSVGs( + start, end, target, + 121.56601, 6.80706, 0, + moon.OccultationSearchOptions{}, + moonsvg.LocalStarOccultationSVGOptions{Width: 920, Height: 700, Location: cst}, +) +fmt.Println(err, len(localSVGs)) + +globalSVGs, err := moonsvg.FindStarOccultationSVGs( + start, end, target, + moon.OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200}, + moonsvg.StarOccultationSVGOptions{ + Width: 1200, Height: 800, Location: cst, + TimeLabelStep: 30 * time.Minute, + }, +) +fmt.Println(err, len(globalSVGs)) +``` + +本地图按指定观测者的站心几何绘制。全球图使用 Natural Earth `1:50m` 海岸线,不含行政边界;默认每 30 分钟在中心线上标记 `HH:MM`,高纬事件可自动切换极区投影。 + +下面两张图沿用前文 `2025-06-05` 月掩进贤增九(HR 4799)的样例。局地图的观测点为 `121.56601°E, 6.80706°N`,靠近全球几何掩甚点;图中的掩始、掩甚和掩终是该地点实际看到的站心接触时刻,并同时给出月面方向、白道、月高、方位和地平可见性。 + +![2025 月掩进贤增九指定地点见掩图](doc/lunar-occultation-hr4799-2025-06-05-local.svg) + +全球图展示同一事件的掩带边界、可见/几何中心线、全球阶段点以及每 30 分钟的中心线时间标记。全球掩始和掩终表示月影首次接触和最后离开地球,并不是上述指定地点的接触时刻。 + +![2025 月掩进贤增九全球见掩图](doc/lunar-occultation-hr4799-2025-06-05-global.svg) + +### 天象地图与 GeoJSON + +#### 全球见食图 SVG + +`eclipse/svg` 可直接生成日食和月食全球图。日食图绘制完整偏食可见区、全食/环食中心带、中心线、全球阶段信息和中心线时间标记;同时显示食甚时的晨昏圈与日下点、影轴进出地球点、`P1-P4/U1-U4` 接触点、定时半影轮廓和本影/反本影轮廓。月食图绘制 P1/P4 可见半球、月出/月落过渡区和整场可见区。 + +```go +package main + +import ( + "os" + "time" + + eclipsesvg "b612.me/astro/eclipse/svg" +) + +func main() { + cst := time.FixedZone("CST", 8*3600) + + solar, ok := eclipsesvg.SolarEclipseMapSVG( + time.Date(2009, 7, 22, 12, 0, 0, 0, cst), + eclipsesvg.SolarEclipseMapSVGOptions{ + Width: 1200, Height: 800, Location: cst, + TimeLabelStep: 30 * time.Minute, + PenumbralOutlineStep: 60 * time.Minute, + CentralShadowStep: 10 * time.Minute, + }, + ) + if ok { + _ = os.WriteFile("doc/solar-eclipse-yangshan-2009-global.svg", []byte(solar), 0o644) + } + + lunar, ok := eclipsesvg.LunarEclipseMapSVG( + time.Date(2029, 1, 1, 0, 0, 0, 0, cst), + eclipsesvg.LunarEclipseMapSVGOptions{Width: 1200, Height: 800, Location: cst}, + ) + if ok { + _ = os.WriteFile("doc/lunar-eclipse-2029-01-01-global.svg", []byte(lunar), 0o644) + } +} +``` + +日食图右侧事件表按时间列出可用的 `P1-P4/U1-U4`、影轴进出地球和食甚时刻,空间允许时同时显示接触点经纬度;摘要还包含沙罗序列、食带宽、食甚点太阳高度/方位和中心食持续时间。图中的橙色虚线是带 `HH:MM` 标记的瞬时半影边界,灰色虚线是食甚时晨昏圈,棕色实线是本影或反本影瞬时轮廓。 + +`PenumbralOutlineStep` 与 `CentralShadowStep` 的零值分别使用 60 分钟和 10 分钟,负值关闭对应轮廓;显式设置仍可使用 30 分钟等其他间隔。`TimeLabelStep` 的零值为 30 分钟,负值关闭中心线时刻标记。 + +日食和月掩的自动投影会在适合时选择北极或南极图;月食默认使用等经纬投影。投影仅影响 SVG 表达,不改变底层 WGS84 地理结果。 + +日月食通过 `EclipseMapProjectionEquirectangular`、`EclipseMapProjectionNorthPolar`、`EclipseMapProjectionSouthPolar` 强制投影;月掩使用对应的 `MapProjection...` 常量。 + +下面的全球图沿用前文局地 SVG 的事件日期。2009 长江大日食、2012 厦门日环食和 2035 北京日全食使用等经纬投影: + +![2009 长江大日食全球见食图](doc/solar-eclipse-yangshan-2009-global.svg) + +![2012 厦门日环食全球见食图](doc/solar-eclipse-xiamen-2012-global.svg) + +![2035 北京日全食全球见食图](doc/solar-eclipse-beijing-2035-global.svg) + +`2012-05-21` 日环食的偏食可见区覆盖北极点。下面把同一事件强制切换为北极方位等距投影,以便查看跨反经线的北极区见食范围;圆形边界是投影范围,不是行政或政治边界: + +![2012 日环食北极区全球见食图](doc/solar-eclipse-arctic-2012-global.svg) + +月食沿用前文 `2029-01-01` 跨年月全食,显示全程可见、带食月出、带食月落和不可见区域: + +![2029 跨年月全食全球可见图](doc/lunar-eclipse-2029-01-01-global.svg) + +#### GeoJSON + +`geojson` 接收已经计算好的日食、月食或月掩结果,返回 `[]byte`。这段字节是完整的 UTF-8 RFC 7946 `FeatureCollection` JSON,不是图片,也不是压缩数据,可以直接写入 `.geojson`、交给 `encoding/json`,或发送给前端地图组件。 + +```go +package main + +import ( + "encoding/json" + "fmt" + "time" + + "b612.me/astro/eclipse" + "b612.me/astro/geojson" +) + +func main() { + date := time.Date(2024, 4, 8, 0, 0, 0, 0, time.UTC) + partial, ok := eclipse.SolarEclipsePartialFootprints( + date, + eclipse.SolarEclipsePartialFootprintOptions{ + Step: 10 * time.Minute, BoundaryPoints: 180, + }, + ) + if !ok { + return + } + central, hasCentral := eclipse.SolarEclipseCentralPath( + date, + eclipse.SolarEclipsePathOptions{Step: time.Minute, TargetSpacingKM: 20}, + ) + var centralPath *eclipse.SolarEclipsePath + if hasCentral { + centralPath = ¢ral + } + + data, err := geojson.MarshalSolarEclipseWithTimeMarkers( + partial, centralPath, + geojson.TimeMarkerOptions{ + Step: 30 * time.Minute, + Location: time.FixedZone("CST", 8*3600), + }, + ) + fmt.Println(err, json.Valid(data)) +} +``` + +对应的无时间标记和带时间标记入口包括: + +- `MarshalSolarEclipse` / `MarshalSolarEclipseWithTimeMarkers` +- `MarshalLunarEclipse` / `MarshalLunarEclipseWithTimeMarkers` +- `MarshalStarOccultation` / `MarshalStarOccultationWithTimeMarkers` +- `MarshalPlanetOccultation` / `MarshalPlanetOccultationWithTimeMarkers` + +坐标统一为 WGS84 经度、纬度,跨反经线的线和面会拆分。带时路径的 `times` 属性与各段坐标逐点对齐;`WithTimeMarkers` 另加 `role=time-marker` 的 Point Feature,本地化 `label` 用于显示,`time` 始终是 UTC RFC 3339。 + +`TimeMarkerOptions.Step` 的零值为 30 分钟,正值至少 1 分钟,每次导出最多 1440 个标记。GeoJSON 不携带底图、国家边界、样式或投影;Web Mercator、极区图、瓦片选择和政治边界由应用自行决定。 + ### 行星 #### 内行星 @@ -1692,7 +2013,7 @@ func main() { 143.99353431082105 18.7404068044953 manual az=281.869347 alt=24.489608 zen=65.510392 ha=73.866900 gal lon=0.000047 lat=-0.000079 -apparent alt=10.092644 +apparent alt=10.093429 ``` `coord` 里的研究型接口不会自动代入当前日期的黄赤交角或恒星时,适合做“不同自转轴倾角”“手工指定时角”这类推演。常规观测计算仍建议使用 `EclipticToEquatorial`、`EquatorialToHorizontal` 等带 `time.Time` 的接口。 @@ -1925,7 +2246,8 @@ func main() { ## 已实现 - ✅ 太阳位置、高度角、天顶距、方位角、中天、晨昏朦影、日出日落、节气、日食、日面物理参数 -- ✅ 月亮位置、高度角、天顶距、方位角、中天、升落、月相、月食、天平动、近远地点、最大赤纬 +- ✅ 月亮位置、高度角、天顶距、方位角、中天、升落、月相、月食、天平动、近远地点、最大赤纬,以及恒星/行星月掩 +- ✅ 日食、月食和月掩的全球 SVG 投影图、指定地点月掩图、GeoJSON 与可选时间标记 - ✅ `lite/sun`、`lite/moon` 轻量太阳/月亮链路:面向分钟级升落、轻量位置和月相计算 - ✅ 地球偏心率、日地距离、近日点、远日点 - ✅ 真平恒星时、星座计算、常用坐标转换、大气折射、大气质量、视差角、银道坐标 diff --git a/basic/ancient_station_regression_test.go b/basic/ancient_station_regression_test.go new file mode 100644 index 0000000..96b1013 --- /dev/null +++ b/basic/ancient_station_regression_test.go @@ -0,0 +1,50 @@ +package basic + +import ( + "math" + "testing" + "time" +) + +const ancientStationTolerance = 10.0 / 1440.0 + +func ancientStationTT(year int, month time.Month, day int) float64 { + return TD2UT(Date2JDE(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)), true) +} + +func ancientStationUT(year int, month time.Month, day, hour, minute int) float64 { + return Date2JDE(time.Date(year, month, day, hour, minute, 0, 0, time.UTC)) +} + +func assertAncientNextStation(t *testing.T, queryTT, gotUT, wantUT float64) { + t.Helper() + if !eventUTQueryAfterOrEqual(gotUT, queryTT) { + t.Fatalf("station is before query: query TT %.9f, got UT %.9f", queryTT, gotUT) + } + if diff := math.Abs(gotUT - wantUT); diff > ancientStationTolerance { + t.Fatalf("station differs by %.3f minutes: got %s, want %s", diff*1440, + JDE2DateByZone(gotUT, time.UTC, false), JDE2DateByZone(wantUT, time.UTC, false)) + } +} + +func TestMarsAncientNextStationsStayOnTheirOppositionSides(t *testing.T) { + queryTT := ancientStationTT(-210, time.April, 1) + p2r := NextMarsProgradeToRetrograde(queryTT) + r2p := NextMarsRetrogradeToPrograde(queryTT) + + assertAncientNextStation(t, queryTT, p2r, ancientStationUT(-209, time.March, 25, 20, 20)) + assertAncientNextStation(t, queryTT, r2p, ancientStationUT(-209, time.June, 6, 11, 4)) + if sameEventJD(p2r, r2p) { + t.Fatalf("typed Mars stations collapsed to one event: %.9f", p2r) + } +} + +func TestMercuryAncientNextStationsDoNotSkipOrLoop(t *testing.T) { + janQueryTT := ancientStationTT(-210, time.January, 1) + assertAncientNextStation(t, janQueryTT, NextMercuryProgradeToRetrograde(janQueryTT), ancientStationUT(-210, time.February, 6, 21, 29)) + assertAncientNextStation(t, janQueryTT, NextMercuryRetrogradeToPrograde(janQueryTT), ancientStationUT(-210, time.March, 1, 14, 20)) + + marQueryTT := ancientStationTT(-210, time.March, 1) + assertAncientNextStation(t, marQueryTT, NextMercuryProgradeToRetrograde(marQueryTT), ancientStationUT(-210, time.June, 11, 18, 50)) + assertAncientNextStation(t, marQueryTT, NextMercuryRetrogradeToPrograde(marQueryTT), ancientStationUT(-210, time.March, 1, 14, 20)) +} diff --git a/basic/coordinate.go b/basic/coordinate.go index 0927b03..aa99bbc 100644 --- a/basic/coordinate.go +++ b/basic/coordinate.go @@ -84,7 +84,7 @@ func TopocentricRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) { sinpi := Sin(0.0024427777777) / au pcosi := pcosi(lat, h) psini := psini(lat, h) - tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra) + tH := Limit360(ApparentSiderealTime(jd)*15 + lon - ra) nra := math.Atan2(-pcosi*sinpi*Sin(tH), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi ndec := math.Atan2((Sin(dec)-psini*sinpi)*Cos(nra), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi @@ -92,22 +92,12 @@ func TopocentricRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) { } func TopocentricRa(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时 - sinpi := Sin(0.0024427777777) / au - pcosi := pcosi(lat, h) - tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra) - nra := math.Atan2(-pcosi*sinpi*Sin(tH), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi - return ra + nra + topocentricRA, _ := TopocentricRaDec(ra, dec, lat, lon, jd, au, h) + return topocentricRA } func TopocentricDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时 - - sinpi := Sin(0.0024427777777) / au - pcosi := pcosi(lat, h) - psini := psini(lat, h) - tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra) - nra := math.Atan2(-pcosi*sinpi*Sin(tH), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi - - ndec := math.Atan2((Sin(dec)-psini*sinpi)*Cos(nra), (Cos(dec)-pcosi*sinpi*Cos(tH))) * 180 / math.Pi - return ndec + _, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd, au, h) + return topocentricDec } func TopocentricLo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时 @@ -115,7 +105,7 @@ func TopocentricLo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林 s := psini(lat, h) sinpi := Sin(0.0024427777777) / au ra := LoToRa(jd, lo, bo) - tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra) + tH := Limit360(ApparentSiderealTime(jd)*15 + lon - ra) n := Cos(lo)*Cos(bo) - c*sinpi*Cos(tH) nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(s*Sin(TrueObliquity(jd))+c*Cos(TrueObliquity(jd))*Sin(tH)), n) * 180 / math.Pi return nlo @@ -126,7 +116,7 @@ func TopocentricBo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林 s := psini(lat, h) sinpi := Sin(0.0024427777777) / au ra := LoToRa(jd, lo, bo) - tH := Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + lon - ra) + tH := Limit360(ApparentSiderealTime(jd)*15 + lon - ra) n := Cos(lo)*Cos(bo) - c*sinpi*Cos(tH) nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(s*Sin(TrueObliquity(jd))+c*Cos(TrueObliquity(jd))*Sin(tH)), n) * 180 / math.Pi nbo := math.Atan2(Cos(nlo)*(Sin(bo)-sinpi*(s*Cos(TrueObliquity(jd))-c*Sin(TrueObliquity(jd))*Sin(tH))), n) * 180 / math.Pi diff --git a/basic/coordinate_topocentric_test.go b/basic/coordinate_topocentric_test.go new file mode 100644 index 0000000..efa0e25 --- /dev/null +++ b/basic/coordinate_topocentric_test.go @@ -0,0 +1,81 @@ +package basic + +import ( + "math" + "testing" + "time" + + . "b612.me/astro/tools" +) + +func TestTopocentricRaDecUsesUTJulianDateForSiderealTime(t *testing.T) { + ut := Date2JDE(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC)) + ra := 189.527817246 + dec := -5.973400893 + lat := 6.79657 + lon := 121.55381 + distanceAU := HMoonAwayN(TD2UT(ut, true), -1) / 149597870.7 + + gotRA, gotDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0) + wantRA, wantDec := independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0) + if delta := angularDistanceArcsec(gotRA, gotDec, wantRA, wantDec); delta > 1e-6 { + t.Fatalf("TopocentricRaDec differs from independent formula by %.9f arcsec", delta) + } +} + +func TestTopocentricRaAndDecMatchCombinedResult(t *testing.T) { + ut := Date2JDE(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC)) + ra := 189.527817246 + dec := -5.973400893 + lat := 6.79657 + lon := 121.55381 + distanceAU := HMoonAwayN(TD2UT(ut, true), -1) / 149597870.7 + + wantRA, wantDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0) + if got := TopocentricRa(ra, dec, lat, lon, ut, distanceAU, 0); got != wantRA { + t.Fatalf("TopocentricRa = %.12f, want %.12f", got, wantRA) + } + if got := TopocentricDec(ra, dec, lat, lon, ut, distanceAU, 0); got != wantDec { + t.Fatalf("TopocentricDec = %.12f, want %.12f", got, wantDec) + } +} + +func TestHMoonHeightUsesUTForTopocentricCorrection(t *testing.T) { + ut := Date2JDE(time.Date(2026, 4, 28, 16, 1, 30, 0, time.UTC)) + longitude := 0.0 + latitude := 51.4779 + ra, dec := HMoonApparentRaDecN(ut, longitude, latitude, 0, -1) + hourAngle := Limit360(ApparentSiderealTime(ut)*15 + longitude - ra) + want := ArcSin(Sin(latitude)*Sin(dec) + Cos(dec)*Cos(latitude)*Cos(hourAngle)) + got := HMoonHeightN(ut, longitude, latitude, 0, -1) + if difference := math.Abs(got - want); difference > 1e-10 { + t.Fatalf("HMoonHeightN differs from the UT topocentric position by %.12f degrees", difference) + } +} + +func independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, height float64) (float64, float64) { + const ( + equatorialRadiusKM = 6378.14 + polarRadiusKM = 6356.755 + ) + u := math.Atan(polarRadiusKM / equatorialRadiusKM * Tan(lat)) + rhoCos := math.Cos(u) + height/6378140.0*Cos(lat) + rhoSin := polarRadiusKM/equatorialRadiusKM*math.Sin(u) + height/6378140.0*Sin(lat) + sinParallax := Sin(0.0024427777777) / distanceAU + hourAngle := Limit360(ApparentSiderealTime(ut)*15 + lon - ra) + deltaRA := math.Atan2( + -rhoCos*sinParallax*Sin(hourAngle), + Cos(dec)-rhoCos*sinParallax*Cos(hourAngle), + ) + topRA := ra + deltaRA*180/math.Pi + topDec := math.Atan2( + (Sin(dec)-rhoSin*sinParallax)*math.Cos(deltaRA), + Cos(dec)-rhoCos*sinParallax*Cos(hourAngle), + ) * 180 / math.Pi + return topRA, topDec +} + +func angularDistanceArcsec(ra1, dec1, ra2, dec2 float64) float64 { + cosDistance := Sin(dec1)*Sin(dec2) + Cos(dec1)*Cos(dec2)*Cos(ra1-ra2) + return math.Acos(math.Max(-1, math.Min(1, cosDistance))) * 180 / math.Pi * 3600 +} diff --git a/basic/event_refine.go b/basic/event_refine.go index 5b3602a..dff4dca 100644 --- a/basic/event_refine.go +++ b/basic/event_refine.go @@ -2,6 +2,125 @@ package basic import "math" +const ( + eventNewtonMaxIterations = 24 + eventDirectionalSearchIterations = 128 + eventRiseSetScanStep = 1.0 / 1440 +) + +func isFiniteFloat(value float64) bool { + return !math.IsNaN(value) && !math.IsInf(value, 0) +} + +// eventNewtonRefine 执行有界牛顿迭代;修正函数返回 f(x)/f'(x),调用者保留现有导数计算 / eventNewtonRefine performs a bounded Newton iteration. The correction +// 对格式错误输入和不收敛迭代快速失败 / function returns f(x)/f'(x), so callers retain their existing derivative +// 计算 / calculation while malformed input and non-convergent iterations fail fast. +func eventNewtonRefine(seed, tolerance float64, correction func(float64) float64) (float64, bool) { + if !isFiniteFloat(seed) || !isFiniteFloat(tolerance) || tolerance <= 0 { + return math.NaN(), false + } + current := seed + for i := 0; i < eventNewtonMaxIterations; i++ { + step := correction(current) + if !isFiniteFloat(step) { + return math.NaN(), false + } + next := current - step + if !isFiniteFloat(next) { + return math.NaN(), false + } + if math.Abs(next-current) <= tolerance { + return next, true + } + current = next + } + return math.NaN(), false +} + +func eventRiseSetCandidateValid(candidate, civilDayStart, slope float64, isRise bool) bool { + if !isFiniteFloat(candidate) || !isFiniteFloat(civilDayStart) || !isFiniteFloat(slope) || + candidate < civilDayStart || candidate >= civilDayStart+1 { + return false + } + if isRise { + return slope > 0 + } + return slope < 0 +} + +func eventDirectionalRiseSetSearch(civilDayStart float64, isRise bool, fallbackErr error, + residual func(float64) float64) (float64, error) { + if !isFiniteFloat(civilDayStart) { + return 0, ErrInvalidObservationInput + } + + previousJD := civilDayStart + previousValue := residual(previousJD) + if !isFiniteFloat(previousValue) { + return 0, ErrInvalidObservationInput + } + minimum, maximum := previousValue, previousValue + steps := int(math.Round(1 / eventRiseSetScanStep)) + for i := 1; i <= steps; i++ { + currentJD := civilDayStart + float64(i)*eventRiseSetScanStep + currentValue := residual(currentJD) + if !isFiniteFloat(currentValue) { + return 0, ErrInvalidObservationInput + } + minimum = math.Min(minimum, currentValue) + maximum = math.Max(maximum, currentValue) + if eventCrossesDirection(previousValue, currentValue, isRise) { + eventJD := eventDirectionalBracketRefine(previousJD, currentJD, previousValue, currentValue, residual) + if eventJD < civilDayStart+1 { + return eventJD, nil + } + } + previousJD = currentJD + previousValue = currentValue + } + + switch { + case fallbackErr != nil: + return 0, fallbackErr + case maximum < 0: + return 0, ErrNeverRise + case minimum > 0: + return 0, ErrNeverSet + default: + return 0, ErrNotOnThisDate + } +} + +func eventCrossesDirection(leftValue, rightValue float64, isRise bool) bool { + if isRise { + return leftValue <= 0 && rightValue >= 0 && leftValue != rightValue + } + return leftValue >= 0 && rightValue <= 0 && leftValue != rightValue +} + +func eventDirectionalBracketRefine(leftJD, rightJD, leftValue, rightValue float64, residual func(float64) float64) float64 { + if leftValue == 0 { + return leftJD + } + if rightValue == 0 { + return rightJD + } + for i := 0; i < 48; i++ { + middleJD := (leftJD + rightJD) / 2 + middleValue := residual(middleJD) + if middleValue == 0 { + return middleJD + } + if (leftValue < 0) == (middleValue < 0) { + leftJD = middleJD + leftValue = middleValue + } else { + rightJD = middleJD + } + } + return (leftJD + rightJD) / 2 +} + func eventFixedScanRefine(seed, halfWindow, step float64, fn func(float64) float64) float64 { start := seed - halfWindow bestJD := start diff --git a/basic/jupiter.go b/basic/jupiter.go index 68a8482..dde6176 100644 --- a/basic/jupiter.go +++ b/basic/jupiter.go @@ -173,14 +173,14 @@ func JupiterCulminationTime(jde, lon, timezone float64) float64 { } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/jupiter_events.go b/basic/jupiter_events.go index 70ed724..2bd664f 100644 --- a/basic/jupiter_events.go +++ b/basic/jupiter_events.go @@ -64,6 +64,9 @@ func jupiterRADerivativeN(jde, delta float64, n int) float64 { func jupiterConjunctionFull(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := JUPITER_S_PERIOD / 360 currentDelta := jupiterSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -72,20 +75,29 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } func jupiterConjunction(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := JUPITER_S_PERIOD / 360 currentDelta := jupiterSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -94,24 +106,36 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := jupiterSunLongitudeDeltaN(prevJD, degree, true, jupiterEventSearchN) longitudeSlope := (jupiterSunLongitudeDeltaN(prevJD+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJD-0.000005, degree, true, jupiterEventSearchN)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= jupiterPhaseCoarseTolerance { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= jupiterPhaseCoarseTolerance { + converged = true break } } - for { + if !converged { + return math.NaN() + } + converged = false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } diff --git a/basic/jupiter_satellites.go b/basic/jupiter_satellites.go index 6716d32..a82b9f5 100644 --- a/basic/jupiter_satellites.go +++ b/basic/jupiter_satellites.go @@ -361,17 +361,32 @@ func jupiterGalileanElementsToPV(mu float64, elements jupiterGalileanElements) [ p := elements.P a := elements.A al := elements.L + invalid := func() [6]float64 { + nan := math.NaN() + return [6]float64{nan, nan, nan, nan, nan, nan} + } + if !isFiniteFloat(mu) || !isFiniteFloat(k) || !isFiniteFloat(h) || !isFiniteFloat(q) || !isFiniteFloat(p) || !isFiniteFloat(a) || !isFiniteFloat(al) || a == 0 { + return invalid() + } an := math.Sqrt(mu / math.Pow(a, 3)) ee := al + k*math.Sin(al) - h*math.Cos(al) - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { ce := math.Cos(ee) se := math.Sin(ee) de := (al - ee + k*se - h*ce) / (1 - k*ce - h*se) + if !isFiniteFloat(de) { + return invalid() + } ee += de if math.Abs(de) < 1e-12 { + converged = true break } } + if !converged { + return invalid() + } ce := math.Cos(ee) se := math.Sin(ee) dle := h*ce - k*se diff --git a/basic/mars.go b/basic/mars.go index b1dcd50..98baf58 100644 --- a/basic/mars.go +++ b/basic/mars.go @@ -183,14 +183,14 @@ func MarsCulminationTime(jde, lon, timezone float64) float64 { } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/mars_events.go b/basic/mars_events.go index 8fffb54..6e2be60 100644 --- a/basic/mars_events.go +++ b/basic/mars_events.go @@ -9,9 +9,12 @@ import ( // Pos const ( - MARS_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 686.98)) - marsEventSearchN = 16 - marsPhaseCoarseTolerance = 30.0 / 86400.0 + MARS_S_PERIOD = 1 / ((1 / 365.256363004) - (1 / 686.98)) + marsEventSearchN = 16 + marsPhaseCoarseTolerance = 30.0 / 86400.0 + marsStationDerivativeStepDay = 0.01 + marsStationCoarseStepDay = 6.0 + marsStationHalfWindowDay = 6.0 ) func marsSunLongitudeDelta(jde, degree float64, filter bool) float64 { @@ -64,6 +67,9 @@ func marsRADerivativeN(jde, val float64, n int) float64 { func marsConjunctionFull(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := MARS_S_PERIOD / 360 currentDelta := marsSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -72,20 +78,29 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } func marsConjunction(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := MARS_S_PERIOD / 360 currentDelta := marsSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -94,24 +109,36 @@ func marsConjunction(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := marsSunLongitudeDeltaN(prevJD, degree, true, marsEventSearchN) longitudeSlope := (marsSunLongitudeDeltaN(prevJD+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJD-0.000005, degree, true, marsEventSearchN)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= marsPhaseCoarseTolerance { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= marsPhaseCoarseTolerance { + converged = true break } } - for { + if !converged { + return math.NaN() + } + converged = false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } @@ -148,122 +175,60 @@ func LastMarsWesternQuadrature(jde float64) float64 { } func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition bool) float64 { - jde := oppositionJD + oppositionTT := TD2UT(oppositionJD, true) + startTT := oppositionTT + endTT := oppositionTT if searchBeforeOpposition { - jde -= 60 + easternQuadratureUT := marsConjunction(oppositionTT, 90, 0) + startTT = TD2UT(easternQuadratureUT, true) } else { - jde += 60 + westernQuadratureUT := marsConjunction(oppositionTT, 270, 1) + endTT = TD2UT(westernQuadratureUT, true) } - for { - currentRate := marsRADerivative(jde, 1.0/86400.0) - if math.Abs(currentRate) > 0.55 { - jde += 2 - continue - } - break - } - estimateJD := jde - for { - prevJD := estimateJD - rateValue := marsRADerivative(prevJD, 2.0/86400.0) - rateSlope := (marsRADerivative(prevJD+15.0/86400.0, 2.0/86400.0) - marsRADerivative(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0) - estimateJD = prevJD - rateValue/rateSlope - if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 { - break - } - } - bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 { - return marsRADerivative(jd, 0.5/86400.0) + bestJD := zeroEventInWindow(startTT, endTT, marsStationCoarseStepDay, marsStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 { + return marsRADerivativeN(jd, marsStationDerivativeStepDay, marsEventSearchN) + }, func(jd float64) float64 { + return marsRADerivative(jd, marsStationDerivativeStepDay) }) return TD2UT(bestJD, false) } -func marsOppositionFromBefore(oppositionJD float64) float64 { - return marsConjunctionFull(eventUTLastQueryTT(oppositionJD), 180, 1) -} - -func marsOppositionFromAfter(oppositionJD float64) float64 { - return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0) -} - -func stabilizeMarsStationNearQuery(jde, date float64, searchBeforeOpposition bool) float64 { - if math.Abs(eventUTQueryTTDelta(date, jde)) > exactEventTolerance { - return date - } - if searchBeforeOpposition { - stableOppositionJD := NextMarsOpposition(jde) - sameOppositionJD := marsOppositionFromAfter(stableOppositionJD) - return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, true), marsRetrogradeAroundOpposition(sameOppositionJD, true)) - } - stableOppositionJD := LastMarsOpposition(jde) - sameOppositionJD := marsOppositionFromBefore(stableOppositionJD) - return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, false), marsRetrogradeAroundOpposition(sameOppositionJD, false)) -} - func NextMarsRetrogradeToPrograde(jde float64) float64 { lastOppositionJD := marsConjunctionFull(jde, 180, 0) date := marsRetrogradeAroundOpposition(lastOppositionJD, false) - date = stabilizeMarsStationNearQuery(jde, date, false) - if sameEventUTQueryTT(date, jde) { - stableOppositionJD := LastMarsOpposition(jde) - stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false) - sameOppositionJD := marsOppositionFromBefore(stableOppositionJD) - return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false)) + if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) { + return date } - if !eventUTQueryAfterOrEqual(date, jde) { - nextOppositionJD := marsConjunctionFull(jde, 180, 1) - return marsRetrogradeAroundOpposition(nextOppositionJD, false) - } - return date + nextOppositionJD := marsConjunctionFull(jde, 180, 1) + return marsRetrogradeAroundOpposition(nextOppositionJD, false) } func LastMarsRetrogradeToPrograde(jde float64) float64 { lastOppositionJD := marsConjunctionFull(jde, 180, 0) date := marsRetrogradeAroundOpposition(lastOppositionJD, false) - date = stabilizeMarsStationNearQuery(jde, date, false) - if sameEventUTQueryTT(date, jde) { - stableOppositionJD := LastMarsOpposition(jde) - stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false) - sameOppositionJD := marsOppositionFromBefore(stableOppositionJD) - return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false)) + if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) { + return date } - if !eventUTQueryBeforeOrEqual(date, jde) { - previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0) - return marsRetrogradeAroundOpposition(previousOppositionJD, false) - } - return date + previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0) + return marsRetrogradeAroundOpposition(previousOppositionJD, false) } func NextMarsProgradeToRetrograde(jde float64) float64 { nextOppositionJD := marsConjunctionFull(jde, 180, 1) date := marsRetrogradeAroundOpposition(nextOppositionJD, true) - date = stabilizeMarsStationNearQuery(jde, date, true) - if sameEventUTQueryTT(date, jde) { - stableOppositionJD := NextMarsOpposition(jde) - stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true) - sameOppositionJD := marsOppositionFromAfter(stableOppositionJD) - return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true)) + if sameEventUTQueryTT(date, jde) || eventUTQueryAfterOrEqual(date, jde) { + return date } - if !eventUTQueryAfterOrEqual(date, jde) { - followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1) - return marsRetrogradeAroundOpposition(followingOppositionJD, true) - } - return date + followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1) + return marsRetrogradeAroundOpposition(followingOppositionJD, true) } func LastMarsProgradeToRetrograde(jde float64) float64 { nextOppositionJD := marsConjunctionFull(jde, 180, 1) date := marsRetrogradeAroundOpposition(nextOppositionJD, true) - date = stabilizeMarsStationNearQuery(jde, date, true) - if sameEventUTQueryTT(date, jde) { - stableOppositionJD := NextMarsOpposition(jde) - stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true) - sameOppositionJD := marsOppositionFromAfter(stableOppositionJD) - return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true)) + if sameEventUTQueryTT(date, jde) || eventUTQueryBeforeOrEqual(date, jde) { + return date } - if !eventUTQueryBeforeOrEqual(date, jde) { - lastOppositionJD := marsConjunctionFull(jde, 180, 0) - return marsRetrogradeAroundOpposition(lastOppositionJD, true) - } - return date + lastOppositionJD := marsConjunctionFull(jde, 180, 0) + return marsRetrogradeAroundOpposition(lastOppositionJD, true) } diff --git a/basic/mercury.go b/basic/mercury.go index 82a64e9..2fc2ea3 100644 --- a/basic/mercury.go +++ b/basic/mercury.go @@ -166,14 +166,14 @@ func MercuryCulminationTime(jde, lon, timezone float64) float64 { } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/mercury_events.go b/basic/mercury_events.go index 42e10c9..8ed1f2c 100644 --- a/basic/mercury_events.go +++ b/basic/mercury_events.go @@ -12,6 +12,11 @@ const ( mercuryConjunctionDerivativeStepDay = 2e-5 * 36525.0 mercuryLightTimeDaysPerAU = 0.0057755183 mercuryEventSearchN = 16 + mercuryStationWindowDays = 30.0 + mercuryStationDerivativeStepDay = 0.01 + mercuryStationCoarseStepDay = 2.0 + mercuryStationHalfWindowDay = 2.0 + mercuryStationMotionTolerance = 1e-3 ) type mercuryConjunctionLBR struct { @@ -109,65 +114,29 @@ func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 { func mercuryConjunctionExactTT(seed float64, inferior bool) float64 { estimateJD := mercuryConjunctionApproxTT(seed, inferior) - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := mercuryConjunctionExactDelta(prevJD) longitudeSlope := (mercuryConjunctionExactDelta(prevJD+0.000005) - mercuryConjunctionExactDelta(prevJD-0.000005)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return estimateJD } -func mercuryConjunctionLegacy(jde float64, next uint8) float64 { - //0=last 1=next - longitudeDeltaAt := func(jde float64) float64 { - return mercuryConjunctionExactDelta(jde) - } - currentDelta := longitudeDeltaAt(jde) - distanceTrend := math.Abs(longitudeDeltaAt(jde+1/86400.0)) - math.Abs(currentDelta) - if distanceTrend >= 0 && next == 1 && currentDelta > 0 { - jde += MERCURY_S_PERIOD/8.0 + 2 - } - if distanceTrend >= 0 && next == 1 && currentDelta < 0 { - jde += MERCURY_S_PERIOD/6.0 + 2 - } - if distanceTrend <= 0 && next == 0 && currentDelta < 0 { - jde -= MERCURY_S_PERIOD/8.0 + 2 - } - if distanceTrend <= 0 && next == 0 && currentDelta > 0 { - jde -= MERCURY_S_PERIOD/6.0 + 2 - } - for { - currentDelta := longitudeDeltaAt(jde) - distanceTrend := math.Abs(longitudeDeltaAt(jde+1/86400.0)) - math.Abs(currentDelta) - if math.Abs(currentDelta) > 12 || (distanceTrend > 0 && next == 1) || (distanceTrend < 0 && next == 0) { - if next == 1 { - jde += 2 - } else { - jde -= 2 - } - continue - } - break - } - estimateJD := jde - for { - prevJD := estimateJD - longitudeDelta := longitudeDeltaAt(prevJD) - longitudeSlope := (longitudeDeltaAt(prevJD+0.000005) - longitudeDeltaAt(prevJD-0.000005)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } - } - return TD2UT(estimateJD, false) -} - func mercuryConjunction(jde float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) { + return math.NaN() + } if math.Abs(mercuryConjunctionExactDelta(jde)) <= 30.0/86400.0 { best := math.NaN() consider := func(inferior bool) { @@ -203,9 +172,14 @@ func mercuryConjunction(jde float64, next uint8) float64 { if distanceTrend <= 0 && next == 0 && currentDelta > 0 { jde -= MERCURY_S_PERIOD/6.0 + 2 } - for { + found := false + for i := 0; i < eventDirectionalSearchIterations; i++ { currentDelta := mercuryConjunctionExactDelta(jde) - distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta) + nextDelta := mercuryConjunctionExactDelta(jde + 1/86400.0) + if !isFiniteFloat(currentDelta) || !isFiniteFloat(nextDelta) { + return math.NaN() + } + distanceTrend := math.Abs(nextDelta) - math.Abs(currentDelta) if math.Abs(currentDelta) > 12 || (distanceTrend > 0 && next == 1) || (distanceTrend < 0 && next == 0) { if next == 1 { jde += 2 @@ -214,11 +188,18 @@ func mercuryConjunction(jde float64, next uint8) float64 { } continue } + found = true break } + if !found { + return math.NaN() + } inferior := mercuryConjunctionExactTT(jde, true) superior := mercuryConjunctionExactTT(jde, false) + if !isFiniteFloat(inferior) || !isFiniteFloat(superior) { + return math.NaN() + } best := inferior if math.Abs(superior-jde) < math.Abs(inferior-jde) { best = superior @@ -274,51 +255,6 @@ func LastMercurySuperiorConjunction(jde float64) float64 { return date } -func mercuryRetrograde(jde float64) float64 { - //0=last 1=next - solarRADelta := func(jde float64) float64 { - sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde)) - if sub > 180 { - sub -= 360 - } - if sub < -180 { - sub += 360 - } - return sub - } - lastConjunction := mercuryConjunctionLegacy(jde, 0) - nextConjunction := mercuryConjunctionLegacy(jde, 1) - currentRADelta := solarRADelta(jde) - if currentRADelta > 0 { - jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.0 * 3.5) - } else { - jde = lastConjunction + ((nextConjunction - lastConjunction) / 5.5) - } - for { - currentRate := mercuryRADerivative(jde, 1.0/86400.0) - if math.Abs(currentRate) > 0.55 { - jde += 2 - continue - } - break - } - estimateJD := jde - for { - prevJD := estimateJD - rateValue := mercuryRADerivative(prevJD, 2.0/86400.0) - rateSlope := (mercuryRADerivative(prevJD+15.0/86400.0, 2.0/86400.0) - mercuryRADerivative(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0) - estimateJD = prevJD - rateValue/rateSlope - if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 { - break - } - } - bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 { - return mercuryRADerivative(jd, 0.5/86400.0) - }) - //fmt.Println((bestJD - lastConjunction) / (nextConjunction - lastConjunction)) - return TD2UT(bestJD, false) -} - func mercuryRADerivative(jde, delta float64) float64 { sub := MercuryApparentRa(jde+delta) - MercuryApparentRa(jde-delta) if sub > 180 { @@ -330,55 +266,174 @@ func mercuryRADerivative(jde, delta float64) float64 { return sub / (2 * delta) } -func mercuryStationIsProgradeToRetrograde(eventUT float64) bool { - for _, offset := range []float64{0.25, 0.5, 1.0} { - before := mercuryRADerivative(eventUT-offset, 0.5/86400.0) - after := mercuryRADerivative(eventUT+offset, 0.5/86400.0) - if before > 0 && after < 0 { +func mercuryRADerivativeN(jde, delta float64, n int) float64 { + sub := MercuryApparentRaN(jde+delta, n) - MercuryApparentRaN(jde-delta, n) + if sub > 180 { + sub -= 360 + } + if sub < -180 { + sub += 360 + } + return sub / (2 * delta) +} + +func mercuryStationInWindow(startTT, endTT float64) float64 { + bestJD := zeroEventInWindow(startTT, endTT, mercuryStationCoarseStepDay, mercuryStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 { + return mercuryRADerivativeN(jd, mercuryStationDerivativeStepDay, mercuryEventSearchN) + }, func(jd float64) float64 { + return mercuryRADerivative(jd, mercuryStationDerivativeStepDay) + }) + return TD2UT(bestJD, false) +} + +func mercuryStationBetween(startTT, endTT float64) bool { + if endTT < startTT { + startTT, endTT = endTT, startTT + } + if endTT-startTT <= 0 { + return false + } + if endTT-startTT > mercuryStationWindowDays { + return true + } + // 截断扫描足以判断单候选快速路径是否安全 / A truncated scan is enough to decide whether the one-candidate fast path is safe. + left := startTT + leftValue := mercuryRADerivativeN(left, mercuryStationDerivativeStepDay, mercuryEventSearchN) + for left < endTT { + right := left + mercuryStationCoarseStepDay + if right > endTT { + right = endTT + } + rightValue := mercuryRADerivativeN(right, mercuryStationDerivativeStepDay, mercuryEventSearchN) + if leftValue == 0 || leftValue*rightValue < 0 || rightValue == 0 { return true } - if before < 0 && after > 0 { - return false - } + left = right + leftValue = rightValue } - before := mercuryRADerivative(eventUT-0.25, 0.5/86400.0) - after := mercuryRADerivative(eventUT+0.25, 0.5/86400.0) - return before > after + return false } -func nextMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 { - date := NextMercuryRetrogradeStrict(jde) - for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde { - date = NextMercuryRetrogradeStrict(eventUTNextQueryTT(date)) - } - return date +func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64) float64 { + inferiorTT := TD2UT(inferiorUT, true) + return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT) } -func lastMercuryTypedStation(jde float64, progradeToRetrograde bool) float64 { - date := LastMercuryRetrogradeStrict(jde) - for mercuryStationIsProgradeToRetrograde(date) != progradeToRetrograde { - date = LastMercuryRetrogradeStrict(eventUTLastQueryTT(date)) +func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64) float64 { + inferiorTT := TD2UT(inferiorUT, true) + return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays) +} + +func NextMercuryProgradeToRetrograde(jde float64) float64 { + inferior := NextMercuryInferiorConjunction(jde) + date := mercuryProgradeToRetrogradeAroundInferior(inferior) + if eventUTQueryAfterOrEqual(date, jde) { + return date } - return date + followingInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(inferior)) + return mercuryProgradeToRetrogradeAroundInferior(followingInferior) +} + +func NextMercuryRetrogradeToPrograde(jde float64) float64 { + inferior := LastMercuryInferiorConjunction(jde) + date := mercuryRetrogradeToProgradeAroundInferior(inferior) + if eventUTQueryAfterOrEqual(date, jde) { + return date + } + nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(inferior)) + return mercuryRetrogradeToProgradeAroundInferior(nextInferior) +} + +func LastMercuryProgradeToRetrograde(jde float64) float64 { + inferior := NextMercuryInferiorConjunction(jde) + date := mercuryProgradeToRetrogradeAroundInferior(inferior) + if eventUTQueryBeforeOrEqual(date, jde) { + return date + } + previousInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(inferior)) + return mercuryProgradeToRetrogradeAroundInferior(previousInferior) +} + +func LastMercuryRetrogradeToPrograde(jde float64) float64 { + inferior := LastMercuryInferiorConjunction(jde) + date := mercuryRetrogradeToProgradeAroundInferior(inferior) + if eventUTQueryBeforeOrEqual(date, jde) { + return date + } + previousInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(inferior)) + return mercuryRetrogradeToProgradeAroundInferior(previousInferior) +} + +func nextMercuryRetrogradeFromTyped(jde float64) float64 { + p2r := NextMercuryProgradeToRetrograde(jde) + r2p := NextMercuryRetrogradeToPrograde(jde) + if p2r < r2p { + return p2r + } + return r2p } func NextMercuryRetrograde(jde float64) float64 { - date := mercuryRetrograde(jde) - if !eventUTQueryAfterOrEqual(date, jde) { - nextConjunction := NextMercuryConjunctionStrict(jde) - return mercuryRetrograde(nextConjunction + 2) + motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay) + if motion > mercuryStationMotionTolerance { + p2r := NextMercuryProgradeToRetrograde(jde) + if !mercuryStationBetween(jde, TD2UT(p2r, true)) { + return p2r + } + r2p := NextMercuryRetrogradeToPrograde(jde) + if p2r < r2p { + return p2r + } + return r2p } - return date + if motion < -mercuryStationMotionTolerance { + r2p := NextMercuryRetrogradeToPrograde(jde) + if !mercuryStationBetween(jde, TD2UT(r2p, true)) { + return r2p + } + p2r := NextMercuryProgradeToRetrograde(jde) + if p2r < r2p { + return p2r + } + return r2p + } + return nextMercuryRetrogradeFromTyped(jde) +} + +func lastMercuryRetrogradeFromTyped(jde float64) float64 { + p2r := LastMercuryProgradeToRetrograde(jde) + r2p := LastMercuryRetrogradeToPrograde(jde) + if p2r > r2p { + return p2r + } + return r2p } func LastMercuryRetrograde(jde float64) float64 { - lastConjunction := LastMercuryConjunctionStrict(jde) - date := mercuryRetrograde(lastConjunction + 2) - if !eventUTQueryBeforeOrEqual(date, jde) { - previousConjunction := LastMercuryConjunctionStrict(eventUTLastQueryTT(lastConjunction)) - return mercuryRetrograde(previousConjunction + 2) + motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay) + if motion > mercuryStationMotionTolerance { + r2p := LastMercuryRetrogradeToPrograde(jde) + if !mercuryStationBetween(TD2UT(r2p, true), jde) { + return r2p + } + p2r := LastMercuryProgradeToRetrograde(jde) + if p2r > r2p { + return p2r + } + return r2p } - return date + if motion < -mercuryStationMotionTolerance { + p2r := LastMercuryProgradeToRetrograde(jde) + if !mercuryStationBetween(TD2UT(p2r, true), jde) { + return p2r + } + r2p := LastMercuryRetrogradeToPrograde(jde) + if p2r > r2p { + return p2r + } + return r2p + } + return lastMercuryRetrogradeFromTyped(jde) } func LastMercuryRetrogradeStrict(jde float64) float64 { @@ -389,22 +444,6 @@ func NextMercuryRetrogradeStrict(jde float64) float64 { return NextMercuryRetrograde(jde) } -func NextMercuryProgradeToRetrograde(jde float64) float64 { - return nextMercuryTypedStation(jde, true) -} - -func NextMercuryRetrogradeToPrograde(jde float64) float64 { - return nextMercuryTypedStation(jde, false) -} - -func LastMercuryProgradeToRetrograde(jde float64) float64 { - return lastMercuryTypedStation(jde, true) -} - -func LastMercuryRetrogradeToPrograde(jde float64) float64 { - return lastMercuryTypedStation(jde, false) -} - func MercurySunElongation(jde float64) float64 { lo1, bo1 := MercuryApparentLoBo(jde) lo2 := HSunApparentLo(jde) @@ -466,52 +505,77 @@ func mercuryWestElongationWindowContaining(jde float64) (float64, float64) { } func nextMercuryGreatestElongationTyped(jde float64, east bool) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } if east { start, windowEnd := mercuryEastElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := mercuryGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryAfterOrEqual(date, jde) { return date } nextInferior := NextMercuryInferiorConjunction(eventUTNextQueryTT(windowEnd)) start, windowEnd = mercuryEastElongationWindowEndingAt(nextInferior) } + return math.NaN() } start, windowEnd := mercuryWestElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := mercuryGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryAfterOrEqual(date, jde) { return date } nextSuperior := NextMercurySuperiorConjunction(eventUTNextQueryTT(windowEnd)) start, windowEnd = mercuryWestElongationWindowEndingAt(nextSuperior) } + return math.NaN() } func lastMercuryGreatestElongationTyped(jde float64, east bool) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } if east { start, windowEnd := mercuryEastElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := mercuryGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryBeforeOrEqual(date, jde) { return date } prevInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(start)) start, windowEnd = mercuryEastElongationWindowEndingAt(prevInferior) } + return math.NaN() } start, windowEnd := mercuryWestElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := mercuryGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryBeforeOrEqual(date, jde) { return date } prevSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(start)) start, windowEnd = mercuryWestElongationWindowEndingAt(prevSuperior) } + return math.NaN() } func mercuryGreatestElongation(jde float64) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } solarRADelta := func(jde float64) float64 { sub := Limit360(MercuryApparentRa(jde) - SunApparentRa(jde)) if sub > 180 { @@ -540,23 +604,31 @@ func mercuryGreatestElongation(jde float64) float64 { } else { jde = lastConjunction + ((nextConjunction - lastConjunction) / 6.0) } - for { + found := false + for i := 0; i < eventDirectionalSearchIterations; i++ { currentRate := elongationRate(jde, 1.0/86400.0) + if !isFiniteFloat(currentRate) { + return math.NaN() + } if math.Abs(currentRate) > 0.4 { jde += 2 continue } + found = true break } + if !found { + return math.NaN() + } estimateJD := jde - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 30.0/86400.0, func(prevJD float64) float64 { rateValue := elongationRate(prevJD, 2.0/86400.0) rateSlope := (elongationRate(prevJD+15.0/86400.0, 2.0/86400.0) - elongationRate(prevJD-15.0/86400.0, 2.0/86400.0)) / (30.0 / 86400.0) - estimateJD = prevJD - rateValue/rateSlope - if math.Abs(estimateJD-prevJD) <= 30.0/86400.0 { - break - } + return rateValue / rateSlope + }) + if !ok { + return math.NaN() } bestJD := eventZeroRefine(estimateJD, 15.0/86400.0, 0.5/86400.0, func(jd float64) float64 { return elongationRate(jd, 0.5/86400.0) diff --git a/basic/mercury_retrograde_optimization_test.go b/basic/mercury_retrograde_optimization_test.go new file mode 100644 index 0000000..78da959 --- /dev/null +++ b/basic/mercury_retrograde_optimization_test.go @@ -0,0 +1,31 @@ +package basic + +import ( + "math" + "testing" +) + +func TestMercuryRetrogradeFastPathKeepsTypedCandidateOrder(t *testing.T) { + // 这些查询覆盖一个普通古代周期和两个长间隔边界 / These queries cover a normal ancient cycle and two long-gap boundaries + //,类型化驻留搜索必须作为最终权威 / where the typed station search must remain the final authority. + for _, queryUT := range []float64{ + 1026548.810500779, + 1644733.927538287, + 1645082.416782375, + } { + queryTT := TD2UT(queryUT, true) + nextP2R := NextMercuryProgradeToRetrograde(queryTT) + nextR2P := NextMercuryRetrogradeToPrograde(queryTT) + wantNext := math.Min(nextP2R, nextR2P) + if got := NextMercuryRetrograde(queryTT); math.Abs(got-wantNext) > 1e-7 { + t.Fatalf("next aggregate mismatch at %.9f: got %.12f want %.12f", queryUT, got, wantNext) + } + + lastP2R := LastMercuryProgradeToRetrograde(queryTT) + lastR2P := LastMercuryRetrogradeToPrograde(queryTT) + wantLast := math.Max(lastP2R, lastR2P) + if got := LastMercuryRetrograde(queryTT); math.Abs(got-wantLast) > 1e-7 { + t.Fatalf("last aggregate mismatch at %.9f: got %.12f want %.12f", queryUT, got, wantLast) + } + } +} diff --git a/basic/moon_observation.go b/basic/moon_observation.go index b81a60b..0c71a98 100644 --- a/basic/moon_observation.go +++ b/basic/moon_observation.go @@ -89,16 +89,50 @@ func HMoonHeight(jd, lon, lat, tz float64) float64 { return HMoonHeightN(jd, lon, lat, tz, -1) } +type moonObservationState struct { + altitude float64 + distanceKM float64 +} + +func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObservationState { + calculationJD := TD2UT(jd-tz/24, true) + ra, dec := HMoonTrueRaDecN(calculationJD, n) + distanceKM := HMoonAwayN(calculationJD, n) + distanceAU := distanceKM / angularDiameterAstronomicalUnitKM + topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, distanceAU, height) + siderealTime := Limit360(ApparentSiderealTime(jd-tz/24)*15 + lon) + hourAngle := Limit360(siderealTime - topocentricRA) + altitudeSine := Sin(lat)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(lat)*Cos(hourAngle) + return moonObservationState{ + altitude: ArcSin(altitudeSine), + distanceKM: distanceKM, + } +} + func HMoonHeightN(jd, lon, lat, tz float64, n int) float64 { - calcjd := TD2UT(jd-tz/24, true) - ra, dec := HMoonTrueRaDecN(calcjd, n) - away := HMoonAwayN(calcjd, n) / 149597870.7 - nra, ndec := TopocentricRaDec(ra, dec, lat, lon, calcjd, away, 0) - calcjd = jd - tz/24 - st := Limit360(ApparentSiderealTime(calcjd)*15 + lon) - hourAngle := Limit360(st - nra) - tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle) - return ArcSin(tmp2) + return hMoonObservationStateN(jd, lon, lat, tz, 0, n).altitude +} + +func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 { + state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n) + // 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon. + residual := state.altitude + HeightDegreeByLat(height, latitude) + if zenithShift != 0 { + residual += RefractionFromTrueAltitude(state.altitude, refractionStandardPressureHPa, refractionStandardTemperatureC) + residual += angularSemidiameterArcsec(moonEquatorialRadiusKM, state.distanceKM) / 3600 + } + return residual +} + +func moonRiseSetOnCivilDay(candidate, slope, civilDayStart, longitude, latitude, originalTimeZone, + localTimeZone, zenithShift, height float64, isRise bool, fallbackErr error) (float64, error) { + if eventRiseSetCandidateValid(candidate, civilDayStart, slope, isRise) { + return candidate, nil + } + return eventDirectionalRiseSetSearch(civilDayStart, isRise, fallbackErr, func(outputJD float64) float64 { + localJD := outputJD + localTimeZone/24 - originalTimeZone/24 + return moonRiseSetResidual(localJD, longitude, latitude, localTimeZone, zenithShift, height, -1) + }) } // 废弃 @@ -109,14 +143,14 @@ func GetMoonTZTime(jd, lon, lat, tz float64) float64 { //实际中天时间{ jd += 0.5 } estimateJD := jd - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := MoonTimeAngle(prevJD, lon, lat, tz) - 359.599 stDegreep := (MoonTimeAngle(prevJD+0.000005, lon, lat, tz) - MoonTimeAngle(prevJD-0.000005, lon, lat, tz)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } @@ -133,14 +167,14 @@ func MoonCulminationTime(jde, lon, lat, timezone float64) float64 { } return ha } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := limitHA(prevJD, lon, timezone) - 360 stDegreep := (limitHA(prevJD+0.000005, lon, timezone) - limitHA(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } @@ -155,21 +189,24 @@ func MoonTimeAngle(jd, lon, lat, tz float64) float64 { } func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) { + if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) { + return 0, ErrInvalidObservationInput + } originalTimeZone := timeZone timeZone = longitude / 15 var timeToMeridian float64 - julianDayZero := math.Floor(julianDay) + 0.5 + civilDayStart := math.Floor(julianDay) + 0.5 //julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE //fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错 julianDay = math.Floor(julianDay) + 0.5 estimatedTime := julianDay - moonHeight := MoonHeight(julianDay, longitude, latitude, originalTimeZone) // 求此时月亮高度 + moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1) moonAngle := StandardAltitudeMoon(zenithShift, height, latitude) moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone) - if moonHeight-moonAngle > 0 { // 月亮在地平线上或在落下与下中天之间 + if moonResidual > 0 { // 月亮在地平线上或在落下与下中天之间 if moonAngleTime > 180 { timeToMeridian = (180 + 360 - moonAngleTime) / 15 } else { @@ -178,10 +215,10 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0) } - if moonHeight-moonAngle < 0 && moonAngleTime > 180 { + if moonResidual < 0 && moonAngleTime > 180 { timeToMeridian = (180 - moonAngleTime) / 15 estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0) - } else if moonHeight-moonAngle < 0 && moonAngleTime < 180 { + } else if moonResidual < 0 && moonAngleTime < 180 { timeToMeridian = (180 - moonAngleTime) / 15 estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0) } @@ -191,10 +228,11 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig estimatedTime += (180 - currentAngle) * 4.0 / 60.0 / 24.0 } - currentHeight := HMoonHeight(estimatedTime, longitude, latitude, timeZone) - if !(currentHeight < -10 && math.Abs(latitude) < 60) { - if currentHeight > moonAngle { - return 0, ErrNeverSet + currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) + if !(currentResidual < -10 && math.Abs(latitude) < 60) { + if currentResidual > 0 { + return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude, + originalTimeZone, timeZone, zenithShift, height, true, ErrNeverSet) } checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0 checkAngle := MoonTimeAngle(checkTime, longitude, latitude, timeZone) @@ -202,8 +240,9 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig checkAngle += 360 } checkTime += (360 - checkAngle) * 4.0 / 60.0 / 24.0 - if HMoonHeight(checkTime, longitude, latitude, timeZone) < moonAngle { - return 0, ErrNeverRise + if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 { + return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude, + originalTimeZone, timeZone, zenithShift, height, true, ErrNeverRise) } } @@ -215,7 +254,7 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig estimatedTime += hourAngle/24.00 + hourAngle/33.00/15.00 } else { i := 0 - for MoonHeight(estimatedTime, longitude, latitude, timeZone) < moonAngle { + for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 { i++ estimatedTime += 15.0 / 60.0 / 24.0 if i > 48 { @@ -225,41 +264,40 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig } // 使用牛顿迭代法求精确解 - estimatedTime = moonRiseSetNewtonRaphsonIteration(estimatedTime, longitude, latitude, timeZone, moonAngle, HMoonHeight, 0.00002) - + estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002) estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24 - - if estimatedTime > julianDayZero+1 || estimatedTime < julianDayZero { - return 0, ErrNotOnThisDate - } - return estimatedTime, nil + return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude, + originalTimeZone, timeZone, zenithShift, height, true, nil) } func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) { + if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) { + return 0, ErrInvalidObservationInput + } originalTimeZone := timeZone timeZone = longitude / 15 var timeToMeridian float64 - julianDayZero := math.Floor(julianDay) + 0.5 + civilDayStart := math.Floor(julianDay) + 0.5 //julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE //fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错 julianDay = math.Floor(julianDay) + 0.5 estimatedTime := julianDay - moonHeight := MoonHeight(julianDay, longitude, latitude, originalTimeZone) // 求此时月亮高度 + moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1) moonAngle := StandardAltitudeMoon(zenithShift, height, latitude) moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone) - if moonHeight-moonAngle < 0 { + if moonResidual < 0 { timeToMeridian = (360 - moonAngleTime) / 15 estimatedTime += (timeToMeridian/24 + (timeToMeridian/24.0*12.0)/15.0/24.0) } // 月亮在地平线上或在落下与下中天之间 - if moonHeight-moonAngle > 0 && moonAngleTime < 180 { + if moonResidual > 0 && moonAngleTime < 180 { timeToMeridian = (-moonAngleTime) / 15 estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0) - } else if moonHeight-moonAngle > 0 { + } else if moonResidual > 0 { timeToMeridian = (360 - moonAngleTime) / 15 estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0) } @@ -273,16 +311,18 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh } // estimatedTime = 月球中天时间 - currentHeight := HMoonHeight(estimatedTime, longitude, latitude, timeZone) - if !(currentHeight > 10 && math.Abs(latitude) < 60) { - if currentHeight < moonAngle { - return 0, ErrNeverRise + currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) + if !(currentResidual > 10 && math.Abs(latitude) < 60) { + if currentResidual < 0 { + return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude, + originalTimeZone, timeZone, zenithShift, height, false, ErrNeverRise) } checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0 angleSubtraction := 180 - MoonTimeAngle(checkTime, longitude, latitude, timeZone) checkTime += angleSubtraction * 4.0 / 60.0 / 24.0 - if HMoonHeight(checkTime, longitude, latitude, timeZone) > moonAngle { - return 0, ErrNeverSet + if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 { + return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude, + originalTimeZone, timeZone, zenithShift, height, false, ErrNeverSet) } } @@ -294,7 +334,7 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh estimatedTime += hourAngle/24 + hourAngle/33.0/15.0 } else { i := 0 - for MoonHeight(estimatedTime, longitude, latitude, timeZone) > moonAngle { + for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 { i++ estimatedTime += 15.0 / 60.0 / 24.0 if i > 48 { @@ -304,14 +344,10 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh } // 使用牛顿迭代法求精确解 - estimatedTime = moonRiseSetNewtonRaphsonIteration(estimatedTime, longitude, latitude, timeZone, moonAngle, HMoonHeight, 0.00002) - + estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002) estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24 - - if estimatedTime > julianDayZero+1 || estimatedTime < julianDayZero { - return 0, ErrNotOnThisDate - } - return estimatedTime, nil + return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude, + originalTimeZone, timeZone, zenithShift, height, false, nil) } // heightFunction 高度函数类型定义,用于牛顿迭代法 @@ -324,24 +360,32 @@ func moonRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, timeZon currentTime := initialTime - for { - previousTime := currentTime - - // 计算函数值:f(t) = height(t) - targetAngle + var ok bool + currentTime, ok = eventNewtonRefine(currentTime, tolerance, func(previousTime float64) float64 { functionValue := heightFunc(previousTime, longitude, latitude, timeZone) - targetAngle - - // 计算导数:f'(t) ≈ (f(t+h) - f(t-h)) / (2h) derivative := (heightFunc(previousTime+derivativeStep, longitude, latitude, timeZone) - heightFunc(previousTime-derivativeStep, longitude, latitude, timeZone)) / (2 * derivativeStep) - - // 牛顿-拉夫逊公式:t_new = t_old - f(t) / f'(t) - currentTime = previousTime - functionValue/derivative - - // 检查收敛 - if math.Abs(currentTime-previousTime) <= tolerance { - break - } + return functionValue / derivative + }) + if !ok { + return math.NaN() } return currentTime } + +func moonRiseSetResidualIteration(initialTime, longitude, latitude, timeZone, zenithShift, height, tolerance float64) (float64, float64) { + const derivativeStep = 0.000005 + + slope := math.NaN() + currentTime, ok := eventNewtonRefine(initialTime, tolerance, func(previousTime float64) float64 { + functionValue := moonRiseSetResidual(previousTime, longitude, latitude, timeZone, zenithShift, height, -1) + slope = (moonRiseSetResidual(previousTime+derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1) - + moonRiseSetResidual(previousTime-derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1)) / (2 * derivativeStep) + return functionValue / slope + }) + if !ok { + return math.NaN(), math.NaN() + } + return currentTime, slope +} diff --git a/basic/moon_phase.go b/basic/moon_phase.go index 65c90ad..abb1548 100644 --- a/basic/moon_phase.go +++ b/basic/moon_phase.go @@ -38,14 +38,14 @@ func SunMoonSeek(jde float64, degree float64) float64 { func CalcMoonSHByJDE(jde float64, phaseType int) float64 { phaseType = phaseType * 180 estimateJD := jde - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := SunMoonSeek(prevJD, float64(phaseType)) stDegreep := (SunMoonSeek(prevJD+0.000005, float64(phaseType)) - SunMoonSeek(prevJD-0.000005, float64(phaseType))) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } @@ -54,14 +54,14 @@ func CalcMoonSH(year float64, phaseType int) float64 { jde := CalcMoonS(year, phaseType) phaseType = phaseType * 180 estimateJD := jde - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := SunMoonSeek(prevJD, float64(phaseType)) stDegreep := (SunMoonSeek(prevJD+0.000005, float64(phaseType)) - SunMoonSeek(prevJD-0.000005, float64(phaseType))) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } @@ -125,14 +125,14 @@ func CalcMoonXHByJDE(jde float64, quarterType int) float64 { quarterType = -90 } estimateJD := jde - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := SunMoonSeek(prevJD, float64(quarterType)) stDegreep := (SunMoonSeek(prevJD+0.000005, float64(quarterType)) - SunMoonSeek(prevJD-0.000005, float64(quarterType))) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } @@ -145,14 +145,14 @@ func CalcMoonXH(year float64, quarterType int) float64 { quarterType = -90 } estimateJD := jde - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := SunMoonSeek(prevJD, float64(quarterType)) stDegreep := (SunMoonSeek(prevJD+0.000005, float64(quarterType)) - SunMoonSeek(prevJD-0.000005, float64(quarterType))) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/moon_precision.go b/basic/moon_precision.go index f98d971..22ad076 100644 --- a/basic/moon_precision.go +++ b/basic/moon_precision.go @@ -6,35 +6,47 @@ import ( . "b612.me/astro/tools" ) +var moonCir = [][][]float64{{ + //ML0 + { + 22639.586, 0.78475822, 8328.691424623, 1.5229241, 25.0719, -0.123598, 4586.438, 0.1873974, 7214.06286536, -2.184756, -18.860, 0.08280, 2369.914, 2.5429520, 15542.75428998, -0.661832, 6.212, -0.04080, 769.026, 3.140313, 16657.38284925, 3.04585, 50.144, -0.2472, 666.418, 1.527671, 628.30195521, -0.02664, 0.062, -0.0054, 411.596, 4.826607, 16866.9323150, -1.28012, -1.07, -0.0059, 211.656, 4.115028, -1114.6285593, -3.70768, -43.93, 0.2064, 205.436, 0.230523, 6585.7609101, -2.15812, -18.92, 0.0882, 191.956, 4.898507, 23871.4457146, 0.86109, 31.28, -0.164, 164.729, 2.586078, 14914.4523348, -0.6352, 6.15, -0.035, 147.321, 5.45530, -7700.3894694, -1.5496, -25.01, 0.118, 124.988, 0.48608, 7771.3771450, -0.3309, 3.11, -0.020, 109.380, 3.88323, 8956.9933798, 1.4963, 25.13, -0.129, 55.177, 5.57033, -1324.1780250, 0.6183, 7.3, -0.035, 45.100, 0.89898, 25195.623740, 0.2428, 24.0, -0.129, 39.533, 3.81213, -8538.240890, 2.8030, 26.1, -0.118, 38.430, 4.30115, 22756.817155, -2.8466, -12.6, 0.042, 36.124, 5.49587, 24986.074274, 4.5688, 75.2, -0.371, 30.773, 1.94559, 14428.125731, -4.3695, -37.7, 0.166, 28.397, 3.28586, 7842.364821, -2.2114, -18.8, 0.077, 24.358, 5.64142, 16171.056245, -0.6885, 6.3, -0.046, 18.585, 4.41371, -557.314280, -1.8538, -22.0, 0.10, 17.954, 3.58454, 8399.679100, -0.3576, 3.2, -0.03, 14.530, 4.9416, 23243.143759, 0.888, 31.2, -0.16, 14.380, 0.9709, 32200.137139, 2.384, 56.4, -0.29, 14.251, 5.7641, -2.301200, 1.523, 25.1, -0.12, 13.899, 0.3735, 31085.508580, -1.324, 12.4, -0.08, 13.194, 1.7595, -9443.319984, -5.231, -69.0, 0.33, 9.679, 3.0997, -16029.080894, -3.072, -50.1, 0.24, 9.366, 0.3016, 24080.995180, -3.465, -19.9, 0.08, 8.606, 4.1582, -1742.930514, -3.681, -44.0, 0.21, 8.453, 2.8416, 16100.068570, 1.192, 28.2, -0.14, 8.050, 2.6292, 14286.150380, -0.609, 6.1, -0.03, 7.630, 6.2388, 17285.684804, 3.019, 50.2, -0.25, 7.447, 1.4845, 1256.603910, -0.053, 0.1, -0.01, 7.371, 0.2736, 5957.458955, -2.131, -19.0, 0.09, 7.063, 5.6715, 33.757047, -0.308, -3.6, 0.02, 6.383, 4.7843, 7004.513400, 2.141, 32.4, -0.16, 5.742, 2.6572, 32409.686605, -1.942, 5, -0.05, 4.374, 4.3443, 22128.51520, -2.820, -13, 0.05, 3.998, 3.2545, 33524.31516, 1.766, 49, -0.25, 3.210, 2.2443, 14985.44001, -2.516, -16, 0.06, 2.915, 1.7138, 24499.74767, 0.834, 31, -0.17, 2.732, 1.9887, 13799.82378, -4.343, -38, 0.17, 2.568, 5.4122, -7072.08751, -1.576, -25, 0.11, 2.521, 3.2427, 8470.66678, -2.238, -19, 0.07, 2.489, 4.0719, -486.32660, -3.734, -44, 0.20, 2.146, 5.6135, -1952.47998, 0.645, 7, -0.03, 1.978, 2.7291, 39414.20000, 0.199, 37, -0.21, 1.934, 1.5682, 33314.76570, 6.092, 100, -0.5, 1.871, 0.4166, 30457.20662, -1.297, 12, -0.1, 1.753, 2.0582, -8886.00570, -3.38, -47, 0.2, 1.437, 2.386, -695.87607, 0.59, 7, 0, 1.373, 3.026, -209.54947, 4.33, 51, -0.2, 1.262, 5.940, 16728.37052, 1.17, 28, -0.1, 1.224, 6.172, 6656.74859, -4.04, -41, 0.2, 1.187, 5.873, 6099.43431, -5.89, -63, 0.3, 1.177, 1.014, 31571.83518, 2.41, 56, -0.3, 1.162, 3.840, 9585.29534, 1.47, 25, -0.1, 1.143, 5.639, 8364.73984, -2.18, -19, 0.1, 1.078, 1.229, 70.98768, -1.88, -22, 0.1, 1.059, 3.326, 40528.82856, 3.91, 81, -0.4, 0.990, 5.013, 40738.37803, -0.42, 30, -0.2, 0.948, 5.687, -17772.01141, -6.75, -94, 0.5, 0.876, 0.298, -0.35232, 0, 0, 0, 0.822, 2.994, 393.02097, 0, 0, 0, 0.788, 1.836, 8326.39022, 3.05, 50, -0.2, 0.752, 4.985, 22614.84180, 0.91, 31, -0.2, 0.740, 2.875, 8330.99262, 0, 0, 0, 0.669, 0.744, -24357.77232, -4.60, -75, 0.4, 0.644, 1.314, 8393.12577, -2.18, -19, 0.1, 0.639, 5.888, 575.33849, 0, 0, 0, 0.635, 1.116, 23385.11911, -2.87, -13, 0, 0.584, 5.197, 24428.75999, 2.71, 53, -0.3, 0.583, 3.513, -9095.55517, 0.95, 4, 0, 0.572, 6.059, 29970.88002, -5.03, -32, 0.1, 0.565, 2.960, 0.32863, 1.52, 25, -0.1, 0.561, 4.001, -17981.56087, -2.43, -43, 0.2, 0.557, 0.529, 7143.07519, -0.30, 3, 0, 0.546, 2.311, 25614.37623, 4.54, 75, -0.4, 0.536, 4.229, 15752.30376, -4.99, -45, 0.2, 0.493, 3.316, -8294.9344, -1.83, -29, 0.1, 0.491, 1.744, 8362.4485, 1.21, 21, -0.1, 0.478, 1.803, -10071.6219, -5.20, -69, 0.3, 0.454, 0.857, 15333.2048, 3.66, 57, -0.3, 0.445, 2.071, 8311.7707, -2.18, -19, 0.1, 0.426, 0.345, 23452.6932, -3.44, -20, 0.1, 0.420, 4.941, 33733.8646, -2.56, -2, 0, 0.413, 1.642, 17495.2343, -1.31, -1, 0, 0.404, 1.458, 23314.1314, -0.99, 9, -0.1, 0.395, 2.132, 38299.5714, -3.51, -6, 0, 0.382, 2.700, 31781.3846, -1.92, 5, 0, 0.375, 4.827, 6376.2114, 2.17, 32, -0.2, 0.361, 3.867, 16833.1753, -0.97, 3, 0, 0.358, 5.044, 15056.4277, -4.40, -38, 0.2, 0.350, 5.157, -8257.7037, -3.40, -47, 0.2, 0.344, 4.233, 157.7344, 0, 0, 0, 0.340, 2.672, 13657.8484, -0.58, 6, 0, 0.329, 5.610, 41853.0066, 3.29, 74, -0.4, 0.325, 5.895, -39.8149, 0, 0, 0, 0.309, 4.387, 21500.2132, -2.79, -13, 0.1, 0.302, 1.278, 786.0419, 0, 0, 0, 0.302, 5.341, -24567.3218, -0.27, -24, 0.1, 0.301, 1.045, 5889.8848, -1.57, -12, 0, 0.294, 4.201, -2371.2325, -3.65, -44, 0.2, 0.293, 3.704, 21642.1886, -6.55, -57, 0.2, 0.290, 4.069, 32828.4391, 2.36, 56, -0.3, 0.289, 3.472, 31713.8105, -1.35, 12, -0.1, 0.285, 5.407, -33.7814, 0.31, 4, 0, 0.283, 5.998, -16.9207, -3.71, -44, 0.2, 0.283, 2.772, 38785.8980, 0.23, 37, -0.2, 0.274, 5.343, 15613.7420, -2.54, -16, 0.1, 0.263, 3.997, 25823.9257, 0.22, 24, -0.1, 0.254, 0.600, 24638.3095, -1.61, 2, 0, 0.253, 1.344, 6447.1991, 0.29, 10, -0.1, 0.250, 0.887, 141.9754, -3.76, -44, 0.2, 0.247, 0.317, 5329.1570, -2.10, -19, 0.1, 0.245, 0.141, 36.0484, -3.71, -44, 0.2, 0.231, 2.287, 14357.1381, -2.49, -16, 0.1, 0.227, 5.158, 2.6298, 0, 0, 0, 0.219, 5.085, 47742.8914, 1.72, 63, -0.3, 0.211, 2.145, 6638.7244, -2.18, -19, 0.1, 0.201, 4.415, 39623.7495, -4.13, -14, 0, 0.194, 2.091, 588.4927, 0, 0, 0, 0.193, 3.057, -15400.7789, -3.10, -50, 0, 0.186, 5.598, 16799.3582, -0.72, 6, 0, 0.185, 3.886, 1150.6770, 0, 0, 0, 0.183, 1.619, 7178.0144, 1.52, 25, 0, 0.181, 2.635, 8328.3391, 1.52, 25, 0, 0.181, 2.077, 8329.0437, 1.52, 25, 0, 0.179, 3.215, -9652.8694, -0.90, -18, 0, 0.176, 1.716, -8815.0180, -5.26, -69, 0, 0.175, 5.673, 550.7553, 0, 0, 0, 0.170, 2.060, 31295.0580, -5.6, -39, 0, 0.167, 1.239, 7211.7617, -0.7, 6, 0, 0.165, 4.499, 14967.4158, -0.7, 6, 0, 0.164, 3.595, 15540.4531, 0.9, 31, 0, 0.164, 4.237, 522.3694, 0, 0, 0, 0.163, 4.633, 15545.0555, -2.2, -19, 0, 0.161, 0.478, 6428.0209, -2.2, -19, 0, 0.158, 2.03, 13171.5218, -4.3, -38, 0, 0.157, 2.28, 7216.3641, -3.7, -44, 0, 0.154, 5.65, 7935.6705, 1.5, 25, 0, 0.152, 0.46, 29828.9047, -1.3, 12, 0, 0.151, 1.19, -0.7113, 0, 0, 0, 0.150, 1.42, 23942.4334, -1.0, 9, 0, 0.144, 2.75, 7753.3529, 1.5, 25, 0, 0.137, 2.08, 7213.7105, -2.2, -19, 0, 0.137, 1.44, 7214.4152, -2.2, -19, 0, 0.136, 4.46, -1185.6162, -1.8, -22, 0, 0.136, 3.03, 8000.1048, -2.2, -19, 0, 0.134, 2.83, 14756.7124, -0.7, 6, 0, 0.131, 5.05, 6821.0419, -2.2, -19, 0, 0.128, 5.99, -17214.6971, -4.9, -72, 0, 0.127, 5.35, 8721.7124, 1.5, 25, 0, 0.126, 4.49, 46628.2629, -2.0, 19, 0, 0.125, 5.94, 7149.6285, 1.5, 25, 0, 0.124, 1.09, 49067.0695, 1.1, 55, 0, 0.121, 2.88, 15471.7666, 1.2, 28, 0, 0.111, 3.92, 41643.4571, 7.6, 125, -1, 0.110, 1.96, 8904.0299, 1.5, 25, 0, 0.106, 3.30, -18.0489, -2.2, -19, 0, 0.105, 2.30, -4.9310, 1.5, 25, 0, 0.104, 2.22, -6.5590, -1.9, -22, 0, 0.101, 1.44, 1884.9059, -0.1, 0, 0, 0.100, 5.92, 5471.1324, -5.9, -63, 0, 0.099, 1.12, 15149.7333, -0.7, 6, 0, 0.096, 4.73, 15508.9972, -0.4, 10, 0, 0.095, 5.18, 7230.9835, 1.5, 25, 0, 0.093, 3.37, 39900.5266, 3.9, 81, 0, 0.092, 2.01, 25057.0619, 2.7, 53, 0, 0.092, 1.21, -79.6298, 0, 0, 0, 0.092, 1.65, -26310.2523, -4.0, -68, 0, 0.091, 1.01, 42062.5561, -1.0, 23, 0, 0.090, 6.10, 29342.5781, -5.0, -32, 0, 0.090, 4.43, 15542.4020, -0.7, 6, 0, 0.090, 3.80, 15543.1066, -0.7, 6, 0, 0.089, 4.15, 6063.3859, -2.2, -19, 0, 0.086, 4.03, 52.9691, 0, 0, 0, 0.085, 0.49, 47952.4409, -2.6, 11, 0, 0.085, 1.60, 7632.8154, 2.1, 32, 0, 0.084, 0.22, 14392.0773, -0.7, 6, 0, 0.083, 6.22, 6028.4466, -4.0, -41, 0, 0.083, 0.63, -7909.9389, 2.8, 26, 0, 0.083, 5.20, -77.5523, 0, 0, 0, 0.082, 2.74, 8786.1467, -2.2, -19, 0, 0.080, 2.43, 9166.5428, -2.8, -26, 0, 0.080, 3.70, -25405.1732, 4.1, 27, 0, 0.078, 5.68, 48857.5200, 5.4, 106, -1, 0.077, 1.85, 8315.5735, -2.2, -19, 0, 0.075, 5.46, -18191.1103, 1.9, 8, 0, 0.075, 1.41, -16238.6304, 1.3, 1, 0, 0.074, 5.06, 40110.0761, -0.4, 30, 0, 0.072, 2.10, 64.4343, -3.7, -44, 0, 0.071, 2.17, 37671.2695, -3.5, -6, 0, 0.069, 1.71, 16693.4313, -0.7, 6, 0, 0.069, 3.33, -26100.7028, -8.3, -119, 1, 0.068, 1.09, 8329.4028, 1.5, 25, 0, 0.068, 3.62, 8327.9801, 1.5, 25, 0, 0.068, 2.41, 16833.1509, -1.0, 3, 0, 0.067, 3.40, 24709.2971, -3.5, -20, 0, 0.067, 1.65, 8346.7156, -0.3, 3, 0, 0.066, 2.61, 22547.2677, 1.5, 39, 0, 0.066, 3.50, 15576.5113, -1.0, 3, 0, 0.065, 5.76, 33037.9886, -2.0, 5, 0, 0.065, 4.58, 8322.1325, -0.3, 3, 0, 0.065, 6.20, 17913.9868, 3.0, 50, 0, 0.065, 1.50, 22685.8295, -1.0, 9, 0, 0.065, 2.37, 7180.3058, -1.9, -15, 0, 0.064, 1.06, 30943.5332, 2.4, 56, 0, 0.064, 1.89, 8288.8765, 1.5, 25, 0, 0.064, 4.70, 6.0335, 0.3, 4, 0, 0.063, 2.83, 8368.5063, 1.5, 25, 0, 0.063, 5.66, -2580.7819, 0.7, 7, 0, 0.062, 3.78, 7056.3285, -2.2, -19, 0, 0.061, 1.49, 8294.9100, 1.8, 29, 0, 0.061, 0.12, -10281.1714, -0.9, -18, 0, 0.061, 3.06, -8362.4729, -1.2, -21, 0, 0.061, 4.43, 8170.9571, 1.5, 25, 0, 0.059, 5.78, -13.1179, -3.7, -44, 0, 0.059, 5.97, 6625.5702, -2.2, -19, 0, 0.058, 5.01, -0.5080, -0.3, 0, 0, 0.058, 2.73, 7161.0938, -2.2, -19, 0, 0.057, 0.19, 7214.0629, -2.2, -19, 0, 0.057, 4.00, 22199.5029, -4.7, -35, 0, 0.057, 5.38, 8119.1420, 5.8, 76, 0, 0.056, 1.07, 7542.6495, 1.5, 25, 0, 0.056, 0.28, 8486.4258, 1.5, 25, 0, 0.054, 4.19, 16655.0816, 4.6, 75, 0, 0.053, 0.72, 7267.0320, -2.2, -19, 0, 0.053, 3.12, 12.6192, 0.6, 7, 0, 0.052, 2.99, -32896.013, -1.8, -49, 0, 0.052, 3.46, 1097.708, 0, 0, 0, 0.051, 5.37, -6443.786, -1.6, -25, 0, 0.051, 1.35, 7789.401, -2.2, -19, 0, 0.051, 5.83, 40042.502, 0.2, 38, 0, 0.051, 3.63, 9114.733, 1.5, 25, 0, 0.050, 1.51, 8504.484, -2.5, -22, 0, 0.050, 5.23, 16659.684, 1.5, 25, 0, 0.050, 1.15, 7247.820, -2.5, -23, 0, 0.047, 0.25, -1290.421, 0.3, 0, 0, 0.047, 4.67, -32686.464, -6.1, -100, 0, 0.047, 3.49, 548.678, 0, 0, 0, 0.047, 2.37, 6663.308, -2.2, -19, 0, 0.046, 0.98, 1572.084, 0, 0, 0, 0.046, 2.04, 14954.262, -0.7, 6, 0, 0.046, 3.72, 6691.693, -2.2, -19, 0, 0.045, 6.19, -235.287, 0, 0, 0, 0.044, 2.96, 32967.001, -0.1, 27, 0, 0.044, 3.82, -1671.943, -5.6, -66, 0, 0.043, 5.82, 1179.063, 0, 0, 0, 0.043, 0.07, 34152.617, 1.7, 49, 0, 0.043, 3.71, 6514.773, -0.3, 0, 0, 0.043, 5.62, 15.732, -2.5, -23, 0, 0.043, 5.80, 8351.233, -2.2, -19, 0, 0.042, 0.27, 7740.199, 1.5, 25, 0, 0.042, 6.14, 15385.020, -0.7, 6, 0, 0.042, 6.13, 7285.051, -4.1, -41, 0, 0.041, 1.27, 32757.451, 4.2, 78, 0, 0.041, 4.46, 8275.722, 1.5, 25, 0, 0.040, 0.23, 8381.661, 1.5, 25, 0, 0.040, 5.87, -766.864, 2.5, 29, 0, 0.040, 1.66, 254.431, 0, 0, 0, 0.040, 0.40, 9027.981, -0.4, 0, 0, 0.040, 2.96, 7777.936, 1.5, 25, 0, 0.039, 4.67, 33943.068, 6.1, 100, 0, 0.039, 3.52, 8326.062, 1.5, 25, 0, 0.039, 3.75, 21013.887, -6.5, -57, 0, 0.039, 5.60, 606.978, 0, 0, 0, 0.039, 1.19, 8331.321, 1.5, 25, 0, 0.039, 2.84, 7211.433, -2.2, -19, 0, 0.038, 0.67, 7216.693, -2.2, -19, 0, 0.038, 6.22, 25161.867, 0.6, 28, 0, 0.038, 4.40, 7806.322, 1.5, 25, 0, 0.038, 4.16, 9179.168, -2.2, -19, 0, 0.037, 4.73, 14991.999, -0.7, 6, 0, 0.036, 0.35, 67.514, -0.6, -7, 0, 0.036, 3.70, 25266.611, -1.6, 0, 0, 0.036, 5.39, 16328.796, -0.7, 6, 0, 0.035, 1.44, 7174.248, -2.2, -19, 0, 0.035, 5.00, 15684.730, -4.4, -38, 0, 0.035, 0.39, -15.419, -2.2, -19, 0, 0.035, 6.07, 15020.385, -0.7, 6, 0, 0.034, 6.01, 7371.797, -2.2, -19, 0, 0.034, 0.96, -16623.626, -3.4, -54, 0, 0.033, 6.24, 9479.368, 1.5, 25, 0, 0.033, 3.21, 23661.896, 5.2, 82, 0, 0.033, 4.06, 8311.418, -2.2, -19, 0, 0.033, 2.40, 1965.105, 0, 0, 0, 0.033, 5.17, 15489.785, -0.7, 6, 0, 0.033, 5.03, 21986.540, 0.9, 31, 0, 0.033, 4.10, 16691.140, 2.7, 46, 0, 0.033, 5.13, 47114.589, 1.7, 63, 0, 0.033, 4.45, 8917.184, 1.5, 25, 0, 0.033, 4.23, 2.078, 0, 0, 0, 0.032, 2.33, 75.251, 1.5, 25, 0, 0.032, 2.10, 7253.878, -2.2, -19, 0, 0.032, 3.11, -0.224, 1.5, 25, 0, 0.032, 4.43, 16640.462, -0.7, 6, 0, 0.032, 5.68, 8328.363, 0, 0, 0, 0.031, 5.32, 8329.020, 3.0, 50, 0, 0.031, 3.70, 16118.093, -0.7, 6, 0, 0.030, 3.67, 16721.817, -0.7, 6, 0, 0.030, 5.27, -1881.492, -1.2, -15, 0, 0.030, 5.72, 8157.839, -2.2, -19, 0, 0.029, 5.73, -18400.313, -6.7, -94, 0, 0.029, 2.76, 16.000, -2.2, -19, 0, 0.029, 1.75, 8879.447, 1.5, 25, 0, 0.029, 0.32, 8851.061, 1.5, 25, 0, 0.029, 0.90, 14704.903, 3.7, 57, 0, 0.028, 2.90, 15595.723, -0.7, 6, 0, 0.028, 5.88, 16864.631, 0.2, 24, 0, 0.028, 0.63, 16869.234, -2.8, -26, 0, 0.028, 4.04, -18609.863, -2.4, -43, 0, 0.027, 5.83, 6727.736, -5.9, -63, 0, 0.027, 6.12, 418.752, 4.3, 51, 0, 0.027, 0.14, 41157.131, 3.9, 81, 0, 0.026, 3.80, 15.542, 0, 0, 0, 0.026, 1.68, 50181.698, 4.8, 99, -1, 0.026, 0.32, 315.469, 0, 0, 0, 0.025, 5.67, 19.188, 0.3, 0, 0, 0.025, 3.16, 62.133, -2.2, -19, 0, 0.025, 3.76, 15502.939, -0.7, 6, 0, 0.025, 4.53, 45999.961, -2.0, 19, 0, 0.024, 3.21, 837.851, -4.4, -51, 0, 0.024, 2.82, 38157.596, 0.3, 37, 0, 0.024, 5.21, 15540.124, -0.7, 6, 0, 0.024, 0.26, 14218.576, 0, 13, 0, 0.024, 3.01, 15545.384, -0.7, 6, 0, 0.024, 1.16, -17424.247, -0.6, -21, 0, 0.023, 2.34, -67.574, 0.6, 7, 0, 0.023, 2.44, 18.024, -1.9, -22, 0, 0.023, 3.70, 469.400, 0, 0, 0, 0.023, 0.72, 7136.511, -2.2, -19, 0, 0.023, 4.50, 15582.569, -0.7, 6, 0, 0.023, 2.80, -16586.395, -4.9, -72, 0, 0.023, 1.51, 80.182, 0, 0, 0, 0.023, 1.09, 5261.583, -1.5, -12, 0, 0.023, 0.56, 54956.954, -0.5, 44, 0, 0.023, 4.01, 8550.860, -2.2, -19, 0, 0.023, 4.46, 38995.448, -4.1, -14, 0, 0.023, 3.82, 2358.126, 0, 0, 0, 0.022, 3.77, 32271.125, 0.5, 34, 0, 0.022, 0.82, 15935.775, -0.7, 6, 0, 0.022, 1.07, 24013.421, -2.9, -13, 0, 0.022, 0.40, 8940.078, -2.2, -19, 0, 0.022, 2.06, 15700.489, -0.7, 6, 0, 0.022, 4.27, 15124.002, -5.0, -45, 0, 0.021, 1.16, 56071.583, 3.2, 88, 0, 0.021, 5.58, 9572.189, -2.2, -19, 0, 0.020, 1.70, -17.273, -3.7, -44, 0, 0.020, 3.05, 214.617, 0, 0, 0, 0.020, 4.41, 8391.048, -2.2, -19, 0, 0.020, 5.95, 23869.145, 2.4, 56, 0, 0.020, 0.42, 40947.927, -4.7, -21, 0, 0.019, 1.39, 5818.897, 0.3, 10, 0, 0.019, 0.71, 23873.747, -0.7, 6, 0, 0.019, 2.81, 7291.615, -2.2, -19, 0, 0.019, 5.09, 8428.018, -2.2, -19, 0, 0.019, 4.14, 6518.187, -1.6, -12, 0, 0.019, 3.85, 21.330, 0, 0, 0, 0.018, 0.66, 14445.046, -0.7, 6, 0, 0.018, 1.65, 0.966, -4.0, -48, 0, 0.018, 5.64, -17143.709, -6.8, -94, 0, 0.018, 6.01, 7736.432, -2.2, -19, 0, 0.018, 2.74, 31153.083, -1.9, 5, 0, 0.018, 4.58, 6116.355, -2.2, -19, 0, 0.018, 2.28, 46.401, 0.3, 0, 0, 0.018, 3.80, 10213.597, 1.4, 25, 0, 0.018, 2.84, 56281.132, -1.1, 36, 0, 0.018, 3.53, 8249.062, 1.5, 25, 0, 0.017, 4.43, 20871.911, -3, -13, 0, 0.017, 4.44, 627.596, 0, 0, 0, 0.017, 1.85, 628.308, 0, 0, 0, 0.017, 1.19, 8408.321, 2, 25, 0, 0.017, 1.95, 7214.056, -2, -19, 0, 0.017, 1.57, 7214.070, -2, -19, 0, 0.017, 1.65, 13870.811, -6, -60, 0, 0.017, 0.30, 22.542, -4, -44, 0, 0.017, 2.62, -119.445, 0, 0, 0, 0.016, 4.87, 5747.909, 2, 32, 0, 0.016, 4.45, 14339.108, -1, 6, 0, 0.016, 1.83, 41366.680, 0, 30, 0, 0.016, 4.53, 16309.618, -3, -23, 0, 0.016, 2.54, 15542.754, -1, 6, 0, 0.016, 6.05, 1203.646, 0, 0, 0, 0.015, 5.2, 2751.147, 0, 0, 0, 0.015, 1.8, -10699.924, -5, -69, 0, 0.015, 0.4, 22824.391, -3, -20, 0, 0.015, 2.1, 30666.756, -6, -39, 0, 0.015, 2.1, 6010.417, -2, -19, 0, 0.015, 0.7, -23729.470, -5, -75, 0, 0.015, 1.4, 14363.691, -1, 6, 0, 0.015, 5.8, 16900.689, -2, 0, 0, 0.015, 5.2, 23800.458, 3, 53, 0, 0.015, 5.3, 6035.000, -2, -19, 0, 0.015, 1.2, 8251.139, 2, 25, 0, 0.015, 3.6, -8.860, 0, 0, 0, 0.015, 0.8, 882.739, 0, 0, 0, 0.015, 3.0, 1021.329, 0, 0, 0, 0.015, 0.6, 23296.107, 1, 31, 0, 0.014, 5.4, 7227.181, 2, 25, 0, 0.014, 0.1, 7213.352, -2, -19, 0, 0.014, 4.0, 15506.706, 3, 50, 0, 0.014, 3.4, 7214.774, -2, -19, 0, 0.014, 4.6, 6665.385, -2, -19, 0, 0.014, 0.1, -8.636, -2, -22, 0, 0.014, 3.1, 15465.202, -1, 6, 0, 0.014, 4.9, 508.863, 0, 0, 0, 0.014, 3.5, 8406.244, 2, 25, 0, 0.014, 1.3, 13313.497, -8, -82, 0, 0.014, 2.8, 49276.619, -3, 0, 0, 0.014, 0.1, 30528.194, -3, -10, 0, 0.013, 1.7, 25128.050, 1, 31, 0, 0.013, 2.9, 14128.405, -1, 6, 0, 0.013, 3.4, 57395.761, 3, 80, 0, 0.013, 2.7, 13029.546, -1, 6, 0, 0.013, 3.9, 7802.556, -2, -19, 0, 0.013, 1.6, 8258.802, -2, -19, 0, 0.013, 2.2, 8417.709, -2, -19, 0, 0.013, 0.7, 9965.210, -2, -19, 0, 0.013, 3.4, 50391.247, 0, 48, 0, 0.013, 3.0, 7134.433, -2, -19, 0, 0.013, 2.9, 30599.182, -5, -31, 0, 0.013, 3.6, -9723.857, 1, 0, 0, 0.013, 4.8, 7607.084, -2, -19, 0, 0.012, 0.8, 23837.689, 1, 35, 0, 0.012, 3.6, 4.409, -4, -44, 0, 0.012, 5.0, 16657.031, 3, 50, 0, 0.012, 4.4, 16657.735, 3, 50, 0, 0.012, 1.1, 15578.803, -4, -38, 0, 0.012, 6.0, -11.490, 0, 0, 0, 0.012, 1.9, 8164.398, 0, 0, 0, 0.012, 2.4, 31852.372, -4, -17, 0, 0.012, 2.4, 6607.085, -2, -19, 0, 0.012, 4.2, 8359.870, 0, 0, 0, 0.012, 0.5, 5799.713, -2, -19, 0, 0.012, 2.7, 7220.622, 0, 0, 0, 0.012, 4.3, -139.720, 0, 0, 0, 0.012, 2.3, 13728.836, -2, -16, 0, 0.011, 3.6, 14912.146, 1, 31, 0, 0.011, 4.7, 14916.748, -2, -19, 0}, + //ML1 + { + 1.67680, 4.66926, 628.301955, -0.0266, 0.1, -0.005, 0.51642, 3.3721, 6585.760910, -2.158, -18.9, 0.09, 0.41383, 5.7277, 14914.452335, -0.635, 6.2, -0.04, 0.37115, 3.9695, 7700.389469, 1.550, 25.0, -0.12, 0.27560, 0.7416, 8956.993380, 1.496, 25.1, -0.13, 0.24599, 4.2253, -2.301200, 1.523, 25.1, -0.12, 0.07118, 0.1443, 7842.36482, -2.211, -19, 0.08, 0.06128, 2.4998, 16171.05625, -0.688, 6, 0, 0.04516, 0.443, 8399.67910, -0.36, 3, 0, 0.04048, 5.771, 14286.15038, -0.61, 6, 0, 0.03747, 4.626, 1256.60391, -0.05, 0, 0, 0.03707, 3.415, 5957.45895, -2.13, -19, 0.1, 0.03649, 1.800, 23243.14376, 0.89, 31, -0.2, 0.02438, 0.042, 16029.08089, 3.07, 50, -0.2, 0.02165, 1.017, -1742.93051, -3.68, -44, 0.2, 0.01923, 3.097, 17285.68480, 3.02, 50, -0.3, 0.01692, 1.280, 0.3286, 1.52, 25, -0.1, 0.01361, 0.298, 8326.3902, 3.05, 50, -0.2, 0.01293, 4.013, 7072.0875, 1.58, 25, -0.1, 0.01276, 4.413, 8330.9926, 0, 0, 0, 0.01270, 0.101, 8470.6668, -2.24, -19, 0.1, 0.01097, 1.203, 22128.5152, -2.82, -13, 0, 0.01088, 2.545, 15542.7543, -0.66, 6, 0, 0.00835, 0.190, 7214.0629, -2.18, -19, 0.1, 0.00734, 4.855, 24499.7477, 0.83, 31, -0.2, 0.00686, 5.130, 13799.8238, -4.34, -38, 0.2, 0.00631, 0.930, -486.3266, -3.73, -44, 0, 0.00585, 0.699, 9585.2953, 1.5, 25, 0, 0.00566, 4.073, 8328.3391, 1.5, 25, 0, 0.00566, 0.638, 8329.0437, 1.5, 25, 0, 0.00539, 2.472, -1952.4800, 0.6, 7, 0, 0.00509, 2.88, -0.7113, 0, 0, 0, 0.00469, 3.56, 30457.2066, -1.3, 12, 0, 0.00387, 0.78, -0.3523, 0, 0, 0, 0.00378, 1.84, 22614.8418, 0.9, 31, 0, 0.00362, 5.53, -695.8761, 0.6, 7, 0, 0.00317, 2.80, 16728.3705, 1.2, 28, 0, 0.00303, 6.07, 157.7344, 0, 0, 0, 0.00300, 2.53, 33.7570, -0.3, -4, 0, 0.00295, 4.16, 31571.8352, 2.4, 56, 0, 0.00289, 5.98, 7211.7617, -0.7, 6, 0, 0.00285, 2.06, 15540.4531, 0.9, 31, 0, 0.00283, 2.65, 2.6298, 0, 0, 0, 0.00282, 6.17, 15545.0555, -2.2, -19, 0, 0.00278, 1.23, -39.8149, 0, 0, 0, 0.00272, 3.82, 7216.3641, -3.7, -44, 0, 0.00270, 4.37, 70.9877, -1.9, -22, 0, 0.00256, 5.81, 13657.8484, -0.6, 6, 0, 0.00244, 5.64, -0.2237, 1.5, 25, 0, 0.00240, 2.96, 8311.7707, -2.2, -19, 0, 0.00239, 0.87, -33.7814, 0.3, 4, 0, 0.00216, 2.31, 15.9995, -2.2, -19, 0, 0.00186, 3.46, 5329.1570, -2.1, -19, 0, 0.00169, 2.40, 24357.772, 4.6, 75, 0, 0.00161, 5.80, 8329.403, 1.5, 25, 0, 0.00161, 5.20, 8327.980, 1.5, 25, 0, 0.00160, 4.26, 23385.119, -2.9, -13, 0, 0.00156, 1.26, 550.755, 0, 0, 0, 0.00155, 1.25, 21500.213, -2.8, -13, 0, 0.00152, 0.60, -16.921, -3.7, -44, 0, 0.00150, 2.71, -79.630, 0, 0, 0, 0.00150, 5.29, 15.542, 0, 0, 0, 0.00148, 1.06, -2371.232, -3.7, -44, 0, 0.00141, 0.77, 8328.691, 1.5, 25, 0, 0.00141, 3.67, 7143.075, -0.3, 0, 0, 0.00138, 5.45, 25614.376, 4.5, 75, 0, 0.00129, 4.90, 23871.446, 0.9, 31, 0, 0.00126, 4.03, 141.975, -3.8, -44, 0, 0.00124, 6.01, 522.369, 0, 0, 0, 0.00120, 4.94, -10071.622, -5.2, -69, 0, 0.00118, 5.07, -15.419, -2.2, -19, 0, 0.00107, 3.49, 23452.693, -3.4, -20, 0, 0.00104, 4.78, 17495.234, -1.3, 0, 0, 0.00103, 1.44, -18.049, -2.2, -19, 0, 0.00102, 5.63, 15542.402, -0.7, 6, 0, 0.00102, 2.59, 15543.107, -0.7, 6, 0, 0.00100, 4.11, -6.559, -1.9, -22, 0, 0.00097, 0.08, 15400.779, 3.1, 50, 0, 0.00096, 5.84, 31781.385, -1.9, 5, 0, 0.00094, 1.08, 8328.363, 0, 0, 0, 0.00094, 2.46, 16799.358, -0.7, 6, 0, 0.00094, 1.69, 6376.211, 2.2, 32, 0, 0.00093, 3.64, 8329.020, 3.0, 50, 0, 0.00093, 2.65, 16655.082, 4.6, 75, 0, 0.00090, 1.90, 15056.428, -4.4, -38, 0, 0.00089, 1.59, 52.969, 0, 0, 0, 0.00088, 2.02, -8257.704, -3.4, -47, 0, 0.00088, 3.02, 7213.711, -2.2, -19, 0, 0.00087, 0.50, 7214.415, -2.2, -19, 0, 0.00087, 0.49, 16659.684, 1.5, 25, 0, 0.00082, 5.64, -4.931, 1.5, 25, 0, 0.00079, 5.17, 13171.522, -4.3, -38, 0, 0.00076, 3.60, 29828.905, -1.3, 12, 0, 0.00076, 4.08, 24567.322, 0.3, 24, 0, 0.00076, 4.58, 1884.906, -0.1, 0, 0, 0.00073, 0.33, 31713.811, -1.4, 12, 0, 0.00073, 0.93, 32828.439, 2.4, 56, 0, 0.00071, 5.91, 38785.898, 0.2, 37, 0, 0.00069, 2.20, 15613.742, -2.5, -16, 0, 0.00066, 3.87, 15.732, -2.5, -23, 0, 0.00066, 0.86, 25823.926, 0.2, 24, 0, 0.00065, 2.52, 8170.957, 1.5, 25, 0, 0.00063, 0.18, 8322.132, -0.3, 0, 0, 0.00060, 5.84, 8326.062, 1.5, 25, 0, 0.00060, 5.15, 8331.321, 1.5, 25, 0, 0.00060, 2.18, 8486.426, 1.5, 25, 0, 0.00058, 2.30, -1.731, -4, -44, 0, 0.00058, 5.43, 14357.138, -2, -16, 0, 0.00057, 3.09, 8294.910, 2, 29, 0, 0.00057, 4.67, -8362.473, -1, -21, 0, 0.00056, 4.15, 16833.151, -1, 0, 0, 0.00054, 1.93, 7056.329, -2, -19, 0, 0.00054, 5.27, 8315.574, -2, -19, 0, 0.00052, 5.6, 8311.418, -2, -19, 0, 0.00052, 2.7, -77.552, 0, 0, 0, 0.00051, 4.3, 7230.984, 2, 25, 0, 0.00050, 0.4, -0.508, 0, 0, 0, 0.00049, 5.4, 7211.433, -2, -19, 0, 0.00049, 4.4, 7216.693, -2, -19, 0, 0.00049, 4.3, 16864.631, 0, 24, 0, 0.00049, 2.2, 16869.234, -3, -26, 0, 0.00047, 6.1, 627.596, 0, 0, 0, 0.00047, 5.0, 12.619, 1, 7, 0, 0.00045, 4.9, -8815.018, -5, -69, 0, 0.00044, 1.6, 62.133, -2, -19, 0, 0.00042, 2.9, -13.118, -4, -44, 0, 0.00042, 4.1, -119.445, 0, 0, 0, 0.00041, 4.3, 22756.817, -3, -13, 0, 0.00041, 3.6, 8288.877, 2, 25, 0, 0.00040, 0.5, 6663.308, -2, -19, 0, 0.00040, 1.1, 8368.506, 2, 25, 0, 0.00039, 4.1, 6443.786, 2, 25, 0, 0.00039, 3.1, 16657.383, 3, 50, 0, 0.00038, 0.1, 16657.031, 3, 50, 0, 0.00038, 3.0, 16657.735, 3, 50, 0, 0.00038, 4.6, 23942.433, -1, 9, 0, 0.00037, 4.3, 15385.020, -1, 6, 0, 0.00037, 5.0, 548.678, 0, 0, 0, 0.00036, 1.8, 7213.352, -2, -19, 0, 0.00036, 1.7, 7214.774, -2, -19, 0, 0.00035, 1.1, 7777.936, 2, 25, 0, 0.00035, 1.6, -8.860, 0, 0, 0, 0.00035, 4.4, 23869.145, 2, 56, 0, 0.00035, 2.0, 6691.693, -2, -19, 0, 0.00034, 1.3, -1185.616, -2, -22, 0, 0.00034, 2.2, 23873.747, -1, 6, 0, 0.00033, 2.0, -235.287, 0, 0, 0, 0.00033, 3.1, 17913.987, 3, 50, 0, 0.00033, 1.0, 8351.233, -2, -19, 0}, + //ML2 + {0.004870, 4.6693, 628.30196, -0.027, 0, -0.01, 0.002280, 2.6746, -2.30120, 1.523, 25, -0.12, 0.001500, 3.372, 6585.76091, -2.16, -19, 0.1, 0.001200, 5.728, 14914.45233, -0.64, 6, 0, 0.001080, 3.969, 7700.38947, 1.55, 25, -0.1, 0.000800, 0.742, 8956.99338, 1.50, 25, -0.1, 0.000254, 6.002, 0.3286, 1.52, 25, -0.1, 0.000210, 0.144, 7842.3648, -2.21, -19, 0, 0.000180, 2.500, 16171.0562, -0.7, 6, 0, 0.000130, 0.44, 8399.6791, -0.4, 3, 0, 0.000126, 5.03, 8326.3902, 3.0, 50, 0, 0.000120, 5.77, 14286.1504, -0.6, 6, 0, 0.000118, 5.96, 8330.9926, 0, 0, 0, 0.000110, 1.80, 23243.1438, 0.9, 31, 0, 0.000110, 3.42, 5957.4590, -2.1, -19, 0, 0.000110, 4.63, 1256.6039, -0.1, 0, 0, 0.000099, 4.70, -0.7113, 0, 0, 0, 0.000070, 0.04, 16029.0809, 3.1, 50, 0, 0.000070, 5.14, 8328.3391, 1.5, 25, 0, 0.000070, 5.85, 8329.0437, 1.5, 25, 0, 0.000060, 1.02, -1742.9305, -3.7, -44, 0, 0.000060, 3.10, 17285.6848, 3.0, 50, 0, 0.000054, 5.69, -0.352, 0, 0, 0, 0.000043, 0.52, 15.542, 0, 0, 0, 0.000041, 2.03, 2.630, 0, 0, 0, 0.000040, 0.10, 8470.667, -2.2, -19, 0, 0.000040, 4.01, 7072.088, 1.6, 25, 0, 0.000036, 2.93, -8.860, -0.3, 0, 0, 0.000030, 1.20, 22128.515, -2.8, -13, 0, 0.000030, 2.54, 15542.754, -0.7, 6, 0, 0.000027, 4.43, 7211.762, -0.7, 6, 0, 0.000026, 0.51, 15540.453, 0.9, 31, 0, 0.000026, 1.44, 15545.055, -2.2, -19, 0, 0.000025, 5.37, 7216.364, -3.7, -44, 0}, + //ML3 + {0.00001200, 1.041, -2.3012, 1.52, 25, -0.1, 0.00000170, 0.31, -0.711, 0, 0, 0}}, + + { //精度1角秒 + //MB0 + {18461.240, 0.05710892, 8433.466157492, -0.6400617, -0.5345, -0.00294, 1010.167, 2.412663, 16762.15758211, 0.88286, 24.537, -0.1265, 999.694, 5.440038, -104.77473287, 2.16299, 25.606, -0.1207, 623.652, 0.915047, 7109.28813249, -0.02177, 6.746, -0.0379, 199.484, 1.815303, 15647.5290228, -2.82482, -19.39, 0.0799, 166.574, 4.842677, -1219.4032921, -1.5447, -18.33, 0.086, 117.261, 4.17086, 23976.2204475, -1.3019, 5.68, -0.044, 61.912, 4.76822, 25090.8490067, 2.4058, 49.61, -0.250, 33.357, 3.27060, 15437.979557, 1.5012, 31.8, -0.161, 31.760, 1.51241, 8223.916692, 3.6859, 50.7, -0.244, 29.577, 0.95817, 6480.986177, 0.0049, 6.7, -0.032, 15.566, 2.4871, -9548.094717, -3.068, -43.4, 0.21, 15.122, 0.2432, 32304.911872, 0.221, 30.7, -0.17, 12.094, 4.0135, 7737.590088, -0.048, 6.8, -0.04, 8.868, 1.8584, 15019.227068, -2.798, -19.5, 0.09, 8.045, 5.3812, 8399.709110, -0.332, 3.1, -0.02, 7.959, 4.2140, 23347.918492, -1.275, 5.6, -0.04, 7.435, 4.8858, -1847.705247, -1.518, -18.4, 0.09, 6.731, 3.8274, -16133.855627, -0.910, -24.5, 0.12, 6.580, 2.6732, 14323.350998, -2.207, -12.1, 0.04, 6.460, 3.1556, 9061.768113, -0.667, -0.5, -0.01, 6.296, 0.1713, 25300.398472, -1.920, -1.6, -0.01, 5.632, 0.8000, 733.076688, -2.190, -26, 0.12, 5.368, 2.1140, 16204.843302, -0.971, 3, -0.02, 5.311, 5.5111, 17390.459537, 0.856, 25, -0.13, 5.076, 2.2553, 523.52722, 2.136, 26, -0.13, 4.840, 6.1830, -7805.16420, 0.613, 1, 0, 4.806, 5.1414, -662.08901, 0.309, 4, -0.02, 3.984, 0.8406, 33419.54043, 3.929, 75, -0.37, 3.674, 5.0288, 22652.04242, -0.684, 13, -0.08, 2.998, 5.9291, 31190.28331, -3.487, -13, 0.04, 2.799, 2.1842, -16971.70705, 3.443, 27, -0.11, 2.414, 3.5735, 22861.59189, -5.010, -38, 0.16, 2.186, 3.9424, -9757.64418, 1.258, 8, -0.03, 2.146, 5.6262, 23766.67098, 3.024, 57, -0.29, 1.766, 3.3137, 14809.67760, 1.528, 32, -0.2, 1.624, 2.6013, 7318.83760, -4.35, -44, 0.2, 1.581, 3.8680, 16552.60812, 5.21, 76, -0.4, 1.520, 2.599, 40633.60330, 1.74, 56, -0.3, 1.516, 0.132, -17876.78614, -4.59, -68, 0.3, 1.510, 3.927, 8399.68473, -0.33, 3, 0, 1.318, 4.914, 16275.83098, -2.85, -19, 0.1, 1.264, 0.986, 24604.52240, -1.33, 6, 0, 1.192, 2.001, 39518.97474, -1.96, 12, -0.1, 1.135, 0.286, 31676.60992, 0.25, 31, -0.2, 1.086, 1.001, 5852.68422, 0.03, 7, 0, 1.019, 2.527, 33629.08990, -0.40, 23, -0.1, 0.823, 0.086, 16066.28151, 1.47, 32, -0.2, 0.804, 1.957, -33.78706, 0.28, 4, 0, 0.803, 5.212, 16833.14526, -1.00, 3, 0, 0.793, 1.472, -24462.54705, -2.43, -50, 0.2, 0.791, 1.658, -591.10134, -1.57, -18, 0.1, 0.667, 4.470, 24533.53473, 0.55, 28, -0.1, 0.650, 2.530, -10176.39667, -3.04, -43, 0.2, 0.639, 1.583, 25719.15096, 2.38, 50, -0.3, 0.634, 0.318, 5994.65957, -3.73, -37, 0.2, 0.631, 2.147, 8435.76736, -2.16, -26, 0.1, 0.630, 1.109, 8431.16496, 0.88, 25, -0.1, 0.596, 2.716, 13695.04904, -2.18, -12, 0.1, 0.589, 1.214, 7666.60241, 1.83, 29, -0.1, 0.473, 1.101, 30980.7338, 0.84, 38, -0.2, 0.456, 0.116, -71.0177, 1.85, 22, -0.1, 0.430, 2.786, -8990.7804, -1.21, -21, 0.1, 0.416, 1.454, 16728.4005, 1.19, 28, -0.1, 0.415, 5.072, 22023.7405, -0.66, 13, -0.1, 0.383, 4.257, 22719.6165, -1.25, 6, 0, 0.352, 2.972, 14880.6653, -0.35, 10, -0.1, 0.339, 5.972, 30561.9814, -3.46, -13, 0, 0.329, 1.587, -18086.3356, -0.26, -17, 0.1, 0.326, 1.016, 8467.2232, -0.95, -4, 0, 0.315, 1.902, 14390.9251, -2.77, -20, 0.1, 0.313, 4.611, 8852.2186, 3.66, 51, -0.2, 0.305, 0.616, 6551.9739, -1.88, -15, 0.1, 0.301, 4.728, -7595.6147, -3.71, -51, 0.2, 0.299, 1.874, 7143.0452, -0.33, 3, 0, 0.291, 3.156, -1428.9528, 2.78, 33, -0.2, 0.269, 4.929, -2476.0072, -1.49, -18, 0.1, 0.263, 3.196, 41748.2319, 5.45, 100, -0.5, 0.254, 3.387, -1009.8538, -5.87, -70, 0.3, 0.245, 1.930, 32514.4613, -4.10, -20, 0.1, 0.237, 3.342, 32933.2138, 0.19, 31, -0.2, 0.214, 3.617, 22233.2899, -4.98, -38, 0.2, 0.213, 4.357, 47847.6662, -0.44, 37, -0.2, 0.206, 3.872, 23418.9062, -3.16, -16, 0.1, 0.172, 5.772, 14951.6530, -2.2, -12, 0, 0.158, 2.04, 38890.6728, -1.9, 12, 0, 0.146, 1.70, 32095.3624, 4.5, 82, 0, 0.145, 4.29, 40843.1528, -2.6, 5, 0, 0.139, 2.90, 7876.1519, -2.5, -23, 0, 0.138, 4.95, 48962.2947, 3.3, 81, 0, 0.134, 3.97, 8365.8920, -0.1, 7, 0, 0.134, 4.06, -26205.4776, -6.1, -94, 0, 0.130, 1.40, -8643.0156, 5.0, 52, 0, 0.129, 5.67, 23138.3690, 3.1, 57, 0, 0.124, 2.64, 40005.3013, 1.8, 56, 0, 0.118, 4.88, 41957.7813, 1.1, 49, 0, 0.113, 3.78, -15505.5537, -0.9, -24, 0, 0.113, 4.87, 16904.1329, -2.9, -19, 0, 0.113, 1.84, 23280.3444, -0.7, 13, 0, 0.110, 0.43, -17319.4719, -2.7, -47, 0, 0.105, 1.61, 37.2006, -1.6, -18, 0, 0.102, 1.28, 25161.8367, 0.5, 28, 0, 0.095, 0.76, 1361.3786, -2.2, -25, 0, 0.094, 0.50, 29866.1053, -2.9, -6, 0, 0.092, 6.22, 24881.2995, 6.7, 101, 0, 0.088, 3.99, -10385.9461, 1.3, 8, 0, 0.085, 4.71, 70.9933, -1.9, -22, 0, 0.084, 0.86, 15613.7720, -2.5, -16, 0, 0.081, 4.43, 21537.4139, -4.4, -31, 0, 0.080, 1.86, -8365.9521, 0, -7, 0, 0.080, 0, 16728.3762, 1.2, 28, 0, 0.079, 2.44, -8919.7928, -3.1, -43, 0, 0.078, 3.69, -452.5395, -4.0, -48, 0, 0.075, 5.40, -32791.2385, -4.0, -75, 0, 0.073, 5.80, -1185.6462, -1.9, -22, 0, 0.070, 3.46, 16759.8564, 2.4, 50, 0, 0.069, 3.36, 14181.3756, 1.6, 32, 0, 0.068, 4.50, 16764.4588, -0.6, -1, 0, 0.067, 4.74, 8446.0854, 0, 7, 0, 0.066, 5.86, 24185.7699, -5.6, -46, 0, 0.064, 0.54, 32862.2262, 2.1, 53, 0, 0.063, 2.44, 24394.9729, 3.0, 57, 0, 0.063, 1.77, 5785.1101, 0.6, 14, 0, 0.062, 2.64, 6690.5356, -4.3, -45, 0, 0.062, 2.21, 1151.8292, 2.1, 26, 0, 0.062, 3.94, 34047.8424, 3.9, 75, 0, 0.060, 1.40, 38404.3462, -5.7, -32, 0, 0.058, 0.33, 31048.3080, 0.3, 31, 0, 0.057, 3.11, 9690.0701, -0.7, 0, 0, 0.057, 1.14, 30352.4319, 0.9, 38, 0, 0.056, 6.00, 8504.4538, -2.5, -22, 0, 0.055, 5.47, 18018.7615, 0.8, 25, 0, 0.055, 0.17, -18505.0881, -4.6, -69, 0, 0.055, 0.76, -9129.3422, 1.2, 8, 0, 0.054, 5.70, 7947.1396, -4.4, -44, 0, 0.053, 0.36, 5366.358, -3.7, -37, 0, 0.052, 4.01, -68.726, -1.5, -18, 0, 0.051, 2.74, 31818.585, -3.5, -13, 0, 0.051, 0.98, 16798.206, -2.8, -19, 0, 0.050, 5.94, 8293.747, -0.3, 0, 0, 0.049, 1.52, 15090.215, -4.7, -41, 0, 0.048, 3.46, 39309.425, 2.4, 63, 0, 0.046, 3.21, 23942.463, -1.0, 9, 0, 0.046, 3.35, 7143.070, -0.3, 0, 0, 0.042, 3.76, 46733.038, -4.1, -7, 0, 0.042, 5.18, 8288.351, 0, 7, 0, 0.040, 3.37, 16795.915, 0.6, 21, 0, 0.039, 4.54, -1776.718, -3.4, -40, 0, 0.039, 0.04, 8439.500, -0.3, 0, 0, 0.037, 3.91, 8479.867, -0.3, 0, 0, 0.037, 2.86, 38194.797, -1.3, 19, 0, 0.036, 1.04, 5224.382, 0.1, 7, 0, 0.036, 3.57, 15995.294, 3.4, 54, 0, 0.036, 5.33, 23209.357, 1.2, 35, 0, 0.035, 1.02, 8452.654, -0.3, 0, 0, 0.035, 4.31, 8294.904, 1.8, 29, 0, 0.035, 2.76, 13066.747, -2.2, -12, 0, 0.034, 6.07, 15508.967, -0.4, 10, 0, 0.032, 1.89, -17529.021, 1.6, 0, 0, 0.031, 5.70, 41261.905, 1.7, 56, 0, 0.031, 5.33, 30075.655, -7.2, -57, 0, 0.031, 5.97, -40.340, -1.5, -18, 0, 0.030, 2.87, 6533.950, 0, 7, 0, 0.030, 0.36, 49171.844, -1.1, 30, 0, 0.030, 4.40, 47219.364, -0.4, 37, 0, 0.030, 0.39, 23489.894, -5.0, -38, 0, 0.029, 5.12, 21395.439, -0.6, 13, 0, 0.029, 2.62, 8715.153, -0.3, 0, 0, 0.029, 2.94, 16826.592, -2.8, -19, 0, 0.028, 6.23, 31747.598, -1.6, 9, 0, 0.028, 0.43, 56176.358, 1.1, 62, 0, 0.027, 2.32, 8792.706, -0.3, 0, 0, 0.026, 3.02, 14252.363, -0.3, 10, 0, 0.025, 5.10, 40147.277, -2.0, 12, 0, 0.025, 4.59, 8433.795, 0.9, 25, 0, 0.025, 4.95, 8433.138, -2.2, -26, 0, 0.025, 1.03, -9338.545, -7.4, -95, 0, 0.024, 0.68, 17180.910, 5.2, 76, 0, 0.024, 3.81, 25057.092, 2.7, 53, 0, 0.024, 6.02, 29933.679, -3.4, -13, 0, 0.024, 2.37, -15924.306, -5.2, -76, 0, 0.024, 3.46, 8681.372, 0, 7, 0, 0.023, 2.80, 7108.936, 0, 7, 0, 0.023, 2.17, 7109.640, 0, 7, 0, 0.023, 2.57, -10804.699, -3.0, -44, 0, 0.022, 1.21, 6323.246, 0, 7, 0, 0.022, 3.22, 8259.965, 0, 7, 0, 0.022, 1.97, 7106.987, 1.5, 32, 0, 0.022, 3.00, 7111.589, -1.5, -18, 0, 0.022, 1.22, 14532.900, -6.5, -63, 0, 0.021, 0.66, 5923.672, -1.8, -15, 0, 0.021, 0.69, 24047.208, -3.2, -16, 0, 0.020, 5.51, -26415.027, -1.8, -42, 0, 0.020, 3.70, 16745.237, -2.8, -19, 0, 0.020, 1.26, 7038.300, 1.9, 29, 0, 0.020, 5.78, 6716.267, 0, 7, 0, 0.020, 3.76, 7895.330, 0, 7, 0, 0.019, 0.44, -121.695, -1.5, -18, 0, 0.018, 2.16, 15576.541, -0.9, 0, 0, 0.018, 0.09, -17248.484, -4.6, -68, 0, 0.018, 6.14, -7176.862, 0.6, 0, 0, 0.018, 5.55, 50076.923, 7.0, 125, -1, 0.017, 2.47, 8257.674, 3, 47, 0, 0.017, 4.20, 31609.036, 1, 38, 0, 0.017, 0.50, 175.762, -4, -48, 0, 0.017, 3.20, -2057.255, 3, 33, 0}, + //MB1 + {0.07430, 4.0998, 6480.98618, 0.005, 7, -0.03, 0.03043, 0.872, 7737.59009, -0.05, 7, 0, 0.02229, 5.000, 15019.22707, -2.80, -19, 0.1, 0.01999, 1.072, 23347.91849, -1.28, 6, 0, 0.01869, 1.744, -1847.70525, -1.52, -18, 0.1, 0.01696, 5.597, 16133.8556, 0.91, 24, -0.1, 0.01623, 0.014, 9061.7681, -0.67, 0, 0, 0.01419, 3.942, 733.0767, -2.19, -26, 0.1, 0.01338, 2.370, 17390.4595, 0.86, 25, -0.1, 0.01304, 5.633, 8399.6847, -0.33, 3, 0, 0.01279, 0.886, -523.5272, -2.14, -26, 0.1, 0.01215, 3.242, 7805.1642, -0.61, -1, 0, 0.01088, 3.686, 8435.7674, -2.16, -26, 0.1, 0.01088, 5.853, 8431.1650, 0.88, 25, -0.1, 0.00546, 4.143, 5852.6842, 0, 7, 0, 0.00443, 0.17, 14809.6776, 1.5, 32, 0, 0.00342, 2.24, 8399.7091, -0.3, 3, 0, 0.00330, 1.77, 16275.8310, -2.9, -19, 0, 0.00318, 4.13, 24604.5224, -1.3, 6, 0, 0.00296, 0.90, 7109.2881, 0, 7, 0, 0.00285, 3.43, 31676.6099, 0.2, 31, 0, 0.00207, 3.23, 16066.2815, 1.5, 32, 0, 0.00202, 2.07, 16833.1453, -1.0, 3, 0, 0.00202, 5.10, -33.7871, 0.3, 4, 0, 0.00200, 1.67, 24462.5471, 2.4, 50, 0, 0.00198, 4.80, -591.1013, -1.6, -18, 0, 0.00193, 1.12, 22719.6165, -1.2, 6, 0, 0.00164, 5.67, -10176.397, -3.0, -43, 0, 0.00161, 4.73, 25719.151, 2.4, 50, 0, 0.00158, 5.04, 14390.925, -2.8, -20, 0, 0.00149, 5.86, 13695.049, -2.2, -12, 0, 0.00135, 1.79, -2476.007, -1.5, -18, 0, 0.00121, 1.93, 16759.856, 2.4, 50, 0, 0.00117, 6.04, 16764.459, -0.6, 0, 0, 0.00104, 1.93, 22023.740, -0.7, 13, 0, 0.00085, 2.83, 30561.981, -3.5, -13, 0, 0.00079, 4.81, -8852.219, -3.7, -51, 0, 0.00076, 1.59, -7595.615, -3.7, -51, 0, 0.00075, 2.91, 8433.795, 0.9, 25, 0, 0.00075, 0.35, 8433.138, -2.2, -26, 0, 0.00073, 0.13, 70.993, -1.9, -22, 0, 0.00069, 1.70, 16728.376, 1.2, 28, 0, 0.00068, 0.83, 8365.892, -0.1, 7, 0, 0.00060, 0.20, 32933.214, 0.2, 31, 0, 0.00060, 0.31, 8446.085, 0, 7, 0}, + //MB2 + {0.000220, 4.100, 6480.9862, 0, 7, 0, 0.000101, 5.24, 8435.7674, -2.2, -26, 0, 0.000101, 4.30, 8431.1650, 0.9, 25, 0, 0.000090, 0.87, 7737.5901, 0, 7, 0, 0.000060, 1.07, 23347.9185, -1.3, 6, 0, 0.000060, 5.00, 15019.2271, -2.8, -19, 0, 0.000050, 1.74, -1847.705, -1.5, -18, 0, 0.000050, 5.60, 16133.856, 0.9, 24, 0, 0.000050, 0.01, 9061.768, -0.7, 0, 0, 0.000040, 3.24, 7805.164, -0.6, 0, 0, 0.000040, 3.94, 733.077, -2.2, -26, 0, 0.000040, 0.89, -523.527, -2.1, -26, 0}}, + + { //精度1千米 + //MR0 + {385000.510, 0, 0, 0, 0, 0, 20905.354, 5.4971472, 8328.691424623, 1.522924, 25.0719, -0.12360, 3699.111, 4.8997864, 7214.06286536, -2.184756, -18.860, 0.0828, 2955.967, 0.972156, 15542.75428998, -0.66183, 6.212, -0.0408, 569.925, 1.569516, 16657.3828492, 3.04585, 50.14, -0.2472, 246.158, 5.68582, -1114.6285593, -3.7077, -43.93, 0.206, 204.586, 1.01528, 14914.4523348, -0.6352, 6.15, -0.035, 170.733, 3.32771, 23871.4457146, 0.8611, 31.28, -0.164, 152.138, 4.94291, 6585.7609101, -2.1581, -18.92, 0.088, 129.620, 0.74291, -7700.3894694, -1.5496, -25.01, 0.118, 108.743, 5.19847, 7771.3771450, -0.3309, 3.1, -0.020, 104.755, 2.31243, 8956.993380, 1.4963, 25.1, -0.129, 79.661, 5.38293, -8538.240890, 2.8030, 26.1, -0.118, 48.888, 6.24006, 628.301955, -0.0266, 0.1, -0.005, 34.783, 2.73035, 22756.817155, -2.847, -12.6, 0.04, 30.824, 4.0706, 16171.056245, -0.688, 6.3, -0.05, 24.208, 1.7151, 7842.364821, -2.211, -18.8, 0.08, 23.210, 3.9251, 24986.074274, 4.569, 75.2, -0.37, 21.636, 0.3748, 14428.125731, -4.370, -37.7, 0.17, 16.675, 2.0137, 8399.679100, -0.358, 3.2, -0.03, 14.403, 3.3303, -9443.319984, -5.231, -69.0, 0.33, 12.831, 3.3708, 23243.143759, 0.888, 31.2, -0.16, 11.650, 5.0859, 31085.508580, -1.324, 12, -0.08, 10.445, 5.6833, 32200.13714, 2.384, 56, -0.29, 10.321, 0.8579, -1324.17803, 0.618, 7, -0.03, 10.056, 5.7290, -1742.93051, -3.681, -44, 0.21, 9.884, 1.0584, 14286.15038, -0.609, 6, -0.03, 8.752, 4.7856, -9652.86945, -0.905, -18, 0.09, 8.379, 5.9845, -557.31428, -1.854, -22, 0.10, 7.003, 4.6705, -16029.08089, -3.072, -50, 0.24, 6.322, 1.2708, 16100.06857, 1.192, 28, -0.14, 5.751, 4.6680, 17285.68480, 3.019, 50, -0.25, 4.950, 4.9860, 5957.45895, -2.131, -19, 0.09, 4.421, 4.5969, -209.54947, 4.326, 51, -0.24, 4.131, 3.2135, 7004.51340, 2.141, 32, -0.16, 3.958, 2.7735, 22128.51520, -2.820, -13, 0.05, 3.258, 0.6735, 14985.44001, -2.52, -16, 0.1, 3.148, 0.114, 16866.93231, -1.28, -1, 0, 2.616, 0.143, 24499.74767, 0.83, 31, -0.2, 2.354, 1.672, 8470.66678, -2.24, -19, 0.1, 2.117, 0.700, -7072.08751, -1.58, -25, 0.1, 1.897, 0.418, 13799.82378, -4.34, -38, 0.2, 1.739, 3.629, -8886.00570, -3.38, -47, 0.2, 1.571, 5.129, 30457.20662, -1.30, 12, -0.1, 1.423, 1.158, 39414.20000, 0.20, 37, -0.2, 1.419, 6.171, 23314.13143, -0.99, 9, -0.1, 1.166, 2.269, 9585.29534, 1.47, 25, -0.1, 1.117, 6.281, 33314.76570, 6.09, 100, -0.5, 1.066, 6.197, 1256.60391, -0.05, 0, 0, 1.059, 4.068, 8364.73984, -2.18, -19, 0.1, 0.933, 4.369, 16728.3705, 1.17, 28, -0.1, 0.862, 4.601, 6656.7486, -4.04, -41, 0.2, 0.851, 2.800, 70.9877, -1.88, -22, 0.1, 0.849, 5.726, 31571.8352, 2.41, 56, -0.3, 0.796, 5.084, -9095.5552, 0.95, 4, 0, 0.779, 0.975, -17772.0114, -6.75, -94, 0.5, 0.774, 2.658, 15752.3038, -4.99, -45, 0.2, 0.728, 0.266, 8326.3902, 3.05, 50, -0.2, 0.683, 1.304, 8330.9926, 0, 0, 0, 0.670, 1.756, 40528.8286, 3.91, 81, -0.4, 0.658, 3.414, 22614.8418, 0.91, 31, -0.2, 0.657, 0.901, -1952.4800, 0.64, 7, 0, 0.598, 6.026, 8393.1258, -2.18, -19, 0.1, 0.596, 5.014, 24080.9952, -3.46, -20, 0.1, 0.579, 5.829, 23385.1191, -2.87, -13, 0, 0.514, 4.302, 6099.4343, -5.89, -63, 0.3, 0.508, 1.830, 14218.5763, -0.04, 13, -0.1, 0.498, 5.242, 7143.0752, -0.30, 3, 0, 0.495, 3.373, -10071.6219, -5.20, -69, 0.3, 0.473, 2.430, -17981.5609, -2.43, -43, 0.2, 0.456, 4.887, -8294.9344, -1.83, -29, 0.1, 0.453, 0.173, 8362.4485, 1.21, 21, -0.1, 0.423, 4.489, 29970.8800, -5.03, -32, 0.1, 0.422, 2.315, -24357.7723, -4.60, -75, 0.4, 0.411, 1.102, 13657.8484, -0.58, 6, 0, 0.410, 0.500, 8311.7707, -2.18, -19, 0.1, 0.379, 3.626, 24428.7600, 2.71, 53, 0, 0.355, 0.740, 25614.3762, 4.54, 75, 0, 0.343, 5.772, -2371.2325, -3.7, -44, 0, 0.335, 0.857, 9166.5428, -2.8, -26, 0, 0.332, 0.444, -8257.7037, -3.4, -47, 0, 0.323, 4.829, -10281.1714, -0.9, -18, 0, 0.322, 5.758, 5889.8848, -1.6, -12, 0, 0.287, 0.56, 38299.5714, -3.5, -6, 0, 0.284, 5.57, 15333.2048, 3.7, 57, 0, 0.279, 2.82, 21500.2132, -2.8, -13, 0, 0.256, 0.72, 14357.1381, -2.5, -16, 0, 0.248, 2.20, -7909.9389, 2.8, 26, 0, 0.245, 1.90, 31713.8105, -1.4, 12, 0, 0.237, 3.47, 15056.4277, -4.4, -38, 0, 0.213, 3.77, 15613.7420, -2.5, -16, 0, 0.213, 2.50, 32828.4391, 2.4, 56, 0, 0.209, 3.26, 6376.2114, 2.2, 32, 0, 0.205, 2.93, 14967.4158, -0.7, 6, 0, 0.205, 2.02, 15540.4531, 0.9, 31, 0, 0.204, 3.06, 15545.0555, -2.2, -19, 0, 0.203, 1.20, 38785.8980, 0.2, 37, 0, 0.201, 6.06, 6447.1991, 0.3, 10, 0, 0.186, 6.13, -16238.6304, 1.3, 1, 0, 0.183, 2.13, 21642.1886, -6.6, -57, 0, 0.169, 3.29, -8815.0180, -5.3, -69, 0, 0.167, 1.06, 8328.3391, 1.5, 25, 0, 0.167, 0.51, 8329.0437, 1.5, 25, 0, 0.167, 1.26, 14756.7124, -0.7, 6, 0, 0.158, 0.07, 17495.2343, -1.3, -1, 0, 0.157, 0.57, 6638.7244, -2.2, -19, 0, 0.157, 6.21, 22685.8295, -1.0, 9, 0, 0.148, 5.03, 5329.1570, -2.1, -19, 0, 0.148, 4.03, 16799.3582, -0.7, 6, 0, 0.145, 0.05, 7178.0144, 1.5, 25, 0, 0.144, 5.64, -486.3266, -3.7, -44, 0, 0.139, 3.51, 47742.8914, 1.7, 63, 0, 0.138, 4.07, 7935.6705, 1.5, 25, 0, 0.136, 4.63, -15400.7789, -3.1, -50, 0, 0.136, 3.96, -695.8761, 0.6, 7, 0, 0.135, 5.95, 7211.7617, -0.7, 6, 0, 0.128, 5.17, 29828.9047, -1.3, 12, 0, 0.127, 1.18, 7753.3529, 1.5, 25, 0, 0.127, 0.71, 7216.3641, -3.7, -44, 0, 0.124, 5.83, 15149.7333, -0.7, 6, 0, 0.121, 1.46, 8000.1048, -2.2, -19, 0, 0.120, 3.78, 8721.7124, 1.5, 25, 0, 0.116, 5.19, 6428.0209, -2.2, -19, 0, 0.114, 2.89, -1185.6162, -1.8, -22, 0, 0.112, 2.85, 15542.4020, -0.7, 6, 0, 0.112, 2.23, 15543.1066, -0.7, 6, 0, 0.110, 0.51, 7213.7105, -2.2, -19, 0, 0.110, 6.15, 7214.4152, -2.2, -19, 0, 0.110, 1.31, 15471.7666, 1.2, 28, 0, 0.109, 2.46, 141.9754, -3.8, -44, 0, 0.108, 0.46, 13171.5218, -4.3, -38, 0, 0.108, 6.13, 23942.4334, -1.0, 9, 0, 0.107, 3.15, 15508.9972, -0.4, 10, 0, 0.105, 0.39, 8904.030, 1.5, 25, 0, 0.105, 4.93, 14392.077, -0.7, 6, 0, 0.103, 2.47, 25195.624, 0.2, 24, 0, 0.101, 3.48, 6821.042, -2.2, -19, 0, 0.099, 4.37, 7149.629, 1.5, 25, 0, 0.099, 1.27, -17214.697, -4.9, -72, 0, 0.096, 1.93, 15576.511, -1.0, 0, 0, 0.086, 2.92, 46628.263, -2.0, 19, 0, 0.085, 6.22, 8504.484, -2.5, -22, 0, 0.080, 3.40, -2438.807, -3.1, -37, 0, 0.080, 1.17, 8786.147, -2.2, -19, 0, 0.077, 3.61, 7230.984, 1.5, 25, 0, 0.071, 0.28, 8315.574, -2.2, -19, 0, 0.067, 4.53, 29342.578, -5.0, -32, 0, 0.065, 2.24, 31642.823, 0.5, 34, 0, 0.063, 5.80, 8329.403, 1.5, 25, 0, 0.063, 2.05, 8327.980, 1.5, 25, 0, 0.062, 0.08, 8346.716, -0.3, 0, 0, 0.061, 4.85, 36.048, -3.7, -44, 0, 0.061, 2.58, 6063.386, -2.2, -19, 0, 0.061, 4.30, -766.864, 2.5, 29, 0, 0.060, 3.01, 8322.132, -0.3, 0, 0, 0.059, 0.44, 25057.062, 2.7, 53, 0, 0.059, 0.31, 8288.877, 1.5, 25, 0, 0.059, 2.35, 41643.457, 7.6, 125, -1, 0.059, 1.26, 8368.506, 1.5, 25, 0, 0.058, 1.80, 39900.527, 3.9, 81, 0, 0.058, 1.87, 13590.274, 0, 13, 0, 0.057, 0.47, 14954.262, -0.7, 6, 0, 0.057, 6.20, 8294.910, 1.8, 29, 0, 0.056, 4.63, -8362.473, -1.2, -21, 0, 0.055, 2.86, 8170.957, 1.5, 25, 0, 0.055, 0.03, 7632.815, 2.1, 32, 0, 0.053, 0.80, 7180.306, -1.9, -15, 0, 0.053, 4.64, 6028.447, -4.0, -41, 0, 0.053, 4.57, 15385.020, -0.7, 6, 0, 0.052, 0.60, 37671.269, -3.5, -6, 0, 0.052, 4.99, 8486.426, 1.5, 25, 0, 0.051, 4.62, 17913.987, 3.0, 50, 0, 0.050, 1.64, 837.851, -4.4, -51, 0, 0.049, 5.79, 7542.649, 1.5, 25, 0, 0.049, 2.06, 9114.733, 1.5, 25, 0, 0.049, 2.21, 7056.329, -2.2, -19, 0, 0.049, 4.90, 7214.063, -2.2, -19, 0, 0.048, 5.39, -1671.943, -5.6, -66, 0, 0.047, 4.90, -26100.703, -8.3, -119, 1, 0.047, 1.60, -9024.567, -0.9, -18, 0, 0.046, 1.16, 7161.094, -2.2, -19, 0, 0.046, 5.77, 30943.533, 2.4, 56, 0, 0.046, 2.43, 22199.503, -4.7, -35, 0, 0.046, 3.16, 14991.999, -0.7, 6, 0, 0.044, 4.11, 48857.520, 5.4, 106, -1, 0.044, 4.39, 6625.570, -2.2, -19, 0, 0.044, 6.06, 7789.401, -2.2, -19, 0, 0.043, 0.14, 16693.431, -0.7, 6, 0, 0.043, 4.50, 15020.385, -0.7, 6, 0, 0.043, 4.35, 5471.132, -5.9, -63, 0, 0.043, 4.32, 575.338, 0, 0, 0, 0.043, 5.43, 7267.032, -2.2, -19, 0, 0.043, 3.82, 16328.796, -0.7, 6, 0, 0.042, 2.73, -17424.247, -0.6, -21, 0, 0.041, 3.60, 15489.785, -0.7, 6, 0, 0.040, 2.62, 16655.082, 4.6, 75, 0, 0.040, 4.23, 8351.233, -2.2, -19, 0, 0.039, 0.66, -6443.786, -1.6, -25, 0, 0.039, 2.13, 16118.093, -0.7, 6, 0, 0.039, 5.86, 7247.820, -2.5, -23, 0, 0.038, 4.56, 7285.051, -4.1, -41, 0, 0.038, 2.59, 9179.168, -2.2, -19, 0, 0.038, 1.42, 393.021, 0, 0, 0, 0.038, 4.94, 8381.661, 1.5, 25, 0, 0.037, 5.06, 23452.693, -3.4, -20, 0, 0.037, 5.11, 9027.981, -0.4, 0, 0, 0.037, 4.98, 7740.199, 1.5, 25, 0, 0.037, 3.66, 16659.684, 1.5, 25, 0, 0.037, 2.89, 8275.722, 1.5, 25, 0, 0.037, 4.26, 40042.502, 0.2, 38, 0, 0.036, 1.95, 8326.062, 1.5, 25, 0, 0.036, 5.90, 8331.321, 1.5, 25, 0, 0.035, 1.33, 15595.723, -0.7, 6, 0, 0.035, 1.39, 7777.936, 2, 25, 0, 0.035, 0.80, 6663.308, -2, -19, 0, 0.035, 0.53, 64.434, -4, -44, 0, 0.034, 2.15, 6691.693, -2, -19, 0, 0.034, 1.90, -8467.253, 1, 0, 0, 0.033, 2.83, 7806.322, 2, 25, 0, 0.033, 4.67, 9479.368, 2, 25, 0, 0.033, 1.41, 418.752, 4, 51, 0}, + //MR1 + {0.5139, 4.1569, 14914.452335, -0.635, 6.2, -0.04, 0.3824, 1.8013, 6585.760910, -2.158, -19, 0.09, 0.3265, 2.3987, 7700.38947, 1.550, 25, -0.12, 0.2640, 5.4540, 8956.99338, 1.496, 25, -0.13, 0.1230, 3.0985, 628.30196, -0.027, 0, 0, 0.0775, 0.929, 16171.05625, -0.69, 6, 0, 0.0607, 4.857, 7842.36482, -2.21, -19, 0.1, 0.0497, 4.200, 14286.15038, -0.61, 6, 0, 0.0419, 5.155, 8399.67910, -0.36, 3, 0, 0.0322, 0.229, 23243.1438, 0.89, 31, -0.2, 0.0253, 2.587, -1742.9305, -3.68, -44, 0.2, 0.0249, 1.844, 5957.4590, -2.13, -19, 0.1, 0.0176, 4.754, 16029.0809, 3.07, 50, -0.2, 0.0145, 1.526, 17285.6848, 3.02, 50, -0.3, 0.0137, 1.004, 15542.7543, -0.66, 6, 0, 0.0126, 5.010, 8326.3902, 3.05, 50, 0, 0.0119, 4.814, 8470.6668, -2.24, -19, 0, 0.0118, 2.843, 8330.9926, 0, 0, 0, 0.0107, 2.442, 7072.0875, 1.6, 25, 0, 0.0099, 5.92, 22128.5152, -2.8, -13, 0, 0.0066, 3.28, 24499.7477, 0.8, 31, 0, 0.0065, 4.90, 7214.0629, -2.2, -19, 0, 0.0059, 5.41, 9585.2953, 1.5, 25, 0, 0.0054, 3.06, 1256.6039, -0.1, 0, 0, 0.0052, 2.50, 8328.3391, 1.5, 25, 0, 0.0052, 5.35, 8329.0437, 1.5, 25, 0, 0.0048, 3.56, 13799.8238, -4.3, -38, 0, 0.0039, 1.99, 30457.2066, -1.3, 12, 0, 0.0035, 0.49, 15540.4531, 0.9, 31, 0, 0.0035, 4.60, 15545.0555, -2.2, -19, 0, 0.0033, 0.27, 22614.842, 0.9, 31, 0, 0.0031, 4.24, 13657.848, -0.6, 6, 0, 0.0023, 1.23, 16728.371, 1.2, 28, 0, 0.0023, 5.50, 8328.691, 1.5, 25, 0, 0.0023, 0, 0, 0, 0, 0, 0.0023, 4.41, 7211.762, -0.7, 6, 0, 0.0022, 1.39, 8311.771, -2.2, -19, 0, 0.0022, 2.25, 7216.364, -3.7, -44, 0, 0.0021, 5.94, 70.988, -1.9, -22, 0, 0.0021, 2.58, 31571.835, 2.4, 56, 0, 0.0017, 2.63, -2371.232, -3.7, -44, 0, 0.0016, 4.04, -1952.480, 0.6, 7, 0, 0.0015, 4.23, 8329.403, 1.5, 25, 0, 0.0015, 3.63, 8327.980, 1.5, 25, 0, 0.0015, 2.69, 23385.119, -2.9, -13, 0, 0.0014, 5.96, 21500.213, -2.8, -13, 0, 0.0013, 4.06, 15542.402, -0.7, 6, 0, 0.0013, 1.02, 15543.107, -0.7, 6, 0, 0.0013, 2.10, 7143.075, -0.3, 0, 0, 0.0012, 0.23, -10071.622, -5.2, -69, 0, 0.0011, 3.33, 23871.446, 1, 31, 0, 0.0011, 1.89, 5329.157, -2, -19, 0}, + //MR2 + {0.001490, 4.157, 14914.45233, -0.64, 6, 0, 0.001110, 1.801, 6585.7609, -2.16, -19, 0.1, 0.000950, 2.399, 7700.3895, 1.55, 25, -0.1, 0.000770, 5.454, 8956.9934, 1.50, 25, -0.1, 0.000360, 3.098, 628.3020, 0, 0, 0, 0.000230, 0.93, 16171.0562, -0.7, 6, 0, 0.000180, 4.86, 7842.3648, -2.2, -19, 0, 0.000140, 4.20, 14286.1504, -0.6, 6, 0, 0.000120, 5.16, 8399.6791, -0.4, 0, 0, 0.000116, 3.46, 8326.390, 3.0, 50, 0, 0.000109, 4.39, 8330.993, 0, 0, 0, 0.000090, 0.23, 23243.144, 0.9, 31, 0}}} + +// GetMoonCir 返回月球级数系数的独立副本。 +// 月球计算直接读取包内的大型不可变数据,避免重复构建这组大型不可变数据。 +// GetMoonCir returns an independent copy of the lunar-series coefficients. +// Moon calculations read the package-owned table directly so repeated calls do not rebuild this large immutable data set. func GetMoonCir() [][][]float64 { - XL1 := [][][]float64{{ - //ML0 - { - 22639.586, 0.78475822, 8328.691424623, 1.5229241, 25.0719, -0.123598, 4586.438, 0.1873974, 7214.06286536, -2.184756, -18.860, 0.08280, 2369.914, 2.5429520, 15542.75428998, -0.661832, 6.212, -0.04080, 769.026, 3.140313, 16657.38284925, 3.04585, 50.144, -0.2472, 666.418, 1.527671, 628.30195521, -0.02664, 0.062, -0.0054, 411.596, 4.826607, 16866.9323150, -1.28012, -1.07, -0.0059, 211.656, 4.115028, -1114.6285593, -3.70768, -43.93, 0.2064, 205.436, 0.230523, 6585.7609101, -2.15812, -18.92, 0.0882, 191.956, 4.898507, 23871.4457146, 0.86109, 31.28, -0.164, 164.729, 2.586078, 14914.4523348, -0.6352, 6.15, -0.035, 147.321, 5.45530, -7700.3894694, -1.5496, -25.01, 0.118, 124.988, 0.48608, 7771.3771450, -0.3309, 3.11, -0.020, 109.380, 3.88323, 8956.9933798, 1.4963, 25.13, -0.129, 55.177, 5.57033, -1324.1780250, 0.6183, 7.3, -0.035, 45.100, 0.89898, 25195.623740, 0.2428, 24.0, -0.129, 39.533, 3.81213, -8538.240890, 2.8030, 26.1, -0.118, 38.430, 4.30115, 22756.817155, -2.8466, -12.6, 0.042, 36.124, 5.49587, 24986.074274, 4.5688, 75.2, -0.371, 30.773, 1.94559, 14428.125731, -4.3695, -37.7, 0.166, 28.397, 3.28586, 7842.364821, -2.2114, -18.8, 0.077, 24.358, 5.64142, 16171.056245, -0.6885, 6.3, -0.046, 18.585, 4.41371, -557.314280, -1.8538, -22.0, 0.10, 17.954, 3.58454, 8399.679100, -0.3576, 3.2, -0.03, 14.530, 4.9416, 23243.143759, 0.888, 31.2, -0.16, 14.380, 0.9709, 32200.137139, 2.384, 56.4, -0.29, 14.251, 5.7641, -2.301200, 1.523, 25.1, -0.12, 13.899, 0.3735, 31085.508580, -1.324, 12.4, -0.08, 13.194, 1.7595, -9443.319984, -5.231, -69.0, 0.33, 9.679, 3.0997, -16029.080894, -3.072, -50.1, 0.24, 9.366, 0.3016, 24080.995180, -3.465, -19.9, 0.08, 8.606, 4.1582, -1742.930514, -3.681, -44.0, 0.21, 8.453, 2.8416, 16100.068570, 1.192, 28.2, -0.14, 8.050, 2.6292, 14286.150380, -0.609, 6.1, -0.03, 7.630, 6.2388, 17285.684804, 3.019, 50.2, -0.25, 7.447, 1.4845, 1256.603910, -0.053, 0.1, -0.01, 7.371, 0.2736, 5957.458955, -2.131, -19.0, 0.09, 7.063, 5.6715, 33.757047, -0.308, -3.6, 0.02, 6.383, 4.7843, 7004.513400, 2.141, 32.4, -0.16, 5.742, 2.6572, 32409.686605, -1.942, 5, -0.05, 4.374, 4.3443, 22128.51520, -2.820, -13, 0.05, 3.998, 3.2545, 33524.31516, 1.766, 49, -0.25, 3.210, 2.2443, 14985.44001, -2.516, -16, 0.06, 2.915, 1.7138, 24499.74767, 0.834, 31, -0.17, 2.732, 1.9887, 13799.82378, -4.343, -38, 0.17, 2.568, 5.4122, -7072.08751, -1.576, -25, 0.11, 2.521, 3.2427, 8470.66678, -2.238, -19, 0.07, 2.489, 4.0719, -486.32660, -3.734, -44, 0.20, 2.146, 5.6135, -1952.47998, 0.645, 7, -0.03, 1.978, 2.7291, 39414.20000, 0.199, 37, -0.21, 1.934, 1.5682, 33314.76570, 6.092, 100, -0.5, 1.871, 0.4166, 30457.20662, -1.297, 12, -0.1, 1.753, 2.0582, -8886.00570, -3.38, -47, 0.2, 1.437, 2.386, -695.87607, 0.59, 7, 0, 1.373, 3.026, -209.54947, 4.33, 51, -0.2, 1.262, 5.940, 16728.37052, 1.17, 28, -0.1, 1.224, 6.172, 6656.74859, -4.04, -41, 0.2, 1.187, 5.873, 6099.43431, -5.89, -63, 0.3, 1.177, 1.014, 31571.83518, 2.41, 56, -0.3, 1.162, 3.840, 9585.29534, 1.47, 25, -0.1, 1.143, 5.639, 8364.73984, -2.18, -19, 0.1, 1.078, 1.229, 70.98768, -1.88, -22, 0.1, 1.059, 3.326, 40528.82856, 3.91, 81, -0.4, 0.990, 5.013, 40738.37803, -0.42, 30, -0.2, 0.948, 5.687, -17772.01141, -6.75, -94, 0.5, 0.876, 0.298, -0.35232, 0, 0, 0, 0.822, 2.994, 393.02097, 0, 0, 0, 0.788, 1.836, 8326.39022, 3.05, 50, -0.2, 0.752, 4.985, 22614.84180, 0.91, 31, -0.2, 0.740, 2.875, 8330.99262, 0, 0, 0, 0.669, 0.744, -24357.77232, -4.60, -75, 0.4, 0.644, 1.314, 8393.12577, -2.18, -19, 0.1, 0.639, 5.888, 575.33849, 0, 0, 0, 0.635, 1.116, 23385.11911, -2.87, -13, 0, 0.584, 5.197, 24428.75999, 2.71, 53, -0.3, 0.583, 3.513, -9095.55517, 0.95, 4, 0, 0.572, 6.059, 29970.88002, -5.03, -32, 0.1, 0.565, 2.960, 0.32863, 1.52, 25, -0.1, 0.561, 4.001, -17981.56087, -2.43, -43, 0.2, 0.557, 0.529, 7143.07519, -0.30, 3, 0, 0.546, 2.311, 25614.37623, 4.54, 75, -0.4, 0.536, 4.229, 15752.30376, -4.99, -45, 0.2, 0.493, 3.316, -8294.9344, -1.83, -29, 0.1, 0.491, 1.744, 8362.4485, 1.21, 21, -0.1, 0.478, 1.803, -10071.6219, -5.20, -69, 0.3, 0.454, 0.857, 15333.2048, 3.66, 57, -0.3, 0.445, 2.071, 8311.7707, -2.18, -19, 0.1, 0.426, 0.345, 23452.6932, -3.44, -20, 0.1, 0.420, 4.941, 33733.8646, -2.56, -2, 0, 0.413, 1.642, 17495.2343, -1.31, -1, 0, 0.404, 1.458, 23314.1314, -0.99, 9, -0.1, 0.395, 2.132, 38299.5714, -3.51, -6, 0, 0.382, 2.700, 31781.3846, -1.92, 5, 0, 0.375, 4.827, 6376.2114, 2.17, 32, -0.2, 0.361, 3.867, 16833.1753, -0.97, 3, 0, 0.358, 5.044, 15056.4277, -4.40, -38, 0.2, 0.350, 5.157, -8257.7037, -3.40, -47, 0.2, 0.344, 4.233, 157.7344, 0, 0, 0, 0.340, 2.672, 13657.8484, -0.58, 6, 0, 0.329, 5.610, 41853.0066, 3.29, 74, -0.4, 0.325, 5.895, -39.8149, 0, 0, 0, 0.309, 4.387, 21500.2132, -2.79, -13, 0.1, 0.302, 1.278, 786.0419, 0, 0, 0, 0.302, 5.341, -24567.3218, -0.27, -24, 0.1, 0.301, 1.045, 5889.8848, -1.57, -12, 0, 0.294, 4.201, -2371.2325, -3.65, -44, 0.2, 0.293, 3.704, 21642.1886, -6.55, -57, 0.2, 0.290, 4.069, 32828.4391, 2.36, 56, -0.3, 0.289, 3.472, 31713.8105, -1.35, 12, -0.1, 0.285, 5.407, -33.7814, 0.31, 4, 0, 0.283, 5.998, -16.9207, -3.71, -44, 0.2, 0.283, 2.772, 38785.8980, 0.23, 37, -0.2, 0.274, 5.343, 15613.7420, -2.54, -16, 0.1, 0.263, 3.997, 25823.9257, 0.22, 24, -0.1, 0.254, 0.600, 24638.3095, -1.61, 2, 0, 0.253, 1.344, 6447.1991, 0.29, 10, -0.1, 0.250, 0.887, 141.9754, -3.76, -44, 0.2, 0.247, 0.317, 5329.1570, -2.10, -19, 0.1, 0.245, 0.141, 36.0484, -3.71, -44, 0.2, 0.231, 2.287, 14357.1381, -2.49, -16, 0.1, 0.227, 5.158, 2.6298, 0, 0, 0, 0.219, 5.085, 47742.8914, 1.72, 63, -0.3, 0.211, 2.145, 6638.7244, -2.18, -19, 0.1, 0.201, 4.415, 39623.7495, -4.13, -14, 0, 0.194, 2.091, 588.4927, 0, 0, 0, 0.193, 3.057, -15400.7789, -3.10, -50, 0, 0.186, 5.598, 16799.3582, -0.72, 6, 0, 0.185, 3.886, 1150.6770, 0, 0, 0, 0.183, 1.619, 7178.0144, 1.52, 25, 0, 0.181, 2.635, 8328.3391, 1.52, 25, 0, 0.181, 2.077, 8329.0437, 1.52, 25, 0, 0.179, 3.215, -9652.8694, -0.90, -18, 0, 0.176, 1.716, -8815.0180, -5.26, -69, 0, 0.175, 5.673, 550.7553, 0, 0, 0, 0.170, 2.060, 31295.0580, -5.6, -39, 0, 0.167, 1.239, 7211.7617, -0.7, 6, 0, 0.165, 4.499, 14967.4158, -0.7, 6, 0, 0.164, 3.595, 15540.4531, 0.9, 31, 0, 0.164, 4.237, 522.3694, 0, 0, 0, 0.163, 4.633, 15545.0555, -2.2, -19, 0, 0.161, 0.478, 6428.0209, -2.2, -19, 0, 0.158, 2.03, 13171.5218, -4.3, -38, 0, 0.157, 2.28, 7216.3641, -3.7, -44, 0, 0.154, 5.65, 7935.6705, 1.5, 25, 0, 0.152, 0.46, 29828.9047, -1.3, 12, 0, 0.151, 1.19, -0.7113, 0, 0, 0, 0.150, 1.42, 23942.4334, -1.0, 9, 0, 0.144, 2.75, 7753.3529, 1.5, 25, 0, 0.137, 2.08, 7213.7105, -2.2, -19, 0, 0.137, 1.44, 7214.4152, -2.2, -19, 0, 0.136, 4.46, -1185.6162, -1.8, -22, 0, 0.136, 3.03, 8000.1048, -2.2, -19, 0, 0.134, 2.83, 14756.7124, -0.7, 6, 0, 0.131, 5.05, 6821.0419, -2.2, -19, 0, 0.128, 5.99, -17214.6971, -4.9, -72, 0, 0.127, 5.35, 8721.7124, 1.5, 25, 0, 0.126, 4.49, 46628.2629, -2.0, 19, 0, 0.125, 5.94, 7149.6285, 1.5, 25, 0, 0.124, 1.09, 49067.0695, 1.1, 55, 0, 0.121, 2.88, 15471.7666, 1.2, 28, 0, 0.111, 3.92, 41643.4571, 7.6, 125, -1, 0.110, 1.96, 8904.0299, 1.5, 25, 0, 0.106, 3.30, -18.0489, -2.2, -19, 0, 0.105, 2.30, -4.9310, 1.5, 25, 0, 0.104, 2.22, -6.5590, -1.9, -22, 0, 0.101, 1.44, 1884.9059, -0.1, 0, 0, 0.100, 5.92, 5471.1324, -5.9, -63, 0, 0.099, 1.12, 15149.7333, -0.7, 6, 0, 0.096, 4.73, 15508.9972, -0.4, 10, 0, 0.095, 5.18, 7230.9835, 1.5, 25, 0, 0.093, 3.37, 39900.5266, 3.9, 81, 0, 0.092, 2.01, 25057.0619, 2.7, 53, 0, 0.092, 1.21, -79.6298, 0, 0, 0, 0.092, 1.65, -26310.2523, -4.0, -68, 0, 0.091, 1.01, 42062.5561, -1.0, 23, 0, 0.090, 6.10, 29342.5781, -5.0, -32, 0, 0.090, 4.43, 15542.4020, -0.7, 6, 0, 0.090, 3.80, 15543.1066, -0.7, 6, 0, 0.089, 4.15, 6063.3859, -2.2, -19, 0, 0.086, 4.03, 52.9691, 0, 0, 0, 0.085, 0.49, 47952.4409, -2.6, 11, 0, 0.085, 1.60, 7632.8154, 2.1, 32, 0, 0.084, 0.22, 14392.0773, -0.7, 6, 0, 0.083, 6.22, 6028.4466, -4.0, -41, 0, 0.083, 0.63, -7909.9389, 2.8, 26, 0, 0.083, 5.20, -77.5523, 0, 0, 0, 0.082, 2.74, 8786.1467, -2.2, -19, 0, 0.080, 2.43, 9166.5428, -2.8, -26, 0, 0.080, 3.70, -25405.1732, 4.1, 27, 0, 0.078, 5.68, 48857.5200, 5.4, 106, -1, 0.077, 1.85, 8315.5735, -2.2, -19, 0, 0.075, 5.46, -18191.1103, 1.9, 8, 0, 0.075, 1.41, -16238.6304, 1.3, 1, 0, 0.074, 5.06, 40110.0761, -0.4, 30, 0, 0.072, 2.10, 64.4343, -3.7, -44, 0, 0.071, 2.17, 37671.2695, -3.5, -6, 0, 0.069, 1.71, 16693.4313, -0.7, 6, 0, 0.069, 3.33, -26100.7028, -8.3, -119, 1, 0.068, 1.09, 8329.4028, 1.5, 25, 0, 0.068, 3.62, 8327.9801, 1.5, 25, 0, 0.068, 2.41, 16833.1509, -1.0, 3, 0, 0.067, 3.40, 24709.2971, -3.5, -20, 0, 0.067, 1.65, 8346.7156, -0.3, 3, 0, 0.066, 2.61, 22547.2677, 1.5, 39, 0, 0.066, 3.50, 15576.5113, -1.0, 3, 0, 0.065, 5.76, 33037.9886, -2.0, 5, 0, 0.065, 4.58, 8322.1325, -0.3, 3, 0, 0.065, 6.20, 17913.9868, 3.0, 50, 0, 0.065, 1.50, 22685.8295, -1.0, 9, 0, 0.065, 2.37, 7180.3058, -1.9, -15, 0, 0.064, 1.06, 30943.5332, 2.4, 56, 0, 0.064, 1.89, 8288.8765, 1.5, 25, 0, 0.064, 4.70, 6.0335, 0.3, 4, 0, 0.063, 2.83, 8368.5063, 1.5, 25, 0, 0.063, 5.66, -2580.7819, 0.7, 7, 0, 0.062, 3.78, 7056.3285, -2.2, -19, 0, 0.061, 1.49, 8294.9100, 1.8, 29, 0, 0.061, 0.12, -10281.1714, -0.9, -18, 0, 0.061, 3.06, -8362.4729, -1.2, -21, 0, 0.061, 4.43, 8170.9571, 1.5, 25, 0, 0.059, 5.78, -13.1179, -3.7, -44, 0, 0.059, 5.97, 6625.5702, -2.2, -19, 0, 0.058, 5.01, -0.5080, -0.3, 0, 0, 0.058, 2.73, 7161.0938, -2.2, -19, 0, 0.057, 0.19, 7214.0629, -2.2, -19, 0, 0.057, 4.00, 22199.5029, -4.7, -35, 0, 0.057, 5.38, 8119.1420, 5.8, 76, 0, 0.056, 1.07, 7542.6495, 1.5, 25, 0, 0.056, 0.28, 8486.4258, 1.5, 25, 0, 0.054, 4.19, 16655.0816, 4.6, 75, 0, 0.053, 0.72, 7267.0320, -2.2, -19, 0, 0.053, 3.12, 12.6192, 0.6, 7, 0, 0.052, 2.99, -32896.013, -1.8, -49, 0, 0.052, 3.46, 1097.708, 0, 0, 0, 0.051, 5.37, -6443.786, -1.6, -25, 0, 0.051, 1.35, 7789.401, -2.2, -19, 0, 0.051, 5.83, 40042.502, 0.2, 38, 0, 0.051, 3.63, 9114.733, 1.5, 25, 0, 0.050, 1.51, 8504.484, -2.5, -22, 0, 0.050, 5.23, 16659.684, 1.5, 25, 0, 0.050, 1.15, 7247.820, -2.5, -23, 0, 0.047, 0.25, -1290.421, 0.3, 0, 0, 0.047, 4.67, -32686.464, -6.1, -100, 0, 0.047, 3.49, 548.678, 0, 0, 0, 0.047, 2.37, 6663.308, -2.2, -19, 0, 0.046, 0.98, 1572.084, 0, 0, 0, 0.046, 2.04, 14954.262, -0.7, 6, 0, 0.046, 3.72, 6691.693, -2.2, -19, 0, 0.045, 6.19, -235.287, 0, 0, 0, 0.044, 2.96, 32967.001, -0.1, 27, 0, 0.044, 3.82, -1671.943, -5.6, -66, 0, 0.043, 5.82, 1179.063, 0, 0, 0, 0.043, 0.07, 34152.617, 1.7, 49, 0, 0.043, 3.71, 6514.773, -0.3, 0, 0, 0.043, 5.62, 15.732, -2.5, -23, 0, 0.043, 5.80, 8351.233, -2.2, -19, 0, 0.042, 0.27, 7740.199, 1.5, 25, 0, 0.042, 6.14, 15385.020, -0.7, 6, 0, 0.042, 6.13, 7285.051, -4.1, -41, 0, 0.041, 1.27, 32757.451, 4.2, 78, 0, 0.041, 4.46, 8275.722, 1.5, 25, 0, 0.040, 0.23, 8381.661, 1.5, 25, 0, 0.040, 5.87, -766.864, 2.5, 29, 0, 0.040, 1.66, 254.431, 0, 0, 0, 0.040, 0.40, 9027.981, -0.4, 0, 0, 0.040, 2.96, 7777.936, 1.5, 25, 0, 0.039, 4.67, 33943.068, 6.1, 100, 0, 0.039, 3.52, 8326.062, 1.5, 25, 0, 0.039, 3.75, 21013.887, -6.5, -57, 0, 0.039, 5.60, 606.978, 0, 0, 0, 0.039, 1.19, 8331.321, 1.5, 25, 0, 0.039, 2.84, 7211.433, -2.2, -19, 0, 0.038, 0.67, 7216.693, -2.2, -19, 0, 0.038, 6.22, 25161.867, 0.6, 28, 0, 0.038, 4.40, 7806.322, 1.5, 25, 0, 0.038, 4.16, 9179.168, -2.2, -19, 0, 0.037, 4.73, 14991.999, -0.7, 6, 0, 0.036, 0.35, 67.514, -0.6, -7, 0, 0.036, 3.70, 25266.611, -1.6, 0, 0, 0.036, 5.39, 16328.796, -0.7, 6, 0, 0.035, 1.44, 7174.248, -2.2, -19, 0, 0.035, 5.00, 15684.730, -4.4, -38, 0, 0.035, 0.39, -15.419, -2.2, -19, 0, 0.035, 6.07, 15020.385, -0.7, 6, 0, 0.034, 6.01, 7371.797, -2.2, -19, 0, 0.034, 0.96, -16623.626, -3.4, -54, 0, 0.033, 6.24, 9479.368, 1.5, 25, 0, 0.033, 3.21, 23661.896, 5.2, 82, 0, 0.033, 4.06, 8311.418, -2.2, -19, 0, 0.033, 2.40, 1965.105, 0, 0, 0, 0.033, 5.17, 15489.785, -0.7, 6, 0, 0.033, 5.03, 21986.540, 0.9, 31, 0, 0.033, 4.10, 16691.140, 2.7, 46, 0, 0.033, 5.13, 47114.589, 1.7, 63, 0, 0.033, 4.45, 8917.184, 1.5, 25, 0, 0.033, 4.23, 2.078, 0, 0, 0, 0.032, 2.33, 75.251, 1.5, 25, 0, 0.032, 2.10, 7253.878, -2.2, -19, 0, 0.032, 3.11, -0.224, 1.5, 25, 0, 0.032, 4.43, 16640.462, -0.7, 6, 0, 0.032, 5.68, 8328.363, 0, 0, 0, 0.031, 5.32, 8329.020, 3.0, 50, 0, 0.031, 3.70, 16118.093, -0.7, 6, 0, 0.030, 3.67, 16721.817, -0.7, 6, 0, 0.030, 5.27, -1881.492, -1.2, -15, 0, 0.030, 5.72, 8157.839, -2.2, -19, 0, 0.029, 5.73, -18400.313, -6.7, -94, 0, 0.029, 2.76, 16.000, -2.2, -19, 0, 0.029, 1.75, 8879.447, 1.5, 25, 0, 0.029, 0.32, 8851.061, 1.5, 25, 0, 0.029, 0.90, 14704.903, 3.7, 57, 0, 0.028, 2.90, 15595.723, -0.7, 6, 0, 0.028, 5.88, 16864.631, 0.2, 24, 0, 0.028, 0.63, 16869.234, -2.8, -26, 0, 0.028, 4.04, -18609.863, -2.4, -43, 0, 0.027, 5.83, 6727.736, -5.9, -63, 0, 0.027, 6.12, 418.752, 4.3, 51, 0, 0.027, 0.14, 41157.131, 3.9, 81, 0, 0.026, 3.80, 15.542, 0, 0, 0, 0.026, 1.68, 50181.698, 4.8, 99, -1, 0.026, 0.32, 315.469, 0, 0, 0, 0.025, 5.67, 19.188, 0.3, 0, 0, 0.025, 3.16, 62.133, -2.2, -19, 0, 0.025, 3.76, 15502.939, -0.7, 6, 0, 0.025, 4.53, 45999.961, -2.0, 19, 0, 0.024, 3.21, 837.851, -4.4, -51, 0, 0.024, 2.82, 38157.596, 0.3, 37, 0, 0.024, 5.21, 15540.124, -0.7, 6, 0, 0.024, 0.26, 14218.576, 0, 13, 0, 0.024, 3.01, 15545.384, -0.7, 6, 0, 0.024, 1.16, -17424.247, -0.6, -21, 0, 0.023, 2.34, -67.574, 0.6, 7, 0, 0.023, 2.44, 18.024, -1.9, -22, 0, 0.023, 3.70, 469.400, 0, 0, 0, 0.023, 0.72, 7136.511, -2.2, -19, 0, 0.023, 4.50, 15582.569, -0.7, 6, 0, 0.023, 2.80, -16586.395, -4.9, -72, 0, 0.023, 1.51, 80.182, 0, 0, 0, 0.023, 1.09, 5261.583, -1.5, -12, 0, 0.023, 0.56, 54956.954, -0.5, 44, 0, 0.023, 4.01, 8550.860, -2.2, -19, 0, 0.023, 4.46, 38995.448, -4.1, -14, 0, 0.023, 3.82, 2358.126, 0, 0, 0, 0.022, 3.77, 32271.125, 0.5, 34, 0, 0.022, 0.82, 15935.775, -0.7, 6, 0, 0.022, 1.07, 24013.421, -2.9, -13, 0, 0.022, 0.40, 8940.078, -2.2, -19, 0, 0.022, 2.06, 15700.489, -0.7, 6, 0, 0.022, 4.27, 15124.002, -5.0, -45, 0, 0.021, 1.16, 56071.583, 3.2, 88, 0, 0.021, 5.58, 9572.189, -2.2, -19, 0, 0.020, 1.70, -17.273, -3.7, -44, 0, 0.020, 3.05, 214.617, 0, 0, 0, 0.020, 4.41, 8391.048, -2.2, -19, 0, 0.020, 5.95, 23869.145, 2.4, 56, 0, 0.020, 0.42, 40947.927, -4.7, -21, 0, 0.019, 1.39, 5818.897, 0.3, 10, 0, 0.019, 0.71, 23873.747, -0.7, 6, 0, 0.019, 2.81, 7291.615, -2.2, -19, 0, 0.019, 5.09, 8428.018, -2.2, -19, 0, 0.019, 4.14, 6518.187, -1.6, -12, 0, 0.019, 3.85, 21.330, 0, 0, 0, 0.018, 0.66, 14445.046, -0.7, 6, 0, 0.018, 1.65, 0.966, -4.0, -48, 0, 0.018, 5.64, -17143.709, -6.8, -94, 0, 0.018, 6.01, 7736.432, -2.2, -19, 0, 0.018, 2.74, 31153.083, -1.9, 5, 0, 0.018, 4.58, 6116.355, -2.2, -19, 0, 0.018, 2.28, 46.401, 0.3, 0, 0, 0.018, 3.80, 10213.597, 1.4, 25, 0, 0.018, 2.84, 56281.132, -1.1, 36, 0, 0.018, 3.53, 8249.062, 1.5, 25, 0, 0.017, 4.43, 20871.911, -3, -13, 0, 0.017, 4.44, 627.596, 0, 0, 0, 0.017, 1.85, 628.308, 0, 0, 0, 0.017, 1.19, 8408.321, 2, 25, 0, 0.017, 1.95, 7214.056, -2, -19, 0, 0.017, 1.57, 7214.070, -2, -19, 0, 0.017, 1.65, 13870.811, -6, -60, 0, 0.017, 0.30, 22.542, -4, -44, 0, 0.017, 2.62, -119.445, 0, 0, 0, 0.016, 4.87, 5747.909, 2, 32, 0, 0.016, 4.45, 14339.108, -1, 6, 0, 0.016, 1.83, 41366.680, 0, 30, 0, 0.016, 4.53, 16309.618, -3, -23, 0, 0.016, 2.54, 15542.754, -1, 6, 0, 0.016, 6.05, 1203.646, 0, 0, 0, 0.015, 5.2, 2751.147, 0, 0, 0, 0.015, 1.8, -10699.924, -5, -69, 0, 0.015, 0.4, 22824.391, -3, -20, 0, 0.015, 2.1, 30666.756, -6, -39, 0, 0.015, 2.1, 6010.417, -2, -19, 0, 0.015, 0.7, -23729.470, -5, -75, 0, 0.015, 1.4, 14363.691, -1, 6, 0, 0.015, 5.8, 16900.689, -2, 0, 0, 0.015, 5.2, 23800.458, 3, 53, 0, 0.015, 5.3, 6035.000, -2, -19, 0, 0.015, 1.2, 8251.139, 2, 25, 0, 0.015, 3.6, -8.860, 0, 0, 0, 0.015, 0.8, 882.739, 0, 0, 0, 0.015, 3.0, 1021.329, 0, 0, 0, 0.015, 0.6, 23296.107, 1, 31, 0, 0.014, 5.4, 7227.181, 2, 25, 0, 0.014, 0.1, 7213.352, -2, -19, 0, 0.014, 4.0, 15506.706, 3, 50, 0, 0.014, 3.4, 7214.774, -2, -19, 0, 0.014, 4.6, 6665.385, -2, -19, 0, 0.014, 0.1, -8.636, -2, -22, 0, 0.014, 3.1, 15465.202, -1, 6, 0, 0.014, 4.9, 508.863, 0, 0, 0, 0.014, 3.5, 8406.244, 2, 25, 0, 0.014, 1.3, 13313.497, -8, -82, 0, 0.014, 2.8, 49276.619, -3, 0, 0, 0.014, 0.1, 30528.194, -3, -10, 0, 0.013, 1.7, 25128.050, 1, 31, 0, 0.013, 2.9, 14128.405, -1, 6, 0, 0.013, 3.4, 57395.761, 3, 80, 0, 0.013, 2.7, 13029.546, -1, 6, 0, 0.013, 3.9, 7802.556, -2, -19, 0, 0.013, 1.6, 8258.802, -2, -19, 0, 0.013, 2.2, 8417.709, -2, -19, 0, 0.013, 0.7, 9965.210, -2, -19, 0, 0.013, 3.4, 50391.247, 0, 48, 0, 0.013, 3.0, 7134.433, -2, -19, 0, 0.013, 2.9, 30599.182, -5, -31, 0, 0.013, 3.6, -9723.857, 1, 0, 0, 0.013, 4.8, 7607.084, -2, -19, 0, 0.012, 0.8, 23837.689, 1, 35, 0, 0.012, 3.6, 4.409, -4, -44, 0, 0.012, 5.0, 16657.031, 3, 50, 0, 0.012, 4.4, 16657.735, 3, 50, 0, 0.012, 1.1, 15578.803, -4, -38, 0, 0.012, 6.0, -11.490, 0, 0, 0, 0.012, 1.9, 8164.398, 0, 0, 0, 0.012, 2.4, 31852.372, -4, -17, 0, 0.012, 2.4, 6607.085, -2, -19, 0, 0.012, 4.2, 8359.870, 0, 0, 0, 0.012, 0.5, 5799.713, -2, -19, 0, 0.012, 2.7, 7220.622, 0, 0, 0, 0.012, 4.3, -139.720, 0, 0, 0, 0.012, 2.3, 13728.836, -2, -16, 0, 0.011, 3.6, 14912.146, 1, 31, 0, 0.011, 4.7, 14916.748, -2, -19, 0}, - //ML1 - { - 1.67680, 4.66926, 628.301955, -0.0266, 0.1, -0.005, 0.51642, 3.3721, 6585.760910, -2.158, -18.9, 0.09, 0.41383, 5.7277, 14914.452335, -0.635, 6.2, -0.04, 0.37115, 3.9695, 7700.389469, 1.550, 25.0, -0.12, 0.27560, 0.7416, 8956.993380, 1.496, 25.1, -0.13, 0.24599, 4.2253, -2.301200, 1.523, 25.1, -0.12, 0.07118, 0.1443, 7842.36482, -2.211, -19, 0.08, 0.06128, 2.4998, 16171.05625, -0.688, 6, 0, 0.04516, 0.443, 8399.67910, -0.36, 3, 0, 0.04048, 5.771, 14286.15038, -0.61, 6, 0, 0.03747, 4.626, 1256.60391, -0.05, 0, 0, 0.03707, 3.415, 5957.45895, -2.13, -19, 0.1, 0.03649, 1.800, 23243.14376, 0.89, 31, -0.2, 0.02438, 0.042, 16029.08089, 3.07, 50, -0.2, 0.02165, 1.017, -1742.93051, -3.68, -44, 0.2, 0.01923, 3.097, 17285.68480, 3.02, 50, -0.3, 0.01692, 1.280, 0.3286, 1.52, 25, -0.1, 0.01361, 0.298, 8326.3902, 3.05, 50, -0.2, 0.01293, 4.013, 7072.0875, 1.58, 25, -0.1, 0.01276, 4.413, 8330.9926, 0, 0, 0, 0.01270, 0.101, 8470.6668, -2.24, -19, 0.1, 0.01097, 1.203, 22128.5152, -2.82, -13, 0, 0.01088, 2.545, 15542.7543, -0.66, 6, 0, 0.00835, 0.190, 7214.0629, -2.18, -19, 0.1, 0.00734, 4.855, 24499.7477, 0.83, 31, -0.2, 0.00686, 5.130, 13799.8238, -4.34, -38, 0.2, 0.00631, 0.930, -486.3266, -3.73, -44, 0, 0.00585, 0.699, 9585.2953, 1.5, 25, 0, 0.00566, 4.073, 8328.3391, 1.5, 25, 0, 0.00566, 0.638, 8329.0437, 1.5, 25, 0, 0.00539, 2.472, -1952.4800, 0.6, 7, 0, 0.00509, 2.88, -0.7113, 0, 0, 0, 0.00469, 3.56, 30457.2066, -1.3, 12, 0, 0.00387, 0.78, -0.3523, 0, 0, 0, 0.00378, 1.84, 22614.8418, 0.9, 31, 0, 0.00362, 5.53, -695.8761, 0.6, 7, 0, 0.00317, 2.80, 16728.3705, 1.2, 28, 0, 0.00303, 6.07, 157.7344, 0, 0, 0, 0.00300, 2.53, 33.7570, -0.3, -4, 0, 0.00295, 4.16, 31571.8352, 2.4, 56, 0, 0.00289, 5.98, 7211.7617, -0.7, 6, 0, 0.00285, 2.06, 15540.4531, 0.9, 31, 0, 0.00283, 2.65, 2.6298, 0, 0, 0, 0.00282, 6.17, 15545.0555, -2.2, -19, 0, 0.00278, 1.23, -39.8149, 0, 0, 0, 0.00272, 3.82, 7216.3641, -3.7, -44, 0, 0.00270, 4.37, 70.9877, -1.9, -22, 0, 0.00256, 5.81, 13657.8484, -0.6, 6, 0, 0.00244, 5.64, -0.2237, 1.5, 25, 0, 0.00240, 2.96, 8311.7707, -2.2, -19, 0, 0.00239, 0.87, -33.7814, 0.3, 4, 0, 0.00216, 2.31, 15.9995, -2.2, -19, 0, 0.00186, 3.46, 5329.1570, -2.1, -19, 0, 0.00169, 2.40, 24357.772, 4.6, 75, 0, 0.00161, 5.80, 8329.403, 1.5, 25, 0, 0.00161, 5.20, 8327.980, 1.5, 25, 0, 0.00160, 4.26, 23385.119, -2.9, -13, 0, 0.00156, 1.26, 550.755, 0, 0, 0, 0.00155, 1.25, 21500.213, -2.8, -13, 0, 0.00152, 0.60, -16.921, -3.7, -44, 0, 0.00150, 2.71, -79.630, 0, 0, 0, 0.00150, 5.29, 15.542, 0, 0, 0, 0.00148, 1.06, -2371.232, -3.7, -44, 0, 0.00141, 0.77, 8328.691, 1.5, 25, 0, 0.00141, 3.67, 7143.075, -0.3, 0, 0, 0.00138, 5.45, 25614.376, 4.5, 75, 0, 0.00129, 4.90, 23871.446, 0.9, 31, 0, 0.00126, 4.03, 141.975, -3.8, -44, 0, 0.00124, 6.01, 522.369, 0, 0, 0, 0.00120, 4.94, -10071.622, -5.2, -69, 0, 0.00118, 5.07, -15.419, -2.2, -19, 0, 0.00107, 3.49, 23452.693, -3.4, -20, 0, 0.00104, 4.78, 17495.234, -1.3, 0, 0, 0.00103, 1.44, -18.049, -2.2, -19, 0, 0.00102, 5.63, 15542.402, -0.7, 6, 0, 0.00102, 2.59, 15543.107, -0.7, 6, 0, 0.00100, 4.11, -6.559, -1.9, -22, 0, 0.00097, 0.08, 15400.779, 3.1, 50, 0, 0.00096, 5.84, 31781.385, -1.9, 5, 0, 0.00094, 1.08, 8328.363, 0, 0, 0, 0.00094, 2.46, 16799.358, -0.7, 6, 0, 0.00094, 1.69, 6376.211, 2.2, 32, 0, 0.00093, 3.64, 8329.020, 3.0, 50, 0, 0.00093, 2.65, 16655.082, 4.6, 75, 0, 0.00090, 1.90, 15056.428, -4.4, -38, 0, 0.00089, 1.59, 52.969, 0, 0, 0, 0.00088, 2.02, -8257.704, -3.4, -47, 0, 0.00088, 3.02, 7213.711, -2.2, -19, 0, 0.00087, 0.50, 7214.415, -2.2, -19, 0, 0.00087, 0.49, 16659.684, 1.5, 25, 0, 0.00082, 5.64, -4.931, 1.5, 25, 0, 0.00079, 5.17, 13171.522, -4.3, -38, 0, 0.00076, 3.60, 29828.905, -1.3, 12, 0, 0.00076, 4.08, 24567.322, 0.3, 24, 0, 0.00076, 4.58, 1884.906, -0.1, 0, 0, 0.00073, 0.33, 31713.811, -1.4, 12, 0, 0.00073, 0.93, 32828.439, 2.4, 56, 0, 0.00071, 5.91, 38785.898, 0.2, 37, 0, 0.00069, 2.20, 15613.742, -2.5, -16, 0, 0.00066, 3.87, 15.732, -2.5, -23, 0, 0.00066, 0.86, 25823.926, 0.2, 24, 0, 0.00065, 2.52, 8170.957, 1.5, 25, 0, 0.00063, 0.18, 8322.132, -0.3, 0, 0, 0.00060, 5.84, 8326.062, 1.5, 25, 0, 0.00060, 5.15, 8331.321, 1.5, 25, 0, 0.00060, 2.18, 8486.426, 1.5, 25, 0, 0.00058, 2.30, -1.731, -4, -44, 0, 0.00058, 5.43, 14357.138, -2, -16, 0, 0.00057, 3.09, 8294.910, 2, 29, 0, 0.00057, 4.67, -8362.473, -1, -21, 0, 0.00056, 4.15, 16833.151, -1, 0, 0, 0.00054, 1.93, 7056.329, -2, -19, 0, 0.00054, 5.27, 8315.574, -2, -19, 0, 0.00052, 5.6, 8311.418, -2, -19, 0, 0.00052, 2.7, -77.552, 0, 0, 0, 0.00051, 4.3, 7230.984, 2, 25, 0, 0.00050, 0.4, -0.508, 0, 0, 0, 0.00049, 5.4, 7211.433, -2, -19, 0, 0.00049, 4.4, 7216.693, -2, -19, 0, 0.00049, 4.3, 16864.631, 0, 24, 0, 0.00049, 2.2, 16869.234, -3, -26, 0, 0.00047, 6.1, 627.596, 0, 0, 0, 0.00047, 5.0, 12.619, 1, 7, 0, 0.00045, 4.9, -8815.018, -5, -69, 0, 0.00044, 1.6, 62.133, -2, -19, 0, 0.00042, 2.9, -13.118, -4, -44, 0, 0.00042, 4.1, -119.445, 0, 0, 0, 0.00041, 4.3, 22756.817, -3, -13, 0, 0.00041, 3.6, 8288.877, 2, 25, 0, 0.00040, 0.5, 6663.308, -2, -19, 0, 0.00040, 1.1, 8368.506, 2, 25, 0, 0.00039, 4.1, 6443.786, 2, 25, 0, 0.00039, 3.1, 16657.383, 3, 50, 0, 0.00038, 0.1, 16657.031, 3, 50, 0, 0.00038, 3.0, 16657.735, 3, 50, 0, 0.00038, 4.6, 23942.433, -1, 9, 0, 0.00037, 4.3, 15385.020, -1, 6, 0, 0.00037, 5.0, 548.678, 0, 0, 0, 0.00036, 1.8, 7213.352, -2, -19, 0, 0.00036, 1.7, 7214.774, -2, -19, 0, 0.00035, 1.1, 7777.936, 2, 25, 0, 0.00035, 1.6, -8.860, 0, 0, 0, 0.00035, 4.4, 23869.145, 2, 56, 0, 0.00035, 2.0, 6691.693, -2, -19, 0, 0.00034, 1.3, -1185.616, -2, -22, 0, 0.00034, 2.2, 23873.747, -1, 6, 0, 0.00033, 2.0, -235.287, 0, 0, 0, 0.00033, 3.1, 17913.987, 3, 50, 0, 0.00033, 1.0, 8351.233, -2, -19, 0}, - //ML2 - {0.004870, 4.6693, 628.30196, -0.027, 0, -0.01, 0.002280, 2.6746, -2.30120, 1.523, 25, -0.12, 0.001500, 3.372, 6585.76091, -2.16, -19, 0.1, 0.001200, 5.728, 14914.45233, -0.64, 6, 0, 0.001080, 3.969, 7700.38947, 1.55, 25, -0.1, 0.000800, 0.742, 8956.99338, 1.50, 25, -0.1, 0.000254, 6.002, 0.3286, 1.52, 25, -0.1, 0.000210, 0.144, 7842.3648, -2.21, -19, 0, 0.000180, 2.500, 16171.0562, -0.7, 6, 0, 0.000130, 0.44, 8399.6791, -0.4, 3, 0, 0.000126, 5.03, 8326.3902, 3.0, 50, 0, 0.000120, 5.77, 14286.1504, -0.6, 6, 0, 0.000118, 5.96, 8330.9926, 0, 0, 0, 0.000110, 1.80, 23243.1438, 0.9, 31, 0, 0.000110, 3.42, 5957.4590, -2.1, -19, 0, 0.000110, 4.63, 1256.6039, -0.1, 0, 0, 0.000099, 4.70, -0.7113, 0, 0, 0, 0.000070, 0.04, 16029.0809, 3.1, 50, 0, 0.000070, 5.14, 8328.3391, 1.5, 25, 0, 0.000070, 5.85, 8329.0437, 1.5, 25, 0, 0.000060, 1.02, -1742.9305, -3.7, -44, 0, 0.000060, 3.10, 17285.6848, 3.0, 50, 0, 0.000054, 5.69, -0.352, 0, 0, 0, 0.000043, 0.52, 15.542, 0, 0, 0, 0.000041, 2.03, 2.630, 0, 0, 0, 0.000040, 0.10, 8470.667, -2.2, -19, 0, 0.000040, 4.01, 7072.088, 1.6, 25, 0, 0.000036, 2.93, -8.860, -0.3, 0, 0, 0.000030, 1.20, 22128.515, -2.8, -13, 0, 0.000030, 2.54, 15542.754, -0.7, 6, 0, 0.000027, 4.43, 7211.762, -0.7, 6, 0, 0.000026, 0.51, 15540.453, 0.9, 31, 0, 0.000026, 1.44, 15545.055, -2.2, -19, 0, 0.000025, 5.37, 7216.364, -3.7, -44, 0}, - //ML3 - {0.00001200, 1.041, -2.3012, 1.52, 25, -0.1, 0.00000170, 0.31, -0.711, 0, 0, 0}}, - - { //精度1角秒 - //MB0 - {18461.240, 0.05710892, 8433.466157492, -0.6400617, -0.5345, -0.00294, 1010.167, 2.412663, 16762.15758211, 0.88286, 24.537, -0.1265, 999.694, 5.440038, -104.77473287, 2.16299, 25.606, -0.1207, 623.652, 0.915047, 7109.28813249, -0.02177, 6.746, -0.0379, 199.484, 1.815303, 15647.5290228, -2.82482, -19.39, 0.0799, 166.574, 4.842677, -1219.4032921, -1.5447, -18.33, 0.086, 117.261, 4.17086, 23976.2204475, -1.3019, 5.68, -0.044, 61.912, 4.76822, 25090.8490067, 2.4058, 49.61, -0.250, 33.357, 3.27060, 15437.979557, 1.5012, 31.8, -0.161, 31.760, 1.51241, 8223.916692, 3.6859, 50.7, -0.244, 29.577, 0.95817, 6480.986177, 0.0049, 6.7, -0.032, 15.566, 2.4871, -9548.094717, -3.068, -43.4, 0.21, 15.122, 0.2432, 32304.911872, 0.221, 30.7, -0.17, 12.094, 4.0135, 7737.590088, -0.048, 6.8, -0.04, 8.868, 1.8584, 15019.227068, -2.798, -19.5, 0.09, 8.045, 5.3812, 8399.709110, -0.332, 3.1, -0.02, 7.959, 4.2140, 23347.918492, -1.275, 5.6, -0.04, 7.435, 4.8858, -1847.705247, -1.518, -18.4, 0.09, 6.731, 3.8274, -16133.855627, -0.910, -24.5, 0.12, 6.580, 2.6732, 14323.350998, -2.207, -12.1, 0.04, 6.460, 3.1556, 9061.768113, -0.667, -0.5, -0.01, 6.296, 0.1713, 25300.398472, -1.920, -1.6, -0.01, 5.632, 0.8000, 733.076688, -2.190, -26, 0.12, 5.368, 2.1140, 16204.843302, -0.971, 3, -0.02, 5.311, 5.5111, 17390.459537, 0.856, 25, -0.13, 5.076, 2.2553, 523.52722, 2.136, 26, -0.13, 4.840, 6.1830, -7805.16420, 0.613, 1, 0, 4.806, 5.1414, -662.08901, 0.309, 4, -0.02, 3.984, 0.8406, 33419.54043, 3.929, 75, -0.37, 3.674, 5.0288, 22652.04242, -0.684, 13, -0.08, 2.998, 5.9291, 31190.28331, -3.487, -13, 0.04, 2.799, 2.1842, -16971.70705, 3.443, 27, -0.11, 2.414, 3.5735, 22861.59189, -5.010, -38, 0.16, 2.186, 3.9424, -9757.64418, 1.258, 8, -0.03, 2.146, 5.6262, 23766.67098, 3.024, 57, -0.29, 1.766, 3.3137, 14809.67760, 1.528, 32, -0.2, 1.624, 2.6013, 7318.83760, -4.35, -44, 0.2, 1.581, 3.8680, 16552.60812, 5.21, 76, -0.4, 1.520, 2.599, 40633.60330, 1.74, 56, -0.3, 1.516, 0.132, -17876.78614, -4.59, -68, 0.3, 1.510, 3.927, 8399.68473, -0.33, 3, 0, 1.318, 4.914, 16275.83098, -2.85, -19, 0.1, 1.264, 0.986, 24604.52240, -1.33, 6, 0, 1.192, 2.001, 39518.97474, -1.96, 12, -0.1, 1.135, 0.286, 31676.60992, 0.25, 31, -0.2, 1.086, 1.001, 5852.68422, 0.03, 7, 0, 1.019, 2.527, 33629.08990, -0.40, 23, -0.1, 0.823, 0.086, 16066.28151, 1.47, 32, -0.2, 0.804, 1.957, -33.78706, 0.28, 4, 0, 0.803, 5.212, 16833.14526, -1.00, 3, 0, 0.793, 1.472, -24462.54705, -2.43, -50, 0.2, 0.791, 1.658, -591.10134, -1.57, -18, 0.1, 0.667, 4.470, 24533.53473, 0.55, 28, -0.1, 0.650, 2.530, -10176.39667, -3.04, -43, 0.2, 0.639, 1.583, 25719.15096, 2.38, 50, -0.3, 0.634, 0.318, 5994.65957, -3.73, -37, 0.2, 0.631, 2.147, 8435.76736, -2.16, -26, 0.1, 0.630, 1.109, 8431.16496, 0.88, 25, -0.1, 0.596, 2.716, 13695.04904, -2.18, -12, 0.1, 0.589, 1.214, 7666.60241, 1.83, 29, -0.1, 0.473, 1.101, 30980.7338, 0.84, 38, -0.2, 0.456, 0.116, -71.0177, 1.85, 22, -0.1, 0.430, 2.786, -8990.7804, -1.21, -21, 0.1, 0.416, 1.454, 16728.4005, 1.19, 28, -0.1, 0.415, 5.072, 22023.7405, -0.66, 13, -0.1, 0.383, 4.257, 22719.6165, -1.25, 6, 0, 0.352, 2.972, 14880.6653, -0.35, 10, -0.1, 0.339, 5.972, 30561.9814, -3.46, -13, 0, 0.329, 1.587, -18086.3356, -0.26, -17, 0.1, 0.326, 1.016, 8467.2232, -0.95, -4, 0, 0.315, 1.902, 14390.9251, -2.77, -20, 0.1, 0.313, 4.611, 8852.2186, 3.66, 51, -0.2, 0.305, 0.616, 6551.9739, -1.88, -15, 0.1, 0.301, 4.728, -7595.6147, -3.71, -51, 0.2, 0.299, 1.874, 7143.0452, -0.33, 3, 0, 0.291, 3.156, -1428.9528, 2.78, 33, -0.2, 0.269, 4.929, -2476.0072, -1.49, -18, 0.1, 0.263, 3.196, 41748.2319, 5.45, 100, -0.5, 0.254, 3.387, -1009.8538, -5.87, -70, 0.3, 0.245, 1.930, 32514.4613, -4.10, -20, 0.1, 0.237, 3.342, 32933.2138, 0.19, 31, -0.2, 0.214, 3.617, 22233.2899, -4.98, -38, 0.2, 0.213, 4.357, 47847.6662, -0.44, 37, -0.2, 0.206, 3.872, 23418.9062, -3.16, -16, 0.1, 0.172, 5.772, 14951.6530, -2.2, -12, 0, 0.158, 2.04, 38890.6728, -1.9, 12, 0, 0.146, 1.70, 32095.3624, 4.5, 82, 0, 0.145, 4.29, 40843.1528, -2.6, 5, 0, 0.139, 2.90, 7876.1519, -2.5, -23, 0, 0.138, 4.95, 48962.2947, 3.3, 81, 0, 0.134, 3.97, 8365.8920, -0.1, 7, 0, 0.134, 4.06, -26205.4776, -6.1, -94, 0, 0.130, 1.40, -8643.0156, 5.0, 52, 0, 0.129, 5.67, 23138.3690, 3.1, 57, 0, 0.124, 2.64, 40005.3013, 1.8, 56, 0, 0.118, 4.88, 41957.7813, 1.1, 49, 0, 0.113, 3.78, -15505.5537, -0.9, -24, 0, 0.113, 4.87, 16904.1329, -2.9, -19, 0, 0.113, 1.84, 23280.3444, -0.7, 13, 0, 0.110, 0.43, -17319.4719, -2.7, -47, 0, 0.105, 1.61, 37.2006, -1.6, -18, 0, 0.102, 1.28, 25161.8367, 0.5, 28, 0, 0.095, 0.76, 1361.3786, -2.2, -25, 0, 0.094, 0.50, 29866.1053, -2.9, -6, 0, 0.092, 6.22, 24881.2995, 6.7, 101, 0, 0.088, 3.99, -10385.9461, 1.3, 8, 0, 0.085, 4.71, 70.9933, -1.9, -22, 0, 0.084, 0.86, 15613.7720, -2.5, -16, 0, 0.081, 4.43, 21537.4139, -4.4, -31, 0, 0.080, 1.86, -8365.9521, 0, -7, 0, 0.080, 0, 16728.3762, 1.2, 28, 0, 0.079, 2.44, -8919.7928, -3.1, -43, 0, 0.078, 3.69, -452.5395, -4.0, -48, 0, 0.075, 5.40, -32791.2385, -4.0, -75, 0, 0.073, 5.80, -1185.6462, -1.9, -22, 0, 0.070, 3.46, 16759.8564, 2.4, 50, 0, 0.069, 3.36, 14181.3756, 1.6, 32, 0, 0.068, 4.50, 16764.4588, -0.6, -1, 0, 0.067, 4.74, 8446.0854, 0, 7, 0, 0.066, 5.86, 24185.7699, -5.6, -46, 0, 0.064, 0.54, 32862.2262, 2.1, 53, 0, 0.063, 2.44, 24394.9729, 3.0, 57, 0, 0.063, 1.77, 5785.1101, 0.6, 14, 0, 0.062, 2.64, 6690.5356, -4.3, -45, 0, 0.062, 2.21, 1151.8292, 2.1, 26, 0, 0.062, 3.94, 34047.8424, 3.9, 75, 0, 0.060, 1.40, 38404.3462, -5.7, -32, 0, 0.058, 0.33, 31048.3080, 0.3, 31, 0, 0.057, 3.11, 9690.0701, -0.7, 0, 0, 0.057, 1.14, 30352.4319, 0.9, 38, 0, 0.056, 6.00, 8504.4538, -2.5, -22, 0, 0.055, 5.47, 18018.7615, 0.8, 25, 0, 0.055, 0.17, -18505.0881, -4.6, -69, 0, 0.055, 0.76, -9129.3422, 1.2, 8, 0, 0.054, 5.70, 7947.1396, -4.4, -44, 0, 0.053, 0.36, 5366.358, -3.7, -37, 0, 0.052, 4.01, -68.726, -1.5, -18, 0, 0.051, 2.74, 31818.585, -3.5, -13, 0, 0.051, 0.98, 16798.206, -2.8, -19, 0, 0.050, 5.94, 8293.747, -0.3, 0, 0, 0.049, 1.52, 15090.215, -4.7, -41, 0, 0.048, 3.46, 39309.425, 2.4, 63, 0, 0.046, 3.21, 23942.463, -1.0, 9, 0, 0.046, 3.35, 7143.070, -0.3, 0, 0, 0.042, 3.76, 46733.038, -4.1, -7, 0, 0.042, 5.18, 8288.351, 0, 7, 0, 0.040, 3.37, 16795.915, 0.6, 21, 0, 0.039, 4.54, -1776.718, -3.4, -40, 0, 0.039, 0.04, 8439.500, -0.3, 0, 0, 0.037, 3.91, 8479.867, -0.3, 0, 0, 0.037, 2.86, 38194.797, -1.3, 19, 0, 0.036, 1.04, 5224.382, 0.1, 7, 0, 0.036, 3.57, 15995.294, 3.4, 54, 0, 0.036, 5.33, 23209.357, 1.2, 35, 0, 0.035, 1.02, 8452.654, -0.3, 0, 0, 0.035, 4.31, 8294.904, 1.8, 29, 0, 0.035, 2.76, 13066.747, -2.2, -12, 0, 0.034, 6.07, 15508.967, -0.4, 10, 0, 0.032, 1.89, -17529.021, 1.6, 0, 0, 0.031, 5.70, 41261.905, 1.7, 56, 0, 0.031, 5.33, 30075.655, -7.2, -57, 0, 0.031, 5.97, -40.340, -1.5, -18, 0, 0.030, 2.87, 6533.950, 0, 7, 0, 0.030, 0.36, 49171.844, -1.1, 30, 0, 0.030, 4.40, 47219.364, -0.4, 37, 0, 0.030, 0.39, 23489.894, -5.0, -38, 0, 0.029, 5.12, 21395.439, -0.6, 13, 0, 0.029, 2.62, 8715.153, -0.3, 0, 0, 0.029, 2.94, 16826.592, -2.8, -19, 0, 0.028, 6.23, 31747.598, -1.6, 9, 0, 0.028, 0.43, 56176.358, 1.1, 62, 0, 0.027, 2.32, 8792.706, -0.3, 0, 0, 0.026, 3.02, 14252.363, -0.3, 10, 0, 0.025, 5.10, 40147.277, -2.0, 12, 0, 0.025, 4.59, 8433.795, 0.9, 25, 0, 0.025, 4.95, 8433.138, -2.2, -26, 0, 0.025, 1.03, -9338.545, -7.4, -95, 0, 0.024, 0.68, 17180.910, 5.2, 76, 0, 0.024, 3.81, 25057.092, 2.7, 53, 0, 0.024, 6.02, 29933.679, -3.4, -13, 0, 0.024, 2.37, -15924.306, -5.2, -76, 0, 0.024, 3.46, 8681.372, 0, 7, 0, 0.023, 2.80, 7108.936, 0, 7, 0, 0.023, 2.17, 7109.640, 0, 7, 0, 0.023, 2.57, -10804.699, -3.0, -44, 0, 0.022, 1.21, 6323.246, 0, 7, 0, 0.022, 3.22, 8259.965, 0, 7, 0, 0.022, 1.97, 7106.987, 1.5, 32, 0, 0.022, 3.00, 7111.589, -1.5, -18, 0, 0.022, 1.22, 14532.900, -6.5, -63, 0, 0.021, 0.66, 5923.672, -1.8, -15, 0, 0.021, 0.69, 24047.208, -3.2, -16, 0, 0.020, 5.51, -26415.027, -1.8, -42, 0, 0.020, 3.70, 16745.237, -2.8, -19, 0, 0.020, 1.26, 7038.300, 1.9, 29, 0, 0.020, 5.78, 6716.267, 0, 7, 0, 0.020, 3.76, 7895.330, 0, 7, 0, 0.019, 0.44, -121.695, -1.5, -18, 0, 0.018, 2.16, 15576.541, -0.9, 0, 0, 0.018, 0.09, -17248.484, -4.6, -68, 0, 0.018, 6.14, -7176.862, 0.6, 0, 0, 0.018, 5.55, 50076.923, 7.0, 125, -1, 0.017, 2.47, 8257.674, 3, 47, 0, 0.017, 4.20, 31609.036, 1, 38, 0, 0.017, 0.50, 175.762, -4, -48, 0, 0.017, 3.20, -2057.255, 3, 33, 0}, - //MB1 - {0.07430, 4.0998, 6480.98618, 0.005, 7, -0.03, 0.03043, 0.872, 7737.59009, -0.05, 7, 0, 0.02229, 5.000, 15019.22707, -2.80, -19, 0.1, 0.01999, 1.072, 23347.91849, -1.28, 6, 0, 0.01869, 1.744, -1847.70525, -1.52, -18, 0.1, 0.01696, 5.597, 16133.8556, 0.91, 24, -0.1, 0.01623, 0.014, 9061.7681, -0.67, 0, 0, 0.01419, 3.942, 733.0767, -2.19, -26, 0.1, 0.01338, 2.370, 17390.4595, 0.86, 25, -0.1, 0.01304, 5.633, 8399.6847, -0.33, 3, 0, 0.01279, 0.886, -523.5272, -2.14, -26, 0.1, 0.01215, 3.242, 7805.1642, -0.61, -1, 0, 0.01088, 3.686, 8435.7674, -2.16, -26, 0.1, 0.01088, 5.853, 8431.1650, 0.88, 25, -0.1, 0.00546, 4.143, 5852.6842, 0, 7, 0, 0.00443, 0.17, 14809.6776, 1.5, 32, 0, 0.00342, 2.24, 8399.7091, -0.3, 3, 0, 0.00330, 1.77, 16275.8310, -2.9, -19, 0, 0.00318, 4.13, 24604.5224, -1.3, 6, 0, 0.00296, 0.90, 7109.2881, 0, 7, 0, 0.00285, 3.43, 31676.6099, 0.2, 31, 0, 0.00207, 3.23, 16066.2815, 1.5, 32, 0, 0.00202, 2.07, 16833.1453, -1.0, 3, 0, 0.00202, 5.10, -33.7871, 0.3, 4, 0, 0.00200, 1.67, 24462.5471, 2.4, 50, 0, 0.00198, 4.80, -591.1013, -1.6, -18, 0, 0.00193, 1.12, 22719.6165, -1.2, 6, 0, 0.00164, 5.67, -10176.397, -3.0, -43, 0, 0.00161, 4.73, 25719.151, 2.4, 50, 0, 0.00158, 5.04, 14390.925, -2.8, -20, 0, 0.00149, 5.86, 13695.049, -2.2, -12, 0, 0.00135, 1.79, -2476.007, -1.5, -18, 0, 0.00121, 1.93, 16759.856, 2.4, 50, 0, 0.00117, 6.04, 16764.459, -0.6, 0, 0, 0.00104, 1.93, 22023.740, -0.7, 13, 0, 0.00085, 2.83, 30561.981, -3.5, -13, 0, 0.00079, 4.81, -8852.219, -3.7, -51, 0, 0.00076, 1.59, -7595.615, -3.7, -51, 0, 0.00075, 2.91, 8433.795, 0.9, 25, 0, 0.00075, 0.35, 8433.138, -2.2, -26, 0, 0.00073, 0.13, 70.993, -1.9, -22, 0, 0.00069, 1.70, 16728.376, 1.2, 28, 0, 0.00068, 0.83, 8365.892, -0.1, 7, 0, 0.00060, 0.20, 32933.214, 0.2, 31, 0, 0.00060, 0.31, 8446.085, 0, 7, 0}, - //MB2 - {0.000220, 4.100, 6480.9862, 0, 7, 0, 0.000101, 5.24, 8435.7674, -2.2, -26, 0, 0.000101, 4.30, 8431.1650, 0.9, 25, 0, 0.000090, 0.87, 7737.5901, 0, 7, 0, 0.000060, 1.07, 23347.9185, -1.3, 6, 0, 0.000060, 5.00, 15019.2271, -2.8, -19, 0, 0.000050, 1.74, -1847.705, -1.5, -18, 0, 0.000050, 5.60, 16133.856, 0.9, 24, 0, 0.000050, 0.01, 9061.768, -0.7, 0, 0, 0.000040, 3.24, 7805.164, -0.6, 0, 0, 0.000040, 3.94, 733.077, -2.2, -26, 0, 0.000040, 0.89, -523.527, -2.1, -26, 0}}, - - { //精度1千米 - //MR0 - {385000.510, 0, 0, 0, 0, 0, 20905.354, 5.4971472, 8328.691424623, 1.522924, 25.0719, -0.12360, 3699.111, 4.8997864, 7214.06286536, -2.184756, -18.860, 0.0828, 2955.967, 0.972156, 15542.75428998, -0.66183, 6.212, -0.0408, 569.925, 1.569516, 16657.3828492, 3.04585, 50.14, -0.2472, 246.158, 5.68582, -1114.6285593, -3.7077, -43.93, 0.206, 204.586, 1.01528, 14914.4523348, -0.6352, 6.15, -0.035, 170.733, 3.32771, 23871.4457146, 0.8611, 31.28, -0.164, 152.138, 4.94291, 6585.7609101, -2.1581, -18.92, 0.088, 129.620, 0.74291, -7700.3894694, -1.5496, -25.01, 0.118, 108.743, 5.19847, 7771.3771450, -0.3309, 3.1, -0.020, 104.755, 2.31243, 8956.993380, 1.4963, 25.1, -0.129, 79.661, 5.38293, -8538.240890, 2.8030, 26.1, -0.118, 48.888, 6.24006, 628.301955, -0.0266, 0.1, -0.005, 34.783, 2.73035, 22756.817155, -2.847, -12.6, 0.04, 30.824, 4.0706, 16171.056245, -0.688, 6.3, -0.05, 24.208, 1.7151, 7842.364821, -2.211, -18.8, 0.08, 23.210, 3.9251, 24986.074274, 4.569, 75.2, -0.37, 21.636, 0.3748, 14428.125731, -4.370, -37.7, 0.17, 16.675, 2.0137, 8399.679100, -0.358, 3.2, -0.03, 14.403, 3.3303, -9443.319984, -5.231, -69.0, 0.33, 12.831, 3.3708, 23243.143759, 0.888, 31.2, -0.16, 11.650, 5.0859, 31085.508580, -1.324, 12, -0.08, 10.445, 5.6833, 32200.13714, 2.384, 56, -0.29, 10.321, 0.8579, -1324.17803, 0.618, 7, -0.03, 10.056, 5.7290, -1742.93051, -3.681, -44, 0.21, 9.884, 1.0584, 14286.15038, -0.609, 6, -0.03, 8.752, 4.7856, -9652.86945, -0.905, -18, 0.09, 8.379, 5.9845, -557.31428, -1.854, -22, 0.10, 7.003, 4.6705, -16029.08089, -3.072, -50, 0.24, 6.322, 1.2708, 16100.06857, 1.192, 28, -0.14, 5.751, 4.6680, 17285.68480, 3.019, 50, -0.25, 4.950, 4.9860, 5957.45895, -2.131, -19, 0.09, 4.421, 4.5969, -209.54947, 4.326, 51, -0.24, 4.131, 3.2135, 7004.51340, 2.141, 32, -0.16, 3.958, 2.7735, 22128.51520, -2.820, -13, 0.05, 3.258, 0.6735, 14985.44001, -2.52, -16, 0.1, 3.148, 0.114, 16866.93231, -1.28, -1, 0, 2.616, 0.143, 24499.74767, 0.83, 31, -0.2, 2.354, 1.672, 8470.66678, -2.24, -19, 0.1, 2.117, 0.700, -7072.08751, -1.58, -25, 0.1, 1.897, 0.418, 13799.82378, -4.34, -38, 0.2, 1.739, 3.629, -8886.00570, -3.38, -47, 0.2, 1.571, 5.129, 30457.20662, -1.30, 12, -0.1, 1.423, 1.158, 39414.20000, 0.20, 37, -0.2, 1.419, 6.171, 23314.13143, -0.99, 9, -0.1, 1.166, 2.269, 9585.29534, 1.47, 25, -0.1, 1.117, 6.281, 33314.76570, 6.09, 100, -0.5, 1.066, 6.197, 1256.60391, -0.05, 0, 0, 1.059, 4.068, 8364.73984, -2.18, -19, 0.1, 0.933, 4.369, 16728.3705, 1.17, 28, -0.1, 0.862, 4.601, 6656.7486, -4.04, -41, 0.2, 0.851, 2.800, 70.9877, -1.88, -22, 0.1, 0.849, 5.726, 31571.8352, 2.41, 56, -0.3, 0.796, 5.084, -9095.5552, 0.95, 4, 0, 0.779, 0.975, -17772.0114, -6.75, -94, 0.5, 0.774, 2.658, 15752.3038, -4.99, -45, 0.2, 0.728, 0.266, 8326.3902, 3.05, 50, -0.2, 0.683, 1.304, 8330.9926, 0, 0, 0, 0.670, 1.756, 40528.8286, 3.91, 81, -0.4, 0.658, 3.414, 22614.8418, 0.91, 31, -0.2, 0.657, 0.901, -1952.4800, 0.64, 7, 0, 0.598, 6.026, 8393.1258, -2.18, -19, 0.1, 0.596, 5.014, 24080.9952, -3.46, -20, 0.1, 0.579, 5.829, 23385.1191, -2.87, -13, 0, 0.514, 4.302, 6099.4343, -5.89, -63, 0.3, 0.508, 1.830, 14218.5763, -0.04, 13, -0.1, 0.498, 5.242, 7143.0752, -0.30, 3, 0, 0.495, 3.373, -10071.6219, -5.20, -69, 0.3, 0.473, 2.430, -17981.5609, -2.43, -43, 0.2, 0.456, 4.887, -8294.9344, -1.83, -29, 0.1, 0.453, 0.173, 8362.4485, 1.21, 21, -0.1, 0.423, 4.489, 29970.8800, -5.03, -32, 0.1, 0.422, 2.315, -24357.7723, -4.60, -75, 0.4, 0.411, 1.102, 13657.8484, -0.58, 6, 0, 0.410, 0.500, 8311.7707, -2.18, -19, 0.1, 0.379, 3.626, 24428.7600, 2.71, 53, 0, 0.355, 0.740, 25614.3762, 4.54, 75, 0, 0.343, 5.772, -2371.2325, -3.7, -44, 0, 0.335, 0.857, 9166.5428, -2.8, -26, 0, 0.332, 0.444, -8257.7037, -3.4, -47, 0, 0.323, 4.829, -10281.1714, -0.9, -18, 0, 0.322, 5.758, 5889.8848, -1.6, -12, 0, 0.287, 0.56, 38299.5714, -3.5, -6, 0, 0.284, 5.57, 15333.2048, 3.7, 57, 0, 0.279, 2.82, 21500.2132, -2.8, -13, 0, 0.256, 0.72, 14357.1381, -2.5, -16, 0, 0.248, 2.20, -7909.9389, 2.8, 26, 0, 0.245, 1.90, 31713.8105, -1.4, 12, 0, 0.237, 3.47, 15056.4277, -4.4, -38, 0, 0.213, 3.77, 15613.7420, -2.5, -16, 0, 0.213, 2.50, 32828.4391, 2.4, 56, 0, 0.209, 3.26, 6376.2114, 2.2, 32, 0, 0.205, 2.93, 14967.4158, -0.7, 6, 0, 0.205, 2.02, 15540.4531, 0.9, 31, 0, 0.204, 3.06, 15545.0555, -2.2, -19, 0, 0.203, 1.20, 38785.8980, 0.2, 37, 0, 0.201, 6.06, 6447.1991, 0.3, 10, 0, 0.186, 6.13, -16238.6304, 1.3, 1, 0, 0.183, 2.13, 21642.1886, -6.6, -57, 0, 0.169, 3.29, -8815.0180, -5.3, -69, 0, 0.167, 1.06, 8328.3391, 1.5, 25, 0, 0.167, 0.51, 8329.0437, 1.5, 25, 0, 0.167, 1.26, 14756.7124, -0.7, 6, 0, 0.158, 0.07, 17495.2343, -1.3, -1, 0, 0.157, 0.57, 6638.7244, -2.2, -19, 0, 0.157, 6.21, 22685.8295, -1.0, 9, 0, 0.148, 5.03, 5329.1570, -2.1, -19, 0, 0.148, 4.03, 16799.3582, -0.7, 6, 0, 0.145, 0.05, 7178.0144, 1.5, 25, 0, 0.144, 5.64, -486.3266, -3.7, -44, 0, 0.139, 3.51, 47742.8914, 1.7, 63, 0, 0.138, 4.07, 7935.6705, 1.5, 25, 0, 0.136, 4.63, -15400.7789, -3.1, -50, 0, 0.136, 3.96, -695.8761, 0.6, 7, 0, 0.135, 5.95, 7211.7617, -0.7, 6, 0, 0.128, 5.17, 29828.9047, -1.3, 12, 0, 0.127, 1.18, 7753.3529, 1.5, 25, 0, 0.127, 0.71, 7216.3641, -3.7, -44, 0, 0.124, 5.83, 15149.7333, -0.7, 6, 0, 0.121, 1.46, 8000.1048, -2.2, -19, 0, 0.120, 3.78, 8721.7124, 1.5, 25, 0, 0.116, 5.19, 6428.0209, -2.2, -19, 0, 0.114, 2.89, -1185.6162, -1.8, -22, 0, 0.112, 2.85, 15542.4020, -0.7, 6, 0, 0.112, 2.23, 15543.1066, -0.7, 6, 0, 0.110, 0.51, 7213.7105, -2.2, -19, 0, 0.110, 6.15, 7214.4152, -2.2, -19, 0, 0.110, 1.31, 15471.7666, 1.2, 28, 0, 0.109, 2.46, 141.9754, -3.8, -44, 0, 0.108, 0.46, 13171.5218, -4.3, -38, 0, 0.108, 6.13, 23942.4334, -1.0, 9, 0, 0.107, 3.15, 15508.9972, -0.4, 10, 0, 0.105, 0.39, 8904.030, 1.5, 25, 0, 0.105, 4.93, 14392.077, -0.7, 6, 0, 0.103, 2.47, 25195.624, 0.2, 24, 0, 0.101, 3.48, 6821.042, -2.2, -19, 0, 0.099, 4.37, 7149.629, 1.5, 25, 0, 0.099, 1.27, -17214.697, -4.9, -72, 0, 0.096, 1.93, 15576.511, -1.0, 0, 0, 0.086, 2.92, 46628.263, -2.0, 19, 0, 0.085, 6.22, 8504.484, -2.5, -22, 0, 0.080, 3.40, -2438.807, -3.1, -37, 0, 0.080, 1.17, 8786.147, -2.2, -19, 0, 0.077, 3.61, 7230.984, 1.5, 25, 0, 0.071, 0.28, 8315.574, -2.2, -19, 0, 0.067, 4.53, 29342.578, -5.0, -32, 0, 0.065, 2.24, 31642.823, 0.5, 34, 0, 0.063, 5.80, 8329.403, 1.5, 25, 0, 0.063, 2.05, 8327.980, 1.5, 25, 0, 0.062, 0.08, 8346.716, -0.3, 0, 0, 0.061, 4.85, 36.048, -3.7, -44, 0, 0.061, 2.58, 6063.386, -2.2, -19, 0, 0.061, 4.30, -766.864, 2.5, 29, 0, 0.060, 3.01, 8322.132, -0.3, 0, 0, 0.059, 0.44, 25057.062, 2.7, 53, 0, 0.059, 0.31, 8288.877, 1.5, 25, 0, 0.059, 2.35, 41643.457, 7.6, 125, -1, 0.059, 1.26, 8368.506, 1.5, 25, 0, 0.058, 1.80, 39900.527, 3.9, 81, 0, 0.058, 1.87, 13590.274, 0, 13, 0, 0.057, 0.47, 14954.262, -0.7, 6, 0, 0.057, 6.20, 8294.910, 1.8, 29, 0, 0.056, 4.63, -8362.473, -1.2, -21, 0, 0.055, 2.86, 8170.957, 1.5, 25, 0, 0.055, 0.03, 7632.815, 2.1, 32, 0, 0.053, 0.80, 7180.306, -1.9, -15, 0, 0.053, 4.64, 6028.447, -4.0, -41, 0, 0.053, 4.57, 15385.020, -0.7, 6, 0, 0.052, 0.60, 37671.269, -3.5, -6, 0, 0.052, 4.99, 8486.426, 1.5, 25, 0, 0.051, 4.62, 17913.987, 3.0, 50, 0, 0.050, 1.64, 837.851, -4.4, -51, 0, 0.049, 5.79, 7542.649, 1.5, 25, 0, 0.049, 2.06, 9114.733, 1.5, 25, 0, 0.049, 2.21, 7056.329, -2.2, -19, 0, 0.049, 4.90, 7214.063, -2.2, -19, 0, 0.048, 5.39, -1671.943, -5.6, -66, 0, 0.047, 4.90, -26100.703, -8.3, -119, 1, 0.047, 1.60, -9024.567, -0.9, -18, 0, 0.046, 1.16, 7161.094, -2.2, -19, 0, 0.046, 5.77, 30943.533, 2.4, 56, 0, 0.046, 2.43, 22199.503, -4.7, -35, 0, 0.046, 3.16, 14991.999, -0.7, 6, 0, 0.044, 4.11, 48857.520, 5.4, 106, -1, 0.044, 4.39, 6625.570, -2.2, -19, 0, 0.044, 6.06, 7789.401, -2.2, -19, 0, 0.043, 0.14, 16693.431, -0.7, 6, 0, 0.043, 4.50, 15020.385, -0.7, 6, 0, 0.043, 4.35, 5471.132, -5.9, -63, 0, 0.043, 4.32, 575.338, 0, 0, 0, 0.043, 5.43, 7267.032, -2.2, -19, 0, 0.043, 3.82, 16328.796, -0.7, 6, 0, 0.042, 2.73, -17424.247, -0.6, -21, 0, 0.041, 3.60, 15489.785, -0.7, 6, 0, 0.040, 2.62, 16655.082, 4.6, 75, 0, 0.040, 4.23, 8351.233, -2.2, -19, 0, 0.039, 0.66, -6443.786, -1.6, -25, 0, 0.039, 2.13, 16118.093, -0.7, 6, 0, 0.039, 5.86, 7247.820, -2.5, -23, 0, 0.038, 4.56, 7285.051, -4.1, -41, 0, 0.038, 2.59, 9179.168, -2.2, -19, 0, 0.038, 1.42, 393.021, 0, 0, 0, 0.038, 4.94, 8381.661, 1.5, 25, 0, 0.037, 5.06, 23452.693, -3.4, -20, 0, 0.037, 5.11, 9027.981, -0.4, 0, 0, 0.037, 4.98, 7740.199, 1.5, 25, 0, 0.037, 3.66, 16659.684, 1.5, 25, 0, 0.037, 2.89, 8275.722, 1.5, 25, 0, 0.037, 4.26, 40042.502, 0.2, 38, 0, 0.036, 1.95, 8326.062, 1.5, 25, 0, 0.036, 5.90, 8331.321, 1.5, 25, 0, 0.035, 1.33, 15595.723, -0.7, 6, 0, 0.035, 1.39, 7777.936, 2, 25, 0, 0.035, 0.80, 6663.308, -2, -19, 0, 0.035, 0.53, 64.434, -4, -44, 0, 0.034, 2.15, 6691.693, -2, -19, 0, 0.034, 1.90, -8467.253, 1, 0, 0, 0.033, 2.83, 7806.322, 2, 25, 0, 0.033, 4.67, 9479.368, 2, 25, 0, 0.033, 1.41, 418.752, 4, 51, 0}, - //MR1 - {0.5139, 4.1569, 14914.452335, -0.635, 6.2, -0.04, 0.3824, 1.8013, 6585.760910, -2.158, -19, 0.09, 0.3265, 2.3987, 7700.38947, 1.550, 25, -0.12, 0.2640, 5.4540, 8956.99338, 1.496, 25, -0.13, 0.1230, 3.0985, 628.30196, -0.027, 0, 0, 0.0775, 0.929, 16171.05625, -0.69, 6, 0, 0.0607, 4.857, 7842.36482, -2.21, -19, 0.1, 0.0497, 4.200, 14286.15038, -0.61, 6, 0, 0.0419, 5.155, 8399.67910, -0.36, 3, 0, 0.0322, 0.229, 23243.1438, 0.89, 31, -0.2, 0.0253, 2.587, -1742.9305, -3.68, -44, 0.2, 0.0249, 1.844, 5957.4590, -2.13, -19, 0.1, 0.0176, 4.754, 16029.0809, 3.07, 50, -0.2, 0.0145, 1.526, 17285.6848, 3.02, 50, -0.3, 0.0137, 1.004, 15542.7543, -0.66, 6, 0, 0.0126, 5.010, 8326.3902, 3.05, 50, 0, 0.0119, 4.814, 8470.6668, -2.24, -19, 0, 0.0118, 2.843, 8330.9926, 0, 0, 0, 0.0107, 2.442, 7072.0875, 1.6, 25, 0, 0.0099, 5.92, 22128.5152, -2.8, -13, 0, 0.0066, 3.28, 24499.7477, 0.8, 31, 0, 0.0065, 4.90, 7214.0629, -2.2, -19, 0, 0.0059, 5.41, 9585.2953, 1.5, 25, 0, 0.0054, 3.06, 1256.6039, -0.1, 0, 0, 0.0052, 2.50, 8328.3391, 1.5, 25, 0, 0.0052, 5.35, 8329.0437, 1.5, 25, 0, 0.0048, 3.56, 13799.8238, -4.3, -38, 0, 0.0039, 1.99, 30457.2066, -1.3, 12, 0, 0.0035, 0.49, 15540.4531, 0.9, 31, 0, 0.0035, 4.60, 15545.0555, -2.2, -19, 0, 0.0033, 0.27, 22614.842, 0.9, 31, 0, 0.0031, 4.24, 13657.848, -0.6, 6, 0, 0.0023, 1.23, 16728.371, 1.2, 28, 0, 0.0023, 5.50, 8328.691, 1.5, 25, 0, 0.0023, 0, 0, 0, 0, 0, 0.0023, 4.41, 7211.762, -0.7, 6, 0, 0.0022, 1.39, 8311.771, -2.2, -19, 0, 0.0022, 2.25, 7216.364, -3.7, -44, 0, 0.0021, 5.94, 70.988, -1.9, -22, 0, 0.0021, 2.58, 31571.835, 2.4, 56, 0, 0.0017, 2.63, -2371.232, -3.7, -44, 0, 0.0016, 4.04, -1952.480, 0.6, 7, 0, 0.0015, 4.23, 8329.403, 1.5, 25, 0, 0.0015, 3.63, 8327.980, 1.5, 25, 0, 0.0015, 2.69, 23385.119, -2.9, -13, 0, 0.0014, 5.96, 21500.213, -2.8, -13, 0, 0.0013, 4.06, 15542.402, -0.7, 6, 0, 0.0013, 1.02, 15543.107, -0.7, 6, 0, 0.0013, 2.10, 7143.075, -0.3, 0, 0, 0.0012, 0.23, -10071.622, -5.2, -69, 0, 0.0011, 3.33, 23871.446, 1, 31, 0, 0.0011, 1.89, 5329.157, -2, -19, 0}, - //MR2 - {0.001490, 4.157, 14914.45233, -0.64, 6, 0, 0.001110, 1.801, 6585.7609, -2.16, -19, 0.1, 0.000950, 2.399, 7700.3895, 1.55, 25, -0.1, 0.000770, 5.454, 8956.9934, 1.50, 25, -0.1, 0.000360, 3.098, 628.3020, 0, 0, 0, 0.000230, 0.93, 16171.0562, -0.7, 6, 0, 0.000180, 4.86, 7842.3648, -2.2, -19, 0, 0.000140, 4.20, 14286.1504, -0.6, 6, 0, 0.000120, 5.16, 8399.6791, -0.4, 0, 0, 0.000116, 3.46, 8326.390, 3.0, 50, 0, 0.000109, 4.39, 8330.993, 0, 0, 0, 0.000090, 0.23, 23243.144, 0.9, 31, 0}}} - return XL1 + result := make([][][]float64, len(moonCir)) + for coordinateIndex, coordinate := range moonCir { + result[coordinateIndex] = make([][]float64, len(coordinate)) + for seriesIndex, series := range coordinate { + result[coordinateIndex][seriesIndex] = append([]float64(nil), series...) + } + } + return result } func MoonCalcNew(coordIndex int, jd float64) float64 { @@ -44,8 +56,7 @@ func MoonCalcNew(coordIndex int, jd float64) float64 { func MoonCalcNewN(coordIndex int, jd float64, n int) float64 { rad := 180.0 * 3600.0 / math.Pi t := (jd - 2451545.0) / 36525.0 - XL1 := GetMoonCir() - ob := XL1[coordIndex] + ob := moonCir[coordIndex] var v float64 var tn float64 = 1 t2 := t * t diff --git a/basic/moon_rise_set_external_test.go b/basic/moon_rise_set_external_test.go new file mode 100644 index 0000000..10c08ea --- /dev/null +++ b/basic/moon_rise_set_external_test.go @@ -0,0 +1,325 @@ +package basic + +import ( + "encoding/json" + "fmt" + "math" + "os" + "testing" + "time" + + "b612.me/astro/tools" +) + +type moonRiseSetExternalEvents struct { + RiseUTC string `json:"rise_utc"` + SetUTC string `json:"set_utc"` +} + +type moonRiseSetExternalSample struct { + Site string `json:"site"` + DateUTC string `json:"date_utc"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ObserverHeight float64 `json:"observer_height_m"` + Horizons moonRiseSetExternalEvents `json:"jpl_horizons"` + METNorway moonRiseSetExternalEvents `json:"met_norway"` + IMCCEMiriade moonRiseSetExternalEvents `json:"imcce_miriade"` +} + +type moonRiseSetExternalBaseline struct { + SchemaVersion int `json:"schema_version"` + Sources map[string]struct { + Provider string `json:"provider"` + Model string `json:"model"` + } `json:"sources"` + Samples []moonRiseSetExternalSample `json:"samples"` +} + +type moonRiseSetErrorStats struct { + Total time.Duration + Max time.Duration + Count int +} + +func (stats *moonRiseSetErrorStats) Add(value time.Duration) { + stats.Total += value + stats.Count++ + if value > stats.Max { + stats.Max = value + } +} + +func (stats moonRiseSetErrorStats) Mean() time.Duration { + if stats.Count == 0 { + return 0 + } + return stats.Total / time.Duration(stats.Count) +} + +type moonRiseSetExternalTolerances struct { + Horizons time.Duration + METNorway time.Duration + IMCCEMiriade time.Duration + HorizonsVsMET time.Duration +} + +type moonRiseSetComparisonStats struct { + CurrentHorizons moonRiseSetErrorStats + LegacyHorizons moonRiseSetErrorStats + CurrentMET moonRiseSetErrorStats + LegacyMET moonRiseSetErrorStats + CurrentIMCCE moonRiseSetErrorStats + LegacyIMCCE moonRiseSetErrorStats + HorizonsVsMET moonRiseSetErrorStats + HorizonsVsIMCCE moonRiseSetErrorStats + CurrentCloserJPL int + LegacyCloserJPL int + TiesJPL int + CurrentCloserMET int + LegacyCloserMET int + TiesMET int + CurrentCloserIMCCE int + LegacyCloserIMCCE int + TiesIMCCE int +} + +func TestMoonRiseSetMatchesExternalBaselines(t *testing.T) { + previousDeltaT := defDeltaTFn + SetDeltaTFn(DefaultDeltaTv2) + defer SetDeltaTFn(previousDeltaT) + + baseline := loadMoonRiseSetExternalBaseline(t) + if baseline.SchemaVersion != 1 { + t.Fatalf("unsupported baseline schema version %d", baseline.SchemaVersion) + } + if baseline.Sources["jpl_horizons"].Model != "DE441" { + t.Fatalf("unexpected Horizons model %q", baseline.Sources["jpl_horizons"].Model) + } + if len(baseline.Samples) < 7 { + t.Fatalf("external baseline has only %d samples", len(baseline.Samples)) + } + + tolerances := moonRiseSetExternalTolerances{ + Horizons: 2 * time.Second, + METNorway: 90 * time.Second, + IMCCEMiriade: 8 * time.Minute, + HorizonsVsMET: 90 * time.Second, + } + var stats moonRiseSetComparisonStats + + for _, sample := range baseline.Samples { + day, err := time.Parse("2006-01-02", sample.DateUTC) + if err != nil { + t.Fatalf("parse %s date %q: %v", sample.Site, sample.DateUTC, err) + } + jd := Date2JDE(day) + currentRiseJD, err := GetMoonRiseTime(jd, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight) + if err != nil { + t.Fatalf("%s current moonrise: %v", sample.Site, err) + } + currentSetJD, err := GetMoonSetTime(jd, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight) + if err != nil { + t.Fatalf("%s current moonset: %v", sample.Site, err) + } + legacyRiseJD, err := legacyMoonRiseSetFromCurrent(currentRiseJD, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight) + if err != nil { + t.Fatalf("%s legacy moonrise: %v", sample.Site, err) + } + legacySetJD, err := legacyMoonRiseSetFromCurrent(currentSetJD, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight) + if err != nil { + t.Fatalf("%s legacy moonset: %v", sample.Site, err) + } + + compareMoonRiseSetEvent(t, sample.Site+".rise", currentRiseJD, legacyRiseJD, + sample.Horizons.RiseUTC, sample.METNorway.RiseUTC, sample.IMCCEMiriade.RiseUTC, + tolerances, &stats) + compareMoonRiseSetEvent(t, sample.Site+".set", currentSetJD, legacySetJD, + sample.Horizons.SetUTC, sample.METNorway.SetUTC, sample.IMCCEMiriade.SetUTC, + tolerances, &stats) + } + + t.Logf("moon rise/set external baseline: current vs JPL mean=%v max=%v; legacy vs JPL mean=%v max=%v", + stats.CurrentHorizons.Mean(), stats.CurrentHorizons.Max, stats.LegacyHorizons.Mean(), stats.LegacyHorizons.Max) + t.Logf("moon rise/set external baseline: current vs MET mean=%v max=%v; legacy vs MET mean=%v max=%v", + stats.CurrentMET.Mean(), stats.CurrentMET.Max, stats.LegacyMET.Mean(), stats.LegacyMET.Max) + t.Logf("moon rise/set external baseline: current vs IMCCE mean=%v max=%v; legacy vs IMCCE mean=%v max=%v", + stats.CurrentIMCCE.Mean(), stats.CurrentIMCCE.Max, stats.LegacyIMCCE.Mean(), stats.LegacyIMCCE.Max) + t.Logf("moon rise/set external baseline: JPL vs MET mean=%v max=%v; JPL vs IMCCE mean=%v max=%v", + stats.HorizonsVsMET.Mean(), stats.HorizonsVsMET.Max, stats.HorizonsVsIMCCE.Mean(), stats.HorizonsVsIMCCE.Max) + t.Logf("moon rise/set external baseline: JPL current closer=%d legacy closer=%d ties=%d", + stats.CurrentCloserJPL, stats.LegacyCloserJPL, stats.TiesJPL) + t.Logf("moon rise/set external baseline: MET current closer=%d legacy closer=%d ties=%d", + stats.CurrentCloserMET, stats.LegacyCloserMET, stats.TiesMET) + t.Logf("moon rise/set external baseline: IMCCE current closer=%d legacy closer=%d ties=%d", + stats.CurrentCloserIMCCE, stats.LegacyCloserIMCCE, stats.TiesIMCCE) +} + +func TestMoonRiseSetLegacyComparatorMatchesPreFixSnapshot(t *testing.T) { + previousDeltaT := defDeltaTFn + SetDeltaTFn(DefaultDeltaTv2) + defer SetDeltaTFn(previousDeltaT) + + jd := JDECalc(2023, 1, 15) + currentRise, err := GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0) + if err != nil { + t.Fatalf("current moonrise: %v", err) + } + currentSet, err := GetMoonSetTime(jd, 116.4074, 39.9042, 8, 1, 0) + if err != nil { + t.Fatalf("current moonset: %v", err) + } + legacyRise, err := legacyMoonRiseSetFromCurrent(currentRise, 116.4074, 39.9042, 8, 1, 0) + if err != nil { + t.Fatalf("legacy moonrise: %v", err) + } + legacySet, err := legacyMoonRiseSetFromCurrent(currentSet, 116.4074, 39.9042, 8, 1, 0) + if err != nil { + t.Fatalf("legacy moonset: %v", err) + } + + const snapshotTolerance = 2.0 / 86400 + if difference := math.Abs(legacyRise - 2459959.509182); difference > snapshotTolerance { + t.Errorf("legacy moonrise snapshot mismatch: got %.9f want %.9f difference=%.3fs", + legacyRise, 2459959.509182, difference*86400) + } + if difference := math.Abs(legacySet - 2459959.988676); difference > snapshotTolerance { + t.Errorf("legacy moonset snapshot mismatch: got %.9f want %.9f difference=%.3fs", + legacySet, 2459959.988676, difference*86400) + } +} + +func loadMoonRiseSetExternalBaseline(t *testing.T) moonRiseSetExternalBaseline { + t.Helper() + data, err := os.ReadFile("testdata/moon_rise_set_baseline.json") + if err != nil { + t.Fatalf("read moon rise/set baseline: %v", err) + } + var baseline moonRiseSetExternalBaseline + if err := json.Unmarshal(data, &baseline); err != nil { + t.Fatalf("decode moon rise/set baseline: %v", err) + } + return baseline +} + +func compareMoonRiseSetEvent(t *testing.T, name string, currentJD, legacyJD float64, + horizonsUTC, metUTC, imcceUTC string, tolerances moonRiseSetExternalTolerances, + stats *moonRiseSetComparisonStats) { + t.Helper() + current := JDE2DateByZone(currentJD, time.UTC, false) + legacy := JDE2DateByZone(legacyJD, time.UTC, false) + horizons := parseMoonRiseSetExternalTime(t, name+".jpl", horizonsUTC) + met := parseMoonRiseSetExternalTime(t, name+".met", metUTC) + imcce := parseMoonRiseSetExternalTime(t, name+".imcce", imcceUTC) + currentHorizonsError := absoluteTimeDifference(current, horizons) + legacyHorizonsError := absoluteTimeDifference(legacy, horizons) + currentMETError := absoluteTimeDifference(current, met) + legacyMETError := absoluteTimeDifference(legacy, met) + currentIMCCEError := absoluteTimeDifference(current, imcce) + legacyIMCCEError := absoluteTimeDifference(legacy, imcce) + stats.CurrentHorizons.Add(currentHorizonsError) + stats.LegacyHorizons.Add(legacyHorizonsError) + stats.CurrentMET.Add(currentMETError) + stats.LegacyMET.Add(legacyMETError) + stats.CurrentIMCCE.Add(currentIMCCEError) + stats.LegacyIMCCE.Add(legacyIMCCEError) + horizonsVsMET := absoluteTimeDifference(horizons, met) + stats.HorizonsVsMET.Add(horizonsVsMET) + stats.HorizonsVsIMCCE.Add(absoluteTimeDifference(horizons, imcce)) + + if currentHorizonsError > tolerances.Horizons { + t.Errorf("%s current mismatch against JPL: got %s want %s difference=%v tolerance=%v", + name, current.Format(time.RFC3339Nano), horizonsUTC, currentHorizonsError, tolerances.Horizons) + } + if currentMETError > tolerances.METNorway { + t.Errorf("%s current mismatch against MET Norway: got %s want %s difference=%v tolerance=%v", + name, current.Format(time.RFC3339Nano), metUTC, currentMETError, tolerances.METNorway) + } + if currentIMCCEError > tolerances.IMCCEMiriade { + t.Errorf("%s current mismatch against IMCCE Miriade: got %s want %s difference=%v tolerance=%v", + name, current.Format(time.RFC3339Nano), imcceUTC, currentIMCCEError, tolerances.IMCCEMiriade) + } + if horizonsVsMET > tolerances.HorizonsVsMET { + t.Errorf("%s external sources disagree: JPL=%s MET=%s difference=%v tolerance=%v", + name, horizonsUTC, metUTC, horizonsVsMET, tolerances.HorizonsVsMET) + } + switch { + case currentHorizonsError < legacyHorizonsError: + stats.CurrentCloserJPL++ + case legacyHorizonsError < currentHorizonsError: + stats.LegacyCloserJPL++ + default: + stats.TiesJPL++ + } + switch { + case currentMETError < legacyMETError: + stats.CurrentCloserMET++ + case legacyMETError < currentMETError: + stats.LegacyCloserMET++ + default: + stats.TiesMET++ + } + switch { + case currentIMCCEError < legacyIMCCEError: + stats.CurrentCloserIMCCE++ + case legacyIMCCEError < currentIMCCEError: + stats.LegacyCloserIMCCE++ + default: + stats.TiesIMCCE++ + } + t.Logf("%s current_jpl=%v legacy_jpl=%v current_met=%v legacy_met=%v current_imcce=%v legacy_imcce=%v", name, + currentHorizonsError, legacyHorizonsError, currentMETError, legacyMETError, currentIMCCEError, legacyIMCCEError) +} + +func parseMoonRiseSetExternalTime(t *testing.T, name, value string) time.Time { + t.Helper() + parsed, err := time.Parse(time.RFC3339, value) + if err != nil { + t.Fatalf("parse %s time %q: %v", name, value, err) + } + return parsed +} + +func absoluteTimeDifference(left, right time.Time) time.Duration { + difference := left.Sub(right) + if difference < 0 { + return -difference + } + return difference +} + +func legacyMoonRiseSetFromCurrent(currentJD, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) { + localTimeZone := longitude / 15 + localJD := currentJD + localTimeZone/24 - timeZone/24 + targetAltitude := StandardAltitudeMoon(zenithShift, height, latitude) + legacyJD := moonRiseSetNewtonRaphsonIteration(localJD, longitude, latitude, localTimeZone, + targetAltitude, legacyHMoonHeight, 0.00002) + if math.IsNaN(legacyJD) || math.IsInf(legacyJD, 0) { + return 0, fmt.Errorf("legacy height iteration did not converge") + } + return legacyJD - localTimeZone/24 + timeZone/24, nil +} + +func legacyHMoonHeight(jd, longitude, latitude, timeZone float64) float64 { + calculationJD := TD2UT(jd-timeZone/24, true) + ra, dec := HMoonTrueRaDecN(calculationJD, -1) + distanceAU := HMoonAwayN(calculationJD, -1) / 149597870.7 + topocentricRA, topocentricDec := legacyTopocentricRaDec(ra, dec, latitude, longitude, calculationJD, distanceAU, 0) + siderealTime := tools.Limit360(ApparentSiderealTime(jd-timeZone/24)*15 + longitude) + hourAngle := tools.Limit360(siderealTime - topocentricRA) + altitudeSine := tools.Sin(latitude)*tools.Sin(topocentricDec) + + tools.Cos(topocentricDec)*tools.Cos(latitude)*tools.Cos(hourAngle) + return tools.ArcSin(altitudeSine) +} + +func legacyTopocentricRaDec(ra, dec, latitude, longitude, jd, distanceAU, height float64) (float64, float64) { + horizontalParallaxSine := tools.Sin(0.0024427777777) / distanceAU + observerCosine := pcosi(latitude, height) + observerSine := psini(latitude, height) + hourAngle := tools.Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + longitude - ra) + raCorrection := math.Atan2(-observerCosine*horizontalParallaxSine*tools.Sin(hourAngle), + tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi + correctedDec := math.Atan2((tools.Sin(dec)-observerSine*horizontalParallaxSine)*tools.Cos(raCorrection), + tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi + return ra + raCorrection, correctedDec +} diff --git a/basic/moon_test.go b/basic/moon_test.go index 8ae9fc0..bfac47f 100644 --- a/basic/moon_test.go +++ b/basic/moon_test.go @@ -27,524 +27,525 @@ type MoonRiseSetTestCase struct { ExpectedSet float64 } -// moonRiseSetTestData 月出月落测试数据 +// moonRiseSetTestData 月出月落测试数据。ZenithShift=1 的数值来自旧的固定 +// -0.83333 度口径,仅用于防止事件跳到相邻周期;动态折射精度由外部基线测试约束。 var moonRiseSetTestData = []MoonRiseSetTestCase{ - {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 0, 0, 2459959.512382, 2459959.985558}, - {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 0, 100, 2459959.511153, 2459959.986753}, - {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 0, 1000, 2459959.508489, 2459959.989349}, - {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 1, 0, 2459959.509182, 2459959.988676}, - {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 1, 100, 2459959.507955, 2459959.989868}, - {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 1, 1000, 2459959.505295, 2459959.992458}, - {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 0, 0, 2460023.743390, 2460024.191155}, - {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 0, 100, 2460023.742131, 2460024.192411}, - {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460023.739404, 2460024.195141}, - {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 1, 0, 2460023.740112, 2460024.194436}, - {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 1, 100, 2460023.738858, 2460024.195688}, - {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460023.736141, 2460024.198409}, - {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 0, 0, 2460116.797325, 2460117.432318}, - {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 0, 100, 2460116.795884, 2460117.433679}, - {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460116.792739, 2460117.436652}, - {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 1, 0, 2460116.793560, 2460117.435878}, - {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 1, 100, 2460116.792105, 2460117.437250}, - {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460116.788927, 2460117.440248}, - {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 0, 0, 2460211.101507, 2460211.459840}, - {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 0, 100, 2460211.099949, 2460211.461381}, - {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460211.096593, 2460211.464710}, - {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 1, 0, 2460211.097463, 2460211.463851}, - {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 1, 100, 2460211.095924, 2460211.465374}, - {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460211.092607, 2460211.468664}, - {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 0, 0, 2460301.062528, 2460300.594160}, - {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 0, 100, 2460301.061295, 2460300.595393}, - {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460301.058613, 2460300.598084}, - {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 1, 0, 2460301.059311, 2460300.597387}, - {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 1, 100, 2460301.058074, 2460300.598625}, - {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460301.055382, 2460300.601325}, - {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 0, 0, 2460370.449673, 2460369.861269}, - {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 0, 100, 2460370.448375, 2460369.862488}, - {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460370.445568, 2460369.865132}, - {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 1, 0, 2460370.446299, 2460369.864446}, - {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 1, 100, 2460370.445007, 2460369.865661}, - {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460370.442214, 2460369.868297}, - {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 0, 0, 2460861.080905, 2460860.500448}, - {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 0, 100, 2460861.079623, 2460860.501655}, - {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460861.076853, 2460860.504274}, - {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 1, 0, 2460861.077574, 2460860.503595}, - {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 1, 100, 2460861.076299, 2460860.504799}, - {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460861.073540, 2460860.507411}, - {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 0, 0, 2459959.528073, 2459959.967571}, - {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 0, 100, 2459959.526515, 2459959.969080}, - {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 0, 1000, 2459959.523150, 2459959.972345}, - {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 1, 0, 2459959.524029, 2459959.971495}, - {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 1, 100, 2459959.522479, 2459959.972996}, - {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 1, 1000, 2459959.519128, 2459959.976244}, - {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 0, 0, 2460023.756367, 2460024.186083}, - {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 0, 100, 2460023.754816, 2460024.187653}, - {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460023.751465, 2460024.191054}, - {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 1, 0, 2460023.752338, 2460024.190171}, - {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 1, 100, 2460023.750797, 2460024.191733}, - {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460023.747466, 2460024.195116}, - {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 0, 0, 2460116.767473, 2460117.458902}, - {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 0, 100, 2460116.765511, 2460117.460678}, - {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460116.761206, 2460117.464572}, - {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 1, 0, 2460116.762340, 2460117.463551}, - {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 1, 100, 2460116.760335, 2460117.465357}, - {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460116.755931, 2460117.469316}, - {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 0, 0, 2460211.152489, 2460211.418569}, - {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 0, 100, 2460211.150147, 2460211.420895}, - {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460211.145172, 2460211.425846}, - {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 1, 0, 2460211.146459, 2460211.424570}, - {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 1, 100, 2460211.144192, 2460211.426821}, - {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460211.139369, 2460211.431620}, - {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 0, 0, 2460301.041592, 2460300.614867}, - {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 0, 100, 2460301.040020, 2460300.616452}, - {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460301.036594, 2460300.619912}, - {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 1, 0, 2460301.037491, 2460300.619011}, - {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 1, 100, 2460301.035906, 2460300.620607}, - {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460301.032451, 2460300.624095}, - {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 0, 0, 2460370.477943, 2460369.838418}, - {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 0, 100, 2460370.476216, 2460369.839982}, - {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460370.472503, 2460369.843360}, - {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 1, 0, 2460370.473471, 2460369.842481}, - {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 1, 100, 2460370.471764, 2460369.844033}, - {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460370.468092, 2460369.847386}, - {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 0, 0, 2460861.106361, 2460861.487905}, - {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 0, 100, 2460861.104676, 2460861.489558}, - {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460861.101051, 2460861.493119}, - {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 1, 0, 2460861.101998, 2460861.492193}, - {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 1, 100, 2460861.100330, 2460861.493828}, - {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460861.096740, 2460861.497354}, - {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 0, 0, 2459959.524199, 2459959.980835}, - {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 0, 100, 2459959.522935, 2459959.982068}, - {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 0, 1000, 2459959.520198, 2459959.984744}, - {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 1, 0, 2459959.520911, 2459959.984050}, - {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 1, 100, 2459959.519651, 2459959.985280}, - {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 1, 1000, 2459959.516921, 2459959.987947}, - {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 0, 0, 2460023.742085, 2460024.205917}, - {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 0, 100, 2460023.740843, 2460024.207164}, - {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460023.738150, 2460024.209876}, - {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 1, 0, 2460023.738849, 2460024.209175}, - {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 1, 100, 2460023.737610, 2460024.210419}, - {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460023.734925, 2460024.213124}, - {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 0, 0, 2460116.805442, 2460117.432629}, - {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 0, 100, 2460116.804010, 2460117.433973}, - {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460116.800889, 2460117.436908}, - {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 1, 0, 2460116.801705, 2460117.436144}, - {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 1, 100, 2460116.800260, 2460117.437498}, - {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460116.797108, 2460117.440456}, - {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 0, 0, 2460211.112450, 2460211.469949}, - {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 0, 100, 2460211.110879, 2460211.471502}, - {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460211.107495, 2460211.474858}, - {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 1, 0, 2460211.108372, 2460211.473992}, - {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 1, 100, 2460211.106820, 2460211.475527}, - {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460211.103476, 2460211.478843}, - {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 0, 0, 2460301.058738, 2460300.608360}, - {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 0, 100, 2460301.057459, 2460300.609636}, - {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460301.054673, 2460300.612421}, - {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 1, 0, 2460301.055399, 2460300.611699}, - {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 1, 100, 2460301.054113, 2460300.612981}, - {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460301.051313, 2460300.615779}, - {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 0, 0, 2460370.461773, 2460369.855646}, - {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 0, 100, 2460370.460419, 2460369.856909}, - {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460370.457495, 2460369.859648}, - {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 1, 0, 2460370.458256, 2460369.858938}, - {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 1, 100, 2460370.456911, 2460369.860196}, - {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460370.454004, 2460369.862924}, - {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 0, 0, 2460861.092013, -3.000000}, - {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 0, 100, 2460861.090681, -3.000000}, - {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460861.087804, -3.000000}, - {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 1, 0, 2460861.088553, -3.000000}, - {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 1, 100, 2460861.087229, -3.000000}, - {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460861.084368, 2460860.501759}, - {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 0, 0, 2459960.490771, 2459960.006541}, - {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 0, 100, 2459960.489647, 2459960.007661}, - {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 0, 1000, 2459960.487201, 2459960.010104}, - {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 1, 0, 2459960.487836, 2459960.009472}, - {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 1, 100, 2459960.486710, 2459960.010595}, - {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 1, 1000, 2459960.484259, 2459960.013044}, - {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 0, 0, 2460023.660650, 2460024.225595}, - {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 0, 100, 2460023.659463, 2460024.226711}, - {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460023.656880, 2460024.229146}, - {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 1, 0, 2460023.657552, 2460024.228516}, - {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 1, 100, 2460023.656361, 2460024.229635}, - {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460023.653768, 2460024.232076}, - {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 0, 0, 2460116.898383, 2460117.306845}, - {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 0, 100, 2460116.897095, 2460117.308116}, - {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460116.894309, 2460117.310872}, - {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 1, 0, 2460116.895031, 2460117.310161}, - {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 1, 100, 2460116.893751, 2460117.311424}, - {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460116.890981, 2460117.314164}, - {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 0, 0, 2460210.944935, 2460210.538063}, - {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 0, 100, 2460210.943584, 2460210.539396}, - {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460210.940634, 2460210.542315}, - {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 1, 0, 2460210.941402, 2460210.541559}, - {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 1, 100, 2460210.940040, 2460210.542904}, - {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460210.937065, 2460210.545848}, - {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 0, 0, 2460301.101438, 2460300.536189}, - {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 0, 100, 2460301.100269, 2460300.537293}, - {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460301.097738, 2460300.539695}, - {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 1, 0, 2460301.098395, 2460300.539074}, - {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 1, 100, 2460301.097230, 2460300.540177}, - {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460301.094705, 2460300.542575}, - {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 0, 0, 2460370.367522, 2460369.898187}, - {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 0, 100, 2460370.366378, 2460369.899322}, - {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460370.363890, 2460369.901801}, - {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 1, 0, 2460370.364537, 2460369.901160}, - {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 1, 100, 2460370.363390, 2460369.902299}, - {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460370.360894, 2460369.904785}, - {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 0, 0, 2460861.005584, 2460860.530894}, - {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 0, 100, 2460861.004451, 2460860.532021}, - {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460861.001987, 2460860.534479}, - {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 1, 0, 2460861.002627, 2460860.533844}, - {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 1, 100, 2460861.001492, 2460860.534973}, - {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460860.999020, 2460860.537439}, - {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 0, 0, -3.000000, 2459959.964633}, - {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 0, 100, -3.000000, 2459959.965758}, - {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 0, 1000, -3.000000, 2459959.968203}, - {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 1, 0, -3.000000, 2459959.967570}, - {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 1, 100, -3.000000, 2459959.968693}, - {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 1, 1000, -3.000000, 2459959.971135}, - {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 0, 0, 2460023.713764, 2460024.169244}, - {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 0, 100, 2460023.712577, 2460024.170423}, - {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460023.710003, 2460024.172990}, - {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 1, 0, 2460023.710670, 2460024.172328}, - {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 1, 100, 2460023.709487, 2460024.173504}, - {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460023.706921, 2460024.176064}, - {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 0, 0, 2460116.781251, 2460117.399403}, - {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 0, 100, 2460116.779918, 2460117.400670}, - {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460116.777011, 2460117.403439}, - {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 1, 0, 2460116.777769, 2460117.402720}, - {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 1, 100, 2460116.776425, 2460117.403996}, - {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460116.773495, 2460117.406783}, - {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 0, 0, 2460211.063872, 2460211.445971}, - {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 0, 100, 2460211.062451, 2460211.447375}, - {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460211.059383, 2460211.450416}, - {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 1, 0, 2460211.060178, 2460211.449632}, - {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 1, 100, 2460211.058770, 2460211.451024}, - {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460211.055729, 2460211.454038}, - {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 0, 0, 2460301.042488, 2460300.564993}, - {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 0, 100, 2460301.041330, 2460300.566149}, - {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460301.038808, 2460300.568671}, - {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 1, 0, 2460301.039464, 2460300.568019}, - {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 1, 100, 2460301.038302, 2460300.569178}, - {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460301.035773, 2460300.571707}, - {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 0, 0, 2460370.418418, 2460369.841663}, - {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 0, 100, 2460370.417208, 2460369.842807}, - {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460370.414589, 2460369.845291}, - {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 1, 0, 2460370.415270, 2460369.844648}, - {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 1, 100, 2460370.414065, 2460369.845789}, - {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460370.411455, 2460369.848267}, - {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 0, 0, 2460861.050307, 2460861.497476}, - {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 0, 100, 2460861.049110, 2460861.498651}, - {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460861.046520, -3.000000}, - {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 1, 0, 2460861.047194, -3.000000}, - {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 1, 100, 2460861.046002, -3.000000}, - {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460861.043420, -3.000000}, - {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 0, 0, 2459959.559564, 2459960.005972}, - {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 0, 100, 2459959.558099, 2459960.007394}, - {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 0, 1000, 2459959.554933, 2459960.010471}, - {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 1, 0, 2459959.555759, 2459960.009670}, - {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 1, 100, 2459959.554300, 2459960.011086}, - {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 1, 1000, 2459959.551145, 2459960.014149}, - {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 0, 0, 2460023.786730, 2460024.223914}, - {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 0, 100, 2460023.785269, 2460024.225388}, - {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460023.782112, 2460024.228584}, - {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 1, 0, 2460023.782934, 2460024.227755}, - {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 1, 100, 2460023.781482, 2460024.229222}, - {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460023.778340, 2460024.232405}, - {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 0, 0, 2460116.811447, 2460117.485600}, - {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 0, 100, 2460116.809659, 2460117.487241}, - {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460116.805742, 2460117.490834}, - {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 1, 0, 2460116.806771, 2460117.489894}, - {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 1, 100, 2460116.804952, 2460117.491558}, - {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460116.800964, 2460117.495201}, - {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 0, 0, 2460211.173246, 2460211.466625}, - {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 0, 100, 2460211.171184, 2460211.468671}, - {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460211.166780, 2460211.473050}, - {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 1, 0, 2460211.167921, 2460211.471920}, - {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 1, 100, 2460211.165908, 2460211.473916}, - {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460211.161606, 2460211.478194}, - {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 0, 0, 2460301.080943, 2460300.645364}, - {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 0, 100, 2460301.079466, 2460300.646849}, - {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460301.076249, 2460300.650092}, - {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 1, 0, 2460301.077090, 2460300.649248}, - {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 1, 100, 2460301.075603, 2460300.650743}, - {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460301.072363, 2460300.654007}, - {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 0, 0, -3.000000, 2460369.878053}, - {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 0, 100, -3.000000, 2460369.879520}, - {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 0, 1000, -3.000000, 2460369.882691}, - {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 1, 0, -3.000000, 2460369.881866}, - {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 1, 100, -3.000000, 2460369.883323}, - {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460370.496709, 2460369.886475}, - {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 0, 0, 2460861.134958, 2460860.518461}, - {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 0, 100, 2460861.133389, 2460860.519905}, - {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460861.130011, 2460860.523028}, - {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 1, 0, 2460861.130892, 2460860.522216}, - {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 1, 100, 2460861.129337, 2460860.523652}, - {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460861.125986, 2460860.526759}, - {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 0, 0, 2459959.522069, 2459959.987849}, - {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 0, 100, 2459959.520918, 2459959.988976}, - {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 0, 1000, 2459959.518423, 2459959.991425}, - {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 1, 0, 2459959.519071, 2459959.990791}, - {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 1, 100, 2459959.517923, 2459959.991916}, - {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 1, 1000, 2459959.515433, 2459959.994359}, - {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 0, 0, 2460023.736567, 2460024.214432}, - {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 0, 100, 2460023.735438, 2460024.215558}, - {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460023.732989, 2460024.218013}, - {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 1, 0, 2460023.733624, 2460024.217380}, - {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 1, 100, 2460023.732497, 2460024.218505}, - {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460023.730052, 2460024.220957}, - {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 0, 0, 2460116.821912, 2460117.421511}, - {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 0, 100, 2460116.820648, 2460117.422709}, - {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460116.817895, 2460117.425326}, - {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 1, 0, 2460116.818612, 2460117.424647}, - {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 1, 100, 2460116.817341, 2460117.425852}, - {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460116.814570, 2460117.428482}, - {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 0, 0, 2460211.096832, 2460211.491736}, - {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 0, 100, 2460211.095470, 2460211.493079}, - {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460211.092526, 2460211.495992}, - {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 1, 0, 2460211.093289, 2460211.495241}, - {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 1, 100, 2460211.091937, 2460211.496574}, - {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460211.089016, 2460211.499464}, - {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 0, 0, 2460301.067741, 2460300.604932}, - {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 0, 100, 2460301.066573, 2460300.606090}, - {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460301.064029, 2460300.608620}, - {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 1, 0, 2460301.064690, 2460300.607966}, - {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 1, 100, 2460301.063518, 2460300.609128}, - {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460301.060965, 2460300.611667}, - {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 0, 0, 2460370.454262, 2460369.864349}, - {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 0, 100, 2460370.453043, 2460369.865499}, - {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460370.450405, 2460369.867997}, - {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 1, 0, 2460370.451090, 2460369.867351}, - {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 1, 100, 2460370.449876, 2460369.868497}, - {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460370.447250, 2460369.870987}, - {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 0, 0, 2460861.085220, 2460860.502309}, - {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 0, 100, 2460861.084017, 2460860.503446}, - {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460861.081416, 2460860.505916}, - {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 1, 0, 2460861.082092, 2460860.505277}, - {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 1, 100, 2460861.080895, 2460860.506411}, - {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460861.078303, 2460860.508875}, - {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 0, 0, 2459959.547825, 2459959.980183}, - {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 0, 100, 2459959.546091, 2459959.981857}, - {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 0, 1000, 2459959.542352, 2459959.985470}, - {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 1, 0, 2459959.543330, 2459959.984528}, - {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 1, 100, 2459959.541607, 2459959.986191}, - {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 1, 1000, 2459959.537887, 2459959.989782}, - {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 0, 0, 2460023.783999, 2460024.192547}, - {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 0, 100, 2460023.782247, 2460024.194326}, - {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460023.778469, 2460024.198172}, - {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 1, 0, 2460023.779453, 2460024.197173}, - {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 1, 100, 2460023.777717, 2460024.198938}, - {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460023.773972, 2460024.202755}, - {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 0, 0, 2460116.763078, 2460117.493338}, - {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 0, 100, 2460116.760673, 2460117.495441}, - {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460116.755357, -3.000000}, - {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 1, 0, 2460116.756765, 2460117.498850}, - {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 1, 100, 2460116.754275, -3.000000}, - {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460116.748756, -3.000000}, - {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 0, 0, 2460211.198496, 2460211.403741}, - {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 0, 100, 2460211.195303, 2460211.406919}, - {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460211.188650, 2460211.413549}, - {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 1, 0, 2460211.190358, 2460211.411852}, - {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 1, 100, 2460211.187360, 2460211.414835}, - {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460211.181078, 2460211.421094}, - {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 0, 0, 2460301.052264, 2460300.635877}, - {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 0, 100, 2460301.050516, 2460300.637649}, - {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460301.046704, 2460300.641518}, - {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 1, 0, 2460301.047704, 2460300.640507}, - {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 1, 100, 2460301.045938, 2460300.642295}, - {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460301.042086, 2460300.646202}, - {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 0, 0, -3.000000, 2460369.848787}, - {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 0, 100, -3.000000, 2460369.850534}, - {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460370.498519, 2460369.854299}, - {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 1, 0, 2460370.499620, 2460369.853318}, - {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 1, 100, 2460370.497681, 2460369.855048}, - {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460370.493523, 2460369.858778}, - {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 0, 0, 2460861.132034, 2460861.495198}, - {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 0, 100, 2460861.130125, 2460861.497068}, - {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460861.126028, -3.000000}, - {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 1, 0, 2460861.127098, -3.000000}, - {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 1, 100, 2460861.125214, -3.000000}, - {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460861.121170, 2460860.500614}, - {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 0, 0, 2459959.503589, 2459959.983951}, - {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 0, 100, 2459959.502503, 2459959.985013}, - {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 0, 1000, 2459959.500147, 2459959.987324}, - {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 1, 0, 2459959.500758, 2459959.986727}, - {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 1, 100, -3.000000, 2459959.987788}, - {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 1, 1000, -3.000000, 2459959.990096}, - {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 0, 0, 2460023.724714, 2460024.198340}, - {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 0, 100, 2460023.723619, 2460024.199426}, - {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460023.721241, 2460024.201793}, - {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 1, 0, 2460023.721856, 2460024.201183}, - {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 1, 100, 2460023.720764, 2460024.202268}, - {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460023.718390, 2460024.204631}, - {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 0, 0, 2460116.813768, 2460117.406719}, - {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 0, 100, 2460116.812556, 2460117.407876}, - {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460116.809916, 2460117.410402}, - {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 1, 0, 2460116.810603, 2460117.409748}, - {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 1, 100, 2460116.809385, 2460117.410911}, - {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460116.806730, 2460117.413449}, - {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 0, 0, 2460211.072262, 2460211.482710}, - {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 0, 100, 2460211.070975, 2460211.483979}, - {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460211.068188, 2460211.486735}, - {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 1, 0, 2460211.068910, 2460211.486026}, - {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 1, 100, 2460211.067630, 2460211.487287}, - {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460211.064861, 2460211.490027}, - {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 0, 0, 2460301.063789, 2460300.584090}, - {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 0, 100, 2460301.062692, 2460300.585176}, - {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460301.060304, 2460300.587550}, - {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 1, 0, 2460301.060923, 2460300.586938}, - {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 1, 100, 2460301.059824, 2460300.588027}, - {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460301.057430, 2460300.590407}, - {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 0, 0, 2460370.433373, 2460369.862017}, - {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 0, 100, 2460370.432237, 2460369.863096}, - {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460370.429775, 2460369.865444}, - {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 1, 0, 2460370.430414, 2460369.864837}, - {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 1, 100, 2460370.429281, 2460369.865915}, - {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460370.426826, 2460369.868257}, - {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 0, 0, 2460861.065509, -3.000000}, - {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 0, 100, 2460861.064384, 2460860.500980}, - {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460861.061949, 2460860.503308}, - {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 1, 0, 2460861.062582, 2460860.502699}, - {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 1, 100, 2460861.061461, 2460860.503775}, - {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460861.059032, 2460860.506098}, - {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 0, 0, -3.000000, 2459960.019905}, - {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 0, 100, -3.000000, 2459960.020918}, - {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 0, 1000, -3.000000, 2459960.023131}, - {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 1, 0, -3.000000, 2459960.022561}, - {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 1, 100, -3.000000, 2459960.023576}, - {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 1, 1000, -3.000000, 2459960.025793}, - {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 0, 0, 2460023.694314, 2460024.230676}, - {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 0, 100, 2460023.693279, 2460024.231665}, - {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460023.691025, 2460024.233826}, - {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 1, 0, 2460023.691609, 2460024.233270}, - {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 1, 100, 2460023.690571, 2460024.234261}, - {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460023.688314, 2460024.236424}, - {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 0, 0, 2460116.892241, 2460117.344631}, - {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 0, 100, 2460116.891136, 2460117.345716}, - {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460116.888738, 2460117.348079}, - {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 1, 0, 2460116.889358, 2460117.347471}, - {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 1, 100, 2460116.888256, 2460117.348553}, - {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460116.885865, 2460117.350908}, - {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 0, 0, 2460210.989126, 2460210.536957}, - {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 0, 100, 2460210.987956, 2460210.538111}, - {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460210.985404, 2460210.540637}, - {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 1, 0, 2460210.986066, 2460210.539987}, - {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 1, 100, 2460210.984890, 2460210.541146}, - {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460210.982326, 2460210.543684}, - {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 0, 0, 2460301.112646, 2460300.554998}, - {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 0, 100, 2460301.111589, 2460300.556005}, - {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460301.109294, 2460300.558202}, - {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 1, 0, 2460301.109888, 2460300.557636}, - {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 1, 100, 2460301.108833, 2460300.558643}, - {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460301.106543, 2460300.560837}, - {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 0, 0, 2460370.388602, 2460369.907565}, - {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 0, 100, 2460370.387558, 2460369.908592}, - {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460370.385285, 2460369.910835}, - {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 1, 0, 2460370.385874, 2460369.910257}, - {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 1, 100, 2460370.384828, 2460369.911287}, - {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460370.382551, 2460369.913535}, - {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 0, 0, 2460861.024935, 2460860.540907}, - {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 0, 100, 2460861.023901, 2460860.541926}, - {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460861.021651, 2460860.544150}, - {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 1, 0, 2460861.022235, 2460860.543577}, - {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 1, 100, 2460861.021199, 2460860.544598}, - {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460861.018944, 2460860.546826}, - {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 0, 0, 2459959.533659, 2459960.042803}, - {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 0, 100, 2459959.532736, 2459960.043718}, - {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 0, 1000, 2459959.530726, 2459960.045716}, - {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 1, 0, 2459959.531245, 2459960.045203}, - {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 1, 100, 2459959.530321, 2459960.046118}, - {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 1, 1000, 2459959.528311, 2459960.048117}, - {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 0, 0, 2460023.744413, 2460024.255583}, - {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 0, 100, 2460023.743460, 2460024.256507}, - {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460023.741384, 2460024.258530}, - {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 1, 0, 2460023.741919, 2460024.258011}, - {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 1, 100, 2460023.740966, 2460024.258937}, - {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460023.738890, 2460024.260959}, - {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 0, 0, 2460116.896343, 2460117.411271}, - {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 0, 100, 2460116.895329, 2460117.412257}, - {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460116.893122, 2460117.414410}, - {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 1, 0, 2460116.893692, 2460117.413857}, - {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 1, 100, 2460116.892677, 2460117.414843}, - {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460116.890469, 2460117.416997}, - {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 0, 0, 2460211.061198, 2460210.530116}, - {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 0, 100, 2460211.060143, 2460210.531151}, - {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460211.057847, 2460210.533414}, - {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 1, 0, 2460211.058440, 2460210.532834}, - {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 1, 100, 2460211.057385, 2460210.533870}, - {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460211.055090, 2460210.536133}, - {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 0, 0, 2460301.128082, 2460300.607764}, - {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 0, 100, 2460301.127137, 2460300.608684}, - {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460301.125078, 2460300.610694}, - {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 1, 0, 2460301.125610, 2460300.610178}, - {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 1, 100, 2460301.124664, 2460300.611098}, - {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460301.122605, 2460300.613109}, - {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 0, 0, 2460370.448355, 2460369.926847}, - {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 0, 100, 2460370.447403, 2460369.927771}, - {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460370.445333, 2460369.929790}, - {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 1, 0, 2460370.445868, 2460369.929271}, - {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 1, 100, 2460370.444917, 2460369.930196}, - {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460370.442847, 2460369.932215}, - {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 0, 0, 2460861.083263, 2460860.562492}, - {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 0, 100, 2460861.082319, 2460860.563411}, - {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460861.080264, 2460860.565417}, - {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 1, 0, 2460861.080795, 2460860.564902}, - {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 1, 100, 2460861.079851, 2460860.565821}, - {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460861.077796, 2460860.567828}, - {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 0, 0, 2459959.611424, 2459959.989333}, - {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 0, 100, 2459959.609258, 2459959.991433}, - {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 0, 1000, 2459959.604610, 2459959.995941}, - {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 1, 0, 2459959.605825, 2459959.994766}, - {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 1, 100, 2459959.603687, 2459959.996835}, - {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 1, 1000, 2459959.599095, 2459960.001283}, - {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 0, 0, 2460023.818593, 2460024.240506}, - {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 0, 100, 2460023.816608, 2460024.242553}, - {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460023.812331, 2460024.246975}, - {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 1, 0, 2460023.813448, 2460024.245824}, - {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 1, 100, 2460023.811481, 2460024.247856}, - {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460023.807242, 2460024.252248}, - {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 0, 0, 2460116.779694, 2460116.541691}, - {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 0, 100, 2460116.776499, 2460116.544856}, - {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460116.769322, 2460116.551979}, - {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 1, 0, 2460116.771242, 2460116.550076}, - {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 1, 100, 2460116.767840, 2460116.553450}, - {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460116.760140, 2460116.561096}, - {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 0, 0, 2460211.309271, 2460211.381831}, - {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 0, 100, 2460211.300322, 2460211.390769}, - {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460211.285237, 2460211.405841}, - {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 1, 0, 2460211.288811, 2460211.402275}, - {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 1, 100, 2460211.282640, 2460211.408435}, - {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460211.270923, 2460211.420140}, - {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 0, 0, 2460301.058819, 2460300.705421}, - {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 0, 100, 2460301.056561, 2460300.707705}, - {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460301.051607, 2460300.712725}, - {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 1, 0, 2460301.052913, 2460300.711406}, - {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 1, 100, 2460301.050606, 2460300.713738}, - {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460301.045537, 2460300.718869}, - {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 0, 0, 2460369.515897, 2460369.852889}, - {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 0, 100, 2460369.513573, 2460369.855159}, - {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460369.508609, 2460369.860014}, - {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 1, 0, 2460369.509904, 2460369.858750}, - {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 1, 100, 2460369.507626, 2460369.860974}, - {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460369.502753, 2460369.865734}, - {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 0, 0, 2460861.205484, 2460861.493716}, - {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 0, 100, 2460861.202866, 2460861.496292}, - {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460861.197312, 2460860.503722}, - {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 1, 0, 2460861.198757, 2460860.502500}, - {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 1, 100, 2460861.196218, 2460860.504648}, - {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460861.190823, 2460860.509247}, - {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 0, 0, 2459959.583014, 2459959.895953}, - {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 0, 100, 2459959.579313, 2459959.899514}, - {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 0, 1000, 2459959.571484, 2459959.907035}, - {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 1, 0, 2459959.573523, 2459959.905081}, - {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 1, 100, 2459959.569948, 2459959.908508}, - {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 1, 1000, 2459959.562362, 2459959.915773}, - {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 0, 0, 2460023.817806, 2460024.130940}, - {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 0, 100, 2460023.814218, 2460024.134658}, - {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 0, 1000, 2460023.806634, 2460024.142543}, - {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 1, 0, 2460023.808601, 2460024.140499}, - {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 1, 100, 2460023.805148, 2460024.144090}, - {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 1, 1000, 2460023.797827, 2460024.151730}, + {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 0, 0, 2459959.512554, 2459959.985385}, + {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 0, 100, 2459959.511326, 2459959.986593}, + {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 0, 1000, 2459959.508675, 2459959.989201}, + {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 1, 0, 2459959.509370, 2459959.988518}, + {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 1, 100, 2459959.508144, 2459959.989724}, + {2023, 1, 15.0, 116.4074, 39.9042, 8.0, 1, 1000, 2459959.505497, 2459959.992326}, + {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 0, 0, 2460023.743580, 2460024.190959}, + {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 0, 100, 2460023.742322, 2460024.192231}, + {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460023.739608, 2460024.194976}, + {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 1, 0, 2460023.740319, 2460024.194257}, + {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 1, 100, 2460023.739065, 2460024.195525}, + {2023, 3, 20.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460023.736362, 2460024.198261}, + {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 0, 0, 2460116.797519, 2460117.432130}, + {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 0, 100, 2460116.796079, 2460117.433504}, + {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460116.792947, 2460117.436489}, + {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 1, 0, 2460116.793770, 2460117.435705}, + {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 1, 100, 2460116.792316, 2460117.437090}, + {2023, 6, 21.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460116.789151, 2460117.440100}, + {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 0, 0, 2460211.101734, 2460211.459607}, + {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 0, 100, 2460211.100176, 2460211.461165}, + {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460211.096833, 2460211.464509}, + {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 1, 0, 2460211.097706, 2460211.463636}, + {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 1, 100, 2460211.096167, 2460211.465175}, + {2023, 9, 23.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460211.092862, 2460211.468480}, + {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 0, 0, 2460301.062707, 2460300.593974}, + {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 0, 100, 2460301.061475, 2460300.595223}, + {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460301.058806, 2460300.597927}, + {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 1, 0, 2460301.059506, 2460300.597218}, + {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 1, 100, 2460301.058270, 2460300.598470}, + {2023, 12, 22.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460301.055591, 2460300.601184}, + {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 0, 0, 2460370.449849, 2460369.861099}, + {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 0, 100, 2460370.448553, 2460369.862331}, + {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460370.445759, 2460369.864987}, + {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 1, 0, 2460370.446490, 2460369.864292}, + {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 1, 100, 2460370.445200, 2460369.865519}, + {2024, 2, 29.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460370.442419, 2460369.868167}, + {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 0, 0, 2460861.081077, 2460860.500281}, + {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 0, 100, 2460861.079798, 2460860.501500}, + {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 0, 1000, 2460861.077040, 2460860.504131}, + {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 1, 0, 2460861.077761, 2460860.503442}, + {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 1, 100, 2460861.076488, 2460860.504658}, + {2025, 7, 4.0, 116.4074, 39.9042, 8.0, 1, 1000, 2460861.073741, 2460860.507283}, + {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 0, 0, 2459959.528216, 2459959.967427}, + {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 0, 100, 2459959.526661, 2459959.968948}, + {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 0, 1000, 2459959.523310, 2459959.972225}, + {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 1, 0, 2459959.524188, 2459959.971367}, + {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 1, 100, 2459959.522641, 2459959.972879}, + {2023, 1, 15.0, -0.1276, 51.5074, 0.0, 1, 1000, 2459959.519304, 2459959.976140}, + {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 0, 0, 2460023.756521, 2460024.185921}, + {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 0, 100, 2460023.754972, 2460024.187506}, + {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460023.751635, 2460024.190922}, + {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 1, 0, 2460023.752509, 2460024.190027}, + {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 1, 100, 2460023.750969, 2460024.191604}, + {2023, 3, 20.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460023.747652, 2460024.195001}, + {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 0, 0, 2460116.767646, 2460117.458741}, + {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 0, 100, 2460116.765687, 2460117.460530}, + {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460116.761396, 2460117.464434}, + {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 1, 0, 2460116.762529, 2460117.463404}, + {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 1, 100, 2460116.760527, 2460117.465222}, + {2023, 6, 21.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460116.756138, 2460117.469192}, + {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 0, 0, 2460211.152717, 2460211.418335}, + {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 0, 100, 2460211.150373, 2460211.420681}, + {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460211.145408, 2460211.425649}, + {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 1, 0, 2460211.146698, 2460211.424358}, + {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 1, 100, 2460211.144431, 2460211.426627}, + {2023, 9, 23.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460211.139618, 2460211.431444}, + {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 0, 0, 2460301.041742, 2460300.614710}, + {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 0, 100, 2460301.040171, 2460300.616310}, + {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460301.036759, 2460300.619784}, + {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 1, 0, 2460301.037657, 2460300.618870}, + {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 1, 100, 2460301.036073, 2460300.620481}, + {2023, 12, 22.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460301.032632, 2460300.623982}, + {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 0, 0, 2460370.478098, 2460369.838275}, + {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 0, 100, 2460370.476374, 2460369.839850}, + {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460370.472673, 2460369.843240}, + {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 1, 0, 2460370.473641, 2460369.842353}, + {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 1, 100, 2460370.471937, 2460369.843916}, + {2024, 2, 29.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460370.468277, 2460369.847281}, + {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 0, 0, 2460861.106509, 2460861.487755}, + {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 0, 100, 2460861.104828, 2460861.489419}, + {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 0, 1000, 2460861.101215, 2460861.492993}, + {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 1, 0, 2460861.102160, 2460861.492058}, + {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 1, 100, 2460861.100496, 2460861.493705}, + {2025, 7, 4.0, -0.1276, 51.5074, 0.0, 1, 1000, 2460861.096919, 2460861.497243}, + {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 0, 0, 2459959.524373, 2459959.980659}, + {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 0, 100, 2459959.523111, 2459959.981906}, + {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 0, 1000, 2459959.520387, 2459959.984594}, + {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 1, 0, 2459959.521100, 2459959.983890}, + {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 1, 100, 2459959.519842, 2459959.985133}, + {2023, 1, 15.0, -74.0060, 40.7128, -5.0, 1, 1000, 2459959.517125, 2459959.987813}, + {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 0, 0, 2460023.742268, 2460024.205728}, + {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 0, 100, 2460023.741027, 2460024.206990}, + {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460023.738347, 2460024.209716}, + {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 1, 0, 2460023.739049, 2460024.209002}, + {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 1, 100, 2460023.737811, 2460024.210262}, + {2023, 3, 20.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460023.735139, 2460024.212982}, + {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 0, 0, 2460116.805629, 2460117.432448}, + {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 0, 100, 2460116.804200, 2460117.433805}, + {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460116.801091, 2460117.436752}, + {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 1, 0, 2460116.801908, 2460117.435978}, + {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 1, 100, 2460116.800465, 2460117.437345}, + {2023, 6, 21.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460116.797326, 2460117.440314}, + {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 0, 0, 2460211.112676, 2460211.469717}, + {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 0, 100, 2460211.111105, 2460211.471288}, + {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460211.107733, 2460211.474659}, + {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 1, 0, 2460211.108613, 2460211.473779}, + {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 1, 100, 2460211.107061, 2460211.475330}, + {2023, 9, 23.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460211.103729, 2460211.478661}, + {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 0, 0, 2460301.058919, 2460300.608173}, + {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 0, 100, 2460301.057640, 2460300.609464}, + {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460301.054868, 2460300.612263}, + {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 1, 0, 2460301.055595, 2460300.611528}, + {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 1, 100, 2460301.054311, 2460300.612825}, + {2023, 12, 22.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460301.051524, 2460300.615638}, + {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 0, 0, 2460370.461953, 2460369.855473}, + {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 0, 100, 2460370.460601, 2460369.856749}, + {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460370.457690, 2460369.859501}, + {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 1, 0, 2460370.458451, 2460369.858781}, + {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 1, 100, 2460370.457108, 2460369.860051}, + {2024, 2, 29.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460370.454214, 2460369.862791}, + {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 0, 0, 2460861.092187, -3.000000}, + {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 0, 100, 2460861.090857, -3.000000}, + {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 0, 1000, 2460861.087993, -3.000000}, + {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 1, 0, 2460861.088742, -3.000000}, + {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 1, 100, 2460861.087420, -3.000000}, + {2025, 7, 4.0, -74.0060, 40.7128, -5.0, 1, 1000, 2460861.084572, 2460860.501630}, + {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 0, 0, 2459960.490958, 2459960.006349}, + {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 0, 100, 2459960.489835, 2459960.007484}, + {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 0, 1000, 2459960.487402, 2459960.009941}, + {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 1, 0, 2459960.488040, 2459960.009297}, + {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 1, 100, 2459960.486913, 2459960.010434}, + {2023, 1, 15.0, 151.2093, -33.8688, 10.0, 1, 1000, 2459960.484475, 2459960.012896}, + {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 0, 0, 2460023.660858, 2460024.225393}, + {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 0, 100, 2460023.659673, 2460024.226523}, + {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460023.657104, 2460024.228972}, + {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 1, 0, 2460023.657778, 2460024.228330}, + {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 1, 100, 2460023.656588, 2460024.229464}, + {2023, 3, 20.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460023.654010, 2460024.231918}, + {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 0, 0, 2460116.898588, 2460117.306637}, + {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 0, 100, 2460116.897300, 2460117.307923}, + {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460116.894525, 2460117.310693}, + {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 1, 0, 2460116.895250, 2460117.309969}, + {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 1, 100, 2460116.893970, 2460117.311247}, + {2023, 6, 21.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460116.891213, 2460117.314000}, + {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 0, 0, 2460210.945165, 2460210.537830}, + {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 0, 100, 2460210.943814, 2460210.539180}, + {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460210.940878, 2460210.542112}, + {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 1, 0, 2460210.941649, 2460210.541342}, + {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 1, 100, 2460210.940287, 2460210.542703}, + {2023, 9, 23.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460210.937326, 2460210.545660}, + {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 0, 0, 2460301.101636, 2460300.535995}, + {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 0, 100, 2460301.100469, 2460300.537113}, + {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460301.097951, 2460300.539529}, + {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 1, 0, 2460301.098609, 2460300.538896}, + {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 1, 100, 2460301.097446, 2460300.540013}, + {2023, 12, 22.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460301.094934, 2460300.542424}, + {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 0, 0, 2460370.367704, 2460369.898000}, + {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 0, 100, 2460370.366562, 2460369.899150}, + {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460370.364086, 2460369.901641}, + {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 1, 0, 2460370.364735, 2460369.900988}, + {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 1, 100, 2460370.363588, 2460369.902141}, + {2024, 2, 29.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460370.361104, 2460369.904640}, + {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 0, 0, 2460861.005763, 2460860.530709}, + {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 0, 100, 2460861.004631, 2460860.531850}, + {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 0, 1000, 2460861.002179, 2460860.534321}, + {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 1, 0, 2460861.002822, 2460860.533674}, + {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 1, 100, 2460861.001687, 2460860.534818}, + {2025, 7, 4.0, 151.2093, -33.8688, 10.0, 1, 1000, 2460860.999227, 2460860.537296}, + {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 0, 0, -3.000000, 2459959.964451}, + {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 0, 100, -3.000000, 2459959.965589}, + {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 0, 1000, -3.000000, 2459959.968047}, + {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 1, 0, -3.000000, 2459959.967403}, + {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 1, 100, -3.000000, 2459959.968539}, + {2023, 1, 15.0, 139.6503, 35.6762, 9.0, 1, 1000, -3.000000, 2459959.970994}, + {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 0, 0, 2460023.713965, 2460024.169037}, + {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 0, 100, 2460023.712778, 2460024.170233}, + {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460023.710218, 2460024.172814}, + {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 1, 0, 2460023.710888, 2460024.172139}, + {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 1, 100, 2460023.709705, 2460024.173331}, + {2023, 3, 20.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460023.707153, 2460024.175905}, + {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 0, 0, 2460116.781452, 2460117.399206}, + {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 0, 100, 2460116.780120, 2460117.400487}, + {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460116.777226, 2460117.403268}, + {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 1, 0, 2460116.777986, 2460117.402538}, + {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 1, 100, 2460116.776643, 2460117.403828}, + {2023, 6, 21.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460116.773726, 2460117.406627}, + {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 0, 0, 2460211.064104, 2460211.445733}, + {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 0, 100, 2460211.062683, 2460211.447154}, + {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460211.059628, 2460211.450209}, + {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 1, 0, 2460211.060426, 2460211.449411}, + {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 1, 100, 2460211.059018, 2460211.450820}, + {2023, 9, 23.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460211.055989, 2460211.453848}, + {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 0, 0, 2460301.042677, 2460300.564798}, + {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 0, 100, 2460301.041519, 2460300.565969}, + {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460301.039011, 2460300.568505}, + {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 1, 0, 2460301.039668, 2460300.567840}, + {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 1, 100, 2460301.038507, 2460300.569014}, + {2023, 12, 22.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460301.035991, 2460300.571557}, + {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 0, 0, 2460370.418603, 2460369.841484}, + {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 0, 100, 2460370.417394, 2460369.842641}, + {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460370.414788, 2460369.845138}, + {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 1, 0, 2460370.415470, 2460369.844484}, + {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 1, 100, 2460370.414266, 2460369.845638}, + {2024, 2, 29.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460370.411669, 2460369.848129}, + {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 0, 0, 2460861.050487, 2460861.497293}, + {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 0, 100, 2460861.049292, 2460861.498481}, + {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 0, 1000, 2460861.046715, -3.000000}, + {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 1, 0, 2460861.047389, -3.000000}, + {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 1, 100, 2460861.046199, -3.000000}, + {2025, 7, 4.0, 139.6503, 35.6762, 9.0, 1, 1000, 2460861.043629, -3.000000}, + {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 0, 0, 2459959.559715, 2459960.005821}, + {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 0, 100, 2459959.558253, 2459960.007254}, + {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 0, 1000, 2459959.555099, 2459960.010344}, + {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 1, 0, 2459959.555926, 2459960.009534}, + {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 1, 100, 2459959.554469, 2459960.010961}, + {2023, 1, 15.0, 2.3522, 48.8566, 1.0, 1, 1000, 2459959.551327, 2459960.014038}, + {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 0, 0, 2460023.786892, 2460024.223745}, + {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 0, 100, 2460023.785433, 2460024.225234}, + {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460023.782289, 2460024.228444}, + {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 1, 0, 2460023.783113, 2460024.227603}, + {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 1, 100, 2460023.781662, 2460024.229086}, + {2023, 3, 20.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460023.778534, 2460024.232282}, + {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 0, 0, 2460116.811624, 2460117.485433}, + {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 0, 100, 2460116.809837, 2460117.487087}, + {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460116.805935, 2460117.490692}, + {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 1, 0, 2460116.806964, 2460117.489742}, + {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 1, 100, 2460116.805147, 2460117.491418}, + {2023, 6, 21.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460116.801173, 2460117.495072}, + {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 0, 0, 2460211.173470, 2460211.466396}, + {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 0, 100, 2460211.171407, 2460211.468460}, + {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460211.167013, 2460211.472855}, + {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 1, 0, 2460211.168157, 2460211.471711}, + {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 1, 100, 2460211.166145, 2460211.473725}, + {2023, 9, 23.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460211.161854, 2460211.478018}, + {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 0, 0, 2460301.081101, 2460300.645199}, + {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 0, 100, 2460301.079625, 2460300.646700}, + {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460301.076421, 2460300.649956}, + {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 1, 0, 2460301.077264, 2460300.649100}, + {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 1, 100, 2460301.075778, 2460300.650610}, + {2023, 12, 22.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460301.072551, 2460300.653887}, + {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 0, 0, -3.000000, 2460369.877903}, + {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 0, 100, -3.000000, 2460369.879381}, + {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 0, 1000, -3.000000, 2460369.882564}, + {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 1, 0, -3.000000, 2460369.881731}, + {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 1, 100, -3.000000, 2460369.883199}, + {2024, 2, 29.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460370.496899, 2460369.886363}, + {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 0, 0, 2460861.135112, 2460860.518313}, + {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 0, 100, 2460861.133547, 2460860.519769}, + {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 0, 1000, 2460861.130181, 2460860.522905}, + {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 1, 0, 2460861.131061, 2460860.522084}, + {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 1, 100, 2460861.129509, 2460860.523531}, + {2025, 7, 4.0, 2.3522, 48.8566, 1.0, 1, 1000, 2460861.126170, 2460860.526651}, + {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 0, 0, 2459959.522258, 2459959.987657}, + {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 0, 100, 2459959.521109, 2459959.988797}, + {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 0, 1000, 2459959.518627, 2459959.991259}, + {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 1, 0, 2459959.519277, 2459959.990615}, + {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 1, 100, 2459959.518130, 2459959.991752}, + {2023, 1, 15.0, -118.2437, 34.0522, -8.0, 1, 1000, 2459959.515653, 2459959.994209}, + {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 0, 0, 2460023.736766, 2460024.214227}, + {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 0, 100, 2460023.735638, 2460024.215370}, + {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460023.733201, 2460024.217839}, + {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 1, 0, 2460023.733839, 2460024.217193}, + {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 1, 100, 2460023.732713, 2460024.218334}, + {2023, 3, 20.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460023.730281, 2460024.220800}, + {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 0, 0, 2460116.822110, 2460117.421317}, + {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 0, 100, 2460116.820847, 2460117.422529}, + {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460116.818107, 2460117.425158}, + {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 1, 0, 2460116.818826, 2460117.424469}, + {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 1, 100, 2460116.817555, 2460117.425687}, + {2023, 6, 21.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460116.814798, 2460117.428329}, + {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 0, 0, 2460211.097066, 2460211.491497}, + {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 0, 100, 2460211.095704, 2460211.492857}, + {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460211.092773, 2460211.495784}, + {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 1, 0, 2460211.093538, 2460211.495019}, + {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 1, 100, 2460211.092187, 2460211.496369}, + {2023, 9, 23.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460211.089278, 2460211.499274}, + {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 0, 0, 2460301.067938, 2460300.604730}, + {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 0, 100, 2460301.066770, 2460300.605903}, + {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460301.064240, 2460300.608446}, + {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 1, 0, 2460301.064903, 2460300.607780}, + {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 1, 100, 2460301.063731, 2460300.608957}, + {2023, 12, 22.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460301.061191, 2460300.611510}, + {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 0, 0, 2460370.454456, 2460369.864161}, + {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 0, 100, 2460370.453239, 2460369.865324}, + {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460370.450613, 2460369.867834}, + {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 1, 0, 2460370.451300, 2460369.867178}, + {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 1, 100, 2460370.450087, 2460369.868337}, + {2024, 2, 29.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460370.447473, 2460369.870839}, + {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 0, 0, 2460861.085408, 2460860.502124}, + {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 0, 100, 2460861.084207, 2460860.503275}, + {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 0, 1000, 2460861.081618, 2460860.505757}, + {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 1, 0, 2460861.082295, 2460860.505108}, + {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 1, 100, 2460861.081100, 2460860.506255}, + {2025, 7, 4.0, -118.2437, 34.0522, -8.0, 1, 1000, 2460861.078521, 2460860.508731}, + {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 0, 0, 2459959.547954, 2459959.980053}, + {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 0, 100, 2459959.546225, 2459959.981737}, + {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 0, 1000, 2459959.542499, 2459959.985364}, + {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 1, 0, 2459959.543476, 2459959.984414}, + {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 1, 100, 2459959.541756, 2459959.986087}, + {2023, 1, 15.0, 37.6176, 55.7558, 3.0, 1, 1000, 2459959.538049, 2459959.989690}, + {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 0, 0, 2460023.784142, 2460024.192397}, + {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 0, 100, 2460023.782392, 2460024.194191}, + {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460023.778627, 2460024.198052}, + {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 1, 0, 2460023.779613, 2460024.197041}, + {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 1, 100, 2460023.777878, 2460024.198821}, + {2023, 3, 20.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460023.774146, 2460024.202652}, + {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 0, 0, 2460116.763250, 2460117.493183}, + {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 0, 100, 2460116.760849, 2460117.495298}, + {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460116.755549, 2460117.499936}, + {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 1, 0, 2460116.756956, 2460117.498709}, + {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 1, 100, 2460116.754470, -3.000000}, + {2023, 6, 21.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460116.748967, -3.000000}, + {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 0, 0, 2460211.198752, 2460211.403480}, + {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 0, 100, 2460211.195553, 2460211.406681}, + {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460211.188905, 2460211.413333}, + {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 1, 0, 2460211.190618, 2460211.411619}, + {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 1, 100, 2460211.187616, 2460211.414624}, + {2023, 9, 23.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460211.181340, 2460211.420904}, + {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 0, 0, 2460301.052400, 2460300.635733}, + {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 0, 100, 2460301.050654, 2460300.637519}, + {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460301.046856, 2460300.641402}, + {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 1, 0, 2460301.047856, 2460300.640379}, + {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 1, 100, 2460301.046092, 2460300.642182}, + {2023, 12, 22.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460301.042253, 2460300.646102}, + {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 0, 0, -3.000000, 2460369.848656}, + {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 0, 100, -3.000000, 2460369.850414}, + {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460370.498678, 2460369.854191}, + {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 1, 0, 2460370.499778, 2460369.853202}, + {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 1, 100, 2460370.497843, 2460369.854943}, + {2024, 2, 29.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460370.493697, 2460369.858685}, + {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 0, 0, 2460861.132171, 2460861.495059}, + {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 0, 100, 2460861.130266, 2460861.496940}, + {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 0, 1000, 2460861.126181, -3.000000}, + {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 1, 0, 2460861.127249, 2460861.499917}, + {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 1, 100, 2460861.125370, -3.000000}, + {2025, 7, 4.0, 37.6176, 55.7558, 3.0, 1, 1000, 2460861.121338, 2460860.500525}, + {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 0, 0, 2459959.503783, 2459959.983754}, + {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 0, 100, 2459959.502699, 2459959.984830}, + {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 0, 1000, 2459959.500355, 2459959.987154}, + {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 1, 0, 2459959.500968, 2459959.986546}, + {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 1, 100, -3.000000, 2459959.987621}, + {2023, 1, 15.0, 31.2357, 30.0444, 2.0, 1, 1000, -3.000000, 2459959.989941}, + {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 0, 0, 2460023.724925, 2460024.198124}, + {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 0, 100, 2460023.723830, 2460024.199227}, + {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460023.721465, 2460024.201608}, + {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 1, 0, 2460023.722084, 2460024.200985}, + {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 1, 100, 2460023.720991, 2460024.202086}, + {2023, 3, 20.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460023.718631, 2460024.204463}, + {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 0, 0, 2460116.813976, 2460117.406515}, + {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 0, 100, 2460116.812765, 2460117.407686}, + {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460116.810138, 2460117.410224}, + {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 1, 0, 2460116.810826, 2460117.409559}, + {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 1, 100, 2460116.809609, 2460117.410735}, + {2023, 6, 21.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460116.806967, 2460117.413285}, + {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 0, 0, 2460211.072502, 2460211.482464}, + {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 0, 100, 2460211.071214, 2460211.483751}, + {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460211.068441, 2460211.486521}, + {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 1, 0, 2460211.069165, 2460211.485798}, + {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 1, 100, 2460211.067886, 2460211.487076}, + {2023, 9, 23.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460211.065130, 2460211.489830}, + {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 0, 0, 2460301.063991, 2460300.583881}, + {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 0, 100, 2460301.062895, 2460300.584984}, + {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460301.060520, 2460300.587371}, + {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 1, 0, 2460301.061142, 2460300.586746}, + {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 1, 100, 2460301.060043, 2460300.587851}, + {2023, 12, 22.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460301.057662, 2460300.590245}, + {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 0, 0, 2460370.433571, 2460369.861825}, + {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 0, 100, 2460370.432435, 2460369.862917}, + {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460370.429986, 2460369.865277}, + {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 1, 0, 2460370.430626, 2460369.864660}, + {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 1, 100, 2460370.429494, 2460369.865751}, + {2024, 2, 29.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460370.427052, 2460369.868106}, + {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 0, 0, 2460861.065701, -3.000000}, + {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 0, 100, 2460861.064578, 2460860.500804}, + {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 0, 1000, 2460861.062156, 2460860.503144}, + {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 1, 0, 2460861.062789, 2460860.502532}, + {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 1, 100, 2460861.061670, 2460860.503613}, + {2025, 7, 4.0, 31.2357, 30.0444, 2.0, 1, 1000, 2460861.059253, 2460860.505949}, + {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 0, 0, -3.000000, 2459960.019691}, + {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 0, 100, -3.000000, 2459960.020719}, + {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 0, 1000, -3.000000, 2459960.022945}, + {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 1, 0, -3.000000, 2459960.022363}, + {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 1, 100, -3.000000, 2459960.023393}, + {2023, 1, 15.0, -43.1729, -22.9068, -3.0, 1, 1000, -3.000000, 2459960.025623}, + {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 0, 0, 2460023.694539, 2460024.230455}, + {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 0, 100, 2460023.693504, 2460024.231460}, + {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460023.691264, 2460024.233634}, + {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 1, 0, 2460023.691850, 2460024.233066}, + {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 1, 100, 2460023.690813, 2460024.234072}, + {2023, 3, 20.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460023.688569, 2460024.236249}, + {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 0, 0, 2460116.892457, 2460117.344413}, + {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 0, 100, 2460116.891351, 2460117.345514}, + {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460116.888965, 2460117.347889}, + {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 1, 0, 2460116.889589, 2460117.347268}, + {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 1, 100, 2460116.888486, 2460117.348365}, + {2023, 6, 21.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460116.886107, 2460117.350733}, + {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 0, 0, 2460210.989373, 2460210.536707}, + {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 0, 100, 2460210.988203, 2460210.537878}, + {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460210.985665, 2460210.540418}, + {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 1, 0, 2460210.986329, 2460210.539753}, + {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 1, 100, 2460210.985153, 2460210.540929}, + {2023, 9, 23.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460210.982604, 2460210.543480}, + {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 0, 0, 2460301.112866, 2460300.554780}, + {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 0, 100, 2460301.111809, 2460300.555803}, + {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460301.109528, 2460300.558013}, + {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 1, 0, 2460301.110124, 2460300.557435}, + {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 1, 100, 2460301.109070, 2460300.558457}, + {2023, 12, 22.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460301.106793, 2460300.560664}, + {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 0, 0, 2460370.388808, 2460369.907357}, + {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 0, 100, 2460370.387764, 2460369.908399}, + {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460370.385504, 2460369.910654}, + {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 1, 0, 2460370.386096, 2460369.910064}, + {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 1, 100, 2460370.385049, 2460369.911108}, + {2024, 2, 29.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460370.382784, 2460369.913369}, + {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 0, 0, 2460861.025137, 2460860.540702}, + {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 0, 100, 2460861.024103, 2460860.541735}, + {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 0, 1000, 2460861.021865, 2460860.543972}, + {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 1, 0, 2460861.022451, 2460860.543387}, + {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 1, 100, 2460861.021415, 2460860.544422}, + {2025, 7, 4.0, -43.1729, -22.9068, -3.0, 1, 1000, 2460861.019173, 2460860.546664}, + {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 0, 0, 2459959.533880, 2459960.042576}, + {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 0, 100, 2459959.532956, 2459960.043506}, + {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 0, 1000, 2459959.530959, 2459960.045518}, + {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 1, 0, 2459959.531480, 2459960.044992}, + {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 1, 100, 2459959.530557, 2459960.045923}, + {2023, 1, 15.0, 103.8198, 1.3521, 8.0, 1, 1000, 2459959.528559, 2459960.047934}, + {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 0, 0, 2460023.744657, 2460024.255338}, + {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 0, 100, 2460023.743704, 2460024.256280}, + {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460023.741642, 2460024.258316}, + {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 1, 0, 2460023.742180, 2460024.257784}, + {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 1, 100, 2460023.741227, 2460024.258726}, + {2023, 3, 20.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460023.739164, 2460024.260762}, + {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 0, 0, 2460116.896576, 2460117.411038}, + {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 0, 100, 2460116.895561, 2460117.412039}, + {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460116.893367, 2460117.414204}, + {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 1, 0, 2460116.893940, 2460117.413639}, + {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 1, 100, 2460116.892925, 2460117.414640}, + {2023, 6, 21.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460116.890729, 2460117.416806}, + {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 0, 0, 2460211.061459, 2460210.529853}, + {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 0, 100, 2460211.060403, 2460210.530906}, + {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460211.058121, 2460210.533182}, + {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 1, 0, 2460211.058717, 2460210.532587}, + {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 1, 100, 2460211.057662, 2460210.533640}, + {2023, 9, 23.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460211.055380, 2460210.535917}, + {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 0, 0, 2460301.128315, 2460300.607529}, + {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 0, 100, 2460301.127369, 2460300.608465}, + {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460301.125324, 2460300.610488}, + {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 1, 0, 2460301.125859, 2460300.609960}, + {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 1, 100, 2460301.124913, 2460300.610896}, + {2023, 12, 22.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460301.122867, 2460300.612920}, + {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 0, 0, 2460370.448575, 2460369.926627}, + {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 0, 100, 2460370.447624, 2460369.927566}, + {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460370.445566, 2460369.929597}, + {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 1, 0, 2460370.446104, 2460369.929066}, + {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 1, 100, 2460370.445152, 2460369.930006}, + {2024, 2, 29.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460370.443095, 2460369.932037}, + {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 0, 0, 2460861.083480, 2460860.562274}, + {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 0, 100, 2460861.082535, 2460860.563208}, + {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 0, 1000, 2460861.080493, 2460860.565226}, + {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 1, 0, 2460861.081027, 2460860.564699}, + {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 1, 100, 2460861.080082, 2460860.565633}, + {2025, 7, 4.0, 103.8198, 1.3521, 8.0, 1, 1000, 2460861.078040, 2460860.567652}, + {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 0, 0, 2459959.611543, 2459959.989213}, + {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 0, 100, 2459959.609382, 2459959.991323}, + {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 0, 1000, 2459959.604747, 2459959.995844}, + {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 1, 0, 2459959.605960, 2459959.994661}, + {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 1, 100, 2459959.603827, 2459959.996741}, + {2023, 1, 15.0, -149.9003, 61.2181, -9.0, 1, 1000, 2459959.599248, 2459960.001201}, + {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 0, 0, 2460023.818712, 2460024.240379}, + {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 0, 100, 2460023.816729, 2460024.242440}, + {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460023.812466, 2460024.246878}, + {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 1, 0, 2460023.813583, 2460024.245714}, + {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 1, 100, 2460023.811618, 2460024.247761}, + {2023, 3, 20.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460023.807392, 2460024.252168}, + {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 0, 0, 2460116.779861, 2460116.541522}, + {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 0, 100, 2460116.776671, 2460116.544697}, + {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460116.769512, 2460116.551826}, + {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 1, 0, 2460116.771431, 2460116.549915}, + {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 1, 100, 2460116.768034, 2460116.553298}, + {2023, 6, 21.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460116.760356, 2460116.560948}, + {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 0, 0, 2460211.309866, 2460211.381231}, + {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 0, 100, 2460211.300793, 2460211.390310}, + {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460211.285614, 2460211.405503}, + {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 1, 0, 2460211.289218, 2460211.401895}, + {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 1, 100, 2460211.283007, 2460211.408113}, + {2023, 9, 23.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460211.271258, 2460211.419877}, + {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 0, 0, 2460301.058948, 2460300.705285}, + {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 0, 100, 2460301.056692, 2460300.707584}, + {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460301.051751, 2460300.712617}, + {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 1, 0, 2460301.053058, 2460300.711286}, + {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 1, 100, 2460301.050753, 2460300.713633}, + {2023, 12, 22.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460301.045697, 2460300.718777}, + {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 0, 0, 2460369.516019, 2460369.852765}, + {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 0, 100, 2460369.513700, 2460369.855045}, + {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460369.508748, 2460369.859912}, + {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 1, 0, 2460369.510042, 2460369.858641}, + {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 1, 100, 2460369.507768, 2460369.860875}, + {2024, 2, 29.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460369.502907, 2460369.865648}, + {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 0, 0, 2460861.205621, 2460861.493577}, + {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 0, 100, 2460861.203007, 2460861.496163}, + {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 0, 1000, 2460861.197464, 2460860.503626}, + {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 1, 0, 2460861.198907, 2460860.502401}, + {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 1, 100, 2460861.196373, 2460860.504555}, + {2025, 7, 4.0, -149.9003, 61.2181, -9.0, 1, 1000, 2460861.190989, 2460860.509166}, + {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 0, 0, 2459959.583098, 2459959.895869}, + {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 0, 100, 2459959.579404, 2459959.899437}, + {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 0, 1000, 2459959.571589, 2459959.906971}, + {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 1, 0, 2459959.573623, 2459959.905012}, + {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 1, 100, 2459959.570055, 2459959.908447}, + {2023, 1, 15.0, -42.6043, 71.7069, -3.0, 1, 1000, 2459959.562482, 2459959.915724}, + {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 0, 0, 2460023.817900, 2460024.130839}, + {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 0, 100, 2460023.814313, 2460024.134573}, + {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 0, 1000, 2460023.806740, 2460024.142474}, + {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 1, 0, 2460023.808709, 2460024.140418}, + {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 1, 100, 2460023.805257, 2460024.144025}, + {2023, 3, 20.0, -42.6043, 71.7069, -3.0, 1, 1000, 2460023.797947, 2460024.151680}, {2023, 6, 21.0, -42.6043, 71.7069, -3.0, 0, 0, -1.000000, -1.000000}, {2023, 6, 21.0, -42.6043, 71.7069, -3.0, 0, 100, -1.000000, -1.000000}, {2023, 6, 21.0, -42.6043, 71.7069, -3.0, 0, 1000, -1.000000, -1.000000}, @@ -557,24 +558,24 @@ var moonRiseSetTestData = []MoonRiseSetTestCase{ {2023, 9, 23.0, -42.6043, 71.7069, -3.0, 1, 0, -2.000000, -2.000000}, {2023, 9, 23.0, -42.6043, 71.7069, -3.0, 1, 100, -2.000000, -2.000000}, {2023, 9, 23.0, -42.6043, 71.7069, -3.0, 1, 1000, -2.000000, -2.000000}, - {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 0, 0, 2460300.951391, 2460300.689493}, - {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 0, 100, 2460300.947139, 2460300.693837}, - {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 0, 1000, 2460300.937499, 2460300.703677}, - {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 1, 0, 2460300.940091, 2460300.701037}, - {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 1, 100, 2460300.935496, 2460300.705720}, - {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 1, 1000, 2460300.924958, 2460300.716448}, - {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 0, 0, 2460369.510257, 2460369.739050}, - {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 0, 100, 2460369.505593, 2460369.743606}, - {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 0, 1000, -3.000000, 2460369.752996}, - {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 1, 0, -2.000000, 2460369.750583}, - {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 1, 100, -3.000000, 2460369.754803}, - {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 1, 1000, -3.000000, 2460369.763575}, - {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 0, 0, 2460861.253176, 2460861.327087}, - {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 0, 100, 2460861.241274, 2460861.338907}, - {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 0, 1000, 2460861.222145, 2460861.357865}, - {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 1, 0, 2460861.226647, 2460861.353412}, - {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 1, 100, 2460861.218910, 2460861.361065}, - {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 1, 1000, 2460861.204387, 2460861.375411}, + {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 0, 0, 2460300.951496, 2460300.689381}, + {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 0, 100, 2460300.947245, 2460300.693741}, + {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 0, 1000, 2460300.937621, 2460300.703591}, + {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 1, 0, 2460300.940215, 2460300.700939}, + {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 1, 100, 2460300.935621, 2460300.705636}, + {2023, 12, 22.0, -42.6043, 71.7069, -3.0, 1, 1000, 2460300.925100, 2460300.716374}, + {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 0, 0, 2460369.510360, 2460369.738945}, + {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 0, 100, 2460369.505703, 2460369.743510}, + {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 0, 1000, -3.000000, 2460369.752915}, + {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 1, 0, -2.000000, 2460369.750498}, + {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 1, 100, -3.000000, 2460369.754725}, + {2024, 2, 29.0, -42.6043, 71.7069, -3.0, 1, 1000, -3.000000, 2460369.763512}, + {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 0, 0, 2460861.253475, 2460861.326785}, + {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 0, 100, 2460861.241516, 2460861.338678}, + {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 0, 1000, 2460861.222342, 2460861.357705}, + {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 1, 0, 2460861.226848, 2460861.353239}, + {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 1, 100, 2460861.219103, 2460861.360914}, + {2025, 7, 4.0, -42.6043, 71.7069, -3.0, 1, 1000, 2460861.204571, 2460861.375294}, {2023, 1, 15.0, 0.0000, -85.0000, 0.0, 0, 0, -1.000000, -1.000000}, {2023, 1, 15.0, 0.0000, -85.0000, 0.0, 0, 100, -1.000000, -1.000000}, {2023, 1, 15.0, 0.0000, -85.0000, 0.0, 0, 1000, -1.000000, -1.000000}, @@ -624,7 +625,11 @@ func TestMoonRiseSetRegression(t *testing.T) { beforeDeltaT := defDeltaTFn SetDeltaTFn(DefaultDeltaTv2) defer SetDeltaTFn(beforeDeltaT) - const tolerance = 0.00011574074 + const ( + geometricTolerance = 0.00011574074 + dynamicCycleTolerance = 10.0 / 1440 + dynamicResidualDegrees = 0.01 + ) for i, testCase := range moonRiseSetTestData { julianDay := JDECalc(testCase.Year, testCase.Month, testCase.Day) @@ -634,7 +639,12 @@ func TestMoonRiseSetRegression(t *testing.T) { actualRise, riseErr := GetMoonRiseTime(julianDay, testCase.Longitude, testCase.Latitude, testCase.TimeZone, testCase.ZenithShift, testCase.Height) - if !moonRiseSetMatches(actualRise, riseErr, testCase.ExpectedRise, tolerance) { + riseMatches := moonRiseSetMatches(actualRise, riseErr, testCase.ExpectedRise, geometricTolerance) + if testCase.ZenithShift != 0 { + riseMatches = moonRiseSetDynamicMatches(actualRise, riseErr, testCase, testCase.ExpectedRise, + dynamicCycleTolerance, dynamicResidualDegrees) + } + if !riseMatches { t.Errorf("测试用例 %d 月出时间不匹配:\n"+ " 日期: %d-%d-%.1f, 经纬度: (%.4f, %.4f), 时区: %.1f, 天顶修正: %.0f, 海拔: %.0f\n"+ " 期望月出: %s, 实际月出: %.6f, 实际错误: %v, 差值: %.9f", @@ -648,7 +658,12 @@ func TestMoonRiseSetRegression(t *testing.T) { actualSet, setErr := GetMoonSetTime(julianDay, testCase.Longitude, testCase.Latitude, testCase.TimeZone, testCase.ZenithShift, testCase.Height) - if !moonRiseSetMatches(actualSet, setErr, testCase.ExpectedSet, tolerance) { + setMatches := moonRiseSetMatches(actualSet, setErr, testCase.ExpectedSet, geometricTolerance) + if testCase.ZenithShift != 0 { + setMatches = moonRiseSetDynamicMatches(actualSet, setErr, testCase, testCase.ExpectedSet, + dynamicCycleTolerance, dynamicResidualDegrees) + } + if !setMatches { t.Errorf("测试用例 %d 月落时间不匹配:\n"+ " 日期: %d-%d-%.1f, 经纬度: (%.4f, %.4f), 时区: %.1f, 天顶修正: %.0f, 海拔: %.0f\n"+ " 期望月落: %s, 实际月落: %.6f, 实际错误: %v, 差值: %.9f", @@ -750,6 +765,36 @@ func moonRiseSetMatches(actual float64, err error, expected float64, tolerance f return floatEquals(actual, expected, tolerance) } +func moonRiseSetDynamicMatches(actual float64, err error, testCase MoonRiseSetTestCase, legacyExpected, cycleTolerance, residualTolerance float64) bool { + if expectedErr := moonRiseSetExpectedError(legacyExpected); expectedErr != nil { + if errors.Is(expectedErr, ErrNotOnThisDate) && err == nil { + return moonRiseSetNearCivilBoundary(actual, testCase, cycleTolerance) && + moonRiseSetDynamicResidualMatches(actual, testCase, residualTolerance) + } + return errors.Is(err, expectedErr) + } + if errors.Is(err, ErrNotOnThisDate) { + return moonRiseSetNearCivilBoundary(legacyExpected, testCase, cycleTolerance) + } + if err != nil || math.Abs(actual-legacyExpected) > cycleTolerance { + return false + } + return moonRiseSetDynamicResidualMatches(actual, testCase, residualTolerance) +} + +func moonRiseSetDynamicResidualMatches(actual float64, testCase MoonRiseSetTestCase, residualTolerance float64) bool { + localTimeZone := testCase.Longitude / 15 + localJD := actual + localTimeZone/24 - testCase.TimeZone/24 + residual := moonRiseSetResidual(localJD, testCase.Longitude, testCase.Latitude, localTimeZone, + testCase.ZenithShift, testCase.Height, -1) + return math.Abs(residual) <= residualTolerance +} + +func moonRiseSetNearCivilBoundary(jd float64, testCase MoonRiseSetTestCase, tolerance float64) bool { + dayStart := math.Floor(JDECalc(testCase.Year, testCase.Month, testCase.Day)) + 0.5 + return math.Abs(jd-dayStart) <= tolerance || math.Abs(jd-(dayStart+1)) <= tolerance +} + // floatEquals 比较两个浮点数是否在给定容差内相等 func floatEquals(a, b, tolerance float64) bool { return math.Abs(a-b) <= tolerance diff --git a/basic/neptune.go b/basic/neptune.go index 12df924..0ce9b4d 100644 --- a/basic/neptune.go +++ b/basic/neptune.go @@ -173,14 +173,14 @@ func NeptuneCulminationTime(jde, lon, timezone float64) float64 { } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/neptune_events.go b/basic/neptune_events.go index 3101455..114fad0 100644 --- a/basic/neptune_events.go +++ b/basic/neptune_events.go @@ -64,6 +64,9 @@ func neptuneRADerivativeN(jde, delta float64, n int) float64 { func neptuneConjunctionFull(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := NEPTUNE_S_PERIOD / 360 currentDelta := neptuneSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -72,20 +75,29 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } func neptuneConjunction(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := NEPTUNE_S_PERIOD / 360 currentDelta := neptuneSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -94,24 +106,36 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := neptuneSunLongitudeDeltaN(prevJD, degree, true, neptuneEventSearchN) longitudeSlope := (neptuneSunLongitudeDeltaN(prevJD+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJD-0.000005, degree, true, neptuneEventSearchN)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= neptunePhaseCoarseTolerance { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= neptunePhaseCoarseTolerance { + converged = true break } } - for { + if !converged { + return math.NaN() + } + converged = false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } diff --git a/basic/occultation.go b/basic/occultation.go new file mode 100644 index 0000000..019e242 --- /dev/null +++ b/basic/occultation.go @@ -0,0 +1,518 @@ +package basic + +import ( + "errors" + "fmt" + "math" + "strings" + "time" +) + +// ErrInvalidOccultationInput 表示月掩输入契约无效。 +// ErrInvalidOccultationInput reports invalid lunar-occultation contract input. +var ErrInvalidOccultationInput = errors.New("invalid lunar occultation input") + +// ErrOccultationPathSamplingLimit 表示请求的时间步长、中心线间距或有限盘面采样超过确定性的工作量或输出预算。 +// ErrOccultationPathSamplingLimit reports that the requested time step, center-line spacing, or aggregate finite-disk sampling would exceed the implementation's deterministic work or output budget. +var ErrOccultationPathSamplingLimit = errors.New("lunar occultation path sampling limit exceeded") + +const ( + occultationSearchMinimumStep = 250 * time.Millisecond + occultationPathMinimumStep = time.Second + occultationPathMinimumTargetSpacingKM = 1.0 + occultationEventSelectionTolerance = 10 * time.Millisecond + occultationEventSelectionToleranceDays = float64(occultationEventSelectionTolerance) / float64(24*time.Hour) +) + +// CoordinateFrame 标识恒星输入坐标使用的赤道坐标系。 +// CoordinateFrame identifies the equatorial coordinate frame used by an input stellar coordinate. +type CoordinateFrame string + +const ( + // CoordinateFrameICRS 表示 ICRS 星表坐标系。 + // CoordinateFrameICRS is the ICRS catalog frame. + CoordinateFrameICRS CoordinateFrame = "icrs" + // CoordinateFrameJ2000 表示 J2000 平均赤道坐标系。 + // CoordinateFrameJ2000 is the mean equatorial J2000 frame. + CoordinateFrameJ2000 CoordinateFrame = "j2000" + // CoordinateFrameApparentOfDate 表示历元时刻的视赤道坐标系。 + // CoordinateFrameApparentOfDate is the apparent equatorial frame of date. + CoordinateFrameApparentOfDate CoordinateFrame = "apparent_of_date" +) + +// OccultationType 标识月掩结果的几何类型。 +// OccultationType identifies the result geometry. +type OccultationType string + +const ( + // OccultationTotal 表示掩甚时目标盘面被完全覆盖。 + // OccultationTotal means the target disk is fully covered at greatest occultation. + OccultationTotal OccultationType = "total" + // OccultationPartial 表示掩甚时有限目标盘面只有部分被覆盖。 + // OccultationPartial means only part of a finite target disk is covered at greatest occultation. + OccultationPartial OccultationType = "partial" + // OccultationGrazing 表示两边缘相切,且没有正持续时间的重叠。 + // OccultationGrazing means the limbs are tangent without a positive-duration overlap. + OccultationGrazing OccultationType = "grazing" +) + +// OccultationPlanet 标识有限盘面的行星目标。 +// OccultationPlanet identifies a finite-disk planetary target. +type OccultationPlanet string + +const ( + // OccultationMercury 表示水星有限盘面目标。 + // OccultationMercury identifies Mercury as the finite-disk target. + OccultationMercury OccultationPlanet = "mercury" + // OccultationVenus 表示金星有限盘面目标。 + // OccultationVenus identifies Venus as the finite-disk target. + OccultationVenus OccultationPlanet = "venus" + // OccultationMars 表示火星有限盘面目标。 + // OccultationMars identifies Mars as the finite-disk target. + OccultationMars OccultationPlanet = "mars" + // OccultationJupiter 表示木星有限盘面目标。 + // OccultationJupiter identifies Jupiter as the finite-disk target. + OccultationJupiter OccultationPlanet = "jupiter" + // OccultationSaturn 表示土星有限盘面目标。 + // OccultationSaturn identifies Saturn as the finite-disk target. + OccultationSaturn OccultationPlanet = "saturn" + // OccultationUranus 表示天王星有限盘面目标。 + // OccultationUranus identifies Uranus as the finite-disk target. + OccultationUranus OccultationPlanet = "uranus" + // OccultationNeptune 表示海王星有限盘面目标。 + // OccultationNeptune identifies Neptune as the finite-disk target. + OccultationNeptune OccultationPlanet = "neptune" +) + +// String 返回结果标识中使用的英文目标名称。 +// String returns the English target name used in result identifiers. +func (p OccultationPlanet) String() string { + switch p { + case OccultationMercury: + return "Mercury" + case OccultationVenus: + return "Venus" + case OccultationMars: + return "Mars" + case OccultationJupiter: + return "Jupiter" + case OccultationSaturn: + return "Saturn" + case OccultationUranus: + return "Uranus" + case OccultationNeptune: + return "Neptune" + default: + return "" + } +} + +// Validate 检查行星目标是否受支持。 +// Validate checks whether the planetary target is supported. +func (p OccultationPlanet) Validate() error { + if p.String() == "" { + return fmt.Errorf("%w: unsupported occultation planet %q", ErrInvalidOccultationInput, p) + } + return nil +} + +func validateOccultationTimeRange(start, end time.Time) error { + if start.IsZero() || end.IsZero() { + return fmt.Errorf("%w: start and end are required", ErrInvalidOccultationInput) + } + if !end.After(start) { + return fmt.Errorf("%w: end must be after start", ErrInvalidOccultationInput) + } + return nil +} + +func occultationTimeInSelectionWindow(value, start, end time.Time) bool { + return value.Sub(start) >= -occultationEventSelectionTolerance && + value.Sub(end) <= occultationEventSelectionTolerance +} + +// Observer 描述站心观测地点。 +// Observer describes the topocentric observing site. +type Observer struct { + // Longitude 是经度,东经为正,单位为度。 + // Longitude is east-positive, in degrees. + Longitude float64 + // Latitude 是纬度,北纬为正,单位为度。 + // Latitude is north-positive, in degrees. + Latitude float64 + // Height 是观测者相对平均海平面的高度,单位为米。 + // Height is the observer elevation above mean sea level, in meters. + Height float64 +} + +// moonTopocentricSemidiameterN 返回指定地点看到的月球角半径。 +// 通用 MoonSemidiameterN 使用地心距离;月掩接触使用同一站心视差修正后的观测者到月球距离。 +// moonTopocentricSemidiameterN returns the lunar angular radius as seen from the supplied site. +// The usual MoonSemidiameterN uses geocentric distance; occultation contacts use the observer-to-Moon distance after the same topocentric parallax correction as the direction. +func moonTopocentricSemidiameterN(tt float64, observer Observer, n int) float64 { + moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n) + moonDistanceKM := HMoonAwayN(tt, n) + if !finite(moonRA) || !finite(moonDec) || !finite(moonDistanceKM) || moonDistanceKM <= 0 { + return math.NaN() + } + distanceKM := topocentricDistanceKM(moonRA, moonDec, moonDistanceKM, observer, TD2UT(tt, false)) + if !finite(distanceKM) || distanceKM <= 0 { + return math.NaN() + } + return angularSemidiameterArcsec(moonEquatorialRadiusKM, distanceKM) +} + +// topocentricDistanceKM 使用与 TopocentricRaDec 相同的 WGS-84 风格站点因子计算观测者到目标的距离 / +// The target is supplied in apparent equatorial coordinates, and the sidereal angle uses UTC/UT like TopocentricRaDec. +func topocentricDistanceKM(ra, dec, distanceKM float64, observer Observer, ut float64) float64 { + const earthEquatorialRadius = 6378.14 + const astronomicalUnitKM = angularDiameterAstronomicalUnitKM + + distanceAU := distanceKM / astronomicalUnitKM + if distanceAU <= 0 { + return math.NaN() + } + raRad := ra * math.Pi / 180 + decRad := dec * math.Pi / 180 + moon := [3]float64{ + distanceAU * math.Cos(decRad) * math.Cos(raRad), + distanceAU * math.Cos(decRad) * math.Sin(raRad), + distanceAU * math.Sin(decRad), + } + theta := (ApparentSiderealTime(ut)*15 + observer.Longitude) * math.Pi / 180 + observerAU := earthEquatorialRadius / astronomicalUnitKM + observerVector := [3]float64{ + observerAU * pcosi(observer.Latitude, observer.Height) * math.Cos(theta), + observerAU * pcosi(observer.Latitude, observer.Height) * math.Sin(theta), + observerAU * psini(observer.Latitude, observer.Height), + } + dx := moon[0] - observerVector[0] + dy := moon[1] - observerVector[1] + dz := moon[2] - observerVector[2] + return math.Sqrt(dx*dx+dy*dy+dz*dz) * astronomicalUnitKM +} + +// Validate 检查站心计算所需的地理范围。 +// Validate checks the geographic bounds needed by topocentric calculations. +func (o Observer) Validate() error { + if !finite(o.Longitude) || !finite(o.Latitude) || !finite(o.Height) { + return fmt.Errorf("%w: observer values must be finite", ErrInvalidOccultationInput) + } + if o.Longitude < -180 || o.Longitude > 180 { + return fmt.Errorf("%w: observer longitude must be in [-180, 180]", ErrInvalidOccultationInput) + } + if o.Latitude < -90 || o.Latitude > 90 { + return fmt.Errorf("%w: observer latitude must be in [-90, 90]", ErrInvalidOccultationInput) + } + return nil +} + +// StarCoordinate 是调用者为恒星提供的星表坐标或视位置坐标。 +// RA 和 Dec 的单位为度;ProperMotionRACosDecMasPerYear 使用星表常见的 dRA*cos(Dec) 约定,单位为毫角秒/年。 +// StarCoordinate is a catalog or apparent coordinate supplied for a star. +// RA and Dec are degrees; ProperMotionRACosDecMasPerYear uses the usual catalog convention of dRA*cos(Dec), in milliarcseconds per year. +type StarCoordinate struct { + ID string + + RA float64 + Dec float64 + Epoch time.Time + Frame CoordinateFrame + + ProperMotionRACosDecMasPerYear float64 + ProperMotionDecMasPerYear float64 + ParallaxMas float64 +} + +// Validate 在构造目标前检查恒星坐标契约。 +// Validate checks the coordinate contract before a target is constructed. +func (s StarCoordinate) Validate() error { + if !finite(s.RA) || s.RA < 0 || s.RA >= 360 { + return fmt.Errorf("%w: star RA must be in [0, 360)", ErrInvalidOccultationInput) + } + if !finite(s.Dec) || s.Dec < -90 || s.Dec > 90 { + return fmt.Errorf("%w: star Dec must be in [-90, 90]", ErrInvalidOccultationInput) + } + if s.Epoch.IsZero() { + return fmt.Errorf("%w: star epoch is required", ErrInvalidOccultationInput) + } + if !validCoordinateFrame(s.Frame) { + return fmt.Errorf("%w: unsupported star coordinate frame %q", ErrInvalidOccultationInput, s.Frame) + } + if !finite(s.ProperMotionRACosDecMasPerYear) || !finite(s.ProperMotionDecMasPerYear) { + return fmt.Errorf("%w: star proper motion must be finite", ErrInvalidOccultationInput) + } + if !finite(s.ParallaxMas) || s.ParallaxMas < 0 { + return fmt.Errorf("%w: star parallax must be finite and non-negative", ErrInvalidOccultationInput) + } + return nil +} + +// StarCoordinateFromStarData 将一条内嵌星表记录转换为月掩搜索使用的 J2000 坐标契约。 +// 星表自行从角秒/年转换为毫角秒/年;正的秒差距距离转换为毫角秒年视差。本函数只转换传入值,不会加载星表。 +// StarCoordinateFromStarData converts one embedded-catalog entry into the J2000 coordinate contract used by lunar-occultation searches. +// The catalog's proper motions are converted from arcseconds/year to milliarcseconds/year; a positive parsec distance is converted to annual parallax in milliarcseconds. +// This function only converts the supplied value and never loads the catalog. +func StarCoordinateFromStarData(star StarData) (StarCoordinate, error) { + if star.HR == 0 { + return StarCoordinate{}, fmt.Errorf("%w: star catalog HR number is required", ErrInvalidOccultationInput) + } + if !finite(star.Pc) || star.Pc < 0 { + return StarCoordinate{}, fmt.Errorf("%w: star distance must be finite and non-negative", ErrInvalidOccultationInput) + } + + parallaxMas := 0.0 + if star.Pc > 0 { + parallaxMas = 1000 / star.Pc + } + coordinate := StarCoordinate{ + ID: starCoordinateIDFromStarData(star), + RA: star.Ra, + Dec: star.Dec, + Epoch: time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC), + Frame: CoordinateFrameJ2000, + ProperMotionRACosDecMasPerYear: star.PmRA * 1000, + ProperMotionDecMasPerYear: star.PmDec * 1000, + ParallaxMas: parallaxMas, + } + if err := coordinate.Validate(); err != nil { + return StarCoordinate{}, fmt.Errorf("convert star catalog coordinate: %w", err) + } + return coordinate, nil +} + +func starCoordinateIDFromStarData(star StarData) string { + for _, name := range []string{star.ChineseName, star.ChineseAlias, star.CommonName, star.Name} { + if name = strings.TrimSpace(name); name != "" { + return name + } + } + if star.HR > 0 { + return fmt.Sprintf("HR %d", star.HR) + } + return "" +} + +// OccultationSearchOptions 控制固定目标和行星月掩搜索。 +// 零值使用实现默认值;MaxEvents == 0 表示不限制数量。 +// OccultationSearchOptions controls fixed-target and planetary occultation searches. +// Zero values select implementation defaults; MaxEvents == 0 means unlimited. +type OccultationSearchOptions struct { + // MaxStep 是粗略搜索的最大步长;小于 250ms 的正值会被拒绝,因为在支持的时间范围内无法可靠地用儒略日浮点数推进。 + // MaxStep is the maximum coarse-search step. Positive values below 250 ms are rejected because they cannot be advanced reliably in Julian-day floating-point arithmetic over the supported time span. + MaxStep time.Duration + // SafetyMarginArcsec 是加入粗略候选和黄纬预筛的安全余量,单位为角秒。 + // SafetyMarginArcsec is added to coarse candidate and latitude prefilters. + SafetyMarginArcsec float64 + // MaxEvents 为正时限制返回事件数量。 + // MaxEvents limits the number of returned events when positive. + MaxEvents int +} + +// OccultationPathOptions 控制全球月掩路径采样。 +// +// Step 为路径采样的基础时间步长,正值至少为 1 秒。TargetSpacingKM 要求相邻中心线点超过目标地面距离时进行自适应加密。 +// 正的 TargetSpacingKM 至少为 1 km;超过中心线或有限盘面路径工作量预算时返回 ErrOccultationPathSamplingLimit,不会静默降低请求分辨率。行星瞬时足迹使用结果中说明的独立有界采样策略。 +// OccultationPathOptions controls global occultation-path sampling. +// Step is the base time step used for path samples; positive values must be at least one second. TargetSpacingKM requests adaptive refinement when adjacent center-line points exceed the requested ground distance. +// Positive TargetSpacingKM values must be at least 1 km. Requests that exceed the center-line or aggregate finite-disk work budgets return ErrOccultationPathSamplingLimit instead of silently reducing resolution. Planetary instantaneous footprints have a separate bounded sampling policy documented on the result. +type OccultationPathOptions struct { + Step time.Duration + TargetSpacingKM float64 +} + +// Validate 检查全球路径采样选项。 +// Validate checks global path sampling options. +func (o OccultationPathOptions) Validate() error { + if o.Step < 0 { + return fmt.Errorf("%w: path step cannot be negative", ErrInvalidOccultationInput) + } + if o.Step > 0 && o.Step < occultationPathMinimumStep { + return fmt.Errorf("%w: path step must be zero or at least %s", ErrInvalidOccultationInput, occultationPathMinimumStep) + } + if !finite(o.TargetSpacingKM) || o.TargetSpacingKM < 0 { + return fmt.Errorf("%w: path target spacing must be finite and non-negative", ErrInvalidOccultationInput) + } + if o.TargetSpacingKM > 0 && o.TargetSpacingKM < occultationPathMinimumTargetSpacingKM { + return fmt.Errorf("%w: path target spacing must be zero or at least %.0f km", ErrInvalidOccultationInput, occultationPathMinimumTargetSpacingKM) + } + return nil +} + +// Validate 检查选项值,但不选择算法专用默认值。 +// Validate checks option values without selecting algorithm-specific defaults. +func (o OccultationSearchOptions) Validate() error { + if o.MaxStep < 0 { + return fmt.Errorf("%w: search max step cannot be negative", ErrInvalidOccultationInput) + } + if o.MaxStep > 0 && o.MaxStep < occultationSearchMinimumStep { + return fmt.Errorf("%w: search max step must be zero or at least %s", ErrInvalidOccultationInput, occultationSearchMinimumStep) + } + if !finite(o.SafetyMarginArcsec) || o.SafetyMarginArcsec < 0 { + return fmt.Errorf("%w: search safety margin must be finite and non-negative", ErrInvalidOccultationInput) + } + if o.MaxEvents < 0 { + return fmt.Errorf("%w: search max events cannot be negative", ErrInvalidOccultationInput) + } + return nil +} + +// StarOccultationInfo 描述点光源恒星月掩;掩始和掩终是月缘交点。 +// StarOccultationInfo describes a point-source stellar occultation. The immersion and emersion times are the Moon-limb crossings. +type StarOccultationInfo struct { + TargetID string + Observer Observer + Type OccultationType + + Immersion time.Time + Greatest time.Time + Emersion time.Time + // ContactsComplete 表示两个月缘接触时刻均已求解。 + // ContactsComplete is true when both lunar-limb contacts were solved. + ContactsComplete bool + + MinimumSeparationArcsec float64 + PositionAngleDeg float64 + MoonSemidiameterArcsec float64 + + MoonAltitudeAtGreatest float64 + MoonAzimuthAtGreatest float64 + VisibleAtGreatest bool +} + +// PlanetOccultationInfo 描述有限盘面行星月掩。 +// ExternalImmersion 和 ExternalEmersion 分别是 C1 和 C4;全掩事件的 InternalImmersion 和 InternalEmersion 分别是 C2 和 C3,偏掩和掠掩时为零。 +// ContactsComplete 表示报告几何适用的所有接触均已求解;目标按赤道半径建模为圆盘,环、大气延伸和扁率不在模型内。 +// PlanetOccultationInfo describes a finite-disk planetary occultation. +// ExternalImmersion and ExternalEmersion are C1 and C4. For a total event, InternalImmersion and InternalEmersion are C2 and C3; they are zero for partial and grazing events. +// ContactsComplete means every contact applicable to the reported geometry was solved. The target is modeled as a circular disk using its equatorial body radius; rings, atmospheric extensions, and oblateness are outside this contact model. +type PlanetOccultationInfo struct { + Planet OccultationPlanet + TargetID string + Observer Observer + Type OccultationType + + ExternalImmersion time.Time + InternalImmersion time.Time + Greatest time.Time + InternalEmersion time.Time + ExternalEmersion time.Time + + HasInternalContacts bool + ContactsComplete bool + + MinimumSeparationArcsec float64 + PositionAngleDeg float64 + MoonSemidiameterArcsec float64 + PlanetSemidiameterArcsec float64 + + MoonAltitudeAtGreatest float64 + MoonAzimuthAtGreatest float64 + VisibleAtGreatest bool +} + +// OccultationPathPoint 是全球月掩路径上的一个地理采样点。 +// Start 和 End 描述月缘外接触掩带;WidthKM 是垂直地面轨迹方向的切平面宽度,仅对中心线采样点有意义。 +// 基础采样直接求解,自适应插入点使用宽度插值并进行五米采样误差检查。 +// OccultationPathPoint is a geographic sample of a global lunar-occultation path. +// Start and End describe the outer lunar-limb footprint. WidthKM is the local tangent-plane width perpendicular to the ground track and is meaningful only on center-line samples. +// Base samples are solved directly; adaptive samples use width interpolation and five-meter error checks. +type OccultationPathPoint struct { + Time time.Time + Longitude float64 + Latitude float64 + MoonAltitude float64 + WidthKM float64 +} + +// StarOccultationPath 包含点光源恒星月掩的全球掩带。 +// 中心线是月心与恒星对齐的轨迹;NorthernLimit 和 SouthernLimit 是中心线两侧采样的月缘外边界。 +// StarOccultationPath contains the global footprint of a point-source stellar occultation. +// The center line is the locus where the lunar center aligns with the star; NorthernLimit and SouthernLimit are the two outer lunar-limb boundaries sampled beside that line. +type StarOccultationPath struct { + TargetID string + + Start OccultationPathPoint + Greatest OccultationPathPoint + End OccultationPathPoint + // Complete 表示 Start 和 End 是全球月缘外接触点,而不是查询窗口裁剪点。 + // Complete is true when Start and End are the global outer-limb contacts rather than query-window clipping points. + Complete bool + + CenterLine []OccultationPathPoint + NorthernLimit []OccultationPathPoint + SouthernLimit []OccultationPathPoint + + Step time.Duration + TargetSpacingKM float64 +} + +// PlanetOccultationFootprint 是一个时刻的可见接触足迹。 +// Polygons 包含接触锥圆弧;当锥面与椭球的交线在朝月半球开放时,沿月球地平线闭合。 +// PlanetOccultationFootprint is one instantaneous visible contact footprint. +// Polygons contain contact-cone arcs closed along the lunar horizon when the cone/ellipsoid intersection is open on the Moon-facing hemisphere. +type PlanetOccultationFootprint struct { + Time time.Time + Polygons [][]OccultationPathPoint +} + +// PlanetOccultationPath 包含有限盘面行星月掩的全球掩带。 +// NorthernLimit 和 SouthernLimit 是行星盘面任意部分被覆盖的外接触边界。 +// HasTotalBand 为 true 时,NorthernTotalLimit 和 SouthernTotalLimit 是行星圆盘完全被月球覆盖的内接触边界; +// 环、大气延伸和扁率不在两种接触模型内。 +// PlanetOccultationPath contains the global footprint of a finite-disk planetary occultation. +// NorthernLimit and SouthernLimit are the outer-contact boundaries where any part of the planet disk is covered. +// When HasTotalBand is true, NorthernTotalLimit and SouthernTotalLimit are the inner-contact boundaries where the complete circular planet disk is covered by the Moon. Rings, atmospheric extensions, and oblateness are outside both contact models. +type PlanetOccultationPath struct { + Planet OccultationPlanet + TargetID string + + Start OccultationPathPoint + Greatest OccultationPathPoint + End OccultationPathPoint + // Complete 表示 Start 和 End 是全球外接触点。 + // Complete is true when Start and End are the global outer contacts. + Complete bool + + CenterLine []OccultationPathPoint + NorthernLimit []OccultationPathPoint + SouthernLimit []OccultationPathPoint + // PartialFootprints 是时刻采样的可见外接触区域,其扫掠构成全球偏掩区域;为限制输出和运行时间,采样可能比 Step 更粗, + // 每个足迹携带实际采样时刻。 + // PartialFootprints are instantaneous visible outer-contact regions whose sweep forms the global partial-occultation area. To bound output and runtime, sampling may be coarser than Step; each footprint carries its actual sample time. + PartialFootprints []PlanetOccultationFootprint + + HasTotalBand bool + // TotalStart 和 TotalEnd 是全球内接触的起止点。 + // TotalStart and TotalEnd are the first and last global inner contacts. + TotalStart OccultationPathPoint + TotalEnd OccultationPathPoint + // TotalComplete 表示 TotalStart 和 TotalEnd 未被内部搜索范围截断。 + // TotalComplete is true when TotalStart and TotalEnd are not clipped by the internal search span. + TotalComplete bool + + NorthernTotalLimit []OccultationPathPoint + SouthernTotalLimit []OccultationPathPoint + // TotalFootprints 是时刻采样的可见内接触区域,其扫掠构成全球全掩区域;采样使用与 PartialFootprints 相同的有界策略。 + // TotalFootprints are instantaneous visible inner-contact regions whose sweep forms the global full-coverage area. Their sampling uses the same bounded policy as PartialFootprints. + TotalFootprints []PlanetOccultationFootprint + // GreatestTotalWidthKM 是全球掩甚时的全掩带宽度。 + // GreatestTotalWidthKM is the full-coverage band width at global greatest. + GreatestTotalWidthKM float64 + + Step time.Duration + TargetSpacingKM float64 +} + +func finite(value float64) bool { + return !math.IsNaN(value) && !math.IsInf(value, 0) +} + +func validCoordinateFrame(frame CoordinateFrame) bool { + switch frame { + case CoordinateFrameICRS, CoordinateFrameJ2000, CoordinateFrameApparentOfDate: + return true + default: + return false + } +} diff --git a/basic/occultation_path.go b/basic/occultation_path.go new file mode 100644 index 0000000..bfedd5a --- /dev/null +++ b/basic/occultation_path.go @@ -0,0 +1,1040 @@ +package basic + +import ( + "math" + "sort" + "time" +) + +const ( + occultationPathDefaultStepDays = 1.0 / 1440.0 + occultationPathMinStepDays = 1.0 / 86400.0 + occultationPathMaxSampleCount = 30000 + occultationPathMaxAdaptiveDepth = 20 + occultationPathBoundarySpacingKM = 500.0 + occultationPathVelocityStepDays = 1.0 / 1440.0 + occultationPathBoundaryScanPoints = 720 + occultationPathRootToleranceDays = occultationEventSelectionToleranceDays + occultationPathRangeStepDays = 5.0 / 1440.0 + occultationPathSearchSpanDays = 2.0 + occultationPathWidthToleranceKM = 0.005 + + occultationPathEarthEquatorialRadiusKM = 6378.1366 + occultationPathEarthPolarRatio = 0.99664719 + occultationPathAstronomicalUnitKM = 149597870.7 +) + +// FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带。 +// 查询窗口按全球几何掩甚点选择事件,端点容差为 10 ms,与数值根精度一致。返回路径扩展到完整全球起止点;函数不会加载内嵌星表,调用者需显式提供坐标。 +// FindStarOccultationPaths searches the global lunar-occultation footprint of one point-source star. +// The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end; the function does not load the embedded catalog. +func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) { + if err := validateOccultationTimeRange(start, end); err != nil { + return nil, err + } + if err := star.Validate(); err != nil { + return nil, err + } + if err := options.Validate(); err != nil { + return nil, err + } + + options = normalizeOccultationPathOptions(options) + startTT := occultationTimeToTT(start) + endTT := occultationTimeToTT(end) + candidateStartTT := startTT - occultationPathSearchSpanDays + candidateEndTT := endTT + occultationPathSearchSpanDays + coarseOptions := OccultationSearchOptions{} + candidates := starOccultationGeocentricCandidateGreatestTimes( + candidateStartTT, + candidateEndTT, + starOccultationCoarseStepDays(coarseOptions), + star, + 0, + ) + paths := make([]StarOccultationPath, 0, len(candidates)) + for _, seedTT := range candidates { + path, ok, err := starOccultationPathAtSeed(seedTT, star, options, start.Location()) + if err != nil { + return nil, err + } + if !ok { + continue + } + if !occultationTimeInSelectionWindow(path.Greatest.Time, start, end) { + continue + } + if len(paths) > 0 && math.Abs(paths[len(paths)-1].Greatest.Time.Sub(path.Greatest.Time).Seconds()) <= 60 { + continue + } + paths = append(paths, path) + } + sort.SliceStable(paths, func(i, j int) bool { + return paths[i].Greatest.Time.Before(paths[j].Greatest.Time) + }) + return paths, nil +} + +func normalizeOccultationPathOptions(options OccultationPathOptions) OccultationPathOptions { + if options.Step <= 0 { + options.Step = time.Duration(occultationPathDefaultStepDays * float64(24*time.Hour)) + } + if float64(options.Step)/float64(24*time.Hour) < occultationPathMinStepDays { + options.Step = time.Second + } + if options.TargetSpacingKM <= 0 || math.IsNaN(options.TargetSpacingKM) || math.IsInf(options.TargetSpacingKM, 0) { + options.TargetSpacingKM = 0 + } + return options +} + +func starOccultationPathAtSeed( + seedTT float64, + star StarCoordinate, + options OccultationPathOptions, + location *time.Location, +) (StarOccultationPath, bool, error) { + searchStart := seedTT - occultationPathSearchSpanDays + searchEnd := seedTT + occultationPathSearchSpanDays + outerStart, outerEnd, ok := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, false) + if !ok { + return StarOccultationPath{}, false, nil + } + + centerStart, centerEnd, hasCenter := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, true) + greatestTT := starOccultationPathGreatest(seedTT, outerStart, outerEnd, star) + greatest, greatestOK := starOccultationPathCenterPoint(greatestTT, star, location) + if !greatestOK { + if hasCenter { + greatestTT = math.Max(centerStart, math.Min(centerEnd, greatestTT)) + greatest, greatestOK = starOccultationPathCenterPoint(greatestTT, star, location) + } + } + if !greatestOK { + frameAt := func(tt float64) (occultationPathFrame, bool) { + return starOccultationPathFrameAt(tt, star) + } + greatest, greatestOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, location) + } + if !greatestOK { + return StarOccultationPath{}, false, nil + } + + start := starOccultationPathBoundaryEndpoint(outerStart, star, location, 1) + end := starOccultationPathBoundaryEndpoint(outerEnd, star, location, -1) + if !start.valid || !end.valid { + return StarOccultationPath{}, false, nil + } + + path := StarOccultationPath{ + TargetID: star.ID, + Start: start.point, + Greatest: greatest, + End: end.point, + Complete: outerStart > searchStart && outerEnd < searchEnd, + Step: options.Step, + TargetSpacingKM: options.TargetSpacingKM, + } + centerLine, northern, southern, err := starOccultationPathSamples( + outerStart, + outerEnd, + centerStart, + centerEnd, + hasCenter, + greatestTT, + star, + options, + location, + ) + if err != nil { + return StarOccultationPath{}, false, err + } + path.CenterLine = centerLine + path.NorthernLimit = occultationPathWithEndpoints(start.point, end.point, northern) + path.SouthernLimit = occultationPathWithEndpoints(start.point, end.point, southern) + return path, true, nil +} + +func occultationPathWithEndpoints(start, end OccultationPathPoint, points []OccultationPathPoint) []OccultationPathPoint { + result := make([]OccultationPathPoint, 0, len(points)+2) + result = append(result, start) + for _, point := range points { + if point.Time.After(start.Time) && point.Time.Before(end.Time) { + result = append(result, point) + } + } + return append(result, end) +} + +func starOccultationPathWindow(seedTT, startTT, endTT float64, star StarCoordinate, center bool) (float64, float64, bool) { + left := math.Max(startTT, seedTT-occultationPathSearchSpanDays) + right := math.Min(endTT, seedTT+occultationPathSearchSpanDays) + if right <= left { + return 0, 0, false + } + predicate := func(tt float64) bool { + frame, ok := starOccultationPathFrameAt(tt, star) + if !ok { + return false + } + if center { + _, _, ok = occultationEarthLineIntersection(frame.moon, frame.axis) + return ok + } + return starOccultationPathHasBoundary(frame) + } + + step := occultationPathRangeStepDays + first := math.NaN() + previous := left + previousOK := predicate(previous) + if previousOK { + first = previous + } else { + for tt := left + step; tt <= right; tt += step { + current := math.Min(tt, right) + currentOK := predicate(current) + if currentOK { + first = occultationPathRefineTransition(previous, current, predicate, false) + break + } + previous = current + previousOK = currentOK + } + } + if math.IsNaN(first) { + return 0, 0, false + } + + last := first + previous = first + previousOK = true + for tt := first + step; tt <= right; tt += step { + current := math.Min(tt, right) + currentOK := predicate(current) + if !currentOK { + last = occultationPathRefineTransition(previous, current, predicate, true) + return first, last, true + } + last = current + previous = current + previousOK = currentOK + } + if previousOK { + last = right + } + return first, last, true +} + +func occultationPathRefineTransition(left, right float64, predicate func(float64) bool, trueToFalse bool) float64 { + leftOK := predicate(left) + for i := 0; i < 48 && math.Abs(right-left) > occultationPathRootToleranceDays; i++ { + mid := (left + right) / 2 + midOK := predicate(mid) + if trueToFalse { + if midOK { + left = mid + } else { + right = mid + } + continue + } + if midOK { + right = mid + } else { + left = mid + } + } + if trueToFalse { + return left + } + if leftOK { + return left + } + return right +} + +func starOccultationPathGreatest(seedTT, startTT, endTT float64, star StarCoordinate) float64 { + left := math.Max(startTT, seedTT-0.75) + right := math.Min(endTT, seedTT+0.75) + if right <= left { + return seedTT + } + const goldenRatio = 0.6180339887498949 + x1 := right - goldenRatio*(right-left) + x2 := left + goldenRatio*(right-left) + f1 := starOccultationPathImpact(x1, star) + f2 := starOccultationPathImpact(x2, star) + for i := 0; i < 56; i++ { + if f1 > f2 { + left = x1 + x1 = x2 + f1 = f2 + x2 = left + goldenRatio*(right-left) + f2 = starOccultationPathImpact(x2, star) + } else { + right = x2 + x2 = x1 + f2 = f1 + x1 = right - goldenRatio*(right-left) + f1 = starOccultationPathImpact(x1, star) + } + } + return (left + right) / 2 +} + +func starOccultationPathImpact(tt float64, star StarCoordinate) float64 { + frame, ok := starOccultationPathFrameAt(tt, star) + if !ok { + return math.Inf(1) + } + return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY()) +} + +func starOccultationPathSamples( + outerStartTT, outerEndTT float64, + centerStartTT, centerEndTT float64, + hasCenter bool, + greatestTT float64, + star StarCoordinate, + options OccultationPathOptions, + location *time.Location, +) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) { + var points []OccultationPathPoint + if hasCenter { + var err error + points, err = starOccultationPathCenterSamples(centerStartTT, centerEndTT, greatestTT, star, options, location) + if err != nil { + return nil, nil, nil, err + } + } + + frameAt := func(tt float64) (occultationPathFrame, bool) { + return starOccultationPathFrameAt(tt, star) + } + northern, southern := occultationPathBoundarySamplesForFrame( + outerStartTT, outerEndTT, greatestTT, frameAt, options, location, + ) + return points, northern, southern, nil +} + +func starOccultationPathCenterSamples( + startTT, endTT, greatestTT float64, + star StarCoordinate, + options OccultationPathOptions, + location *time.Location, +) ([]OccultationPathPoint, error) { + stepDays := float64(options.Step) / float64(24*time.Hour) + times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays) + points := make([]OccultationPathPoint, 0, len(times)) + for _, tt := range times { + point, ok := starOccultationPathCenterPoint(tt, star, location) + if ok { + points = append(points, point) + } + } + if options.TargetSpacingKM > 0 { + return refineOccultationPathSpacing(points, star, options.TargetSpacingKM, location) + } + return points, nil +} + +func occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays float64) []float64 { + return occultationPathSampleTimesWithLimit( + startTT, endTT, greatestTT, stepDays, occultationPathMaxSampleCount, + ) +} + +func occultationPathSampleTimesWithLimit( + startTT, endTT, greatestTT, stepDays float64, + maximumCount int, +) []float64 { + if endTT < startTT { + startTT, endTT = endTT, startTT + } + if maximumCount < 3 { + maximumCount = 3 + } + duration := endTT - startTT + if duration <= 0 { + return []float64{startTT} + } + if stepDays <= 0 || !finite(stepDays) { + stepDays = duration + } + baseSampleCount := int(math.Ceil(duration/stepDays)) + 1 + if baseSampleCount+1 > maximumCount { + times := []float64{startTT, greatestTT, endTT} + interiorCount := maximumCount - len(times) + for index := 1; index <= interiorCount; index++ { + times = append(times, startTT+duration*float64(index)/float64(interiorCount+1)) + } + sort.Float64s(times) + return uniqueOccultationPathTimes(times) + } + times := []float64{startTT, greatestTT, endTT} + for index := 1; ; index++ { + tt := startTT + float64(index)*stepDays + if tt >= endTT { + break + } + times = append(times, tt) + } + sort.Float64s(times) + return uniqueOccultationPathTimes(times) +} + +func refineOccultationPathSpacing( + points []OccultationPathPoint, + star StarCoordinate, + targetSpacingKM float64, + location *time.Location, +) ([]OccultationPathPoint, error) { + if len(points) < 2 || targetSpacingKM <= 0 { + return points, nil + } + refined := make([]OccultationPathPoint, 0, len(points)) + refined = append(refined, points[0]) + widthAt := func(tt float64) (float64, bool) { + _, _, width, ok := starOccultationPathLimitsAndWidthAt(tt, star) + return width, ok + } + for i := 1; i < len(points); i++ { + segmentStart := len(refined) - 1 + var err error + refined, err = appendOccultationPathSegment(refined, points[i-1], points[i], star, targetSpacingKM, location, 0) + if err != nil { + return nil, err + } + refineOccultationPathWidths(refined[segmentStart:], widthAt) + } + return refined, nil +} + +func appendOccultationPathSegment( + points []OccultationPathPoint, + start, end OccultationPathPoint, + star StarCoordinate, + targetSpacingKM float64, + location *time.Location, + depth int, +) ([]OccultationPathPoint, error) { + distance := occultationPathDistanceKM(start, end) + if distance <= targetSpacingKM { + if len(points) >= occultationPathMaxSampleCount { + return nil, ErrOccultationPathSamplingLimit + } + return append(points, end), nil + } + if depth >= occultationPathMaxAdaptiveDepth || len(points) >= occultationPathMaxSampleCount { + return nil, ErrOccultationPathSamplingLimit + } + startTT := centerTimeTT(start.Time) + endTT := centerTimeTT(end.Time) + midTT := (startTT + endTT) / 2 + mid, ok := starOccultationPathCenterPointWithoutWidth(midTT, star, location) + if !ok { + return append(points, end), nil + } + mid.WidthKM = (start.WidthKM + end.WidthKM) / 2 + var err error + points, err = appendOccultationPathSegment(points, start, mid, star, targetSpacingKM, location, depth+1) + if err != nil { + return nil, err + } + return appendOccultationPathSegment(points, mid, end, star, targetSpacingKM, location, depth+1) +} + +func uniqueOccultationPathTimes(times []float64) []float64 { + if len(times) < 2 { + return times + } + unique := times[:1] + for _, tt := range times[1:] { + if math.Abs(tt-unique[len(unique)-1]) <= 1e-10 { + continue + } + unique = append(unique, tt) + } + return unique +} + +type occultationPathFrame struct { + moon occultationPathVector + axis occultationPathVector + first occultationPathVector + second occultationPathVector + moonRadius float64 + targetRadius float64 +} + +type occultationPathEndpoint struct { + point OccultationPathPoint + valid bool +} + +func (f occultationPathFrame) moonProjectionX() float64 { return occultationPathDot(f.moon, f.first) } +func (f occultationPathFrame) moonProjectionY() float64 { return occultationPathDot(f.moon, f.second) } + +func starOccultationPathFrameAt(tt float64, star StarCoordinate) (occultationPathFrame, bool) { + moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1) + moonDistance := HMoonAwayN(tt, -1) + if !finite(moonRA) || !finite(moonDec) || !finite(moonDistance) || moonDistance <= 0 { + return occultationPathFrame{}, false + } + moon := occultationPathRaDecVector(moonRA, moonDec, moonDistance) + starRA, starDec := starApparentRaDecGeocentric(tt, star) + starDirection := occultationPathRaDecVector(starRA, starDec, 1) + axis := occultationPathScale(starDirection, -1) + if star.ParallaxMas > 0 { + distanceAU := 206264806.247 / star.ParallaxMas + target := occultationPathRaDecVector(starRA, starDec, distanceAU*occultationPathAstronomicalUnitKM) + axis = occultationPathScale(occultationPathSub(target, moon), -1) + } + axis = occultationPathUnit(axis) + north := occultationPathVector{z: 1} + first := occultationPathCross(north, axis) + if occultationPathNorm(first) < 1e-12 { + first = occultationPathCross(occultationPathVector{x: 1}, axis) + } + first = occultationPathUnit(first) + second := occultationPathUnit(occultationPathCross(axis, first)) + return occultationPathFrame{ + moon: moon, + axis: axis, + first: first, + second: second, + moonRadius: MoonSemidiameter(tt) * math.Pi / (180 * 3600), + }, true +} + +func starOccultationPathHasBoundary(frame occultationPathFrame) bool { + _, _, ok := occultationPathBoundaryTangent(frame) + return ok +} + +func starOccultationPathBoundaryEndpoint(tt float64, star StarCoordinate, location *time.Location, direction int) occultationPathEndpoint { + if _, ok := starOccultationPathFrameAt(tt, star); !ok { + return occultationPathEndpoint{} + } + for offset := 0; offset <= 3; offset++ { + candidateTT := tt + float64(direction)*float64(offset)*0.5/86400.0 + candidateFrame, candidateOK := starOccultationPathFrameAt(candidateTT, star) + if !candidateOK { + continue + } + vector, _, valid := occultationPathBoundaryTangent(candidateFrame) + if valid { + return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true} + } + } + return occultationPathEndpoint{} +} + +func starOccultationPathCenterPoint(tt float64, star StarCoordinate, location *time.Location) (OccultationPathPoint, bool) { + frame, ok := starOccultationPathFrameAt(tt, star) + if !ok { + return OccultationPathPoint{}, false + } + point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis) + if !ok { + return OccultationPathPoint{}, false + } + width := 0.0 + if _, _, tangentWidth, limitsOK := starOccultationPathLimitsAndWidthAt(tt, star); limitsOK { + width = tangentWidth + } + return occultationPathPointFromVectorWithMoon(tt, point, width, frame.moon, location), true +} + +func starOccultationPathCenterPointWithoutWidth(tt float64, star StarCoordinate, location *time.Location) (OccultationPathPoint, bool) { + frame, ok := starOccultationPathFrameAt(tt, star) + if !ok { + return OccultationPathPoint{}, false + } + point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis) + if !ok { + return OccultationPathPoint{}, false + } + return occultationPathPointFromVectorWithMoon(tt, point, 0, frame.moon, location), true +} + +func starOccultationPathLimitsAndWidthAt(tt float64, star StarCoordinate) (occultationPathVector, occultationPathVector, float64, bool) { + frameAt := func(candidateTT float64) (occultationPathFrame, bool) { + return starOccultationPathFrameAt(candidateTT, star) + } + return occultationPathLimitsAndWidthForFrame(tt, frameAt) +} + +func occultationPathScannedLimitsAtFrame(tt float64, frame occultationPathFrame) (occultationPathVector, occultationPathVector, bool) { + var northern, southern occultationPathVector + northLatitude := math.Inf(-1) + southLatitude := math.Inf(1) + consider := func(vector occultationPathVector) { + _, latitude := occultationPathGeodetic(tt, vector) + if latitude > northLatitude { + northLatitude = latitude + northern = vector + } + if latitude < southLatitude { + southLatitude = latitude + southern = vector + } + } + if tangentPoint, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame); tangentOK { + consider(tangentPoint) + if leftTheta, rightTheta, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta); intervalOK { + const intervalSamples = 128 + for i := 0; i <= intervalSamples; i++ { + theta := leftTheta + (rightTheta-leftTheta)*float64(i)/intervalSamples + if vector, _, ok := occultationPathBoundaryVector(frame, theta); ok { + consider(vector) + } + } + } + } + for i := 0; i < occultationPathBoundaryScanPoints; i++ { + vector, _, ok := occultationPathBoundaryVector(frame, 2*math.Pi*float64(i)/float64(occultationPathBoundaryScanPoints)) + if !ok { + continue + } + consider(vector) + } + return northern, southern, finite(northLatitude) && finite(southLatitude) +} + +func occultationPathBoundaryThetaInterval(frame occultationPathFrame, centerTheta float64) (float64, float64, bool) { + if !occultationPathBoundaryLineIntersects(frame, centerTheta) { + return 0, 0, false + } + step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints) + findEdge := func(direction float64) (float64, bool) { + inside := centerTheta + for i := 1; i <= occultationPathBoundaryScanPoints; i++ { + outside := centerTheta + direction*step*float64(i) + if occultationPathBoundaryLineIntersects(frame, outside) { + inside = outside + continue + } + for iteration := 0; iteration < 56; iteration++ { + mid := (inside + outside) / 2 + if occultationPathBoundaryLineIntersects(frame, mid) { + inside = mid + } else { + outside = mid + } + } + return inside, true + } + return 0, false + } + left, leftOK := findEdge(-1) + right, rightOK := findEdge(1) + return left, right, leftOK && rightOK && right > left +} + +func occultationPathBoundaryLineIntersects(frame occultationPathFrame, theta float64) bool { + discriminant, b, _, ok := occultationPathBoundaryLine(frame, theta) + return ok && b < 0 && discriminant >= 0 +} + +func occultationPathBoundaryWidth(north, south occultationPathVector, ok bool) float64 { + if !ok { + return 0 + } + return occultationPathNorm(occultationPathSub(north, south)) +} + +func occultationPathBoundaryVector(frame occultationPathFrame, theta float64) (occultationPathVector, float64, bool) { + origin, direction, ok := occultationPathBoundaryRay(frame, theta) + if !ok { + return occultationPathVector{}, 0, false + } + return occultationEarthLineIntersection(origin, direction) +} + +// occultationPathBoundaryRay 返回月缘圆柱或两球公切锥的一个母线 / +// occultationPathBoundaryRay returns one generator of the lunar-limb cylinder or a two-sphere common-tangent cone. +// targetRadius 带符号:正值表示异侧外切,负值表示同侧内切 / +// targetRadius is signed: positive for opposite-side outer tangency and negative for same-side inner tangency. +func occultationPathBoundaryRay(frame occultationPathFrame, theta float64) (occultationPathVector, occultationPathVector, bool) { + moonDistance := occultationPathNorm(frame.moon) + if moonDistance <= 0 || !finite(moonDistance) || !finite(frame.targetRadius) { + return occultationPathVector{}, occultationPathVector{}, false + } + radial := occultationPathAdd( + occultationPathScale(frame.first, math.Cos(theta)), + occultationPathScale(frame.second, math.Sin(theta)), + ) + sine, cosine := math.Sincos(frame.targetRadius) + normal := occultationPathAdd( + occultationPathScale(radial, cosine), + occultationPathScale(frame.axis, -sine), + ) + origin := occultationPathAdd( + frame.moon, + occultationPathScale(normal, moonDistance*math.Sin(frame.moonRadius)), + ) + direction := occultationPathAdd( + occultationPathScale(frame.axis, cosine), + occultationPathScale(radial, sine), + ) + return origin, occultationPathUnit(direction), true +} + +// occultationPathBoundaryTangent 求边界锥与地球椭球的连续切点 / +// occultationPathBoundaryTangent finds the continuous tangency of a boundary cone with the Earth ellipsoid. +// 采样网格提供搜索盆地,再对线判别式做局部极大化以恢复网格点之间的切点 / +// A sample grid supplies a basin, while local maximization of the line discriminant recovers tangencies between grid points. +func occultationPathBoundaryTangent(frame occultationPathFrame) (occultationPathVector, float64, bool) { + step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints) + bestTheta := 0.0 + bestDiscriminant := math.Inf(-1) + for i := 0; i < occultationPathBoundaryScanPoints; i++ { + theta := step * float64(i) + discriminant, _, _, ok := occultationPathBoundaryLine(frame, theta) + if ok && discriminant > bestDiscriminant { + bestDiscriminant = discriminant + bestTheta = theta + } + } + if !finite(bestDiscriminant) { + return occultationPathVector{}, 0, false + } + + left := bestTheta - step + right := bestTheta + step + const goldenRatio = 0.6180339887498949 + x1 := right - goldenRatio*(right-left) + x2 := left + goldenRatio*(right-left) + f1, _, _, _ := occultationPathBoundaryLine(frame, x1) + f2, _, _, _ := occultationPathBoundaryLine(frame, x2) + for i := 0; i < 40; i++ { + if f1 < f2 { + left = x1 + x1, f1 = x2, f2 + x2 = left + goldenRatio*(right-left) + f2, _, _, _ = occultationPathBoundaryLine(frame, x2) + } else { + right = x2 + x2, f2 = x1, f1 + x1 = right - goldenRatio*(right-left) + f1, _, _, _ = occultationPathBoundaryLine(frame, x1) + } + } + theta := (left + right) / 2 + discriminant, b, scale, ok := occultationPathBoundaryLine(frame, theta) + if !ok { + return occultationPathVector{}, 0, false + } + tolerance := 1e-12 * math.Max(scale, 1) + if discriminant < -tolerance || b >= 0 { + return occultationPathVector{}, 0, false + } + vector, _, valid := occultationPathBoundaryIntersection(frame, theta, tolerance) + return vector, theta, valid +} + +func occultationPathBoundaryLine(frame occultationPathFrame, theta float64) (discriminant, b, scale float64, ok bool) { + origin, direction, rayOK := occultationPathBoundaryRay(frame, theta) + if !rayOK { + return 0, 0, 0, false + } + polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio + a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared + b = origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared + c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM + discriminant = b*b - a*c + scale = math.Max(math.Abs(b*b), math.Abs(a*c)) + return discriminant, b, scale, a > 0 && finite(discriminant) +} + +func occultationPathBoundaryIntersection(frame occultationPathFrame, theta, tolerance float64) (occultationPathVector, float64, bool) { + origin, direction, ok := occultationPathBoundaryRay(frame, theta) + if !ok { + return occultationPathVector{}, 0, false + } + return occultationEarthLineIntersectionWithTolerance(origin, direction, tolerance) +} + +func occultationPathPointFromVector(tt float64, vector occultationPathVector, width float64, location *time.Location) OccultationPathPoint { + lon, lat := occultationPathGeodetic(tt, vector) + moonRA, moonDec := moonTopocentricApparentRaDec(tt, Observer{Longitude: lon, Latitude: lat}, -1) + return OccultationPathPoint{ + Time: occultationTTToLocation(tt, location), + Longitude: lon, + Latitude: lat, + MoonAltitude: occultationAltitude(tt, Observer{Longitude: lon, Latitude: lat}, moonRA, moonDec), + WidthKM: width, + } +} + +func occultationPathPointFromVectorWithMoon( + tt float64, + vector occultationPathVector, + width float64, + moon occultationPathVector, + location *time.Location, +) OccultationPathPoint { + lon, lat := occultationPathGeodetic(tt, vector) + moonDistance := occultationPathNorm(moon) + moonRA := normalizeRA(math.Atan2(moon.y, moon.x) * 180 / math.Pi) + moonDec := math.Asin(math.Max(-1, math.Min(1, moon.z/moonDistance))) * 180 / math.Pi + moonRA, moonDec = TopocentricRaDec( + moonRA, moonDec, lat, lon, TD2UT(tt, false), + moonDistance/occultationPathAstronomicalUnitKM, 0, + ) + return OccultationPathPoint{ + Time: occultationTTToLocation(tt, location), + Longitude: lon, + Latitude: lat, + MoonAltitude: occultationAltitude(tt, Observer{Longitude: lon, Latitude: lat}, normalizeRA(moonRA), moonDec), + WidthKM: width, + } +} + +func centerTimeTT(value time.Time) float64 { return occultationTimeToTT(value) } + +type occultationPathWidthFunc func(float64) (float64, bool) + +func refineOccultationPathWidths(points []OccultationPathPoint, widthAt occultationPathWidthFunc) { + if len(points) < 3 { + return + } + cache := make(map[int]bool) + var refine func(int, int, int) + refine = func(left, right, depth int) { + if right-left <= 1 || depth >= occultationPathMaxAdaptiveDepth { + return + } + if right-left <= 4 { + for index := left + 1; index < right; index++ { + setOccultationPathExactWidth(points, index, widthAt, cache) + } + return + } + + indices := uniqueOccultationPathWidthIndices(left, right) + withinTolerance := true + leftTime := points[left].Time + duration := points[right].Time.Sub(leftTime).Seconds() + for _, index := range indices { + linear := (points[left].WidthKM + points[right].WidthKM) / 2 + if duration != 0 { + fraction := points[index].Time.Sub(leftTime).Seconds() / duration + linear = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM) + } + if !setOccultationPathExactWidth(points, index, widthAt, cache) || + math.Abs(points[index].WidthKM-linear) > occultationPathWidthToleranceKM { + withinTolerance = false + } + } + anchors := append([]int{left}, indices...) + anchors = append(anchors, right) + if withinTolerance { + for index := 1; index < len(anchors); index++ { + interpolateOccultationPathWidths(points, anchors[index-1], anchors[index]) + } + return + } + for index := 1; index < len(anchors); index++ { + refine(anchors[index-1], anchors[index], depth+1) + } + } + refine(0, len(points)-1, 0) +} + +func uniqueOccultationPathWidthIndices(left, right int) []int { + indices := make([]int, 0, 3) + for _, numerator := range []int{1, 2, 3} { + index := left + (right-left)*numerator/4 + if index <= left || index >= right || len(indices) > 0 && index == indices[len(indices)-1] { + continue + } + indices = append(indices, index) + } + return indices +} + +func setOccultationPathExactWidth( + points []OccultationPathPoint, + index int, + widthAt occultationPathWidthFunc, + cache map[int]bool, +) bool { + if ok, found := cache[index]; found { + return ok + } + width, ok := widthAt(centerTimeTT(points[index].Time)) + if ok && finite(width) && width >= 0 { + points[index].WidthKM = width + } else { + ok = false + } + cache[index] = ok + return ok +} + +func interpolateOccultationPathWidths(points []OccultationPathPoint, left, right int) { + if right-left <= 1 { + return + } + leftTime := points[left].Time + duration := points[right].Time.Sub(leftTime).Seconds() + for index := left + 1; index < right; index++ { + fraction := float64(index-left) / float64(right-left) + if duration != 0 { + fraction = points[index].Time.Sub(leftTime).Seconds() / duration + } + points[index].WidthKM = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM) + } +} + +func occultationPathDistanceKM(a, b OccultationPathPoint) float64 { + return occultationPathDistanceKMValues(a.Longitude, a.Latitude, b.Longitude, b.Latitude) +} + +func occultationPathDistanceKMValues(lon1, lat1, lon2, lat2 float64) float64 { + lat1 *= math.Pi / 180 + lat2 *= math.Pi / 180 + dLat := lat2 - lat1 + dLon := (lon2 - lon1) * math.Pi / 180 + dLon = math.Mod(dLon+math.Pi, 2*math.Pi) + if dLon < 0 { + dLon += 2 * math.Pi + } + dLon -= math.Pi + h := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2) + return 2 * occultationPathEarthEquatorialRadiusKM * math.Asin(math.Sqrt(math.Min(1, h))) +} + +type occultationPathVector struct{ x, y, z float64 } + +func occultationPathRaDecVector(ra, dec, distance float64) occultationPathVector { + ra *= math.Pi / 180 + dec *= math.Pi / 180 + return occultationPathVector{ + x: distance * math.Cos(dec) * math.Cos(ra), + y: distance * math.Cos(dec) * math.Sin(ra), + z: distance * math.Sin(dec), + } +} + +func occultationPathAdd(a, b occultationPathVector) occultationPathVector { + return occultationPathVector{x: a.x + b.x, y: a.y + b.y, z: a.z + b.z} +} + +func occultationPathSub(a, b occultationPathVector) occultationPathVector { + return occultationPathVector{x: a.x - b.x, y: a.y - b.y, z: a.z - b.z} +} + +func occultationPathScale(a occultationPathVector, scalar float64) occultationPathVector { + return occultationPathVector{x: a.x * scalar, y: a.y * scalar, z: a.z * scalar} +} + +func occultationPathDot(a, b occultationPathVector) float64 { + return a.x*b.x + a.y*b.y + a.z*b.z +} + +func occultationPathCross(a, b occultationPathVector) occultationPathVector { + return occultationPathVector{ + x: a.y*b.z - a.z*b.y, + y: a.z*b.x - a.x*b.z, + z: a.x*b.y - a.y*b.x, + } +} + +func occultationPathNorm(value occultationPathVector) float64 { + return math.Sqrt(occultationPathDot(value, value)) +} + +func occultationPathUnit(value occultationPathVector) occultationPathVector { + norm := occultationPathNorm(value) + if norm <= 0 { + return occultationPathVector{} + } + return occultationPathScale(value, 1/norm) +} + +func occultationEarthLineIntersection(origin, direction occultationPathVector) (occultationPathVector, float64, bool) { + return occultationEarthLineIntersectionWithTolerance(origin, direction, 0) +} + +// occultationPathTrackReference 将影轴地面轨迹连续延伸到掠过阶段 / +// occultationPathTrackReference extends the shadow-axis ground track through grazing phases. +// 在地球外时,将椭球度量下最近点径向投影到表面,并在相切处与真实近侧交点连续连接 / +// Outside the Earth, the closest point under the ellipsoid metric is projected radially onto the surface and joined continuously to the real near-side intersection at tangency. +func occultationPathTrackReference(frame occultationPathFrame) (occultationPathVector, bool) { + if point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis); ok { + return point, true + } + + polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio + a := frame.axis.x*frame.axis.x + frame.axis.y*frame.axis.y + frame.axis.z*frame.axis.z/polarRatioSquared + b := frame.moon.x*frame.axis.x + frame.moon.y*frame.axis.y + frame.moon.z*frame.axis.z/polarRatioSquared + if a <= 0 || !finite(a) || !finite(b) { + return occultationPathVector{}, false + } + closest := occultationPathAdd(frame.moon, occultationPathScale(frame.axis, -b/a)) + metricRadius := math.Sqrt(closest.x*closest.x + closest.y*closest.y + closest.z*closest.z/polarRatioSquared) + if metricRadius <= 1e-9 || !finite(metricRadius) { + return occultationPathVector{}, false + } + return occultationPathScale(closest, occultationPathEarthEquatorialRadiusKM/metricRadius), true +} + +func occultationEarthLineIntersectionWithTolerance(origin, direction occultationPathVector, tolerance float64) (occultationPathVector, float64, bool) { + polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio + a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared + b := origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared + c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM + discriminant := b*b - a*c + if discriminant < -tolerance || a <= 0 { + return occultationPathVector{}, 0, false + } + if discriminant < 0 { + discriminant = 0 + } + root := math.Sqrt(discriminant) + roots := [2]float64{(-b - root) / a, (-b + root) / a} + chosen := math.Inf(1) + for _, root := range roots { + if root >= 0 && root < chosen { + chosen = root + } + } + if math.IsInf(chosen, 1) { + return occultationPathVector{}, 0, false + } + return occultationPathAdd(origin, occultationPathScale(direction, chosen)), chosen, true +} + +func occultationPathGeodetic(tt float64, vector occultationPathVector) (float64, float64) { + ut := TD2UT(tt, false) + gst := ApparentSiderealTime(ut) * 15 + longitude := normalizeLongitude(math.Atan2(vector.y, vector.x)*180/math.Pi - gst) + latitude := math.Atan2( + vector.z, + occultationPathEarthPolarRatio*occultationPathEarthPolarRatio*math.Hypot(vector.x, vector.y), + ) * 180 / math.Pi + return longitude, latitude +} + +func occultationPathEarthFixedVector(tt float64, vector occultationPathVector) occultationPathVector { + angle := ApparentSiderealTime(TD2UT(tt, false)) * 15 * math.Pi / 180 + cosAngle := math.Cos(angle) + sinAngle := math.Sin(angle) + return occultationPathVector{ + x: cosAngle*vector.x + sinAngle*vector.y, + y: -sinAngle*vector.x + cosAngle*vector.y, + z: vector.z, + } +} + +func normalizeLongitude(longitude float64) float64 { + longitude = math.Mod(longitude+180, 360) + if longitude < 0 { + longitude += 360 + } + return longitude - 180 +} diff --git a/basic/occultation_planet.go b/basic/occultation_planet.go new file mode 100644 index 0000000..7f4c69e --- /dev/null +++ b/basic/occultation_planet.go @@ -0,0 +1,502 @@ +package basic + +import ( + "math" + "sort" + "time" +) + +const ( + planetOccultationDefaultStepDays = 0.25 + planetOccultationCandidateLimitArcsec = 10 * 3600.0 + planetOccultationLatitudeMarginArcsec = 3600.0 + planetOccultationContactStepDays = 10.0 / 1440.0 + planetOccultationContactSpanDays = 2.0 + planetOccultationGrazingToleranceArcsec = 0.01 + planetOccultationRootToleranceDays = occultationEventSelectionToleranceDays + planetOccultationMaxContactSteps = 10000 +) + +type planetOccultationConfig struct { + planet OccultationPlanet + equatorialRadiusKM float64 + apparentRaDecN func(float64, int) (float64, float64) + earthDistanceN func(float64, int) float64 + semidiameterN func(float64, int) float64 +} + +type planetMoonPosition struct { + moonRA, moonDec float64 + planetRA, planetDec float64 + valid bool +} + +type planetOccultationState struct { + position planetMoonPosition + separationArcsec float64 + moonSemidiameter float64 + planetSemidiameter float64 + externalContactMetric float64 + internalContactMetric float64 + valid bool +} + +// FindPlanetOccultations 搜索固定观测点的有限盘面行星月掩。 +// 经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。目标位置、视差和视半径会在每次候选、掩甚和接触计算时重新计算。 +// FindPlanetOccultations searches one finite-disk planet at a fixed observing site. +// Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is the observer elevation above mean sea level in meters. The target position, parallax, and semidiameter are recomputed at every candidate, greatest, and contact evaluation. +func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64, + options OccultationSearchOptions) ([]PlanetOccultationInfo, error) { + if err := validateOccultationTimeRange(start, end); err != nil { + return nil, err + } + if err := planet.Validate(); err != nil { + return nil, err + } + observer := Observer{Longitude: longitude, Latitude: latitude, Height: height} + if err := observer.Validate(); err != nil { + return nil, err + } + if err := options.Validate(); err != nil { + return nil, err + } + + config, _ := planetOccultationConfigFor(planet) + startTT := occultationTimeToTT(start) + endTT := occultationTimeToTT(end) + resultLocation := start.Location() + candidates := planetOccultationCandidateGreatestTimes( + startTT, endTT, planetOccultationCoarseStepDays(options), config, &observer, options.SafetyMarginArcsec, + ) + results := make([]PlanetOccultationInfo, 0, len(candidates)) + for _, greatestTT := range candidates { + info, ok := planetOccultationInfoAtGreatest(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation) + if !ok { + continue + } + if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 { + results = append(results, info) + if options.MaxEvents > 0 && len(results) >= options.MaxEvents { + break + } + } + } + sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) }) + return results, nil +} + +// FindBestPlanetOccultations 返回窗口内每次有限盘面行星月掩的全球海平面几何掩甚点。 +// 地心数据只用于搜索初值;返回位置与 FindPlanetOccultationPaths 一致,地平线可见性只报告、不参与点选择。距离查询端点 10 ms 内的掩甚时刻也会包含,与数值根精度一致。 +// FindBestPlanetOccultations returns the global geometric greatest point at sea level for every finite-disk planetary occultation in the window. +// Geocentric data only seeds the search. The returned location matches FindPlanetOccultationPaths; horizon visibility is reported but does not select the point. A greatest instant within 10 ms of either query endpoint is included, matching the numerical root precision. +func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet, + options OccultationSearchOptions) ([]PlanetOccultationInfo, error) { + if err := validateOccultationTimeRange(start, end); err != nil { + return nil, err + } + if err := planet.Validate(); err != nil { + return nil, err + } + if err := options.Validate(); err != nil { + return nil, err + } + + config, _ := planetOccultationConfigFor(planet) + startTT := occultationTimeToTT(start) + endTT := occultationTimeToTT(end) + selectionStartTT := startTT - occultationEventSelectionToleranceDays + selectionEndTT := endTT + occultationEventSelectionToleranceDays + candidateStartTT := startTT - occultationPathSearchSpanDays + candidateEndTT := endTT + occultationPathSearchSpanDays + resultLocation := start.Location() + candidates := planetOccultationCandidateGreatestTimes( + candidateStartTT, candidateEndTT, planetOccultationCoarseStepDays(options), config, nil, options.SafetyMarginArcsec, + ) + results := make([]PlanetOccultationInfo, 0, len(candidates)) + for _, seedTT := range candidates { + greatestTT, observer, _, observerOK := planetOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, config) + if !observerOK { + continue + } + info, ok := planetOccultationInfoAtGreatest(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation) + if !ok { + continue + } + if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 { + results = append(results, info) + if options.MaxEvents > 0 && len(results) >= options.MaxEvents { + break + } + } + } + sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) }) + return results, nil +} + +func planetOccultationConfigFor(planet OccultationPlanet) (planetOccultationConfig, bool) { + config := planetOccultationConfig{planet: planet} + switch planet { + case OccultationMercury: + config.equatorialRadiusKM = mercuryEquatorialRadiusKM + config.apparentRaDecN = MercuryApparentRaDecN + config.earthDistanceN = EarthMercuryAwayN + config.semidiameterN = MercurySemidiameterN + case OccultationVenus: + config.equatorialRadiusKM = venusEquatorialRadiusKM + config.apparentRaDecN = VenusApparentRaDecN + config.earthDistanceN = EarthVenusAwayN + config.semidiameterN = VenusSemidiameterN + case OccultationMars: + config.equatorialRadiusKM = marsEquatorialRadiusKM + config.apparentRaDecN = MarsApparentRaDecN + config.earthDistanceN = EarthMarsAwayN + config.semidiameterN = MarsSemidiameterN + case OccultationJupiter: + config.equatorialRadiusKM = jupiterEquatorialRadiusKM + config.apparentRaDecN = JupiterApparentRaDecN + config.earthDistanceN = EarthJupiterAwayN + config.semidiameterN = JupiterSemidiameterN + case OccultationSaturn: + config.equatorialRadiusKM = saturnEquatorialRadiusKM + config.apparentRaDecN = SaturnApparentRaDecN + config.earthDistanceN = EarthSaturnAwayN + config.semidiameterN = SaturnSemidiameterN + case OccultationUranus: + config.equatorialRadiusKM = uranusEquatorialRadiusKM + config.apparentRaDecN = UranusApparentRaDecN + config.earthDistanceN = EarthUranusAwayN + config.semidiameterN = UranusSemidiameterN + case OccultationNeptune: + config.equatorialRadiusKM = neptuneEquatorialRadiusKM + config.apparentRaDecN = NeptuneApparentRaDecN + config.earthDistanceN = EarthNeptuneAwayN + config.semidiameterN = NeptuneSemidiameterN + default: + return planetOccultationConfig{}, false + } + return config, true +} + +func planetOccultationCandidateGreatestTimes( + startTT, endTT, step float64, + config planetOccultationConfig, + observer *Observer, + safetyMarginArcsec float64, +) []float64 { + if endTT <= startTT { + return nil + } + duration := endTT - startTT + if step > duration/4 { + step = math.Max(duration/4, 0.25/86400.0) + } + step = math.Max(step, 0.25/86400.0) + scanStart := startTT - step + scanEnd := endTT + step + + leftTT := scanStart + centerTT := math.Min(leftTT+step, scanEnd) + leftValue := planetOccultationExternalContactMetric(leftTT, config, observer, 8) + centerValue := planetOccultationExternalContactMetric(centerTT, config, observer, 8) + results := make([]float64, 0) + for centerTT < scanEnd { + rightTT := math.Min(centerTT+step, scanEnd) + rightValue := planetOccultationExternalContactMetric(rightTT, config, observer, 8) + candidateLimit := planetOccultationCandidateLimitArcsec + safetyMarginArcsec + if finite(leftValue) && finite(centerValue) && finite(rightValue) && + centerValue <= leftValue && centerValue <= rightValue && centerValue <= candidateLimit { + // 最小外接触度量同时包含动态月面和行星盘面,因此定义事件是否存在以及报告的掩甚时刻。 + // The minimum outer-contact metric includes both dynamic disks and therefore defines event existence and the reported greatest instant. + greatestTT := planetOccultationMinimizeExternalMetric(leftTT, rightTT, config, observer) + if greatestTT >= startTT && greatestTT <= endTT && + planetOccultationLatitudePass(greatestTT, config, observer, safetyMarginArcsec) { + if len(results) == 0 || math.Abs(greatestTT-results[len(results)-1]) > 60.0/86400.0 { + results = append(results, greatestTT) + } + } + } + leftTT, leftValue = centerTT, centerValue + centerTT, centerValue = rightTT, rightValue + } + return results +} + +func planetOccultationMinimizeExternalMetric(left, right float64, config planetOccultationConfig, observer *Observer) float64 { + if right <= left { + return left + } + const goldenRatio = 0.6180339887498949 + x1 := right - goldenRatio*(right-left) + x2 := left + goldenRatio*(right-left) + f1 := planetOccultationExternalContactMetric(x1, config, observer, -1) + f2 := planetOccultationExternalContactMetric(x2, config, observer, -1) + for i := 0; i < 64 && right-left > planetOccultationRootToleranceDays; i++ { + if f1 > f2 { + left = x1 + x1, f1 = x2, f2 + x2 = left + goldenRatio*(right-left) + f2 = planetOccultationExternalContactMetric(x2, config, observer, -1) + } else { + right = x2 + x2, f2 = x1, f1 + x1 = right - goldenRatio*(right-left) + f1 = planetOccultationExternalContactMetric(x1, config, observer, -1) + } + } + return (left + right) / 2 +} + +func planetOccultationBestObserver(seedTT, startTT, endTT float64, config planetOccultationConfig) (float64, Observer, float64, bool) { + frameAt := func(tt float64) (occultationPathFrame, bool) { + return planetOccultationPathFrameAt(tt, config) + } + searchStart := seedTT - occultationPathSearchSpanDays + searchEnd := seedTT + occultationPathSearchSpanDays + outerStart, outerEnd, ok := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, false) + if !ok { + return 0, Observer{}, 0, false + } + greatestTT := occultationPathGreatestForFrame(seedTT, outerStart, outerEnd, frameAt) + if greatestTT < startTT || greatestTT > endTT { + return 0, Observer{}, 0, false + } + point, pointOK := occultationPathCenterPointForFrame(greatestTT, frameAt, time.UTC) + if !pointOK { + point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC) + } + if !pointOK { + return 0, Observer{}, 0, false + } + observer := Observer{Longitude: point.Longitude, Latitude: point.Latitude} + state := planetOccultationStateAt(greatestTT, config, &observer, -1) + if !state.valid { + return 0, Observer{}, 0, false + } + return greatestTT, observer, state.externalContactMetric, true +} + +func planetOccultationInfoAtGreatest( + greatestTT float64, + config planetOccultationConfig, + observer Observer, + safetyMarginArcsec float64, + location *time.Location, +) (PlanetOccultationInfo, bool) { + if !planetOccultationLatitudePass(greatestTT, config, &observer, safetyMarginArcsec) { + return PlanetOccultationInfo{}, false + } + state := planetOccultationStateAt(greatestTT, config, &observer, -1) + if !state.valid || state.externalContactMetric > 0 { + return PlanetOccultationInfo{}, false + } + info := PlanetOccultationInfo{ + Planet: config.planet, + TargetID: config.planet.String(), + Observer: observer, + Type: OccultationPartial, + Greatest: occultationTTToLocation(greatestTT, location), + MinimumSeparationArcsec: state.separationArcsec, + PositionAngleDeg: occultationPositionAngle(state.position.moonRA, state.position.moonDec, state.position.planetRA, state.position.planetDec), + MoonSemidiameterArcsec: state.moonSemidiameter, + PlanetSemidiameterArcsec: state.planetSemidiameter, + MoonAltitudeAtGreatest: occultationAltitude(greatestTT, observer, state.position.moonRA, state.position.moonDec), + MoonAzimuthAtGreatest: occultationAzimuth(greatestTT, observer, state.position.moonRA, state.position.moonDec), + } + info.VisibleAtGreatest = info.MoonAltitudeAtGreatest >= 0 + + if math.Abs(state.externalContactMetric) <= planetOccultationGrazingToleranceArcsec { + info.Type = OccultationGrazing + info.ExternalImmersion = info.Greatest + info.ExternalEmersion = info.Greatest + info.ContactsComplete = true + return info, true + } + + externalImmersionTT, externalImmersionOK := planetOccultationContact(greatestTT, -1, false, config, observer) + externalEmersionTT, externalEmersionOK := planetOccultationContact(greatestTT, 1, false, config, observer) + if !externalImmersionOK || !externalEmersionOK || externalEmersionTT <= externalImmersionTT { + return PlanetOccultationInfo{}, false + } + info.ExternalImmersion = occultationTTToLocation(externalImmersionTT, location) + info.ExternalEmersion = occultationTTToLocation(externalEmersionTT, location) + + if state.internalContactMetric < -planetOccultationGrazingToleranceArcsec { + internalImmersionTT, internalImmersionOK := planetOccultationContact(greatestTT, -1, true, config, observer) + internalEmersionTT, internalEmersionOK := planetOccultationContact(greatestTT, 1, true, config, observer) + if !internalImmersionOK || !internalEmersionOK || + internalImmersionTT <= externalImmersionTT || internalEmersionTT >= externalEmersionTT || + internalImmersionTT >= greatestTT || internalEmersionTT <= greatestTT { + return PlanetOccultationInfo{}, false + } + info.Type = OccultationTotal + info.InternalImmersion = occultationTTToLocation(internalImmersionTT, location) + info.InternalEmersion = occultationTTToLocation(internalEmersionTT, location) + info.HasInternalContacts = true + } + info.ContactsComplete = true + return info, true +} + +func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *Observer, n int) planetMoonPosition { + moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n) + planetRA, planetDec := config.apparentRaDecN(tt, n) + if observer != nil { + ut := TD2UT(tt, false) + moonDistanceAU := HMoonAwayN(tt, n) / angularDiameterAstronomicalUnitKM + planetDistanceAU := config.earthDistanceN(tt, n) + moonRA, moonDec = TopocentricRaDec(moonRA, moonDec, observer.Latitude, observer.Longitude, ut, moonDistanceAU, observer.Height) + planetRA, planetDec = TopocentricRaDec(planetRA, planetDec, observer.Latitude, observer.Longitude, ut, planetDistanceAU, observer.Height) + moonRA = normalizeRA(moonRA) + planetRA = normalizeRA(planetRA) + } + return planetMoonPosition{ + moonRA: moonRA, + moonDec: moonDec, + planetRA: planetRA, + planetDec: planetDec, + valid: finite(moonRA) && finite(moonDec) && finite(planetRA) && finite(planetDec), + } +} + +func planetOccultationStateAt(tt float64, config planetOccultationConfig, observer *Observer, n int) planetOccultationState { + position := planetMoonPositionAt(tt, config, observer, n) + moonRadius := MoonSemidiameterN(tt, n) + planetRadius := config.semidiameterN(tt, n) + if observer != nil { + moonRadius = moonTopocentricSemidiameterN(tt, *observer, n) + planetRadius = planetTopocentricSemidiameterN(tt, config, *observer, n) + } + if !position.valid || !finite(moonRadius) || !finite(planetRadius) || moonRadius <= planetRadius || planetRadius <= 0 { + return planetOccultationState{} + } + separation := angularSeparationDegrees(position.moonRA, position.moonDec, position.planetRA, position.planetDec) * 3600 + return planetOccultationState{ + position: position, + separationArcsec: separation, + moonSemidiameter: moonRadius, + planetSemidiameter: planetRadius, + externalContactMetric: separation - (moonRadius + planetRadius), + internalContactMetric: separation - (moonRadius - planetRadius), + valid: finite(separation), + } +} + +func planetTopocentricSemidiameterN(tt float64, config planetOccultationConfig, observer Observer, n int) float64 { + ra, dec := config.apparentRaDecN(tt, n) + distanceKM := config.earthDistanceN(tt, n) * angularDiameterAstronomicalUnitKM + if !finite(ra) || !finite(dec) || !finite(distanceKM) || distanceKM <= 0 { + return math.NaN() + } + distanceKM = topocentricDistanceKM(ra, dec, distanceKM, observer, TD2UT(tt, false)) + if !finite(distanceKM) || distanceKM <= config.equatorialRadiusKM { + return math.NaN() + } + return angularSemidiameterArcsec(config.equatorialRadiusKM, distanceKM) +} + +func planetOccultationExternalContactMetric(tt float64, config planetOccultationConfig, observer *Observer, n int) float64 { + state := planetOccultationStateAt(tt, config, observer, n) + if !state.valid { + return math.Inf(1) + } + return state.externalContactMetric +} + +func planetMoonSeparationArcsec(tt float64, config planetOccultationConfig, observer *Observer, n int) float64 { + position := planetMoonPositionAt(tt, config, observer, n) + if !position.valid { + return math.Inf(1) + } + return angularSeparationDegrees(position.moonRA, position.moonDec, position.planetRA, position.planetDec) * 3600 +} + +func planetOccultationLatitudePass(tt float64, config planetOccultationConfig, observer *Observer, safetyMarginArcsec float64) bool { + state := planetOccultationStateAt(tt, config, observer, -1) + if !state.valid { + return false + } + _, moonLatitude := RaDecToLoBo(tt, state.position.moonRA, state.position.moonDec) + _, planetLatitude := RaDecToLoBo(tt, state.position.planetRA, state.position.planetDec) + limit := state.moonSemidiameter + state.planetSemidiameter + planetOccultationLatitudeMarginArcsec + safetyMarginArcsec + return math.Abs(moonLatitude-planetLatitude)*3600 <= limit +} + +func planetOccultationContact( + greatestTT float64, + direction int, + internal bool, + config planetOccultationConfig, + observer Observer, +) (float64, bool) { + if direction != -1 && direction != 1 { + return math.NaN(), false + } + metric := func(tt float64) float64 { + state := planetOccultationStateAt(tt, config, &observer, -1) + if !state.valid { + return math.NaN() + } + if internal { + return state.internalContactMetric + } + return state.externalContactMetric + } + nearTT := greatestTT + nearValue := metric(nearTT) + if !finite(nearValue) || nearValue > 0 { + return math.NaN(), false + } + maxSteps := int(math.Ceil(planetOccultationContactSpanDays / planetOccultationContactStepDays)) + if maxSteps > planetOccultationMaxContactSteps { + maxSteps = planetOccultationMaxContactSteps + } + for i := 1; i <= maxSteps; i++ { + farTT := greatestTT + float64(direction*i)*planetOccultationContactStepDays + farValue := metric(farTT) + if !finite(farValue) { + continue + } + if farValue >= 0 { + return planetOccultationRoot(nearTT, farTT, nearValue, farValue, metric) + } + nearTT, nearValue = farTT, farValue + } + return math.NaN(), false +} + +func planetOccultationRoot( + leftTT, rightTT, leftValue, rightValue float64, + metric func(float64) float64, +) (float64, bool) { + if leftTT > rightTT { + leftTT, rightTT = rightTT, leftTT + leftValue, rightValue = rightValue, leftValue + } + if !finite(leftValue) || !finite(rightValue) || leftValue*rightValue > 0 { + return math.NaN(), false + } + for i := 0; i < 64 && math.Abs(rightTT-leftTT) > planetOccultationRootToleranceDays; i++ { + midTT := (leftTT + rightTT) / 2 + midValue := metric(midTT) + if !finite(midValue) { + return math.NaN(), false + } + if leftValue*midValue <= 0 { + rightTT, rightValue = midTT, midValue + } else { + leftTT, leftValue = midTT, midValue + } + } + return (leftTT + rightTT) / 2, true +} + +func planetOccultationCoarseStepDays(options OccultationSearchOptions) float64 { + step := planetOccultationDefaultStepDays + if options.MaxStep > 0 { + requested := options.MaxStep.Hours() / 24 + if requested > 0 && requested < step { + step = requested + } + } + return math.Max(step, occultationSearchMinimumStep.Hours()/24) +} diff --git a/basic/occultation_planet_diagram.go b/basic/occultation_planet_diagram.go new file mode 100644 index 0000000..5d250d7 --- /dev/null +++ b/basic/occultation_planet_diagram.go @@ -0,0 +1,321 @@ +package basic + +import ( + "math" + "sort" +) + +const ( + planetOccultationDiagramDefaultStepDays = 2.0 / 1440.0 + planetOccultationDiagramMinStepDays = 1.0 / 86400.0 + planetOccultationDiagramMaxSamples = 2000 + planetOccultationDiagramDuplicateDays = 1e-10 + planetOccultationDiagramGeometryArcsec = 0.05 + planetOccultationDiagramPositionDeg = 0.01 + planetOccultationDiagramContactTimeDays = 1e-6 +) + +// PlanetOccultationDiagramOptions 控制本地行星月掩轨迹采样。 +// PlanetOccultationDiagramOptions controls local planetary-occultation track sampling. +type PlanetOccultationDiagramOptions struct { + // StepDays 是请求的轨迹采样步长,单位为日;非正值或非有限值使用两分钟,正值小于一秒时使用一秒。长事件可能增大实际步长,使基础轨迹不超过 2000 个采样点;必要阶段帧仍会额外保留。结果会报告实际采用的值。 + // StepDays is the requested track sampling step in days. Non-positive or non-finite values use two minutes, and positive values below one second use one second. Long events may increase the effective step to keep the base track within 2000 samples; required phase frames are retained in addition. The result reports the effective value. + StepDays float64 +} + +// PlanetOccultationDiagramFrame 描述一个时刻的站心月球与行星几何。 +// PlanetOccultationDiagramFrame describes topocentric Moon-planet geometry at one instant. +type PlanetOccultationDiagramFrame struct { + // JDE 是 TT 儒略历书日。 + // JDE is the TT Julian ephemeris day. + JDE float64 + // PlanetXArcsec 和 PlanetYArcsec 是相对月心的切平面偏移,单位为角秒。X 向东为正,Y 向北为正。 + // PlanetXArcsec and PlanetYArcsec are tangent-plane offsets from the lunar center. X is positive east and Y is positive north. + PlanetXArcsec float64 + PlanetYArcsec float64 + // MoonRadiusArcsec 和 PlanetRadiusArcsec 是站心视半径,单位为角秒。 + // MoonRadiusArcsec and PlanetRadiusArcsec are topocentric apparent semidiameters. + MoonRadiusArcsec float64 + PlanetRadiusArcsec float64 + // SeparationArcsec 和 PositionAngleDeg 描述行星中心相对月心的位置。 + // SeparationArcsec and PositionAngleDeg describe the planet center relative to the lunar center. + SeparationArcsec float64 + PositionAngleDeg float64 + // MoonAltitudeDeg 和 MoonAzimuthDeg 是站心地平坐标。 + // MoonAltitudeDeg and MoonAzimuthDeg are topocentric horizontal coordinates. + MoonAltitudeDeg float64 + MoonAzimuthDeg float64 + // DisksOverlap 表示两个视盘面存在正面积交集。 + // DisksOverlap is true while the two apparent disks have a positive-area intersection. + DisksOverlap bool + // FullyOcculted 表示行星盘面完全位于月缘内侧。 + // FullyOcculted is true while the planet disk lies strictly inside the lunar limb. + FullyOcculted bool + // Label 是主阶段标识;Labels 在掠掩事件中保留重合阶段。 + // Label is the primary key phase; Labels retains coincident phases for grazing events. + Label string + Labels []string +} + +// PlanetOccultationDiagramResult 包含固定地点行星月掩的几何数据。 +// PlanetOccultationDiagramResult contains geometry for a fixed-site planetary occultation. +type PlanetOccultationDiagramResult struct { + Occultation PlanetOccultationInfo + Frames []PlanetOccultationDiagramFrame + // StepDays 是实际采用的基础轨迹采样步长,单位为日。 + // StepDays is the effective base-track sampling step in days. + StepDays float64 +} + +type planetOccultationDiagramTime struct { + jde float64 + labels []string +} + +// PlanetOccultationDiagram 为已求解的固定地点行星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。 +// PlanetOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site planetary occultation. Invalid or incomplete event data produces a result without frames. +func PlanetOccultationDiagram( + info PlanetOccultationInfo, + options PlanetOccultationDiagramOptions, +) PlanetOccultationDiagramResult { + options = normalizePlanetOccultationDiagramOptions(options) + result := PlanetOccultationDiagramResult{Occultation: info, StepDays: options.StepDays} + if !planetOccultationDiagramInputValid(info) { + return result + } + + config, ok := planetOccultationConfigFor(info.Planet) + if !ok { + return result + } + startTT := occultationTimeToTT(info.ExternalImmersion) + greatestTT := occultationTimeToTT(info.Greatest) + endTT := occultationTimeToTT(info.ExternalEmersion) + externalImmersionFrame, externalImmersionOK := planetOccultationDiagramFrameAt(startTT, config, info.Observer) + greatestFrame, greatestOK := planetOccultationDiagramFrameAt(greatestTT, config, info.Observer) + externalEmersionFrame, externalEmersionOK := planetOccultationDiagramFrameAt(endTT, config, info.Observer) + if !externalImmersionOK || !greatestOK || !externalEmersionOK { + return result + } + var ( + internalImmersionFrame, internalEmersionFrame PlanetOccultationDiagramFrame + internalImmersionOK, internalEmersionOK bool + ) + if info.HasInternalContacts { + internalImmersionFrame, internalImmersionOK = planetOccultationDiagramFrameAt( + occultationTimeToTT(info.InternalImmersion), config, info.Observer, + ) + internalEmersionFrame, internalEmersionOK = planetOccultationDiagramFrameAt( + occultationTimeToTT(info.InternalEmersion), config, info.Observer, + ) + } + if (info.HasInternalContacts && (!internalImmersionOK || !internalEmersionOK)) || + !planetOccultationDiagramMatchesInfo( + info, startTT, greatestTT, endTT, + externalImmersionFrame, internalImmersionFrame, greatestFrame, internalEmersionFrame, externalEmersionFrame, + ) { + return result + } + times, stepDays := planetOccultationDiagramTimes(info, startTT, greatestTT, endTT, options.StepDays) + result.StepDays = stepDays + result.Frames = make([]PlanetOccultationDiagramFrame, 0, len(times)) + for _, item := range times { + frame, frameOK := planetOccultationDiagramFrameAt(item.jde, config, info.Observer) + if !frameOK { + return PlanetOccultationDiagramResult{Occultation: info, StepDays: stepDays} + } + frame.Labels = append([]string(nil), item.labels...) + frame.Label = planetOccultationDiagramPrimaryLabel(item.labels) + result.Frames = append(result.Frames, frame) + } + return result +} + +func planetOccultationDiagramMatchesInfo( + info PlanetOccultationInfo, + startTT, greatestTT, endTT float64, + externalImmersion, internalImmersion, greatest, internalEmersion, externalEmersion PlanetOccultationDiagramFrame, +) bool { + if !finite(info.MinimumSeparationArcsec) || info.MinimumSeparationArcsec < 0 || + !finite(info.PositionAngleDeg) || + !finite(info.MoonSemidiameterArcsec) || info.MoonSemidiameterArcsec <= 0 || + !finite(info.PlanetSemidiameterArcsec) || info.PlanetSemidiameterArcsec <= 0 { + return false + } + if math.Abs(externalImmersion.SeparationArcsec-externalImmersion.MoonRadiusArcsec-externalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec || + math.Abs(externalEmersion.SeparationArcsec-externalEmersion.MoonRadiusArcsec-externalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec { + return false + } + if math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) > planetOccultationDiagramGeometryArcsec || + math.Abs(greatest.MoonRadiusArcsec-info.MoonSemidiameterArcsec) > planetOccultationDiagramGeometryArcsec || + math.Abs(greatest.PlanetRadiusArcsec-info.PlanetSemidiameterArcsec) > planetOccultationDiagramGeometryArcsec || + math.Abs(signedAngleDifference(greatest.PositionAngleDeg, info.PositionAngleDeg)) > planetOccultationDiagramPositionDeg { + return false + } + + externalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec - greatest.PlanetRadiusArcsec + internalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec + greatest.PlanetRadiusArcsec + switch info.Type { + case OccultationPartial: + return externalMetric < -planetOccultationGrazingToleranceArcsec && + internalMetric >= -planetOccultationGrazingToleranceArcsec + case OccultationGrazing: + return math.Abs(externalMetric) <= planetOccultationGrazingToleranceArcsec && + math.Abs(startTT-greatestTT) <= planetOccultationDiagramContactTimeDays && + math.Abs(endTT-greatestTT) <= planetOccultationDiagramContactTimeDays + case OccultationTotal: + if math.Abs(internalImmersion.SeparationArcsec-internalImmersion.MoonRadiusArcsec+internalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec || + math.Abs(internalEmersion.SeparationArcsec-internalEmersion.MoonRadiusArcsec+internalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec { + return false + } + return externalMetric < -planetOccultationGrazingToleranceArcsec && + internalMetric < -planetOccultationGrazingToleranceArcsec + default: + return false + } +} + +func normalizePlanetOccultationDiagramOptions(options PlanetOccultationDiagramOptions) PlanetOccultationDiagramOptions { + if options.StepDays <= 0 || !finite(options.StepDays) { + options.StepDays = planetOccultationDiagramDefaultStepDays + } + if options.StepDays < planetOccultationDiagramMinStepDays { + options.StepDays = planetOccultationDiagramMinStepDays + } + return options +} + +func planetOccultationDiagramInputValid(info PlanetOccultationInfo) bool { + if info.Planet.Validate() != nil || info.Observer.Validate() != nil || !info.ContactsComplete || + info.ExternalImmersion.IsZero() || info.Greatest.IsZero() || info.ExternalEmersion.IsZero() || + info.Greatest.Before(info.ExternalImmersion) || info.ExternalEmersion.Before(info.Greatest) { + return false + } + switch info.Type { + case OccultationTotal: + return info.HasInternalContacts && !info.InternalImmersion.IsZero() && !info.InternalEmersion.IsZero() && + info.InternalImmersion.After(info.ExternalImmersion) && info.InternalImmersion.Before(info.Greatest) && + info.InternalEmersion.After(info.Greatest) && info.InternalEmersion.Before(info.ExternalEmersion) + case OccultationPartial: + return !info.HasInternalContacts && info.InternalImmersion.IsZero() && info.InternalEmersion.IsZero() + case OccultationGrazing: + return !info.HasInternalContacts && info.InternalImmersion.IsZero() && info.InternalEmersion.IsZero() + default: + return false + } +} + +func planetOccultationDiagramTimes( + info PlanetOccultationInfo, + startTT, greatestTT, endTT, stepDays float64, +) ([]planetOccultationDiagramTime, float64) { + if !finite(startTT) || !finite(greatestTT) || !finite(endTT) || greatestTT < startTT || endTT < greatestTT { + return nil, stepDays + } + if endTT > startTT { + if sampleCount := int(math.Ceil((endTT-startTT)/stepDays)) + 1; sampleCount > planetOccultationDiagramMaxSamples { + stepDays = (endTT - startTT) / float64(planetOccultationDiagramMaxSamples-1) + } + } + times := []planetOccultationDiagramTime{ + {jde: startTT, labels: []string{"C1"}}, + {jde: greatestTT, labels: []string{"Greatest"}}, + {jde: endTT, labels: []string{"C4"}}, + } + if info.HasInternalContacts { + times = append(times, + planetOccultationDiagramTime{jde: occultationTimeToTT(info.InternalImmersion), labels: []string{"C2"}}, + planetOccultationDiagramTime{jde: occultationTimeToTT(info.InternalEmersion), labels: []string{"C3"}}, + ) + } + for jde := startTT + stepDays; jde < endTT; jde += stepDays { + times = append(times, planetOccultationDiagramTime{jde: jde}) + } + sort.SliceStable(times, func(i, j int) bool { + if times[i].jde == times[j].jde { + return planetOccultationDiagramLabelPriority(times[i].labels) < planetOccultationDiagramLabelPriority(times[j].labels) + } + return times[i].jde < times[j].jde + }) + return uniquePlanetOccultationDiagramTimes(times), stepDays +} + +func uniquePlanetOccultationDiagramTimes(times []planetOccultationDiagramTime) []planetOccultationDiagramTime { + unique := times[:0] + for _, item := range times { + if !finite(item.jde) { + continue + } + if len(unique) == 0 || math.Abs(item.jde-unique[len(unique)-1].jde) > planetOccultationDiagramDuplicateDays { + item.labels = append([]string(nil), item.labels...) + unique = append(unique, item) + continue + } + unique[len(unique)-1].labels = mergeStarOccultationDiagramLabels(unique[len(unique)-1].labels, item.labels) + } + return unique +} + +func planetOccultationDiagramPrimaryLabel(labels []string) string { + for _, label := range labels { + if label == "Greatest" { + return label + } + } + if len(labels) == 0 { + return "" + } + return labels[0] +} + +func planetOccultationDiagramLabelPriority(labels []string) int { + if len(labels) == 0 { + return 99 + } + switch labels[0] { + case "C1": + return 0 + case "C2": + return 1 + case "Greatest": + return 2 + case "C3": + return 3 + case "C4": + return 4 + default: + return 99 + } +} + +func planetOccultationDiagramFrameAt( + tt float64, + config planetOccultationConfig, + observer Observer, +) (PlanetOccultationDiagramFrame, bool) { + state := planetOccultationStateAt(tt, config, &observer, -1) + if !state.valid { + return PlanetOccultationDiagramFrame{}, false + } + positionAngle := occultationPositionAngle( + state.position.moonRA, state.position.moonDec, + state.position.planetRA, state.position.planetDec, + ) + if !finite(positionAngle) { + return PlanetOccultationDiagramFrame{}, false + } + angle := positionAngle * math.Pi / 180 + return PlanetOccultationDiagramFrame{ + JDE: tt, + PlanetXArcsec: state.separationArcsec * math.Sin(angle), + PlanetYArcsec: state.separationArcsec * math.Cos(angle), + MoonRadiusArcsec: state.moonSemidiameter, + PlanetRadiusArcsec: state.planetSemidiameter, + SeparationArcsec: state.separationArcsec, + PositionAngleDeg: positionAngle, + MoonAltitudeDeg: occultationAltitude(tt, observer, state.position.moonRA, state.position.moonDec), + MoonAzimuthDeg: occultationAzimuth(tt, observer, state.position.moonRA, state.position.moonDec), + DisksOverlap: state.externalContactMetric < -planetOccultationGrazingToleranceArcsec, + FullyOcculted: state.internalContactMetric < -planetOccultationGrazingToleranceArcsec, + }, true +} diff --git a/basic/occultation_planet_diagram_test.go b/basic/occultation_planet_diagram_test.go new file mode 100644 index 0000000..56388bd --- /dev/null +++ b/basic/occultation_planet_diagram_test.go @@ -0,0 +1,112 @@ +package basic + +import ( + "math" + "testing" + "time" +) + +func TestPlanetOccultationDiagramSaturnIncludesFiveDynamicDiskStages(t *testing.T) { + location := time.FixedZone("UTC+8", 8*3600) + start := time.Date(2025, time.February, 1, 0, 0, 0, 0, location) + end := start.Add(24 * time.Hour) + events, err := FindPlanetOccultations( + start, end, OccultationSaturn, 104.52219613, 55.25401991, 0, OccultationSearchOptions{}, + ) + if err != nil { + t.Fatalf("FindPlanetOccultations() error = %v", err) + } + if len(events) != 1 { + t.Fatalf("FindPlanetOccultations() returned %d events, want 1", len(events)) + } + diagram := PlanetOccultationDiagram(events[0], PlanetOccultationDiagramOptions{}) + if len(diagram.Frames) < 5 { + t.Fatalf("PlanetOccultationDiagram() returned %d frames, want at least 5", len(diagram.Frames)) + } + + frames := make(map[string]PlanetOccultationDiagramFrame) + for _, frame := range diagram.Frames { + for _, label := range frame.Labels { + frames[label] = frame + } + } + for _, label := range []string{"C1", "C2", "Greatest", "C3", "C4"} { + if _, ok := frames[label]; !ok { + t.Fatalf("diagram is missing %s", label) + } + } + for _, label := range []string{"C1", "C4"} { + frame := frames[label] + residual := frame.SeparationArcsec - frame.MoonRadiusArcsec - frame.PlanetRadiusArcsec + if math.Abs(residual) > 0.1 { + t.Errorf("%s outer-contact residual = %.6f arcsec", label, residual) + } + } + for _, label := range []string{"C2", "C3"} { + frame := frames[label] + residual := frame.SeparationArcsec - frame.MoonRadiusArcsec + frame.PlanetRadiusArcsec + if math.Abs(residual) > 0.1 { + t.Errorf("%s inner-contact residual = %.6f arcsec", label, residual) + } + } + if !frames["Greatest"].FullyOcculted { + t.Fatal("greatest frame does not report a fully occulted Saturn disk") + } +} + +func TestPlanetOccultationDiagramRejectsIncompleteEvent(t *testing.T) { + result := PlanetOccultationDiagram(PlanetOccultationInfo{Planet: OccultationSaturn}, PlanetOccultationDiagramOptions{}) + if len(result.Frames) != 0 { + t.Fatalf("invalid event returned %d frames, want none", len(result.Frames)) + } +} + +func TestPlanetOccultationDiagramRejectsTargetGeometryMismatch(t *testing.T) { + location := time.FixedZone("UTC+8", 8*3600) + events, err := FindPlanetOccultations( + time.Date(2025, time.February, 1, 0, 0, 0, 0, location), + time.Date(2025, time.February, 2, 0, 0, 0, 0, location), + OccultationSaturn, 104.52219613, 55.25401991, 0, OccultationSearchOptions{}, + ) + if err != nil || len(events) != 1 { + t.Fatalf("FindPlanetOccultations() = %d events, %v; want one", len(events), err) + } + // TargetID 只是显示元数据,但改变建模行星必须 / TargetID is display metadata, but changing the modeled planet must + // 使事件几何失效,而不是静默绘制另一个天体 / invalidate the event geometry rather than silently drawing another body. + event := events[0] + event.Planet = OccultationVenus + event.TargetID = "Venus" + result := PlanetOccultationDiagram(event, PlanetOccultationDiagramOptions{}) + if len(result.Frames) != 0 { + t.Fatalf("diagram accepted mismatched planet and returned %d frames", len(result.Frames)) + } +} + +func TestPlanetOccultationDiagramPartialEventHasOnlyExternalStages(t *testing.T) { + start := time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC) + end := time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC) + events, err := FindPlanetOccultations( + start, end, OccultationSaturn, -30.072, -6.5, 0, OccultationSearchOptions{}, + ) + if err != nil || len(events) != 1 { + t.Fatalf("FindPlanetOccultations() = %d events, %v; want one", len(events), err) + } + if events[0].Type != OccultationPartial { + t.Fatalf("event type = %q, want partial", events[0].Type) + } + diagram := PlanetOccultationDiagram(events[0], PlanetOccultationDiagramOptions{}) + labels := make(map[string]bool) + for _, frame := range diagram.Frames { + for _, label := range frame.Labels { + labels[label] = true + } + } + for _, label := range []string{"C1", "Greatest", "C4"} { + if !labels[label] { + t.Fatalf("partial diagram is missing %s", label) + } + } + if labels["C2"] || labels["C3"] { + t.Fatalf("partial diagram contains internal contacts: %+v", labels) + } +} diff --git a/basic/occultation_planet_footprint.go b/basic/occultation_planet_footprint.go new file mode 100644 index 0000000..6ea02ac --- /dev/null +++ b/basic/occultation_planet_footprint.go @@ -0,0 +1,188 @@ +package basic + +import ( + "math" + "time" +) + +const ( + planetOccultationFootprintBoundaryPoints = 180 + planetOccultationHorizonPoints = 360 + planetOccultationFootprintMaxSamples = 360 +) + +type planetOccultationFootprintSample struct { + point OccultationPathPoint + ok bool +} + +func planetOccultationFootprints( + startTT, endTT, greatestTT float64, + frameAt occultationPathFrameFunc, + options OccultationPathOptions, + location *time.Location, +) []PlanetOccultationFootprint { + stepDays := float64(options.Step) / float64(24*time.Hour) + times := occultationPathSampleTimesWithLimit( + startTT, endTT, greatestTT, stepDays, planetOccultationFootprintMaxSamples, + ) + footprints := make([]PlanetOccultationFootprint, 0, len(times)) + for _, tt := range times { + footprint, ok := planetOccultationFootprintAt(tt, frameAt, location) + if ok { + footprints = append(footprints, footprint) + } + } + return footprints +} + +func planetOccultationFootprintAt( + tt float64, + frameAt occultationPathFrameFunc, + location *time.Location, +) (PlanetOccultationFootprint, bool) { + frame, ok := frameAt(tt) + if !ok { + return PlanetOccultationFootprint{}, false + } + samples := make([]planetOccultationFootprintSample, planetOccultationFootprintBoundaryPoints) + for index := range samples { + theta := 2 * math.Pi * float64(index) / float64(len(samples)) + vector, _, valid := occultationPathBoundaryVector(frame, theta) + if valid { + samples[index] = planetOccultationFootprintSample{ + point: occultationPathPointFromVector(tt, vector, 0, location), + ok: true, + } + } + } + + segments, closed := planetOccultationFootprintSegments(samples) + polygons := make([][]OccultationPathPoint, 0, len(segments)) + for _, segment := range segments { + if len(segment) < 2 { + continue + } + polygon := append([]OccultationPathPoint(nil), segment...) + if closed { + polygon = append(polygon, polygon[0]) + } else { + polygon = append(polygon, planetOccultationHorizonArc(tt, frame, segment[len(segment)-1], segment[0], location)...) + } + if len(polygon) >= 4 { + polygons = append(polygons, polygon) + } + } + if len(polygons) == 0 { + return PlanetOccultationFootprint{}, false + } + return PlanetOccultationFootprint{ + Time: occultationTTToLocation(tt, location), + Polygons: polygons, + }, true +} + +func planetOccultationFootprintSegments( + samples []planetOccultationFootprintSample, +) ([][]OccultationPathPoint, bool) { + segments := make([][]OccultationPathPoint, 0, 2) + current := make([]OccultationPathPoint, 0, len(samples)) + allValid := len(samples) > 0 + for _, sample := range samples { + if !sample.ok { + allValid = false + if len(current) > 0 { + segments = append(segments, current) + current = nil + } + continue + } + current = append(current, sample.point) + } + if len(current) > 0 { + segments = append(segments, current) + } + if len(segments) > 1 && samples[0].ok && samples[len(samples)-1].ok { + first := segments[0] + last := segments[len(segments)-1] + merged := make([]OccultationPathPoint, 0, len(last)+len(first)) + merged = append(merged, last...) + merged = append(merged, first...) + segments[0] = merged + segments = segments[:len(segments)-1] + } + return segments, allValid && len(segments) == 1 +} + +func planetOccultationHorizonArc( + tt float64, + frame occultationPathFrame, + from, to OccultationPathPoint, + location *time.Location, +) []OccultationPathPoint { + sublunarLongitude, sublunarLatitude := occultationPathGeodetic(tt, frame.moon) + circle := planetOccultationSphericalCircle( + occultationTTToLocation(tt, location), sublunarLongitude, sublunarLatitude, + 90, planetOccultationHorizonPoints, + ) + fromIndex := planetOccultationNearestPointIndex(circle, from) + toIndex := planetOccultationNearestPointIndex(circle, to) + forwardSteps := (toIndex - fromIndex + len(circle)) % len(circle) + backwardSteps := (fromIndex - toIndex + len(circle)) % len(circle) + direction := 1 + steps := forwardSteps + if backwardSteps < forwardSteps { + direction = -1 + steps = backwardSteps + } + arc := make([]OccultationPathPoint, 0, steps+1) + for step := 1; step < steps; step++ { + index := (fromIndex + direction*step) % len(circle) + if index < 0 { + index += len(circle) + } + arc = append(arc, circle[index]) + } + return append(arc, to) +} + +func planetOccultationSphericalCircle( + value time.Time, + centerLongitude, centerLatitude, radius float64, + count int, +) []OccultationPathPoint { + centerLongitude *= math.Pi / 180 + centerLatitude *= math.Pi / 180 + radius *= math.Pi / 180 + points := make([]OccultationPathPoint, count) + for index := range points { + bearing := 2 * math.Pi * float64(index) / float64(count) + latitude := math.Asin( + math.Sin(centerLatitude)*math.Cos(radius) + + math.Cos(centerLatitude)*math.Sin(radius)*math.Cos(bearing), + ) + longitude := centerLongitude + math.Atan2( + math.Sin(bearing)*math.Sin(radius)*math.Cos(centerLatitude), + math.Cos(radius)-math.Sin(centerLatitude)*math.Sin(latitude), + ) + points[index] = OccultationPathPoint{ + Time: value, + Longitude: normalizeLongitude(longitude * 180 / math.Pi), + Latitude: latitude * 180 / math.Pi, + } + } + return points +} + +func planetOccultationNearestPointIndex(points []OccultationPathPoint, target OccultationPathPoint) int { + nearest := 0 + distance := math.Inf(1) + for index, point := range points { + candidate := occultationPathDistanceKM(point, target) + if candidate < distance { + nearest = index + distance = candidate + } + } + return nearest +} diff --git a/basic/occultation_planet_footprint_test.go b/basic/occultation_planet_footprint_test.go new file mode 100644 index 0000000..51e508f --- /dev/null +++ b/basic/occultation_planet_footprint_test.go @@ -0,0 +1,116 @@ +package basic + +import ( + "errors" + "math" + "testing" + "time" +) + +func TestPlanetOccultationSaturnFootprintsContainCenterLine(t *testing.T) { + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + seedTT := occultationTimeToTT(time.Date(2025, time.February, 1, 4, 0, 48, 0, time.UTC)) + for _, contact := range []struct { + name string + frameAt occultationPathFrameFunc + }{ + {name: "partial", frameAt: func(tt float64) (occultationPathFrame, bool) { + return planetOccultationPathFrameAt(tt, config) + }}, + {name: "total", frameAt: func(tt float64) (occultationPathFrame, bool) { + return planetOccultationTotalPathFrameAt(tt, config) + }}, + } { + startTT, endTT, found := occultationPathWindowForFrame( + seedTT, seedTT-occultationPathSearchSpanDays, seedTT+occultationPathSearchSpanDays, + contact.frameAt, true, + ) + if !found { + t.Fatalf("%s center interval is unavailable", contact.name) + } + checked := 0 + for tt := startTT + 2.0/1440.0; tt < endTT-2.0/1440.0; tt += 5.0 / 1440.0 { + frame, frameOK := contact.frameAt(tt) + center, _, centerOK := occultationEarthLineIntersection(frame.moon, frame.axis) + footprint, footprintOK := planetOccultationFootprintAt(tt, contact.frameAt, time.UTC) + if !frameOK || !centerOK || !footprintOK { + t.Fatalf("%s center or footprint is unavailable at TT %.9f", contact.name, tt) + } + longitude, latitude := occultationPathGeodetic(tt, center) + if !planetOccultationFootprintContains(footprint, longitude, latitude) { + t.Fatalf("%s footprint does not contain center %.6f, %.6f at TT %.9f", + contact.name, longitude, latitude, tt) + } + checked++ + } + if checked < 10 { + t.Fatalf("%s checked only %d center samples", contact.name, checked) + } + } +} + +func TestPlanetOccultationPathRejectsExcessiveAggregateSampling(t *testing.T) { + start := time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC) + _, err := FindPlanetOccultationPaths( + start, start.Add(24*time.Hour), OccultationSaturn, + OccultationPathOptions{Step: time.Second}, + ) + if !errors.Is(err, ErrOccultationPathSamplingLimit) { + t.Fatalf("FindPlanetOccultationPaths() error = %v, want ErrOccultationPathSamplingLimit", err) + } +} + +func TestPlanetOccultationFootprintsHaveIndependentSampleBudget(t *testing.T) { + times := occultationPathSampleTimesWithLimit( + 0, 1, 0.500001, 1.0/86400.0, planetOccultationFootprintMaxSamples, + ) + if len(times) > planetOccultationFootprintMaxSamples { + t.Fatalf("footprint sample count = %d, maximum %d", len(times), planetOccultationFootprintMaxSamples) + } + foundGreatest := false + for _, sample := range times { + if sample == 0.500001 { + foundGreatest = true + break + } + } + if !foundGreatest { + t.Fatal("bounded footprint samples omitted greatest") + } +} + +func planetOccultationFootprintContains( + footprint PlanetOccultationFootprint, + longitude, latitude float64, +) bool { + for _, polygon := range footprint.Polygons { + inside := false + for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 { + currentX := math.Remainder(polygon[current].Longitude-longitude, 360) + previousX := math.Remainder(polygon[previous].Longitude-longitude, 360) + currentY := polygon[current].Latitude + previousY := polygon[previous].Latitude + if math.Abs(currentX-previousX) > 180 { + if currentX < previousX { + currentX += 360 + } else { + previousX += 360 + } + } + if (currentY > latitude) == (previousY > latitude) { + continue + } + intersectionX := previousX + (latitude-previousY)*(currentX-previousX)/(currentY-previousY) + if intersectionX > 0 { + inside = !inside + } + } + if inside { + return true + } + } + return false +} diff --git a/basic/occultation_planet_path.go b/basic/occultation_planet_path.go new file mode 100644 index 0000000..58553e3 --- /dev/null +++ b/basic/occultation_planet_path.go @@ -0,0 +1,895 @@ +package basic + +import ( + "math" + "sort" + "time" +) + +const planetOccultationPathMaxTemporalSamples = 5000 + +type occultationPathFrameFunc func(float64) (occultationPathFrame, bool) + +// FindPlanetOccultationPaths 搜索有限盘面行星月掩的全球外接触和内接触掩带。 +// 查询窗口按全球几何掩甚点选择事件;端点容差 10 ms 与数值根精度一致。求解成功时,每条路径扩展到完整全球起止点。 +// FindPlanetOccultationPaths searches the global outer- and inner-contact footprints of one finite-disk planet. +// The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end when solved. +func FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet, + options OccultationPathOptions) ([]PlanetOccultationPath, error) { + if err := validateOccultationTimeRange(start, end); err != nil { + return nil, err + } + if err := planet.Validate(); err != nil { + return nil, err + } + if err := options.Validate(); err != nil { + return nil, err + } + + config, _ := planetOccultationConfigFor(planet) + options = normalizeOccultationPathOptions(options) + startTT := occultationTimeToTT(start) + endTT := occultationTimeToTT(end) + candidateStartTT := startTT - occultationPathSearchSpanDays + candidateEndTT := endTT + occultationPathSearchSpanDays + candidates := planetOccultationCandidateGreatestTimes( + candidateStartTT, candidateEndTT, planetOccultationDefaultStepDays, config, nil, 0, + ) + paths := make([]PlanetOccultationPath, 0, len(candidates)) + for _, seedTT := range candidates { + path, ok, err := planetOccultationPathAtSeed(seedTT, config, options, start.Location()) + if err != nil { + return nil, err + } + if !ok { + continue + } + if !occultationTimeInSelectionWindow(path.Greatest.Time, start, end) { + continue + } + if len(paths) > 0 && math.Abs(paths[len(paths)-1].Greatest.Time.Sub(path.Greatest.Time).Seconds()) <= 60 { + continue + } + paths = append(paths, path) + } + sort.SliceStable(paths, func(i, j int) bool { + return paths[i].Greatest.Time.Before(paths[j].Greatest.Time) + }) + return paths, nil +} + +func planetOccultationPathAtSeed( + seedTT float64, + config planetOccultationConfig, + options OccultationPathOptions, + location *time.Location, +) (PlanetOccultationPath, bool, error) { + frameAt := func(tt float64) (occultationPathFrame, bool) { + return planetOccultationPathFrameAt(tt, config) + } + totalFrameAt := func(tt float64) (occultationPathFrame, bool) { + return planetOccultationTotalPathFrameAt(tt, config) + } + searchStart := seedTT - occultationPathSearchSpanDays + searchEnd := seedTT + occultationPathSearchSpanDays + outerStart, outerEnd, ok := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, false) + if !ok { + return PlanetOccultationPath{}, false, nil + } + centerStart, centerEnd, hasCenter := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, true) + greatestTT := occultationPathGreatestForFrame(seedTT, outerStart, outerEnd, frameAt) + greatest, greatestOK := occultationPathCenterPointForFrame(greatestTT, frameAt, location) + if !greatestOK && hasCenter { + greatestTT = math.Max(centerStart, math.Min(centerEnd, greatestTT)) + greatest, greatestOK = occultationPathCenterPointForFrame(greatestTT, frameAt, location) + } + if !greatestOK { + greatest, greatestOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, location) + } + if !greatestOK { + return PlanetOccultationPath{}, false, nil + } + _, _, greatestWidth, greatestWidthOK := occultationPathLimitsAndWidthForFrame(greatestTT, frameAt) + if !greatestWidthOK || greatestWidth <= 0 { + return PlanetOccultationPath{}, false, nil + } + // 仅有边界的事件没有影轴与椭球交点;原回退点使用纬度极值弦宽,全掩带使用下方的地面横向宽度。统一两种接触带宽度定义,使有限盘面内外接触宽度可比较。 + // Boundary-only events do not have an axis/ellipsoid intersection. Their fallback point used to carry a latitude-extrema chord width, while total bands used the ground cross-track width below. Keep both contact bands on the same width definition so finite-disk inner/outer widths are comparable. + greatest.WidthKM = greatestWidth + totalStartTT, totalEndTT, hasTotal := occultationPathWindowForFrame( + seedTT, searchStart, searchEnd, totalFrameAt, false, + ) + hasTotal = hasTotal && greatestTT >= totalStartTT && greatestTT <= totalEndTT + if planetOccultationPathTemporalSampleCount( + outerStart, outerEnd, centerStart, centerEnd, hasCenter, + totalStartTT, totalEndTT, hasTotal, greatestTT, options, + ) > planetOccultationPathMaxTemporalSamples { + return PlanetOccultationPath{}, false, ErrOccultationPathSamplingLimit + } + + start := occultationPathBoundaryEndpointForFrame(outerStart, frameAt, location, 1) + end := occultationPathBoundaryEndpointForFrame(outerEnd, frameAt, location, -1) + if !start.valid || !end.valid { + return PlanetOccultationPath{}, false, nil + } + centerLine, northern, southern, err := planetOccultationPathSamples( + outerStart, outerEnd, centerStart, centerEnd, hasCenter, greatestTT, frameAt, options, location, + ) + if err != nil { + return PlanetOccultationPath{}, false, err + } + path := PlanetOccultationPath{ + Planet: config.planet, + TargetID: config.planet.String(), + Start: start.point, + Greatest: greatest, + End: end.point, + Complete: outerStart > searchStart && outerEnd < searchEnd, + CenterLine: centerLine, + NorthernLimit: occultationPathWithEndpoints(start.point, end.point, northern), + SouthernLimit: occultationPathWithEndpoints(start.point, end.point, southern), + Step: options.Step, + TargetSpacingKM: options.TargetSpacingKM, + } + path.PartialFootprints = planetOccultationFootprints( + outerStart, outerEnd, greatestTT, frameAt, options, location, + ) + + if !hasTotal { + return path, true, nil + } + totalStart := occultationPathBoundaryEndpointForFrame(totalStartTT, totalFrameAt, location, 1) + totalEnd := occultationPathBoundaryEndpointForFrame(totalEndTT, totalFrameAt, location, -1) + _, _, totalWidth, totalWidthOK := occultationPathLimitsAndWidthForFrame(greatestTT, totalFrameAt) + if !totalStart.valid || !totalEnd.valid || !totalWidthOK || totalWidth <= 0 { + return path, true, nil + } + totalNorthern, totalSouthern := occultationPathBoundarySamplesForFrame( + totalStartTT, totalEndTT, greatestTT, totalFrameAt, options, location, + ) + if len(totalNorthern) == 0 || len(totalSouthern) == 0 { + return path, true, nil + } + path.HasTotalBand = true + path.TotalStart = totalStart.point + path.TotalEnd = totalEnd.point + path.TotalComplete = totalStartTT > searchStart && totalEndTT < searchEnd + path.NorthernTotalLimit = occultationPathWithEndpoints(totalStart.point, totalEnd.point, totalNorthern) + path.SouthernTotalLimit = occultationPathWithEndpoints(totalStart.point, totalEnd.point, totalSouthern) + path.TotalFootprints = planetOccultationFootprints( + totalStartTT, totalEndTT, greatestTT, totalFrameAt, options, location, + ) + path.GreatestTotalWidthKM = totalWidth + return path, true, nil +} + +func planetOccultationPathTemporalSampleCount( + outerStartTT, outerEndTT float64, + centerStartTT, centerEndTT float64, + hasCenter bool, + totalStartTT, totalEndTT float64, + hasTotal bool, + greatestTT float64, + options OccultationPathOptions, +) int { + stepDays := float64(options.Step) / float64(24*time.Hour) + count := len(occultationPathSampleTimes(outerStartTT, outerEndTT, greatestTT, stepDays)) + count += len(occultationPathSampleTimesWithLimit( + outerStartTT, outerEndTT, greatestTT, stepDays, planetOccultationFootprintMaxSamples, + )) + if hasCenter { + count += len(occultationPathSampleTimes(centerStartTT, centerEndTT, greatestTT, stepDays)) + } + if hasTotal { + count += len(occultationPathSampleTimes(totalStartTT, totalEndTT, greatestTT, stepDays)) + count += len(occultationPathSampleTimesWithLimit( + totalStartTT, totalEndTT, greatestTT, stepDays, planetOccultationFootprintMaxSamples, + )) + } + return count +} + +func occultationPathWindowForFrame( + seedTT, startTT, endTT float64, + frameAt occultationPathFrameFunc, + center bool, +) (float64, float64, bool) { + left := math.Max(startTT, seedTT-occultationPathSearchSpanDays) + right := math.Min(endTT, seedTT+occultationPathSearchSpanDays) + if right <= left { + return 0, 0, false + } + predicate := func(tt float64) bool { + frame, ok := frameAt(tt) + if !ok { + return false + } + if center { + _, _, ok = occultationEarthLineIntersection(frame.moon, frame.axis) + return ok + } + return occultationPathFrameHasBoundary(frame) + } + + step := occultationPathRangeStepDays + first := math.NaN() + previous := left + previousOK := predicate(previous) + if previousOK { + first = previous + } else { + for tt := left + step; tt <= right; tt += step { + current := math.Min(tt, right) + currentOK := predicate(current) + if currentOK { + first = occultationPathRefineTransition(previous, current, predicate, false) + break + } + previous = current + previousOK = currentOK + } + } + if math.IsNaN(first) { + return 0, 0, false + } + + last := first + previous = first + previousOK = true + for tt := first + step; tt <= right; tt += step { + current := math.Min(tt, right) + currentOK := predicate(current) + if !currentOK { + last = occultationPathRefineTransition(previous, current, predicate, true) + return first, last, true + } + last = current + previous = current + previousOK = currentOK + } + if previousOK { + last = right + } + return first, last, true +} + +func occultationPathGreatestForFrame(seedTT, startTT, endTT float64, frameAt occultationPathFrameFunc) float64 { + left := math.Max(startTT, seedTT-0.75) + right := math.Min(endTT, seedTT+0.75) + if right <= left { + return seedTT + } + impact := func(tt float64) float64 { + frame, ok := frameAt(tt) + if !ok { + return math.Inf(1) + } + return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY()) + } + const goldenRatio = 0.6180339887498949 + x1 := right - goldenRatio*(right-left) + x2 := left + goldenRatio*(right-left) + f1 := impact(x1) + f2 := impact(x2) + for i := 0; i < 56; i++ { + if f1 > f2 { + left = x1 + x1, f1 = x2, f2 + x2 = left + goldenRatio*(right-left) + f2 = impact(x2) + } else { + right = x2 + x2, f2 = x1, f1 + x1 = right - goldenRatio*(right-left) + f1 = impact(x1) + } + } + return (left + right) / 2 +} + +func planetOccultationPathSamples( + outerStartTT, outerEndTT float64, + centerStartTT, centerEndTT float64, + hasCenter bool, + greatestTT float64, + frameAt occultationPathFrameFunc, + options OccultationPathOptions, + location *time.Location, +) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) { + var centerLine []OccultationPathPoint + if hasCenter { + var err error + centerLine, err = occultationPathCenterSamplesForFrame(centerStartTT, centerEndTT, greatestTT, frameAt, options, location) + if err != nil { + return nil, nil, nil, err + } + } + northern, southern := occultationPathBoundarySamplesForFrame( + outerStartTT, outerEndTT, greatestTT, frameAt, options, location, + ) + return centerLine, northern, southern, nil +} + +func occultationPathBoundarySamplesForFrame( + startTT, endTT, greatestTT float64, + frameAt occultationPathFrameFunc, + options OccultationPathOptions, + location *time.Location, +) ([]OccultationPathPoint, []OccultationPathPoint) { + stepDays := float64(options.Step) / float64(24*time.Hour) + times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays) + samples := make([]occultationPathBoundaryPairSample, 0, len(times)) + for _, tt := range times { + firstVector, secondVector, ok := occultationPathCrossTrackLimitsForFrame(tt, frameAt) + if !ok { + continue + } + sample := occultationPathBoundaryPairSample{tt: tt, first: firstVector, second: secondVector} + if len(samples) == 0 { + samples = append(samples, sample) + continue + } + samples = appendOccultationPathBoundaryPairSegment(samples, samples[len(samples)-1], sample, frameAt, 0) + } + first := make([]OccultationPathPoint, len(samples)) + second := make([]OccultationPathPoint, len(samples)) + for index, sample := range samples { + first[index] = occultationPathPointFromVector(sample.tt, sample.first, 0, location) + second[index] = occultationPathPointFromVector(sample.tt, sample.second, 0, location) + } + return occultationPathOrientBoundarySamples(first, second, greatestTT) +} + +type occultationPathBoundaryPairSample struct { + tt float64 + first, second occultationPathVector +} + +func appendOccultationPathBoundaryPairSegment( + samples []occultationPathBoundaryPairSample, + start, end occultationPathBoundaryPairSample, + frameAt occultationPathFrameFunc, + depth int, +) []occultationPathBoundaryPairSample { + if depth >= occultationPathMaxAdaptiveDepth || + occultationPathBoundaryPairSpacing(start, end) <= occultationPathBoundarySpacingKM { + return append(samples, end) + } + midTT := (start.tt + end.tt) / 2 + first, second, ok := occultationPathCrossTrackLimitsForFrame(midTT, frameAt) + if !ok { + return append(samples, end) + } + mid := occultationPathBoundaryPairSample{tt: midTT, first: first, second: second} + samples = appendOccultationPathBoundaryPairSegment(samples, start, mid, frameAt, depth+1) + return appendOccultationPathBoundaryPairSegment(samples, mid, end, frameAt, depth+1) +} + +func occultationPathBoundaryPairSpacing(first, second occultationPathBoundaryPairSample) float64 { + firstA := occultationPathEarthFixedVector(first.tt, first.first) + firstB := occultationPathEarthFixedVector(first.tt, first.second) + secondA := occultationPathEarthFixedVector(second.tt, second.first) + secondB := occultationPathEarthFixedVector(second.tt, second.second) + direct := math.Max( + occultationPathNorm(occultationPathSub(secondA, firstA)), + occultationPathNorm(occultationPathSub(secondB, firstB)), + ) + swapped := math.Max( + occultationPathNorm(occultationPathSub(secondB, firstA)), + occultationPathNorm(occultationPathSub(secondA, firstB)), + ) + return math.Min(direct, swapped) +} + +func occultationPathCrossTrackLimitsForFrame( + tt float64, + frameAt occultationPathFrameFunc, +) (occultationPathVector, occultationPathVector, bool) { + frame, ok := frameAt(tt) + before, beforeOK := frameAt(tt - occultationPathVelocityStepDays) + after, afterOK := frameAt(tt + occultationPathVelocityStepDays) + if !ok || !beforeOK || !afterOK { + return occultationPathVector{}, occultationPathVector{}, false + } + vx := after.moonProjectionX() - before.moonProjectionX() + vy := after.moonProjectionY() - before.moonProjectionY() + if math.Hypot(vx, vy) <= 1e-12 { + return occultationPathVector{}, occultationPathVector{}, false + } + + // 复用日食中心线构造:取基准面中垂直于运动方向的两条影锥母线。点源掠过阶段将缺失母线限制到可见角度区间;有限目标使用最近可见区间两端,直到两条横向母线分别与地球相交。 + // Match the solar-eclipse central-path construction: take the two shadow generators perpendicular to motion in the fundamental plane. During a grazing point-source phase, clamp a missing generator to the visible-angle interval. For a finite target, use both ends of the nearest visible interval until the two cross-track generators intersect Earth independently. + theta := math.Atan2(vx, -vy) + first, _, firstOK := occultationPathBoundaryVector(frame, theta) + second, _, secondOK := occultationPathBoundaryVector(frame, theta+math.Pi) + if frame.targetRadius == 0 { + if !firstOK { + first, firstOK = occultationPathBoundaryAtNearestPointSourceTheta(frame, theta) + } + if !secondOK { + second, secondOK = occultationPathBoundaryAtNearestPointSourceTheta(frame, theta+math.Pi) + } + if !firstOK || !secondOK { + return occultationPathVector{}, occultationPathVector{}, false + } + return first, second, true + } + intervals := occultationPathBoundaryThetaIntervals(frame) + interval, intervalOK := occultationPathNearestThetaInterval(intervals, theta) + if firstOK && secondOK { + if intervalOK && occultationPathAngleDistance(interval.left, theta) > occultationPathAngleDistance(interval.left, theta+math.Pi) { + first, second = second, first + } + return first, second, true + } + if !intervalOK { + return occultationPathVector{}, occultationPathVector{}, false + } + first, _, firstOK = occultationPathBoundaryVector(frame, interval.left) + second, _, secondOK = occultationPathBoundaryVector(frame, interval.right) + if !firstOK || !secondOK { + return occultationPathVector{}, occultationPathVector{}, false + } + return first, second, true +} + +func occultationPathAngleDistance(first, second float64) float64 { + return math.Abs(math.Remainder(first-second, 2*math.Pi)) +} + +func occultationPathBoundaryAtNearestPointSourceTheta( + frame occultationPathFrame, + theta float64, +) (occultationPathVector, bool) { + _, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame) + if !tangentOK { + return occultationPathVector{}, false + } + left, right, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta) + if !intervalOK { + return occultationPathVector{}, false + } + middle := (left + right) / 2 + theta += 2 * math.Pi * math.Round((middle-theta)/(2*math.Pi)) + if theta < left { + theta = left + } else if theta > right { + theta = right + } + point, _, ok := occultationPathBoundaryVector(frame, theta) + return point, ok +} + +type occultationPathThetaInterval struct { + left, right float64 +} + +func occultationPathNearestThetaInterval( + intervals []occultationPathThetaInterval, + theta float64, +) (occultationPathThetaInterval, bool) { + var closest occultationPathThetaInterval + closestDistance := math.Inf(1) + for _, interval := range intervals { + middle := (interval.left + interval.right) / 2 + firstDelta := math.Abs(math.Remainder(theta-middle, 2*math.Pi)) + secondDelta := math.Abs(math.Remainder(theta+math.Pi-middle, 2*math.Pi)) + distance := math.Min(firstDelta, secondDelta) + if distance < closestDistance { + closest = interval + closestDistance = distance + } + } + if !finite(closestDistance) { + return occultationPathThetaInterval{}, false + } + return closest, true +} + +func occultationPathBoundaryThetaIntervals(frame occultationPathFrame) []occultationPathThetaInterval { + step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints) + discriminants := make([]float64, occultationPathBoundaryScanPoints) + for index := range discriminants { + discriminant, _, _, ok := occultationPathBoundaryLine(frame, step*float64(index)) + if !ok { + discriminant = math.Inf(-1) + } + discriminants[index] = discriminant + } + + intervals := make([]occultationPathThetaInterval, 0, 2) + for index, value := range discriminants { + previous := discriminants[(index+len(discriminants)-1)%len(discriminants)] + next := discriminants[(index+1)%len(discriminants)] + if value < previous || value < next { + continue + } + theta := occultationPathRefineBoundaryMaximum(frame, step*float64(index), step) + discriminant, b, scale, ok := occultationPathBoundaryLine(frame, theta) + tolerance := 1e-12 * math.Max(scale, 1) + if !ok || b >= 0 || discriminant < -tolerance { + continue + } + left, right, intervalOK := occultationPathBoundaryThetaInterval(frame, theta) + if intervalOK { + intervals = append(intervals, occultationPathThetaInterval{left: left, right: right}) + } + } + return intervals +} + +func occultationPathRefineBoundaryMaximum(frame occultationPathFrame, center, step float64) float64 { + left := center - step + right := center + step + const goldenRatio = 0.6180339887498949 + x1 := right - goldenRatio*(right-left) + x2 := left + goldenRatio*(right-left) + f1, _, _, _ := occultationPathBoundaryLine(frame, x1) + f2, _, _, _ := occultationPathBoundaryLine(frame, x2) + for iteration := 0; iteration < 40; iteration++ { + if f1 < f2 { + left = x1 + x1, f1 = x2, f2 + x2 = left + goldenRatio*(right-left) + f2, _, _, _ = occultationPathBoundaryLine(frame, x2) + } else { + right = x2 + x2, f2 = x1, f1 + x1 = right - goldenRatio*(right-left) + f1, _, _, _ = occultationPathBoundaryLine(frame, x1) + } + } + return (left + right) / 2 +} + +func occultationPathOrientBoundarySamples( + first, second []OccultationPathPoint, + greatestTT float64, +) ([]OccultationPathPoint, []OccultationPathPoint) { + if len(first) == 0 || len(first) != len(second) { + return first, second + } + nearest := 0 + nearestDelta := math.Inf(1) + for index := range first { + delta := math.Abs(centerTimeTT(first[index].Time) - greatestTT) + if delta < nearestDelta { + nearest = index + nearestDelta = delta + } + } + if first[nearest].Latitude >= second[nearest].Latitude { + return first, second + } + return second, first +} + +func occultationPathCenterSamplesForFrame( + startTT, endTT, greatestTT float64, + frameAt occultationPathFrameFunc, + options OccultationPathOptions, + location *time.Location, +) ([]OccultationPathPoint, error) { + stepDays := float64(options.Step) / float64(24*time.Hour) + times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays) + points := make([]OccultationPathPoint, 0, len(times)) + for _, tt := range times { + point, ok := occultationPathCenterPointForFrame(tt, frameAt, location) + if ok { + points = append(points, point) + } + } + if options.TargetSpacingKM > 0 { + return refineOccultationPathSpacingForFrame(points, frameAt, options.TargetSpacingKM, location) + } + return points, nil +} + +func refineOccultationPathSpacingForFrame( + points []OccultationPathPoint, + frameAt occultationPathFrameFunc, + targetSpacingKM float64, + location *time.Location, +) ([]OccultationPathPoint, error) { + if len(points) < 2 || targetSpacingKM <= 0 { + return points, nil + } + refined := make([]OccultationPathPoint, 0, len(points)) + refined = append(refined, points[0]) + widthAt := func(tt float64) (float64, bool) { + _, _, width, ok := occultationPathLimitsAndWidthForFrame(tt, frameAt) + return width, ok + } + for i := 1; i < len(points); i++ { + segmentStart := len(refined) - 1 + var err error + refined, err = appendOccultationPathSegmentForFrame(refined, points[i-1], points[i], frameAt, targetSpacingKM, location, 0) + if err != nil { + return nil, err + } + refineOccultationPathWidths(refined[segmentStart:], widthAt) + } + return refined, nil +} + +func appendOccultationPathSegmentForFrame( + points []OccultationPathPoint, + start, end OccultationPathPoint, + frameAt occultationPathFrameFunc, + targetSpacingKM float64, + location *time.Location, + depth int, +) ([]OccultationPathPoint, error) { + distance := occultationPathDistanceKM(start, end) + if distance <= targetSpacingKM { + if len(points) >= occultationPathMaxSampleCount { + return nil, ErrOccultationPathSamplingLimit + } + return append(points, end), nil + } + if depth >= occultationPathMaxAdaptiveDepth || len(points) >= occultationPathMaxSampleCount { + return nil, ErrOccultationPathSamplingLimit + } + midTT := (centerTimeTT(start.Time) + centerTimeTT(end.Time)) / 2 + mid, ok := occultationPathCenterPointForFrameWithoutWidth(midTT, frameAt, location) + if !ok { + return append(points, end), nil + } + mid.WidthKM = (start.WidthKM + end.WidthKM) / 2 + var err error + points, err = appendOccultationPathSegmentForFrame(points, start, mid, frameAt, targetSpacingKM, location, depth+1) + if err != nil { + return nil, err + } + return appendOccultationPathSegmentForFrame(points, mid, end, frameAt, targetSpacingKM, location, depth+1) +} + +func planetOccultationPathFrameAt(tt float64, config planetOccultationConfig) (occultationPathFrame, bool) { + return planetOccultationContactPathFrameAt(tt, config, false) +} + +func planetOccultationTotalPathFrameAt(tt float64, config planetOccultationConfig) (occultationPathFrame, bool) { + return planetOccultationContactPathFrameAt(tt, config, true) +} + +func planetOccultationContactPathFrameAt(tt float64, config planetOccultationConfig, total bool) (occultationPathFrame, bool) { + moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1) + moonDistance := HMoonAwayN(tt, -1) + planetRA, planetDec := config.apparentRaDecN(tt, -1) + planetDistance := config.earthDistanceN(tt, -1) * occultationPathAstronomicalUnitKM + if !finite(moonRA) || !finite(moonDec) || !finite(moonDistance) || moonDistance <= 0 || + !finite(planetRA) || !finite(planetDec) || !finite(planetDistance) || planetDistance <= moonDistance { + return occultationPathFrame{}, false + } + moon := occultationPathRaDecVector(moonRA, moonDec, moonDistance) + target := occultationPathRaDecVector(planetRA, planetDec, planetDistance) + moonToTarget := occultationPathSub(target, moon) + moonToTargetDistance := occultationPathNorm(moonToTarget) + moonRadius := MoonSemidiameter(tt) * math.Pi / (180 * 3600) + moonRadiusKM := occultationPathNorm(moon) * math.Sin(moonRadius) + contactRadiusKM := moonRadiusKM + config.equatorialRadiusKM + if total { + contactRadiusKM = moonRadiusKM - config.equatorialRadiusKM + } + if moonToTargetDistance <= math.Abs(contactRadiusKM) { + return occultationPathFrame{}, false + } + axis := occultationPathUnit(occultationPathScale(moonToTarget, -1)) + north := occultationPathVector{z: 1} + first := occultationPathCross(north, axis) + if occultationPathNorm(first) < 1e-12 { + first = occultationPathCross(occultationPathVector{x: 1}, axis) + } + first = occultationPathUnit(first) + second := occultationPathUnit(occultationPathCross(axis, first)) + return occultationPathFrame{ + moon: moon, + axis: axis, + first: first, + second: second, + moonRadius: moonRadius, + targetRadius: math.Asin(contactRadiusKM / moonToTargetDistance), + }, true +} + +func occultationPathFrameHasBoundary(frame occultationPathFrame) bool { + _, _, ok := occultationPathBoundaryTangent(frame) + return ok +} + +func occultationPathBoundaryEndpointForFrame( + tt float64, + frameAt occultationPathFrameFunc, + location *time.Location, + direction int, +) occultationPathEndpoint { + if _, ok := frameAt(tt); !ok { + return occultationPathEndpoint{} + } + for offset := 0; offset <= 3; offset++ { + candidateTT := tt + float64(direction*offset)*0.5/86400.0 + frame, ok := frameAt(candidateTT) + if !ok { + continue + } + vector, _, valid := occultationPathBoundaryTangent(frame) + if valid { + return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true} + } + } + return occultationPathEndpoint{} +} + +func occultationPathCenterPointForFrame( + tt float64, + frameAt occultationPathFrameFunc, + location *time.Location, +) (OccultationPathPoint, bool) { + frame, ok := frameAt(tt) + if !ok { + return OccultationPathPoint{}, false + } + point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis) + if !ok { + return OccultationPathPoint{}, false + } + width := 0.0 + if _, _, tangentWidth, limitsOK := occultationPathLimitsAndWidthForFrame(tt, frameAt); limitsOK { + width = tangentWidth + } + return occultationPathPointFromVectorWithMoon(tt, point, width, frame.moon, location), true +} + +func occultationPathCenterPointForFrameWithoutWidth( + tt float64, + frameAt occultationPathFrameFunc, + location *time.Location, +) (OccultationPathPoint, bool) { + frame, ok := frameAt(tt) + if !ok { + return OccultationPathPoint{}, false + } + point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis) + if !ok { + return OccultationPathPoint{}, false + } + return occultationPathPointFromVectorWithMoon(tt, point, 0, frame.moon, location), true +} + +func occultationPathBoundaryPointForFrame( + tt float64, + frameAt occultationPathFrameFunc, + location *time.Location, +) (OccultationPathPoint, bool) { + frame, ok := frameAt(tt) + if !ok { + return OccultationPathPoint{}, false + } + point, _, ok := occultationPathBoundaryTangent(frame) + if !ok { + return OccultationPathPoint{}, false + } + north, south, limitsOK := occultationPathScannedLimitsAtFrame(tt, frame) + return occultationPathPointFromVector(tt, point, occultationPathBoundaryWidth(north, south, limitsOK), location), true +} + +func occultationPathLimitsAndWidthForFrame( + tt float64, + frameAt occultationPathFrameFunc, +) (occultationPathVector, occultationPathVector, float64, bool) { + frame, ok := frameAt(tt) + if !ok { + return occultationPathVector{}, occultationPathVector{}, 0, false + } + beforeFrame, beforeOK := frameAt(tt - occultationPathVelocityStepDays) + afterFrame, afterOK := frameAt(tt + occultationPathVelocityStepDays) + if !beforeOK || !afterOK { + north, south, scannedOK := occultationPathScannedLimitsForFrame(tt, frame) + return north, south, occultationPathBoundaryWidth(north, south, scannedOK), scannedOK + } + + vx := afterFrame.moonProjectionX() - beforeFrame.moonProjectionX() + vy := afterFrame.moonProjectionY() - beforeFrame.moonProjectionY() + speed := math.Hypot(vx, vy) + if speed <= 1e-12 { + north, south, scannedOK := occultationPathScannedLimitsForFrame(tt, frame) + return north, south, occultationPathBoundaryWidth(north, south, scannedOK), scannedOK + } + planeCrossTrack := occultationPathUnit(occultationPathAdd( + occultationPathScale(frame.first, -vy/speed), + occultationPathScale(frame.second, vx/speed), + )) + + var centerFixed, groundCrossTrack occultationPathVector + groundWidthOK := false + center, centerOK := occultationPathTrackReference(frame) + before, beforeCenterOK := occultationPathTrackReference(beforeFrame) + after, afterCenterOK := occultationPathTrackReference(afterFrame) + if centerOK && beforeCenterOK && afterCenterOK { + centerFixed = occultationPathEarthFixedVector(tt, center) + beforeFixed := occultationPathEarthFixedVector(tt-occultationPathVelocityStepDays, before) + afterFixed := occultationPathEarthFixedVector(tt+occultationPathVelocityStepDays, after) + polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio + normal := occultationPathUnit(occultationPathVector{x: centerFixed.x, y: centerFixed.y, z: centerFixed.z / polarRatioSquared}) + track := occultationPathSub(afterFixed, beforeFixed) + track = occultationPathSub(track, occultationPathScale(normal, occultationPathDot(track, normal))) + if occultationPathNorm(track) > 1e-12 { + groundCrossTrack = occultationPathUnit(occultationPathCross(normal, occultationPathUnit(track))) + groundWidthOK = true + } + } + + minimumOffset := math.Inf(1) + maximumOffset := math.Inf(-1) + minimumGroundOffset := math.Inf(1) + maximumGroundOffset := math.Inf(-1) + var minimumPoint, maximumPoint occultationPathVector + var minimumGroundPoint, maximumGroundPoint occultationPathVector + consider := func(point occultationPathVector) { + offset := occultationPathDot(point, planeCrossTrack) + if offset < minimumOffset { + minimumOffset = offset + minimumPoint = point + } + if offset > maximumOffset { + maximumOffset = offset + maximumPoint = point + } + if groundWidthOK { + fixed := occultationPathEarthFixedVector(tt, point) + groundOffset := occultationPathDot(occultationPathSub(fixed, centerFixed), groundCrossTrack) + if groundOffset < minimumGroundOffset { + minimumGroundOffset = groundOffset + minimumGroundPoint = point + } + if groundOffset > maximumGroundOffset { + maximumGroundOffset = groundOffset + maximumGroundPoint = point + } + } + } + if tangentPoint, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame); tangentOK { + consider(tangentPoint) + if leftTheta, rightTheta, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta); intervalOK { + const intervalSamples = 128 + for i := 0; i <= intervalSamples; i++ { + theta := leftTheta + (rightTheta-leftTheta)*float64(i)/intervalSamples + if point, _, pointOK := occultationPathBoundaryVector(frame, theta); pointOK { + consider(point) + } + } + } + } + for i := 0; i < occultationPathBoundaryScanPoints; i++ { + point, _, pointOK := occultationPathBoundaryVector(frame, 2*math.Pi*float64(i)/float64(occultationPathBoundaryScanPoints)) + if !pointOK { + continue + } + consider(point) + } + if !finite(minimumOffset) || !finite(maximumOffset) { + return occultationPathVector{}, occultationPathVector{}, 0, false + } + width := maximumOffset - minimumOffset + if groundWidthOK && finite(minimumGroundOffset) && finite(maximumGroundOffset) { + width = maximumGroundOffset - minimumGroundOffset + // 有限目标会把月影打开或收束成圆锥;接近地平线时其影面投影可能折叠,使全球投影极值在远处地平线交点间跳变。地面轨迹极值仍位于掩带的同一物理侧。 + // A finite target opens or closes the lunar shadow into a cone. Near the horizon its shadow-plane projection can fold, causing the global projected extremum to jump between distant horizon intersections. Ground-track extrema remain on the same physical sides of the band. + if frame.targetRadius != 0 { + return maximumGroundPoint, minimumGroundPoint, width, true + } + } + _, minimumLatitude := occultationPathGeodetic(tt, minimumPoint) + _, maximumLatitude := occultationPathGeodetic(tt, maximumPoint) + if maximumLatitude >= minimumLatitude { + return maximumPoint, minimumPoint, width, true + } + return minimumPoint, maximumPoint, width, true +} + +func occultationPathScannedLimitsForFrame( + tt float64, + frame occultationPathFrame, +) (occultationPathVector, occultationPathVector, bool) { + return occultationPathScannedLimitsAtFrame(tt, frame) +} diff --git a/basic/occultation_planet_path_test.go b/basic/occultation_planet_path_test.go new file mode 100644 index 0000000..25162e0 --- /dev/null +++ b/basic/occultation_planet_path_test.go @@ -0,0 +1,342 @@ +package basic + +import ( + "math" + "strings" + "testing" + "time" +) + +func TestPlanetOccultationFiniteDiskExpandsOuterAndContractsTotalPath(t *testing.T) { + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + tt := occultationTimeToTT(time.Date(2024, time.August, 21, 2, 41, 36, 0, time.UTC)) + frameAt := func(tt float64) (occultationPathFrame, bool) { + return planetOccultationPathFrameAt(tt, config) + } + _, _, finiteWidth, finiteOK := occultationPathLimitsAndWidthForFrame(tt, frameAt) + if !finiteOK { + t.Fatal("finite-disk path limits are unavailable") + } + pointFrameAt := func(tt float64) (occultationPathFrame, bool) { + frame, valid := planetOccultationPathFrameAt(tt, config) + frame.targetRadius = 0 + return frame, valid + } + _, _, pointWidth, pointOK := occultationPathLimitsAndWidthForFrame(tt, pointFrameAt) + if !pointOK { + t.Fatal("point-source comparison limits are unavailable") + } + if finiteWidth <= pointWidth { + t.Fatalf("finite-disk outer width = %.6f km, want greater than point-source width %.6f km", finiteWidth, pointWidth) + } + if finiteWidth-pointWidth < 1 { + t.Fatalf("finite-disk expansion = %.6f km, want a measurable planetary-radius contribution", finiteWidth-pointWidth) + } + innerFrameAt := func(tt float64) (occultationPathFrame, bool) { + return planetOccultationTotalPathFrameAt(tt, config) + } + _, _, totalWidth, totalOK := occultationPathLimitsAndWidthForFrame(tt, innerFrameAt) + if !totalOK { + t.Fatal("finite-disk total-occultation limits are unavailable") + } + if totalWidth >= pointWidth { + t.Fatalf("finite-disk total width = %.6f km, want less than point-source width %.6f km", totalWidth, pointWidth) + } + if pointWidth-totalWidth < 1 { + t.Fatalf("finite-disk contraction = %.6f km, want a measurable planetary-radius contribution", pointWidth-totalWidth) + } +} + +func TestPlanetOccultationConesUseTwoSphereCommonTangents(t *testing.T) { + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + tt := occultationTimeToTT(time.Date(2025, time.February, 1, 4, 0, 48, 0, time.UTC)) + outer, ok := planetOccultationPathFrameAt(tt, config) + if !ok { + t.Fatal("Saturn outer-contact cone is unavailable") + } + inner, ok := planetOccultationTotalPathFrameAt(tt, config) + if !ok { + t.Fatal("Saturn inner-contact cone is unavailable") + } + + planetRA, planetDec := config.apparentRaDecN(tt, -1) + planetDistance := config.earthDistanceN(tt, -1) * occultationPathAstronomicalUnitKM + target := occultationPathRaDecVector(planetRA, planetDec, planetDistance) + moonToTargetDistance := occultationPathNorm(occultationPathSub(target, outer.moon)) + moonRadiusKM := occultationPathNorm(outer.moon) * math.Sin(outer.moonRadius) + wantOuter := math.Asin((moonRadiusKM + config.equatorialRadiusKM) / moonToTargetDistance) + wantInner := math.Asin((moonRadiusKM - config.equatorialRadiusKM) / moonToTargetDistance) + if difference := math.Abs(outer.targetRadius - wantOuter); difference > 1e-15 { + t.Fatalf("outer-contact cone angle = %.15g rad, want %.15g (difference %.3g)", outer.targetRadius, wantOuter, difference) + } + if difference := math.Abs(inner.targetRadius - wantInner); difference > 1e-15 { + t.Fatalf("inner-contact cone angle = %.15g rad, want %.15g (difference %.3g)", inner.targetRadius, wantInner, difference) + } + for _, contact := range []struct { + name string + frame occultationPathFrame + }{ + {name: "outer", frame: outer}, + {name: "inner", frame: inner}, + } { + origin, direction, rayOK := occultationPathBoundaryRay(contact.frame, 0.73) + if !rayOK { + t.Fatalf("%s-contact boundary ray is unavailable", contact.name) + } + moonNormal := occultationPathSub(origin, contact.frame.moon) + if difference := math.Abs(occultationPathNorm(moonNormal) - moonRadiusKM); difference > 1e-6 { + t.Fatalf("%s-contact lunar tangency radius differs by %.9f km", contact.name, difference) + } + if residual := math.Abs(occultationPathDot(moonNormal, direction)); residual > 1e-6 { + t.Fatalf("%s-contact ray/lunar-radius dot product = %.9f km", contact.name, residual) + } + targetParameter := occultationPathDot(occultationPathSub(target, origin), direction) + targetTangent := occultationPathAdd(origin, occultationPathScale(direction, targetParameter)) + targetNormal := occultationPathSub(targetTangent, target) + if difference := math.Abs(occultationPathNorm(targetNormal) - config.equatorialRadiusKM); difference > 1e-5 { + t.Fatalf("%s-contact planetary tangency radius differs by %.9f km", contact.name, difference) + } + if residual := math.Abs(occultationPathDot(targetNormal, direction)); residual > 1e-5 { + t.Fatalf("%s-contact ray/planet-radius dot product = %.9f km", contact.name, residual) + } + } +} + +func TestPlanetOccultationInnerConeUsesSignedTargetRadius(t *testing.T) { + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + tt := occultationTimeToTT(time.Date(2025, time.February, 1, 4, 0, 48, 0, time.UTC)) + frame, ok := planetOccultationTotalPathFrameAt(tt, config) + if !ok { + t.Fatal("Saturn inner-contact cone is unavailable") + } + + for index := 0; index < occultationPathBoundaryScanPoints; index++ { + theta := 2 * math.Pi * float64(index) / float64(occultationPathBoundaryScanPoints) + want, _, wantOK := occultationPathBoundaryVector(frame, theta) + if !wantOK { + continue + } + discriminant, _, scale, lineOK := occultationPathBoundaryLine(frame, theta) + if !lineOK || discriminant < 0 { + continue + } + got, _, gotOK := occultationPathBoundaryIntersection(frame, theta, 1e-12*math.Max(scale, 1)) + if !gotOK { + t.Fatalf("signed inner-cone intersection is unavailable at theta %.9f", theta) + } + if difference := occultationPathNorm(occultationPathSub(got, want)); difference > 1e-6 { + t.Fatalf("inner-cone intersection differs by %.6f km at theta %.9f", difference, theta) + } + return + } + t.Fatal("no comparable Saturn inner-cone boundary point found") +} + +func TestOccultationPathBoundaryTangentFindsBetweenSamples(t *testing.T) { + const boundaryRadiusKM = 1737.4 + theta := math.Pi / float64(occultationPathBoundaryScanPoints) + offset := occultationPathEarthEquatorialRadiusKM + boundaryRadiusKM - 0.01 + moon := occultationPathVector{ + x: 384000, + y: -offset * math.Cos(theta), + z: -offset * math.Sin(theta), + } + frame := occultationPathFrame{ + moon: moon, + axis: occultationPathVector{x: -1}, + first: occultationPathVector{y: 1}, + second: occultationPathVector{z: 1}, + moonRadius: math.Asin(boundaryRadiusKM / occultationPathNorm(moon)), + } + for _, sampledTheta := range []float64{0, 2 * math.Pi / float64(occultationPathBoundaryScanPoints)} { + if _, _, ok := occultationPathBoundaryVector(frame, sampledTheta); ok { + t.Fatalf("fixture is not narrower than the old sample spacing at theta %.9f", sampledTheta) + } + } + point, tangentTheta, ok := occultationPathBoundaryTangent(frame) + if !ok { + t.Fatal("continuous boundary tangency was not found between scan points") + } + if math.Abs(tangentTheta-theta) > 5e-5 { + t.Fatalf("tangent theta = %.9f, want %.9f", tangentTheta, theta) + } + polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio + ellipsoidResidual := point.x*point.x + point.y*point.y + point.z*point.z/polarRatioSquared - + occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM + if math.Abs(ellipsoidResidual) > 1e-3 { + t.Fatalf("tangent point ellipsoid residual = %.9f", ellipsoidResidual) + } + frameAt := func(float64) (occultationPathFrame, bool) { return frame, true } + if _, _, centerOK := occultationEarthLineIntersection(frame.moon, frame.axis); centerOK { + t.Fatal("synthetic center line unexpectedly intersects Earth") + } + north, south, width, limitsOK := occultationPathLimitsAndWidthForFrame(2451545, frameAt) + if !limitsOK { + t.Fatal("boundary-only event did not produce path limits") + } + if separation := occultationPathNorm(occultationPathSub(north, south)); separation <= 1e-6 { + t.Fatalf("boundary-only path limits collapsed to one point: separation=%.12f km", separation) + } + if width <= 0 { + t.Fatalf("boundary-only path width = %.12f km, want positive", width) + } + greatest, greatestOK := occultationPathBoundaryPointForFrame(2451545, frameAt, time.UTC) + if !greatestOK { + t.Fatal("boundary-only event did not produce a greatest surface point") + } + if greatest.WidthKM <= 0 { + t.Fatalf("boundary-only greatest width = %.12f km, want positive", greatest.WidthKM) + } +} + +func TestPlanetOccultationSaturnLimitsRemainContinuous(t *testing.T) { + location := time.FixedZone("UTC+8", 8*3600) + paths, err := FindPlanetOccultationPaths( + time.Date(2025, time.February, 1, 0, 0, 0, 0, location), + time.Date(2025, time.February, 2, 0, 0, 0, 0, location), + OccultationSaturn, + OccultationPathOptions{Step: 2 * time.Minute}, + ) + if err != nil { + t.Fatalf("FindPlanetOccultationPaths() error = %v", err) + } + if len(paths) != 1 { + t.Fatalf("FindPlanetOccultationPaths() returned %d paths, want 1", len(paths)) + } + + for _, limit := range []struct { + name string + points []OccultationPathPoint + }{ + {name: "outer northern", points: paths[0].NorthernLimit}, + {name: "outer southern", points: paths[0].SouthernLimit}, + {name: "total northern", points: paths[0].NorthernTotalLimit}, + {name: "total southern", points: paths[0].SouthernTotalLimit}, + } { + for index := 1; index < len(limit.points); index++ { + distance := occultationPathDistanceKM(limit.points[index-1], limit.points[index]) + if distance > 1000 { + t.Fatalf("%s limit jumps %.1f km between %v and %v", limit.name, distance, + limit.points[index-1].Time, limit.points[index].Time) + } + } + } +} + +func TestRefinedPlanetOccultationCenterLineRespectsWidthTolerance(t *testing.T) { + start := time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC) + paths, err := FindPlanetOccultationPaths( + start, start.Add(24*time.Hour), OccultationSaturn, + OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 50}, + ) + if err != nil || len(paths) != 1 { + t.Fatalf("FindPlanetOccultationPaths() paths=%d err=%v, want one", len(paths), err) + } + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + frameAt := func(tt float64) (occultationPathFrame, bool) { + return planetOccultationPathFrameAt(tt, config) + } + for index, point := range paths[0].CenterLine { + exact, pointOK := occultationPathCenterPointForFrame(centerTimeTT(point.Time), frameAt, time.UTC) + if !pointOK { + t.Fatalf("exact center point %d is unavailable", index) + } + if difference := math.Abs(point.WidthKM - exact.WidthKM); difference > occultationPathWidthToleranceKM { + t.Fatalf("center point %d width differs from exact value by %.9f km: got %.9f want %.9f", + index, difference, point.WidthKM, exact.WidthKM) + } + } +} + +func TestPlanetOccultationSaturnLimitsDoNotDependOnStep(t *testing.T) { + location := time.FixedZone("UTC+8", 8*3600) + start := time.Date(2024, time.August, 21, 0, 0, 0, 0, location) + end := time.Date(2024, time.August, 22, 0, 0, 0, 0, location) + fine := findSinglePlanetOccultationPath(t, start, end, 30*time.Second) + coarse := findSinglePlanetOccultationPath(t, start, end, 2*time.Minute) + + for _, limits := range []struct { + name string + fine, coarse []OccultationPathPoint + }{ + {name: "outer northern", fine: fine.NorthernLimit, coarse: coarse.NorthernLimit}, + {name: "outer southern", fine: fine.SouthernLimit, coarse: coarse.SouthernLimit}, + {name: "total northern", fine: fine.NorthernTotalLimit, coarse: coarse.NorthernTotalLimit}, + {name: "total southern", fine: fine.SouthernTotalLimit, coarse: coarse.SouthernTotalLimit}, + } { + assertOccultationPathCommonSamplesEqual(t, limits.name, limits.fine, limits.coarse) + for index := 1; index+1 < len(limits.coarse); index++ { + paired := coarse.SouthernLimit + if strings.HasPrefix(limits.name, "total") { + paired = coarse.SouthernTotalLimit + } + if strings.HasSuffix(limits.name, "southern") { + continue + } + if distance := occultationPathDistanceKM(limits.coarse[index], paired[index]); distance < 0.001 { + t.Fatalf("%s and southern limit collapse at %v", limits.name, limits.coarse[index].Time) + } + } + } +} + +func findSinglePlanetOccultationPath(t *testing.T, start, end time.Time, step time.Duration) PlanetOccultationPath { + t.Helper() + paths, err := FindPlanetOccultationPaths(start, end, OccultationSaturn, OccultationPathOptions{Step: step}) + if err != nil { + t.Fatalf("FindPlanetOccultationPaths(step=%v) error = %v", step, err) + } + if len(paths) != 1 { + t.Fatalf("FindPlanetOccultationPaths(step=%v) returned %d paths, want 1", step, len(paths)) + } + if !paths[0].HasTotalBand { + t.Fatalf("FindPlanetOccultationPaths(step=%v) has no total band", step) + } + return paths[0] +} + +func assertOccultationPathCommonSamplesEqual(t *testing.T, name string, fine, coarse []OccultationPathPoint) { + t.Helper() + matched := 0 + fineIndex := 0 + for _, coarsePoint := range coarse[1 : len(coarse)-1] { + for fineIndex+1 < len(fine) && fine[fineIndex].Time.Before(coarsePoint.Time.Add(-20*time.Millisecond)) { + fineIndex++ + } + nearest := -1 + nearestDelta := math.Inf(1) + for candidateIndex := fineIndex - 2; candidateIndex <= fineIndex+2; candidateIndex++ { + if candidateIndex < 0 || candidateIndex >= len(fine) { + continue + } + delta := math.Abs(fine[candidateIndex].Time.Sub(coarsePoint.Time).Seconds()) + if delta < nearestDelta { + nearest = candidateIndex + nearestDelta = delta + } + } + if nearest < 0 || nearestDelta > 0.00001 { + continue + } + matched++ + if distance := occultationPathDistanceKM(fine[nearest], coarsePoint); distance > 5 { + t.Fatalf("%s differs by %.1f km at common time %v (sample delta %.6f s)", + name, distance, coarsePoint.Time, nearestDelta) + } + } + if matched < 10 { + t.Fatalf("%s compared only %d common samples, want at least 10", name, matched) + } +} diff --git a/basic/occultation_planet_test.go b/basic/occultation_planet_test.go new file mode 100644 index 0000000..3986e91 --- /dev/null +++ b/basic/occultation_planet_test.go @@ -0,0 +1,284 @@ +package basic + +import ( + "math" + "testing" + "time" +) + +func TestPlanetOccultationSupportsAllPlanetTargets(t *testing.T) { + tt := TD2UT(Date2JDE(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)), true) + tests := []struct { + planet OccultationPlanet + name string + }{ + {OccultationMercury, "Mercury"}, + {OccultationVenus, "Venus"}, + {OccultationMars, "Mars"}, + {OccultationJupiter, "Jupiter"}, + {OccultationSaturn, "Saturn"}, + {OccultationUranus, "Uranus"}, + {OccultationNeptune, "Neptune"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.planet.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + if test.planet.String() != test.name { + t.Fatalf("String() = %q, want %q", test.planet.String(), test.name) + } + config, ok := planetOccultationConfigFor(test.planet) + if !ok { + t.Fatal("planet occultation config is unavailable") + } + state := planetOccultationStateAt(tt, config, nil, -1) + if !state.valid || state.planetSemidiameter <= 0 || state.moonSemidiameter <= state.planetSemidiameter { + t.Fatalf("invalid planet state: %+v", state) + } + }) + } +} + +func TestPlanetOccultationSaturnContactsSolveDynamicDiskMetrics(t *testing.T) { + observer := Observer{Longitude: -30.072, Latitude: 16.21} + start := time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC) + end := time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC) + results, err := FindPlanetOccultations( + start, end, OccultationSaturn, + observer.Longitude, observer.Latitude, observer.Height, + OccultationSearchOptions{}, + ) + if err != nil { + t.Fatalf("FindPlanetOccultations() error = %v", err) + } + if len(results) != 1 { + t.Fatalf("FindPlanetOccultations() returned %d events, want 1", len(results)) + } + result := results[0] + if result.Type != OccultationTotal || !result.HasInternalContacts || !result.ContactsComplete { + t.Fatalf("unexpected event geometry: type=%q internal=%v complete=%v", result.Type, result.HasInternalContacts, result.ContactsComplete) + } + + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + contacts := []struct { + name string + value time.Time + internal bool + }{ + {"C1", result.ExternalImmersion, false}, + {"C2", result.InternalImmersion, true}, + {"C3", result.InternalEmersion, true}, + {"C4", result.ExternalEmersion, false}, + } + for _, contact := range contacts { + state := planetOccultationStateAt(occultationTimeToTT(contact.value), config, &observer, -1) + metric := state.externalContactMetric + if contact.internal { + metric = state.internalContactMetric + } + if math.Abs(metric) > 0.1 { + t.Errorf("%s contact residual = %.6f arcsec, want <= 0.1", contact.name, metric) + } + } + + if !(result.ExternalImmersion.Before(result.InternalImmersion) && + result.InternalImmersion.Before(result.Greatest) && + result.Greatest.Before(result.InternalEmersion) && + result.InternalEmersion.Before(result.ExternalEmersion)) { + t.Fatalf("contact order is invalid: %+v", result) + } + if result.PlanetSemidiameterArcsec <= 0 || result.MoonSemidiameterArcsec <= result.PlanetSemidiameterArcsec { + t.Fatalf("invalid dynamic semidiameters: Moon=%.6f planet=%.6f", result.MoonSemidiameterArcsec, result.PlanetSemidiameterArcsec) + } +} + +func TestPlanetOccultationUsesStationMoonDistanceForRadius(t *testing.T) { + tt := occultationTimeToTT(time.Date(2024, time.August, 21, 2, 49, 10, 0, time.UTC)) + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + moonRA, _ := HMoonGeocentricApparentRaDecN(tt, -1) + subMoonLongitude := normalizeLongitude180(moonRA - ApparentSiderealTime(TD2UT(tt, false))*15) + near := Observer{Longitude: subMoonLongitude, Latitude: 0} + far := Observer{Longitude: normalizeLongitude180(subMoonLongitude + 180), Latitude: 0} + nearState := planetOccultationStateAt(tt, config, &near, -1) + farState := planetOccultationStateAt(tt, config, &far, -1) + if !nearState.valid || !farState.valid { + t.Fatalf("invalid station states: near=%+v far=%+v", nearState, farState) + } + if nearState.moonSemidiameter <= farState.moonSemidiameter { + t.Fatalf("station Moon radius did not follow station distance: near=%.6f far=%.6f", nearState.moonSemidiameter, farState.moonSemidiameter) + } + if nearState.moonSemidiameter-MoonSemidiameterN(tt, -1) <= 0 || + farState.moonSemidiameter-MoonSemidiameterN(tt, -1) >= 0 { + t.Fatalf("station radius does not straddle geocentric radius: near=%.6f geo=%.6f far=%.6f", + nearState.moonSemidiameter, MoonSemidiameterN(tt, -1), farState.moonSemidiameter) + } +} + +func TestPlanetOccultationUsesStationPlanetDistanceForRadius(t *testing.T) { + tt := occultationTimeToTT(time.Date(2024, time.March, 11, 1, 0, 0, 0, time.UTC)) + config, ok := planetOccultationConfigFor(OccultationMercury) + if !ok { + t.Fatal("Mercury occultation config is unavailable") + } + planetRA, _ := config.apparentRaDecN(tt, -1) + subPlanetLongitude := normalizeLongitude180(planetRA - ApparentSiderealTime(TD2UT(tt, false))*15) + near := Observer{Longitude: subPlanetLongitude, Latitude: 0} + far := Observer{Longitude: normalizeLongitude180(subPlanetLongitude + 180), Latitude: 0} + nearState := planetOccultationStateAt(tt, config, &near, -1) + farState := planetOccultationStateAt(tt, config, &far, -1) + if !nearState.valid || !farState.valid { + t.Fatalf("invalid station states: near=%+v far=%+v", nearState, farState) + } + if nearState.planetSemidiameter <= farState.planetSemidiameter { + t.Fatalf("station planet radius did not follow station distance: near=%.12f far=%.12f", + nearState.planetSemidiameter, farState.planetSemidiameter) + } + geocentric := config.semidiameterN(tt, -1) + if nearState.planetSemidiameter <= geocentric || farState.planetSemidiameter >= geocentric { + t.Fatalf("station planet radius does not straddle geocentric radius: near=%.12f geo=%.12f far=%.12f", + nearState.planetSemidiameter, geocentric, farState.planetSemidiameter) + } +} + +func TestPlanetOccultationBestObserverRefinesDynamicMetric(t *testing.T) { + config, ok := planetOccultationConfigFor(OccultationSaturn) + if !ok { + t.Fatal("Saturn occultation config is unavailable") + } + startTT := occultationTimeToTT(time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC)) + endTT := occultationTimeToTT(time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC)) + seedTT := occultationTimeToTT(time.Date(2024, time.August, 21, 2, 49, 10, 0, time.UTC)) + bestTT, observer, _, bestOK := planetOccultationBestObserver(seedTT, startTT, endTT, config) + if !bestOK { + t.Fatal("best-observer search failed") + } + metric := planetOccultationExternalContactMetric(bestTT, config, &observer, -1) + for _, deltaSeconds := range []float64{-0.1, 0.1} { + neighbor := planetOccultationExternalContactMetric(bestTT+deltaSeconds/86400, config, &observer, -1) + if metric > neighbor+1e-6 { + t.Fatalf("best time does not minimize the dynamic metric: center=%.12f neighbor(%+.1fs)=%.12f", + metric, deltaSeconds, neighbor) + } + } +} + +func TestPlanetOccultationInnerPlanetContactsUseDynamicTargets(t *testing.T) { + tests := []struct { + name string + planet OccultationPlanet + start time.Time + end time.Time + }{ + {"Mercury", OccultationMercury, time.Date(2024, time.March, 10, 0, 0, 0, 0, time.UTC), time.Date(2024, time.March, 12, 0, 0, 0, 0, time.UTC)}, + {"Venus", OccultationVenus, time.Date(2024, time.April, 6, 0, 0, 0, 0, time.UTC), time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results, err := FindBestPlanetOccultations(test.start, test.end, test.planet, OccultationSearchOptions{MaxEvents: 1}) + if err != nil { + t.Fatalf("FindBestPlanetOccultations() error = %v", err) + } + if len(results) != 1 { + t.Fatalf("FindBestPlanetOccultations() returned %d events, want 1", len(results)) + } + result := results[0] + if result.Planet != test.planet || !result.HasInternalContacts || !result.ContactsComplete { + t.Fatalf("unexpected event geometry: %+v", result) + } + if !(result.ExternalImmersion.Before(result.InternalImmersion) && + result.InternalImmersion.Before(result.Greatest) && + result.Greatest.Before(result.InternalEmersion) && + result.InternalEmersion.Before(result.ExternalEmersion)) { + t.Fatalf("contact order is invalid: %+v", result) + } + + config, ok := planetOccultationConfigFor(test.planet) + if !ok { + t.Fatalf("%s occultation config is unavailable", test.name) + } + contacts := []struct { + value time.Time + internal bool + }{ + {result.ExternalImmersion, false}, + {result.InternalImmersion, true}, + {result.InternalEmersion, true}, + {result.ExternalEmersion, false}, + } + for index, contact := range contacts { + state := planetOccultationStateAt(occultationTimeToTT(contact.value), config, &result.Observer, -1) + metric := state.externalContactMetric + if contact.internal { + metric = state.internalContactMetric + } + if math.Abs(metric) > 0.1 { + t.Errorf("contact %d residual = %.6f arcsec, want <= 0.1", index+1, metric) + } + } + }) + } +} + +func TestPlanetOccultationOuterPlanetContactsUseDynamicTargets(t *testing.T) { + tests := []struct { + name string + planet OccultationPlanet + start time.Time + end time.Time + observer Observer + }{ + {"Mars", OccultationMars, time.Date(2020, 2, 18, 11, 0, 0, 0, time.UTC), time.Date(2020, 2, 18, 16, 0, 0, 0, time.UTC), Observer{Longitude: -76.01, Latitude: 29.918}}, + {"Jupiter", OccultationJupiter, time.Date(2020, 1, 23, 0, 0, 0, 0, time.UTC), time.Date(2020, 1, 23, 5, 0, 0, 0, time.UTC), Observer{Longitude: 120.15, Latitude: -45.552}}, + {"Uranus", OccultationUranus, time.Date(2022, 2, 7, 19, 0, 0, 0, time.UTC), time.Date(2022, 2, 7, 22, 0, 0, 0, time.UTC), Observer{Longitude: 11.186, Latitude: -62.948}}, + {"Neptune", OccultationNeptune, time.Date(2023, 9, 1, 7, 0, 0, 0, time.UTC), time.Date(2023, 9, 1, 10, 0, 0, 0, time.UTC), Observer{Longitude: -13.896, Latitude: -62.038}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results, err := FindPlanetOccultations( + test.start, test.end, test.planet, + test.observer.Longitude, test.observer.Latitude, test.observer.Height, + OccultationSearchOptions{}, + ) + if err != nil { + t.Fatalf("FindPlanetOccultations() error = %v", err) + } + if len(results) != 1 { + t.Fatalf("FindPlanetOccultations() returned %d events, want 1", len(results)) + } + result := results[0] + if result.Type != OccultationTotal || !result.HasInternalContacts || !result.ContactsComplete { + t.Fatalf("unexpected event geometry: %+v", result) + } + config, ok := planetOccultationConfigFor(test.planet) + if !ok { + t.Fatalf("%s occultation config is unavailable", test.name) + } + contacts := []struct { + value time.Time + internal bool + }{ + {result.ExternalImmersion, false}, + {result.InternalImmersion, true}, + {result.InternalEmersion, true}, + {result.ExternalEmersion, false}, + } + for index, contact := range contacts { + state := planetOccultationStateAt(occultationTimeToTT(contact.value), config, &test.observer, -1) + metric := state.externalContactMetric + if contact.internal { + metric = state.internalContactMetric + } + if math.Abs(metric) > 0.1 { + t.Errorf("contact %d residual = %.6f arcsec, want <= 0.1", index+1, metric) + } + } + }) + } +} diff --git a/basic/occultation_star.go b/basic/occultation_star.go new file mode 100644 index 0000000..95df268 --- /dev/null +++ b/basic/occultation_star.go @@ -0,0 +1,719 @@ +package basic + +import ( + "math" + "sort" + "time" +) + +const ( + starOccultationSiderealMonthDays = 27.321661 + starOccultationDefaultStepDays = 0.25 + starOccultationContactStepDays = 10.0 / 1440.0 + starOccultationContactSpanDays = 2.0 + starOccultationLatitudeMarginAS = 3600.0 + starOccultationMoonLatitudeDeg = 6.0 + starOccultationGrazingTolerance = 0.01 + starOccultationRootToleranceDays = occultationEventSelectionToleranceDays + starOccultationMaxContactSteps = 10000 +) + +// FindStarOccultations 搜索单颗点源恒星的月掩星。 +// +// 输入坐标会从历元传播并转换到当日视坐标系;若提供视差,还会修正观测者的恒星视差。 +// 搜索不会加载内嵌 9100 星表;需要全星表搜索时,调用者必须显式加载并选择恒星。经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。 +// FindStarOccultations searches for lunar occultations of one point-source star. +// The input coordinate is propagated from its epoch, converted to the apparent frame of date, and corrected for the observer's stellar parallax when one is supplied. +// The search does not load the embedded 9100-star catalog; callers that need a catalog-wide search must load and select stars explicitly. Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is the observer elevation above mean sea level in meters. +func FindStarOccultations(start, end time.Time, star StarCoordinate, longitude, latitude, height float64, + options OccultationSearchOptions) ([]StarOccultationInfo, error) { + if err := validateOccultationTimeRange(start, end); err != nil { + return nil, err + } + if err := star.Validate(); err != nil { + return nil, err + } + observer := Observer{Longitude: longitude, Latitude: latitude, Height: height} + if err := observer.Validate(); err != nil { + return nil, err + } + if err := options.Validate(); err != nil { + return nil, err + } + + startTT := occultationTimeToTT(start) + endTT := occultationTimeToTT(end) + resultLocation := start.Location() + results := make([]StarOccultationInfo, 0) + for _, greatestTT := range starOccultationCandidateGreatestTimes(startTT, endTT, starOccultationCoarseStepDays(options), star, observer, options.SafetyMarginArcsec) { + info, ok := starOccultationInfoAtGreatest(greatestTT, star, observer, options.SafetyMarginArcsec, resultLocation) + if !ok { + continue + } + if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 { + results = append(results, info) + if options.MaxEvents > 0 && len(results) >= options.MaxEvents { + break + } + } + } + + sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) }) + return results, nil +} + +// FindBestStarOccultations 返回窗口内每次恒星月掩在地球上的全球几何掩甚点。 +// 返回的 StarOccultationInfo.Observer 是海平面大地测量位置,由月掩几何选择,不使用地平线或可见性评分。 +// +// 地心数据只用于月周期搜索初值;最终点是与 FindStarOccultationPaths 一致的标准全球掩甚路径点,然后在该处重新进行站心接触和可见性计算。查询端点 10 ms 内的掩甚时刻也会包含,与数值根精度一致。 +// FindBestStarOccultations returns the global geometric greatest point on Earth for each stellar occultation in the window. +// The returned StarOccultationInfo.Observer is the geodetic location at sea level; it is selected from the occultation geometry, without a horizon or visibility score. +// Geocentric data only seeds each lunar-month search. The final point is the canonical global greatest-path point, matching FindStarOccultationPaths; event contacts and visibility are then recomputed topocentrically there. A greatest instant within 10 ms of either query endpoint is included, matching the numerical root precision. +func FindBestStarOccultations(start, end time.Time, star StarCoordinate, options OccultationSearchOptions) ([]StarOccultationInfo, error) { + if err := validateOccultationTimeRange(start, end); err != nil { + return nil, err + } + if err := star.Validate(); err != nil { + return nil, err + } + if err := options.Validate(); err != nil { + return nil, err + } + + startTT := occultationTimeToTT(start) + endTT := occultationTimeToTT(end) + selectionStartTT := startTT - occultationEventSelectionToleranceDays + selectionEndTT := endTT + occultationEventSelectionToleranceDays + candidateStartTT := startTT - occultationPathSearchSpanDays + candidateEndTT := endTT + occultationPathSearchSpanDays + resultLocation := start.Location() + results := make([]StarOccultationInfo, 0) + for _, seedTT := range starOccultationGeocentricCandidateGreatestTimes(candidateStartTT, candidateEndTT, starOccultationCoarseStepDays(options), star, options.SafetyMarginArcsec) { + greatestTT, observer, _, observerOK := starOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, star) + if !observerOK { + continue + } + info, ok := starOccultationInfoAtGreatest(greatestTT, star, observer, options.SafetyMarginArcsec, resultLocation) + if !ok { + continue + } + if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 { + results = append(results, info) + if options.MaxEvents > 0 && len(results) >= options.MaxEvents { + break + } + } + } + sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) }) + return results, nil +} + +func starOccultationCandidateGreatestTimes(startTT, endTT, step float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64) []float64 { + results := make([]float64, 0) + for cycleStart := startTT; cycleStart < endTT; cycleStart += starOccultationSiderealMonthDays { + cycleEnd := math.Min(cycleStart+starOccultationSiderealMonthDays, endTT) + scanStart := math.Max(startTT-step, cycleStart-step) + scanEnd := math.Min(endTT+step, cycleEnd+step) + if !starOccultationLatitudeEnvelopePass(scanStart, scanEnd, star, &observer, safetyMarginArcsec) { + continue + } + results = append(results, starOccultationScanCandidates( + scanStart, scanEnd, step, + func(tt float64) float64 { return starMoonSeparationArcsec(tt, star, observer) }, + func(tt float64) bool { + return starOccultationLatitudePass(tt, star, observer, safetyMarginArcsec) + }, + )...) + } + return uniqueOccultationCandidateTimes(results, startTT, endTT) +} + +func starOccultationGeocentricCandidateGreatestTimes(startTT, endTT, step float64, star StarCoordinate, safetyMarginArcsec float64) []float64 { + results := make([]float64, 0) + for cycleStart := startTT; cycleStart < endTT; cycleStart += starOccultationSiderealMonthDays { + cycleEnd := math.Min(cycleStart+starOccultationSiderealMonthDays, endTT) + scanStart := math.Max(startTT-step, cycleStart-step) + scanEnd := math.Min(endTT+step, cycleEnd+step) + if !starOccultationLatitudeEnvelopePass(scanStart, scanEnd, star, nil, safetyMarginArcsec) { + continue + } + results = append(results, starOccultationScanCandidates( + scanStart, scanEnd, step, + func(tt float64) float64 { return starOccultationGeocentricSeparationArcsec(tt, star) }, + func(tt float64) bool { + return starOccultationLatitudePassGeocentric(tt, star, safetyMarginArcsec) + }, + )...) + } + return uniqueOccultationCandidateTimes(results, startTT, endTT) +} + +// starOccultationScanCandidates 找出粗扫描中的所有局部最小值。 +// 恒星月仍适合作为黄纬预筛桶,但不能假定每个桶恰好只有一个最近接近。 +// starOccultationScanCandidates finds every local minimum in a coarse scan. +// A lunar month remains a useful latitude-prefilter bucket, but it must not be treated as a promise that exactly one closest approach exists in that bucket. +func starOccultationScanCandidates( + startTT, endTT, step float64, + value func(float64) float64, + accept func(float64) bool, +) []float64 { + if endTT < startTT { + return nil + } + if endTT == startTT { + if accept(startTT) && finite(value(startTT)) { + return []float64{startTT} + } + return nil + } + if step <= 0 || !finite(step) { + step = starOccultationDefaultStepDays + } + if step > (endTT-startTT)/2 { + step = (endTT - startTT) / 2 + } + appendCandidate := func(results *[]float64, tt float64) { + if tt < startTT || tt > endTT || !finite(tt) || !finite(value(tt)) || !accept(tt) { + return + } + if len(*results) == 0 || math.Abs(tt-(*results)[len(*results)-1]) > 60.0/86400.0 { + *results = append(*results, tt) + } + } + + results := make([]float64, 0, 2) + leftTT, leftValue := startTT, value(startTT) + centerTT := math.Min(startTT+step, endTT) + centerValue := value(centerTT) + for centerTT < endTT { + rightTT := math.Min(centerTT+step, endTT) + rightValue := value(rightTT) + if finite(leftValue) && finite(centerValue) && finite(rightValue) && + centerValue <= leftValue && centerValue <= rightValue { + candidate := starOccultationMinimizeValue(leftTT, rightTT, value) + appendCandidate(&results, candidate) + } + leftTT, leftValue = centerTT, centerValue + centerTT, centerValue = rightTT, rightValue + } + return results +} + +func uniqueOccultationCandidateTimes(times []float64, startTT, endTT float64) []float64 { + if len(times) == 0 { + return nil + } + sort.Float64s(times) + unique := times[:0] + for _, tt := range times { + if tt < startTT || tt > endTT { + continue + } + if len(unique) == 0 || math.Abs(tt-unique[len(unique)-1]) > 60.0/86400.0 { + unique = append(unique, tt) + } + } + return unique +} + +func starOccultationLatitudeEnvelopePass(startTT, endTT float64, star StarCoordinate, observer *Observer, safetyMarginArcsec float64) bool { + minimumLatitude := math.Inf(1) + maximumLatitude := math.Inf(-1) + maximumMoonRadiusDeg := 0.0 + for _, tt := range []float64{startTT, (startTT + endTT) / 2, endTT} { + var ra, dec float64 + if observer == nil { + ra, dec = starApparentRaDecGeocentric(tt, star) + } else { + ra, dec = starApparentRaDec(tt, star, *observer) + } + _, latitude := RaDecToLoBo(tt, ra, dec) + minimumLatitude = math.Min(minimumLatitude, latitude) + maximumLatitude = math.Max(maximumLatitude, latitude) + moonRadius := MoonSemidiameter(tt) + if observer != nil { + moonRadius = moonTopocentricSemidiameterN(tt, *observer, -1) + } + maximumMoonRadiusDeg = math.Max(maximumMoonRadiusDeg, moonRadius/3600) + } + limit := starOccultationMoonLatitudeDeg + starOccultationLatitudeMarginAS/3600 + safetyMarginArcsec/3600 + maximumMoonRadiusDeg + return minimumLatitude <= limit && maximumLatitude >= -limit +} + +func starOccultationGeocentricLongitudeCandidate(startTT, endTT, step float64, star StarCoordinate) float64 { + bestTT := math.NaN() + bestDelta := math.Inf(1) + for tt := startTT; tt <= endTT; tt += step { + delta := math.Abs(signedAngleDifference(HMoonTrueLoN(tt, 8), starOccultationGeocentricStarLongitude(tt, star))) + if delta < bestDelta { + bestDelta = delta + bestTT = tt + } + } + if endTT > startTT { + delta := math.Abs(signedAngleDifference(HMoonTrueLoN(endTT, 8), starOccultationGeocentricStarLongitude(endTT, star))) + if delta < bestDelta { + bestTT = endTT + } + } + return bestTT +} + +func starOccultationGeocentricStarLongitude(tt float64, star StarCoordinate) float64 { + ra, dec := starApparentRaDecGeocentric(tt, star) + longitude, _ := RaDecToLoBo(tt, ra, dec) + return longitude +} + +func starOccultationMinimizeGeocentricSeparation(seed, startTT, endTT float64, star StarCoordinate) float64 { + halfWindow := 0.75 + left := math.Max(startTT, seed-halfWindow) + right := math.Min(endTT, seed+halfWindow) + return starOccultationMinimizeValue(left, right, func(tt float64) float64 { + return starOccultationGeocentricSeparationArcsec(tt, star) + }) +} + +func starOccultationMinimizeValue(left, right float64, value func(float64) float64) float64 { + if right <= left { + return left + } + const goldenRatio = 0.6180339887498949 + x1 := right - goldenRatio*(right-left) + x2 := left + goldenRatio*(right-left) + f1 := value(x1) + f2 := value(x2) + for i := 0; i < 64 && right-left > starOccultationRootToleranceDays; i++ { + if f1 > f2 { + left = x1 + x1, f1 = x2, f2 + x2 = left + goldenRatio*(right-left) + f2 = value(x2) + } else { + right = x2 + x2, f2 = x1, f1 + x1 = right - goldenRatio*(right-left) + f1 = value(x1) + } + } + return (left + right) / 2 +} + +func starOccultationGeocentricSeparationArcsec(tt float64, star StarCoordinate) float64 { + moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1) + starRA, starDec := starApparentRaDecGeocentric(tt, star) + return angularSeparationDegrees(moonRA, moonDec, starRA, starDec) * 3600 +} + +func starOccultationBestObserver(seedTT, startTT, endTT float64, star StarCoordinate) (float64, Observer, float64, bool) { + searchStart := seedTT - occultationPathSearchSpanDays + searchEnd := seedTT + occultationPathSearchSpanDays + outerStart, outerEnd, ok := starOccultationPathWindow(seedTT, searchStart, searchEnd, star, false) + if !ok { + return 0, Observer{}, 0, false + } + greatestTT := starOccultationPathGreatest(seedTT, outerStart, outerEnd, star) + if greatestTT < startTT || greatestTT > endTT { + return 0, Observer{}, 0, false + } + point, pointOK := starOccultationPathCenterPoint(greatestTT, star, time.UTC) + if !pointOK { + frameAt := func(tt float64) (occultationPathFrame, bool) { + return starOccultationPathFrameAt(tt, star) + } + point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC) + } + if !pointOK { + return 0, Observer{}, 0, false + } + observer := Observer{Longitude: point.Longitude, Latitude: point.Latitude} + position := starMoonPositionAt(greatestTT, star, observer) + moonRadius := moonTopocentricSemidiameterN(greatestTT, observer, -1) + if !position.valid || !finite(moonRadius) { + return 0, Observer{}, 0, false + } + metric := angularSeparationDegrees(position.moonRA, position.moonDec, position.starRA, position.starDec)*3600 - moonRadius + return greatestTT, observer, metric, finite(metric) +} + +func normalizeLongitude180(longitude float64) float64 { + longitude = math.Mod(longitude+180, 360) + if longitude < 0 { + longitude += 360 + } + return longitude - 180 +} + +func starOccultationInfoAtGreatest(greatestTT float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64, location *time.Location) (StarOccultationInfo, bool) { + if !starOccultationLatitudePass(greatestTT, star, observer, safetyMarginArcsec) { + return StarOccultationInfo{}, false + } + minimumSeparation := starMoonSeparationArcsec(greatestTT, star, observer) + moonRadius := moonTopocentricSemidiameterN(greatestTT, observer, -1) + if !finite(minimumSeparation) || !finite(moonRadius) || minimumSeparation > moonRadius { + return StarOccultationInfo{}, false + } + greatestPosition := starMoonPositionAt(greatestTT, star, observer) + if !greatestPosition.valid { + return StarOccultationInfo{}, false + } + info := StarOccultationInfo{ + TargetID: star.ID, + Observer: observer, + Type: OccultationTotal, + Greatest: occultationTTToLocation(greatestTT, location), + MinimumSeparationArcsec: minimumSeparation, + PositionAngleDeg: occultationPositionAngle(greatestPosition.moonRA, greatestPosition.moonDec, greatestPosition.starRA, greatestPosition.starDec), + MoonSemidiameterArcsec: moonRadius, + MoonAltitudeAtGreatest: occultationAltitude(greatestTT, observer, greatestPosition.moonRA, greatestPosition.moonDec), + MoonAzimuthAtGreatest: occultationAzimuth(greatestTT, observer, greatestPosition.moonRA, greatestPosition.moonDec), + } + info.VisibleAtGreatest = info.MoonAltitudeAtGreatest >= 0 + + minimumResidual := minimumSeparation - moonRadius + if math.Abs(minimumResidual) <= starOccultationGrazingTolerance { + info.Type = OccultationGrazing + info.Immersion = info.Greatest + info.Emersion = info.Greatest + info.ContactsComplete = true + return info, true + } + immersionTT, immersionOK := starOccultationContact(greatestTT, greatestTT-starOccultationContactSpanDays, -1, star, observer) + emersionTT, emersionOK := starOccultationContact(greatestTT, greatestTT+starOccultationContactSpanDays, 1, star, observer) + if !immersionOK || !emersionOK { + return StarOccultationInfo{}, false + } + info.Immersion = occultationTTToLocation(immersionTT, location) + info.Emersion = occultationTTToLocation(emersionTT, location) + info.ContactsComplete = true + return info, true +} + +type starMoonPosition struct { + moonRA, moonDec float64 + starRA, starDec float64 + valid bool +} + +func starMoonPositionAt(tt float64, star StarCoordinate, observer Observer) starMoonPosition { + moonRA, moonDec := moonTopocentricApparentRaDec(tt, observer, -1) + starRA, starDec := starApparentRaDec(tt, star, observer) + return starMoonPosition{ + moonRA: moonRA, + moonDec: moonDec, + starRA: starRA, + starDec: starDec, + valid: finite(moonRA) && finite(moonDec) && finite(starRA) && finite(starDec), + } +} + +func moonTopocentricApparentRaDec(tt float64, observer Observer, n int) (float64, float64) { + ra, dec := HMoonGeocentricApparentRaDecN(tt, n) + ut := TD2UT(tt, false) + distanceAU := HMoonAwayN(tt, n) / 149597870.7 + ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distanceAU, observer.Height) + return normalizeRA(ra), dec +} + +func starApparentRaDec(tt float64, star StarCoordinate, observer Observer) (float64, float64) { + ra, dec := starApparentRaDecGeocentric(tt, star) + if star.ParallaxMas > 0 { + // 1 秒差距处 1 角秒对应 206264.806 AU。 + // One arcsecond at 1 pc corresponds to 206264.806 AU. + distanceAU := 206264806.247 / star.ParallaxMas + ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, TD2UT(tt, false), distanceAU, observer.Height) + ra = normalizeRA(ra) + } + return ra, dec +} + +func starApparentRaDecGeocentric(tt float64, star StarCoordinate) (float64, float64) { + epochJD := occultationTimeToTT(star.Epoch) + years := (tt - epochJD) / 365.25 + ra := star.RA + dec := star.Dec + precessionEpoch := 2451545.0 + if star.Frame == CoordinateFrameICRS { + ra, dec = starICRSToMeanJ2000RaDec(ra, dec) + } else if star.Frame == CoordinateFrameApparentOfDate { + ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, star.ParallaxMas) + precessionEpoch = epochJD + } + cosDec := math.Cos(dec * math.Pi / 180) + if math.Abs(cosDec) > 1e-12 { + ra += years * star.ProperMotionRACosDecMasPerYear / (3600000.0 * cosDec) + } + dec += years * star.ProperMotionDecMasPerYear / 3600000.0 + dec = math.Max(-90, math.Min(90, dec)) + + ra, dec = Precess(ra, dec, precessionEpoch, tt) + return starMeanToApparentRaDec(tt, ra, dec, star.ParallaxMas) +} + +func starICRSToMeanJ2000RaDec(ra, dec float64) (float64, float64) { + // IAU SOFA 框架偏差矩阵,将 GCRS/ICRS 向量转换为 J2000.0 平均赤道和春分点。 + // IAU SOFA frame-bias matrix, transforming a GCRS/ICRS vector to the mean equator and equinox of J2000.0. + const ( + b00 = 0.9999999999999942 + b01 = -0.7078279744199197e-7 + b02 = 0.8056217146976134e-7 + b10 = 0.7078279477857337e-7 + b11 = 0.9999999999999969 + b12 = 0.3306041454222148e-7 + b20 = -0.8056217380986972e-7 + b21 = -0.3306040883980553e-7 + b22 = 0.9999999999999962 + ) + raRad := ra * math.Pi / 180 + decRad := dec * math.Pi / 180 + x := math.Cos(decRad) * math.Cos(raRad) + y := math.Cos(decRad) * math.Sin(raRad) + z := math.Sin(decRad) + biasedX := b00*x + b01*y + b02*z + biasedY := b10*x + b11*y + b12*z + biasedZ := b20*x + b21*y + b22*z + return normalizeRA(math.Atan2(biasedY, biasedX) * 180 / math.Pi), + math.Atan2(biasedZ, math.Hypot(biasedX, biasedY)) * 180 / math.Pi +} + +func starMeanToApparentRaDec(tt, ra, dec, parallaxMas float64) (float64, float64) { + longitude, latitude := starMeanEquatorialToEcliptic(tt, ra, dec) + if parallaxMas > 0 { + longitude, latitude = starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas) + } + meanLongitude, meanLatitude := longitude, latitude + longitude = normalizeRA(meanLongitude + GXCLo(meanLongitude, meanLatitude, tt)/3600 + Nutation2000Bi(tt)) + latitude = meanLatitude + GXCBo(meanLongitude, meanLatitude, tt)/3600 + ra, dec = LoBoToRaDec(tt, longitude, latitude) + return normalizeRA(ra), dec +} + +func starApparentToMeanRaDec(tt, apparentRA, apparentDec, parallaxMas float64) (float64, float64) { + meanRA, meanDec := apparentRA, apparentDec + for i := 0; i < 8; i++ { + computedRA, computedDec := starMeanToApparentRaDec(tt, meanRA, meanDec, parallaxMas) + meanRA = normalizeRA(meanRA - signedAngleDifference(computedRA, apparentRA)) + meanDec -= computedDec - apparentDec + } + return meanRA, math.Max(-90, math.Min(90, meanDec)) +} + +func starMeanEquatorialToEcliptic(tt, ra, dec float64) (float64, float64) { + obliquity := EclipticObliquity(tt, false) * math.Pi / 180 + ra *= math.Pi / 180 + dec *= math.Pi / 180 + longitude := math.Atan2( + math.Sin(ra)*math.Cos(obliquity)+math.Tan(dec)*math.Sin(obliquity), + math.Cos(ra), + ) * 180 / math.Pi + latitude := math.Asin( + math.Sin(dec)*math.Cos(obliquity)-math.Cos(dec)*math.Sin(obliquity)*math.Sin(ra), + ) * 180 / math.Pi + return normalizeRA(longitude), latitude +} + +func starAnnualParallaxEcliptic(tt, longitude, latitude, parallaxMas float64) (float64, float64) { + distanceAU := 206264806.247 / parallaxMas + longitudeRad := longitude * math.Pi / 180 + latitudeRad := latitude * math.Pi / 180 + cosLatitude := math.Cos(latitudeRad) + starX := distanceAU * cosLatitude * math.Cos(longitudeRad) + starY := distanceAU * cosLatitude * math.Sin(longitudeRad) + starZ := distanceAU * math.Sin(latitudeRad) + + earthLongitude := normalizeRA(HSunTrueLoN(tt, -1)+180) * math.Pi / 180 + earthDistance := EarthAwayN(tt, -1) + starX -= earthDistance * math.Cos(earthLongitude) + starY -= earthDistance * math.Sin(earthLongitude) + + longitude = math.Atan2(starY, starX) * 180 / math.Pi + latitude = math.Atan2(starZ, math.Hypot(starX, starY)) * 180 / math.Pi + return normalizeRA(longitude), latitude +} + +func starOccultationLongitudeCandidate(startTT, endTT, step float64, star StarCoordinate, observer Observer) float64 { + bestTT := math.NaN() + bestDelta := math.Inf(1) + for tt := startTT; tt <= endTT; tt += step { + delta := starOccultationLongitudeDistance(tt, star, observer) + if delta < bestDelta { + bestDelta = delta + bestTT = tt + } + } + if endTT > startTT { + delta := starOccultationLongitudeDistance(endTT, star, observer) + if delta < bestDelta { + bestTT = endTT + } + } + return bestTT +} + +func starOccultationLongitudeDistance(tt float64, star StarCoordinate, observer Observer) float64 { + moonLongitude := HMoonTrueLoN(tt, 8) + starRA, starDec := starApparentRaDec(tt, star, observer) + starLongitude, _ := RaDecToLoBo(tt, starRA, starDec) + return math.Abs(signedAngleDifference(moonLongitude, starLongitude)) +} + +func starOccultationMinimizeSeparation(seed, startTT, endTT float64, star StarCoordinate, observer Observer) float64 { + halfWindow := 0.75 + left := math.Max(startTT, seed-halfWindow) + right := math.Min(endTT, seed+halfWindow) + return starOccultationMinimizeValue(left, right, func(tt float64) float64 { + return starMoonSeparationArcsec(tt, star, observer) + }) +} + +func starMoonSeparationArcsec(tt float64, star StarCoordinate, observer Observer) float64 { + position := starMoonPositionAt(tt, star, observer) + if !position.valid { + return math.Inf(1) + } + return angularSeparationDegrees(position.moonRA, position.moonDec, position.starRA, position.starDec) * 3600 +} + +func starOccultationLatitudePass(tt float64, star StarCoordinate, observer Observer, safetyMarginArcsec float64) bool { + starRA, starDec := starApparentRaDec(tt, star, observer) + _, starLatitude := RaDecToLoBo(tt, starRA, starDec) + moonLatitude := HMoonTrueBoN(tt, 8) + moonRadius := moonTopocentricSemidiameterN(tt, observer, -1) + limit := moonRadius + starOccultationLatitudeMarginAS + safetyMarginArcsec + return math.Abs(starLatitude-moonLatitude)*3600 <= limit +} + +func starOccultationLatitudePassGeocentric(tt float64, star StarCoordinate, safetyMarginArcsec float64) bool { + starRA, starDec := starApparentRaDecGeocentric(tt, star) + _, starLatitude := RaDecToLoBo(tt, starRA, starDec) + moonLatitude := HMoonTrueBoN(tt, 8) + limit := MoonSemidiameter(tt) + starOccultationLatitudeMarginAS + safetyMarginArcsec + return math.Abs(starLatitude-moonLatitude)*3600 <= limit +} + +func starOccultationContact(greatestTT, boundaryTT float64, direction int, star StarCoordinate, observer Observer) (float64, bool) { + valueAtGreatest := starMoonSeparationArcsec(greatestTT, star, observer) - moonTopocentricSemidiameterN(greatestTT, observer, -1) + if !finite(valueAtGreatest) || valueAtGreatest > 0 { + return math.NaN(), false + } + currentTT := greatestTT + currentValue := valueAtGreatest + step := starOccultationContactStepDays + for i := 0; i < starOccultationMaxContactSteps; i++ { + nextTT := currentTT + float64(direction)*step + if direction < 0 && nextTT < boundaryTT { + nextTT = boundaryTT + } + if direction > 0 && nextTT > boundaryTT { + nextTT = boundaryTT + } + nextValue := starMoonSeparationArcsec(nextTT, star, observer) - moonTopocentricSemidiameterN(nextTT, observer, -1) + if finite(nextValue) && nextValue >= 0 { + return starOccultationRoot(currentTT, nextTT, currentValue, nextValue, star, observer) + } + if nextTT == boundaryTT { + return math.NaN(), false + } + currentTT = nextTT + currentValue = nextValue + } + return math.NaN(), false +} + +func starOccultationRoot(leftTT, rightTT, leftValue, rightValue float64, star StarCoordinate, observer Observer) (float64, bool) { + if !finite(leftValue) || !finite(rightValue) || leftValue*rightValue > 0 { + return math.NaN(), false + } + if leftValue == 0 { + return leftTT, true + } + if rightValue == 0 { + return rightTT, true + } + for i := 0; i < 64 && math.Abs(rightTT-leftTT) > starOccultationRootToleranceDays; i++ { + midTT := (leftTT + rightTT) / 2 + midValue := starMoonSeparationArcsec(midTT, star, observer) - moonTopocentricSemidiameterN(midTT, observer, -1) + if !finite(midValue) { + return math.NaN(), false + } + if leftValue*midValue <= 0 { + rightTT, rightValue = midTT, midValue + } else { + leftTT, leftValue = midTT, midValue + } + } + return (leftTT + rightTT) / 2, true +} + +func starOccultationCoarseStepDays(options OccultationSearchOptions) float64 { + step := starOccultationDefaultStepDays + if options.MaxStep > 0 { + requested := options.MaxStep.Hours() / 24 + if requested > 0 && requested < step { + step = requested + } + } + return math.Max(step, occultationSearchMinimumStep.Hours()/24) +} + +func angularSeparationDegrees(ra1, dec1, ra2, dec2 float64) float64 { + ra1 *= math.Pi / 180 + ra2 *= math.Pi / 180 + dec1 *= math.Pi / 180 + dec2 *= math.Pi / 180 + cosSeparation := math.Sin(dec1)*math.Sin(dec2) + math.Cos(dec1)*math.Cos(dec2)*math.Cos(ra1-ra2) + return math.Acos(math.Max(-1, math.Min(1, cosSeparation))) * 180 / math.Pi +} + +func occultationPositionAngle(moonRA, moonDec, starRA, starDec float64) float64 { + deltaRA := (starRA - moonRA) * math.Pi / 180 + moonDecRad := moonDec * math.Pi / 180 + starDecRad := starDec * math.Pi / 180 + y := math.Sin(deltaRA) * math.Cos(starDecRad) + x := math.Cos(moonDecRad)*math.Sin(starDecRad) - math.Sin(moonDecRad)*math.Cos(starDecRad)*math.Cos(deltaRA) + return normalizeRA(math.Atan2(y, x) * 180 / math.Pi) +} + +func occultationAltitude(tt float64, observer Observer, ra, dec float64) float64 { + hourAngle := signedAngleDifference(ApparentSiderealTime(TD2UT(tt, false))*15+observer.Longitude, ra) * math.Pi / 180 + lat := observer.Latitude * math.Pi / 180 + declination := dec * math.Pi / 180 + sinAltitude := math.Sin(lat)*math.Sin(declination) + math.Cos(lat)*math.Cos(declination)*math.Cos(hourAngle) + return math.Asin(math.Max(-1, math.Min(1, sinAltitude))) * 180 / math.Pi +} + +func occultationAzimuth(tt float64, observer Observer, ra, dec float64) float64 { + hourAngle := signedAngleDifference(ApparentSiderealTime(TD2UT(tt, false))*15+observer.Longitude, ra) * math.Pi / 180 + lat := observer.Latitude * math.Pi / 180 + declination := dec * math.Pi / 180 + y := math.Sin(hourAngle) + x := math.Cos(hourAngle)*math.Sin(lat) - math.Tan(declination)*math.Cos(lat) + return normalizeRA(math.Atan2(y, x)*180/math.Pi + 180) +} + +func signedAngleDifference(a, b float64) float64 { + difference := math.Mod(a-b+180, 360) + if difference < 0 { + difference += 360 + } + return difference - 180 +} + +func normalizeRA(ra float64) float64 { + ra = math.Mod(ra, 360) + if ra < 0 { + ra += 360 + } + return ra +} + +func occultationTimeToTT(value time.Time) float64 { + return TD2UT(Date2JDE(value.UTC()), true) +} + +func occultationTTToLocation(tt float64, location *time.Location) time.Time { + if location == nil { + location = time.UTC + } + return JDE2DateByZone(TD2UT(tt, false), location, false) +} diff --git a/basic/occultation_star_diagram.go b/basic/occultation_star_diagram.go new file mode 100644 index 0000000..eb8b028 --- /dev/null +++ b/basic/occultation_star_diagram.go @@ -0,0 +1,247 @@ +package basic + +import ( + "math" + "sort" +) + +const ( + starOccultationDiagramDefaultStepDays = 2.0 / 1440.0 + starOccultationDiagramMinStepDays = 1.0 / 86400.0 + starOccultationDiagramMaxSamples = 2000 + starOccultationDiagramDuplicateDays = 1e-10 + starOccultationDiagramGeometryArcsec = 0.05 + starOccultationDiagramPositionDeg = 0.01 +) + +// StarOccultationDiagramOptions 控制本地恒星月掩图的轨迹采样。 +// StarOccultationDiagramOptions controls local stellar-occultation diagram sampling. +type StarOccultationDiagramOptions struct { + // StepDays 是请求的轨迹采样步长,单位为日;非正值或非有限值使用两分钟,正值小于一秒时使用一秒。长事件可能增大实际步长,使基础轨迹不超过 2000 个采样点;必要阶段帧仍会额外保留。结果会报告实际采用的值。 + // StepDays is the requested track sampling step in days. Non-positive or non-finite values use two minutes, and positive values below one second use one second. Long events may increase the effective step to keep the base track within 2000 samples; required phase frames are retained in addition. The result reports the effective value. + StepDays float64 +} + +// StarOccultationDiagramFrame 描述一个时刻的站心月球与恒星几何。 +// StarOccultationDiagramFrame describes topocentric Moon-star geometry at one instant. +type StarOccultationDiagramFrame struct { + // JDE 是 TT 儒略历书日。 + // JDE is the TT Julian ephemeris day. + JDE float64 + // StarXArcsec 和 StarYArcsec 是相对月心的切平面偏移,单位为角秒。X 向东为正,Y 向北为正。 + // StarXArcsec and StarYArcsec are tangent-plane offsets from the lunar center. X is positive east and Y is positive north. + StarXArcsec float64 + StarYArcsec float64 + // MoonRadiusArcsec 是站心月球视半径,单位为角秒。 + // MoonRadiusArcsec is the topocentric apparent lunar semidiameter. + MoonRadiusArcsec float64 + // SeparationArcsec 和 PositionAngleDeg 描述恒星相对月心的位置。 + // SeparationArcsec and PositionAngleDeg describe the star relative to the lunar center. + SeparationArcsec float64 + PositionAngleDeg float64 + // MoonAltitudeDeg 和 MoonAzimuthDeg 是站心地平坐标。 + // MoonAltitudeDeg and MoonAzimuthDeg are topocentric horizontal coordinates. + MoonAltitudeDeg float64 + MoonAzimuthDeg float64 + // BehindMoon 表示点光源恒星位于月缘内侧。 + // BehindMoon is true while the point-source star lies strictly inside the lunar limb. + BehindMoon bool + // Label 是主阶段标识;Labels 在掠掩事件中保留重合阶段。 + // Label is the primary key phase; Labels retains coincident phases for grazing events. + Label string + Labels []string +} + +// StarOccultationDiagramResult 包含固定地点恒星月掩的几何数据。 +// StarOccultationDiagramResult contains geometry for a fixed-site stellar occultation. +type StarOccultationDiagramResult struct { + Occultation StarOccultationInfo + Frames []StarOccultationDiagramFrame + // StepDays 是实际采用的基础轨迹采样步长,单位为日。 + // StepDays is the effective base-track sampling step in days. + StepDays float64 +} + +type starOccultationDiagramTime struct { + jde float64 + labels []string +} + +// StarOccultationDiagram 为已求解的固定地点恒星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。 +// StarOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site stellar occultation. Invalid or incomplete event data produces a result without frames. +func StarOccultationDiagram( + info StarOccultationInfo, + star StarCoordinate, + options StarOccultationDiagramOptions, +) StarOccultationDiagramResult { + options = normalizeStarOccultationDiagramOptions(options) + result := StarOccultationDiagramResult{Occultation: info, StepDays: options.StepDays} + if star.Validate() != nil || info.Observer.Validate() != nil || + !info.ContactsComplete || info.Immersion.IsZero() || info.Greatest.IsZero() || info.Emersion.IsZero() || + info.Greatest.Before(info.Immersion) || info.Emersion.Before(info.Greatest) || + (info.Type != OccultationTotal && info.Type != OccultationGrazing) { + return result + } + + startTT := occultationTimeToTT(info.Immersion) + greatestTT := occultationTimeToTT(info.Greatest) + endTT := occultationTimeToTT(info.Emersion) + immersionFrame, immersionOK := starOccultationDiagramFrameAt(startTT, star, info.Observer) + greatestFrame, greatestOK := starOccultationDiagramFrameAt(greatestTT, star, info.Observer) + emersionFrame, emersionOK := starOccultationDiagramFrameAt(endTT, star, info.Observer) + if !immersionOK || !greatestOK || !emersionOK || + !starOccultationDiagramMatchesInfo(info, immersionFrame, greatestFrame, emersionFrame) { + return result + } + times, stepDays := starOccultationDiagramTimes(startTT, greatestTT, endTT, options.StepDays) + result.StepDays = stepDays + result.Frames = make([]StarOccultationDiagramFrame, 0, len(times)) + for _, item := range times { + frame, ok := starOccultationDiagramFrameAt(item.jde, star, info.Observer) + if !ok { + return StarOccultationDiagramResult{Occultation: info, StepDays: stepDays} + } + frame.Labels = append([]string(nil), item.labels...) + frame.Label = starOccultationDiagramPrimaryLabel(item.labels) + result.Frames = append(result.Frames, frame) + } + return result +} + +func starOccultationDiagramMatchesInfo( + info StarOccultationInfo, + immersion, greatest, emersion StarOccultationDiagramFrame, +) bool { + if !finite(info.MinimumSeparationArcsec) || !finite(info.MoonSemidiameterArcsec) || + !finite(info.PositionAngleDeg) { + return false + } + if math.Abs(immersion.SeparationArcsec-immersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec || + math.Abs(emersion.SeparationArcsec-emersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec { + return false + } + return math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) <= starOccultationDiagramGeometryArcsec && + math.Abs(greatest.MoonRadiusArcsec-info.MoonSemidiameterArcsec) <= starOccultationDiagramGeometryArcsec && + math.Abs(signedAngleDifference(greatest.PositionAngleDeg, info.PositionAngleDeg)) <= starOccultationDiagramPositionDeg +} + +func normalizeStarOccultationDiagramOptions(options StarOccultationDiagramOptions) StarOccultationDiagramOptions { + if options.StepDays <= 0 || !finite(options.StepDays) { + options.StepDays = starOccultationDiagramDefaultStepDays + } + if options.StepDays < starOccultationDiagramMinStepDays { + options.StepDays = starOccultationDiagramMinStepDays + } + return options +} + +func starOccultationDiagramTimes(startTT, greatestTT, endTT, stepDays float64) ([]starOccultationDiagramTime, float64) { + if !finite(startTT) || !finite(greatestTT) || !finite(endTT) || greatestTT < startTT || endTT < greatestTT { + return nil, stepDays + } + if endTT > startTT { + if sampleCount := int(math.Ceil((endTT-startTT)/stepDays)) + 1; sampleCount > starOccultationDiagramMaxSamples { + stepDays = (endTT - startTT) / float64(starOccultationDiagramMaxSamples-1) + } + } + times := []starOccultationDiagramTime{ + {jde: startTT, labels: []string{"Immersion"}}, + {jde: greatestTT, labels: []string{"Greatest"}}, + {jde: endTT, labels: []string{"Emersion"}}, + } + for jde := startTT + stepDays; jde < endTT; jde += stepDays { + times = append(times, starOccultationDiagramTime{jde: jde}) + } + sort.SliceStable(times, func(i, j int) bool { + if times[i].jde == times[j].jde { + return starOccultationDiagramLabelPriority(times[i].labels) < starOccultationDiagramLabelPriority(times[j].labels) + } + return times[i].jde < times[j].jde + }) + return uniqueStarOccultationDiagramTimes(times), stepDays +} + +func uniqueStarOccultationDiagramTimes(times []starOccultationDiagramTime) []starOccultationDiagramTime { + unique := times[:0] + for _, item := range times { + if !finite(item.jde) { + continue + } + if len(unique) == 0 || math.Abs(item.jde-unique[len(unique)-1].jde) > starOccultationDiagramDuplicateDays { + item.labels = append([]string(nil), item.labels...) + unique = append(unique, item) + continue + } + unique[len(unique)-1].labels = mergeStarOccultationDiagramLabels(unique[len(unique)-1].labels, item.labels) + } + return unique +} + +func mergeStarOccultationDiagramLabels(existing, incoming []string) []string { + for _, label := range incoming { + found := false + for _, current := range existing { + if current == label { + found = true + break + } + } + if !found { + existing = append(existing, label) + } + } + return existing +} + +func starOccultationDiagramPrimaryLabel(labels []string) string { + for _, label := range labels { + if label == "Greatest" { + return label + } + } + if len(labels) == 0 { + return "" + } + return labels[0] +} + +func starOccultationDiagramLabelPriority(labels []string) int { + if len(labels) == 0 { + return 99 + } + switch labels[0] { + case "Immersion": + return 0 + case "Greatest": + return 1 + case "Emersion": + return 2 + default: + return 99 + } +} + +func starOccultationDiagramFrameAt(tt float64, star StarCoordinate, observer Observer) (StarOccultationDiagramFrame, bool) { + position := starMoonPositionAt(tt, star, observer) + moonRadius := moonTopocentricSemidiameterN(tt, observer, -1) + if !position.valid || !finite(moonRadius) || moonRadius <= 0 { + return StarOccultationDiagramFrame{}, false + } + separation := angularSeparationDegrees(position.moonRA, position.moonDec, position.starRA, position.starDec) * 3600 + positionAngle := occultationPositionAngle(position.moonRA, position.moonDec, position.starRA, position.starDec) + if !finite(separation) || !finite(positionAngle) { + return StarOccultationDiagramFrame{}, false + } + angle := positionAngle * math.Pi / 180 + return StarOccultationDiagramFrame{ + JDE: tt, + StarXArcsec: separation * math.Sin(angle), + StarYArcsec: separation * math.Cos(angle), + MoonRadiusArcsec: moonRadius, + SeparationArcsec: separation, + PositionAngleDeg: positionAngle, + MoonAltitudeDeg: occultationAltitude(tt, observer, position.moonRA, position.moonDec), + MoonAzimuthDeg: occultationAzimuth(tt, observer, position.moonRA, position.moonDec), + BehindMoon: separation < moonRadius-starOccultationGrazingTolerance, + }, true +} diff --git a/basic/occultation_star_diagram_test.go b/basic/occultation_star_diagram_test.go new file mode 100644 index 0000000..b851737 --- /dev/null +++ b/basic/occultation_star_diagram_test.go @@ -0,0 +1,86 @@ +package basic + +import ( + "math" + "testing" + "time" +) + +func TestStarOccultationDiagramUsesSolvedLocalContacts(t *testing.T) { + location := time.FixedZone("CST", 8*3600) + star := StarCoordinate{ + ID: "HR 4799", + RA: 189.1975, + Dec: -5.831944444444, + Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), + Frame: CoordinateFrameJ2000, + ProperMotionRACosDecMasPerYear: -28, + ProperMotionDecMasPerYear: -18, + } + events, err := FindStarOccultations( + time.Date(2025, 6, 5, 0, 0, 0, 0, location), + time.Date(2025, 6, 6, 0, 0, 0, 0, location), + star, 121.56601, 6.80706, 0, OccultationSearchOptions{}, + ) + if err != nil { + t.Fatalf("FindStarOccultations() error = %v", err) + } + if len(events) != 1 { + t.Fatalf("FindStarOccultations() returned %d events, want 1", len(events)) + } + + diagram := StarOccultationDiagram(events[0], star, StarOccultationDiagramOptions{StepDays: 2.0 / 1440}) + if len(diagram.Frames) < 3 { + t.Fatalf("StarOccultationDiagram() frame count = %d, want at least 3", len(diagram.Frames)) + } + if !starOccultationDiagramFrameHasLabel(diagram.Frames[0], "Immersion") { + t.Fatalf("first frame labels = %v, want Immersion", diagram.Frames[0].Labels) + } + last := diagram.Frames[len(diagram.Frames)-1] + if !starOccultationDiagramFrameHasLabel(last, "Emersion") { + t.Fatalf("last frame labels = %v, want Emersion", last.Labels) + } + if residual := math.Abs(diagram.Frames[0].SeparationArcsec - diagram.Frames[0].MoonRadiusArcsec); residual > 0.1 { + t.Fatalf("immersion limb residual = %.6f arcsec, want <= 0.1", residual) + } + if residual := math.Abs(last.SeparationArcsec - last.MoonRadiusArcsec); residual > 0.1 { + t.Fatalf("emersion limb residual = %.6f arcsec, want <= 0.1", residual) + } + greatest, ok := starOccultationDiagramFrameByLabel(diagram.Frames, "Greatest") + if !ok { + t.Fatalf("diagram does not contain Greatest frame") + } + if !greatest.BehindMoon || greatest.SeparationArcsec >= greatest.MoonRadiusArcsec { + t.Fatalf("greatest frame is not behind Moon: separation=%.6f radius=%.6f", greatest.SeparationArcsec, greatest.MoonRadiusArcsec) + } + + wrongStar := star + wrongStar.RA += 30 + if frames := StarOccultationDiagram(events[0], wrongStar, StarOccultationDiagramOptions{}).Frames; len(frames) != 0 { + t.Fatalf("same-ID wrong coordinate produced %d frames, want none", len(frames)) + } + infoWithoutID := events[0] + infoWithoutID.TargetID = "" + wrongStar.ID = "" + if frames := StarOccultationDiagram(infoWithoutID, wrongStar, StarOccultationDiagramOptions{}).Frames; len(frames) != 0 { + t.Fatalf("empty-ID wrong coordinate produced %d frames, want none", len(frames)) + } +} + +func starOccultationDiagramFrameHasLabel(frame StarOccultationDiagramFrame, label string) bool { + for _, current := range frame.Labels { + if current == label { + return true + } + } + return false +} + +func starOccultationDiagramFrameByLabel(frames []StarOccultationDiagramFrame, label string) (StarOccultationDiagramFrame, bool) { + for _, frame := range frames { + if starOccultationDiagramFrameHasLabel(frame, label) { + return frame, true + } + } + return StarOccultationDiagramFrame{}, false +} diff --git a/basic/occultation_star_internal_test.go b/basic/occultation_star_internal_test.go new file mode 100644 index 0000000..36a04ac --- /dev/null +++ b/basic/occultation_star_internal_test.go @@ -0,0 +1,196 @@ +package basic + +import ( + "math" + "testing" + "time" +) + +func TestStarOccultationLatitudeEnvelopePrefilter(t *testing.T) { + start := occultationTimeToTT(time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)) + end := start + starOccultationSiderealMonthDays + polar := StarCoordinate{RA: 0, Dec: 89, Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameICRS} + if starOccultationLatitudeEnvelopePass(start, end, polar, nil, 0) { + t.Fatal("polar star should be rejected by global ecliptic-latitude envelope") + } + if !starOccultationLatitudeEnvelopePass(start, end, hr4799OccultationCoordinateForTest(), nil, 0) { + t.Fatal("HR 4799 should pass the global ecliptic-latitude envelope") + } +} + +func TestStarOccultationScanCandidatesFindsMultipleLocalMinima(t *testing.T) { + value := func(tt float64) float64 { + return (tt-1)*(tt-1)*(tt-3)*(tt-3) + 0.001 + } + got := starOccultationScanCandidates(0, 4, 0.5, value, func(float64) bool { return true }) + if len(got) != 2 { + t.Fatalf("candidate count = %d, want 2: %v", len(got), got) + } + if math.Abs(got[0]-1) > 1e-6 || math.Abs(got[1]-3) > 1e-6 { + t.Fatalf("candidate minima = %v, want [1 3]", got) + } +} + +func TestStarOccultationScanCandidatesFindsNarrowWindowMidpoint(t *testing.T) { + const ( + start = 10.0 + end = 10.1 + want = (start + end) / 2 + ) + value := func(tt float64) float64 { return (tt - want) * (tt - want) } + got := starOccultationScanCandidates(start, end, starOccultationDefaultStepDays, value, func(float64) bool { return true }) + if len(got) != 1 { + t.Fatalf("candidate count = %d, want 1: %v", len(got), got) + } + if math.Abs(got[0]-want) > 1e-7 { + t.Fatalf("candidate midpoint = %.12f, want %.12f", got[0], want) + } +} + +func TestUniqueOccultationCandidateTimesFiltersSingleOutsideCandidate(t *testing.T) { + if got := uniqueOccultationCandidateTimes([]float64{9.9}, 10, 11); len(got) != 0 { + t.Fatalf("outside candidate was not filtered: %v", got) + } + got := uniqueOccultationCandidateTimes([]float64{10.5}, 10, 11) + if len(got) != 1 || got[0] != 10.5 { + t.Fatalf("inside candidate = %v, want [10.5]", got) + } +} + +func TestStarOccultationPropagatesApparentCoordinateProperMotion(t *testing.T) { + epoch := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + target := epoch.Add(365*24*time.Hour + 6*time.Hour) + star := StarCoordinate{ + RA: 10, + Dec: 20, + Epoch: epoch, + Frame: CoordinateFrameApparentOfDate, + ProperMotionRACosDecMasPerYear: 360000, + ProperMotionDecMasPerYear: -720000, + } + baseline := star + baseline.ProperMotionRACosDecMasPerYear = 0 + baseline.ProperMotionDecMasPerYear = 0 + baselineRA, baselineDec := starApparentRaDec(occultationTimeToTT(target), baseline, Observer{}) + ra, dec := starApparentRaDec(occultationTimeToTT(target), star, Observer{}) + raMotion := signedAngleDifference(ra, baselineRA) * math.Cos(baselineDec*math.Pi/180) + decMotion := dec - baselineDec + if math.Abs(raMotion-0.1) > 0.001 { + t.Fatalf("propagated RA*cos(Dec) motion = %.10f deg, want 0.1", raMotion) + } + if math.Abs(decMotion-(-0.2)) > 0.001 { + t.Fatalf("propagated Dec motion = %.10f deg, want -0.2", decMotion) + } +} + +func TestStarOccultationApparentOfDateRoundTripsAtEpoch(t *testing.T) { + epoch := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC) + star := StarCoordinate{RA: 189.5, Dec: -6.2, Epoch: epoch, Frame: CoordinateFrameApparentOfDate, ParallaxMas: 100} + ra, dec := starApparentRaDecGeocentric(occultationTimeToTT(epoch), star) + if math.Abs(signedAngleDifference(ra, star.RA))*3600 > 1e-5 || math.Abs(dec-star.Dec)*3600 > 1e-5 { + t.Fatalf("apparent coordinate did not round-trip at epoch: got %.12f %.12f", ra, dec) + } +} + +func TestStarOccultationApparentPlaceCorrections(t *testing.T) { + location := time.FixedZone("CST", 8*3600) + tt := occultationTimeToTT(time.Date(2025, 6, 5, 20, 2, 7, 700000000, location)) + star := hr4799OccultationCoordinateForTest() + + gotRA, gotDec := starApparentRaDecGeocentric(tt, star) + if math.Abs(signedAngleDifference(gotRA, 189.527817)) > 0.0002 || math.Abs(gotDec-(-5.973401)) > 0.0002 { + t.Fatalf("apparent place = %.9f %.9f, want near 189.527817 -5.973401", gotRA, gotDec) + } + + years := (tt - Date2JDE(star.Epoch.UTC())) / 365.25 + meanRA := star.RA + years*star.ProperMotionRACosDecMasPerYear/(3600000*math.Cos(star.Dec*math.Pi/180)) + meanDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000 + meanRA, meanDec = Precess(meanRA, meanDec, 2451545, tt) + correction := angularSeparationDegrees(meanRA, meanDec, gotRA, gotDec) * 3600 + if correction < 5 || correction > 30 { + t.Fatalf("apparent-place correction = %.6f arcsec, want a plausible annual correction", correction) + } +} + +func TestStarOccultationApparentPlaceAppliesAnnualParallax(t *testing.T) { + star := hr4799OccultationCoordinateForTest() + tt := occultationTimeToTT(time.Date(2025, 6, 5, 12, 0, 0, 0, time.UTC)) + withoutRA, withoutDec := starApparentRaDecGeocentric(tt, star) + star.ParallaxMas = 1000 + withRA, withDec := starApparentRaDecGeocentric(tt, star) + shift := angularSeparationDegrees(withoutRA, withoutDec, withRA, withDec) * 3600 + if shift < 0.05 || shift > 1.1 { + t.Fatalf("annual parallax shift = %.6f arcsec, want (0.05, 1.1]", shift) + } +} + +func TestStarOccultationICRSAppliesJ2000FrameBias(t *testing.T) { + epoch := time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC) + icrs := StarCoordinate{RA: 0, Dec: 0, Epoch: epoch, Frame: CoordinateFrameICRS} + j2000 := icrs + j2000.Frame = CoordinateFrameJ2000 + tt := occultationTimeToTT(epoch) + icrsRA, icrsDec := starApparentRaDecGeocentric(tt, icrs) + j2000RA, j2000Dec := starApparentRaDecGeocentric(tt, j2000) + raBiasMas := signedAngleDifference(icrsRA, j2000RA) * 3600000 + decBiasMas := (icrsDec - j2000Dec) * 3600000 + if math.Abs(raBiasMas-14.6) > 0.1 || math.Abs(decBiasMas-(-16.617)) > 0.1 { + t.Fatalf("ICRS frame bias = %.6f %.6f mas, want about 14.6 -16.617", raBiasMas, decBiasMas) + } +} + +func TestRefinedStarOccultationCenterLineRespectsWidthTolerance(t *testing.T) { + star := hr4799OccultationCoordinateForTest() + start := time.Date(2025, time.June, 5, 0, 0, 0, 0, time.UTC) + paths, err := FindStarOccultationPaths( + start, start.Add(24*time.Hour), star, + OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 50}, + ) + if err != nil || len(paths) != 1 { + t.Fatalf("FindStarOccultationPaths() paths=%d err=%v, want one", len(paths), err) + } + for index, point := range paths[0].CenterLine { + exact, ok := starOccultationPathCenterPoint(centerTimeTT(point.Time), star, time.UTC) + if !ok { + t.Fatalf("exact center point %d is unavailable", index) + } + if difference := math.Abs(point.WidthKM - exact.WidthKM); difference > occultationPathWidthToleranceKM { + t.Fatalf("center point %d width differs from exact value by %.9f km: got %.9f want %.9f", + index, difference, point.WidthKM, exact.WidthKM) + } + } +} + +func TestRefineOccultationPathWidthsBoundsSmoothInterpolationError(t *testing.T) { + start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC) + startTT := occultationTimeToTT(start) + widthAt := func(tt float64) (float64, bool) { + seconds := (tt - startTT) * 86400 + return 3500 + 0.0002*(seconds-50)*(seconds-50), true + } + points := make([]OccultationPathPoint, 101) + for index := range points { + points[index].Time = start.Add(time.Duration(index) * time.Second) + } + points[0].WidthKM, _ = widthAt(centerTimeTT(points[0].Time)) + points[len(points)-1].WidthKM, _ = widthAt(centerTimeTT(points[len(points)-1].Time)) + refineOccultationPathWidths(points, widthAt) + for index, point := range points { + exact, _ := widthAt(centerTimeTT(point.Time)) + if difference := math.Abs(point.WidthKM - exact); difference > occultationPathWidthToleranceKM { + t.Fatalf("interpolated width %d differs by %.9f km, tolerance %.9f", index, difference, occultationPathWidthToleranceKM) + } + } +} + +func hr4799OccultationCoordinateForTest() StarCoordinate { + return StarCoordinate{ + ID: "HR 4799", + RA: 189.1975, + Dec: -5.831944444444, + Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), + Frame: CoordinateFrameJ2000, + ProperMotionRACosDecMasPerYear: -28, + ProperMotionDecMasPerYear: -18, + } +} diff --git a/basic/orbit_observation.go b/basic/orbit_observation.go index 5980c6e..4dee525 100644 --- a/basic/orbit_observation.go +++ b/basic/orbit_observation.go @@ -52,6 +52,9 @@ func OrbitHourAngle(jde, observerLon, observerLat, timezone, observerHeight floa // OrbitCulminationTime 返回轨道目标的中天时刻,输入输出均沿用本仓库现有观测函数的 JD 语义。 func OrbitCulminationTime(jde, observerLon, observerLat, timezone, observerHeight float64, elements OrbitElements) float64 { + if !isFiniteFloat(jde) || !isFiniteFloat(observerLon) || !isFiniteFloat(observerLat) || !isFiniteFloat(timezone) || !isFiniteFloat(observerHeight) { + return math.NaN() + } jde = math.Floor(jde) + 0.5 estimateJD := jde + Limit360(360-OrbitHourAngle(jde, observerLon, observerLat, timezone, observerHeight, elements))/15.0/24.0*0.99726851851851851851 normalizedHourAngle := func(jde float64) float64 { @@ -61,14 +64,14 @@ func OrbitCulminationTime(jde, observerLon, observerLat, timezone, observerHeigh } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005) - normalizedHourAngle(prevJD-0.000005)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } @@ -84,19 +87,33 @@ func OrbitSetTime(jde, observerLon, observerLat, timezone, aeroCorrection, obser } func orbitRiseDown(jde, observerLon, observerLat, timezone, aeroCorrection, observerHeight float64, elements OrbitElements, isRise bool) (float64, error) { + if !isFiniteFloat(jde) || !isFiniteFloat(observerLon) || !isFiniteFloat(observerLat) || !isFiniteFloat(timezone) || !isFiniteFloat(aeroCorrection) || !isFiniteFloat(observerHeight) { + return 0, ErrInvalidObservationInput + } localTimezone := math.Round(observerLon / 15) targetAltitude := StandardAltitudePlanet(aeroCorrection, observerHeight, observerLat) culminationJD := OrbitCulminationTime(jde, observerLon, observerLat, localTimezone, observerHeight, elements) - if OrbitHeight(culminationJD, observerLon, observerLat, localTimezone, observerHeight, elements) < targetAltitude { + if !isFiniteFloat(culminationJD) { + return 0, ErrInvalidObservationInput + } + culminationHeight := OrbitHeight(culminationJD, observerLon, observerLat, localTimezone, observerHeight, elements) + previousHeight := OrbitHeight(culminationJD-0.5, observerLon, observerLat, localTimezone, observerHeight, elements) + if !isFiniteFloat(culminationHeight) || !isFiniteFloat(previousHeight) { + return 0, ErrInvalidObservationInput + } + if culminationHeight < targetAltitude { return 0, ErrNeverRise } - if OrbitHeight(culminationJD-0.5, observerLon, observerLat, localTimezone, observerHeight, elements) > targetAltitude { + if previousHeight > targetAltitude { return 0, ErrNeverSet } _, dec, _ := orbitTopocentricObservation(culminationJD, observerLon, observerLat, observerHeight, localTimezone, elements) cosHourAngle := (Sin(targetAltitude) - Sin(dec)*Sin(observerLat)) / (Cos(dec) * Cos(observerLat)) + if !isFiniteFloat(dec) || !isFiniteFloat(cosHourAngle) { + return 0, ErrInvalidObservationInput + } var eventJD float64 if math.Abs(cosHourAngle) <= 1 { @@ -122,15 +139,13 @@ func orbitRiseDown(jde, observerLon, observerLat, timezone, aeroCorrection, obse } } - estimateJD := eventJD - for { - prevJD := estimateJD + estimateJD, ok := eventNewtonRefine(eventJD, 0.00001, func(prevJD float64) float64 { altitudeDelta := OrbitHeight(prevJD, observerLon, observerLat, localTimezone, observerHeight, elements) - targetAltitude altitudeSlope := (OrbitHeight(prevJD+0.000005, observerLon, observerLat, localTimezone, observerHeight, elements) - OrbitHeight(prevJD-0.000005, observerLon, observerLat, localTimezone, observerHeight, elements)) / 0.00001 - estimateJD = prevJD - altitudeDelta/altitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return altitudeDelta / altitudeSlope + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimezone/24 + timezone/24, nil } diff --git a/basic/planet_input_guard_test.go b/basic/planet_input_guard_test.go new file mode 100644 index 0000000..52c43a9 --- /dev/null +++ b/basic/planet_input_guard_test.go @@ -0,0 +1,93 @@ +package basic + +import ( + "math" + "testing" +) + +func TestPlanetEventsRejectNonFiniteQuery(t *testing.T) { + nan := math.NaN() + events := []struct { + name string + fn func(float64) float64 + }{ + {"Mars conjunction", NextMarsConjunction}, + {"Jupiter conjunction", NextJupiterConjunction}, + {"Saturn conjunction", NextSaturnConjunction}, + {"Uranus conjunction", NextUranusConjunction}, + {"Neptune conjunction", NextNeptuneConjunction}, + {"Mercury conjunction", NextMercuryConjunction}, + {"Mercury greatest elongation", NextMercuryGreatestElongation}, + {"Venus greatest elongation", NextVenusGreatestElongation}, + {"Venus station", NextVenusProgradeToRetrograde}, + } + for _, tc := range events { + t.Run(tc.name, func(t *testing.T) { + if got := tc.fn(nan); !math.IsNaN(got) { + t.Fatalf("got %v, want NaN", got) + } + }) + } +} + +func TestPlanetObservationRejectsNonFiniteQuery(t *testing.T) { + nan := math.NaN() + culminations := []struct { + name string + fn func(float64, float64, float64) float64 + }{ + {"Mercury", MercuryCulminationTime}, + {"Venus", VenusCulminationTime}, + {"Mars", MarsCulminationTime}, + {"Jupiter", JupiterCulminationTime}, + {"Saturn", SaturnCulminationTime}, + {"Uranus", UranusCulminationTime}, + {"Neptune", NeptuneCulminationTime}, + } + for _, tc := range culminations { + t.Run(tc.name, func(t *testing.T) { + if got := tc.fn(nan, 0, 0); !math.IsNaN(got) { + t.Fatalf("got %v, want NaN", got) + } + }) + } + if got := OrbitCulminationTime(nan, 0, 0, 0, 0, OrbitElements{}); !math.IsNaN(got) { + t.Fatalf("OrbitCulminationTime got %v, want NaN", got) + } + if got := MercuryCulminationTimeN(nan, 0, 0, 16); !math.IsNaN(got) { + t.Fatalf("MercuryCulminationTimeN got %v, want NaN", got) + } + + if _, err := MarsRiseTime(nan, 0, 0, 0, 0, 0); err != ErrInvalidObservationInput { + t.Fatalf("MarsRiseTime error = %v, want ErrInvalidObservationInput", err) + } + if _, err := MercuryRiseTimeN(nan, 0, 0, 0, 0, 0, 16); err != ErrInvalidObservationInput { + t.Fatalf("MercuryRiseTimeN error = %v, want ErrInvalidObservationInput", err) + } + if got := JupiterGalileanSatelliteState(nan, 1); !math.IsNaN(got.X) { + t.Fatalf("JupiterGalileanSatelliteState X = %v, want NaN", got.X) + } +} + +func TestSharedObservationRejectsNonFiniteQuery(t *testing.T) { + nan := math.NaN() + if got := MoonCulminationTime(nan, 0, 0, 0); !math.IsNaN(got) { + t.Fatalf("MoonCulminationTime got %v, want NaN", got) + } + if got := StarCulminationTime(nan, 0, 0, 0); !math.IsNaN(got) { + t.Fatalf("StarCulminationTime got %v, want NaN", got) + } + if got := CalcMoonSHByJDE(nan, 0); !math.IsNaN(got) { + t.Fatalf("CalcMoonSHByJDE got %v, want NaN", got) + } + + if _, err := GetSunRiseTime(nan, 0, 0, 0, 0, 0); err != ErrInvalidObservationInput { + t.Fatalf("GetSunRiseTime error = %v, want ErrInvalidObservationInput", err) + } + if _, err := GetMoonRiseTime(nan, 0, 0, 0, 0, 0); err != ErrInvalidObservationInput { + t.Fatalf("GetMoonRiseTime error = %v, want ErrInvalidObservationInput", err) + } + if _, err := StarRiseTime(nan, 0, 0, 0, 0, 0, 0, false); err != ErrInvalidObservationInput { + t.Fatalf("StarRiseTime error = %v, want ErrInvalidObservationInput", err) + } +} diff --git a/basic/planet_transit.go b/basic/planet_transit.go index 81be681..c417781 100644 --- a/basic/planet_transit.go +++ b/basic/planet_transit.go @@ -195,9 +195,23 @@ func nextPlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, direction for nodeOffset := 0; nodeOffset <= 1; nodeOffset++ { candidate := estimatePlanetTransitSeasonTT(jdTT, cfg, nodeOffset, direction) candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset) - for !planetTransitMatchesDirection(candidate, jdTT, direction, false) { + if !isFiniteFloat(candidate) { + continue + } + matchedDirection := false + for i := 0; i < eventDirectionalSearchIterations; i++ { + if planetTransitMatchesDirection(candidate, jdTT, direction, false) { + matchedDirection = true + break + } candidate += float64(direction) * planetTransitTropicalYearDays candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset) + if !isFiniteFloat(candidate) { + break + } + } + if !matchedDirection { + continue } if !isFiniteFloat(best) || math.Abs(candidate-jdTT) < math.Abs(best-jdTT) { best = candidate @@ -514,7 +528,3 @@ func planetTransitAngleDelta(diff float64) float64 { } return diff } - -func isFiniteFloat(value float64) bool { - return !math.IsNaN(value) && !math.IsInf(value, 0) -} diff --git a/basic/planet_truncated.go b/basic/planet_truncated.go index 05e16ff..41b0351 100644 --- a/basic/planet_truncated.go +++ b/basic/planet_truncated.go @@ -92,6 +92,9 @@ func planetHourAngleN(jd, lon, timezone float64, n int, apparentRa func(float64, } func planetCulminationTimeN(jde, lon, timezone float64, n int, hourAngle func(float64, float64, float64, int) float64) float64 { + if !isFiniteFloat(jde) || !isFiniteFloat(lon) || !isFiniteFloat(timezone) { + return math.NaN() + } jde = math.Floor(jde) + 0.5 estimateJD := jde + Limit360(360-hourAngle(jde, lon, timezone, n))/15.0/24.0*0.99726851851851851851 normalizedHourAngle := func(jde, lon, timezone float64) float64 { @@ -101,31 +104,45 @@ func planetCulminationTimeN(jde, lon, timezone float64, n int, hourAngle func(fl } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } func planetRiseDownN(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool, n int, culmination func(float64, float64, float64, int) float64, height func(float64, float64, float64, float64, int) float64, declination planetDeclinationFuncN) (float64, error) { + if !isFiniteFloat(jd) || !isFiniteFloat(lon) || !isFiniteFloat(lat) || !isFiniteFloat(timezone) || !isFiniteFloat(aeroCorrection) || !isFiniteFloat(observerHeight) { + return 0, ErrInvalidObservationInput + } jd = math.Floor(jd) + 0.5 localTimezone := math.Round(lon / 15) targetAltitude := StandardAltitudePlanet(aeroCorrection, observerHeight, lat) culminationJD := culmination(jd, lon, localTimezone, n) - if height(culminationJD, lon, lat, localTimezone, n) < targetAltitude { + if !isFiniteFloat(culminationJD) { + return 0, ErrInvalidObservationInput + } + culminationHeight := height(culminationJD, lon, lat, localTimezone, n) + previousHeight := height(culminationJD-0.5, lon, lat, localTimezone, n) + if !isFiniteFloat(culminationHeight) || !isFiniteFloat(previousHeight) { + return 0, ErrInvalidObservationInput + } + if culminationHeight < targetAltitude { return 0, ErrNeverRise } - if height(culminationJD-0.5, lon, lat, localTimezone, n) > targetAltitude { + if previousHeight > targetAltitude { return 0, ErrNeverSet } dec := declination(TD2UT(culminationJD-localTimezone/24, true), n) cosHourAngle := (Sin(targetAltitude) - Sin(dec)*Sin(lat)) / (Cos(dec) * Cos(lat)) + if !isFiniteFloat(dec) || !isFiniteFloat(cosHourAngle) { + return 0, ErrInvalidObservationInput + } var eventJD float64 if math.Abs(cosHourAngle) <= 1 { hourOffset := ArcCos(cosHourAngle) / 15 @@ -149,15 +166,13 @@ func planetRiseDownN(jd, lon, lat, timezone, aeroCorrection, observerHeight floa } } } - estimateJD := eventJD - for { - prevJD := estimateJD + estimateJD, ok := eventNewtonRefine(eventJD, 0.00001, func(prevJD float64) float64 { altitudeDelta := height(prevJD, lon, lat, localTimezone, n) - targetAltitude altitudeSlope := (height(prevJD+0.000005, lon, lat, localTimezone, n) - height(prevJD-0.000005, lon, lat, localTimezone, n)) / 0.00001 - estimateJD = prevJD - altitudeDelta/altitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return altitudeDelta / altitudeSlope + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimezone/24 + timezone/24, nil } diff --git a/basic/refraction.go b/basic/refraction.go index 5c129d6..56b6079 100644 --- a/basic/refraction.go +++ b/basic/refraction.go @@ -4,32 +4,37 @@ import "math" const ( refractionStandardPressureHPa = 1010.0 - refractionStandardTemperatureK = 283.0 + refractionStandardTemperatureC = 10.0 + refractionStandardTemperatureK = 283.15 refractionAbsoluteZeroC = -273.15 refractionLowerLimitAltitudeDeg = -5.0 refractionUpperLimitAltitudeDeg = 90.0 ) // RefractionFromApparentAltitude 大气折射修正量,单位度;输入为视高度角。 -// 返回值应从真高度角加上后得到视高度角。 +// 返回值应从视高度角减去后得到真高度角。 +// 若模型在支持的真高度范围内没有逆解,则返回 NaN。 func RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 { if !validRefractionInputs(apparentAltitude, pressureHPa, temperatureC) { return math.NaN() } - if apparentAltitude < refractionLowerLimitAltitudeDeg || apparentAltitude > refractionUpperLimitAltitudeDeg { + if apparentAltitude <= refractionLowerLimitAltitudeDeg || apparentAltitude >= refractionUpperLimitAltitudeDeg { return 0 } - angle := (apparentAltitude + 10.3/(apparentAltitude+5.11)) * math.Pi / 180 - return refractionScale(pressureHPa, temperatureC) * (1.02 / math.Tan(angle)) / 60 + trueAltitude := trueAltitudeFromApparent(apparentAltitude, pressureHPa, temperatureC) + return apparentAltitude - trueAltitude } -// TrueAltitude 真高度角,单位度;输入为视高度角。 +// TrueAltitude 真高度角,单位度;输入为视高度角。若折射模型在支持的 +// 真高度范围内没有逆解,则返回 NaN。 func TrueAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 { - refraction := RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC) - if math.IsNaN(refraction) { + if !validRefractionInputs(apparentAltitude, pressureHPa, temperatureC) { return math.NaN() } - return apparentAltitude - refraction + if apparentAltitude <= refractionLowerLimitAltitudeDeg || apparentAltitude >= refractionUpperLimitAltitudeDeg { + return apparentAltitude + } + return trueAltitudeFromApparent(apparentAltitude, pressureHPa, temperatureC) } // ApparentAltitude 视高度角,单位度;输入为真高度角。 @@ -37,45 +42,118 @@ func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 { if !validRefractionInputs(trueAltitude, pressureHPa, temperatureC) { return math.NaN() } - if trueAltitude < refractionLowerLimitAltitudeDeg || trueAltitude > refractionUpperLimitAltitudeDeg { - return trueAltitude - } - - estimate := trueAltitude + RefractionFromApparentAltitude(trueAltitude, pressureHPa, temperatureC) - for i := 0; i < 8; i++ { - refraction := RefractionFromApparentAltitude(estimate, pressureHPa, temperatureC) - if math.IsNaN(refraction) { - return math.NaN() - } - value := estimate - refraction - trueAltitude - if math.Abs(value) < 1e-12 { - break - } - - const delta = 1e-6 - refractionPlus := RefractionFromApparentAltitude(estimate+delta, pressureHPa, temperatureC) - refractionMinus := RefractionFromApparentAltitude(estimate-delta, pressureHPa, temperatureC) - if math.IsNaN(refractionPlus) || math.IsNaN(refractionMinus) { - return math.NaN() - } - - derivative := 1 - (refractionPlus-refractionMinus)/(2*delta) - if derivative == 0 { - break - } - estimate -= value / derivative - } - return estimate + return trueAltitude + refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC) } // RefractionFromTrueAltitude 大气折射修正量,单位度;输入为真高度角。 // 返回值应从真高度角加上后得到视高度角。 func RefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 { - apparentAltitude := ApparentAltitude(trueAltitude, pressureHPa, temperatureC) - if math.IsNaN(apparentAltitude) { + if !validRefractionInputs(trueAltitude, pressureHPa, temperatureC) { return math.NaN() } - return apparentAltitude - trueAltitude + return refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC) +} + +// Saemundsson 公式以真高度角为输入;逆 API 对同一模型做数值求解,保持公开真/视高度语义一致。 +// Saemundsson's formula takes true altitude. The inverse APIs solve the same model numerically so the public true/apparent semantics remain consistent. +func refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 { + if trueAltitude <= refractionLowerLimitAltitudeDeg || trueAltitude >= refractionUpperLimitAltitudeDeg { + return 0 + } + angle := (trueAltitude + 10.3/(trueAltitude+5.11)) * math.Pi / 180 + return refractionScale(pressureHPa, temperatureC) * (1.02 / math.Tan(angle)) / 60 +} + +func trueAltitudeFromApparent(apparentAltitude, pressureHPa, temperatureC float64) float64 { + lower := math.Nextafter(refractionLowerLimitAltitudeDeg, math.Inf(1)) + upper := apparentAltitude + // Saemundsson 近似在 90 度以下极窄范围会略为负值;此时真高度角高于视高度角。若用视高度角作为根区间上界会漏掉有效解,因此保留模型明确的 90 度边界,允许逆解越过输入的视高度角。 + // Saemundsson's approximation becomes slightly negative just below 90 degrees. In that narrow range the true altitude is above the apparent altitude, so using the apparent altitude as the root bracket would omit the valid solution. Keep the model's explicit 90-degree boundary while allowing the inverse to cross above the apparent input. + if refractionFromTrueAltitude(apparentAltitude, pressureHPa, temperatureC) < 0 { + upper = math.Nextafter(refractionUpperLimitAltitudeDeg, math.Inf(-1)) + } + if estimate, ok := trueAltitudeFromApparentNewton(apparentAltitude, pressureHPa, temperatureC, lower, upper); ok { + return estimate + } + return trueAltitudeFromApparentBisection(apparentAltitude, pressureHPa, temperatureC, lower, upper) +} + +func trueAltitudeFromApparentNewton(apparentAltitude, pressureHPa, temperatureC, lower, upper float64) (float64, bool) { + estimate := apparentAltitude - refractionFromTrueAltitude(apparentAltitude, pressureHPa, temperatureC) + const delta = 1e-6 + for i := 0; i < 12; i++ { + if estimate < lower || estimate > upper || !finiteRefractionValue(estimate) { + return 0, false + } + value := refractionInverseResidual(estimate, apparentAltitude, pressureHPa, temperatureC) + if math.Abs(value) < 1e-12 { + return estimate, true + } + refractionPlus := refractionFromTrueAltitude(estimate+delta, pressureHPa, temperatureC) + refractionMinus := refractionFromTrueAltitude(estimate-delta, pressureHPa, temperatureC) + derivative := 1 + (refractionPlus-refractionMinus)/(2*delta) + if math.Abs(derivative) < 1e-12 || !finiteRefractionValue(derivative) { + return 0, false + } + next := estimate - value/derivative + if next < lower || next > upper || !finiteRefractionValue(next) { + return 0, false + } + estimate = next + } + return 0, false +} + +func trueAltitudeFromApparentBisection(apparentAltitude, pressureHPa, temperatureC, lower, upper float64) float64 { + lowerValue := refractionInverseResidual(lower, apparentAltitude, pressureHPa, temperatureC) + upperValue := refractionInverseResidual(upper, apparentAltitude, pressureHPa, temperatureC) + if !finiteRefractionValue(lowerValue) || !finiteRefractionValue(upperValue) || lowerValue > 0 || upperValue < 0 { + return math.NaN() + } + if math.Abs(lowerValue) < 1e-12 { + return lower + } + if math.Abs(upperValue) < 1e-12 { + return upper + } + + best, bestResidual := lower, math.Abs(lowerValue) + if math.Abs(upperValue) < bestResidual { + best, bestResidual = upper, math.Abs(upperValue) + } + for i := 0; i < 96; i++ { + midpoint := lower + (upper-lower)/2 + if midpoint == lower || midpoint == upper { + break + } + value := refractionInverseResidual(midpoint, apparentAltitude, pressureHPa, temperatureC) + if !finiteRefractionValue(value) { + return math.NaN() + } + if residual := math.Abs(value); residual < bestResidual { + best, bestResidual = midpoint, residual + } + if math.Abs(value) < 1e-12 { + return midpoint + } + if value > 0 { + upper = midpoint + } else { + lower = midpoint + } + } + if bestResidual <= 1e-9 { + return best + } + return math.NaN() +} + +func refractionInverseResidual(trueAltitude, apparentAltitude, pressureHPa, temperatureC float64) float64 { + return trueAltitude + refractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC) - apparentAltitude +} + +func finiteRefractionValue(value float64) bool { + return !math.IsNaN(value) && !math.IsInf(value, 0) } func validRefractionInputs(altitude, pressureHPa, temperatureC float64) bool { @@ -85,5 +163,5 @@ func validRefractionInputs(altitude, pressureHPa, temperatureC float64) bool { } func refractionScale(pressureHPa, temperatureC float64) float64 { - return pressureHPa / refractionStandardPressureHPa * refractionStandardTemperatureK / (273 + temperatureC) + return pressureHPa / refractionStandardPressureHPa * refractionStandardTemperatureK / (temperatureC - refractionAbsoluteZeroC) } diff --git a/basic/refraction_test.go b/basic/refraction_test.go index 413c789..02ec8c9 100644 --- a/basic/refraction_test.go +++ b/basic/refraction_test.go @@ -5,9 +5,17 @@ import ( "testing" ) -func TestRefractionFromApparentAltitudeStandardAtmosphere(t *testing.T) { - assertClose(t, "Refraction@0deg", RefractionFromApparentAltitude(0, 1010, 10), 0.483032, 0.0001) - assertClose(t, "Refraction@45deg", RefractionFromApparentAltitude(45, 1010, 10), 0.016878, 0.0001) +func TestRefractionFromTrueAltitudeStandardAtmosphere(t *testing.T) { + assertClose(t, "RefractionFromTrue@0deg", RefractionFromTrueAltitude(0, 1010, 10), 0.483032, 0.0001) + assertClose(t, "RefractionFromTrue@45deg", RefractionFromTrueAltitude(45, 1010, 10), 0.016878, 0.0001) + assertClose(t, "ApparentAltitude@0deg", ApparentAltitude(0, 1010, 10), 0.483032, 0.0001) +} + +func TestRefractionFromApparentAltitudeUsesApparentInput(t *testing.T) { + // 视地平线处修正约为 34.5 角分,而不是真高度角公式在 0 度返回的 29 角分 / At the apparent horizon the correction is about 34.5 arcminutes, not + // 29 角分 / the 29 arcminutes returned by the true-altitude formula at 0 degrees. + assertClose(t, "RefractionFromApparent@0deg", RefractionFromApparentAltitude(0, 1010, 10), 0.574, 0.001) + assertClose(t, "TrueAltitude@0deg", TrueAltitude(0, 1010, 10), -0.574, 0.001) } func TestRefractionRoundTrip(t *testing.T) { @@ -35,29 +43,77 @@ func TestRefractionRoundTrip(t *testing.T) { } } +func TestRefractionRoundTripNearZenith(t *testing.T) { + for _, trueAltitude := range []float64{89.90, 89.95, 89.99} { + apparentAltitude := ApparentAltitude(trueAltitude, 1010, 10) + if math.IsNaN(apparentAltitude) || math.IsInf(apparentAltitude, 0) { + t.Fatalf("true altitude %.2f produced invalid apparent altitude %v", trueAltitude, apparentAltitude) + } + got := TrueAltitude(apparentAltitude, 1010, 10) + if math.IsNaN(got) || math.IsInf(got, 0) { + t.Fatalf("apparent altitude %.12f produced invalid true altitude %v", apparentAltitude, got) + } + assertClose(t, "NearZenithRoundTrip", got, trueAltitude, 1e-9) + if refraction := RefractionFromApparentAltitude(apparentAltitude, 1010, 10); math.IsNaN(refraction) || math.IsInf(refraction, 0) { + t.Fatalf("apparent altitude %.12f produced invalid refraction %v", apparentAltitude, refraction) + } + } +} + +func TestRefractionExtremeTemperatureInverse(t *testing.T) { + const ( + apparentAltitude = 0.0 + pressureHPa = 1010.0 + ) + trueAltitude := TrueAltitude(apparentAltitude, pressureHPa, -273) + if math.IsNaN(trueAltitude) || math.IsInf(trueAltitude, 0) { + t.Fatalf("-273 C inverse returned invalid true altitude %v", trueAltitude) + } + assertClose(t, "ExtremeTemperatureRoundTrip", + ApparentAltitude(trueAltitude, pressureHPa, -273), apparentAltitude, 1e-9) + + for _, temperatureC := range []float64{-273.14, math.Nextafter(refractionAbsoluteZeroC, math.Inf(1))} { + if got := TrueAltitude(apparentAltitude, pressureHPa, temperatureC); !math.IsNaN(got) { + t.Errorf("temperature %.17g C inverse = %v, want NaN when the model has no root", temperatureC, got) + } + if got := RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC); !math.IsNaN(got) { + t.Errorf("temperature %.17g C apparent refraction = %v, want NaN when the model has no root", temperatureC, got) + } + } +} + func TestRefractionScalingAndBounds(t *testing.T) { - lowPressure := RefractionFromApparentAltitude(0, 980, 10) - highPressure := RefractionFromApparentAltitude(0, 1030, 10) + lowPressure := RefractionFromTrueAltitude(0, 980, 10) + highPressure := RefractionFromTrueAltitude(0, 1030, 10) if !(lowPressure < highPressure) { t.Fatalf("pressure scaling mismatch: low %.12f high %.12f", lowPressure, highPressure) } - cold := RefractionFromApparentAltitude(0, 1010, 0) - hot := RefractionFromApparentAltitude(0, 1010, 30) + cold := RefractionFromTrueAltitude(0, 1010, 0) + hot := RefractionFromTrueAltitude(0, 1010, 30) if !(cold > hot) { t.Fatalf("temperature scaling mismatch: cold %.12f hot %.12f", cold, hot) } - if RefractionFromApparentAltitude(-6, 1010, 10) != 0 { + if RefractionFromTrueAltitude(-6, 1010, 10) != 0 { t.Fatalf("refraction below lower limit should be 0") } - if RefractionFromApparentAltitude(95, 1010, 10) != 0 { + if RefractionFromTrueAltitude(95, 1010, 10) != 0 { t.Fatalf("refraction above upper limit should be 0") } - if !math.IsNaN(RefractionFromApparentAltitude(0, 0, 10)) { + if !math.IsNaN(RefractionFromTrueAltitude(0, 0, 10)) { t.Fatalf("invalid pressure should produce NaN") } - if !math.IsNaN(RefractionFromApparentAltitude(0, 1010, -274)) { + if !math.IsNaN(RefractionFromTrueAltitude(0, 1010, -274)) { t.Fatalf("invalid temperature should produce NaN") } + if !math.IsNaN(RefractionFromTrueAltitude(0, 1010, -273.15)) { + t.Fatalf("absolute zero should produce NaN") + } + for _, temperatureC := range []float64{-273, -273.14} { + refraction := RefractionFromTrueAltitude(0, 1010, temperatureC) + if math.IsNaN(refraction) || math.IsInf(refraction, 0) || refraction <= 0 { + t.Fatalf("temperature %.2f C produced invalid refraction %v", temperatureC, refraction) + } + } } diff --git a/basic/rise_set.go b/basic/rise_set.go index cf5390a..b754803 100644 --- a/basic/rise_set.go +++ b/basic/rise_set.go @@ -8,9 +8,10 @@ import ( ) var ( - ErrNeverRise = errors.New("rise event does not occur on this date") - ErrNeverSet = errors.New("set event does not occur on this date") - ErrNotOnThisDate = errors.New("rise/set event occurs on adjacent date") + ErrNeverRise = errors.New("rise event does not occur on this date") + ErrNeverSet = errors.New("set event does not occur on this date") + ErrNotOnThisDate = errors.New("rise/set event occurs on adjacent date") + ErrInvalidObservationInput = errors.New("invalid observation input") ) func StandardAltitudeStar(aero bool, observerHeight, lat float64) float64 { @@ -50,20 +51,34 @@ type planetHeightFunc func(float64, float64, float64, float64) float64 type planetDeclinationFunc func(float64) float64 func planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float64, isRise bool, culmination planetCulminationFunc, height planetHeightFunc, declination planetDeclinationFunc) (float64, error) { + if !isFiniteFloat(jd) || !isFiniteFloat(lon) || !isFiniteFloat(lat) || !isFiniteFloat(timezone) || !isFiniteFloat(aeroCorrection) || !isFiniteFloat(observerHeight) { + return 0, ErrInvalidObservationInput + } jd = math.Floor(jd) + 0.5 localTimezone := math.Round(lon / 15) targetAltitude := StandardAltitudePlanet(aeroCorrection, observerHeight, lat) culminationJD := culmination(jd, lon, localTimezone) - if height(culminationJD, lon, lat, localTimezone) < targetAltitude { + if !isFiniteFloat(culminationJD) { + return 0, ErrInvalidObservationInput + } + culminationHeight := height(culminationJD, lon, lat, localTimezone) + previousHeight := height(culminationJD-0.5, lon, lat, localTimezone) + if !isFiniteFloat(culminationHeight) || !isFiniteFloat(previousHeight) { + return 0, ErrInvalidObservationInput + } + if culminationHeight < targetAltitude { return 0, ErrNeverRise } - if height(culminationJD-0.5, lon, lat, localTimezone) > targetAltitude { + if previousHeight > targetAltitude { return 0, ErrNeverSet } dec := declination(TD2UT(culminationJD-localTimezone/24, true)) cosHourAngle := (Sin(targetAltitude) - Sin(dec)*Sin(lat)) / (Cos(dec) * Cos(lat)) + if !isFiniteFloat(dec) || !isFiniteFloat(cosHourAngle) { + return 0, ErrInvalidObservationInput + } var eventJD float64 if math.Abs(cosHourAngle) <= 1 { @@ -89,15 +104,13 @@ func planetRiseDown(jd, lon, lat, timezone, aeroCorrection, observerHeight float } } - estimateJD := eventJD - for { - prevJD := estimateJD + estimateJD, ok := eventNewtonRefine(eventJD, 0.00001, func(prevJD float64) float64 { altitudeDelta := height(prevJD, lon, lat, localTimezone) - targetAltitude altitudeSlope := (height(prevJD+0.000005, lon, lat, localTimezone) - height(prevJD-0.000005, lon, lat, localTimezone)) / 0.00001 - estimateJD = prevJD - altitudeDelta/altitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return altitudeDelta / altitudeSlope + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimezone/24 + timezone/24, nil } diff --git a/basic/rise_set_dynamic_test.go b/basic/rise_set_dynamic_test.go new file mode 100644 index 0000000..16d7b30 --- /dev/null +++ b/basic/rise_set_dynamic_test.go @@ -0,0 +1,150 @@ +package basic + +import ( + "errors" + "math" + "testing" + "time" + + . "b612.me/astro/tools" +) + +func TestSunRiseSetDynamicResidual(t *testing.T) { + const ( + longitude = 116.4074 + latitude = 39.9042 + timeZone = 8.0 + height = 0.0 + ) + jd := JDECalc(2025, 6, 5) + + for _, event := range []struct { + name string + get func(float64, float64, float64, float64, float64, float64) (float64, error) + }{ + {name: "rise", get: GetSunRiseTime}, + {name: "set", get: GetSunSetTime}, + } { + eventJD, err := event.get(jd, longitude, latitude, timeZone, 1, height) + if err != nil { + t.Fatalf("%s: %v", event.name, err) + } + naturalTimeZone := math.Round(longitude / 15) + localJD := eventJD + naturalTimeZone/24 - timeZone/24 + residual := sunRiseSetResidual(localJD, longitude, latitude, naturalTimeZone, 1, height, -1) + if math.Abs(residual) > 0.001 { + t.Fatalf("%s dynamic horizon residual = %.9f degrees", event.name, residual) + } + + fixedResidual := SunHeight(localJD, longitude, latitude, naturalTimeZone) - StandardAltitudeSun(1, height, latitude) + if math.Abs(fixedResidual) < 0.01 { + t.Fatalf("%s still matches the legacy fixed-altitude event: residual %.9f degrees", event.name, fixedResidual) + } + } +} + +func TestSunApparentStateReusesDistanceWithoutChangingCoordinates(t *testing.T) { + const jd = 2460827.5 + ra, dec, distanceAU := hSunApparentRaDecDistanceN(jd, -1) + wantRA, wantDec := LoBoToRaDec(jd, HSunApparentLoN(jd, -1), HSunTrueBoN(jd, -1)) + assertClose(t, "sun state RA", ra, wantRA, 1e-12) + assertClose(t, "sun state Dec", dec, wantDec, 1e-12) + assertClose(t, "sun state distance", distanceAU, EarthAwayN(jd, -1), 1e-15) +} + +func TestSunRiseSetDynamicGrazingKeepsDateAndDirection(t *testing.T) { + date := time.Date(2025, 6, 10, 0, 0, 0, 0, time.UTC) + jd := Date2JDE(date) + dayStart := math.Floor(jd) + 0.5 + + rise, err := GetSunRiseTime(jd, 0, 66, 0, 1, 0) + if err != nil { + t.Fatalf("sunrise: %v", err) + } + set, err := GetSunSetTime(jd, 0, 66, 0, 1, 0) + if err != nil { + t.Fatalf("sunset: %v", err) + } + + assertRiseSetEvent(t, "sunrise", rise, dayStart, true, func(eventJD float64) float64 { + return sunRiseSetResidual(eventJD, 0, 66, 0, 1, 0, -1) + }) + assertRiseSetEvent(t, "sunset", set, dayStart, false, func(eventJD float64) float64 { + return sunRiseSetResidual(eventJD, 0, 66, 0, 1, 0, -1) + }) +} + +func TestMoonSetDynamicGrazingKeepsDirection(t *testing.T) { + date := time.Date(2025, 1, 31, 0, 0, 0, 0, time.UTC) + jd := Date2JDE(date) + dayStart := math.Floor(jd) + 0.5 + set, err := GetMoonSetTime(jd, 0, 80, 0, 1, 0) + if err != nil { + t.Fatalf("moonset: %v", err) + } + assertRiseSetEvent(t, "moonset", set, dayStart, false, func(eventJD float64) float64 { + return moonRiseSetResidual(eventJD, 0, 80, 0, 1, 0, -1) + }) +} + +func TestMoonRiseSetDirectionalFallbackPreservesMissingEventError(t *testing.T) { + date := time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC) + jd := Date2JDE(date) + _, err := GetMoonRiseTime(jd, -42.6043, 71.7069, -3, 1, 0) + if !errors.Is(err, ErrNeverRise) { + t.Fatalf("moonrise error = %v, want %v", err, ErrNeverRise) + } +} + +func TestMoonRiseSetDynamicUsesObserverHeightForParallax(t *testing.T) { + date := time.Date(2025, 6, 5, 0, 0, 0, 0, time.UTC) + jd := Date2JDE(date) + const ( + longitude = 116.4074 + latitude = 39.9042 + height = 10000.0 + ) + rise, err := GetMoonRiseTime(jd, longitude, latitude, 0, 1, height) + if err != nil { + t.Fatalf("moonrise: %v", err) + } + residual := moonRiseSetResidualAtObserverHeight(rise, longitude, latitude, 0, 1, height) + if math.Abs(residual) > 1e-5 { + t.Fatalf("moonrise observer-height residual = %.12f degrees", residual) + } +} + +func assertRiseSetEvent(t *testing.T, name string, eventJD, dayStart float64, isRise bool, residual func(float64) float64) { + t.Helper() + if eventJD < dayStart || eventJD >= dayStart+1 { + t.Fatalf("%s %.12f is outside civil day [%.12f, %.12f)", name, eventJD, dayStart, dayStart+1) + } + const step = 1.0 / 1440 + slope := (residual(eventJD+step) - residual(eventJD-step)) / (2 * step) + if isRise && slope <= 0 { + t.Fatalf("%s slope = %.9f degrees/day, want positive", name, slope) + } + if !isRise && slope >= 0 { + t.Fatalf("%s slope = %.9f degrees/day, want negative", name, slope) + } + if value := residual(eventJD); math.Abs(value) > 1e-4 { + t.Fatalf("%s residual = %.12f degrees", name, value) + } +} + +func moonRiseSetResidualAtObserverHeight(jd, longitude, latitude, timeZone, zenithShift, height float64) float64 { + calculationJD := TD2UT(jd-timeZone/24, true) + ra, dec := HMoonTrueRaDecN(calculationJD, -1) + distanceKM := HMoonAwayN(calculationJD, -1) + topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, latitude, longitude, + jd-timeZone/24, distanceKM/angularDiameterAstronomicalUnitKM, height) + siderealTime := Limit360(ApparentSiderealTime(jd-timeZone/24)*15 + longitude) + hourAngle := Limit360(siderealTime - topocentricRA) + altitude := ArcSin(Sin(latitude)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(latitude)*Cos(hourAngle)) + residual := altitude + HeightDegreeByLat(height, latitude) + if zenithShift != 0 { + residual += RefractionFromTrueAltitude(altitude, refractionStandardPressureHPa, refractionStandardTemperatureC) + residual += angularSemidiameterArcsec(moonEquatorialRadiusKM, distanceKM) / 3600 + } + return residual +} diff --git a/basic/saturn.go b/basic/saturn.go index 61de754..45c708d 100644 --- a/basic/saturn.go +++ b/basic/saturn.go @@ -167,14 +167,14 @@ func SaturnCulminationTime(jde, lon, timezone float64) float64 { } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/saturn_events.go b/basic/saturn_events.go index 1b909e0..290ddef 100644 --- a/basic/saturn_events.go +++ b/basic/saturn_events.go @@ -64,6 +64,9 @@ func saturnRADerivativeN(jde, delta float64, n int) float64 { func saturnConjunctionFull(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := SATURN_S_PERIOD / 360 currentDelta := saturnSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -72,20 +75,29 @@ func saturnConjunctionFull(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := saturnSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (saturnSunLongitudeDelta(prevJD+0.000005, degree, true) - saturnSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } func saturnConjunction(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := SATURN_S_PERIOD / 360 currentDelta := saturnSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -94,24 +106,36 @@ func saturnConjunction(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := saturnSunLongitudeDeltaN(prevJD, degree, true, saturnEventSearchN) longitudeSlope := (saturnSunLongitudeDeltaN(prevJD+0.000005, degree, true, saturnEventSearchN) - saturnSunLongitudeDeltaN(prevJD-0.000005, degree, true, saturnEventSearchN)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= saturnPhaseCoarseTolerance { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= saturnPhaseCoarseTolerance { + converged = true break } } - for { + if !converged { + return math.NaN() + } + converged = false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := saturnSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (saturnSunLongitudeDelta(prevJD+0.000005, degree, true) - saturnSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } diff --git a/basic/solar_eclipse_path.go b/basic/solar_eclipse_path.go index 8938974..bf0bcb0 100644 --- a/basic/solar_eclipse_path.go +++ b/basic/solar_eclipse_path.go @@ -19,6 +19,17 @@ const ( solarEclipsePartialFootprintMaxBoundaryPoints = 1440 solarEclipsePartialFootprintPointTolerance = 1e-12 solarEclipsePartialFootprintIterationLimit = 10 + solarEclipsePartialFootprintTransitionIterations = 48 + solarEclipseShadowContactSearchStepDays = 10.0 / 1440.0 + solarEclipseShadowContactSearchSpanDays = 0.75 + solarEclipseShadowContactToleranceDays = 1e-9 +) + +type solarEclipseShadowKind uint8 + +const ( + solarEclipsePenumbralShadow solarEclipseShadowKind = iota + solarEclipseCentralShadow ) // SolarEclipsePathOptions 控制日食中心路径采样。 @@ -78,6 +89,9 @@ type SolarEclipsePartialFootprintOptions struct { // BoundaryPoints 是每个瞬时半影边界的角向采样点数;<=0 时使用 180。 // BoundaryPoints is the angular sample count for each instantaneous penumbral boundary; values <= 0 use 180. BoundaryPoints int + // CentralShadowStepDays 是本影/反本影瞬时足迹的时间步长,单位为日;<=0 时不计算。 + // CentralShadowStepDays is the umbral/antumbral footprint step in days; values <= 0 disable it. + CentralShadowStepDays float64 } // SolarEclipsePartialAreaOptions 是 SolarEclipsePartialFootprintOptions 的兼容别名。 @@ -104,12 +118,30 @@ type SolarEclipsePartialFootprintsResult struct { Eclipse SolarEclipseResult // Footprints 是按时间采样的瞬时半影足迹, sampled instantaneous penumbral footprints. Footprints []SolarEclipsePartialFootprint + // CentralShadowFootprints 是按时间采样的本影/反本影足迹。 + // CentralShadowFootprints are sampled umbral/antumbral footprints. + CentralShadowFootprints []SolarEclipsePartialFootprint + // P1-P4 是半影与地球的外切/内切接触点;不存在的内切点保持零值。 + // P1-P4 are external/internal penumbral contacts; absent internal contacts remain zero. + P1 SolarEclipsePathPoint + P2 SolarEclipsePathPoint + P3 SolarEclipsePathPoint + P4 SolarEclipsePathPoint + // U1-U4 是本影/反本影与地球的外切/内切接触点;不存在时保持零值。 + // U1-U4 are external/internal umbral/antumbral contacts; absent contacts remain zero. + U1 SolarEclipsePathPoint + U2 SolarEclipsePathPoint + U3 SolarEclipsePathPoint + U4 SolarEclipsePathPoint // StepDays 是实际采用的基础时间采样步长,单位为日。 // StepDays is the effective base time step in days. StepDays float64 // BoundaryPoints 是实际采用的边界角向采样点数。 // BoundaryPoints is the effective angular sample count for each boundary. BoundaryPoints int + // CentralShadowStepDays 是本影/反本影足迹的实际采样步长;0 表示未计算。 + // CentralShadowStepDays is the effective umbral/antumbral footprint step; zero means disabled. + CentralShadowStepDays float64 } // SolarEclipsePartialAreaResult 是 SolarEclipsePartialFootprintsResult 的兼容别名。 @@ -237,9 +269,10 @@ func solarEclipsePartialFootprints( options = normalizeSolarEclipsePartialFootprintOptions(options) result := solarEclipse(seedJDE, model) footprintsResult := SolarEclipsePartialFootprintsResult{ - Eclipse: result, - StepDays: options.StepDays, - BoundaryPoints: options.BoundaryPoints, + Eclipse: result, + StepDays: options.StepDays, + BoundaryPoints: options.BoundaryPoints, + CentralShadowStepDays: options.CentralShadowStepDays, } if !result.HasPartial { return footprintsResult @@ -247,6 +280,20 @@ func solarEclipsePartialFootprints( newMoonJDE := CalcMoonSHByJDE(seedJDE, 0) solver := newSolarEclipseSolver(newMoonJDE, model) + footprintsResult.P1, footprintsResult.P4, _ = solver.shadowContactPair( + result.GreatestEclipse, solarEclipsePenumbralShadow, false, + ) + footprintsResult.P2, footprintsResult.P3, _ = solver.shadowContactPair( + result.GreatestEclipse, solarEclipsePenumbralShadow, true, + ) + if result.Type != SolarEclipsePartial { + footprintsResult.U1, footprintsResult.U4, _ = solver.shadowContactPair( + result.GreatestEclipse, solarEclipseCentralShadow, false, + ) + footprintsResult.U2, footprintsResult.U3, _ = solver.shadowContactPair( + result.GreatestEclipse, solarEclipseCentralShadow, true, + ) + } footprints, stepDays := solver.partialFootprints( result.PartialBeginOnEarth, result.PartialEndOnEarth, @@ -255,6 +302,17 @@ func solarEclipsePartialFootprints( ) footprintsResult.StepDays = stepDays footprintsResult.Footprints = footprints + if options.CentralShadowStepDays > 0 && + footprintsResult.U1.JDE != 0 && footprintsResult.U4.JDE != 0 { + footprintsResult.CentralShadowFootprints, footprintsResult.CentralShadowStepDays = solver.shadowFootprints( + footprintsResult.U1.JDE, + footprintsResult.U4.JDE, + result.GreatestEclipse, + options.CentralShadowStepDays, + options.BoundaryPoints, + solarEclipseCentralShadow, + ) + } return footprintsResult } @@ -274,6 +332,11 @@ func normalizeSolarEclipsePartialFootprintOptions(options SolarEclipsePartialFoo if options.BoundaryPoints > solarEclipsePartialFootprintMaxBoundaryPoints { options.BoundaryPoints = solarEclipsePartialFootprintMaxBoundaryPoints } + if options.CentralShadowStepDays <= 0 || math.IsNaN(options.CentralShadowStepDays) || math.IsInf(options.CentralShadowStepDays, 0) { + options.CentralShadowStepDays = 0 + } else if options.CentralShadowStepDays < solarEclipsePathMinStepDays { + options.CentralShadowStepDays = solarEclipsePathMinStepDays + } return options } @@ -316,15 +379,30 @@ func (solver solarEclipseSolver) centralPathPoints( func (solver solarEclipseSolver) partialFootprints( startJDE, endJDE, greatestJDE float64, options SolarEclipsePartialFootprintOptions, +) ([]SolarEclipsePartialFootprint, float64) { + return solver.shadowFootprints( + startJDE, + endJDE, + greatestJDE, + options.StepDays, + options.BoundaryPoints, + solarEclipsePenumbralShadow, + ) +} + +func (solver solarEclipseSolver) shadowFootprints( + startJDE, endJDE, greatestJDE, requestedStepDays float64, + boundaryPoints int, + kind solarEclipseShadowKind, ) ([]SolarEclipsePartialFootprint, float64) { if endJDE < startJDE { startJDE, endJDE = endJDE, startJDE } if startJDE == 0 || endJDE == 0 || endJDE <= startJDE { - return nil, options.StepDays + return nil, requestedStepDays } - stepDays := options.StepDays + stepDays := requestedStepDays if sampleCount := int(math.Ceil((endJDE-startJDE)/stepDays)) + 1; sampleCount > solarEclipsePathMaxSampleCount { stepDays = (endJDE - startJDE) / float64(solarEclipsePathMaxSampleCount-1) } @@ -338,7 +416,7 @@ func (solver solarEclipseSolver) partialFootprints( footprints := make([]SolarEclipsePartialFootprint, 0, len(times)) for _, jd := range times { - footprint := solver.partialFootprintAt(jd, options.BoundaryPoints) + footprint := solver.shadowFootprintAt(jd, boundaryPoints, kind) if len(footprint.Boundaries) > 0 { footprints = append(footprints, footprint) } @@ -509,18 +587,164 @@ func solarEclipsePathPointFromBesselXY(jd, x, y float64, axis solarEclipseAxis) }, true } +func (solver solarEclipseSolver) shadowContactPair( + greatestJDE float64, + kind solarEclipseShadowKind, + internal bool, +) (SolarEclipsePathPoint, SolarEclipsePathPoint, bool) { + middleResidual, ok := solver.shadowContactResidual(greatestJDE, kind, internal) + if !ok || middleResidual > 0 { + return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false + } + firstJDE, firstOK := solver.shadowContactRoot(greatestJDE, -1, middleResidual, kind, internal) + lastJDE, lastOK := solver.shadowContactRoot(greatestJDE, 1, middleResidual, kind, internal) + if !firstOK || !lastOK { + return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false + } + first, firstOK := solver.shadowContactPointAt(firstJDE, kind) + last, lastOK := solver.shadowContactPointAt(lastJDE, kind) + if !firstOK || !lastOK { + return SolarEclipsePathPoint{}, SolarEclipsePathPoint{}, false + } + return first, last, true +} + +func (solver solarEclipseSolver) shadowContactRoot( + greatestJDE float64, + direction float64, + middleResidual float64, + kind solarEclipseShadowKind, + internal bool, +) (float64, bool) { + insideJDE := greatestJDE + insideResidual := middleResidual + for span := solarEclipseShadowContactSearchStepDays; span <= solarEclipseShadowContactSearchSpanDays; span += solarEclipseShadowContactSearchStepDays { + outsideJDE := greatestJDE + direction*span + outsideResidual, ok := solver.shadowContactResidual(outsideJDE, kind, internal) + if !ok { + continue + } + if outsideResidual >= 0 { + leftJDE, rightJDE := outsideJDE, insideJDE + leftResidual, rightResidual := outsideResidual, insideResidual + if leftJDE > rightJDE { + leftJDE, rightJDE = rightJDE, leftJDE + leftResidual, rightResidual = rightResidual, leftResidual + } + for rightJDE-leftJDE > solarEclipseShadowContactToleranceDays { + middleJDE := (leftJDE + rightJDE) / 2 + residual, valid := solver.shadowContactResidual(middleJDE, kind, internal) + if !valid { + return 0, false + } + if (residual >= 0) == (leftResidual >= 0) { + leftJDE, leftResidual = middleJDE, residual + } else { + rightJDE, rightResidual = middleJDE, residual + } + } + return (leftJDE + rightJDE) / 2, true + } + insideJDE, insideResidual = outsideJDE, outsideResidual + } + return 0, false +} + +func (solver solarEclipseSolver) shadowContactResidual( + jd float64, + kind solarEclipseShadowKind, + internal bool, +) (float64, bool) { + moon := solver.besselMoonAt(jd) + distanceSquared := moon[0]*moon[0] + moon[1]*moon[1] + if distanceSquared <= 0 { + return 0, false + } + radius := solver.shadowRadiusAt(moon[2], kind) + if radius <= 0 { + return 0, false + } + earthRadius := 1 - (1/solarEclipseEarthPolarRatioSquared-1)*moon[1]*moon[1]/distanceSquared/2 + limit := earthRadius + radius + if internal { + limit = earthRadius - radius + } + if limit <= 0 { + return 0, false + } + return math.Sqrt(distanceSquared) - limit, true +} + +func (solver solarEclipseSolver) shadowContactPointAt( + jd float64, + kind solarEclipseShadowKind, +) (SolarEclipsePathPoint, bool) { + moon := solver.besselMoonAt(jd) + distance := math.Hypot(moon[0], moon[1]) + if distance <= 0 || solver.shadowRadiusAt(moon[2], kind) <= 0 { + return SolarEclipsePathPoint{}, false + } + axis := solver.besselAxisAt(jd) + unitX, unitY := moon[0]/distance, moon[1]/distance + insideScale, outsideScale := 0.0, 1.1 + var intersection solarEclipseLineIntersection + for iteration := 0; iteration < 48; iteration++ { + scale := (insideScale + outsideScale) / 2 + candidate := solarEclipseLineEar2( + scale*unitX, scale*unitY, 2, + scale*unitX, scale*unitY, 0, + solarEclipseEarthPolarRatio, 1, axis, + ) + if candidate.valid { + insideScale = scale + intersection = candidate + } else { + outsideScale = scale + } + } + if !intersection.valid { + return SolarEclipsePathPoint{}, false + } + longitude, latitude := solarEclipseIntersectionGeodetic(intersection, axis) + sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jd, longitude, latitude, axis.gst) + return SolarEclipsePathPoint{ + JDE: jd, + Longitude: longitude, + Latitude: latitude, + SunAltitude: sunAltitudeRad / rad, + }, true +} + +func (solver solarEclipseSolver) shadowRadiusAt(moonBesselZ float64, kind solarEclipseShadowKind) float64 { + radii := solver.shadowRadiiAt(moonBesselZ) + if kind == solarEclipseCentralShadow { + return radii.absUmbraRadius + } + return radii.penumbraRadius +} + func (solver solarEclipseSolver) partialFootprintAt(jd float64, boundaryPoints int) SolarEclipsePartialFootprint { + return solver.shadowFootprintAt(jd, boundaryPoints, solarEclipsePenumbralShadow) +} + +func (solver solarEclipseSolver) shadowFootprintAt( + jd float64, + boundaryPoints int, + kind solarEclipseShadowKind, +) SolarEclipsePartialFootprint { moon := solver.besselMoonAt(jd) axis := solver.besselAxisAt(jd) samples := make([]solarEclipsePartialBoundarySample, boundaryPoints) for i := range samples { angle := 2 * math.Pi * float64(i) / float64(boundaryPoints) - point, ok := solver.partialFootprintPointAt(jd, moon, axis, angle) + point, ok := solver.shadowFootprintPointAt(jd, moon, axis, angle, kind) samples[i] = solarEclipsePartialBoundarySample{ point: point, ok: ok, + angle: angle, } } + samples = solver.refineShadowFootprintTransitions(jd, moon, axis, samples, kind) boundaries, closed := solarEclipsePartialBoundarySegments(samples) return SolarEclipsePartialFootprint{ @@ -533,17 +757,83 @@ func (solver solarEclipseSolver) partialFootprintAt(jd float64, boundaryPoints i type solarEclipsePartialBoundarySample struct { point SolarEclipsePathPoint ok bool + angle float64 } -func (solver solarEclipseSolver) partialFootprintPointAt( +func (solver solarEclipseSolver) refineShadowFootprintTransitions( + jd float64, + moon [3]float64, + axis solarEclipseAxis, + samples []solarEclipsePartialBoundarySample, + kind solarEclipseShadowKind, +) []solarEclipsePartialBoundarySample { + if len(samples) < 2 { + return samples + } + result := make([]solarEclipsePartialBoundarySample, 0, len(samples)+4) + for index, sample := range samples { + result = append(result, sample) + next := samples[(index+1)%len(samples)] + if sample.ok == next.ok { + continue + } + nextAngle := next.angle + if index == len(samples)-1 { + nextAngle += 2 * math.Pi + } + refined := solver.refineShadowFootprintTransition(jd, moon, axis, sample, next, nextAngle, kind) + result = append(result, refined) + } + return result +} + +func (solver solarEclipseSolver) refineShadowFootprintTransition( + jd float64, + moon [3]float64, + axis solarEclipseAxis, + first, second solarEclipsePartialBoundarySample, + secondAngle float64, + kind solarEclipseShadowKind, +) solarEclipsePartialBoundarySample { + leftAngle := first.angle + rightAngle := secondAngle + leftOK := first.ok + best := first + if second.ok { + best = second + best.angle = secondAngle + } + for iteration := 0; iteration < solarEclipsePartialFootprintTransitionIterations; iteration++ { + middleAngle := (leftAngle + rightAngle) / 2 + evaluationAngle := math.Mod(middleAngle, 2*math.Pi) + point, ok := solver.shadowFootprintPointAt(jd, moon, axis, evaluationAngle, kind) + middle := solarEclipsePartialBoundarySample{point: point, ok: ok, angle: middleAngle} + if ok { + best = middle + } + if ok == leftOK { + leftAngle = middleAngle + } else { + rightAngle = middleAngle + } + if rightAngle-leftAngle <= solarEclipsePartialFootprintPointTolerance { + break + } + } + best.angle = math.Mod(best.angle, 2*math.Pi) + return best +} + +func (solver solarEclipseSolver) shadowFootprintPointAt( jd float64, moon [3]float64, axis solarEclipseAxis, angle float64, + kind solarEclipseShadowKind, ) (SolarEclipsePathPoint, bool) { cosAngle := math.Cos(angle) sinAngle := math.Sin(angle) - radius := solver.shadowRadiiAt(moon[2]).penumbraRadius + radius := solver.shadowRadiusAt(moon[2], kind) if radius <= 0 { return SolarEclipsePathPoint{}, false } @@ -567,7 +857,7 @@ func (solver solarEclipseSolver) partialFootprintPointAt( return SolarEclipsePathPoint{}, false } - nextRadius := solver.shadowRadiiAt(moon[2] - intersection.r2).penumbraRadius + nextRadius := solver.shadowRadiusAt(moon[2]-intersection.r2, kind) if nextRadius <= 0 { return SolarEclipsePathPoint{}, false } @@ -608,8 +898,10 @@ func (solver solarEclipseSolver) partialFootprintPointAt( func solarEclipsePartialBoundarySegments(samples []solarEclipsePartialBoundarySample) ([][]SolarEclipsePathPoint, bool) { segments := make([][]SolarEclipsePathPoint, 0, 2) var current []SolarEclipsePathPoint + allSamplesValid := len(samples) > 0 for _, sample := range samples { if !sample.ok { + allSamplesValid = false segments = appendSolarEclipsePartialSegment(segments, current) current = nil continue @@ -623,11 +915,20 @@ func solarEclipsePartialBoundarySegments(samples []solarEclipsePartialBoundarySa segments = appendSolarEclipsePartialSegment(segments, current) segments = mergeSolarEclipsePartialWrapSegment(segments, samples) - if len(segments) == 1 && len(segments[0]) > 2 && !solarEclipsePathCrossesAntimeridian(segments[0][len(segments[0])-1], segments[0][0]) { - segments[0] = append(segments[0], segments[0][0]) - return segments, true + if !allSamplesValid { + return segments, false } - return segments, false + totalPoints := 0 + for _, segment := range segments { + totalPoints += len(segment) + } + if totalPoints < 3 { + return segments, false + } + if len(segments) == 1 && !solarEclipsePathCrossesAntimeridian(segments[0][len(segments[0])-1], segments[0][0]) { + segments[0] = append(segments[0], segments[0][0]) + } + return segments, true } func appendSolarEclipsePartialSegment( diff --git a/basic/solar_eclipse_path_test.go b/basic/solar_eclipse_path_test.go index 1133e1c..623ac0d 100644 --- a/basic/solar_eclipse_path_test.go +++ b/basic/solar_eclipse_path_test.go @@ -3,6 +3,7 @@ package basic import ( "math" "testing" + "time" ) func TestSolarEclipseCentralPathMatchesGlobalGreatest(t *testing.T) { @@ -128,6 +129,9 @@ func TestSolarEclipsePartialFootprintsIncludeGreatest(t *testing.T) { if !foundGreatest { t.Fatalf("partial footprints should include greatest eclipse JDE %.12f", global.GreatestEclipse) } + if footprints.Footprints[0].Closed { + t.Fatal("grazing first footprint must remain open for horizon closure") + } } func TestSolarEclipsePartialFootprintsWorkForPartialOnlyEclipse(t *testing.T) { @@ -147,6 +151,101 @@ func TestSolarEclipsePartialFootprintsWorkForPartialOnlyEclipse(t *testing.T) { } } +func TestSolarEclipseShadowContactsAgainstNASA2012Baseline(t *testing.T) { + result := SolarEclipsePartialFootprints( + solarEclipseUTToTTJDE(time.Date(2012, 5, 20, 0, 0, 0, 0, time.UTC)), + SolarEclipsePartialFootprintOptions{ + StepDays: 30.0 / 1440.0, + BoundaryPoints: 72, + CentralShadowStepDays: 10.0 / 1440.0, + }, + ) + baseline := []struct { + name string + point SolarEclipsePathPoint + want time.Time + }{ + {"P1", result.P1, time.Date(2012, 5, 20, 20, 56, 7, 0, time.UTC)}, + {"P4", result.P4, time.Date(2012, 5, 21, 2, 49, 21, 500000000, time.UTC)}, + {"U1", result.U1, time.Date(2012, 5, 20, 22, 6, 16, 600000000, time.UTC)}, + {"U2", result.U2, time.Date(2012, 5, 20, 22, 11, 46, 400000000, time.UTC)}, + {"U3", result.U3, time.Date(2012, 5, 21, 1, 33, 42, 800000000, time.UTC)}, + {"U4", result.U4, time.Date(2012, 5, 21, 1, 39, 11, 200000000, time.UTC)}, + } + for _, contact := range baseline { + if contact.point.JDE == 0 { + t.Fatalf("%s contact is absent", contact.name) + } + assertLocalSolarEclipseJDEClose( + t, + contact.name, + contact.point.JDE, + solarEclipseUTToTTJDE(contact.want), + 3*time.Second, + ) + if math.Abs(contact.point.SunAltitude) > 0.01 { + t.Fatalf("%s Sun altitude = %.9f degrees, want horizon contact", contact.name, contact.point.SunAltitude) + } + } + if result.P2.JDE != 0 || result.P3.JDE != 0 { + t.Fatalf("2012 eclipse unexpectedly has P2/P3 contacts: P2=%+v P3=%+v", result.P2, result.P3) + } + if len(result.CentralShadowFootprints) == 0 { + t.Fatal("expected sampled central-shadow footprints") + } + if math.Abs(result.CentralShadowStepDays-10.0/1440.0) > 1e-12 { + t.Fatalf("central shadow step = %.12f days, want ten minutes", result.CentralShadowStepDays) + } + for _, footprint := range result.CentralShadowFootprints { + if len(footprint.Boundaries) == 0 { + t.Fatalf("central-shadow footprint at %.12f has no boundary", footprint.JDE) + } + } +} + +func TestSolarEclipseShadowContactsIncludeP2P3WhenPenumbraEntersEarthDisk(t *testing.T) { + result := SolarEclipsePartialFootprints( + solarEclipseUTToTTJDE(time.Date(2024, 4, 8, 0, 0, 0, 0, time.UTC)), + SolarEclipsePartialFootprintOptions{StepDays: 30.0 / 1440.0, BoundaryPoints: 36}, + ) + for name, contacts := range map[string][]SolarEclipsePathPoint{ + "penumbral": {result.P1, result.P2, result.P3, result.P4}, + "central": {result.U1, result.U2, result.U3, result.U4}, + } { + for index, contact := range contacts { + if contact.JDE == 0 { + t.Fatalf("%s contact %d is absent", name, index) + } + if index > 0 && !(contacts[index-1].JDE < contact.JDE) { + t.Fatalf("%s contacts out of order at %d: %.12f >= %.12f", name, index, contacts[index-1].JDE, contact.JDE) + } + } + } + if !(result.P1.JDE < result.U1.JDE && result.U4.JDE < result.P4.JDE) { + t.Fatalf("central shadow contacts must lie inside partial phase: P1=%v U1=%v U4=%v P4=%v", + result.P1.JDE, result.U1.JDE, result.U4.JDE, result.P4.JDE) + } + if result.CentralShadowFootprints != nil || result.CentralShadowStepDays != 0 { + t.Fatal("central-shadow footprints must remain disabled by default") + } +} + +func TestSolarEclipsePartialBoundarySegmentsRemainClosedAcrossAntimeridian(t *testing.T) { + samples := []solarEclipsePartialBoundarySample{ + {point: SolarEclipsePathPoint{Longitude: 170, Latitude: 20}, ok: true}, + {point: SolarEclipsePathPoint{Longitude: -170, Latitude: 25}, ok: true}, + {point: SolarEclipsePathPoint{Longitude: -160, Latitude: 10}, ok: true}, + {point: SolarEclipsePathPoint{Longitude: 160, Latitude: 5}, ok: true}, + } + boundaries, closed := solarEclipsePartialBoundarySegments(samples) + if !closed { + t.Fatal("complete spherical boundary must remain closed after antimeridian splitting") + } + if len(boundaries) < 2 { + t.Fatalf("expected antimeridian split, got %d boundary segment(s)", len(boundaries)) + } +} + func TestSolarEclipsePartialFootprintsNoEvent(t *testing.T) { footprints := SolarEclipsePartialFootprints(JDECalc(2023, 5, 15), SolarEclipsePartialFootprintOptions{}) @@ -180,8 +279,11 @@ func assertSolarEclipseFootprintClosedFlag(t *testing.T, footprint SolarEclipseP if !footprint.Closed { return } - if len(footprint.Boundaries) != 1 { - t.Fatalf("closed footprint should have one boundary: got %d", len(footprint.Boundaries)) + if len(footprint.Boundaries) == 0 { + t.Fatal("closed footprint has no boundaries") + } + if len(footprint.Boundaries) > 1 { + return } boundary := footprint.Boundaries[0] if len(boundary) < 2 { diff --git a/basic/solar_terms.go b/basic/solar_terms.go index 304d7d7..17ca996 100644 --- a/basic/solar_terms.go +++ b/basic/solar_terms.go @@ -79,16 +79,14 @@ func GetJQTime(year, angle int) float64 { // Newton-Raphson iteration to find precise Julian date currentJD := initialJD - for { - previousJD := currentJD + var ok bool + currentJD, ok = eventNewtonRefine(currentJD, 0.00001, func(previousJD float64) float64 { errorValue := JQLospec(previousJD, targetAngle) - targetAngle derivative := (JQLospec(previousJD+0.000005, targetAngle) - JQLospec(previousJD-0.000005, targetAngle)) / 0.00001 - currentJD = previousJD - errorValue/derivative - - // Check for convergence - if math.Abs(currentJD-previousJD) <= 0.00001 { - break - } + return errorValue / derivative + }) + if !ok { + return math.NaN() } // Convert to UT and return diff --git a/basic/star.go b/basic/star.go index 3ffaaec..da9574b 100644 --- a/basic/star.go +++ b/basic/star.go @@ -130,6 +130,9 @@ func StarSetTime(jde, ra, dec, lon, lat, height, timezone float64, aero bool) (f } func StarRiseSetTime(jde, ra, dec, lon, lat, height, timezone float64, aero, isRise bool) (float64, error) { + if !isFiniteFloat(jde) || !isFiniteFloat(ra) || !isFiniteFloat(dec) || !isFiniteFloat(lon) || !isFiniteFloat(lat) || !isFiniteFloat(height) || !isFiniteFloat(timezone) { + return 0, ErrInvalidObservationInput + } //jde 世界时,非力学时,当地时区 0时,无需转换力学时 //ra,dec 瞬时天球座标,非J2000等时间天球坐标 jde = math.Floor(jde) + 0.5 @@ -148,19 +151,22 @@ func StarRiseSetTime(jde, ra, dec, lon, lat, height, timezone float64, aero, isR } else { estimateJD = sct + ArcCos(tmp)/15.0/24.0 } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := StarHeight(prevJD, ra, dec, lon, lat, timezone) - targetAltitude stDegreep := (StarHeight(prevJD+0.000005, ra, dec, lon, lat, timezone) - StarHeight(prevJD-0.000005, ra, dec, lon, lat, timezone)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD, nil } func StarCulminationTime(jde, ra, lon, timezone float64) float64 { + if !isFiniteFloat(jde) || !isFiniteFloat(ra) || !isFiniteFloat(lon) || !isFiniteFloat(timezone) { + return math.NaN() + } //jde 世界时,非力学时,当地时区 0时,无需转换力学时 //ra,dec 瞬时天球座标,非J2000等时间天球坐标 jde = math.Floor(jde) + 0.5 @@ -172,14 +178,14 @@ func StarCulminationTime(jde, ra, lon, timezone float64) float64 { } return ha } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := limitStarHA(prevJD, ra, lon, timezone) - 360 stDegreep := (limitStarHA(prevJD+0.000005, ra, lon, timezone) - limitStarHA(prevJD-0.000005, ra, lon, timezone)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/star_catalog.go b/basic/star_catalog.go index c707fef..253a3e2 100644 --- a/basic/star_catalog.go +++ b/basic/star_catalog.go @@ -4,13 +4,14 @@ import ( "bytes" "errors" "fmt" + "math" "strconv" "strings" "sync" "time" ) -// this file contains bright 9100 stars +// 本文件包含约 9100 颗亮星 / this file contains bright 9100 stars // 9100颗亮星列表 type InnerStarData struct { @@ -20,8 +21,8 @@ type InnerStarData struct { Ra float64 //Ra J2000;J2000历元赤经 Dec float64 //De J2000;J2000历元赤纬 Mag float64 //视星等 - PmRA float64 //赤经年自行 - PmDec float64 //赤纬年自行 + PmRA float64 //赤经投影年自行 cos(赤纬)*d赤经/dt,单位角秒/年 + PmDec float64 //赤纬年自行,单位角秒/年 RadVel float64 //径向速度 km/s RotVel float64 //自行速度 km/s Pc float64 //秒差距 @@ -252,9 +253,15 @@ func StarDataByHR(hr int) (StarData, error) { } func (s InnerStarData) RaDecByJde(jde float64) (float64, float64) { - //计算自行 + // BSC 的 pmRA 是投影自行 cos(Dec)*dRA/dt,而不是 dRA/dt / BSC pmRA is the projected motion cos(Dec)*dRA/dt, not dRA/dt. year := ((jde - 2451545.0) / 365.2422) - return Precess(s.Ra+(year*s.PmRA/3600), s.Dec+(year*s.PmDec/3600), 2451545.0, jde) + dec := s.Dec + year*s.PmDec/3600 + cosDec := math.Cos(s.Dec * math.Pi / 180) + ra := s.Ra + if math.Abs(cosDec) > 1e-12 { + ra += year * s.PmRA / (3600 * cosDec) + } + return Precess(ra, dec, 2451545.0, jde) } func (s StarData) RaDecByDate(date time.Time) (float64, float64) { diff --git a/basic/star_catalog_cache_test.go b/basic/star_catalog_cache_test.go index 5b00593..0768395 100644 --- a/basic/star_catalog_cache_test.go +++ b/basic/star_catalog_cache_test.go @@ -53,6 +53,7 @@ func TestStarDataRegressionSamples(t *testing.T) { {15, 677, "21Alp And", "壁宿二", "", "仙女座α", "Alpheratz", "Andromeda", "仙女座", 2.097083333333, 29.090555555556, 2.06, 0.136, -0.163}, {424, 11767, "1Alp UMi", "勾陈一", "北极星", "小熊座α", "Polaris", "UrsaMinor", "小熊座", 37.952916666667, 89.264166666667, 2.02, 0.038, -0.015}, {2491, 32349, "9Alp CMa", "天狼", "", "大犬座α", "Sirius", "CanisMajor", "大犬座", 101.287083333333, -16.716111111111, -1.46, -0.553, -1.205}, + {4799, 61558, "25 Vir", "进贤增九", "", "室女座25", "", "Virgo", "室女座", 189.1975, -5.831944444444, 5.87, -0.028, -0.018}, {7001, 91262, "3Alp Lyr", "织女一", "织女", "天琴座α", "Vega", "Lyra", "天琴座", 279.234583333333, 38.783611111111, 0.03, 0.202, 0.286}, {9100, 330, "9 Cas", "", "", "", "", "", "", 1.056666666667, 62.287777777778, 5.88, -0.004, 0.006}, } @@ -90,6 +91,7 @@ func TestStarDataByChineseAlias(t *testing.T) { {"北极", 424, 11767}, {"北极星", 424, 11767}, {"织女", 7001, 91262}, + {"进贤增九", 4799, 61558}, } for _, tc := range tests { got, err := StarDataByChinese(tc.name) diff --git a/basic/star_catalog_data.go b/basic/star_catalog_data.go index 0fa7bb7..9a8fc28 100644 --- a/basic/star_catalog_data.go +++ b/basic/star_catalog_data.go @@ -9,15 +9,23 @@ import ( "io" ) -// star_catalog.dat layout after gzip decompression: -// magic[8] | version[1] | rawDataLen[4] | rawData | stringCount[2] | -// repeated(stringLen[uvarint] + stringBytes) | -// maxHR[2] | repeated(maxHR * 6 * stringIndex[2]) | repeated(maxHR * hip[4]). +// star_catalog.dat gzip 解压后的布局 / star_catalog.dat layout after gzip decompression: +// magic[8] | version[1] | rawDataLen[4] | rawData | stringCount[2](字符串计数) | +// repeated(stringLen[uvarint] + stringBytes)(字符串表) | +// maxHR[2] | repeated(maxHR * 6 * stringIndex[2]) | repeated(maxHR * hip[4])(星表记录)。 const starCatalogMagic = "STRCAT01" //go:embed star_catalog.dat var starCatalogCompressed []byte +// supplementalStarDetails 保存嵌入载荷没有对应命名条目的星表元数据 / supplementalStarDetails contains catalog metadata that is maintained in +// 源码形式 / source form when the embedded payload has no corresponding named entry. +// 六个字段按编码顺序排列:中文名、别名、拜耳命名、通用名、中文星座和 IAU 星座 / The six fields follow the encoded detail order: Chinese name, alias, Bayer +// 命名、通用名、中文星座和 IAU 星座 / designation, common name, Chinese constellation, and IAU constellation. +var supplementalStarDetails = map[uint16][]string{ + 4799: {"进贤增九", "", "室女座25", "", "室女座", "Virgo"}, +} + func initStarCatalogData() []byte { reader, err := gzip.NewReader(bytes.NewReader(starCatalogCompressed)) if err != nil { @@ -32,6 +40,9 @@ func initStarCatalogData() []byte { if err != nil { panic(err) } + for hr, record := range supplementalStarDetails { + detail[hr] = record + } hr2detail = detail hr2hip = hip return data diff --git a/basic/star_catalog_test.go b/basic/star_catalog_test.go index b38cd7d..996a1db 100644 --- a/basic/star_catalog_test.go +++ b/basic/star_catalog_test.go @@ -60,3 +60,14 @@ func TestGetRaDecByDate(t *testing.T) { t.Fatal("unexpected empty formatted catalog coordinates") } } + +func TestRaDecByJdeUsesProjectedRightAscensionMotion(t *testing.T) { + star := InnerStarData{Ra: 10, Dec: 60, PmRA: 3600, PmDec: 0} + jde := 2451545.0 + 365.2422 + ra, dec := star.RaDecByJde(jde) + // 投影坐标每年 1 度在 Dec=60 度时对应赤经每年 2 度 / 1 deg/year in the projected coordinate is 2 deg/year in RA at Dec=60. + wantRA, wantDec := Precess(12, 60, 2451545.0, jde) + if math.Abs(signedAngleDifference(ra, wantRA)) > 1e-8 || math.Abs(dec-wantDec) > 1e-8 { + t.Fatalf("position = %.12f %.12f, want %.12f %.12f", ra, dec, wantRA, wantDec) + } +} diff --git a/basic/sun.go b/basic/sun.go index 35bac77..6b38a3d 100644 --- a/basic/sun.go +++ b/basic/sun.go @@ -148,7 +148,17 @@ func HSunApparentRaDec(jd float64) (float64, float64) { } func HSunApparentRaDecN(jd float64, n int) (float64, float64) { - return LoBoToRaDec(jd, HSunApparentLoN(jd, n), HSunTrueBoN(jd, n)) + ra, dec, _ := hSunApparentRaDecDistanceN(jd, n) + return ra, dec +} + +func hSunApparentRaDecDistanceN(jd float64, n int) (ra, dec, distanceAU float64) { + trueLongitude := HSunTrueLoN(jd, n) + trueLatitude := HSunTrueBoN(jd, n) + distanceAU = EarthAwayN(jd, n) + apparentLongitude := trueLongitude + Nutation2000Bi(jd) - 20.49552/distanceAU/3600 + ra, dec = LoBoToRaDec(jd, apparentLongitude, trueLatitude) + return ra, dec, distanceAU } func HSunApparentRa(jd float64) float64 { // '太阳视赤经 diff --git a/basic/sun_observation.go b/basic/sun_observation.go index 3fa4459..bd30d52 100644 --- a/basic/sun_observation.go +++ b/basic/sun_observation.go @@ -49,14 +49,14 @@ func EveningTwilight(jd, lon, lat, tz, targetAltitude float64) (float64, error) } } estimateJD := sundown - 5.00/24.00/60.00 - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := SunHeight(prevJD, lon, lat, localTimeZone) - targetAltitude stDegreep := (SunHeight(prevJD+0.000005, lon, lat, localTimeZone) - SunHeight(prevJD-0.000005, lon, lat, localTimeZone)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) < 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimeZone/24 + tz/24, nil } @@ -88,14 +88,14 @@ func EveningTwilightN(jd, lon, lat, tz, targetAltitude float64, n int) (float64, } } estimateJD := sundown - 5.00/24.00/60.00 - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := SunHeightN(prevJD, lon, lat, localTimeZone, n) - targetAltitude stDegreep := (SunHeightN(prevJD+0.000005, lon, lat, localTimeZone, n) - SunHeightN(prevJD-0.000005, lon, lat, localTimeZone, n)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) < 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimeZone/24 + tz/24, nil } @@ -134,15 +134,14 @@ func MorningTwilight(jd, lon, lat, tz, targetAltitude float64) (float64, error) } estimateJD := sunrise - 5.0/(24.0*60.0) - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { heightDiff := SunHeight(prevJD, lon, lat, localTimeZone) - targetAltitude heightDerivative := (SunHeight(prevJD+0.000005, lon, lat, localTimeZone) - SunHeight(prevJD-0.000005, lon, lat, localTimeZone)) / 0.00001 - estimateJD = prevJD - heightDiff/heightDerivative - - if math.Abs(estimateJD-prevJD) < 0.00001 { - break - } + return heightDiff / heightDerivative + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimeZone/24 + tz/24, nil @@ -174,15 +173,14 @@ func MorningTwilightN(jd, lon, lat, tz, targetAltitude float64, n int) (float64, } estimateJD := sunrise - 5.0/(24.0*60.0) - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { heightDiff := SunHeightN(prevJD, lon, lat, localTimeZone, n) - targetAltitude heightDerivative := (SunHeightN(prevJD+0.000005, lon, lat, localTimeZone, n) - SunHeightN(prevJD-0.000005, lon, lat, localTimeZone, n)) / 0.00001 - estimateJD = prevJD - heightDiff/heightDerivative - - if math.Abs(estimateJD-prevJD) < 0.00001 { - break - } + return heightDiff / heightDerivative + }) + if !ok { + return 0, ErrInvalidObservationInput } return estimateJD - localTimeZone/24 + tz/24, nil @@ -209,6 +207,46 @@ func SunTimeAngleN(jd, lon, lat, tz float64, n int) float64 { return timeangle } +type sunObservationState struct { + altitude float64 + distanceAU float64 +} + +func sunObservationStateN(jd, lon, lat, tz float64, n int) sunObservationState { + calculationJD := jd - tz/24.0 + tt := TD2UT(calculationJD, true) + siderealTime := Limit360(ApparentSiderealTime(calculationJD)*15 + lon) + ra, dec, distanceAU := hSunApparentRaDecDistanceN(tt, n) + hourAngle := Limit360(siderealTime - ra) + altitudeSine := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle) + return sunObservationState{ + altitude: ArcSin(altitudeSine), + distanceAU: distanceAU, + } +} + +func sunRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 { + state := sunObservationStateN(jd, longitude, latitude, timeZone, n) + // 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon. + residual := state.altitude + HeightDegreeByLat(height, latitude) + if zenithShift != 0 { + residual += RefractionFromTrueAltitude(state.altitude, refractionStandardPressureHPa, refractionStandardTemperatureC) + residual += angularSemidiameterFromAU(sunEquatorialRadiusKM, state.distanceAU) / 3600 + } + return residual +} + +func sunRiseSetOnCivilDay(candidate, slope, civilDayStart, longitude, latitude, requestedTimeZone, + localTimeZone, zenithShift, height float64, isSunrise bool, n int, fallbackErr error) (float64, error) { + if eventRiseSetCandidateValid(candidate, civilDayStart, slope, isSunrise) { + return candidate, nil + } + return eventDirectionalRiseSetSearch(civilDayStart, isSunrise, fallbackErr, func(outputJD float64) float64 { + localJD := outputJD + localTimeZone/24 - requestedTimeZone/24 + return sunRiseSetResidual(localJD, longitude, latitude, localTimeZone, zenithShift, height, n) + }) +} + // GetSunRiseTime 精确计算日出时间,传入当日0时JDE func GetSunRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) { return calculateSunRiseSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height, true) @@ -229,6 +267,10 @@ func GetSunSetTimeN(julianDay, longitude, latitude, timeZone, zenithShift, heigh // calculateSunRiseSetTime 统一的日出日落计算函数 func calculateSunRiseSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64, isSunrise bool) (float64, error) { + if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) { + return 0, ErrInvalidObservationInput + } + civilDayStart := math.Floor(julianDay) + 0.5 julianDay = math.Floor(julianDay) + 1.5 naturalTimeZone := math.Round(longitude / 15) sunAngle := StandardAltitudeSun(zenithShift, height, latitude) @@ -237,34 +279,44 @@ func calculateSunRiseSetTime(julianDay, longitude, latitude, timeZone, zenithShi solarNoonTime := CulminationTime(julianDay, longitude, naturalTimeZone) // 检查极夜极昼条件 - if err := checkPolarConditions(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, isSunrise); err != nil { - return 0, err + if err := checkPolarConditions(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height, isSunrise); err != nil { + return sunRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude, timeZone, + naturalTimeZone, zenithShift, height, isSunrise, -1, err) } // 计算初始估算时间 - initialTime := calculateInitialSunTime(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, isSunrise) + initialTime := calculateInitialSunTime(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, zenithShift, height, isSunrise) // 牛顿-拉夫逊迭代求精确解 - return sunRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, naturalTimeZone, sunAngle, timeZone), nil + result, slope := sunRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, naturalTimeZone, zenithShift, height, timeZone) + return sunRiseSetOnCivilDay(result, slope, civilDayStart, longitude, latitude, timeZone, + naturalTimeZone, zenithShift, height, isSunrise, -1, nil) } func calculateSunRiseSetTimeN(julianDay, longitude, latitude, timeZone, zenithShift, height float64, isSunrise bool, n int) (float64, error) { + if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) { + return 0, ErrInvalidObservationInput + } + civilDayStart := math.Floor(julianDay) + 0.5 julianDay = math.Floor(julianDay) + 1.5 naturalTimeZone := math.Round(longitude / 15) sunAngle := StandardAltitudeSun(zenithShift, height, latitude) solarNoonTime := CulminationTimeN(julianDay, longitude, naturalTimeZone, n) - if err := checkPolarConditionsN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, isSunrise, n); err != nil { - return 0, err + if err := checkPolarConditionsN(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height, isSunrise, n); err != nil { + return sunRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude, timeZone, + naturalTimeZone, zenithShift, height, isSunrise, n, err) } - initialTime := calculateInitialSunTimeN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, isSunrise, n) - return sunRiseSetNewtonRaphsonIterationN(initialTime, longitude, latitude, naturalTimeZone, sunAngle, timeZone, n), nil + initialTime := calculateInitialSunTimeN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, zenithShift, height, isSunrise, n) + result, slope := sunRiseSetNewtonRaphsonIterationN(initialTime, longitude, latitude, naturalTimeZone, zenithShift, height, timeZone, n) + return sunRiseSetOnCivilDay(result, slope, civilDayStart, longitude, latitude, timeZone, + naturalTimeZone, zenithShift, height, isSunrise, n, nil) } // checkPolarConditions 检查极夜极昼条件 -func checkPolarConditions(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle float64, isSunrise bool) error { - if SunHeight(solarNoonTime, longitude, latitude, naturalTimeZone) < sunAngle { +func checkPolarConditions(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height float64, isSunrise bool) error { + if sunRiseSetResidual(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height, -1) < 0 { return ErrNeverRise } @@ -273,15 +325,15 @@ func checkPolarConditions(solarNoonTime, longitude, latitude, naturalTimeZone, s checkTime = solarNoonTime - 0.5 } - if SunHeight(checkTime, longitude, latitude, naturalTimeZone) > sunAngle { + if sunRiseSetResidual(checkTime, longitude, latitude, naturalTimeZone, zenithShift, height, -1) > 0 { return ErrNeverSet } return nil } -func checkPolarConditionsN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle float64, isSunrise bool, n int) error { - if SunHeightN(solarNoonTime, longitude, latitude, naturalTimeZone, n) < sunAngle { +func checkPolarConditionsN(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height float64, isSunrise bool, n int) error { + if sunRiseSetResidual(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height, n) < 0 { return ErrNeverRise } @@ -290,7 +342,7 @@ func checkPolarConditionsN(solarNoonTime, longitude, latitude, naturalTimeZone, checkTime = solarNoonTime - 0.5 } - if SunHeightN(checkTime, longitude, latitude, naturalTimeZone, n) > sunAngle { + if sunRiseSetResidual(checkTime, longitude, latitude, naturalTimeZone, zenithShift, height, n) > 0 { return ErrNeverSet } @@ -298,7 +350,7 @@ func checkPolarConditionsN(solarNoonTime, longitude, latitude, naturalTimeZone, } // calculateInitialSunTime 计算日出日落的初始估算时间 -func calculateInitialSunTime(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle float64, isSunrise bool) float64 { +func calculateInitialSunTime(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, zenithShift, height float64, isSunrise bool) float64 { // 使用球面三角法计算: (sin(ho)-sin(φ)*sin(δ))/(cos(φ)*cos(δ)) apparentDeclination := HSunApparentDec(solarNoonTime) cosHourAngle := (Sin(sunAngle) - Sin(apparentDeclination)*Sin(latitude)) / (Cos(apparentDeclination) * Cos(latitude)) @@ -318,11 +370,11 @@ func calculateInitialSunTime(solarNoonTime, longitude, latitude, naturalTimeZone } } else { // 使用迭代逼近法(极地条件) - return iterativeApproach(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, isSunrise) + return iterativeApproach(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height, isSunrise) } } -func calculateInitialSunTimeN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle float64, isSunrise bool, n int) float64 { +func calculateInitialSunTimeN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, zenithShift, height float64, isSunrise bool, n int) float64 { apparentDeclination := HSunApparentDecN(solarNoonTime, n) cosHourAngle := (Sin(sunAngle) - Sin(apparentDeclination)*Sin(latitude)) / (Cos(apparentDeclination) * Cos(latitude)) @@ -339,11 +391,11 @@ func calculateInitialSunTimeN(solarNoonTime, longitude, latitude, naturalTimeZon return solarNoonTime + hourAngle/24 + timeOffset } - return iterativeApproachN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle, isSunrise, n) + return iterativeApproachN(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height, isSunrise, n) } // iterativeApproach 迭代逼近法计算(用于极地等特殊条件) -func iterativeApproach(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle float64, isSunrise bool) float64 { +func iterativeApproach(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height float64, isSunrise bool) float64 { estimatedTime := solarNoonTime stepSize := 15.0 / 60.0 / 24.0 // 15分钟步长 if isSunrise { @@ -351,14 +403,14 @@ func iterativeApproach(solarNoonTime, longitude, latitude, naturalTimeZone, sunA } const maxIterations = 48 - for i := 0; i < maxIterations && LowSunHeight(estimatedTime, longitude, latitude, naturalTimeZone) > sunAngle; i++ { + for i := 0; i < maxIterations && sunRiseSetResidual(estimatedTime, longitude, latitude, naturalTimeZone, zenithShift, height, -1) > 0; i++ { estimatedTime += stepSize } return estimatedTime } -func iterativeApproachN(solarNoonTime, longitude, latitude, naturalTimeZone, sunAngle float64, isSunrise bool, n int) float64 { +func iterativeApproachN(solarNoonTime, longitude, latitude, naturalTimeZone, zenithShift, height float64, isSunrise bool, n int) float64 { estimatedTime := solarNoonTime stepSize := 15.0 / 60.0 / 24.0 if isSunrise { @@ -366,7 +418,7 @@ func iterativeApproachN(solarNoonTime, longitude, latitude, naturalTimeZone, sun } const maxIterations = 48 - for i := 0; i < maxIterations && lowSunHeightForN(estimatedTime, longitude, latitude, naturalTimeZone, n) > sunAngle; i++ { + for i := 0; i < maxIterations && sunRiseSetResidual(estimatedTime, longitude, latitude, naturalTimeZone, zenithShift, height, n) > 0; i++ { estimatedTime += stepSize } @@ -374,82 +426,60 @@ func iterativeApproachN(solarNoonTime, longitude, latitude, naturalTimeZone, sun } // sunRiseSetNewtonRaphsonIteration 牛顿-拉夫逊迭代法求精确解 -func sunRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, naturalTimeZone, sunAngle, timeZone float64) float64 { +func sunRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, naturalTimeZone, zenithShift, height, timeZone float64) (float64, float64) { const ( convergenceThreshold = 0.00001 derivativeStep = 0.000005 ) currentTime := initialTime - - for { - previousTime := currentTime - - // 计算函数值:f(t) = SunHeight(t) - targetAngle - functionValue := SunHeight(previousTime, longitude, latitude, naturalTimeZone) - sunAngle - - // 计算导数:f'(t) ≈ (f(t+h) - f(t-h)) / (2h) - derivative := (SunHeight(previousTime+derivativeStep, longitude, latitude, naturalTimeZone) - - SunHeight(previousTime-derivativeStep, longitude, latitude, naturalTimeZone)) / (2 * derivativeStep) - - // 牛顿-拉夫逊公式:t_new = t_old - f(t) / f'(t) - currentTime = previousTime - functionValue/derivative - - // 检查收敛 - if math.Abs(currentTime-previousTime) <= convergenceThreshold { - break - } + slope := math.NaN() + var ok bool + currentTime, ok = eventNewtonRefine(currentTime, convergenceThreshold, func(previousTime float64) float64 { + functionValue := sunRiseSetResidual(previousTime, longitude, latitude, naturalTimeZone, zenithShift, height, -1) + slope = (sunRiseSetResidual(previousTime+derivativeStep, longitude, latitude, naturalTimeZone, zenithShift, height, -1) - + sunRiseSetResidual(previousTime-derivativeStep, longitude, latitude, naturalTimeZone, zenithShift, height, -1)) / (2 * derivativeStep) + return functionValue / slope + }) + if !ok { + return math.NaN(), math.NaN() } // 转换为指定时区 - return currentTime - naturalTimeZone/24 + timeZone/24 + return currentTime - naturalTimeZone/24 + timeZone/24, slope } -func sunRiseSetNewtonRaphsonIterationN(initialTime, longitude, latitude, naturalTimeZone, sunAngle, timeZone float64, n int) float64 { +func sunRiseSetNewtonRaphsonIterationN(initialTime, longitude, latitude, naturalTimeZone, zenithShift, height, timeZone float64, n int) (float64, float64) { const ( convergenceThreshold = 0.00001 derivativeStep = 0.000005 ) currentTime := initialTime - - for { - previousTime := currentTime - functionValue := SunHeightN(previousTime, longitude, latitude, naturalTimeZone, n) - sunAngle - derivative := (SunHeightN(previousTime+derivativeStep, longitude, latitude, naturalTimeZone, n) - - SunHeightN(previousTime-derivativeStep, longitude, latitude, naturalTimeZone, n)) / (2 * derivativeStep) - currentTime = previousTime - functionValue/derivative - if math.Abs(currentTime-previousTime) <= convergenceThreshold { - break - } + slope := math.NaN() + var ok bool + currentTime, ok = eventNewtonRefine(currentTime, convergenceThreshold, func(previousTime float64) float64 { + functionValue := sunRiseSetResidual(previousTime, longitude, latitude, naturalTimeZone, zenithShift, height, n) + slope = (sunRiseSetResidual(previousTime+derivativeStep, longitude, latitude, naturalTimeZone, zenithShift, height, n) - + sunRiseSetResidual(previousTime-derivativeStep, longitude, latitude, naturalTimeZone, zenithShift, height, n)) / (2 * derivativeStep) + return functionValue / slope + }) + if !ok { + return math.NaN(), math.NaN() } - return currentTime - naturalTimeZone/24 + timeZone/24 + return currentTime - naturalTimeZone/24 + timeZone/24, slope } /* * 太阳高度角 世界时 */ func SunHeight(jd, lon, lat, tz float64) float64 { - //tmp := (tz*15 - lon) * 4 / 60 - //truejd := jd - tmp/24 - calcjd := jd - tz/24.0 - tjde := TD2UT(calcjd, true) - st := Limit360(ApparentSiderealTime(calcjd)*15 + lon) - ra, dec := HSunApparentRaDec(tjde) - hourAngle := Limit360(st - ra) - tmp2 := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle) - return ArcSin(tmp2) + return SunHeightN(jd, lon, lat, tz, -1) } func SunHeightN(jd, lon, lat, tz float64, n int) float64 { - calcjd := jd - tz/24.0 - tjde := TD2UT(calcjd, true) - st := Limit360(ApparentSiderealTime(calcjd)*15 + lon) - ra, dec := HSunApparentRaDecN(tjde, n) - hourAngle := Limit360(st - ra) - tmp2 := Sin(lat)*Sin(dec) + Cos(dec)*Cos(lat)*Cos(hourAngle) - return ArcSin(tmp2) + return sunObservationStateN(jd, lon, lat, tz, n).altitude } func LowSunHeight(jd, lon, lat, tz float64) float64 { diff --git a/basic/testdata/mars_event_baseline.json b/basic/testdata/mars_event_baseline.json index 3aefeb3..1e1958d 100644 --- a/basic/testdata/mars_event_baseline.json +++ b/basic/testdata/mars_event_baseline.json @@ -7,14 +7,14 @@ "LastMarsConjunction": 4702512672991079470, "LastMarsEasternQuadrature": 4702512146017123176, "LastMarsOpposition": 4702511930972130659, - "LastMarsProgradeToRetrograde": 4702511848567808948, - "LastMarsRetrogradeToPrograde": 4702512005746801094, + "LastMarsProgradeToRetrograde": 4702511848566851510, + "LastMarsRetrogradeToPrograde": 4702512005746759120, "LastMarsWesternQuadrature": 4702511701091868571, "NextMarsConjunction": 4702514347420756601, "NextMarsEasternQuadrature": 4702513793695750616, "NextMarsOpposition": 4702513589022085416, - "NextMarsProgradeToRetrograde": 4702513504714100535, - "NextMarsRetrogradeToPrograde": 4702513671731723700, + "NextMarsProgradeToRetrograde": 4702513504713638346, + "NextMarsRetrogradeToPrograde": 4702513671731200986, "NextMarsWesternQuadrature": 4702513385284764112 } }, @@ -25,14 +25,14 @@ "LastMarsConjunction": 4702512672991079470, "LastMarsEasternQuadrature": 4702512146017123176, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702514347420756601, "NextMarsEasternQuadrature": 4702513793695750616, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -43,14 +43,14 @@ "LastMarsConjunction": 4702512672991079470, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714100535, - "LastMarsRetrogradeToPrograde": 4702513671731723700, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200986, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702514347420756601, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -61,14 +61,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714100535, - "LastMarsRetrogradeToPrograde": 4702513671731723700, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200986, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -79,14 +79,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -97,14 +97,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -115,14 +115,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -133,14 +133,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -151,14 +151,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -169,14 +169,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -187,14 +187,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -205,14 +205,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -223,14 +223,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -241,14 +241,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -259,14 +259,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -277,14 +277,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -295,14 +295,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -313,14 +313,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -331,14 +331,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -349,14 +349,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -367,14 +367,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -385,14 +385,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -403,14 +403,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -421,14 +421,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -439,14 +439,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -457,14 +457,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -475,14 +475,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702523573661302688, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702525238535665058, - "NextMarsRetrogradeToPrograde": 4702525402191244475, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108212, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -493,14 +493,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -511,14 +511,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -529,14 +529,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -547,14 +547,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -565,14 +565,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -583,14 +583,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -601,14 +601,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -619,14 +619,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702528610443478642, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702530255495150360, - "NextMarsProgradeToRetrograde": 4702530173883432461, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702530049621000879 } }, @@ -637,14 +637,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702528817168144408, "LastMarsOpposition": 4702528610443478642, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702528702088153624, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702528702087970247, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702530473238549120, "NextMarsOpposition": 4702530255495150360, - "NextMarsProgradeToRetrograde": 4702530173883432461, - "NextMarsRetrogradeToPrograde": 4702530346784276106, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702530346784069217, "NextMarsWesternQuadrature": 4702530049621000879 } }, @@ -655,14 +655,14 @@ "LastMarsConjunction": 4702529491283795617, "LastMarsEasternQuadrature": 4702528817168144408, "LastMarsOpposition": 4702528610443478642, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702528702088153624, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702528702087970247, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702531182749870743, "NextMarsEasternQuadrature": 4702530473238549120, "NextMarsOpposition": 4702530255495150360, - "NextMarsProgradeToRetrograde": 4702530173883432461, - "NextMarsRetrogradeToPrograde": 4702530346784276106, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702530346784069217, "NextMarsWesternQuadrature": 4702530049621000879 } }, @@ -673,14 +673,14 @@ "LastMarsConjunction": 4702529491283795617, "LastMarsEasternQuadrature": 4702528817168144408, "LastMarsOpposition": 4702528610443478642, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702528702088153624, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702528702087970247, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702531182749870743, "NextMarsEasternQuadrature": 4702530473238549120, "NextMarsOpposition": 4702530255495150360, - "NextMarsProgradeToRetrograde": 4702530173883432461, - "NextMarsRetrogradeToPrograde": 4702530346784276106, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702530346784069217, "NextMarsWesternQuadrature": 4702530049621000879 } }, @@ -691,14 +691,14 @@ "LastMarsConjunction": 4702529491283795617, "LastMarsEasternQuadrature": 4702530473238549120, "LastMarsOpposition": 4702530255495150360, - "LastMarsProgradeToRetrograde": 4702530173883432461, - "LastMarsRetrogradeToPrograde": 4702530346784276106, + "LastMarsProgradeToRetrograde": 4702530173884012780, + "LastMarsRetrogradeToPrograde": 4702530346784069217, "LastMarsWesternQuadrature": 4702530049621000879, "NextMarsConjunction": 4702531182749870743, "NextMarsEasternQuadrature": 4702532164571047325, "NextMarsOpposition": 4702531918919958017, - "NextMarsProgradeToRetrograde": 4702531842932954047, - "NextMarsRetrogradeToPrograde": 4702532002409556399, + "NextMarsProgradeToRetrograde": 4702531842932919731, + "NextMarsRetrogradeToPrograde": 4702532002409624070, "NextMarsWesternQuadrature": 4702531693510274880 } }, @@ -709,14 +709,14 @@ "LastMarsConjunction": 4702529491283795617, "LastMarsEasternQuadrature": 4702530473238549120, "LastMarsOpposition": 4702530255495150360, - "LastMarsProgradeToRetrograde": 4702530173883432461, - "LastMarsRetrogradeToPrograde": 4702530346784276106, + "LastMarsProgradeToRetrograde": 4702530173884012780, + "LastMarsRetrogradeToPrograde": 4702530346784069217, "LastMarsWesternQuadrature": 4702530049621000879, "NextMarsConjunction": 4702531182749870743, "NextMarsEasternQuadrature": 4702532164571047325, "NextMarsOpposition": 4702531918919958017, - "NextMarsProgradeToRetrograde": 4702531842932954047, - "NextMarsRetrogradeToPrograde": 4702532002409556399, + "NextMarsProgradeToRetrograde": 4702531842932919731, + "NextMarsRetrogradeToPrograde": 4702532002409624070, "NextMarsWesternQuadrature": 4702531693510274880 } }, @@ -727,14 +727,14 @@ "LastMarsConjunction": 4702531182749870743, "LastMarsEasternQuadrature": 4702530473238549120, "LastMarsOpposition": 4702530255495150360, - "LastMarsProgradeToRetrograde": 4702530173883432461, - "LastMarsRetrogradeToPrograde": 4702530346784276106, + "LastMarsProgradeToRetrograde": 4702530173884012780, + "LastMarsRetrogradeToPrograde": 4702530346784069217, "LastMarsWesternQuadrature": 4702530049621000879, "NextMarsConjunction": 4702532843561122680, "NextMarsEasternQuadrature": 4702532164571047325, "NextMarsOpposition": 4702531918919958017, - "NextMarsProgradeToRetrograde": 4702531842932954047, - "NextMarsRetrogradeToPrograde": 4702532002409556399, + "NextMarsProgradeToRetrograde": 4702531842932919731, + "NextMarsRetrogradeToPrograde": 4702532002409624070, "NextMarsWesternQuadrature": 4702531693510274880 } }, @@ -745,14 +745,14 @@ "LastMarsConjunction": 4702531182749870743, "LastMarsEasternQuadrature": 4702530473238549120, "LastMarsOpposition": 4702530255495150360, - "LastMarsProgradeToRetrograde": 4702531842932954047, - "LastMarsRetrogradeToPrograde": 4702530346784276106, + "LastMarsProgradeToRetrograde": 4702531842932919731, + "LastMarsRetrogradeToPrograde": 4702530346784069217, "LastMarsWesternQuadrature": 4702531693510274880, "NextMarsConjunction": 4702532843561122680, "NextMarsEasternQuadrature": 4702532164571047325, "NextMarsOpposition": 4702531918919958017, - "NextMarsProgradeToRetrograde": 4702533566265746622, - "NextMarsRetrogradeToPrograde": 4702532002409556399, + "NextMarsProgradeToRetrograde": 4702533566262965791, + "NextMarsRetrogradeToPrograde": 4702532002409624070, "NextMarsWesternQuadrature": 4702533360315552681 } }, @@ -763,14 +763,14 @@ "LastMarsConjunction": 4702531182749870743, "LastMarsEasternQuadrature": 4702532164571047325, "LastMarsOpposition": 4702531918919958017, - "LastMarsProgradeToRetrograde": 4702531842932954047, - "LastMarsRetrogradeToPrograde": 4702532002409556399, + "LastMarsProgradeToRetrograde": 4702531842932919731, + "LastMarsRetrogradeToPrograde": 4702532002409624070, "LastMarsWesternQuadrature": 4702531693510274880, "NextMarsConjunction": 4702532843561122680, "NextMarsEasternQuadrature": 4702533904383919033, "NextMarsOpposition": 4702533627774828963, - "NextMarsProgradeToRetrograde": 4702533566265746622, - "NextMarsRetrogradeToPrograde": 4702533696942812205, + "NextMarsProgradeToRetrograde": 4702533566262965791, + "NextMarsRetrogradeToPrograde": 4702533696942547266, "NextMarsWesternQuadrature": 4702533360315552681 } }, @@ -781,14 +781,14 @@ "LastMarsConjunction": 4702531182749870743, "LastMarsEasternQuadrature": 4702532164571047325, "LastMarsOpposition": 4702531918919958017, - "LastMarsProgradeToRetrograde": 4702531842932954047, - "LastMarsRetrogradeToPrograde": 4702532002409556399, + "LastMarsProgradeToRetrograde": 4702531842932919731, + "LastMarsRetrogradeToPrograde": 4702532002409624070, "LastMarsWesternQuadrature": 4702531693510274880, "NextMarsConjunction": 4702532843561122680, "NextMarsEasternQuadrature": 4702533904383919033, "NextMarsOpposition": 4702533627774828963, - "NextMarsProgradeToRetrograde": 4702533566265746622, - "NextMarsRetrogradeToPrograde": 4702533696942812205, + "NextMarsProgradeToRetrograde": 4702533566262965791, + "NextMarsRetrogradeToPrograde": 4702533696942547266, "NextMarsWesternQuadrature": 4702533360315552681 } }, @@ -799,14 +799,14 @@ "LastMarsConjunction": 4702532843561122680, "LastMarsEasternQuadrature": 4702532164571047325, "LastMarsOpposition": 4702531918919958017, - "LastMarsProgradeToRetrograde": 4702531842932954047, - "LastMarsRetrogradeToPrograde": 4702532002409556399, + "LastMarsProgradeToRetrograde": 4702531842932919731, + "LastMarsRetrogradeToPrograde": 4702532002409624070, "LastMarsWesternQuadrature": 4702531693510274880, "NextMarsConjunction": 4702534491554176271, "NextMarsEasternQuadrature": 4702533904383919033, "NextMarsOpposition": 4702533627774828963, - "NextMarsProgradeToRetrograde": 4702533566265746622, - "NextMarsRetrogradeToPrograde": 4702533696942812205, + "NextMarsProgradeToRetrograde": 4702533566262965791, + "NextMarsRetrogradeToPrograde": 4702533696942547266, "NextMarsWesternQuadrature": 4702533360315552681 } }, @@ -817,14 +817,14 @@ "LastMarsConjunction": 4702532843561122680, "LastMarsEasternQuadrature": 4702532164571047325, "LastMarsOpposition": 4702533627774828963, - "LastMarsProgradeToRetrograde": 4702533566265746622, - "LastMarsRetrogradeToPrograde": 4702532002409556399, + "LastMarsProgradeToRetrograde": 4702533566262965791, + "LastMarsRetrogradeToPrograde": 4702532002409624070, "LastMarsWesternQuadrature": 4702533360315552681, "NextMarsConjunction": 4702534491554176271, "NextMarsEasternQuadrature": 4702533904383919033, "NextMarsOpposition": 4702535366718430785, - "NextMarsProgradeToRetrograde": 4702535293198745177, - "NextMarsRetrogradeToPrograde": 4702533696942812205, + "NextMarsProgradeToRetrograde": 4702535293198845174, + "NextMarsRetrogradeToPrograde": 4702533696942547266, "NextMarsWesternQuadrature": 4702535089312982648 } }, @@ -835,14 +835,14 @@ "LastMarsConjunction": 4702532843561122680, "LastMarsEasternQuadrature": 4702533904383919033, "LastMarsOpposition": 4702533627774828963, - "LastMarsProgradeToRetrograde": 4702533566265746622, - "LastMarsRetrogradeToPrograde": 4702533696942812205, + "LastMarsProgradeToRetrograde": 4702533566262965791, + "LastMarsRetrogradeToPrograde": 4702533696942547266, "LastMarsWesternQuadrature": 4702533360315552681, "NextMarsConjunction": 4702534491554176271, "NextMarsEasternQuadrature": 4702535603937495686, "NextMarsOpposition": 4702535366718430785, - "NextMarsProgradeToRetrograde": 4702535293198745177, - "NextMarsRetrogradeToPrograde": 4702535437224826911, + "NextMarsProgradeToRetrograde": 4702535293198845174, + "NextMarsRetrogradeToPrograde": 4702535437223840739, "NextMarsWesternQuadrature": 4702535089312982648 } }, @@ -853,14 +853,14 @@ "LastMarsConjunction": 4702534491554176271, "LastMarsEasternQuadrature": 4702533904383919033, "LastMarsOpposition": 4702533627774828963, - "LastMarsProgradeToRetrograde": 4702533566265746622, - "LastMarsRetrogradeToPrograde": 4702533696942812205, + "LastMarsProgradeToRetrograde": 4702533566262965791, + "LastMarsRetrogradeToPrograde": 4702533696942547266, "LastMarsWesternQuadrature": 4702533360315552681, "NextMarsConjunction": 4702536138075400266, "NextMarsEasternQuadrature": 4702535603937495686, "NextMarsOpposition": 4702535366718430785, - "NextMarsProgradeToRetrograde": 4702535293198745177, - "NextMarsRetrogradeToPrograde": 4702535437224826911, + "NextMarsProgradeToRetrograde": 4702535293198845174, + "NextMarsRetrogradeToPrograde": 4702535437223840739, "NextMarsWesternQuadrature": 4702535089312982648 } }, @@ -871,14 +871,14 @@ "LastMarsConjunction": 4702512672991079470, "LastMarsEasternQuadrature": 4702512146017123176, "LastMarsOpposition": 4702511930972130659, - "LastMarsProgradeToRetrograde": 4702511848567808948, - "LastMarsRetrogradeToPrograde": 4702512005746801094, + "LastMarsProgradeToRetrograde": 4702511848566851510, + "LastMarsRetrogradeToPrograde": 4702512005746759120, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702514347420756601, "NextMarsEasternQuadrature": 4702513793695750616, "NextMarsOpposition": 4702513589022085416, - "NextMarsProgradeToRetrograde": 4702513504714100535, - "NextMarsRetrogradeToPrograde": 4702513671731723700, + "NextMarsProgradeToRetrograde": 4702513504713638346, + "NextMarsRetrogradeToPrograde": 4702513671731200986, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -889,14 +889,14 @@ "LastMarsConjunction": 4702512672991079470, "LastMarsEasternQuadrature": 4702512146017123176, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702514347420756601, "NextMarsEasternQuadrature": 4702513793695750616, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -907,14 +907,14 @@ "LastMarsConjunction": 4702512672991079470, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702514347420756601, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -925,14 +925,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -943,14 +943,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702513385284764112, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702515034746657425 } }, @@ -961,14 +961,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702513793695750616, "LastMarsOpposition": 4702513589022085416, - "LastMarsProgradeToRetrograde": 4702513504714119836, - "LastMarsRetrogradeToPrograde": 4702513671730628658, + "LastMarsProgradeToRetrograde": 4702513504713638346, + "LastMarsRetrogradeToPrograde": 4702513671731200993, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702515436148825805, "NextMarsOpposition": 4702515232188474249, - "NextMarsProgradeToRetrograde": 4702515148308256052, - "NextMarsRetrogradeToPrograde": 4702515321508477837, + "NextMarsProgradeToRetrograde": 4702515148308162856, + "NextMarsRetrogradeToPrograde": 4702515321508671938, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -979,14 +979,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -997,14 +997,14 @@ "LastMarsConjunction": 4702514347420756601, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702516062146454046, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -1015,14 +1015,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -1033,14 +1033,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702515148308256052, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702515148308162856, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702515034746657425, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702516790519357664, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702516790519591907, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702516673165455699 } }, @@ -1051,14 +1051,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702515436148825805, "LastMarsOpposition": 4702515232188474249, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702515321508477837, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702515321508671938, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702517083255855413, "NextMarsOpposition": 4702516873348048812, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702516965529836647, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702516965530268756, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -1069,14 +1069,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -1087,14 +1087,14 @@ "LastMarsConjunction": 4702516062146454046, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702517778500160722, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -1105,14 +1105,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516965530551509, - "LastMarsRetrogradeToPrograde": 4702516965529843409, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268755, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -1123,14 +1123,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702516790519357664, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702516790519591907, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702516673165455699, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702518443318837703, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702518443319353833, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702518311829564273 } }, @@ -1141,14 +1141,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702517083255855413, "LastMarsOpposition": 4702516873348048812, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702516965529836647, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702516965530268756, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702518749267101830, "NextMarsOpposition": 4702518523484696971, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702518612759470372, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702518612758774793, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -1159,14 +1159,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -1177,14 +1177,14 @@ "LastMarsConjunction": 4702517778500160722, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702519455332247028, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -1195,14 +1195,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -1213,14 +1213,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702518443318837703, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702518443319353833, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702518311829564273, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702520129601010070, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702520129599854159, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702519961901894387 } }, @@ -1231,14 +1231,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702518749267101830, "LastMarsOpposition": 4702518523484696971, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702518612759470372, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702518612758774793, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702520463914895419, "NextMarsOpposition": 4702520200681938776, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702520278430293987, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702520278429881065, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -1249,14 +1249,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -1267,14 +1267,14 @@ "LastMarsConjunction": 4702519455332247028, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129600963990, - "LastMarsRetrogradeToPrograde": 4702520278430295096, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881062, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702521109471690662, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -1285,14 +1285,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -1303,14 +1303,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702519961901894387, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702521646667869429 } }, @@ -1321,14 +1321,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702520463914895419, "LastMarsOpposition": 4702520200681938776, - "LastMarsProgradeToRetrograde": 4702520129601010070, - "LastMarsRetrogradeToPrograde": 4702520278430293987, + "LastMarsProgradeToRetrograde": 4702520129599854159, + "LastMarsRetrogradeToPrograde": 4702520278429881065, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702522196782477191, "NextMarsOpposition": 4702521931573037827, - "NextMarsProgradeToRetrograde": 4702521869637003815, - "NextMarsRetrogradeToPrograde": 4702521999942771281, + "NextMarsProgradeToRetrograde": 4702521869636717147, + "NextMarsRetrogradeToPrograde": 4702521999943030902, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -1339,14 +1339,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -1357,14 +1357,14 @@ "LastMarsConjunction": 4702521109471690662, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702522755753620573, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -1375,14 +1375,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -1393,14 +1393,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869637003815, - "LastMarsRetrogradeToPrograde": 4702521999942771281, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030902, "LastMarsWesternQuadrature": 4702521646667869429, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702523400900974448 } }, @@ -1411,14 +1411,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702522196782477191, "LastMarsOpposition": 4702521931573037827, - "LastMarsProgradeToRetrograde": 4702521869636674615, - "LastMarsRetrogradeToPrograde": 4702521999941872407, + "LastMarsProgradeToRetrograde": 4702521869636717147, + "LastMarsRetrogradeToPrograde": 4702521999943030903, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702523876976052288, "NextMarsOpposition": 4702523652958023467, - "NextMarsProgradeToRetrograde": 4702523573661302688, - "NextMarsRetrogradeToPrograde": 4702523725151391970, + "NextMarsProgradeToRetrograde": 4702523573661936412, + "NextMarsRetrogradeToPrograde": 4702523725151062606, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -1429,14 +1429,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -1447,14 +1447,14 @@ "LastMarsConjunction": 4702522755753620573, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702524404470783967, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -1465,14 +1465,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -1483,14 +1483,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702523400900974448, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702525112250558519 } }, @@ -1501,14 +1501,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702523876976052288, "LastMarsOpposition": 4702523652958023467, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702523725151391970, + "LastMarsProgradeToRetrograde": 4702523573661936412, + "LastMarsRetrogradeToPrograde": 4702523725151062606, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702525529803790800, "NextMarsOpposition": 4702525322610471547, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702525402191736316, + "NextMarsProgradeToRetrograde": 4702525238535210259, + "NextMarsRetrogradeToPrograde": 4702525402191108215, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -1519,14 +1519,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -1537,14 +1537,14 @@ "LastMarsConjunction": 4702524404470783967, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702526067991662288, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -1555,14 +1555,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -1573,14 +1573,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702505055477322423, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702525238535210259, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702525112250558519, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702526885608322713, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702526885607895804, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702526771102292594 } }, @@ -1591,14 +1591,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702525529803790800, "LastMarsOpposition": 4702525322610471547, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702525402191736316, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702525402191108215, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702527173148473968, "NextMarsOpposition": 4702526969725142175, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702527056792533776, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702527056790870886, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -1609,14 +1609,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -1627,14 +1627,14 @@ "LastMarsConjunction": 4702526067991662288, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702527766168612385, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -1645,14 +1645,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087806654, - "NextMarsRetrogradeToPrograde": 4702528702088178385, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970248, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -1663,14 +1663,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702526885608322713, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702526885607895804, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702526771102292594, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702528527087795183, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702528527088086023, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702528412245032287 } }, @@ -1681,14 +1681,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702527173148473968, "LastMarsOpposition": 4702526969725142175, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702527056792533776, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702527056790870886, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702528817168144408, "NextMarsOpposition": 4702528610443478642, - "NextMarsProgradeToRetrograde": 4702530173883432461, - "NextMarsRetrogradeToPrograde": 4702528702088153624, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702528702087970247, "NextMarsWesternQuadrature": 4702530049621000879 } }, @@ -1699,14 +1699,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702528817168144408, "LastMarsOpposition": 4702528610443478642, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702528702088153624, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702528702087970247, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702530473238549120, "NextMarsOpposition": 4702530255495150360, - "NextMarsProgradeToRetrograde": 4702530173883432461, - "NextMarsRetrogradeToPrograde": 4702530346784276106, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702530346784069217, "NextMarsWesternQuadrature": 4702530049621000879 } }, @@ -1717,14 +1717,14 @@ "LastMarsConjunction": 4702527766168612385, "LastMarsEasternQuadrature": 4702528817168144408, "LastMarsOpposition": 4702528610443478642, - "LastMarsProgradeToRetrograde": 4702528527087795183, - "LastMarsRetrogradeToPrograde": 4702528702088153624, + "LastMarsProgradeToRetrograde": 4702528527088086023, + "LastMarsRetrogradeToPrograde": 4702528702087970247, "LastMarsWesternQuadrature": 4702528412245032287, "NextMarsConjunction": 4702529491283795617, "NextMarsEasternQuadrature": 4702530473238549120, "NextMarsOpposition": 4702530255495150360, - "NextMarsProgradeToRetrograde": 4702530173883432852, - "NextMarsRetrogradeToPrograde": 4702530346784236839, + "NextMarsProgradeToRetrograde": 4702530173884012780, + "NextMarsRetrogradeToPrograde": 4702530346784069223, "NextMarsWesternQuadrature": 4702530049621000879 } } diff --git a/basic/testdata/moon_rise_set_baseline.json b/basic/testdata/moon_rise_set_baseline.json new file mode 100644 index 0000000..1fbfc69 --- /dev/null +++ b/basic/testdata/moon_rise_set_baseline.json @@ -0,0 +1,162 @@ +{ + "schema_version": 1, + "generated_utc": "2026-08-02T15:47:59Z", + "sources": { + "imcce_miriade": { + "provider": "IMCCE/LTE Miriade RTS", + "url": "https://ssp.imcce.fr/webservices/miriade/api/rts.php", + "definition": "direct rise and set service; the RTS API does not expose its horizon, refraction, or ephemeris-theory settings", + "resolution": "0.1-second display precision; not an uncertainty estimate" + }, + "jpl_horizons": { + "provider": "NASA/JPL Horizons", + "url": "https://ssd.jpl.nasa.gov/api/horizons.api", + "model": "DE441", + "definition": "refracted apparent upper limb crossing the reference-ellipsoid visual horizon at sea level", + "resolution": "linear zero interpolation from 1-minute refracted center elevation and angular diameter" + }, + "met_norway": { + "provider": "MET Norway Sunrise API", + "url": "https://api.met.no/weatherapi/sunrise/3.0/moon", + "model": "Skyfield 1.53 with JPL DE440s", + "definition": "fixed 0.5666-degree refraction plus 0.2667-degree lunar radius; queried by local solar date and returned in UTC", + "resolution": "1 minute", + "license_url": "https://api.met.no/license_data.html" + } + }, + "samples": [ + { + "site": "greenwich", + "date_utc": "2026-04-28", + "longitude": 0, + "latitude": 51.4779, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2026-04-28T16:01:21Z", + "set_utc": "2026-04-28T03:19:32Z" + }, + "met_norway": { + "rise_utc": "2026-04-28T16:01:00Z", + "set_utc": "2026-04-28T03:19:00Z" + }, + "imcce_miriade": { + "rise_utc": "2026-04-28T16:03:10.4Z", + "set_utc": "2026-04-28T03:17:47.1Z" + } + }, + { + "site": "beijing", + "date_utc": "2025-06-05", + "longitude": 116.4074, + "latitude": 39.9042, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2025-06-05T06:01:29Z", + "set_utc": "2025-06-05T17:41:15Z" + }, + "met_norway": { + "rise_utc": "2025-06-05T06:01:00Z", + "set_utc": "2025-06-05T17:41:00Z" + }, + "imcce_miriade": { + "rise_utc": "2025-06-05T06:02:54.6Z", + "set_utc": "2025-06-05T17:39:51.3Z" + } + }, + { + "site": "singapore", + "date_utc": "2025-01-15", + "longitude": 103.8198, + "latitude": 1.3521, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2025-01-15T12:36:30Z", + "set_utc": "2025-01-15T00:15:05Z" + }, + "met_norway": { + "rise_utc": "2025-01-15T12:36:00Z", + "set_utc": "2025-01-15T00:14:00Z" + }, + "imcce_miriade": { + "rise_utc": "2025-01-15T12:37:41.8Z", + "set_utc": "2025-01-15T00:13:51.3Z" + } + }, + { + "site": "sydney", + "date_utc": "2025-07-16", + "longitude": 151.2093, + "latitude": -33.8688, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2025-07-16T12:40:33Z", + "set_utc": "2025-07-16T00:09:02Z" + }, + "met_norway": { + "rise_utc": "2025-07-16T12:40:00Z", + "set_utc": "2025-07-16T00:08:00Z" + }, + "imcce_miriade": { + "rise_utc": "2025-07-16T12:41:59.3Z", + "set_utc": "2025-07-16T00:07:38.7Z" + } + }, + { + "site": "new_york", + "date_utc": "2025-10-15", + "longitude": -74.006, + "latitude": 40.7128, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2025-10-15T04:57:44Z", + "set_utc": "2025-10-15T19:47:14Z" + }, + "met_norway": { + "rise_utc": "2025-10-15T04:58:00Z", + "set_utc": "2025-10-15T19:46:00Z" + }, + "imcce_miriade": { + "rise_utc": "2025-10-15T04:59:27.6Z", + "set_utc": "2025-10-15T19:45:38.0Z" + } + }, + { + "site": "anchorage", + "date_utc": "2025-03-20", + "longitude": -149.9003, + "latitude": 61.2181, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2025-03-20T12:53:14Z", + "set_utc": "2025-03-20T16:04:16Z" + }, + "met_norway": { + "rise_utc": "2025-03-20T12:54:00Z", + "set_utc": "2025-03-20T16:03:00Z" + }, + "imcce_miriade": { + "rise_utc": "2025-03-20T12:59:56.0Z", + "set_utc": "2025-03-20T15:57:34.4Z" + } + }, + { + "site": "rio_de_janeiro", + "date_utc": "2026-01-15", + "longitude": -43.1729, + "latitude": -22.9068, + "observer_height_m": 0, + "jpl_horizons": { + "rise_utc": "2026-01-15T05:13:59Z", + "set_utc": "2026-01-15T19:26:42Z" + }, + "met_norway": { + "rise_utc": "2026-01-15T05:14:00Z", + "set_utc": "2026-01-15T19:26:00Z" + }, + "imcce_miriade": { + "rise_utc": "2026-01-15T05:15:20.6Z", + "set_utc": "2026-01-15T19:25:19.4Z" + } + } + ] +} diff --git a/basic/traditional_calendar.go b/basic/traditional_calendar.go index fab9496..7197395 100644 --- a/basic/traditional_calendar.go +++ b/basic/traditional_calendar.go @@ -59,15 +59,24 @@ func GetWuHouTime(Year, Angle int) float64 { if Angle <= 5 { Angle = 360 + Angle } - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { JD0 := JD1 stDegree := JQLospec(JD0, float64(Angle)) - float64(Angle) stDegreep := (JQLospec(JD0+0.000005, float64(Angle)) - JQLospec(JD0-0.000005, float64(Angle))) / 0.00001 - JD1 = JD0 - stDegree/stDegreep - if math.Abs(JD1-JD0) <= 0.00001 { + nextJD := JD0 - stDegree/stDegreep + if !isFiniteFloat(nextJD) { + return math.NaN() + } + JD1 = nextJD + if math.Abs(nextJD-JD0) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(JD1, false) } diff --git a/basic/uranus.go b/basic/uranus.go index dea08a6..f44f871 100644 --- a/basic/uranus.go +++ b/basic/uranus.go @@ -173,14 +173,14 @@ func UranusCulminationTime(jde, lon, timezone float64) float64 { } return currentHourAngle } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { hourAngleDelta := normalizedHourAngle(prevJD, lon, timezone) - 360 hourAngleSlope := (normalizedHourAngle(prevJD+0.000005, lon, timezone) - normalizedHourAngle(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - hourAngleDelta/hourAngleSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return hourAngleDelta / hourAngleSlope + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/uranus_events.go b/basic/uranus_events.go index 979e08d..ee69e87 100644 --- a/basic/uranus_events.go +++ b/basic/uranus_events.go @@ -64,6 +64,9 @@ func uranusRADerivativeN(jde, delta float64, n int) float64 { func uranusConjunctionFull(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := URANUS_S_PERIOD / 360 currentDelta := uranusSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -72,20 +75,29 @@ func uranusConjunctionFull(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := uranusSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (uranusSunLongitudeDelta(prevJD+0.000005, degree, true) - uranusSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } func uranusConjunction(jde, degree float64, next uint8) float64 { //0=last 1=next + if !isFiniteFloat(jde) || !isFiniteFloat(degree) { + return math.NaN() + } daysPerDegree := URANUS_S_PERIOD / 360 currentDelta := uranusSunLongitudeDelta(jde, degree, false) if next == 0 { @@ -94,24 +106,36 @@ func uranusConjunction(jde, degree float64, next uint8) float64 { jde += daysPerDegree * currentDelta } estimateJD := jde - for { + converged := false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := uranusSunLongitudeDeltaN(prevJD, degree, true, uranusEventSearchN) longitudeSlope := (uranusSunLongitudeDeltaN(prevJD+0.000005, degree, true, uranusEventSearchN) - uranusSunLongitudeDeltaN(prevJD-0.000005, degree, true, uranusEventSearchN)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= uranusPhaseCoarseTolerance { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= uranusPhaseCoarseTolerance { + converged = true break } } - for { + if !converged { + return math.NaN() + } + converged = false + for i := 0; i < eventNewtonMaxIterations; i++ { prevJD := estimateJD longitudeDelta := uranusSunLongitudeDelta(prevJD, degree, true) longitudeSlope := (uranusSunLongitudeDelta(prevJD+0.000005, degree, true) - uranusSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 - estimateJD = prevJD - longitudeDelta/longitudeSlope - if math.Abs(estimateJD-prevJD) <= 0.00001 { + nextJD := prevJD - longitudeDelta/longitudeSlope + estimateJD = nextJD + if math.Abs(nextJD-prevJD) <= 0.00001 { + converged = true break } } + if !converged { + return math.NaN() + } return TD2UT(estimateJD, false) } diff --git a/basic/venus.go b/basic/venus.go index bfac54e..78e4d64 100644 --- a/basic/venus.go +++ b/basic/venus.go @@ -173,14 +173,14 @@ func VenusCulminationTime(jde, lon, timezone float64) float64 { } return ha } - for { - prevJD := estimateJD + var ok bool + estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 { stDegree := limitHA(prevJD, lon, timezone) - 360 stDegreep := (limitHA(prevJD+0.000005, lon, timezone) - limitHA(prevJD-0.000005, lon, timezone)) / 0.00001 - estimateJD = prevJD - stDegree/stDegreep - if math.Abs(estimateJD-prevJD) <= 0.00001 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } return estimateJD } diff --git a/basic/venus_event_regression_test.go b/basic/venus_event_regression_test.go index 6acac72..5e12603 100644 --- a/basic/venus_event_regression_test.go +++ b/basic/venus_event_regression_test.go @@ -5,6 +5,7 @@ import ( "math" "os" "testing" + "time" ) type venusEventBaselineSample struct { @@ -47,7 +48,13 @@ func TestVenusEventBaselineRegression(t *testing.T) { t.Fatalf("%s missing baseline event %s", sample.InputUTC, event.name) } want := math.Float64frombits(wantBits) + if math.IsNaN(want) || math.IsInf(want, 0) { + t.Fatalf("%s %s baseline is non-finite %v", sample.InputUTC, event.name, want) + } got := event.fn(jd) + if math.IsNaN(got) || math.IsInf(got, 0) { + t.Fatalf("%s %s returned non-finite result %v", sample.InputUTC, event.name, got) + } diff := math.Abs(got - want) if diff > event.tolerance { t.Fatalf("%s %s diff %.12f > tolerance %.12f", sample.InputUTC, event.name, diff, event.tolerance) @@ -55,3 +62,16 @@ func TestVenusEventBaselineRegression(t *testing.T) { } } } + +func TestVenusConjunctionExtremeInputIsBounded(t *testing.T) { + started := time.Now() + for _, next := range []uint8{0, 1} { + got := venusConjunction(1e8, next) + if math.IsInf(got, 0) { + t.Fatalf("venusConjunction(1e8, %d) returned infinite result %v", next, got) + } + } + if elapsed := time.Since(started); elapsed > 2*time.Second { + t.Fatalf("venusConjunction extreme input took %s, want <= 2s", elapsed) + } +} diff --git a/basic/venus_events.go b/basic/venus_events.go index 89deb60..3e49c4f 100644 --- a/basic/venus_events.go +++ b/basic/venus_events.go @@ -195,6 +195,9 @@ func venusElongationDerivativeN(jde, val float64, n int) float64 { } func venusConjunction(jde float64, next uint8) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } queryTT := jde direction := -1.0 if next == 1 { @@ -203,13 +206,14 @@ func venusConjunction(jde float64, next uint8) float64 { left := queryTT leftVal := venusSunLongitudeDeltaN(left, venusEventSearchN) if math.Abs(venusSunLongitudeDelta(queryTT)) <= 30.0/86400.0 { - exact := eventZeroRefine(left, 1.0, 0.000005, venusSunLongitudeDelta) - eventUT := TD2UT(exact, false) - if next == 0 && eventUTQueryBeforeOrEqual(eventUT, queryTT) { - return eventUT - } - if next == 1 && eventUTQueryAfterOrEqual(eventUT, queryTT) { - return eventUT + if exact, ok := venusConjunctionRefine(left, 1.0); ok { + eventUT := TD2UT(exact, false) + if next == 0 && eventUTQueryBeforeOrEqual(eventUT, queryTT) { + return eventUT + } + if next == 1 && eventUTQueryAfterOrEqual(eventUT, queryTT) { + return eventUT + } } } const step = 8.0 @@ -219,12 +223,36 @@ func venusConjunction(jde float64, next uint8) float64 { if leftVal == 0 || rightVal == 0 || leftVal*rightVal <= 0 { center := (left + right) / 2.0 halfWindow := math.Abs(right-left) / 2.0 - return TD2UT(eventZeroRefine(center, halfWindow, 0.000005, venusSunLongitudeDelta), false) + if exact, ok := venusConjunctionRefine(center, halfWindow); ok { + return TD2UT(exact, false) + } + return math.NaN() } left = right leftVal = rightVal } - return TD2UT(eventZeroRefine(queryTT, VENUS_S_PERIOD, 0.000005, venusSunLongitudeDelta), false) + // 640 天已经覆盖一个金星会合周期;仍无括号通常表示输入超出解析项的可靠范围。 + // 继续按 5 微日扫描整个周期会产生数亿次星历计算,因此在这里有界失败。 + // The 640-day directional scan already exceeds one Venus synodic period. If it + // still finds no bracket, fail in a bounded way instead of scanning hundreds + // of millions of five-microday samples across the full fallback window. + return math.NaN() +} + +func venusConjunctionRefine(seed, halfWindow float64) (float64, bool) { + leftJD := seed - halfWindow + centerJD := seed + rightJD := seed + halfWindow + leftVal := venusSunLongitudeDelta(leftJD) + centerVal := venusSunLongitudeDelta(centerJD) + rightVal := venusSunLongitudeDelta(rightJD) + if !isFiniteFloat(leftVal) || !isFiniteFloat(centerVal) || !isFiniteFloat(rightVal) { + return math.NaN(), false + } + if _, _, _, _, ok := eventZeroBracket(leftJD, leftVal, centerJD, centerVal, rightJD, rightVal); !ok { + return math.NaN(), false + } + return eventZeroRefine(seed, halfWindow, 0.000005, venusSunLongitudeDelta), true } func venusConjunctionTypeAt(eventUT float64) bool { @@ -289,6 +317,9 @@ func LastVenusSuperiorConjunction(jde float64) float64 { func venusRetrograde(jde float64) float64 { //0=last 1=next + if !isFiniteFloat(jde) { + return math.NaN() + } lastHe := LastVenusConjunctionStrict(jde) nextHe := NextVenusConjunctionStrict(jde) nowSub := venusSunRADelta(jde) @@ -297,23 +328,31 @@ func venusRetrograde(jde float64) float64 { } else { jde = lastHe + 10 } - for { + found := false + for i := 0; i < eventDirectionalSearchIterations; i++ { nowSub := venusRADerivativeN(jde, 1.0/86400.0, venusEventSearchN) + if !isFiniteFloat(nowSub) { + return math.NaN() + } if math.Abs(nowSub) > 0.5 { jde += 5 continue } + found = true break } + if !found { + return math.NaN() + } JD1 := jde - for { - JD0 := JD1 + var ok bool + JD1, ok = eventNewtonRefine(JD1, 20.0/86400.0, func(JD0 float64) float64 { stDegree := venusRADerivative(JD0, 0.5/86400.0) stDegreep := (venusRADerivative(JD0+10.0/86400.0, 0.5/86400.0) - venusRADerivative(JD0-10.0/86400.0, 0.5/86400.0)) / (20.0 / 86400.0) - JD1 = JD0 - stDegree/stDegreep - if math.Abs(JD1-JD0) <= 20.0/86400.0 { - break - } + return stDegree / stDegreep + }) + if !ok { + return math.NaN() } min := eventZeroRefine(JD1, 10.0/86400.0, 0.5/86400.0, func(jd float64) float64 { return venusRADerivative(jd, 0.5/86400.0) @@ -376,46 +415,62 @@ func venusRetrogradeToProgradeAroundInferior(inferior float64) float64 { func NextVenusProgradeToRetrograde(jde float64) float64 { inferior := NextVenusInferiorConjunction(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusProgradeToRetrogradeAroundInferior(inferior) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryAfterOrEqual(date, jde) { return date } inferior = NextVenusInferiorConjunction(eventUTNextQueryTT(inferior)) } + return math.NaN() } func NextVenusRetrogradeToPrograde(jde float64) float64 { inferior := LastVenusInferiorConjunction(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusRetrogradeToProgradeAroundInferior(inferior) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryAfterOrEqual(date, jde) { return date } inferior = NextVenusInferiorConjunction(eventUTNextQueryTT(inferior)) } + return math.NaN() } func LastVenusProgradeToRetrograde(jde float64) float64 { inferior := NextVenusInferiorConjunction(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusProgradeToRetrogradeAroundInferior(inferior) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryBeforeOrEqual(date, jde) { return date } inferior = LastVenusInferiorConjunction(eventUTLastQueryTT(inferior)) } + return math.NaN() } func LastVenusRetrogradeToPrograde(jde float64) float64 { inferior := LastVenusInferiorConjunction(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusRetrogradeToProgradeAroundInferior(inferior) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryBeforeOrEqual(date, jde) { return date } inferior = LastVenusInferiorConjunction(eventUTLastQueryTT(inferior)) } + return math.NaN() } func VenusSunElongation(jde float64) float64 { @@ -465,52 +520,77 @@ func venusWestElongationWindowContaining(jde float64) (float64, float64) { } func nextVenusGreatestElongationTyped(jde float64, east bool) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } if east { start, windowEnd := venusEastElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryAfterOrEqual(date, jde) { return date } nextInferior := NextVenusInferiorConjunction(eventUTNextQueryTT(windowEnd)) start, windowEnd = venusEastElongationWindowEndingAt(nextInferior) } + return math.NaN() } start, windowEnd := venusWestElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryAfterOrEqual(date, jde) { return date } nextSuperior := NextVenusSuperiorConjunction(eventUTNextQueryTT(windowEnd)) start, windowEnd = venusWestElongationWindowEndingAt(nextSuperior) } + return math.NaN() } func lastVenusGreatestElongationTyped(jde float64, east bool) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } if east { start, windowEnd := venusEastElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryBeforeOrEqual(date, jde) { return date } prevInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(start)) start, windowEnd = venusEastElongationWindowEndingAt(prevInferior) } + return math.NaN() } start, windowEnd := venusWestElongationWindowContaining(jde) - for { + for i := 0; i < eventDirectionalSearchIterations; i++ { date := venusGreatestElongationInWindow(start, windowEnd) + if !isFiniteFloat(date) { + return math.NaN() + } if eventUTQueryBeforeOrEqual(date, jde) { return date } prevSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(start)) start, windowEnd = venusWestElongationWindowEndingAt(prevSuperior) } + return math.NaN() } func venusGreatestElongation(jde float64) float64 { + if !isFiniteFloat(jde) { + return math.NaN() + } east := venusSunRADelta(jde) > 0 if east { return nextVenusGreatestElongationTyped(jde, true) diff --git a/doc/lunar-eclipse-2029-01-01-global-en.svg b/doc/lunar-eclipse-2029-01-01-global-en.svg new file mode 100644 index 0000000..9168d89 --- /dev/null +++ b/doc/lunar-eclipse-2029-01-01-global-en.svg @@ -0,0 +1 @@ +2029-01-01 Total Lunar Eclipse Global VisibilityP1 22:03:54 | Greatest 00:52:05 | P4 03:40:11 (CST) | penumbral magnitude 2.274 | umbral magnitude 1.246Entire eclipseMoonrise during eclipseMoonset during eclipseNot visibleEquirectangular projection; P1/P4 Moon-visible hemispheres; Natural Earth 1:50m physical land, no administrative boundaries. \ No newline at end of file diff --git a/doc/lunar-eclipse-2029-01-01-global.svg b/doc/lunar-eclipse-2029-01-01-global.svg new file mode 100644 index 0000000..a00817c --- /dev/null +++ b/doc/lunar-eclipse-2029-01-01-global.svg @@ -0,0 +1 @@ +2029-01-01 月全食全球可见图P1 22:03:54 | 食甚 00:52:05 | P4 03:40:11 (CST) | 半影食分 2.274 | 本影食分 1.246全程可见带食月出带食月落不可见等经纬投影;按 P1/P4 月球可见半球分区;Natural Earth 1:50m 物理陆地底图,不含行政边界。 \ No newline at end of file diff --git a/doc/lunar-occultation-hr4799-2025-06-05-global-en.svg b/doc/lunar-occultation-hr4799-2025-06-05-global-en.svg new file mode 100644 index 0000000..796d04d --- /dev/null +++ b/doc/lunar-occultation-hr4799-2025-06-05-global-en.svg @@ -0,0 +1 @@ +2025-06-05 HR 4799 Lunar Occultation Global PathStart 2025-06-05 17:45:28.4 | Greatest 20:02:06.3 | End 22:18:49.9 (UTC+8)Greatest point 121.5660°E, 6.8071°N | path width 3582.4 km | Moon altitude +75.6°Global center line and occultation limits-120°-60°60°120°-60°-30°30°60°18:3019:0020:0021:00StartGreatestEndVisible center lineGeometric center lineOccultation limitsGlobal eventsStart17:45:28.463.9752°E, 39.0912°NMoon alt. +0.3°Greatest20:02:06.3121.5660°E, 6.8071°NMoon alt. +75.6°End22:18:49.9172.2784°E, 16.6463°SMoon alt. +0.3°Equirectangular projection with Natural Earth 1:50m physical land and no administrative boundaries. Limits use the outer lunar limb on the Earth ellipsoid. \ No newline at end of file diff --git a/doc/lunar-occultation-hr4799-2025-06-05-global.svg b/doc/lunar-occultation-hr4799-2025-06-05-global.svg new file mode 100644 index 0000000..231b385 --- /dev/null +++ b/doc/lunar-occultation-hr4799-2025-06-05-global.svg @@ -0,0 +1 @@ +2025-06-05 月掩进贤增九(HR 4799)全球见掩图掩始 2025-06-05 17:45:28.4 | 掩甚 20:02:06.3 | 掩终 22:18:49.9 (UTC+8)掩甚点 121.5660°E, 6.8071°N | 掩带宽 3582.4 km | 月球高度 +75.6°全球中心线与掩带边界-120°-60°60°120°-60°-30°30°60°18:3019:0020:0021:00掩始掩甚掩终可见中心线几何中心线掩带边界全球事件掩始17:45:28.463.9752°E, 39.0912°N月球高度 +0.3°掩甚20:02:06.3121.5660°E, 6.8071°N月球高度 +75.6°掩终22:18:49.9172.2784°E, 16.6463°S月球高度 +0.3°等经纬投影;Natural Earth 1:50m 物理陆地底图,不含行政边界;掩带边界为地球椭球上的月球外缘投影。 \ No newline at end of file diff --git a/doc/lunar-occultation-hr4799-2025-06-05-local-en.svg b/doc/lunar-occultation-hr4799-2025-06-05-local-en.svg new file mode 100644 index 0000000..f5fcef7 --- /dev/null +++ b/doc/lunar-occultation-hr4799-2025-06-05-local-en.svg @@ -0,0 +1,2 @@ + +2025-06-05 Lunar Occultation of HR 4799Site 121.5660°E, 6.8071°N | elevation 0 m | total | duration 1h 36m 09.6sGreatest 2025-06-05 20:02:06.3 UTC+8 | minimum separation 0.01 arcsec | Moon altitude +75.6° azimuth 207.9°Topocentric star trackNEWSLunar pathImmersionGreatestEmersionLocal contactsImmersion19:14:01.1PA 138.9° | alt 76.4°az 157.7° | above horizonGreatest20:02:06.3PA 218.4° | alt 75.6°az 207.9° | above horizonEmersion20:50:10.7PA 317.4° | alt 67.3°az 235.5° | above horizonContact stagesImmersion19:14:01.1Greatest20:02:06.3Emersion20:50:10.7Moon fixed at center; east is left and north is up. The blue-gray dashed line is the local lunar path; the red dashed line is the star track.The star is a point source; immersion and emersion occur where it crosses the topocentric apparent lunar limb. Lunar texture is schematic. \ No newline at end of file diff --git a/doc/lunar-occultation-hr4799-2025-06-05-local.svg b/doc/lunar-occultation-hr4799-2025-06-05-local.svg new file mode 100644 index 0000000..8f9e238 --- /dev/null +++ b/doc/lunar-occultation-hr4799-2025-06-05-local.svg @@ -0,0 +1,2 @@ + +2025-06-05 月掩进贤增九(HR 4799)观测点 121.5660°E, 6.8071°N | 海拔 0 米 | 全掩 | 掩星历时 1时36分09.6秒掩甚 2025-06-05 20:02:06.3 UTC+8 | 最小角距 0.01 角秒 | 月球高度 +75.6° 方位 207.9°站心恒星轨迹西白道掩始掩甚掩终本地接触时刻掩始19:14:01.1PA 138.9° | 高度 76.4°方位 157.7° | 地平线上掩甚20:02:06.3PA 218.4° | 高度 75.6°方位 207.9° | 地平线上掩终20:50:10.7PA 317.4° | 高度 67.3°方位 235.5° | 地平线上掩始、掩甚与掩终视圆掩始19:14:01.1掩甚20:02:06.3掩终20:50:10.7月球固定在中心;图上左东右西,向上为北。灰蓝虚线为掩甚附近的站心白道,红色虚线为恒星相对月心轨迹。恒星按点光源绘制;掩始和掩终是恒星穿越站心月球视圆外缘的时刻,月面纹理仅作方向辅助。 \ No newline at end of file diff --git a/doc/solar-eclipse-arctic-2012-global-en.svg b/doc/solar-eclipse-arctic-2012-global-en.svg new file mode 100644 index 0000000..1139985 --- /dev/null +++ b/doc/solar-eclipse-arctic-2012-global-en.svg @@ -0,0 +1 @@ +2012-05-21 Annular Solar Eclipse Global VisibilityPartial begins 04:56:08 | Greatest 07:52:47 | Partial ends 10:49:21 (UTC+8) | magnitude 0.944 | Gamma 0.4828path width 237.1 km | Solar Saros 128, member 58/73 | Sun alt 60.9° az 171.0° | central duration 05:46Global visibility and central path05:0006:0007:0009:0010:0006:3007:0008:0009:0009:30Axis entersAxis exitsP1P4U1U2U3U4GreatestSubsolarPartial-eclipse visibilityAnnular pathCenter linePenumbral outlines (60 min)Terminator at greatestAntumbral outlines (10 min)P/U shadow contactsGlobal phasesP1 Partial begins04:56:08131.0542°E, 10.8905°NSun altitude +0.0°U106:06:18109.6294°E, 20.5716°NSun altitude +0.0°Central begins06:09:02109.3148°E, 21.3879°NSun altitude +0.6°U206:11:48107.7417°E, 21.7712°NSun altitude +0.0°Greatest07:52:47176.2686°E, 49.0974°NPath width 237.1 kmU309:33:42100.1492°W, 33.5217°NSun altitude +0.0°Central ends09:36:26102.1819°W, 33.3062°NSun altitude +0.9°U409:39:10102.1488°W, 32.3505°NSun altitude +0.0°P4 Partial ends10:49:21124.2780°W, 22.8049°NSun altitude +0.0°North-polar azimuthal equidistant projection; sampled penumbral sweep and central path; Natural Earth 1:50m physical land, no administrative boundaries. \ No newline at end of file diff --git a/doc/solar-eclipse-arctic-2012-global.svg b/doc/solar-eclipse-arctic-2012-global.svg new file mode 100644 index 0000000..162b5bb --- /dev/null +++ b/doc/solar-eclipse-arctic-2012-global.svg @@ -0,0 +1 @@ +2012-05-21 日环食全球见食图偏食始 04:56:08 | 食甚 07:52:47 | 偏食终 10:49:21 (UTC+8) | 食分 0.944 | Gamma 0.4828食带宽 237.1 km | 太阳沙罗 128,第 58/73 个成员 | 食甚点太阳高度 60.9° 方位 171.0° | 中心食持续 05:46全球见食范围与中心食带05:0006:0007:0009:0010:0006:3007:0008:0009:0009:30中心线始中心线终P1P4U1U2U3U4食甚日下点偏食可见区环食带中心线半影时刻线(60 分钟)食甚晨昏圈反本影轮廓(10 分钟)P/U 影锥接触全球阶段P1 偏食始04:56:08131.0542°E, 10.8905°N太阳高度 +0.0°U106:06:18109.6294°E, 20.5716°N太阳高度 +0.0°中心食始06:09:02109.3148°E, 21.3879°N太阳高度 +0.6°U206:11:48107.7417°E, 21.7712°N太阳高度 +0.0°食甚07:52:47176.2686°E, 49.0974°N食带宽 237.1 kmU309:33:42100.1492°W, 33.5217°N太阳高度 +0.0°中心食终09:36:26102.1819°W, 33.3062°N太阳高度 +0.9°U409:39:10102.1488°W, 32.3505°N太阳高度 +0.0°P4 偏食终10:49:21124.2780°W, 22.8049°N太阳高度 +0.0°北极方位等距投影;偏食区为半影足迹时间扫掠,叠加中心食带;Natural Earth 1:50m 物理陆地底图,不含行政边界。 \ No newline at end of file diff --git a/doc/solar-eclipse-beijing-2035-global-en.svg b/doc/solar-eclipse-beijing-2035-global-en.svg new file mode 100644 index 0000000..b2f9a5c --- /dev/null +++ b/doc/solar-eclipse-beijing-2035-global-en.svg @@ -0,0 +1 @@ +2035-09-02 Total Solar Eclipse Global VisibilityPartial begins 07:15:35 | Greatest 09:55:36 | Partial ends 12:35:47 (UTC+8) | magnitude 1.032 | Gamma 0.3727path width 116.6 km | Solar Saros 145, member 23/77 | Sun alt 67.9° az 198.5° | central duration 02:54Global visibility and central path08:0009:0010:0011:0012:0008:3009:0010:00Axis entersAxis exitsP1P2P3P4U1U2U3U4GreatestSubsolarPartial-eclipse visibilityPath of totalityCenter linePenumbral outlines (60 min)Terminator at greatestUmbral outlines (10 min)P/U shadow contactsGlobal phasesP1 Partial begins07:15:35U108:15:55Central begins08:16:26U208:16:56P209:27:39Greatest09:55:36P310:23:51U311:34:27Central ends11:34:55U411:35:23P4 Partial ends12:35:47Equirectangular projection; sampled penumbral sweep and central path; Natural Earth 1:50m physical land, no administrative boundaries. \ No newline at end of file diff --git a/doc/solar-eclipse-beijing-2035-global.svg b/doc/solar-eclipse-beijing-2035-global.svg new file mode 100644 index 0000000..f728dbb --- /dev/null +++ b/doc/solar-eclipse-beijing-2035-global.svg @@ -0,0 +1 @@ +2035-09-02 日全食全球见食图偏食始 07:15:35 | 食甚 09:55:36 | 偏食终 12:35:47 (UTC+8) | 食分 1.032 | Gamma 0.3727食带宽 116.6 km | 太阳沙罗 145,第 23/77 个成员 | 食甚点太阳高度 67.9° 方位 198.5° | 中心食持续 02:54全球见食范围与中心食带08:0009:0010:0011:0012:0008:3009:0010:00中心线始中心线终P1P2P3P4U1U2U3U4食甚日下点偏食可见区全食带中心线半影时刻线(60 分钟)食甚晨昏圈本影轮廓(10 分钟)P/U 影锥接触全球阶段P1 偏食始07:15:35U108:15:55中心食始08:16:26U208:16:56P209:27:39食甚09:55:36P310:23:51U311:34:27中心食终11:34:55U411:35:23P4 偏食终12:35:47等经纬投影;偏食区为半影足迹时间扫掠,叠加中心食带;Natural Earth 1:50m 物理陆地底图,不含行政边界。 \ No newline at end of file diff --git a/doc/solar-eclipse-xiamen-2012-global-en.svg b/doc/solar-eclipse-xiamen-2012-global-en.svg new file mode 100644 index 0000000..57868e3 --- /dev/null +++ b/doc/solar-eclipse-xiamen-2012-global-en.svg @@ -0,0 +1 @@ +2012-05-21 Annular Solar Eclipse Global VisibilityPartial begins 04:56:08 | Greatest 07:52:47 | Partial ends 10:49:21 (UTC+8) | magnitude 0.944 | Gamma 0.4828path width 237.1 km | Solar Saros 128, member 58/73 | Sun alt 60.9° az 171.0° | central duration 05:46Global visibility and central path05:0006:0007:0008:0009:0010:0006:3007:3008:3009:30Axis entersAxis exitsP1P4U1U2U3U4GreatestSubsolarPartial-eclipse visibilityAnnular pathCenter linePenumbral outlines (60 min)Terminator at greatestAntumbral outlines (10 min)P/U shadow contactsGlobal phasesP1 Partial begins04:56:08U106:06:18Central begins06:09:02U206:11:48Greatest07:52:47U309:33:42Central ends09:36:26U409:39:10P4 Partial ends10:49:21Equirectangular projection; sampled penumbral sweep and central path; Natural Earth 1:50m physical land, no administrative boundaries. \ No newline at end of file diff --git a/doc/solar-eclipse-xiamen-2012-global.svg b/doc/solar-eclipse-xiamen-2012-global.svg new file mode 100644 index 0000000..3b4cb88 --- /dev/null +++ b/doc/solar-eclipse-xiamen-2012-global.svg @@ -0,0 +1 @@ +2012-05-21 日环食全球见食图偏食始 04:56:08 | 食甚 07:52:47 | 偏食终 10:49:21 (UTC+8) | 食分 0.944 | Gamma 0.4828食带宽 237.1 km | 太阳沙罗 128,第 58/73 个成员 | 食甚点太阳高度 60.9° 方位 171.0° | 中心食持续 05:46全球见食范围与中心食带05:0006:0007:0008:0009:0010:0006:3007:3008:3009:30中心线始中心线终P1P4U1U2U3U4食甚日下点偏食可见区环食带中心线半影时刻线(60 分钟)食甚晨昏圈反本影轮廓(10 分钟)P/U 影锥接触全球阶段P1 偏食始04:56:08U106:06:18中心食始06:09:02U206:11:48食甚07:52:47U309:33:42中心食终09:36:26U409:39:10P4 偏食终10:49:21等经纬投影;偏食区为半影足迹时间扫掠,叠加中心食带;Natural Earth 1:50m 物理陆地底图,不含行政边界。 \ No newline at end of file diff --git a/doc/solar-eclipse-yangshan-2009-global-en.svg b/doc/solar-eclipse-yangshan-2009-global-en.svg new file mode 100644 index 0000000..941fea9 --- /dev/null +++ b/doc/solar-eclipse-yangshan-2009-global-en.svg @@ -0,0 +1 @@ +2009-07-22 Total Solar Eclipse Global VisibilityPartial begins 07:58:15 | Greatest 10:35:18 | Partial ends 13:12:22 (UTC+8) | magnitude 1.080 | Gamma 0.0698path width 258.3 km | Solar Saros 136, member 37/71 | Sun alt 85.9° az 197.6° | central duration 06:39Global visibility and central path08:0009:0010:0011:0012:0013:0009:0009:3010:3011:30Axis entersAxis exitsP1P2P3P4U1U2U3U4GreatestSubsolarPartial-eclipse visibilityPath of totalityCenter linePenumbral outlines (60 min)Terminator at greatestUmbral outlines (10 min)P/U shadow contactsGlobal phasesP1 Partial begins07:58:15U108:51:14Central begins08:52:51U208:54:28P209:47:39Greatest10:35:18P311:23:00U312:16:10Central ends12:17:46U412:19:23P4 Partial ends13:12:22Equirectangular projection; sampled penumbral sweep and central path; Natural Earth 1:50m physical land, no administrative boundaries. \ No newline at end of file diff --git a/doc/solar-eclipse-yangshan-2009-global.svg b/doc/solar-eclipse-yangshan-2009-global.svg new file mode 100644 index 0000000..b0c90d5 --- /dev/null +++ b/doc/solar-eclipse-yangshan-2009-global.svg @@ -0,0 +1 @@ +2009-07-22 日全食全球见食图偏食始 07:58:15 | 食甚 10:35:18 | 偏食终 13:12:22 (UTC+8) | 食分 1.080 | Gamma 0.0698食带宽 258.3 km | 太阳沙罗 136,第 37/71 个成员 | 食甚点太阳高度 85.9° 方位 197.6° | 中心食持续 06:39全球见食范围与中心食带08:0009:0010:0011:0012:0013:0009:0009:3010:3011:30中心线始中心线终P1P2P3P4U1U2U3U4食甚日下点偏食可见区全食带中心线半影时刻线(60 分钟)食甚晨昏圈本影轮廓(10 分钟)P/U 影锥接触全球阶段P1 偏食始07:58:15U108:51:14中心食始08:52:51U208:54:28P209:47:39食甚10:35:18P311:23:00U312:16:10中心食终12:17:46U412:19:23P4 偏食终13:12:22等经纬投影;偏食区为半影足迹时间扫掠,叠加中心食带;Natural Earth 1:50m 物理陆地底图,不含行政边界。 \ No newline at end of file diff --git a/eclipse/solar_path.go b/eclipse/solar_path.go index 800143c..0e36085 100644 --- a/eclipse/solar_path.go +++ b/eclipse/solar_path.go @@ -63,6 +63,9 @@ type SolarEclipsePartialFootprintOptions struct { // BoundaryPoints 是每个瞬时半影边界的角向采样点数;<=0 时使用 180。 // BoundaryPoints is the angular sample count for each instantaneous penumbral boundary; values <= 0 use 180. BoundaryPoints int + // CentralShadowStep 是本影/反本影瞬时足迹的采样步长;<=0 时不计算。 + // CentralShadowStep is the umbral/antumbral footprint step; values <= 0 disable it. + CentralShadowStep time.Duration } // SolarEclipsePartialAreaOptions 是 SolarEclipsePartialFootprintOptions 的兼容别名。 @@ -89,11 +92,29 @@ type SolarEclipsePartialFootprintsInfo struct { Eclipse SolarEclipseInfo // Footprints 是按时间采样的瞬时半影足迹, sampled instantaneous penumbral footprints. Footprints []SolarEclipsePartialFootprint + // CentralShadowFootprints 是按时间采样的本影/反本影足迹。 + // CentralShadowFootprints are sampled umbral/antumbral footprints. + CentralShadowFootprints []SolarEclipsePartialFootprint + // P1-P4 是半影与地球的外切/内切接触点;不存在的内切点保持零值。 + // P1-P4 are external/internal penumbral contacts; absent internal contacts remain zero. + P1 SolarEclipsePathPoint + P2 SolarEclipsePathPoint + P3 SolarEclipsePathPoint + P4 SolarEclipsePathPoint + // U1-U4 是本影/反本影与地球的外切/内切接触点;不存在时保持零值。 + // U1-U4 are external/internal umbral/antumbral contacts; absent contacts remain zero. + U1 SolarEclipsePathPoint + U2 SolarEclipsePathPoint + U3 SolarEclipsePathPoint + U4 SolarEclipsePathPoint // Step 是实际采用的基础时间采样步长, effective base time step. Step time.Duration // BoundaryPoints 是实际采用的边界角向采样点数。 // BoundaryPoints is the effective angular sample count for each boundary. BoundaryPoints int + // CentralShadowStep 是本影/反本影足迹的实际采样步长;0 表示未计算。 + // CentralShadowStep is the effective umbral/antumbral footprint step; zero means disabled. + CentralShadowStep time.Duration } // SolarEclipsePartialAreaInfo 是 SolarEclipsePartialFootprintsInfo 的兼容别名。 @@ -103,55 +124,55 @@ type SolarEclipsePartialAreaInfo = SolarEclipsePartialFootprintsInfo type solarEclipsePathCalculator func(float64, basic.SolarEclipsePathOptions) basic.SolarEclipsePathResult type solarEclipsePartialFootprintsCalculator func(float64, basic.SolarEclipsePartialFootprintOptions) basic.SolarEclipsePartialFootprintsResult -// SolarEclipseCentralPath 日食中心路径查询 / central solar eclipse path query. +// SolarEclipseCentralPath 计算指定日期附近的日食中心路径,默认使用 NASA bulletin Split-K 模型。 // SolarEclipseCentralPath computes the central path near the given date, using NASA bulletin Split-K by default. func SolarEclipseCentralPath(date time.Time, options SolarEclipsePathOptions) (SolarEclipsePath, bool) { return SolarEclipseCentralPathNASABulletinSplitK(date, options) } -// SolarEclipseCentralPathNASABulletinSplitK 日食中心路径查询(NASA bulletin Split-K) / central solar eclipse path query with NASA bulletin Split-K. +// SolarEclipseCentralPathNASABulletinSplitK 使用 NASA bulletin Split-K 模型计算日食中心路径。 // SolarEclipseCentralPathNASABulletinSplitK computes the central path with the NASA bulletin Split-K model. func SolarEclipseCentralPathNASABulletinSplitK(date time.Time, options SolarEclipsePathOptions) (SolarEclipsePath, bool) { return solarEclipseCentralPath(date, options, basic.SolarEclipseCentralPathNASABulletinSplitK) } -// SolarEclipseCentralPathIAUSingleK 日食中心路径查询(IAU Single-K) / central solar eclipse path query with IAU Single-K. +// SolarEclipseCentralPathIAUSingleK 使用 IAU Single-K 模型计算日食中心路径。 // SolarEclipseCentralPathIAUSingleK computes the central path with the IAU Single-K model. func SolarEclipseCentralPathIAUSingleK(date time.Time, options SolarEclipsePathOptions) (SolarEclipsePath, bool) { return solarEclipseCentralPath(date, options, basic.SolarEclipseCentralPathIAUSingleK) } -// SolarEclipsePartialFootprints 日食偏食足迹查询 / solar eclipse penumbral footprints query. +// SolarEclipsePartialFootprints 计算指定日期附近的日食半影足迹,默认使用 NASA bulletin Split-K 模型。 // SolarEclipsePartialFootprints computes penumbral footprint samples near the given date, using NASA bulletin Split-K by default. func SolarEclipsePartialFootprints(date time.Time, options SolarEclipsePartialFootprintOptions) (SolarEclipsePartialFootprintsInfo, bool) { return SolarEclipsePartialFootprintsNASABulletinSplitK(date, options) } -// SolarEclipsePartialFootprintsNASABulletinSplitK 日食偏食足迹查询(NASA bulletin Split-K) / solar eclipse penumbral footprints query with NASA bulletin Split-K. +// SolarEclipsePartialFootprintsNASABulletinSplitK 使用 NASA bulletin Split-K 模型计算日食半影足迹。 // SolarEclipsePartialFootprintsNASABulletinSplitK computes penumbral footprint samples with the NASA bulletin Split-K model. func SolarEclipsePartialFootprintsNASABulletinSplitK(date time.Time, options SolarEclipsePartialFootprintOptions) (SolarEclipsePartialFootprintsInfo, bool) { return solarEclipsePartialFootprints(date, options, basic.SolarEclipsePartialFootprintsNASABulletinSplitK) } -// SolarEclipsePartialFootprintsIAUSingleK 日食偏食足迹查询(IAU Single-K) / solar eclipse penumbral footprints query with IAU Single-K. +// SolarEclipsePartialFootprintsIAUSingleK 使用 IAU Single-K 模型计算日食半影足迹。 // SolarEclipsePartialFootprintsIAUSingleK computes penumbral footprint samples with the IAU Single-K model. func SolarEclipsePartialFootprintsIAUSingleK(date time.Time, options SolarEclipsePartialFootprintOptions) (SolarEclipsePartialFootprintsInfo, bool) { return solarEclipsePartialFootprints(date, options, basic.SolarEclipsePartialFootprintsIAUSingleK) } -// SolarEclipsePartialArea 偏食足迹兼容包装 / compatibility wrapper for penumbral footprints. +// SolarEclipsePartialArea 计算半影足迹,是 SolarEclipsePartialFootprints 的兼容包装。 // SolarEclipsePartialArea computes penumbral footprint samples and is a compatibility wrapper for SolarEclipsePartialFootprints. func SolarEclipsePartialArea(date time.Time, options SolarEclipsePartialAreaOptions) (SolarEclipsePartialAreaInfo, bool) { return SolarEclipsePartialFootprints(date, options) } -// SolarEclipsePartialAreaNASABulletinSplitK 偏食足迹兼容包装(NASA bulletin Split-K) / compatibility wrapper for penumbral footprints with NASA bulletin Split-K. +// SolarEclipsePartialAreaNASABulletinSplitK 是 SolarEclipsePartialFootprintsNASABulletinSplitK 的兼容包装。 // SolarEclipsePartialAreaNASABulletinSplitK is a compatibility wrapper for SolarEclipsePartialFootprintsNASABulletinSplitK. func SolarEclipsePartialAreaNASABulletinSplitK(date time.Time, options SolarEclipsePartialAreaOptions) (SolarEclipsePartialAreaInfo, bool) { return SolarEclipsePartialFootprintsNASABulletinSplitK(date, options) } -// SolarEclipsePartialAreaIAUSingleK 偏食足迹兼容包装(IAU Single-K) / compatibility wrapper for penumbral footprints with IAU Single-K. +// SolarEclipsePartialAreaIAUSingleK 是 SolarEclipsePartialFootprintsIAUSingleK 的兼容包装。 // SolarEclipsePartialAreaIAUSingleK is a compatibility wrapper for SolarEclipsePartialFootprintsIAUSingleK. func SolarEclipsePartialAreaIAUSingleK(date time.Time, options SolarEclipsePartialAreaOptions) (SolarEclipsePartialAreaInfo, bool) { return SolarEclipsePartialFootprintsIAUSingleK(date, options) @@ -192,10 +213,20 @@ func solarEclipsePartialFootprints( } footprints := SolarEclipsePartialFootprintsInfo{ - Eclipse: solarEclipseInfoFromBasic(result.Eclipse, location), - Footprints: solarEclipsePartialFootprintsFromBasic(result.Footprints, location), - Step: solarEclipsePathStepDuration(result.StepDays), - BoundaryPoints: result.BoundaryPoints, + Eclipse: solarEclipseInfoFromBasic(result.Eclipse, location), + Footprints: solarEclipsePartialFootprintsFromBasic(result.Footprints, location), + CentralShadowFootprints: solarEclipsePartialFootprintsFromBasic(result.CentralShadowFootprints, location), + P1: solarEclipseOptionalPathPointFromBasic(result.P1, location), + P2: solarEclipseOptionalPathPointFromBasic(result.P2, location), + P3: solarEclipseOptionalPathPointFromBasic(result.P3, location), + P4: solarEclipseOptionalPathPointFromBasic(result.P4, location), + U1: solarEclipseOptionalPathPointFromBasic(result.U1, location), + U2: solarEclipseOptionalPathPointFromBasic(result.U2, location), + U3: solarEclipseOptionalPathPointFromBasic(result.U3, location), + U4: solarEclipseOptionalPathPointFromBasic(result.U4, location), + Step: solarEclipsePathStepDuration(result.StepDays), + BoundaryPoints: result.BoundaryPoints, + CentralShadowStep: solarEclipsePathStepDuration(result.CentralShadowStepDays), } return footprints, true } @@ -217,6 +248,9 @@ func basicSolarEclipsePartialFootprintOptions(options SolarEclipsePartialFootpri if options.Step > 0 { basicOptions.StepDays = options.Step.Hours() / 24 } + if options.CentralShadowStep > 0 { + basicOptions.CentralShadowStepDays = options.CentralShadowStep.Hours() / 24 + } return basicOptions } @@ -245,6 +279,13 @@ func solarEclipsePathPointFromBasic(point basic.SolarEclipsePathPoint, location } } +func solarEclipseOptionalPathPointFromBasic(point basic.SolarEclipsePathPoint, location *time.Location) SolarEclipsePathPoint { + if point.JDE == 0 { + return SolarEclipsePathPoint{} + } + return solarEclipsePathPointFromBasic(point, location) +} + func solarEclipsePartialFootprintsFromBasic( footprints []basic.SolarEclipsePartialFootprint, location *time.Location, diff --git a/eclipse/solar_path_test.go b/eclipse/solar_path_test.go index 590033f..c0708f1 100644 --- a/eclipse/solar_path_test.go +++ b/eclipse/solar_path_test.go @@ -64,8 +64,9 @@ func TestSolarEclipsePartialFootprintsKeepLocation(t *testing.T) { footprints, ok := SolarEclipsePartialFootprints( time.Date(2024, 4, 8, 12, 0, 0, 0, loc), SolarEclipsePartialFootprintOptions{ - Step: 30 * time.Minute, - BoundaryPoints: 72, + Step: 30 * time.Minute, + BoundaryPoints: 72, + CentralShadowStep: 10 * time.Minute, }, ) if !ok { @@ -80,6 +81,9 @@ func TestSolarEclipsePartialFootprintsKeepLocation(t *testing.T) { if footprints.BoundaryPoints != 72 { t.Fatalf("boundary points mismatch: got %d want 72", footprints.BoundaryPoints) } + if footprints.CentralShadowStep != 10*time.Minute || len(footprints.CentralShadowFootprints) == 0 { + t.Fatalf("central shadow sampling mismatch: step=%s footprints=%d", footprints.CentralShadowStep, len(footprints.CentralShadowFootprints)) + } for _, item := range []struct { name string @@ -89,6 +93,15 @@ func TestSolarEclipsePartialFootprintsKeepLocation(t *testing.T) { {name: "Footprints[0].Time", tm: footprints.Footprints[0].Time}, {name: "Footprints[last].Time", tm: footprints.Footprints[len(footprints.Footprints)-1].Time}, {name: "Boundary point", tm: footprints.Footprints[0].Boundaries[0][0].Time}, + {name: "P1 contact", tm: footprints.P1.Time}, + {name: "P2 contact", tm: footprints.P2.Time}, + {name: "P3 contact", tm: footprints.P3.Time}, + {name: "P4 contact", tm: footprints.P4.Time}, + {name: "U1 contact", tm: footprints.U1.Time}, + {name: "U2 contact", tm: footprints.U2.Time}, + {name: "U3 contact", tm: footprints.U3.Time}, + {name: "U4 contact", tm: footprints.U4.Time}, + {name: "Central shadow", tm: footprints.CentralShadowFootprints[0].Time}, } { if item.tm.Location() != loc { t.Fatalf("%s location mismatch: got %q want %q", item.name, item.tm.Location(), loc) @@ -107,6 +120,9 @@ func TestSolarEclipsePartialFootprintsWorkForPartialOnly(t *testing.T) { if footprints.Eclipse.Type != SolarEclipsePartial { t.Fatalf("unexpected eclipse type: got %s want %s", footprints.Eclipse.Type, SolarEclipsePartial) } + if !footprints.U1.Time.IsZero() || !footprints.U4.Time.IsZero() || len(footprints.CentralShadowFootprints) != 0 { + t.Fatal("partial-only eclipse must not return central-shadow contacts or footprints") + } } func TestSolarEclipsePartialFootprintsReturnFalseForNoEvent(t *testing.T) { diff --git a/eclipse/svg/lunar.go b/eclipse/svg/lunar.go index 117c99e..39e1e77 100644 --- a/eclipse/svg/lunar.go +++ b/eclipse/svg/lunar.go @@ -9,6 +9,7 @@ import ( "b612.me/astro/basic" eclipsecore "b612.me/astro/eclipse" + "b612.me/astro/internal/svgasset" ) const ( @@ -23,7 +24,7 @@ const ( // LunarEclipseSVGOptions 控制月食穿影 SVG 输出。 // LunarEclipseSVGOptions controls lunar eclipse shadow-path SVG output. type LunarEclipseSVGOptions struct { - // Width / Height 是 SVG 画布尺寸;<=0 时使用默认尺寸。 + // Width 和 Height 是 SVG 画布尺寸;<=0 时使用默认尺寸。 // Width/Height are SVG canvas size; values <= 0 use defaults. Width int Height int @@ -168,7 +169,7 @@ func renderLunarEclipseSVG( var b strings.Builder fmt.Fprintf(&b, ``, options.Width, options.Height, options.Width, options.Height) b.WriteString(``) - b.WriteString(lunarEclipseSVGMoonSymbol) + b.WriteString(svgasset.MoonFaceSymbol()) b.WriteString(``) b.WriteString(``) fmt.Fprintf(&b, ``, diff --git a/eclipse/svg/lunar_asset.go b/eclipse/svg/lunar_asset.go deleted file mode 100644 index a6732fe..0000000 --- a/eclipse/svg/lunar_asset.go +++ /dev/null @@ -1,9 +0,0 @@ -package svg - -import _ "embed" - -// lunarEclipseSVGMoonSymbol is a compact public-domain Moon face derived from -// labs/Full_Moon_clip_art.svg and simplified for small eclipse contact disks. -// -//go:embed lunar_eclipse_moon.svg -var lunarEclipseSVGMoonSymbol string diff --git a/eclipse/svg/lunar_map.go b/eclipse/svg/lunar_map.go new file mode 100644 index 0000000..19553d5 --- /dev/null +++ b/eclipse/svg/lunar_map.go @@ -0,0 +1,341 @@ +package svg + +import ( + "fmt" + "html" + "math" + "strings" + "time" + + "b612.me/astro/basic" + eclipsecore "b612.me/astro/eclipse" + "b612.me/astro/internal/svgmap" +) + +const ( + lunarEclipseMapDefaultWidth = 960 + lunarEclipseMapDefaultHeight = 640 +) + +// LunarEclipseMapSVGOptions 控制无国界全球可见性地图。 +// LunarEclipseMapSVGOptions controls a border-free global visibility map. +type LunarEclipseMapSVGOptions struct { + // Width 和 Height 是 SVG 画布尺寸;宽度小于 640 或高度小于 420 时使用 960x640 默认值。 + // Width and Height are SVG canvas dimensions in user units. Width values below 640 and height values below 420 use the 960x640 defaults. + Width int + Height int + // Language 为 "en"(不区分大小写)时使用英文,否则使用中文。 + // Language uses English for "en" (case-insensitive) and Chinese otherwise. + Language string + // Location 控制显示的事件时刻;nil 使用 date.Location()。 + // Location controls displayed event times. Nil uses date.Location(). + Location *time.Location + // Projection 选择地图投影;零值使用等经纬投影,不支持的值使渲染器返回 false。 + // Projection selects the map projection. The zero value selects the equirectangular projection; unsupported values make the renderer return false. + Projection EclipseMapProjection + // 空文本字段使用本地化的自动标签。 + // Empty text fields use localized automatic labels. + Title string + FooterNote string +} + +// LunarEclipseMapSVG 使用默认月食模型绘制全球 P1-P4 可见区域。 +// LunarEclipseMapSVG renders the global P1-P4 visibility regions using the default lunar-eclipse model. +func LunarEclipseMapSVG(date time.Time, options LunarEclipseMapSVGOptions) (string, bool) { + return lunarEclipseMapSVG(date, options, eclipsecore.LunarEclipseOnDate) +} + +// LunarEclipseMapSVGDanjon 使用 Danjon 模型绘制全球可见区域。 +// LunarEclipseMapSVGDanjon renders the global visibility regions with Danjon's model. +func LunarEclipseMapSVGDanjon(date time.Time, options LunarEclipseMapSVGOptions) (string, bool) { + return lunarEclipseMapSVG(date, options, eclipsecore.LunarEclipseOnDateDanjon) +} + +// LunarEclipseMapSVGChauvenet 使用 Chauvenet 模型绘制全球可见区域。 +// LunarEclipseMapSVGChauvenet renders the global visibility regions with Chauvenet's model. +func LunarEclipseMapSVGChauvenet(date time.Time, options LunarEclipseMapSVGOptions) (string, bool) { + return lunarEclipseMapSVG(date, options, eclipsecore.LunarEclipseOnDateChauvenet) +} + +func lunarEclipseMapSVG( + date time.Time, + options LunarEclipseMapSVGOptions, + calculator func(time.Time) (eclipsecore.LunarEclipseInfo, bool), +) (string, bool) { + if !validEclipseMapProjection(options.Projection) { + return "", false + } + info, ok := calculator(date) + if !ok || info.PenumbralStart.IsZero() || info.PenumbralEnd.IsZero() { + return "", false + } + options = normalizeLunarEclipseMapSVGOptions(date, options) + projection := internalEclipseMapProjection(options.Projection) + if projection == "" { + projection = svgmap.ProjectionEquirectangular + } + return renderLunarEclipseMapSVG(info, options, projection), true +} + +func normalizeLunarEclipseMapSVGOptions(date time.Time, options LunarEclipseMapSVGOptions) LunarEclipseMapSVGOptions { + if options.Width < 640 { + options.Width = lunarEclipseMapDefaultWidth + } + if options.Height < 420 { + options.Height = lunarEclipseMapDefaultHeight + } + if strings.EqualFold(options.Language, "en") { + options.Language = "en" + } else { + options.Language = "zh" + } + if options.Location == nil { + options.Location = date.Location() + } + return options +} + +func renderLunarEclipseMapSVG( + info eclipsecore.LunarEclipseInfo, + options LunarEclipseMapSVGOptions, + projection svgmap.Projection, +) string { + frame := eclipseMapFrame(options.Width, options.Height, projection, 142, 92) + startPath, startBoundary := lunarEclipseVisibilityPath(info.PenumbralStart, frame) + endPath, endBoundary := lunarEclipseVisibilityPath(info.PenumbralEnd, frame) + title := options.Title + if title == "" { + date := info.Maximum.In(options.Location).Format("2006-01-02") + if options.Language == "en" { + title = fmt.Sprintf("%s %s Global Visibility", date, lunarEclipseSVGTypeName(info.Type, "en")) + } else { + title = fmt.Sprintf("%s %s全球可见图", date, lunarEclipseSVGTypeName(info.Type, "zh")) + } + } + + var builder strings.Builder + fmt.Fprintf(&builder, ``, + options.Width, options.Height, options.Width, options.Height, html.EscapeString(title)) + builder.WriteString(``) + builder.WriteString(frame.ClipDefinition("lunar-map-clip")) + fmt.Fprintf(&builder, ``, startPath) + fmt.Fprintf(&builder, ``, endPath) + builder.WriteString(``) + writeLunarEclipseVisibilityMasks(&builder, frame) + builder.WriteString(``) + builder.WriteString(``) + fmt.Fprintf(&builder, ``, + options.Width-44, options.Height-36) + fmt.Fprintf(&builder, `%s`, + float64(options.Width)/2, html.EscapeString(title)) + writeLunarEclipseMapSummary(&builder, info, options) + + frame.WriteOcean(&builder) + frame.WriteGraticule(&builder, "lunar-map-clip") + frame.WriteLand(&builder, "lunar-map-clip") + fmt.Fprintf(&builder, ``) + writeLunarEclipseUnavailableRegion(&builder, frame) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(``) + writeEclipseMapGeoLine(&builder, frame, startBoundary, "p1-horizon", "#a56c16", 1.2, "4 3", "lunar-map-clip") + writeEclipseMapGeoLine(&builder, frame, endBoundary, "p4-horizon", "#197a82", 1.2, "4 3", "lunar-map-clip") + frame.WriteFrame(&builder) + writeLunarEclipseMapLegend(&builder, frame, options.Language) + writeLunarEclipseMapFooter(&builder, frame, options, projection) + builder.WriteString(``) + return builder.String() +} + +func writeLunarEclipseVisibilityMasks(builder *strings.Builder, frame svgmap.Frame) { + writeLunarEclipseVisibilityMask(builder, "lunar-not-visible-mask", frame, + "lunar-visible-p1-shape", "lunar-visible-p4-shape") + writeLunarEclipseVisibilityMask(builder, "lunar-not-p4-mask", frame, "lunar-visible-p4-shape") + writeLunarEclipseVisibilityMask(builder, "lunar-not-p1-mask", frame, "lunar-visible-p1-shape") +} + +func writeLunarEclipseVisibilityMask( + builder *strings.Builder, + id string, + frame svgmap.Frame, + excludedShapeIDs ...string, +) { + fmt.Fprintf(builder, ``, + id, frame.X, frame.Y, frame.Width, frame.Height) + fmt.Fprintf(builder, ``, + frame.X, frame.Y, frame.Width, frame.Height) + for _, shapeID := range excludedShapeIDs { + fmt.Fprintf(builder, ``, shapeID) + } + builder.WriteString(``) +} + +func eclipseMapFrame(width, height int, projection svgmap.Projection, top, bottom float64) svgmap.Frame { + availableWidth := float64(width) - 90 + availableHeight := float64(height) - top - bottom + mapWidth := math.Min(availableWidth, availableHeight*2) + mapHeight := mapWidth / 2 + if projection != svgmap.ProjectionEquirectangular { + mapWidth = math.Min(availableWidth, availableHeight) + mapHeight = mapWidth + } + return svgmap.Frame{ + X: (float64(width) - mapWidth) / 2, + Y: top, + Width: mapWidth, + Height: mapHeight, + Projection: projection, + } +} + +func lunarEclipseVisibilityPath(value time.Time, frame svgmap.Frame) (string, []svgmap.GeoPoint) { + center := lunarEclipseSubpoint(value) + boundary := svgmap.SphericalCircle(center, 90, 360) + path := lunarEclipseVisibilityPathForCenter(center, frame) + closedBoundary := append(append([]svgmap.GeoPoint(nil), boundary...), boundary[0]) + return path, closedBoundary +} + +func lunarEclipseVisibilityPathForCenter(center svgmap.GeoPoint, frame svgmap.Frame) string { + var builder strings.Builder + for _, polygon := range svgmap.VisibleHemispherePolygons(center, frame.Projection, 360) { + appendEclipseMapPolygonPath(&builder, frame, polygon) + } + return builder.String() +} + +func lunarEclipseVisibilityPolygons( + center svgmap.GeoPoint, + projection svgmap.Projection, + samples int, +) [][]svgmap.GeoPoint { + return svgmap.VisibleHemispherePolygons(center, projection, samples) +} + +func lunarEclipseSubpoint(value time.Time) svgmap.GeoPoint { + ttJDE := timeToTTJDE(value) + ra, dec := basic.HMoonTrueRaDec(ttJDE) + utJDE := basic.TD2UT(ttJDE, false) + longitude := normalizeDegree180(ra - basic.ApparentSiderealTime(utJDE)*15) + return svgmap.GeoPoint{Longitude: longitude, Latitude: dec} +} + +func appendEclipseMapPolygonPath(builder *strings.Builder, frame svgmap.Frame, points []svgmap.GeoPoint) { + if len(points) < 3 { + return + } + for index, point := range points { + x, y, ok := frame.Project(point.Longitude, point.Latitude) + if !ok { + continue + } + command := "L" + if index == 0 { + command = "M" + } + fmt.Fprintf(builder, `%s %.3f %.3f `, command, x, y) + } + builder.WriteString(`Z `) +} + +func writeEclipseMapGeoLine( + builder *strings.Builder, + frame svgmap.Frame, + points []svgmap.GeoPoint, + className, color string, + strokeWidth float64, + dash, clipID string, +) { + for _, segment := range svgmap.PolylineSegments(points, frame.Projection) { + if len(segment) < 2 { + continue + } + fmt.Fprintf(builder, ``, + clipID, color, strokeWidth, dash) + } +} + +func writeLunarEclipseUnavailableRegion(builder *strings.Builder, frame svgmap.Frame) { + if frame.IsPolar() { + fmt.Fprintf(builder, ``, + frame.X+frame.Width/2, frame.Y+frame.Height/2, frame.Width/2) + return + } + fmt.Fprintf(builder, ``, + frame.X, frame.Y, frame.Width, frame.Height) +} + +func writeLunarEclipseMapSummary(builder *strings.Builder, info eclipsecore.LunarEclipseInfo, options LunarEclipseMapSVGOptions) { + start := info.PenumbralStart.In(options.Location) + maximum := info.Maximum.In(options.Location) + end := info.PenumbralEnd.In(options.Location) + zone, _ := maximum.Zone() + if zone == "" { + zone = "UTC" + } + text := fmt.Sprintf("P1 %s | 食甚 %s | P4 %s (%s) | 半影食分 %.3f | 本影食分 %.3f", + start.Format("15:04:05"), maximum.Format("15:04:05"), end.Format("15:04:05"), zone, + info.PenumbralMagnitude, info.UmbralMagnitude) + if options.Language == "en" { + text = fmt.Sprintf("P1 %s | Greatest %s | P4 %s (%s) | penumbral magnitude %.3f | umbral magnitude %.3f", + start.Format("15:04:05"), maximum.Format("15:04:05"), end.Format("15:04:05"), zone, + info.PenumbralMagnitude, info.UmbralMagnitude) + } + fmt.Fprintf(builder, `%s`, + float64(options.Width)/2, html.EscapeString(text)) +} + +func writeLunarEclipseMapLegend(builder *strings.Builder, frame svgmap.Frame, language string) { + labels := []string{"全程可见", "带食月出", "带食月落", "不可见"} + colors := []string{"#5e846d", "#4e9da0", "#e2aa4b", "#747b7d"} + if language == "en" { + labels = []string{"Entire eclipse", "Moonrise during eclipse", "Moonset during eclipse", "Not visible"} + } + y := frame.Y + frame.Height + 31 + itemWidth := frame.Width / 4 + for index, label := range labels { + x := frame.X + float64(index)*itemWidth + fmt.Fprintf(builder, ``, x, y-8, colors[index]) + fmt.Fprintf(builder, `%s`, + x+24, y, html.EscapeString(label)) + } +} + +func writeLunarEclipseMapFooter( + builder *strings.Builder, + frame svgmap.Frame, + options LunarEclipseMapSVGOptions, + projection svgmap.Projection, +) { + text := options.FooterNote + if text == "" { + if options.Language == "en" { + text = eclipseMapProjectionLabel(projection, "en") + "; P1/P4 Moon-visible hemispheres; Natural Earth 1:50m physical land, no administrative boundaries." + } else { + text = eclipseMapProjectionLabel(projection, "zh") + ";按 P1/P4 月球可见半球分区;Natural Earth 1:50m 物理陆地底图,不含行政边界。" + } + } + fmt.Fprintf(builder, `%s`, + frame.X, float64(options.Height)-38, html.EscapeString(text)) +} + +func normalizeDegree180(value float64) float64 { + value = math.Mod(value+180, 360) + if value < 0 { + value += 360 + } + return value - 180 +} diff --git a/eclipse/svg/lunar_map_test.go b/eclipse/svg/lunar_map_test.go new file mode 100644 index 0000000..57180f8 --- /dev/null +++ b/eclipse/svg/lunar_map_test.go @@ -0,0 +1,260 @@ +package svg + +import ( + "encoding/xml" + "errors" + "io" + "math" + "strings" + "testing" + "time" + + "b612.me/astro/internal/svgmap" +) + +func TestLunarEclipseMapSVGVisibilityRegions(t *testing.T) { + diagram, ok := LunarEclipseMapSVG( + time.Date(2026, 3, 3, 0, 0, 0, 0, time.UTC), + LunarEclipseMapSVGOptions{Width: 900, Height: 620, Location: time.UTC}, + ) + if !ok { + t.Fatal("expected lunar-eclipse visibility map") + } + for _, want := range []string{ + "月全食全球可见图", "P1", "P4", "食甚", "全程可见", "带食月出", "带食月落", "不可见", + `class="entire-eclipse-region"`, "moonrise-region", "moonset-region", + `class="p1-horizon"`, `class="p4-horizon"`, `class="land"`, "不含行政边界", + } { + if !strings.Contains(diagram, want) { + t.Fatalf("lunar-eclipse map missing %q", want) + } + } + if err := validateEclipseMapXML(diagram); err != nil { + t.Fatalf("lunar-eclipse map is not valid XML: %v", err) + } +} + +func TestLunarEclipseMapSVGUsesExclusiveVisibilityLayers(t *testing.T) { + cst := time.FixedZone("CST", 8*60*60) + diagram, ok := LunarEclipseMapSVG( + time.Date(2029, 1, 1, 0, 0, 0, 0, cst), + LunarEclipseMapSVGOptions{Width: 1200, Height: 800, Location: cst}, + ) + if !ok { + t.Fatal("expected lunar-eclipse visibility map") + } + + for _, want := range []string{ + `mask id="lunar-not-visible-mask"`, + `mask id="lunar-not-p4-mask"`, + `mask id="lunar-not-p1-mask"`, + `class="eclipse-unavailable-region" mask="url(#lunar-not-visible-mask)"`, + `class="visible-at-p1 moonset-region" mask="url(#lunar-not-p4-mask)"`, + `class="visible-at-p4 moonrise-region" mask="url(#lunar-not-p1-mask)"`, + } { + if !strings.Contains(diagram, want) { + t.Fatalf("lunar-eclipse map does not render exclusive visibility regions: missing %q", want) + } + } + if strings.Contains(diagram, `class="entire-eclipse-region"`) && strings.Contains(diagram, `fill-opacity="0.70"`) { + t.Fatal("entire-eclipse region still uses the opaque stacked-overlay style") + } +} + +func TestLunarEclipseMapSVGSupportsForcedPolarProjection(t *testing.T) { + diagram, ok := LunarEclipseMapSVG( + time.Date(2026, 3, 3, 0, 0, 0, 0, time.UTC), + LunarEclipseMapSVGOptions{Projection: EclipseMapProjectionSouthPolar}, + ) + if !ok { + t.Fatal("expected forced south-polar lunar-eclipse map") + } + for _, want := range []string{`