From 9603c7c70b8d75d119b921f6ee454979a91f0d15 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 21:48:39 +0100 Subject: [PATCH 01/19] Add azimuth and lunar phase computations, enhance target handling - Introduced azimuth computations for Sun, Moon, stars, and ICRS directions in `azimuth.hpp`. - Added lunar phase geometry and events handling in `lunar_phase.hpp`. - Implemented a `Target` class for fixed ICRS pointing with altitude and azimuth computations. - Updated `siderust.hpp` to include new headers for azimuth and lunar phase functionalities. - Modified `spherical.hpp` to correct azimuth and polar coordinate handling. - Enhanced time handling in `time.hpp` to support quantity-based arithmetic. - Updated tests to reflect changes in period handling and quantity usage. --- .clang-format | 19 +- CMakeLists.txt | 31 +- docs/mainpage.md | 9 +- examples/altitude_events_example.cpp | 143 ++------- examples/coordinate_systems_example.cpp | 71 +---- examples/coordinates_examples.cpp | 102 ++---- examples/demo.cpp | 112 +------ examples/solar_system_bodies_example.cpp | 69 +--- include/siderust/azimuth.hpp | 353 +++++++++++++++++++++ include/siderust/coordinates/spherical.hpp | 8 +- include/siderust/lunar_phase.hpp | 265 ++++++++++++++++ include/siderust/siderust.hpp | 3 + include/siderust/target.hpp | 252 +++++++++++++++ include/siderust/time.hpp | 9 +- qtty-cpp | 2 +- siderust | 2 +- tempoch-cpp | 2 +- tests/test_altitude.cpp | 10 +- tests/test_time.cpp | 72 ++++- 19 files changed, 1063 insertions(+), 471 deletions(-) create mode 100644 include/siderust/azimuth.hpp create mode 100644 include/siderust/lunar_phase.hpp create mode 100644 include/siderust/target.hpp diff --git a/.clang-format b/.clang-format index 8b81670..8fe01d1 100644 --- a/.clang-format +++ b/.clang-format @@ -1,10 +1,13 @@ --- +Language: Cpp BasedOnStyle: LLVM -IndentWidth: 4 -ContinuationIndentWidth: 4 -ColumnLimit: 0 -PointerAlignment: Left -ReferenceAlignment: Left -AlignConsecutiveAssignments: Consecutive -AlignConsecutiveDeclarations: Consecutive -... +IndentWidth: 2 +ColumnLimit: 80 +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: All +AllowShortEnumsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AlwaysBreakTemplateDeclarations: MultiLine + diff --git a/CMakeLists.txt b/CMakeLists.txt index 751413a..55eb949 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ endif() # --------------------------------------------------------------------------- # Platform-specific library paths # --------------------------------------------------------------------------- -set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/target/release) +set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/siderust-ffi/target/release) if(APPLE) set(SIDERUST_LIBRARY_PATH ${SIDERUST_ARTIFACT_DIR}/libsiderust_ffi.dylib) @@ -47,11 +47,14 @@ endif() # Build siderust-ffi (depends on tempoch-ffi via tempoch-cpp subdirectory) add_custom_target( build_siderust_ffi - COMMAND ${CARGO_BIN} build --release ${_SIDERUST_FEATURES_ARGS} + # Use `cargo rustc --crate-type cdylib` so that the shared library is + # produced even though Cargo.toml only lists rlib (which keeps coverage + # instrumentation clean during `cargo test`/`cargo llvm-cov`). + COMMAND ${CARGO_BIN} rustc --release --crate-type cdylib ${_SIDERUST_FEATURES_ARGS} WORKING_DIRECTORY ${SIDERUST_FFI_DIR} BYPRODUCTS ${SIDERUST_LIBRARY_PATH} DEPENDS build_tempoch_ffi - COMMENT "Building siderust-ffi via Cargo" + COMMENT "Building siderust-ffi via Cargo (cdylib override)" VERBATIM ) @@ -101,13 +104,17 @@ if(SIDERUST_BUILD_DOCS) set(SIDERUST_DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile.siderust_cpp) configure_file(${SIDERUST_DOXYFILE_IN} ${SIDERUST_DOXYFILE_OUT} @ONLY) - add_custom_target(docs - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen - COMMAND ${DOXYGEN_EXECUTABLE} ${SIDERUST_DOXYFILE_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Generating API documentation with Doxygen" - VERBATIM - ) + if(NOT TARGET docs) + add_custom_target(docs + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen + COMMAND ${DOXYGEN_EXECUTABLE} ${SIDERUST_DOXYFILE_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM + ) + else() + message(STATUS "Top-level 'docs' target already exists; skipping creation to avoid conflict with subprojects") + endif() else() message(STATUS "Doxygen not found; 'docs' target will not be available") endif() @@ -115,9 +122,9 @@ endif() # RPATH for shared library lookup at runtime if(APPLE) - set(_siderust_rpath "@loader_path/../siderust/target/release;@loader_path/../tempoch-cpp/tempoch/tempoch-ffi/target/release;@loader_path/../qtty-cpp/qtty/target/release") + set(_siderust_rpath "@loader_path/../siderust/siderust-ffi/target/release;@loader_path/../tempoch-cpp/tempoch/tempoch-ffi/target/release;@loader_path/../qtty-cpp/qtty/target/release") elseif(UNIX) - set(_siderust_rpath "$ORIGIN/../siderust/target/release:$ORIGIN/../tempoch-cpp/tempoch/tempoch-ffi/target/release:$ORIGIN/../qtty-cpp/qtty/target/release") + set(_siderust_rpath "$ORIGIN/../siderust/siderust-ffi/target/release:$ORIGIN/../tempoch-cpp/tempoch/tempoch-ffi/target/release:$ORIGIN/../qtty-cpp/qtty/target/release") endif() # --------------------------------------------------------------------------- diff --git a/docs/mainpage.md b/docs/mainpage.md index 563c5ff..f378652 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -29,7 +29,8 @@ codebase without writing a single line of Rust. ```cpp #include -#include +#include +#include int main() { using namespace siderust; @@ -41,17 +42,17 @@ int main() { // Sun altitude at the observatory qtty::Radian alt = sun::altitude_at(obs, mjd); - std::printf("Sun altitude: %.4f rad\n", alt.value()); + std::cout << std::fixed << std::setprecision(4) << "Sun altitude: " << alt << " rad\n"; // Star from built-in catalog const auto& vega = VEGA; qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::printf("Vega altitude: %.4f rad\n", star_alt.value()); + std::cout << "Vega altitude: " << star_alt << " rad\n"; // Astronomical night periods (twilight < -18°) auto nights = sun::below_threshold(obs, mjd, mjd + 1.0, -18.0_deg); for (auto& p : nights) - std::printf("Night: MJD %.4f – %.4f\n", p.start_mjd(), p.end_mjd()); + std::cout << "Night: MJD " << p.start() << " – " << p.end() << "\n"; return 0; } diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index a3b1a92..30d6adb 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -1,140 +1,41 @@ /** * @file altitude_events_example.cpp * @example altitude_events_example.cpp - * @brief Altitude windows, crossings, and culminations for Sun, Moon, and - * stars. - * - * Usage: - * cmake --build build-make --target altitude_events_example - * ./build-make/altitude_events_example + * @brief Concise altitude events example using streamed UTC and Period printing. */ #include - -#include +#include +#include #include -using namespace siderust; -using namespace qtty::literals; - -static const char* crossing_direction_name(CrossingDirection d) { - return (d == CrossingDirection::Rising) ? "rising" : "setting"; -} - -static const char* culmination_kind_name(CulminationKind k) { - return (k == CulminationKind::Max) ? "max" : "min"; -} +int main() { + using namespace siderust; + using namespace qtty::literals; -static void print_utc(const UTC& utc) { - std::printf("%04d-%02u-%02u %02u:%02u:%02u", utc.year, utc.month, utc.day, - utc.hour, utc.minute, utc.second); -} + std::cout << "=== altitude_events_example (concise) ===\n"; -static void print_periods(const char* title, const std::vector& periods, - std::size_t max_items = 4) { - std::printf("%s: %zu period(s)\n", title, periods.size()); - const std::size_t n = - (periods.size() < max_items) ? periods.size() : max_items; - for (std::size_t i = 0; i < n; ++i) { - const auto s = periods[i].start().to_utc(); - const auto e = periods[i].end().to_utc(); - std::printf(" %zu) ", i + 1); - print_utc(s); - std::printf(" -> "); - print_utc(e); - std::printf(" (%.2f h)\n", periods[i].duration_days() * 24.0); - } - if (periods.size() > n) { - std::printf(" ... (%zu more)\n", periods.size() - n); - } -} + const auto obs = MAUNA_KEA; + const auto start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const auto end = start + qtty::Day(2.0); -static void print_crossings(const char* title, - const std::vector& events, - std::size_t max_items = 6) { - std::printf("%s: %zu event(s)\n", title, events.size()); - const std::size_t n = (events.size() < max_items) ? events.size() : max_items; - for (std::size_t i = 0; i < n; ++i) { - const auto t = events[i].time.to_utc(); - std::printf(" %zu) ", i + 1); - print_utc(t); - std::printf(" %s\n", crossing_direction_name(events[i].direction)); - } - if (events.size() > n) { - std::printf(" ... (%zu more)\n", events.size() - n); - } -} + auto nights = sun::below_threshold(obs, start, end, -18.0_deg); + std::cout << "Astronomical nights found: " << nights.size() << "\n"; -static void print_culminations(const char* title, - const std::vector& events, - std::size_t max_items = 6) { - std::printf("%s: %zu event(s)\n", title, events.size()); - const std::size_t n = (events.size() < max_items) ? events.size() : max_items; + // Print up to three night periods as UTC ranges and duration in hours + const std::size_t n = std::min(nights.size(), 3); for (std::size_t i = 0; i < n; ++i) { - const auto t = events[i].time.to_utc(); - std::printf(" %zu) ", i + 1); - print_utc(t); - std::printf(" alt=%.3f deg kind=%s\n", events[i].altitude.value(), - culmination_kind_name(events[i].kind)); + const auto &p = nights[i]; + std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " << p.end().to_utc() + << " (" << std::fixed << std::setprecision(2) << p.duration().value() << " h)\n"; } - if (events.size() > n) { - std::printf(" ... (%zu more)\n", events.size() - n); - } -} - -int main() { - std::printf("=== Altitude Events Example ===\n\n"); - - const auto obs = MAUNA_KEA; - const auto start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const auto end = start + 2.0; - const Period window(start, end); - - std::printf("Observer: Mauna Kea (lon=%.4f lat=%.4f h=%.0f m)\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - std::printf("Window MJD: %.6f -> %.6f\n\n", start.value(), end.value()); - - SearchOptions opts; - opts.with_scan_step(1.0 / 144.0).with_tolerance(1e-10); - - // Sun examples. - const auto sun_night = sun::below_threshold(obs, window, -18.0_deg, opts); - const auto sun_cross = sun::crossings(obs, window, -0.833_deg, opts); - const auto sun_culm = sun::culminations(obs, window, opts); - print_periods("Sun below -18 deg (astronomical night)", sun_night); - print_crossings("Sun crossings at -0.833 deg", sun_cross); - print_culminations("Sun culminations", sun_culm); - std::printf("\n"); - - // Moon examples. - const auto moon_above = moon::above_threshold(obs, window, 20.0_deg, opts); - const auto moon_cross = moon::crossings(obs, window, 0.0_deg, opts); - const auto moon_culm = moon::culminations(obs, window, opts); - print_periods("Moon above +20 deg", moon_above); - print_crossings("Moon horizon crossings", moon_cross); - print_culminations("Moon culminations", moon_culm); - std::printf("\n"); - // Star examples. - const auto& vega = VEGA; - const auto vega_above = - star_altitude::above_threshold(vega, obs, window, 25.0_deg, opts); - const auto vega_cross = - star_altitude::crossings(vega, obs, window, 0.0_deg, opts); - const auto vega_culm = star_altitude::culminations(vega, obs, window, opts); - print_periods("VEGA above +25 deg", vega_above); - print_crossings("VEGA horizon crossings", vega_cross); - print_culminations("VEGA culminations", vega_culm); - std::printf("\n"); + auto crossings = sun::crossings(obs, start, end, 0.0_deg); + std::cout << "Sun crossings: " << crossings.size() << "\n"; - // Fixed ICRS direction examples. - const spherical::direction::ICRS dir_icrs(279.23473_deg, 38.78369_deg); - const auto dir_above = - icrs_altitude::above_threshold(dir_icrs, obs, window, 30.0_deg, opts); - const auto dir_below = - icrs_altitude::below_threshold(dir_icrs, obs, window, 0.0_deg, opts); - print_periods("Fixed ICRS direction above +30 deg", dir_above); - print_periods("Fixed ICRS direction below horizon", dir_below); + auto culminations = sun::culminations(obs, start, end); + std::cout << "Culminations: " << culminations.size() << "\n"; + std::cout << "Done.\n"; return 0; } diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index b7eeb42..5cf8f9c 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -10,74 +10,19 @@ #include -#include - -using namespace siderust; -using namespace siderust::frames; -using namespace qtty::literals; +#include +#include int main() { - std::printf("=== Coordinate Systems Example ===\n\n"); - - auto obs = ROQUE_DE_LOS_MUCHACHOS; - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - - std::printf("Observer (Geodetic): lon=%.4f deg lat=%.4f deg h=%.1f m\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - - auto ecef_m = obs.to_cartesian(); - auto ecef_km = obs.to_cartesian(); - std::printf("Observer (ECEF): x=%.2f m y=%.2f m z=%.2f m\n", - ecef_m.x().value(), ecef_m.y().value(), ecef_m.z().value()); - std::printf("Observer (ECEF): x=%.2f km y=%.2f km z=%.2f km\n\n", - ecef_km.x().value(), ecef_km.y().value(), ecef_km.z().value()); + using namespace siderust; + using namespace qtty::literals; - // Vega J2000 ICRS direction. - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + std::cout << "=== coordinate_systems_example===\n"; - auto vega_ecl = vega_icrs.to(jd); - auto vega_eq_mod = vega_icrs.to(jd); - auto vega_eq_tod = vega_icrs.to(jd); - auto vega_hor = vega_icrs.to_horizontal(jd, obs); - auto vega_back = vega_ecl.to(jd); - - std::printf("Vega ICRS: RA=%.6f Dec=%.6f\n", - vega_icrs.ra().value(), vega_icrs.dec().value()); - std::printf("Vega EclipticMeanJ2000: lon=%.6f lat=%.6f\n", - vega_ecl.lon().value(), vega_ecl.lat().value()); - std::printf("Vega EquatorialMeanOfDate: RA=%.6f Dec=%.6f\n", - vega_eq_mod.ra().value(), vega_eq_mod.dec().value()); - std::printf("Vega EquatorialTrueOfDate: RA=%.6f Dec=%.6f\n", - vega_eq_tod.ra().value(), vega_eq_tod.dec().value()); - std::printf("Vega Horizontal: az=%.6f alt=%.6f\n", - vega_hor.az().value(), vega_hor.alt().value()); - std::printf("Vega roundtrip ICRS<-Ecliptic: RA=%.6f Dec=%.6f\n\n", - vega_back.ra().value(), vega_back.dec().value()); - - spherical::position::ICRS target_sph_au( - 120.0_deg, -25.0_deg, 2.0_au); - auto target_dir = target_sph_au.direction(); - std::printf("Spherical ICRS position: RA=%.2f Dec=%.2f dist=%.3f AU\n", - target_sph_au.ra().value(), - target_sph_au.dec().value(), - target_sph_au.distance().value()); - std::printf("Direction extracted from spherical position: RA=%.2f Dec=%.2f\n\n", - target_dir.ra().value(), target_dir.dec().value()); - - cartesian::position::ICRS target_cart_m(1.5e11, -3.0e10, 2.0e10); - cartesian::position::ICRS target_cart_au( - target_cart_m.x().to(), - target_cart_m.y().to(), - target_cart_m.z().to()); + auto obs = ROQUE_DE_LOS_MUCHACHOS; - std::printf("Cartesian ICRS position: x=%.3e m y=%.3e m z=%.3e m\n", - target_cart_m.x().value(), - target_cart_m.y().value(), - target_cart_m.z().value()); - std::printf("Cartesian ICRS position: x=%.6f AU y=%.6f AU z=%.6f AU\n", - target_cart_au.x().value(), - target_cart_au.y().value(), - target_cart_au.z().value()); + std::cout << "Observer lon=" << std::fixed << std::setprecision(4) << obs.lon + << " lat=" << obs.lat << "\n"; return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index f32fa07..0a2ecf5 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -2,100 +2,34 @@ * @file coordinates_examples.cpp * @example coordinates_examples.cpp * @brief Focused examples for creating and converting typed coordinates. - * - * Usage: - * cmake --build build-make --target coordinates_examples - * ./build-make/coordinates_examples */ - #include -#include +#include +#include -using namespace siderust; -using namespace qtty::literals; +int main() { + using namespace siderust; + using namespace qtty::literals; -static void geodetic_and_ecef_example() { - std::printf("1) Geodetic -> ECEF cartesian\n"); + std::cout << "=== coordinates_examples (concise) ===\n"; + // Geodetic -> ECEF (single line) Geodetic obs(-17.8890, 28.7610, 2396.0); - auto ecef = obs.to_cartesian(); - auto ecef_km = obs.to_cartesian(); - - std::printf(" Geodetic lon=%.4f deg lat=%.4f deg h=%.1f m\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - std::printf(" ECEF x=%.2f m y=%.2f m z=%.2f m\n\n", - ecef.x().value(), ecef.y().value(), ecef.z().value()); - std::printf(" ECEF x=%.2f km y=%.2f km z=%.2f km\n\n", - ecef_km.x().value(), ecef_km.y().value(), ecef_km.z().value()); -} - -static void spherical_direction_example() { - std::printf("2) Spherical direction frame conversions\n"); + auto ecef = obs.to_cartesian(); + std::cout << "Geodetic lon=" << std::fixed << std::setprecision(4) << obs.lon.value() + << " lat=" << obs.lat.value() << " h=" << obs.height.value() << " m\n"; + // Spherical direction example (ICRS -> horizontal) spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - - auto ecl = vega_icrs.to(jd); - auto eq_mod = vega_icrs.to(jd); - auto hor = vega_icrs.to_horizontal(jd, ROQUE_DE_LOS_MUCHACHOS); - - std::printf(" ICRS RA=%.5f Dec=%.5f\n", vega_icrs.ra().value(), vega_icrs.dec().value()); - std::printf(" Ecliptic lon=%.5f lat=%.5f\n", ecl.lon().value(), ecl.lat().value()); - std::printf(" Equatorial(MOD) RA=%.5f Dec=%.5f\n", eq_mod.ra().value(), eq_mod.dec().value()); - std::printf(" Horizontal az=%.5f alt=%.5f\n\n", hor.az().value(), hor.alt().value()); -} - -static void spherical_position_example() { - std::printf("3) Spherical position + extracting direction\n"); - - spherical::position::ICRS target( - 120.0_deg, -25.0_deg, 2.0e17_m); - auto dir = target.direction(); - - std::printf(" Position RA=%.2f Dec=%.2f dist=%.3e m\n", - target.ra().value(), target.dec().value(), target.distance().value()); - std::printf(" Direction-only RA=%.2f Dec=%.2f\n\n", - dir.ra().value(), dir.dec().value()); -} - -static void cartesian_and_units_example() { - std::printf("4) Cartesian coordinate creation + unit conversion\n"); - - cartesian::Direction axis_x(1.0, 0.0, 0.0); - cartesian::position::EclipticMeanJ2000 sample_helio_au(1.0, 0.25, -0.1); - - auto x_km = sample_helio_au.x().to(); - auto y_km = sample_helio_au.y().to(); - - std::printf(" Direction x=%.1f y=%.1f z=%.1f\n", axis_x.x, axis_x.y, axis_x.z); - std::printf(" Position x=%.3f AU y=%.3f AU z=%.3f AU\n", - sample_helio_au.x().value(), sample_helio_au.y().value(), sample_helio_au.z().value()); - std::printf(" Same position in km x=%.2f y=%.2f\n\n", x_km.value(), y_km.value()); -} - -static void ephemeris_typed_example() { - std::printf("5) Typed ephemeris coordinates\n"); - - auto jd = JulianDate::J2000(); - auto earth = ephemeris::earth_heliocentric(jd); // cartesian::position::EclipticMeanJ2000 - auto moon = ephemeris::moon_geocentric(jd); // cartesian::position::MoonGeocentric - - std::printf(" Earth heliocentric (AU) x=%.8f y=%.8f z=%.8f\n", - earth.x().value(), earth.y().value(), earth.z().value()); - std::printf(" Moon geocentric (km) x=%.3f y=%.3f z=%.3f\n\n", - moon.x().value(), moon.y().value(), moon.z().value()); -} - -int main() { - std::printf("=== Coordinate Creation & Conversion Examples ===\n\n"); + auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + auto hor = vega_icrs.to_horizontal(jd, obs); + std::cout << "Vega az=" << std::setprecision(2) << hor.az().value() << " alt=" << hor.alt().value() << "\n"; - geodetic_and_ecef_example(); - spherical_direction_example(); - spherical_position_example(); - cartesian_and_units_example(); - ephemeris_typed_example(); + // Ephemeris quick values + auto earth = ephemeris::earth_heliocentric(jd); + std::cout << "Earth x=" << std::setprecision(6) << earth.x().value() << " AU\n"; - std::printf("Done.\n"); + std::cout << "Done.\n"; return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 40c2038..578bc6b 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -8,118 +8,38 @@ */ #include -#include +#include +#include #include int main() { using namespace siderust; - using namespace siderust::frames; using namespace qtty::literals; - std::printf("=== siderust-cpp demo ===\n\n"); + std::cout << "=== siderust-cpp demo (concise) ===\n"; - // --- Time --- + // Time auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - std::printf("JD for 2026-07-15 22:00 UTC: %.6f\n", jd.value()); - std::printf("Julian centuries since J2000: %.10f\n", jd.julian_centuries()); + std::cout << "JD=" << std::fixed << std::setprecision(6) << jd.value() << " UTC=" << jd.to_utc() << "\n"; + // Sun altitude (deg) auto mjd = MJD::from_jd(jd); - std::printf("MJD: %.6f\n\n", mjd.value()); + qtty::Radian sun_alt = sun::altitude_at(ROQUE_DE_LOS_MUCHACHOS, mjd); + std::cout << "Sun alt=" << std::fixed << std::setprecision(2) << sun_alt.to().value() << " deg\n"; - // --- Observatory --- - auto obs = ROQUE_DE_LOS_MUCHACHOS; - std::printf("Roque de los Muchachos: lon=%.4f lat=%.4f h=%.0f m\n\n", - obs.lon.value(), obs.lat.value(), obs.height.value()); - - // --- Sun altitude --- - qtty::Radian sun_alt = sun::altitude_at(obs, mjd); - std::printf("Sun altitude: %.4f rad (%.2f deg)\n\n", - sun_alt.value(), sun_alt.to().value()); - - // --- Star catalog --- + // Vega altitude const auto& vega = VEGA; - std::printf("Star: %s, d=%.2f ly, L=%.2f Lsun\n", - vega.name().c_str(), vega.distance_ly(), - vega.luminosity_solar()); - - // --- Star altitude --- - qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::printf("Vega altitude: %.4f rad (%.2f deg)\n\n", - star_alt.value(), star_alt.to().value()); - - // ================================================================= - // TYPED COORDINATE & EPHEMERIS API - // ================================================================= - std::printf("--- Typed Coordinate API ---\n\n"); - - // Compile-time typed ICRS direction - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - - // Template-targeted transform: ICRS → EclipticMeanJ2000 - auto ecl = vega_icrs.to_frame(jd); - std::printf("Typed ICRS (%.4f, %.4f) -> EclMeanJ2000 (%.4f, %.4f)\n", - vega_icrs.ra().value(), vega_icrs.dec().value(), - ecl.lon().value(), ecl.lat().value()); - - // Shorthand .to<>() syntax - auto eq_j2000 = vega_icrs.to(jd); - std::printf("Typed ICRS -> EquatorialJ2000 (%.4f, %.4f)\n", - eq_j2000.ra().value(), eq_j2000.dec().value()); - - // Horizontal transform - auto hor = vega_icrs.to_horizontal(jd, obs); - std::printf("Typed Horizontal: az=%.4f alt=%.4f deg\n\n", - hor.az().value(), hor.al().value()); - - // Roundtrip: ICRS → Ecliptic → ICRS - auto back = ecl.to_frame(jd); - std::printf("Roundtrip: (%.6f, %.6f) -> (%.6f, %.6f) -> (%.6f, %.6f)\n", - vega_icrs.ra().value(), vega_icrs.dec().value(), - ecl.lon().value(), ecl.lat().value(), - back.ra().value(), back.dec().value()); - - // qtty unit-safe angle conversion - qtty::Radian ra_rad = vega_icrs.ra().to(); - std::printf("Vega RA: %.6f deg = %.6f rad\n\n", vega_icrs.ra().value(), ra_rad.value()); - - // --- Typed Ephemeris --- - std::printf("--- Typed Ephemeris ---\n\n"); + auto vega_alt = star_altitude::altitude_at(vega, ROQUE_DE_LOS_MUCHACHOS, mjd); + std::cout << "Vega alt=" << std::setprecision(2) << vega_alt.to().value() << " deg\n"; + // Simple ephemeris values auto earth = ephemeris::earth_heliocentric(jd); - std::printf("Earth heliocentric (typed AU): (%.8f, %.8f, %.8f)\n", - earth.x().value(), earth.y().value(), earth.z().value()); - - // Unit conversion: AU → km - qtty::Kilometer x_km = earth.comp_x.to(); - qtty::Kilometer y_km = earth.comp_y.to(); - std::printf("Earth heliocentric (km): (%.2f, %.2f, ...)\n\n", - x_km.value(), y_km.value()); + std::cout << "Earth (AU) x=" << std::setprecision(6) << earth.x().value() << " y=" << earth.y().value() << "\n"; auto moon = ephemeris::moon_geocentric(jd); - std::printf("Moon geocentric (typed km): (%.2f, %.2f, %.2f)\n", - moon.x().value(), moon.y().value(), moon.z().value()); - auto moon_r = std::sqrt(moon.x().value() * moon.x().value() + moon.y().value() * moon.y().value() + moon.z().value() * moon.z().value()); - std::printf("Moon distance: %.2f km\n\n", moon_r); - - // --- Planets --- - auto mars_data = MARS; - std::printf("Mars: mass=%.4e kg, radius=%.2f km\n", - mars_data.mass_kg, mars_data.radius_km); - std::printf(" orbit: a=%.6f AU, e=%.6f\n\n", - mars_data.orbit.semi_major_axis_au, - mars_data.orbit.eccentricity); - - // --- Night periods (sun below -18°) --- - auto night_start = mjd; - auto night_end = mjd + 1.0; - auto nights = sun::below_threshold(obs, night_start, night_end, -18.0_deg); - std::printf("Astronomical night periods (sun < -18 deg):\n"); - for (auto& p : nights) { - std::printf(" MJD %.6f – %.6f (%.2f hours)\n", - p.start_mjd(), p.end_mjd(), - p.duration_days() * 24.0); - } + double moon_r = std::sqrt(moon.x().value() * moon.x().value() + moon.y().value() * moon.y().value() + moon.z().value() * moon.z().value()); + std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_r << " km\n"; - std::printf("\nDone.\n"); + std::cout << "Done.\n"; return 0; } diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index bf82f2f..d34a7ea 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -9,71 +9,28 @@ */ #include - +#include +#include #include -#include - -using namespace siderust; - -template -static double norm3(const PosT& p) { - const double x = p.x().value(); - const double y = p.y().value(); - const double z = p.z().value(); - return std::sqrt(x * x + y * y + z * z); -} - -static void print_planet(const char* name, const Planet& p) { - std::printf("%-8s mass=%.4e kg radius=%.1f km a=%.6f AU e=%.6f i=%.3f deg\n", - name, - p.mass_kg, - p.radius_km, - p.orbit.semi_major_axis_au, - p.orbit.eccentricity, - p.orbit.inclination_deg); -} int main() { - std::printf("=== Solar System Bodies Example ===\n\n"); + using namespace siderust; + using namespace qtty::literals; + + std::cout << "=== solar_system_bodies_example (concise) ===\n"; auto jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - std::printf("Epoch JD: %.6f\n\n", jd.value()); + std::cout << "Epoch JD=" << std::fixed << std::setprecision(6) << jd.value() << "\n"; - auto sun_bary = ephemeris::sun_barycentric(jd); - auto earth_bary = ephemeris::earth_barycentric(jd); auto earth_helio = ephemeris::earth_heliocentric(jd); - auto moon_geo = ephemeris::moon_geocentric(jd); - - std::printf("Sun barycentric (EclipticMeanJ2000, AU):\n"); - std::printf(" x=%.9f y=%.9f z=%.9f\n", - sun_bary.x().value(), sun_bary.y().value(), sun_bary.z().value()); - std::printf("Earth barycentric (EclipticMeanJ2000, AU):\n"); - std::printf(" x=%.9f y=%.9f z=%.9f\n", - earth_bary.x().value(), earth_bary.y().value(), earth_bary.z().value()); - std::printf("Earth heliocentric (EclipticMeanJ2000, AU):\n"); - std::printf(" x=%.9f y=%.9f z=%.9f\n", - earth_helio.x().value(), earth_helio.y().value(), earth_helio.z().value()); - std::printf("Moon geocentric (EclipticMeanJ2000, km):\n"); - std::printf(" x=%.3f y=%.3f z=%.3f\n\n", - moon_geo.x().value(), moon_geo.y().value(), moon_geo.z().value()); - - const double earth_sun_au = norm3(earth_helio); - const double moon_dist_km = norm3(moon_geo); - std::printf("Earth-Sun distance: %.6f AU\n", earth_sun_au); - std::printf("Moon distance from geocenter: %.2f km\n", moon_dist_km); + std::cout << "Earth heliocentric x=" << std::setprecision(6) << earth_helio.x().value() << " AU\n"; - const qtty::Kilometer earth_x_km = earth_helio.x().to(); - std::printf("Earth heliocentric x component: %.2f km\n\n", earth_x_km.value()); + auto moon_geo = ephemeris::moon_geocentric(jd); + double moon_dist = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + moon_geo.y().value() * moon_geo.y().value() + moon_geo.z().value() * moon_geo.z().value()); + std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_dist << " km\n"; - std::printf("Planet catalog (static properties):\n"); - print_planet("Mercury", MERCURY); - print_planet("Venus", VENUS); - print_planet("Earth", EARTH); - print_planet("Mars", MARS); - print_planet("Jupiter", JUPITER); - print_planet("Saturn", SATURN); - print_planet("Uranus", URANUS); - print_planet("Neptune", NEPTUNE); + // Print a couple of planets concisely + std::cout << "Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU Earth a=" << EARTH.orbit.semi_major_axis_au << " AU\n"; return 0; } diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp new file mode 100644 index 0000000..8c3baef --- /dev/null +++ b/include/siderust/azimuth.hpp @@ -0,0 +1,353 @@ +#pragma once + +/** + * @file azimuth.hpp + * @brief Azimuth computations for Sun, Moon, stars, and arbitrary ICRS directions. + * + * Wraps siderust-ffi's azimuth API with exception-safe C++ types and + * RAII-managed output arrays. + * + * ### Covered computations + * | Subject | azimuth_at | azimuth_crossings | azimuth_extrema | in_azimuth_range | + * |---------|:----------:|:-----------------:|:---------------:|:----------------:| + * | Sun | ✓ | ✓ | ✓ | ✓ | + * | Moon | ✓ | ✓ | ✓ | ✓ | + * | Star | ✓ | ✓ | – | – | + * | ICRS | ✓ | – | – | – | + */ + +#include "altitude.hpp" +#include "bodies.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include + +namespace siderust { + +// ============================================================================ +// Azimuth event types +// ============================================================================ + +/** + * @brief Distinguishes azimuth extrema: northernmost or southernmost bearing. + */ +enum class AzimuthExtremumKind : int32_t { + Max = 0, ///< Northernmost (or easternmost) direction reached by the body. + Min = 1, ///< Southernmost (or westernmost) direction reached by the body. +}; + +/** + * @brief An azimuth bearing-crossing event. + */ +struct AzimuthCrossingEvent { + MJD time; ///< Epoch of the crossing (MJD). + CrossingDirection direction; ///< Whether the azimuth is increasing or decreasing. + + static AzimuthCrossingEvent from_c(const siderust_azimuth_crossing_event_t& c) { + return {MJD(c.mjd), static_cast(c.direction)}; + } +}; + +/** + * @brief An azimuth extremum event. + */ +struct AzimuthExtremum { + MJD time; ///< Epoch of the extremum (MJD). + qtty::Degree azimuth; ///< Azimuth at the extremum (degrees, N-clockwise). + AzimuthExtremumKind kind; ///< Maximum or minimum. + + static AzimuthExtremum from_c(const siderust_azimuth_extremum_t& c) { + return {MJD(c.mjd), qtty::Degree(c.azimuth_deg), + static_cast(c.kind)}; + } +}; + +// ============================================================================ +// Internal helpers +// ============================================================================ +namespace detail { + +inline std::vector az_crossings_from_c( + siderust_azimuth_crossing_event_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthCrossingEvent::from_c(ptr[i])); + } + siderust_azimuth_crossings_free(ptr, count); + return result; +} + +inline std::vector az_extrema_from_c( + siderust_azimuth_extremum_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthExtremum::from_c(ptr[i])); + } + siderust_azimuth_extrema_free(ptr, count); + return result; +} + +} // namespace detail + +// ============================================================================ +// Sun azimuth +// ============================================================================ + +namespace sun { + +/** + * @brief Compute the Sun's azimuth (degrees, N-clockwise) at a given MJD instant. + */ +inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_sun_azimuth_at(obs.to_c(), mjd.value(), &out), + "sun::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Find epochs when the Sun crosses a given bearing. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_crossings( + obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "sun::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); +} + +/** + * @brief Find azimuth extrema (northernmost / southernmost) for the Sun. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_azimuth_extremum_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_extrema( + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "sun::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const MJD& start, const MJD& end, + const SearchOptions& opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); +} + +/** + * @brief Find periods when the Sun's azimuth is within [min_bearing, max_bearing]. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const Period& window, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_in_azimuth_range( + obs.to_c(), window.c_inner(), + min_bearing.value(), max_bearing.value(), + opts.to_c(), &ptr, &count), + "sun::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +} + +} // namespace sun + +// ============================================================================ +// Moon azimuth +// ============================================================================ + +namespace moon { + +/** + * @brief Compute the Moon's azimuth (degrees, N-clockwise) at a given MJD instant. + */ +inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_moon_azimuth_at(obs.to_c(), mjd.value(), &out), + "moon::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Find epochs when the Moon crosses a given bearing. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_crossings( + obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "moon::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); +} + +/** + * @brief Find azimuth extrema (northernmost / southernmost) for the Moon. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_azimuth_extremum_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_extrema( + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "moon::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_extrema( + const Geodetic& obs, const MJD& start, const MJD& end, + const SearchOptions& opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); +} + +/** + * @brief Find periods when the Moon's azimuth is within [min_bearing, max_bearing]. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const Period& window, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_in_azimuth_range( + obs.to_c(), window.c_inner(), + min_bearing.value(), max_bearing.value(), + opts.to_c(), &ptr, &count), + "moon::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector in_azimuth_range( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions& opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +} + +} // namespace moon + +// ============================================================================ +// Star azimuth +// ============================================================================ + +namespace star_altitude { + +/** + * @brief Compute a star's azimuth (degrees, N-clockwise) at a given MJD instant. + */ +inline qtty::Degree azimuth_at(const Star& s, const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_star_azimuth_at( + s.c_handle(), obs.to_c(), mjd.value(), &out), + "star_altitude::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Find epochs when a star crosses a given azimuth bearing. + */ +inline std::vector azimuth_crossings( + const Star& s, const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_azimuth_crossings( + s.c_handle(), obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "star_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(s, obs, Period(start, end), bearing, opts); +} + +} // namespace star_altitude + +// ============================================================================ +// ICRS direction azimuth +// ============================================================================ + +namespace icrs_altitude { + +/** + * @brief Compute azimuth (degrees, N-clockwise) for a fixed ICRS direction. + */ +inline qtty::Degree azimuth_at(const spherical::direction::ICRS& dir, + const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_icrs_azimuth_at( + dir.to_c(), + obs.to_c(), mjd.value(), &out), + "icrs_altitude::azimuth_at"); + return qtty::Degree(out); +} + +/** + * @brief Backward-compatible RA/Dec overload. + */ +inline qtty::Degree azimuth_at(qtty::Degree ra, qtty::Degree dec, + const Geodetic& obs, const MJD& mjd) { + return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); +} + +} // namespace icrs_altitude + +} // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index cd94da9..58f551e 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -102,11 +102,11 @@ struct Direction { /// @name FFI interop /// @{ siderust_spherical_dir_t to_c() const { - return {azimuth_.value(), polar_.value(), frame_id()}; + return {polar_.value(), azimuth_.value(), frame_id()}; } static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(c.lon_deg, c.lat_deg); + return Direction(c.azimuth_deg, c.polar_deg); } /// @} @@ -129,7 +129,7 @@ struct Direction { siderust_spherical_dir_t out; check_status( siderust_spherical_dir_transform_frame( - azimuth_.value(), polar_.value(), + polar_.value(), azimuth_.value(), frames::FrameTraits::ffi_id, frames::FrameTraits::ffi_id, jd.value(), &out), @@ -158,7 +158,7 @@ struct Direction { siderust_spherical_dir_t out; check_status( siderust_spherical_dir_to_horizontal( - azimuth_.value(), polar_.value(), + polar_.value(), azimuth_.value(), frames::FrameTraits::ffi_id, jd.value(), observer.to_c(), &out), "Direction::to_horizontal"); diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp new file mode 100644 index 0000000..a75c15e --- /dev/null +++ b/include/siderust/lunar_phase.hpp @@ -0,0 +1,265 @@ +#pragma once + +/** + * @file lunar_phase.hpp + * @brief Lunar phase geometry, phase events, and illumination periods. + * + * Wraps siderust-ffi's lunar phase API with exception-safe C++ types and + * RAII-managed output arrays. + * + * All phase-geometry functions accept a Julian Date (siderust::JulianDate). + * Search windows use the regular MJD-based siderust::Period. + */ + +#include "altitude.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include + +namespace siderust { + +// ============================================================================ +// Phase enumerations +// ============================================================================ + +/** + * @brief Principal lunar phase kinds (new-moon quarter events). + */ +enum class PhaseKind : int32_t { + NewMoon = 0, + FirstQuarter = 1, + FullMoon = 2, + LastQuarter = 3, +}; + +/** + * @brief Descriptive moon phase labels (8 canonical phases). + */ +enum class MoonPhaseLabel : int32_t { + NewMoon = 0, + WaxingCrescent = 1, + FirstQuarter = 2, + WaxingGibbous = 3, + FullMoon = 4, + WaningGibbous = 5, + LastQuarter = 6, + WaningCrescent = 7, +}; + +// ============================================================================ +// Phase event / geometry types +// ============================================================================ + +/** + * @brief Geometric description of the Moon's phase at a point in time. + */ +struct MoonPhaseGeometry { + double phase_angle_rad; ///< Phase angle in [0, π], radians. + double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. + double elongation_rad; ///< Sun–Moon elongation, radians. + bool waxing; ///< True when the Moon is waxing. + + static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t& c) { + return {c.phase_angle_rad, c.illuminated_fraction, + c.elongation_rad, static_cast(c.waxing)}; + } +}; + +/** + * @brief A principal lunar phase event (new moon, first quarter, etc.). + */ +struct PhaseEvent { + MJD time; ///< Epoch of the event (MJD). + PhaseKind kind; ///< Which principal phase occurred. + + static PhaseEvent from_c(const siderust_phase_event_t& c) { + return {MJD(c.mjd), static_cast(c.kind)}; + } +}; + +// ============================================================================ +// Internal helpers +// ============================================================================ +namespace detail { + +inline std::vector phase_events_from_c( + siderust_phase_event_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(PhaseEvent::from_c(ptr[i])); + } + siderust_phase_events_free(ptr, count); + return result; +} + +/// Like periods_from_c but for tempoch_period_mjd_t* pointers (freed with +/// siderust_periods_free). +inline std::vector illum_periods_from_c( + tempoch_period_mjd_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); + } + siderust_periods_free(ptr, count); + return result; +} + +} // namespace detail + +// ============================================================================ +// Lunar phase namespace +// ============================================================================ + +namespace moon { + +/** + * @brief Compute geocentric Moon phase geometry at a Julian Date. + * + * @param jd Julian Date (e.g. `siderust::JulianDate(2451545.0)` for J2000.0). + */ +inline MoonPhaseGeometry phase_geocentric(const JulianDate& jd) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_geocentric(jd.value(), &out), + "moon::phase_geocentric"); + return MoonPhaseGeometry::from_c(out); +} + +/** + * @brief Compute topocentric Moon phase geometry at a Julian Date. + * + * @param jd Julian Date. + * @param site Observer geodetic coordinates. + */ +inline MoonPhaseGeometry phase_topocentric(const JulianDate& jd, + const Geodetic& site) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_topocentric(jd.value(), site.to_c(), &out), + "moon::phase_topocentric"); + return MoonPhaseGeometry::from_c(out); +} + +/** + * @brief Determine the descriptive phase label for a given geometry. + * + * @param geom Moon phase geometry (as returned by phase_geocentric / phase_topocentric). + */ +inline MoonPhaseLabel phase_label(const MoonPhaseGeometry& geom) { + siderust_moon_phase_geometry_t c{geom.phase_angle_rad, + geom.illuminated_fraction, + geom.elongation_rad, + static_cast(geom.waxing)}; + siderust_moon_phase_label_t out{}; + check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); + return static_cast(out); +} + +/** + * @brief Find principal phase events (new moon, quarters, full moon) in a window. + * + * @param window MJD search window. + * @param opts Search tolerances (optional). + */ +inline std::vector find_phase_events( + const Period& window, const SearchOptions& opts = {}) { + siderust_phase_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_find_phase_events( + window.c_inner(), opts.to_c(), &ptr, &count), + "moon::find_phase_events"); + return detail::phase_events_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector find_phase_events( + const MJD& start, const MJD& end, const SearchOptions& opts = {}) { + return find_phase_events(Period(start, end), opts); +} + +/** + * @brief Find periods when illuminated fraction is ≥ k_min. + * + * @param window MJD search window. + * @param k_min Minimum illuminated fraction in [0, 1]. + * @param opts Search tolerances (optional). + */ +inline std::vector illumination_above( + const Period& window, double k_min, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_above( + window.c_inner(), k_min, opts.to_c(), &ptr, &count), + "moon::illumination_above"); + return detail::illum_periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector illumination_above( + const MJD& start, const MJD& end, double k_min, + const SearchOptions& opts = {}) { + return illumination_above(Period(start, end), k_min, opts); +} + +/** + * @brief Find periods when illuminated fraction is ≤ k_max. + * + * @param window MJD search window. + * @param k_max Maximum illuminated fraction in [0, 1]. + * @param opts Search tolerances (optional). + */ +inline std::vector illumination_below( + const Period& window, double k_max, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_below( + window.c_inner(), k_max, opts.to_c(), &ptr, &count), + "moon::illumination_below"); + return detail::illum_periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector illumination_below( + const MJD& start, const MJD& end, double k_max, + const SearchOptions& opts = {}) { + return illumination_below(Period(start, end), k_max, opts); +} + +/** + * @brief Find periods when illuminated fraction is within [k_min, k_max]. + * + * @param window MJD search window. + * @param k_min Minimum illuminated fraction in [0, 1]. + * @param k_max Maximum illuminated fraction in [0, 1]. + * @param opts Search tolerances (optional). + */ +inline std::vector illumination_range( + const Period& window, double k_min, double k_max, + const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_range( + window.c_inner(), k_min, k_max, opts.to_c(), &ptr, &count), + "moon::illumination_range"); + return detail::illum_periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector illumination_range( + const MJD& start, const MJD& end, double k_min, double k_max, + const SearchOptions& opts = {}) { + return illumination_range(Period(start, end), k_min, k_max, opts); +} + +} // namespace moon + +} // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 5cf0df1..1ae07d4 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -27,11 +27,14 @@ */ #include "altitude.hpp" +#include "azimuth.hpp" #include "bodies.hpp" #include "centers.hpp" #include "coordinates.hpp" #include "ephemeris.hpp" #include "ffi_core.hpp" #include "frames.hpp" +#include "lunar_phase.hpp" #include "observatories.hpp" +#include "target.hpp" #include "time.hpp" diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp new file mode 100644 index 0000000..ae77df0 --- /dev/null +++ b/include/siderust/target.hpp @@ -0,0 +1,252 @@ +#pragma once + +/** + * @file target.hpp + * @brief RAII C++ wrapper for an siderust Target (fixed ICRS pointing). + * + * A `Target` represents a fixed celestial direction (RA, Dec at a given epoch) + * and exposes altitude and azimuth computations via the same observer/window + * API as the sun/moon/star helpers in altitude.hpp and azimuth.hpp. + * + * Proper motion is stored for future use but presently not applied during + * altitude/azimuth queries (epoch-fixed direction is used throughout). + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include +#include + +namespace siderust { + +/** + * @brief Move-only RAII handle for a siderust target direction. + * + * ### Example + * @code + * siderust::Target vega(279.2348, +38.7836, 2451545.0); // Vega at J2000 + * auto alt = vega.altitude_at(obs, now); + * @endcode + */ +class Target { + public: + // ------------------------------------------------------------------ + // Construction / destruction + // ------------------------------------------------------------------ + + /** + * @brief Create a target from ICRS [RA, Dec] and an epoch. + * + * @param ra_deg Right ascension, degrees [0, 360). + * @param dec_deg Declination, degrees [−90, +90]. + * @param epoch_jd Julian Date of the coordinate epoch (default J2000.0). + */ + explicit Target(double ra_deg, double dec_deg, double epoch_jd = 2451545.0) { + SiderustTarget* h = nullptr; + check_status(siderust_target_create(ra_deg, dec_deg, epoch_jd, &h), + "Target::Target"); + handle_ = h; + } + + ~Target() { + if (handle_) { + siderust_target_free(handle_); + handle_ = nullptr; + } + } + + /// Move constructor. + Target(Target&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + /// Move assignment. + Target& operator=(Target&& other) noexcept { + if (this != &other) { + if (handle_) { + siderust_target_free(handle_); + } + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + // Prevent copying (the handle has unique ownership). + Target(const Target&) = delete; + Target& operator=(const Target&) = delete; + + // ------------------------------------------------------------------ + // Coordinate accessors + // ------------------------------------------------------------------ + + /// Right ascension of the target (degrees). + double ra_deg() const { + double out{}; + check_status(siderust_target_ra_deg(handle_, &out), "Target::ra_deg"); + return out; + } + + /// Declination of the target (degrees). + double dec_deg() const { + double out{}; + check_status(siderust_target_dec_deg(handle_, &out), "Target::dec_deg"); + return out; + } + + /// Epoch of the coordinates (Julian Date). + double epoch_jd() const { + double out{}; + check_status(siderust_target_epoch_jd(handle_, &out), "Target::epoch_jd"); + return out; + } + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + /** + * @brief Compute altitude (degrees) at a given MJD instant. + */ + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const { + double out{}; + check_status(siderust_target_altitude_at( + handle_, obs.to_c(), mjd.value(), &out), + "Target::altitude_at"); + return qtty::Degree(out); + } + + /** + * @brief Find periods when the target is above a threshold altitude. + */ + std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_above_threshold( + handle_, obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "Target::above_threshold"); + return detail_periods_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector above_threshold( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + return above_threshold(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find threshold-crossing events (rising / setting). + */ + std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + siderust_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_crossings( + handle_, obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "Target::crossings"); + return detail::crossings_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + return crossings(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find culmination (local altitude extremum) events. + */ + std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const { + siderust_culmination_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_culminations( + handle_, obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "Target::culminations"); + return detail::culminations_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector culminations( + const Geodetic& obs, const MJD& start, const MJD& end, + const SearchOptions& opts = {}) const { + return culminations(obs, Period(start, end), opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const { + double out{}; + check_status(siderust_target_azimuth_at( + handle_, obs.to_c(), mjd.value(), &out), + "Target::azimuth_at"); + return qtty::Degree(out); + } + + /** + * @brief Find epochs when the target crosses a given azimuth bearing. + */ + std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_azimuth_crossings( + handle_, obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), &ptr, &count), + "Target::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector azimuth_crossings( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) const { + return azimuth_crossings(obs, Period(start, end), bearing, opts); + } + + /// Access the underlying C handle (advanced use). + const SiderustTarget* c_handle() const { return handle_; } + + private: + SiderustTarget* handle_ = nullptr; + + /// Build a Period vector from a tempoch_period_mjd_t* array. + static std::vector detail_periods_from_c( + tempoch_period_mjd_t* ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); + } + siderust_periods_free(ptr, count); + return result; + } +}; + +} // namespace siderust diff --git a/include/siderust/time.hpp b/include/siderust/time.hpp index 351af5a..ddef029 100644 --- a/include/siderust/time.hpp +++ b/include/siderust/time.hpp @@ -13,9 +13,10 @@ namespace siderust { -using UTC = tempoch::UTC; -using JulianDate = tempoch::JulianDate; -using MJD = tempoch::MJD; -using Period = tempoch::Period; +using CivilTime = tempoch::CivilTime; +using UTC = tempoch::UTC; // alias for CivilTime +using JulianDate = tempoch::JulianDate; // Time +using MJD = tempoch::MJD; // Time +using Period = tempoch::Period; } // namespace siderust diff --git a/qtty-cpp b/qtty-cpp index 953ebe1..3612a8f 160000 --- a/qtty-cpp +++ b/qtty-cpp @@ -1 +1 @@ -Subproject commit 953ebe15bcd6f1b929d4516970e5127e2e1ad953 +Subproject commit 3612a8ff03f7290ea7fe53df897f10aa5716582f diff --git a/siderust b/siderust index 283032a..fe37bfd 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 283032a541a8bbcc46622e6a06f807717438f998 +Subproject commit fe37bfd5f9a878bf732681749ec7cdf4ef68e5c7 diff --git a/tempoch-cpp b/tempoch-cpp index 39a5e85..0b27c11 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit 39a5e8557e2382d47dcb3ebe01a3e3237f8a94e5 +Subproject commit 0b27c11cdc03fa016e3545753d3aaab1520ff576 diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index 8ad505a..cc24154 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -16,7 +16,7 @@ class AltitudeTest : public ::testing::Test { void SetUp() override { obs = ROQUE_DE_LOS_MUCHACHOS; start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); - end_ = start + 1.0; // 24 hours + end_ = start + qtty::Day(1.0); // 24 hours window = Period(start, end_); } }; @@ -37,7 +37,7 @@ TEST_F(AltitudeTest, SunAboveThreshold) { auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); EXPECT_GT(periods.size(), 0u); for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } @@ -47,7 +47,7 @@ TEST_F(AltitudeTest, SunBelowThreshold) { // In July at La Palma, astronomical night may be short but should exist // (or possibly not if too close to solstice — accept 0+) for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } @@ -67,7 +67,7 @@ TEST_F(AltitudeTest, SunAltitudePeriods) { // Find periods when sun is between -6° and 0° (civil twilight) auto periods = sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } @@ -85,7 +85,7 @@ TEST_F(AltitudeTest, MoonAboveThreshold) { auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); // Moon may or may not be above horizon for this date; just no crash for (auto& p : periods) { - EXPECT_GT(p.duration_days(), 0.0); + EXPECT_GT(p.duration().value(), 0.0); } } diff --git a/tests/test_time.cpp b/tests/test_time.cpp index 395814c..a2454c3 100644 --- a/tests/test_time.cpp +++ b/tests/test_time.cpp @@ -31,8 +31,8 @@ TEST(Time, JulianDateRoundtripUtc) { TEST(Time, JulianDateArithmetic) { auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + 365.25; - EXPECT_NEAR(jd2 - jd1, 365.25, 1e-10); + auto jd2 = jd1 + qtty::Day(365.25); + EXPECT_NEAR((jd2 - jd1).value(), 365.25, 1e-10); } TEST(Time, JulianCenturies) { @@ -62,24 +62,74 @@ TEST(Time, MjdRoundtrip) { // ============================================================================ TEST(Time, PeriodDuration) { - Period p(60200.0, 60201.0); - EXPECT_NEAR(p.duration_days(), 1.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); } TEST(Time, PeriodIntersection) { - Period a(60200.0, 60202.0); - Period b(60201.0, 60203.0); + Period a(MJD(60200.0), MJD(60202.0)); + Period b(MJD(60201.0), MJD(60203.0)); auto c = a.intersection(b); - EXPECT_NEAR(c.start_mjd(), 60201.0, 1e-10); - EXPECT_NEAR(c.end_mjd(), 60202.0, 1e-10); + EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); + EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); } TEST(Time, PeriodNoIntersection) { - Period a(60200.0, 60201.0); - Period b(60202.0, 60203.0); + Period a(MJD(60200.0), MJD(60201.0)); + Period b(MJD(60202.0), MJD(60203.0)); EXPECT_THROW(a.intersection(b), tempoch::NoIntersectionError); } TEST(Time, PeriodInvalidThrows) { - EXPECT_THROW(Period(60203.0, 60200.0), tempoch::InvalidPeriodError); + EXPECT_THROW(Period(MJD(60203.0), MJD(60200.0)), tempoch::InvalidPeriodError); +} + +// ============================================================================ +// Typed-quantity (_qty) methods +// ============================================================================ + +TEST(Time, JulianCenturiesQty) { + auto jd = JulianDate::J2000(); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 0.0, 1e-10); + EXPECT_EQ(jc.unit_id(), UNIT_ID_JULIAN_CENTURY); +} + +TEST(Time, JulianCenturiesQtyNonZero) { + // 36525 days ≈ 1 Julian century + auto jd = JulianDate(2451545.0 + 36525.0); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 1.0, 1e-10); +} + +TEST(Time, ArithmeticWithHours) { + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + qtty::Hour(24.0); + EXPECT_NEAR((jd2 - jd1).value(), 1.0, 1e-10); +} + +TEST(Time, ArithmeticWithMinutes) { + auto mjd1 = MJD(60200.0); + auto mjd2 = mjd1 + qtty::Minute(1440.0); + EXPECT_NEAR((mjd2 - mjd1).value(), 1.0, 1e-10); +} + +TEST(Time, SubtractQuantityHours) { + auto jd1 = JulianDate(2451546.0); + auto jd2 = jd1 - qtty::Hour(12.0); + EXPECT_NEAR(jd2.value(), 2451545.5, 1e-10); +} + +TEST(Time, DifferenceConvertible) { + auto jd1 = JulianDate(2451545.0); + auto jd2 = JulianDate(2451546.0); + auto diff = jd2 - jd1; + auto hours = diff.to(); + EXPECT_NEAR(hours.value(), 24.0, 1e-10); +} + +TEST(Time, PeriodDurationInMinutes) { + Period p(MJD(60200.0), MJD(60200.5)); + auto min = p.duration(); + EXPECT_NEAR(min.value(), 720.0, 1e-6); } From 7054e73fa4fa624dea579492a15f775b9932b2c4 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 23:26:30 +0100 Subject: [PATCH 02/19] feat: implement BodyTarget and StarTarget classes for celestial object tracking --- CMakeLists.txt | 2 +- include/siderust/azimuth.hpp | 36 ++++ include/siderust/body_target.hpp | 298 +++++++++++++++++++++++++++++++ include/siderust/ffi_core.hpp | 11 ++ include/siderust/lunar_phase.hpp | 39 ++++ include/siderust/siderust.hpp | 2 + include/siderust/star_target.hpp | 95 ++++++++++ include/siderust/target.hpp | 44 ++++- include/siderust/trackable.hpp | 119 ++++++++++++ siderust | 2 +- tests/test_bodies.cpp | 124 +++++++++++++ 11 files changed, 763 insertions(+), 9 deletions(-) create mode 100644 include/siderust/body_target.hpp create mode 100644 include/siderust/star_target.hpp create mode 100644 include/siderust/trackable.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 55eb949..441a999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ endif() # --------------------------------------------------------------------------- # Platform-specific library paths # --------------------------------------------------------------------------- -set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/siderust-ffi/target/release) +set(SIDERUST_ARTIFACT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/siderust/target/release) if(APPLE) set(SIDERUST_LIBRARY_PATH ${SIDERUST_ARTIFACT_DIR}/libsiderust_ffi.dylib) diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 8c3baef..7f1d4f7 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -348,6 +348,42 @@ inline qtty::Degree azimuth_at(qtty::Degree ra, qtty::Degree dec, return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); } +/** + * @brief Find epochs when an ICRS direction crosses a given azimuth bearing. + */ +inline std::vector azimuth_crossings( + const spherical::direction::ICRS& dir, + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_azimuth_crossings( + dir.to_c(), obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), &ptr, &count), + "icrs_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Backward-compatible RA/Dec overload. + */ +inline std::vector azimuth_crossings( + qtty::Degree ra, qtty::Degree dec, + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(spherical::direction::ICRS(ra, dec), obs, window, bearing, opts); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector azimuth_crossings( + const spherical::direction::ICRS& dir, + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree bearing, const SearchOptions& opts = {}) { + return azimuth_crossings(dir, obs, Period(start, end), bearing, opts); +} + } // namespace icrs_altitude } // namespace siderust diff --git a/include/siderust/body_target.hpp b/include/siderust/body_target.hpp new file mode 100644 index 0000000..cc39e2a --- /dev/null +++ b/include/siderust/body_target.hpp @@ -0,0 +1,298 @@ +#pragma once + +/** + * @file body_target.hpp + * @brief Trackable wrapper for solar-system bodies. + * + * `BodyTarget` implements the `Trackable` interface for any solar-system + * body identified by the `Body` enum. It dispatches altitude and azimuth + * computations through the siderust-ffi `siderust_body_*` functions, which + * in turn use VSOP87 (planets), specialised engines (Sun/Moon), or + * Meeus/Williams series (Pluto) for ephemeris. + * + * ### Example + * @code + * using namespace siderust; + * BodyTarget mars(Body::Mars); + * qtty::Degree alt = mars.altitude_at(obs, now); + * + * // Polymorphic usage + * std::vector> targets; + * targets.push_back(std::make_unique(Body::Sun)); + * targets.push_back(std::make_unique(Body::Jupiter)); + * for (const auto& t : targets) { + * std::cout << t->altitude_at(obs, now).value() << "\n"; + * } + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "ffi_core.hpp" +#include "trackable.hpp" + +namespace siderust { + +// ============================================================================ +// Body enum +// ============================================================================ + +/** + * @brief Identifies a solar-system body for generic altitude/azimuth dispatch. + * + * Maps 1:1 to the FFI `SiderustBody` discriminant. + */ +enum class Body : int32_t { + Sun = SIDERUST_BODY_SUN, + Moon = SIDERUST_BODY_MOON, + Mercury = SIDERUST_BODY_MERCURY, + Venus = SIDERUST_BODY_VENUS, + Mars = SIDERUST_BODY_MARS, + Jupiter = SIDERUST_BODY_JUPITER, + Saturn = SIDERUST_BODY_SATURN, + Uranus = SIDERUST_BODY_URANUS, + Neptune = SIDERUST_BODY_NEPTUNE, +}; + +// ============================================================================ +// Free functions in body:: namespace +// ============================================================================ + +namespace body { + +/** + * @brief Compute a body's altitude (radians) at a given MJD instant. + */ +inline qtty::Radian altitude_at(Body b, const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_body_altitude_at( + static_cast(b), obs.to_c(), mjd.value(), &out), + "body::altitude_at"); + return qtty::Radian(out); +} + +/** + * @brief Find periods when a body is above a threshold altitude. + */ +inline std::vector above_threshold( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_above_threshold( + static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::above_threshold"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Find periods when a body is below a threshold altitude. + */ +inline std::vector below_threshold( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_below_threshold( + static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::below_threshold"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Find threshold-crossing events for a body. + */ +inline std::vector crossings( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) { + siderust_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_crossings( + static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::crossings"); + return detail::crossings_from_c(ptr, count); +} + +/** + * @brief Find culmination events for a body. + */ +inline std::vector culminations( + Body b, const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_culmination_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_culminations( + static_cast(b), obs.to_c(), + window.c_inner(), + opts.to_c(), &ptr, &count), + "body::culminations"); + return detail::culminations_from_c(ptr, count); +} + +/** + * @brief Find periods when a body's altitude is within [min, max]. + */ +inline std::vector altitude_periods( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree min_alt, qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), + min_alt.value(), max_alt.value()}; + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_altitude_periods( + static_cast(b), q, &ptr, &count), + "body::altitude_periods"); + return detail::periods_from_c(ptr, count); +} + +} // namespace body + +namespace body { + +// ── Azimuth free functions ────────────────────────────────────────────── + +/** + * @brief Compute a body's azimuth (radians) at a given MJD instant. + */ +inline qtty::Radian azimuth_at(Body b, const Geodetic& obs, const MJD& mjd) { + double out; + check_status(siderust_body_azimuth_at( + static_cast(b), obs.to_c(), mjd.value(), &out), + "body::azimuth_at"); + return qtty::Radian(out); +} + +/** + * @brief Find azimuth-bearing crossing events for a body. + */ +inline std::vector azimuth_crossings( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) { + siderust_azimuth_crossing_event_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_crossings( + static_cast(b), obs.to_c(), + window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "body::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Find azimuth extrema (northernmost/southernmost bearing) for a body. + */ +inline std::vector azimuth_extrema( + Body b, const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) { + siderust_azimuth_extremum_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_extrema( + static_cast(b), obs.to_c(), + window.c_inner(), + opts.to_c(), &ptr, &count), + "body::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Find periods when a body's azimuth is within [min, max]. + */ +inline std::vector in_azimuth_range( + Body b, const Geodetic& obs, const Period& window, + qtty::Degree min, qtty::Degree max, const SearchOptions& opts = {}) { + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_in_azimuth_range( + static_cast(b), obs.to_c(), + window.c_inner(), min.value(), max.value(), + opts.to_c(), &ptr, &count), + "body::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +} // namespace body + +// ============================================================================ +// BodyTarget — Trackable adapter for solar-system bodies +// ============================================================================ + +/** + * @brief Trackable adapter for solar-system bodies. + * + * Wraps a `Body` enum value and dispatches all altitude/azimuth queries + * through the FFI `siderust_body_*` functions. + * + * `BodyTarget` is lightweight (holds a single enum value), copyable, and + * can be used directly or stored as `std::unique_ptr` for + * polymorphic dispatch. + */ +class BodyTarget : public Trackable { + public: + /** + * @brief Construct a BodyTarget for a given solar-system body. + * @param body The body to track. + */ + explicit BodyTarget(Body body) : body_(body) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { + auto rad = body::altitude_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return body::above_threshold(body_, obs, window, threshold, opts); + } + + std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return body::below_threshold(body_, obs, window, threshold, opts); + } + + std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return body::crossings(body_, obs, window, threshold, opts); + } + + std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const override { + return body::culminations(body_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { + auto rad = body::azimuth_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const override { + return body::azimuth_crossings(body_, obs, window, bearing, opts); + } + + /// Access the underlying Body enum value. + Body body() const { return body_; } + + private: + Body body_; +}; + +} // namespace siderust diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index 94d8a9f..d673029 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -112,6 +112,17 @@ inline void check_tempoch_status(tempoch_status_t status, const char* operation) tempoch::check_status(status, operation); } +// ============================================================================ +// FFI version +// ============================================================================ + +/** + * @brief Returns the siderust-ffi ABI version (major*10000 + minor*100 + patch). + */ +inline uint32_t ffi_version() { + return siderust_ffi_version(); +} + // ============================================================================ // Frame and Center Enums (C++ typed) // ============================================================================ diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index a75c15e..b031634 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -262,4 +262,43 @@ inline std::vector illumination_range( } // namespace moon +// ============================================================================ +// Convenience helpers (pure C++, no FFI) +// ============================================================================ + +/** + * @brief Get the illuminated fraction as a percentage [0, 100]. + */ +inline double illuminated_percent(const MoonPhaseGeometry& geom) { + return geom.illuminated_fraction * 100.0; +} + +/** + * @brief Check if a phase label describes a waxing moon. + */ +inline bool is_waxing(MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::WaxingCrescent: + case MoonPhaseLabel::FirstQuarter: + case MoonPhaseLabel::WaxingGibbous: + return true; + default: + return false; + } +} + +/** + * @brief Check if a phase label describes a waning moon. + */ +inline bool is_waning(MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::WaningGibbous: + case MoonPhaseLabel::LastQuarter: + case MoonPhaseLabel::WaningCrescent: + return true; + default: + return false; + } +} + } // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 1ae07d4..dfc22cb 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -29,6 +29,7 @@ #include "altitude.hpp" #include "azimuth.hpp" #include "bodies.hpp" +#include "body_target.hpp" #include "centers.hpp" #include "coordinates.hpp" #include "ephemeris.hpp" @@ -36,5 +37,6 @@ #include "frames.hpp" #include "lunar_phase.hpp" #include "observatories.hpp" +#include "star_target.hpp" #include "target.hpp" #include "time.hpp" diff --git a/include/siderust/star_target.hpp b/include/siderust/star_target.hpp new file mode 100644 index 0000000..72e336a --- /dev/null +++ b/include/siderust/star_target.hpp @@ -0,0 +1,95 @@ +#pragma once + +/** + * @file star_target.hpp + * @brief Trackable adapter for Star objects. + * + * `StarTarget` wraps a `const Star&` and implements the `Trackable` + * interface by delegating to the `star_altitude::` and `star_altitude::` + * namespace free functions. + * + * ### Example + * @code + * siderust::StarTarget vega_target(siderust::VEGA); + * auto alt = vega_target.altitude_at(obs, now); + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "bodies.hpp" +#include "trackable.hpp" + +namespace siderust { + +/** + * @brief Trackable adapter wrapping a `const Star&`. + * + * The referenced `Star` must outlive the `StarTarget`. Typically used with + * the pre-built catalog stars (e.g. `VEGA`, `SIRIUS`) which are `inline const` + * globals and live for the entire program. + */ +class StarTarget : public Trackable { + public: + /** + * @brief Wrap a Star reference as a Trackable. + * @param star Reference to a Star. Must outlive this adapter. + */ + explicit StarTarget(const Star& star) : star_(star) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { + // star_altitude::altitude_at returns Radian; convert to Degree + auto rad = star_altitude::altitude_at(star_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return star_altitude::above_threshold(star_, obs, window, threshold, opts); + } + + std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return star_altitude::below_threshold(star_, obs, window, threshold, opts); + } + + std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + return star_altitude::crossings(star_, obs, window, threshold, opts); + } + + std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const override { + return star_altitude::culminations(star_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { + return star_altitude::azimuth_at(star_, obs, mjd); + } + + std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const override { + return star_altitude::azimuth_crossings(star_, obs, window, bearing, opts); + } + + /// Access the underlying Star reference. + const Star& star() const { return star_; } + + private: + const Star& star_; +}; + +} // namespace siderust diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index ae77df0..640a1d8 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -17,6 +17,7 @@ #include "coordinates.hpp" #include "ffi_core.hpp" #include "time.hpp" +#include "trackable.hpp" #include #include @@ -31,7 +32,7 @@ namespace siderust { * auto alt = vega.altitude_at(obs, now); * @endcode */ -class Target { +class Target : public Trackable { public: // ------------------------------------------------------------------ // Construction / destruction @@ -111,7 +112,7 @@ class Target { /** * @brief Compute altitude (degrees) at a given MJD instant. */ - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const { + qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { double out{}; check_status(siderust_target_altitude_at( handle_, obs.to_c(), mjd.value(), &out), @@ -124,7 +125,7 @@ class Target { */ std::vector above_threshold( const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const { + qtty::Degree threshold, const SearchOptions& opts = {}) const override { tempoch_period_mjd_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_above_threshold( @@ -143,12 +144,41 @@ class Target { return above_threshold(obs, Period(start, end), threshold, opts); } + /** + * @brief Find periods when the target is below a threshold altitude. + */ + std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const override { + // Target wraps an ICRS direction; use icrs_below_threshold FFI. + siderust_spherical_dir_t dir_c{}; + dir_c.polar_deg = dec_deg(); + dir_c.azimuth_deg = ra_deg(); + dir_c.frame = SIDERUST_FRAME_T_ICRS; + tempoch_period_mjd_t* ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_below_threshold( + dir_c, obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "Target::below_threshold"); + return detail_periods_from_c(ptr, count); + } + + /** + * @brief Backward-compatible [start, end] overload. + */ + std::vector below_threshold( + const Geodetic& obs, const MJD& start, const MJD& end, + qtty::Degree threshold, const SearchOptions& opts = {}) const { + return below_threshold(obs, Period(start, end), threshold, opts); + } + /** * @brief Find threshold-crossing events (rising / setting). */ std::vector crossings( const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const { + qtty::Degree threshold, const SearchOptions& opts = {}) const override { siderust_crossing_event_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_crossings( @@ -172,7 +202,7 @@ class Target { */ std::vector culminations( const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const { + const SearchOptions& opts = {}) const override { siderust_culmination_event_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_culminations( @@ -198,7 +228,7 @@ class Target { /** * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. */ - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const { + qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { double out{}; check_status(siderust_target_azimuth_at( handle_, obs.to_c(), mjd.value(), &out), @@ -211,7 +241,7 @@ class Target { */ std::vector azimuth_crossings( const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const { + qtty::Degree bearing, const SearchOptions& opts = {}) const override { siderust_azimuth_crossing_event_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_target_azimuth_crossings( diff --git a/include/siderust/trackable.hpp b/include/siderust/trackable.hpp new file mode 100644 index 0000000..5aefe37 --- /dev/null +++ b/include/siderust/trackable.hpp @@ -0,0 +1,119 @@ +#pragma once + +/** + * @file trackable.hpp + * @brief Abstract base class for trackable celestial objects. + * + * `Trackable` defines a polymorphic interface for any celestial object + * whose altitude and azimuth can be computed at an observer location. + * Implementations include: + * + * - **Target** — fixed ICRS direction (RA/Dec) + * - **StarTarget** — adapter for `Star` catalog objects + * - **BodyTarget** — solar-system bodies (Sun, Moon, planets, Pluto) + * + * Use `std::unique_ptr` to hold heterogeneous collections of + * trackable objects. + * + * ### Example + * @code + * auto sun = std::make_unique(siderust::Body::Sun); + * qtty::Degree alt = sun->altitude_at(obs, now); + * + * // Polymorphic usage + * std::vector> targets; + * targets.push_back(std::move(sun)); + * targets.push_back(std::make_unique(VEGA)); + * for (const auto& t : targets) { + * std::cout << t->altitude_at(obs, now).value() << "\n"; + * } + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "coordinates.hpp" +#include "time.hpp" +#include +#include + +namespace siderust { + +/** + * @brief Abstract interface for any object whose altitude/azimuth can be computed. + * + * This class defines the common API shared by all trackable celestial objects. + * Implementations must provide altitude_at and azimuth_at at minimum; the + * remaining methods have default implementations that throw if not overridden. + */ +class Trackable { + public: + virtual ~Trackable() = default; + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + /** + * @brief Compute altitude at a given MJD instant. + * + * The return unit varies by implementation (radians for sun/moon/star, + * degrees for Target/BodyTarget). Check the concrete class documentation. + * + * @note For BodyTarget, returns radians; for Target, returns degrees. + */ + virtual qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const = 0; + + /** + * @brief Find periods when the object is above a threshold altitude. + */ + virtual std::vector above_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + + /** + * @brief Find periods when the object is below a threshold altitude. + */ + virtual std::vector below_threshold( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + + /** + * @brief Find threshold-crossing events (rising / setting). + */ + virtual std::vector crossings( + const Geodetic& obs, const Period& window, + qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + + /** + * @brief Find culmination (local altitude extremum) events. + */ + virtual std::vector culminations( + const Geodetic& obs, const Period& window, + const SearchOptions& opts = {}) const = 0; + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + virtual qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const = 0; + + /** + * @brief Find epochs when the object crosses a given azimuth bearing. + */ + virtual std::vector azimuth_crossings( + const Geodetic& obs, const Period& window, + qtty::Degree bearing, const SearchOptions& opts = {}) const = 0; + + // Non-copyable, non-movable from base + Trackable() = default; + Trackable(const Trackable&) = delete; + Trackable& operator=(const Trackable&) = delete; + Trackable(Trackable&&) = default; + Trackable& operator=(Trackable&&) = default; +}; + +} // namespace siderust diff --git a/siderust b/siderust index fe37bfd..17d986f 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit fe37bfd5f9a878bf732681749ec7cdf4ef68e5c7 +Subproject commit 17d986fd5d1c9c258df50ce66f36fc524266d6e4 diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index 755e69b..530617e 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -83,3 +83,127 @@ TEST(Bodies, AllPlanets) { EXPECT_GT(URANUS.mass_kg, 0.0); EXPECT_GT(NEPTUNE.mass_kg, 0.0); } + +// ============================================================================ +// BodyTarget — generic solar-system body via Trackable polymorphism +// ============================================================================ + +TEST(Bodies, BodyTargetSunAltitude) { + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = sun.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST(Bodies, BodyTargetMarsAltitude) { + BodyTarget mars(Body::Mars); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = mars.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST(Bodies, BodyTargetAllBodiesAltitude) { + auto obs = geodetic(-17.89, 28.76, 2326.0); // ORM + auto mjd = MJD(60000.5); + std::vector all = { + Body::Sun, Body::Moon, Body::Mercury, Body::Venus, + Body::Mars, Body::Jupiter, Body::Saturn, Body::Uranus, Body::Neptune + }; + for (auto b : all) { + BodyTarget bt(b); + auto alt = bt.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } +} + +TEST(Bodies, BodyTargetAzimuth) { + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = sun.azimuth_at(obs, mjd); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); +} + +TEST(Bodies, BodyTargetJupiterAzimuth) { + BodyTarget jup(Body::Jupiter); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = jup.azimuth_at(obs, mjd); + EXPECT_TRUE(std::isfinite(az.value())); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); +} + +TEST(Bodies, BodyTargetAboveThreshold) { + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto window = Period(MJD(60000.0), MJD(60001.0)); + auto periods = sun.above_threshold(obs, window, qtty::Degree(0.0)); + // Sun should be above horizon for some portion of the day + EXPECT_GT(periods.size(), 0u); +} + +TEST(Bodies, BodyTargetPolymorphic) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(Body::Mars)); + + for (const auto& t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } +} + +TEST(Bodies, BodyNamespaceAltitudeAt) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::altitude_at(Body::Saturn, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); +} + +TEST(Bodies, BodyNamespaceAzimuthAt) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::azimuth_at(Body::Venus, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); + EXPECT_GE(rad.value(), 0.0); +} + +// ============================================================================ +// StarTarget — Trackable adapter for Star +// ============================================================================ + +TEST(Bodies, StarTargetAltitude) { + const auto& vega = VEGA; + StarTarget st(vega); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = st.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST(Bodies, StarTargetPolymorphicWithBodyTarget) { + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(VEGA)); + + for (const auto& t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } +} From ebbc59b9a31e4d896c4771e6fa6882dc4fb6554f Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 23:48:25 +0100 Subject: [PATCH 03/19] Enhance documentation and examples for siderust-cpp - Updated mainpage.md to include new features: azimuth calculations, target tracking, and lunar phase events. - Refined README.md in examples directory to reflect new build instructions and added examples. - Expanded altitude_events_example.cpp to demonstrate altitude and azimuth calculations for multiple celestial bodies. - Introduced azimuth_lunar_phase_example.cpp to showcase azimuth events and lunar phase geometry. - Improved coordinate_systems_example.cpp with compile-time frame transformations and observer details. - Enhanced coordinates_examples.cpp with detailed typed-coordinate construction and conversion examples. - Revamped demo.cpp for a comprehensive end-to-end demonstration of siderust capabilities. - Updated solar_system_bodies_example.cpp to include body dispatch API and ephemeris calculations. - Added trackable_targets_example.cpp to illustrate polymorphic tracking of celestial targets. --- CMakeLists.txt | 18 +++ README.md | 35 ++++-- docs/mainpage.md | 29 +++-- examples/README.md | 28 +++-- examples/altitude_events_example.cpp | 100 +++++++++++---- examples/azimuth_lunar_phase_example.cpp | 125 +++++++++++++++++++ examples/coordinate_systems_example.cpp | 54 +++++++-- examples/coordinates_examples.cpp | 58 ++++++--- examples/demo.cpp | 147 +++++++++++++++++++---- examples/solar_system_bodies_example.cpp | 85 ++++++++++--- examples/trackable_targets_example.cpp | 81 +++++++++++++ 11 files changed, 627 insertions(+), 133 deletions(-) create mode 100644 examples/azimuth_lunar_phase_example.cpp create mode 100644 examples/trackable_targets_example.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 441a999..8131d86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,6 +189,24 @@ if(DEFINED _siderust_rpath) ) endif() +add_executable(trackable_targets_example examples/trackable_targets_example.cpp) +target_link_libraries(trackable_targets_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(trackable_targets_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(azimuth_lunar_phase_example examples/azimuth_lunar_phase_example.cpp) +target_link_libraries(azimuth_lunar_phase_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(azimuth_lunar_phase_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- diff --git a/README.md b/README.md index 8791500..92d7497 100644 --- a/README.md +++ b/README.md @@ -10,38 +10,39 @@ Modern, header-only C++17 wrapper for **siderust** — a high-precision astronom |--------|-------------| | **Time** (`time.hpp`) | `JulianDate`, `MJD`, `UTC`, `Period` — value types with arithmetic and UTC round-trips | | **Coordinates** (`coordinates.hpp`) | Modular typed API (`coordinates/{geodetic,spherical,cartesian,types}.hpp`) plus selective alias headers under `coordinates/types/{spherical,cartesian}/...` | +| **Frames & Centers** (`frames.hpp`, `centers.hpp`) | Compile-time frame/center tags and transform capability traits | | **Bodies** (`bodies.hpp`) | `Star` (RAII, catalog + custom), `Planet` (8 planets), `ProperMotion`, `Orbit` | | **Observatories** (`observatories.hpp`) | Named sites: Roque de los Muchachos, Paranal, Mauna Kea, La Silla | | **Altitude** (`altitude.hpp`) | Sun / Moon / Star / ICRS altitude: instant, above/below threshold, crossings, culminations | +| **Azimuth** (`azimuth.hpp`) | Sun / Moon / Star / ICRS azimuth: instant, crossings, extrema, range windows | +| **Targets** (`trackable.hpp`, `target.hpp`, `body_target.hpp`, `star_target.hpp`) | Polymorphic tracking with `Trackable`, `Target`, `BodyTarget`, and `StarTarget` | +| **Lunar Phase** (`lunar_phase.hpp`) | Phase geometry/labels, principal phase events, illumination window search | | **Ephemeris** (`ephemeris.hpp`) | VSOP87 Sun/Earth positions, ELP2000 Moon position | ## Quick Start ```cpp #include -#include +#include int main() { using namespace siderust; - using namespace qtty::literals; auto obs = ROQUE_DE_LOS_MUCHACHOS; auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); auto mjd = MJD::from_jd(jd); + auto win = Period(mjd, mjd + qtty::Day(1.0)); - // Sun altitude - qtty::Radian alt = sun::altitude_at(obs, mjd); - std::printf("Sun altitude: %.4f rad\n", alt.value()); + qtty::Degree sun_alt = sun::altitude_at(obs, mjd).to(); + qtty::Degree sun_az = sun::azimuth_at(obs, mjd); + std::cout << "Sun alt=" << sun_alt.value() << " deg" + << " az=" << sun_az.value() << " deg\n"; - // Star from catalog - const auto& vega = VEGA; - qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::printf("Vega altitude: %.4f rad\n", star_alt.value()); + Target fixed(279.23473, 38.78369); // Vega-like ICRS target + std::cout << "Target alt=" << fixed.altitude_at(obs, mjd).value() << " deg\n"; - // Night periods (astronomical twilight) - auto nights = sun::below_threshold(obs, mjd, mjd + 1.0, -18.0_deg); - for (auto& p : nights) - std::printf("Night: MJD %.4f – %.4f\n", p.start_mjd(), p.end_mjd()); + auto nights = sun::below_threshold(obs, win, qtty::Degree(-18.0)); + std::cout << "Astronomical-night periods in next 24h: " << nights.size() << "\n"; return 0; } @@ -65,6 +66,8 @@ cmake --build . ./coordinate_systems_example ./solar_system_bodies_example ./altitude_events_example +./trackable_targets_example +./azimuth_lunar_phase_example # Run tests ctest --output-on-failure @@ -153,6 +156,12 @@ siderust-cpp/ │ ├── bodies.hpp ← Star, Planet, ProperMotion │ ├── observatories.hpp ← named observatory locations │ ├── altitude.hpp ← sun/moon/star altitude API +│ ├── azimuth.hpp ← azimuth queries and events +│ ├── lunar_phase.hpp ← moon phase geometry and events +│ ├── trackable.hpp ← polymorphic trackable interface +│ ├── target.hpp ← fixed ICRS target (RAII) +│ ├── body_target.hpp ← body enum trackable adapter +│ ├── star_target.hpp ← star trackable adapter │ └── ephemeris.hpp ← VSOP87/ELP2000 positions ├── examples/demo.cpp ├── tests/ diff --git a/docs/mainpage.md b/docs/mainpage.md index f378652..d24001f 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -21,6 +21,9 @@ codebase without writing a single line of Rust. | **Bodies** (`bodies.hpp`) | `Star` (RAII, catalog + custom), `Planet` (8 planets), `ProperMotion`, `Orbit` | | **Observatories** (`observatories.hpp`) | Named sites: Roque de los Muchachos, Paranal, Mauna Kea, La Silla | | **Altitude** (`altitude.hpp`) | Sun / Moon / Star / ICRS altitude: instant, above/below threshold, crossings, culminations | +| **Azimuth** (`azimuth.hpp`) | Sun / Moon / Star / ICRS azimuth: instant, crossings, extrema, range windows | +| **Targets** (`trackable.hpp`, `target.hpp`, `body_target.hpp`, `star_target.hpp`) | Polymorphic target tracking across bodies, stars, and fixed ICRS directions | +| **Lunar Phase** (`lunar_phase.hpp`) | Moon phase geometry, labels, principal phase events, illumination windows | | **Ephemeris** (`ephemeris.hpp`) | VSOP87 Sun/Earth positions, ELP2000 Moon position | --- @@ -34,25 +37,22 @@ codebase without writing a single line of Rust. int main() { using namespace siderust; - using namespace qtty::literals; auto obs = ROQUE_DE_LOS_MUCHACHOS; auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); auto mjd = MJD::from_jd(jd); + auto win = Period(mjd, mjd + qtty::Day(1.0)); - // Sun altitude at the observatory - qtty::Radian alt = sun::altitude_at(obs, mjd); - std::cout << std::fixed << std::setprecision(4) << "Sun altitude: " << alt << " rad\n"; + qtty::Degree sun_alt = sun::altitude_at(obs, mjd).to(); + qtty::Degree sun_az = sun::azimuth_at(obs, mjd); + std::cout << "Sun alt=" << sun_alt.value() << " deg" + << " az=" << sun_az.value() << " deg\n"; - // Star from built-in catalog - const auto& vega = VEGA; - qtty::Radian star_alt = star_altitude::altitude_at(vega, obs, mjd); - std::cout << "Vega altitude: " << star_alt << " rad\n"; + Target fixed(279.23473, 38.78369); // Vega-like fixed ICRS target + std::cout << "Target alt=" << fixed.altitude_at(obs, mjd).value() << " deg\n"; - // Astronomical night periods (twilight < -18°) - auto nights = sun::below_threshold(obs, mjd, mjd + 1.0, -18.0_deg); - for (auto& p : nights) - std::cout << "Night: MJD " << p.start() << " – " << p.end() << "\n"; + auto nights = sun::below_threshold(obs, win, qtty::Degree(-18.0)); + std::cout << "Astronomical-night periods in next 24h: " << nights.size() << "\n"; return 0; } @@ -113,6 +113,8 @@ cmake --build . ./coordinate_systems_example ./solar_system_bodies_example ./altitude_events_example +./trackable_targets_example +./azimuth_lunar_phase_example # Run tests ctest --output-on-failure @@ -131,6 +133,9 @@ ctest --output-on-failure - `siderust/bodies.hpp` — `Star`, `Planet`, and orbital / proper-motion types - `siderust/observatories.hpp` — known observatory locations and custom geodetic points - `siderust/altitude.hpp` — Sun / Moon / Star altitude queries and event search +- `siderust/azimuth.hpp` — azimuth queries, crossings, extrema, and azimuth ranges +- `siderust/trackable.hpp`, `siderust/target.hpp`, `siderust/body_target.hpp`, `siderust/star_target.hpp` — target abstractions and polymorphic tracking +- `siderust/lunar_phase.hpp` — moon phase geometry, labels, phase events, illumination windows - `siderust/ephemeris.hpp` — VSOP87 / ELP2000 position queries --- diff --git a/examples/README.md b/examples/README.md index ac3ef29..1287282 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,24 +3,28 @@ Build from the repository root: ```bash -cmake -S . -B build-make -cmake --build build-make +cmake -S . -B build +cmake --build build ``` Run selected examples: ```bash -./build-make/demo -./build-make/coordinates_examples -./build-make/coordinate_systems_example -./build-make/solar_system_bodies_example -./build-make/altitude_events_example +./build/siderust_demo +./build/coordinates_examples +./build/coordinate_systems_example +./build/solar_system_bodies_example +./build/altitude_events_example +./build/trackable_targets_example +./build/azimuth_lunar_phase_example ``` ## Files -- `demo.cpp`: broad API walkthrough. -- `coordinates_examples.cpp`: typed coordinate creation and frame transforms. -- `coordinate_systems_example.cpp`: coordinate systems + direction/position transforms in one place. -- `solar_system_bodies_example.cpp`: ephemeris vectors and static planet catalog data. -- `altitude_events_example.cpp`: altitude periods, crossings, and culminations for Sun, Moon, VEGA, and fixed ICRS directions. +- `demo.cpp`: end-to-end extended walkthrough (time, typed coordinates, altitude/azimuth, trackables, ephemeris, lunar phase). +- `coordinates_examples.cpp`: typed coordinate construction and core conversion patterns. +- `coordinate_systems_example.cpp`: frame-tag traits and practical frame/horizontal transforms. +- `solar_system_bodies_example.cpp`: planet catalog constants, body-dispatch API, and ephemeris vectors. +- `altitude_events_example.cpp`: altitude windows/crossings/culminations for Sun, Moon, stars, ICRS directions, and `Target`. +- `trackable_targets_example.cpp`: polymorphic tracking with `Trackable`, `BodyTarget`, `StarTarget`, and `Target`. +- `azimuth_lunar_phase_example.cpp`: azimuth events/ranges plus lunar phase geometry, labels, and phase-event searches. diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index 30d6adb..1a0249b 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -1,41 +1,99 @@ /** * @file altitude_events_example.cpp * @example altitude_events_example.cpp - * @brief Concise altitude events example using streamed UTC and Period printing. + * @brief Altitude periods/crossings/culminations for multiple target types. */ -#include -#include +#include #include +#include #include +#include + +namespace { + +const char* crossing_direction_name(siderust::CrossingDirection dir) { + using siderust::CrossingDirection; + switch (dir) { + case CrossingDirection::Rising: + return "rising"; + case CrossingDirection::Setting: + return "setting"; + } + return "unknown"; +} + +const char* culmination_kind_name(siderust::CulminationKind kind) { + using siderust::CulminationKind; + switch (kind) { + case CulminationKind::Max: + return "max"; + case CulminationKind::Min: + return "min"; + } + return "unknown"; +} + +void print_periods(const std::vector& periods, std::size_t limit) { + const std::size_t n = std::min(periods.size(), limit); + for (std::size_t i = 0; i < n; ++i) { + const auto& p = periods[i]; + std::cout << " " << (i + 1) << ") " + << p.start().to_utc() << " -> " << p.end().to_utc() + << " (" << std::fixed << std::setprecision(2) + << p.duration().value() << " h)\n"; + } +} + +} // namespace + int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== altitude_events_example (concise) ===\n"; + const Geodetic obs = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(2.0); + const Period window(start, end); - const auto obs = MAUNA_KEA; - const auto start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const auto end = start + qtty::Day(2.0); + SearchOptions opts; + opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step - auto nights = sun::below_threshold(obs, start, end, -18.0_deg); - std::cout << "Astronomical nights found: " << nights.size() << "\n"; + std::cout << "=== altitude_events_example ===\n"; + std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - // Print up to three night periods as UTC ranges and duration in hours - const std::size_t n = std::min(nights.size(), 3); - for (std::size_t i = 0; i < n; ++i) { - const auto &p = nights[i]; - std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " << p.end().to_utc() - << " (" << std::fixed << std::setprecision(2) << p.duration().value() << " h)\n"; + auto sun_nights = sun::below_threshold(obs, window, qtty::Degree(-18.0), opts); + std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() << " period(s)\n"; + print_periods(sun_nights, 3); + + auto sun_cross = sun::crossings(obs, window, qtty::Degree(0.0), opts); + std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; + if (!sun_cross.empty()) { + const auto& c = sun_cross.front(); + std::cout << " First crossing: " << c.time.to_utc() + << " (" << crossing_direction_name(c.direction) << ")\n"; } - auto crossings = sun::crossings(obs, start, end, 0.0_deg); - std::cout << "Sun crossings: " << crossings.size() << "\n"; + auto moon_culm = moon::culminations(obs, window, opts); + std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; + if (!moon_culm.empty()) { + const auto& c = moon_culm.front(); + std::cout << " First culmination: " << c.time.to_utc() + << " kind=" << culmination_kind_name(c.kind) + << " alt=" << c.altitude.value() << " deg\n"; + } + + auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, qtty::Degree(30.0), opts); + std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; + print_periods(vega_periods, 2); + + spherical::direction::ICRS target_dir(279.23473, 38.78369); + auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, qtty::Degree(0.0), opts); + std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() << " period(s)\n"; - auto culminations = sun::culminations(obs, start, end); - std::cout << "Culminations: " << culminations.size() << "\n"; + Target fixed_target(279.23473, 38.78369); + auto fixed_target_periods = fixed_target.above_threshold(obs, window, qtty::Degree(45.0), opts); + std::cout << "Target::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; - std::cout << "Done.\n"; return 0; } diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp new file mode 100644 index 0000000..a90ad1f --- /dev/null +++ b/examples/azimuth_lunar_phase_example.cpp @@ -0,0 +1,125 @@ +/** + * @file azimuth_lunar_phase_example.cpp + * @example azimuth_lunar_phase_example.cpp + * @brief Azimuth event search plus lunar phase geometry/events. + */ + +#include +#include +#include +#include + +#include + +namespace { + +const char* az_kind_name(siderust::AzimuthExtremumKind kind) { + using siderust::AzimuthExtremumKind; + switch (kind) { + case AzimuthExtremumKind::Max: + return "max"; + case AzimuthExtremumKind::Min: + return "min"; + } + return "unknown"; +} + +const char* phase_kind_name(siderust::PhaseKind kind) { + using siderust::PhaseKind; + switch (kind) { + case PhaseKind::NewMoon: + return "new moon"; + case PhaseKind::FirstQuarter: + return "first quarter"; + case PhaseKind::FullMoon: + return "full moon"; + case PhaseKind::LastQuarter: + return "last quarter"; + } + return "unknown"; +} + +const char* phase_label_name(siderust::MoonPhaseLabel label) { + using siderust::MoonPhaseLabel; + switch (label) { + case MoonPhaseLabel::NewMoon: + return "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return "full moon"; + case MoonPhaseLabel::WaningGibbous: + return "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return "waning crescent"; + } + return "unknown"; +} + +} // namespace + +int main() { + using namespace siderust; + + const Geodetic site = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(3.0); + const Period window(start, end); + + std::cout << "=== azimuth_lunar_phase_example ===\n"; + std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; + + const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); + std::cout << "Instant azimuth\n"; + std::cout << " Sun : " << sun::azimuth_at(site, now).value() << " deg\n"; + std::cout << " Moon : " << moon::azimuth_at(site, now).value() << " deg\n"; + std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now).value() << " deg\n\n"; + + auto sun_cross = sun::azimuth_crossings(site, window, qtty::Degree(180.0)); + auto sun_ext = sun::azimuth_extrema(site, window); + auto moon_west = moon::in_azimuth_range(site, window, qtty::Degree(240.0), qtty::Degree(300.0)); + + std::cout << "Azimuth events\n"; + std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; + std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; + if (!sun_ext.empty()) { + const auto& e = sun_ext.front(); + std::cout << " first extremum " << az_kind_name(e.kind) + << " at " << e.time.to_utc() + << " az=" << e.azimuth.value() << " deg\n"; + } + std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() << " period(s)\n\n"; + + const JulianDate jd_now = now.to_jd(); + auto geo_phase = moon::phase_geocentric(jd_now); + auto topo_phase = moon::phase_topocentric(jd_now, site); + auto topo_label = moon::phase_label(topo_phase); + + auto phase_events = moon::find_phase_events( + Period(start, start + qtty::Day(30.0))); + auto half_lit = moon::illumination_range(window, 0.45, 0.55); + + std::cout << "Lunar phase\n"; + std::cout << std::fixed << std::setprecision(3) + << " Geocentric illuminated fraction: " << geo_phase.illuminated_fraction << "\n" + << " Topocentric illuminated fraction: " << topo_phase.illuminated_fraction + << " (" << phase_label_name(topo_label) << ")\n"; + + std::cout << " Principal phase events in next 30 days: " << phase_events.size() << "\n"; + const std::size_t n = std::min(phase_events.size(), 4); + for (std::size_t i = 0; i < n; ++i) { + const auto& ev = phase_events[i]; + std::cout << " " << ev.time.to_utc() << " -> " << phase_kind_name(ev.kind) << "\n"; + } + + std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " + << half_lit.size() << "\n"; + + return 0; +} diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index 5cf8f9c..aac5108 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -1,28 +1,56 @@ /** * @file coordinate_systems_example.cpp * @example coordinate_systems_example.cpp - * @brief Coordinate systems and frame transform walkthrough. - * - * Usage: - * cmake --build build-make --target coordinate_systems_example - * ./build-make/coordinate_systems_example + * @brief Compile-time frame tags and transform capabilities walkthrough. */ -#include - -#include #include +#include +#include + +#include int main() { using namespace siderust; - using namespace qtty::literals; + using namespace siderust::frames; + + std::cout << "=== coordinate_systems_example ===\n"; + + static_assert(has_frame_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_horizontal_transform_v); + + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + + spherical::Direction src(279.23473, 38.78369); + const auto ecl = src.to_frame(jd); + const auto mod = src.to_frame(jd); + const auto tod = mod.to_frame(jd); + const auto horiz = src.to_horizontal(jd, observer); - std::cout << "=== coordinate_systems_example===\n"; + std::cout << std::fixed << std::setprecision(6); + std::cout << "Observer\n"; + std::cout << " lon=" << observer.lon.value() << " deg" + << " lat=" << observer.lat.value() << " deg\n\n"; - auto obs = ROQUE_DE_LOS_MUCHACHOS; + std::cout << "Frame transforms for Vega-like direction\n"; + std::cout << " ICRS RA/Dec : " + << src.ra().value() << ", " << src.dec().value() << " deg\n"; + std::cout << " EclipticMeanJ2000 lon/lat : " + << ecl.lon().value() << ", " << ecl.lat().value() << " deg\n"; + std::cout << " EquatorialMeanOfDate RA/Dec: " + << mod.ra().value() << ", " << mod.dec().value() << " deg\n"; + std::cout << " EquatorialTrueOfDate RA/Dec: " + << tod.ra().value() << ", " << tod.dec().value() << " deg\n"; + std::cout << " Horizontal az/alt : " + << horiz.az().value() << ", " << horiz.alt().value() << " deg\n\n"; - std::cout << "Observer lon=" << std::fixed << std::setprecision(4) << obs.lon - << " lat=" << obs.lat << "\n"; + const auto ecef = observer.to_cartesian(); + std::cout << "Observer in ECEF\n"; + std::cout << " x=" << ecef.x().value() << " km" + << " y=" << ecef.y().value() << " km" + << " z=" << ecef.z().value() << " km\n"; return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index 0a2ecf5..16b2b7d 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -1,35 +1,57 @@ /** * @file coordinates_examples.cpp * @example coordinates_examples.cpp - * @brief Focused examples for creating and converting typed coordinates. + * @brief Focused typed-coordinate construction and conversion examples. */ -#include -#include #include +#include +#include + +#include int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== coordinates_examples (concise) ===\n"; + std::cout << "=== coordinates_examples ===\n"; + + const Geodetic site(-17.8890, 28.7610, 2396.0); + const auto ecef_m = site.to_cartesian(); + const auto ecef_km = site.to_cartesian(); - // Geodetic -> ECEF (single line) - Geodetic obs(-17.8890, 28.7610, 2396.0); - auto ecef = obs.to_cartesian(); - std::cout << "Geodetic lon=" << std::fixed << std::setprecision(4) << obs.lon.value() - << " lat=" << obs.lat.value() << " h=" << obs.height.value() << " m\n"; + static_assert(std::is_same_v< + std::remove_cv_t, + cartesian::position::ECEF>); + + std::cout << "Geodetic -> ECEF\n"; + std::cout << " lon=" << site.lon.value() << " deg lat=" << site.lat.value() + << " deg h=" << site.height.value() << " m\n"; + std::cout << " x=" << std::fixed << std::setprecision(3) + << ecef_m.x().value() << " m" + << " (" << ecef_km.x().value() << " km)\n\n"; + + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - // Spherical direction example (ICRS -> horizontal) spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - auto hor = vega_icrs.to_horizontal(jd, obs); - std::cout << "Vega az=" << std::setprecision(2) << hor.az().value() << " alt=" << hor.alt().value() << "\n"; + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_true = vega_icrs.to_frame(jd); + auto vega_horiz = vega_icrs.to_horizontal(jd, site); + + std::cout << "Direction transforms\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs.ra().value() << ", " << vega_icrs.dec().value() << " deg\n"; + std::cout << " Ecliptic lon/lat: " << vega_ecl.lon().value() << ", " << vega_ecl.lat().value() << " deg\n"; + std::cout << " True-of-date RA/Dec: " << vega_true.ra().value() << ", " << vega_true.dec().value() << " deg\n"; + std::cout << " Horizontal az/alt: " << vega_horiz.az().value() << ", " << vega_horiz.alt().value() << " deg\n\n"; + + spherical::position::ICRS synthetic_star( + qtty::Degree(210.0), qtty::Degree(-12.0), qtty::AstronomicalUnit(4.2)); + + cartesian::position::EclipticMeanJ2000 earth = + ephemeris::earth_heliocentric(jd); - // Ephemeris quick values - auto earth = ephemeris::earth_heliocentric(jd); - std::cout << "Earth x=" << std::setprecision(6) << earth.x().value() << " AU\n"; + std::cout << "Typed positions\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance().value() << " AU\n"; + std::cout << " Earth heliocentric x: " << earth.x().value() << " AU\n"; - std::cout << "Done.\n"; return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 578bc6b..ee47365 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -1,45 +1,142 @@ /** * @file demo.cpp * @example demo.cpp - * @brief Demonstrates the siderust C++ API. - * - * Usage: - * cd build && cmake .. && cmake --build . && ./demo + * @brief End-to-end demo of siderust-cpp extended capabilities. */ +#include #include -#include #include +#include +#include +#include + #include +namespace { + +const char* crossing_direction_name(siderust::CrossingDirection dir) { + using siderust::CrossingDirection; + switch (dir) { + case CrossingDirection::Rising: + return "rising"; + case CrossingDirection::Setting: + return "setting"; + } + return "unknown"; +} + +const char* moon_phase_label_name(siderust::MoonPhaseLabel label) { + using siderust::MoonPhaseLabel; + switch (label) { + case MoonPhaseLabel::NewMoon: + return "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return "full moon"; + case MoonPhaseLabel::WaningGibbous: + return "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return "waning crescent"; + } + return "unknown"; +} + +} // namespace + int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== siderust-cpp demo (concise) ===\n"; + const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period next_day(now, now + qtty::Day(1.0)); + + std::cout << "=== siderust-cpp extended demo ===\n"; + std::cout << "Observer: lon=" << site.lon.value() + << " deg lat=" << site.lat.value() + << " deg h=" << site.height.value() << " m\n"; + std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() + << " UTC " << jd.to_utc() << "\n\n"; + + spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_hor = vega_icrs.to_horizontal(jd, site); + std::cout << "Typed coordinates\n"; + std::cout << " Vega ICRS RA=" << vega_icrs.ra().value() + << " deg Dec=" << vega_icrs.dec().value() << " deg\n"; + std::cout << " Vega Ecliptic lon=" << vega_ecl.lon().value() + << " deg lat=" << vega_ecl.lat().value() << " deg\n"; + std::cout << " Vega Horizontal az=" << vega_hor.az().value() + << " deg alt=" << vega_hor.alt().value() << " deg\n\n"; + + qtty::Degree sun_alt = sun::altitude_at(site, now).to(); + qtty::Degree sun_az = sun::azimuth_at(site, now); + std::cout << "Sun instant\n"; + std::cout << " Altitude=" << sun_alt.value() << " deg" + << " Azimuth=" << sun_az.value() << " deg\n"; + + auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); + if (!sun_crossings.empty()) { + std::cout << " Next horizon crossing: " + << sun_crossings.front().time.to_utc() << " (" + << crossing_direction_name(sun_crossings.front().direction) + << ")\n"; + } + std::cout << "\n"; + + BodyTarget mars(Body::Mars); + Target fixed_target(279.23473, 38.78369); // Vega-like fixed ICRS pointing + + std::vector>> targets; + targets.push_back({"Sun", std::make_unique(Body::Sun)}); + targets.push_back({"Vega", std::make_unique(VEGA)}); + targets.push_back({"Fixed target", std::make_unique(279.23473, 38.78369)}); - // Time - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - std::cout << "JD=" << std::fixed << std::setprecision(6) << jd.value() << " UTC=" << jd.to_utc() << "\n"; + std::cout << "Trackable polymorphism\n"; + for (const auto& entry : targets) { + const auto& name = entry.first; + const auto& obj = entry.second; + auto alt = obj->altitude_at(site, now); + auto az = obj->azimuth_at(site, now); + std::cout << " " << std::setw(12) << std::left << name + << " alt=" << std::setw(8) << alt.value() + << " deg az=" << az.value() << " deg\n"; + } + std::cout << " Mars altitude via BodyTarget: " + << mars.altitude_at(site, now).value() << " deg\n"; + std::cout << " Fixed Target altitude: " + << fixed_target.altitude_at(site, now).value() << " deg\n\n"; - // Sun altitude (deg) - auto mjd = MJD::from_jd(jd); - qtty::Radian sun_alt = sun::altitude_at(ROQUE_DE_LOS_MUCHACHOS, mjd); - std::cout << "Sun alt=" << std::fixed << std::setprecision(2) << sun_alt.to().value() << " deg\n"; + auto earth_helio = ephemeris::earth_heliocentric(jd); + auto moon_geo = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt( + moon_geo.x().value() * moon_geo.x().value() + + moon_geo.y().value() * moon_geo.y().value() + + moon_geo.z().value() * moon_geo.z().value()); - // Vega altitude - const auto& vega = VEGA; - auto vega_alt = star_altitude::altitude_at(vega, ROQUE_DE_LOS_MUCHACHOS, mjd); - std::cout << "Vega alt=" << std::setprecision(2) << vega_alt.to().value() << " deg\n"; + std::cout << "Ephemeris\n"; + std::cout << " Earth heliocentric x=" << earth_helio.x().value() + << " AU y=" << earth_helio.y().value() << " AU\n"; + std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - // Simple ephemeris values - auto earth = ephemeris::earth_heliocentric(jd); - std::cout << "Earth (AU) x=" << std::setprecision(6) << earth.x().value() << " y=" << earth.y().value() << "\n"; + auto phase = moon::phase_topocentric(jd, site); + auto label = moon::phase_label(phase); + auto bright_periods = moon::illumination_above( + Period(now, now + qtty::Day(7.0)), 0.8); - auto moon = ephemeris::moon_geocentric(jd); - double moon_r = std::sqrt(moon.x().value() * moon.x().value() + moon.y().value() * moon.y().value() + moon.z().value() * moon.z().value()); - std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_r << " km\n"; + std::cout << "Lunar phase\n"; + std::cout << " Illuminated fraction=" << phase.illuminated_fraction + << " label=" << moon_phase_label_name(label) << "\n"; + std::cout << " Bright-moon periods (next 7 days, k>=0.8): " + << bright_periods.size() << "\n"; - std::cout << "Done.\n"; return 0; } diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index d34a7ea..0159538 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -1,36 +1,83 @@ /** * @file solar_system_bodies_example.cpp * @example solar_system_bodies_example.cpp - * @brief Solar-system body ephemeris and catalog examples. - * - * Usage: - * cmake --build build-make --target solar_system_bodies_example - * ./build-make/solar_system_bodies_example + * @brief Solar-system body catalog, ephemeris, and body-dispatch examples. */ -#include -#include -#include #include +#include +#include +#include + +#include + +namespace { + +const char* az_kind_name(siderust::AzimuthExtremumKind kind) { + using siderust::AzimuthExtremumKind; + switch (kind) { + case AzimuthExtremumKind::Max: + return "max"; + case AzimuthExtremumKind::Min: + return "min"; + } + return "unknown"; +} + +} // namespace int main() { using namespace siderust; - using namespace qtty::literals; - std::cout << "=== solar_system_bodies_example (concise) ===\n"; + const Geodetic site = MAUNA_KEA; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period window(now, now + qtty::Day(2.0)); + + std::cout << "=== solar_system_bodies_example ===\n"; + std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; + + std::cout << "Planet catalog constants\n"; + std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" + << " radius=" << MERCURY.radius_km << " km\n"; + std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" + << " radius=" << EARTH.radius_km << " km\n"; + std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" + << " radius=" << JUPITER.radius_km << " km\n\n"; + + auto earth = ephemeris::earth_heliocentric(jd); + auto moon_pos = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt( + moon_pos.x().value() * moon_pos.x().value() + + moon_pos.y().value() * moon_pos.y().value() + + moon_pos.z().value() * moon_pos.z().value()); - auto jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - std::cout << "Epoch JD=" << std::fixed << std::setprecision(6) << jd.value() << "\n"; + std::cout << "Ephemeris\n"; + std::cout << std::fixed << std::setprecision(6) + << " Earth heliocentric x=" << earth.x().value() + << " AU y=" << earth.y().value() << " AU\n"; + std::cout << std::setprecision(2) + << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - auto earth_helio = ephemeris::earth_heliocentric(jd); - std::cout << "Earth heliocentric x=" << std::setprecision(6) << earth_helio.x().value() << " AU\n"; + std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, Body::Jupiter}; - auto moon_geo = ephemeris::moon_geocentric(jd); - double moon_dist = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + moon_geo.y().value() * moon_geo.y().value() + moon_geo.z().value() * moon_geo.z().value()); - std::cout << "Moon dist=" << std::fixed << std::setprecision(2) << moon_dist << " km\n"; + std::cout << "Body dispatch API at observer\n"; + for (Body b : tracked) { + auto alt = body::altitude_at(b, site, now).to(); + auto az = body::azimuth_at(b, site, now).to(); + std::cout << " body=" << static_cast(b) + << " alt=" << alt.value() << " deg" + << " az=" << az.value() << " deg\n"; + } - // Print a couple of planets concisely - std::cout << "Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU Earth a=" << EARTH.orbit.semi_major_axis_au << " AU\n"; + auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); + if (!moon_extrema.empty()) { + const auto& e = moon_extrema.front(); + std::cout << "\nMoon azimuth extrema\n"; + std::cout << " first " << az_kind_name(e.kind) + << " at " << e.time.to_utc() + << " az=" << e.azimuth.value() << " deg\n"; + } return 0; } diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp new file mode 100644 index 0000000..a627b56 --- /dev/null +++ b/examples/trackable_targets_example.cpp @@ -0,0 +1,81 @@ +/** + * @file trackable_targets_example.cpp + * @example trackable_targets_example.cpp + * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + */ + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +const char* crossing_direction_name(siderust::CrossingDirection dir) { + using siderust::CrossingDirection; + switch (dir) { + case CrossingDirection::Rising: + return "rising"; + case CrossingDirection::Setting: + return "setting"; + } + return "unknown"; +} + +struct NamedTrackable { + std::string name; + std::unique_ptr object; +}; + +} // namespace + +int main() { + using namespace siderust; + + const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); + const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); + const Period window(now, now + qtty::Day(1.0)); + + std::cout << "=== trackable_targets_example ===\n"; + std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; + + Target fixed_vega_like(279.23473, 38.78369); + std::cout << "Target metadata\n"; + std::cout << " RA=" << fixed_vega_like.ra_deg() << " deg" + << " Dec=" << fixed_vega_like.dec_deg() << " deg" + << " epoch JD=" << fixed_vega_like.epoch_jd() << "\n\n"; + + std::vector catalog; + catalog.push_back({"Sun", std::make_unique(Body::Sun)}); + catalog.push_back({"Mars", std::make_unique(Body::Mars)}); + catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); + catalog.push_back({"Fixed Vega-like Target", std::make_unique(279.23473, 38.78369)}); + + for (const auto& entry : catalog) { + const auto& t = entry.object; + auto alt = t->altitude_at(site, now); + auto az = t->azimuth_at(site, now); + + std::cout << std::left << std::setw(22) << entry.name << std::right + << " alt=" << std::setw(9) << alt.value() << " deg" + << " az=" << az.value() << " deg\n"; + + auto crossings = t->crossings(site, window, qtty::Degree(0.0)); + if (!crossings.empty()) { + const auto& first = crossings.front(); + std::cout << " first horizon crossing: " << first.time.to_utc() + << " (" << crossing_direction_name(first.direction) << ")\n"; + } + + auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); + if (!az_cross.empty()) { + std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() << "\n"; + } + } + + return 0; +} From 2617f9c28e45ece62e5b38cc5435f395e408239f Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 23:52:15 +0100 Subject: [PATCH 04/19] refactor: streamline output for crossing direction and culmination kind in altitude events example --- examples/altitude_events_example.cpp | 26 ++------------------------ include/siderust/ffi_core.hpp | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index 1a0249b..2884909 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -13,28 +13,6 @@ namespace { -const char* crossing_direction_name(siderust::CrossingDirection dir) { - using siderust::CrossingDirection; - switch (dir) { - case CrossingDirection::Rising: - return "rising"; - case CrossingDirection::Setting: - return "setting"; - } - return "unknown"; -} - -const char* culmination_kind_name(siderust::CulminationKind kind) { - using siderust::CulminationKind; - switch (kind) { - case CulminationKind::Max: - return "max"; - case CulminationKind::Min: - return "min"; - } - return "unknown"; -} - void print_periods(const std::vector& periods, std::size_t limit) { const std::size_t n = std::min(periods.size(), limit); for (std::size_t i = 0; i < n; ++i) { @@ -71,7 +49,7 @@ int main() { if (!sun_cross.empty()) { const auto& c = sun_cross.front(); std::cout << " First crossing: " << c.time.to_utc() - << " (" << crossing_direction_name(c.direction) << ")\n"; + << " (" << c.direction << ")\n"; } auto moon_culm = moon::culminations(obs, window, opts); @@ -79,7 +57,7 @@ int main() { if (!moon_culm.empty()) { const auto& c = moon_culm.front(); std::cout << " First culmination: " << c.time.to_utc() - << " kind=" << culmination_kind_name(c.kind) + << " kind=" << c.kind << " alt=" << c.altitude.value() << " deg\n"; } diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index d673029..71784ce 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -9,6 +9,7 @@ */ #include +#include #include #include @@ -163,6 +164,30 @@ enum class CulminationKind : int32_t { Min = SIDERUST_CULMINATION_KIND_T_MIN, }; +// ============================================================================ +// Stream operators for enums +// ============================================================================ + +inline std::ostream& operator<<(std::ostream& os, CrossingDirection dir) { + switch (dir) { + case CrossingDirection::Rising: + return os << "rising"; + case CrossingDirection::Setting: + return os << "setting"; + } + return os << "unknown"; +} + +inline std::ostream& operator<<(std::ostream& os, CulminationKind kind) { + switch (kind) { + case CulminationKind::Max: + return os << "max"; + case CulminationKind::Min: + return os << "min"; + } + return os << "unknown"; +} + enum class RaConvention : int32_t { MuAlpha = SIDERUST_RA_CONVENTION_T_MU_ALPHA, MuAlphaStar = SIDERUST_RA_CONVENTION_T_MU_ALPHA_STAR, From 346ca20d93bd50b8109777ab07c82b90c7c998a5 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 01:31:35 +0100 Subject: [PATCH 05/19] Refactor target handling and streamline output formatting - Removed redundant functions for crossing direction and moon phase label names, replacing them with direct usage of enum values. - Updated output formatting in demo and example files to utilize operator overloads for cleaner code. - Introduced stream operators for various classes including AzimuthExtremumKind, Position, Geodetic, Direction, and lunar phase enums for improved output readability. - Enhanced Target class to support strongly-typed celestial directions, ensuring automatic conversion to ICRS where necessary. - Added tests for new Target functionality, verifying altitude calculations and typed accessors for ICRSTarget and EclipticMeanJ2000Target. - Updated coordinate tests to use qtty::Degree for consistency and clarity in direction initialization. --- examples/altitude_events_example.cpp | 20 +- examples/azimuth_lunar_phase_example.cpp | 70 +------ examples/coordinate_systems_example.cpp | 31 +-- examples/coordinates_examples.cpp | 29 ++- examples/demo.cpp | 66 ++---- examples/solar_system_bodies_example.cpp | 6 +- examples/trackable_targets_example.cpp | 50 +++-- include/siderust/azimuth.hpp | 18 ++ include/siderust/coordinates/cartesian.hpp | 14 ++ include/siderust/coordinates/geodetic.hpp | 13 ++ include/siderust/coordinates/spherical.hpp | 45 ++-- include/siderust/lunar_phase.hpp | 47 +++++ include/siderust/siderust.hpp | 2 +- include/siderust/target.hpp | 233 +++++++++++++++------ tests/test_altitude.cpp | 70 ++++++- tests/test_coordinates.cpp | 16 +- 16 files changed, 468 insertions(+), 262 deletions(-) diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index 2884909..ee88acf 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -28,6 +28,7 @@ void print_periods(const std::vector& periods, std::size_t lim int main() { using namespace siderust; + using namespace qtty::literals; const Geodetic obs = MAUNA_KEA; const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); @@ -40,11 +41,11 @@ int main() { std::cout << "=== altitude_events_example ===\n"; std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - auto sun_nights = sun::below_threshold(obs, window, qtty::Degree(-18.0), opts); + auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() << " period(s)\n"; print_periods(sun_nights, 3); - auto sun_cross = sun::crossings(obs, window, qtty::Degree(0.0), opts); + auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; if (!sun_cross.empty()) { const auto& c = sun_cross.front(); @@ -58,20 +59,21 @@ int main() { const auto& c = moon_culm.front(); std::cout << " First culmination: " << c.time.to_utc() << " kind=" << c.kind - << " alt=" << c.altitude.value() << " deg\n"; + << " alt=" << c.altitude + << std::endl; } - auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, qtty::Degree(30.0), opts); + auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; print_periods(vega_periods, 2); - spherical::direction::ICRS target_dir(279.23473, 38.78369); - auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, qtty::Degree(0.0), opts); + spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); + auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() << " period(s)\n"; - Target fixed_target(279.23473, 38.78369); - auto fixed_target_periods = fixed_target.above_threshold(obs, window, qtty::Degree(45.0), opts); - std::cout << "Target::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; + ICRSTarget fixed_target{ spherical::direction::ICRS{279.23473_deg, 38.78369_deg } }; + auto fixed_target_periods = fixed_target.above_threshold(obs, window, 45.0_deg, opts); + std::cout << "ICRSTarget::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; return 0; } diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp index a90ad1f..f07d006 100644 --- a/examples/azimuth_lunar_phase_example.cpp +++ b/examples/azimuth_lunar_phase_example.cpp @@ -12,60 +12,11 @@ #include namespace { - -const char* az_kind_name(siderust::AzimuthExtremumKind kind) { - using siderust::AzimuthExtremumKind; - switch (kind) { - case AzimuthExtremumKind::Max: - return "max"; - case AzimuthExtremumKind::Min: - return "min"; - } - return "unknown"; -} - -const char* phase_kind_name(siderust::PhaseKind kind) { - using siderust::PhaseKind; - switch (kind) { - case PhaseKind::NewMoon: - return "new moon"; - case PhaseKind::FirstQuarter: - return "first quarter"; - case PhaseKind::FullMoon: - return "full moon"; - case PhaseKind::LastQuarter: - return "last quarter"; - } - return "unknown"; -} - -const char* phase_label_name(siderust::MoonPhaseLabel label) { - using siderust::MoonPhaseLabel; - switch (label) { - case MoonPhaseLabel::NewMoon: - return "new moon"; - case MoonPhaseLabel::WaxingCrescent: - return "waxing crescent"; - case MoonPhaseLabel::FirstQuarter: - return "first quarter"; - case MoonPhaseLabel::WaxingGibbous: - return "waxing gibbous"; - case MoonPhaseLabel::FullMoon: - return "full moon"; - case MoonPhaseLabel::WaningGibbous: - return "waning gibbous"; - case MoonPhaseLabel::LastQuarter: - return "last quarter"; - case MoonPhaseLabel::WaningCrescent: - return "waning crescent"; - } - return "unknown"; -} - } // namespace int main() { using namespace siderust; + using namespace qtty::literals; const Geodetic site = MAUNA_KEA; const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); @@ -77,22 +28,23 @@ int main() { const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); std::cout << "Instant azimuth\n"; - std::cout << " Sun : " << sun::azimuth_at(site, now).value() << " deg\n"; - std::cout << " Moon : " << moon::azimuth_at(site, now).value() << " deg\n"; - std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now).value() << " deg\n\n"; + std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; + std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; + std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) << std::endl; - auto sun_cross = sun::azimuth_crossings(site, window, qtty::Degree(180.0)); + auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); auto sun_ext = sun::azimuth_extrema(site, window); - auto moon_west = moon::in_azimuth_range(site, window, qtty::Degree(240.0), qtty::Degree(300.0)); + auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); std::cout << "Azimuth events\n"; std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; if (!sun_ext.empty()) { const auto& e = sun_ext.front(); - std::cout << " first extremum " << az_kind_name(e.kind) + std::cout << " first extremum " << e.kind << " at " << e.time.to_utc() - << " az=" << e.azimuth.value() << " deg\n"; + << " az=" << e.azimuth + << std::endl; } std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() << " period(s)\n\n"; @@ -109,13 +61,13 @@ int main() { std::cout << std::fixed << std::setprecision(3) << " Geocentric illuminated fraction: " << geo_phase.illuminated_fraction << "\n" << " Topocentric illuminated fraction: " << topo_phase.illuminated_fraction - << " (" << phase_label_name(topo_label) << ")\n"; + << " (" << topo_label << ")\n"; std::cout << " Principal phase events in next 30 days: " << phase_events.size() << "\n"; const std::size_t n = std::min(phase_events.size(), 4); for (std::size_t i = 0; i < n; ++i) { const auto& ev = phase_events[i]; - std::cout << " " << ev.time.to_utc() << " -> " << phase_kind_name(ev.kind) << "\n"; + std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; } std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index aac5108..fcefb5a 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -21,36 +21,27 @@ int main() { static_assert(has_horizontal_transform_v); const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const auto ecef = observer.to_cartesian(); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::Direction src(279.23473, 38.78369); + spherical::Direction src(qtty::Degree(279.23473), qtty::Degree(38.78369)); const auto ecl = src.to_frame(jd); const auto mod = src.to_frame(jd); const auto tod = mod.to_frame(jd); const auto horiz = src.to_horizontal(jd, observer); std::cout << std::fixed << std::setprecision(6); - std::cout << "Observer\n"; - std::cout << " lon=" << observer.lon.value() << " deg" - << " lat=" << observer.lat.value() << " deg\n\n"; + std::cout << "Observer: " << observer << std::endl; + std::cout << "Observer in ECEF: " << ecef << std::endl; - std::cout << "Frame transforms for Vega-like direction\n"; - std::cout << " ICRS RA/Dec : " - << src.ra().value() << ", " << src.dec().value() << " deg\n"; - std::cout << " EclipticMeanJ2000 lon/lat : " - << ecl.lon().value() << ", " << ecl.lat().value() << " deg\n"; - std::cout << " EquatorialMeanOfDate RA/Dec: " - << mod.ra().value() << ", " << mod.dec().value() << " deg\n"; - std::cout << " EquatorialTrueOfDate RA/Dec: " - << tod.ra().value() << ", " << tod.dec().value() << " deg\n"; - std::cout << " Horizontal az/alt : " - << horiz.az().value() << ", " << horiz.alt().value() << " deg\n\n"; - const auto ecef = observer.to_cartesian(); - std::cout << "Observer in ECEF\n"; - std::cout << " x=" << ecef.x().value() << " km" - << " y=" << ecef.y().value() << " km" - << " z=" << ecef.z().value() << " km\n"; + std::cout << "Frame transforms for Vega-like direction\n"; + std::cout << " ICRS RA/Dec : " << src << "\n"; + std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; + std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; + std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; + std::cout << " Horizontal az/alt : " << horiz << "\n"; return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index 16b2b7d..d13ccff 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -12,10 +12,11 @@ int main() { using namespace siderust; + using namespace qtty::literals; std::cout << "=== coordinates_examples ===\n"; - const Geodetic site(-17.8890, 28.7610, 2396.0); + const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); const auto ecef_m = site.to_cartesian(); const auto ecef_km = site.to_cartesian(); @@ -23,35 +24,33 @@ int main() { std::remove_cv_t, cartesian::position::ECEF>); - std::cout << "Geodetic -> ECEF\n"; - std::cout << " lon=" << site.lon.value() << " deg lat=" << site.lat.value() - << " deg h=" << site.height.value() << " m\n"; - std::cout << " x=" << std::fixed << std::setprecision(3) - << ecef_m.x().value() << " m" - << " (" << ecef_km.x().value() << " km)\n\n"; + std::cout << "Geodetic -> ECEF \n " + << site << "\n" + << ecef_m << "\n" + << "(" << ecef_km << ")\n"<< std::endl; const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); auto vega_ecl = vega_icrs.to_frame(jd); auto vega_true = vega_icrs.to_frame(jd); auto vega_horiz = vega_icrs.to_horizontal(jd, site); std::cout << "Direction transforms\n"; - std::cout << " ICRS RA/Dec: " << vega_icrs.ra().value() << ", " << vega_icrs.dec().value() << " deg\n"; - std::cout << " Ecliptic lon/lat: " << vega_ecl.lon().value() << ", " << vega_ecl.lat().value() << " deg\n"; - std::cout << " True-of-date RA/Dec: " << vega_true.ra().value() << ", " << vega_true.dec().value() << " deg\n"; - std::cout << " Horizontal az/alt: " << vega_horiz.az().value() << ", " << vega_horiz.alt().value() << " deg\n\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; + std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; + std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; + std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; spherical::position::ICRS synthetic_star( - qtty::Degree(210.0), qtty::Degree(-12.0), qtty::AstronomicalUnit(4.2)); + 210.0_deg, -12.0_deg, 4.2_au); cartesian::position::EclipticMeanJ2000 earth = ephemeris::earth_heliocentric(jd); std::cout << "Typed positions\n"; - std::cout << " Synthetic star distance: " << synthetic_star.distance().value() << " AU\n"; - std::cout << " Earth heliocentric x: " << earth.x().value() << " AU\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance() << std::endl; + std::cout << " Earth heliocentric x: " << earth.x() << std::endl; return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index ee47365..5ea5d96 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -15,40 +15,6 @@ namespace { -const char* crossing_direction_name(siderust::CrossingDirection dir) { - using siderust::CrossingDirection; - switch (dir) { - case CrossingDirection::Rising: - return "rising"; - case CrossingDirection::Setting: - return "setting"; - } - return "unknown"; -} - -const char* moon_phase_label_name(siderust::MoonPhaseLabel label) { - using siderust::MoonPhaseLabel; - switch (label) { - case MoonPhaseLabel::NewMoon: - return "new moon"; - case MoonPhaseLabel::WaxingCrescent: - return "waxing crescent"; - case MoonPhaseLabel::FirstQuarter: - return "first quarter"; - case MoonPhaseLabel::WaxingGibbous: - return "waxing gibbous"; - case MoonPhaseLabel::FullMoon: - return "full moon"; - case MoonPhaseLabel::WaningGibbous: - return "waning gibbous"; - case MoonPhaseLabel::LastQuarter: - return "last quarter"; - case MoonPhaseLabel::WaningCrescent: - return "waning crescent"; - } - return "unknown"; -} - } // namespace int main() { @@ -60,22 +26,17 @@ int main() { const Period next_day(now, now + qtty::Day(1.0)); std::cout << "=== siderust-cpp extended demo ===\n"; - std::cout << "Observer: lon=" << site.lon.value() - << " deg lat=" << site.lat.value() - << " deg h=" << site.height.value() << " m\n"; + std::cout << "Observer: " << site << "\n"; std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() << " UTC " << jd.to_utc() << "\n\n"; - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); + spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto vega_ecl = vega_icrs.to_frame(jd); auto vega_hor = vega_icrs.to_horizontal(jd, site); std::cout << "Typed coordinates\n"; - std::cout << " Vega ICRS RA=" << vega_icrs.ra().value() - << " deg Dec=" << vega_icrs.dec().value() << " deg\n"; - std::cout << " Vega Ecliptic lon=" << vega_ecl.lon().value() - << " deg lat=" << vega_ecl.lat().value() << " deg\n"; - std::cout << " Vega Horizontal az=" << vega_hor.az().value() - << " deg alt=" << vega_hor.alt().value() << " deg\n\n"; + std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; + std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; + std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; qtty::Degree sun_alt = sun::altitude_at(site, now).to(); qtty::Degree sun_az = sun::azimuth_at(site, now); @@ -87,18 +48,20 @@ int main() { if (!sun_crossings.empty()) { std::cout << " Next horizon crossing: " << sun_crossings.front().time.to_utc() << " (" - << crossing_direction_name(sun_crossings.front().direction) + << sun_crossings.front().direction << ")\n"; } std::cout << "\n"; BodyTarget mars(Body::Mars); - Target fixed_target(279.23473, 38.78369); // Vega-like fixed ICRS pointing + ICRSTarget fixed_target{ spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369) } }; // Vega-like std::vector>> targets; targets.push_back({"Sun", std::make_unique(Body::Sun)}); targets.push_back({"Vega", std::make_unique(VEGA)}); - targets.push_back({"Fixed target", std::make_unique(279.23473, 38.78369)}); + targets.push_back({"Fixed target", std::make_unique( + spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); std::cout << "Trackable polymorphism\n"; for (const auto& entry : targets) { @@ -107,8 +70,8 @@ int main() { auto alt = obj->altitude_at(site, now); auto az = obj->azimuth_at(site, now); std::cout << " " << std::setw(12) << std::left << name - << " alt=" << std::setw(8) << alt.value() - << " deg az=" << az.value() << " deg\n"; + << " alt=" << std::setw(8) << alt + << " az=" << az << std::endl; } std::cout << " Mars altitude via BodyTarget: " << mars.altitude_at(site, now).value() << " deg\n"; @@ -123,8 +86,7 @@ int main() { moon_geo.z().value() * moon_geo.z().value()); std::cout << "Ephemeris\n"; - std::cout << " Earth heliocentric x=" << earth_helio.x().value() - << " AU y=" << earth_helio.y().value() << " AU\n"; + std::cout << " Earth heliocentric " << earth_helio << " AU\n"; std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; auto phase = moon::phase_topocentric(jd, site); @@ -134,7 +96,7 @@ int main() { std::cout << "Lunar phase\n"; std::cout << " Illuminated fraction=" << phase.illuminated_fraction - << " label=" << moon_phase_label_name(label) << "\n"; + << " label=" << label << "\n"; std::cout << " Bright-moon periods (next 7 days, k>=0.8): " << bright_periods.size() << "\n"; diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index 0159538..51971eb 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -66,8 +66,8 @@ int main() { auto alt = body::altitude_at(b, site, now).to(); auto az = body::azimuth_at(b, site, now).to(); std::cout << " body=" << static_cast(b) - << " alt=" << alt.value() << " deg" - << " az=" << az.value() << " deg\n"; + << " alt=" << alt + << " az=" << az << std::endl; } auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); @@ -76,7 +76,7 @@ int main() { std::cout << "\nMoon azimuth extrema\n"; std::cout << " first " << az_kind_name(e.kind) << " at " << e.time.to_utc() - << " az=" << e.azimuth.value() << " deg\n"; + << " az=" << e.azimuth << std::endl; } return 0; diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp index a627b56..e8e33b3 100644 --- a/examples/trackable_targets_example.cpp +++ b/examples/trackable_targets_example.cpp @@ -1,7 +1,14 @@ /** * @file trackable_targets_example.cpp * @example trackable_targets_example.cpp - * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + * + * Demonstrates the strongly-typed Target template with multiple frames: + * - ICRSTarget — fixed direction in ICRS equatorial coordinates + * - EclipticMeanJ2000Target — fixed direction in mean ecliptic J2000 + * + * Non-ICRS targets are silently converted to ICRS at construction time for + * the Rust FFI layer; the original typed direction is retained in C++. */ #include @@ -15,17 +22,6 @@ namespace { -const char* crossing_direction_name(siderust::CrossingDirection dir) { - using siderust::CrossingDirection; - switch (dir) { - case CrossingDirection::Rising: - return "rising"; - case CrossingDirection::Setting: - return "setting"; - } - return "unknown"; -} - struct NamedTrackable { std::string name; std::unique_ptr object; @@ -43,17 +39,29 @@ int main() { std::cout << "=== trackable_targets_example ===\n"; std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; - Target fixed_vega_like(279.23473, 38.78369); - std::cout << "Target metadata\n"; - std::cout << " RA=" << fixed_vega_like.ra_deg() << " deg" - << " Dec=" << fixed_vega_like.dec_deg() << " deg" - << " epoch JD=" << fixed_vega_like.epoch_jd() << "\n\n"; + // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. + ICRSTarget fixed_vega_like{ spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369) } }; + std::cout << "ICRSTarget metadata\n"; + std::cout << " RA/Dec=" << fixed_vega_like.direction() + << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; + + // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). + // Automatically converted to ICRS by the constructor. + EclipticMeanJ2000Target ecliptic_vega{ spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8) } }; + auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); + std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; + std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; + std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() << " (converted)\n"; + std::cout << " alt=" << alt_ecliptic << std::endl; std::vector catalog; catalog.push_back({"Sun", std::make_unique(Body::Sun)}); catalog.push_back({"Mars", std::make_unique(Body::Mars)}); catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); - catalog.push_back({"Fixed Vega-like Target", std::make_unique(279.23473, 38.78369)}); + catalog.push_back({"Fixed Vega-like (ICRS)", std::make_unique( + spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); for (const auto& entry : catalog) { const auto& t = entry.object; @@ -61,14 +69,14 @@ int main() { auto az = t->azimuth_at(site, now); std::cout << std::left << std::setw(22) << entry.name << std::right - << " alt=" << std::setw(9) << alt.value() << " deg" - << " az=" << az.value() << " deg\n"; + << " alt=" << std::setw(9) << alt + << " az=" << az << std::endl; auto crossings = t->crossings(site, window, qtty::Degree(0.0)); if (!crossings.empty()) { const auto& first = crossings.front(); std::cout << " first horizon crossing: " << first.time.to_utc() - << " (" << crossing_direction_name(first.direction) << ")\n"; + << " (" << first.direction << ")\n"; } auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 7f1d4f7..4f88f40 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -21,6 +21,7 @@ #include "coordinates.hpp" #include "ffi_core.hpp" #include "time.hpp" +#include #include namespace siderust { @@ -386,4 +387,21 @@ inline std::vector azimuth_crossings( } // namespace icrs_altitude +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for AzimuthExtremumKind. + */ +inline std::ostream& operator<<(std::ostream& os, AzimuthExtremumKind kind) { + switch (kind) { + case AzimuthExtremumKind::Max: + return os << "max"; + case AzimuthExtremumKind::Min: + return os << "min"; + } + return os << "unknown"; +} + } // namespace siderust diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 79a069b..13adad1 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -12,6 +12,8 @@ #include +#include + namespace siderust { namespace cartesian { @@ -86,5 +88,17 @@ struct Position { } }; +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for Position. + */ +template +inline std::ostream& operator<<(std::ostream& os, const Position& pos) { + return os << pos.x() << ", " << pos.y() << ", " << pos.z(); +} + } // namespace cartesian } // namespace siderust diff --git a/include/siderust/coordinates/geodetic.hpp b/include/siderust/coordinates/geodetic.hpp index 86dc552..d244b13 100644 --- a/include/siderust/coordinates/geodetic.hpp +++ b/include/siderust/coordinates/geodetic.hpp @@ -12,6 +12,8 @@ #include +#include + namespace siderust { namespace cartesian { template @@ -60,4 +62,15 @@ struct Geodetic { cartesian::Position to_cartesian() const; }; +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for Geodetic. + */ +inline std::ostream& operator<<(std::ostream& os, const Geodetic& geo) { + return os << "lon=" << geo.lon << " lat=" << geo.lat << " h=" << geo.height; +} + } // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 58f551e..56db30d 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -13,6 +13,7 @@ #include +#include #include namespace siderust { @@ -24,7 +25,8 @@ namespace spherical { * Mirrors Rust's `affn::spherical::Direction`. * * @ingroup coordinates_spherical - * @tparam F Reference frame tag (e.g. `frames::ICRS`). + * @tparam F Reference frame chapter content removed. Restore the original from \texttt{archived\_worktree/tex/chapters/12-logging-audit.tex} if needed. +tag (e.g. `frames::ICRS`). * * @par Accessors * Access values through frame-appropriate getters: @@ -46,10 +48,6 @@ struct Direction { Direction(qtty::Degree azimuth, qtty::Degree polar) : azimuth_(azimuth), polar_(polar) {} - /// Raw-double convenience (degrees). - Direction(double azimuth_deg, double polar_deg) - : azimuth_(qtty::Degree(azimuth_deg)), polar_(qtty::Degree(polar_deg)) {} - /// @name Frame info /// @{ static constexpr siderust_frame_t frame_id() { @@ -106,7 +104,7 @@ struct Direction { } static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(c.azimuth_deg, c.polar_deg); + return Direction(qtty::Degree(c.azimuth_deg), qtty::Degree(c.polar_deg)); } /// @} @@ -124,7 +122,7 @@ struct Direction { Direction> to_frame(const JulianDate& jd) const { if constexpr (std::is_same_v) { - return Direction(azimuth_.value(), polar_.value()); + return Direction(azimuth_, polar_); } else { siderust_spherical_dir_t out; check_status( @@ -193,11 +191,6 @@ struct Position { Position(qtty::Degree azimuth, qtty::Degree polar, U dist) : azimuth_(azimuth), polar_(polar), dist_(dist) {} - Position(double azimuth_deg, double polar_deg, double dist_val) - : azimuth_(qtty::Degree(azimuth_deg)), - polar_(qtty::Degree(polar_deg)), - dist_(U(dist_val)) {} - /// Extract the direction component. Direction direction() const { return Direction(azimuth_, polar_); @@ -233,5 +226,33 @@ struct Position { U distance() const { return dist_; } }; +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for Direction with RA/Dec frames. + */ +template , int> = 0> +inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { + return os << dir.ra() << ", " << dir.dec(); +} + +/** + * @brief Stream operator for Direction with Az/Alt frame. + */ +template , int> = 0> +inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { + return os << dir.az() << ", " << dir.alt(); +} + +/** + * @brief Stream operator for Direction with Lon/Lat frames. + */ +template , int> = 0> +inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { + return os << dir.lon() << ", " << dir.lat(); +} + } // namespace spherical } // namespace siderust diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index b031634..58704ca 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -15,6 +15,7 @@ #include "coordinates.hpp" #include "ffi_core.hpp" #include "time.hpp" +#include #include namespace siderust { @@ -301,4 +302,50 @@ inline bool is_waning(MoonPhaseLabel label) { } } +// ============================================================================ +// Stream operators +// ============================================================================ + +/** + * @brief Stream operator for PhaseKind. + */ +inline std::ostream& operator<<(std::ostream& os, PhaseKind kind) { + switch (kind) { + case PhaseKind::NewMoon: + return os << "new moon"; + case PhaseKind::FirstQuarter: + return os << "first quarter"; + case PhaseKind::FullMoon: + return os << "full moon"; + case PhaseKind::LastQuarter: + return os << "last quarter"; + } + return os << "unknown"; +} + +/** + * @brief Stream operator for MoonPhaseLabel. + */ +inline std::ostream& operator<<(std::ostream& os, MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::NewMoon: + return os << "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return os << "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return os << "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return os << "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return os << "full moon"; + case MoonPhaseLabel::WaningGibbous: + return os << "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return os << "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return os << "waning crescent"; + } + return os << "unknown"; +} + } // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index dfc22cb..483bd6c 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -13,7 +13,7 @@ * using namespace siderust::frames; * * // Typed coordinates with compile-time frame/center - * spherical::direction::ICRS vega_icrs(279.23473, 38.78369); // Direction + * spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); * auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); * * // Template-targeted transform — invalid pairs won't compile diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index 640a1d8..c7d6bc9 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -2,14 +2,30 @@ /** * @file target.hpp - * @brief RAII C++ wrapper for an siderust Target (fixed ICRS pointing). + * @brief Generic strongly-typed RAII wrapper for a siderust Target direction. * - * A `Target` represents a fixed celestial direction (RA, Dec at a given epoch) - * and exposes altitude and azimuth computations via the same observer/window - * API as the sun/moon/star helpers in altitude.hpp and azimuth.hpp. + * `Target` represents a fixed celestial direction in any supported + * reference frame and exposes altitude and azimuth computations via the same + * observer/window API as the sun/moon/star helpers in altitude.hpp and + * azimuth.hpp. * - * Proper motion is stored for future use but presently not applied during - * altitude/azimuth queries (epoch-fixed direction is used throughout). + * The template parameter `C` must be an instantiation of + * `spherical::Direction` for a frame `F` that can be transformed to ICRS + * (i.e., `frames::has_frame_transform_v` must be true). + * Non-ICRS directions are silently converted to ICRS at construction; the + * original typed direction is retained as C++ state. + * + * Supported frames: + * - `frames::ICRS`, `frames::ICRF` + * - `frames::EquatorialMeanJ2000`, `frames::EquatorialMeanOfDate`, + * `frames::EquatorialTrueOfDate` + * - `frames::EclipticMeanJ2000` + * + * Convenience aliases: + * - `ICRSTarget`, `ICRFTarget` + * - `EquatorialMeanJ2000Target`, `EquatorialMeanOfDateTarget`, + * `EquatorialTrueOfDateTarget` + * - `EclipticMeanJ2000Target` */ #include "altitude.hpp" @@ -18,37 +34,113 @@ #include "ffi_core.hpp" #include "time.hpp" #include "trackable.hpp" +#include #include #include namespace siderust { +// ============================================================================ +// Internal type traits +// ============================================================================ + +namespace detail { + +/// @cond INTERNAL + +/// True iff T is an instantiation of spherical::Direction. +template +struct is_spherical_direction : std::false_type {}; + +template +struct is_spherical_direction> : std::true_type {}; + +template +inline constexpr bool is_spherical_direction_v = is_spherical_direction::value; + +/// Extract the frame tag F from spherical::Direction. +template +struct spherical_direction_frame; // undefined primary + +template +struct spherical_direction_frame> { + using type = F; +}; + +template +using spherical_direction_frame_t = typename spherical_direction_frame::type; + +/// @endcond + +} // namespace detail + +// ============================================================================ +// Target +// ============================================================================ + /** - * @brief Move-only RAII handle for a siderust target direction. + * @brief Move-only RAII wrapper for a fixed celestial target direction. + * + * @tparam C Spherical direction type (e.g. `spherical::direction::ICRS`). * - * ### Example + * ### Example — ICRS target (Vega at J2000) * @code - * siderust::Target vega(279.2348, +38.7836, 2451545.0); // Vega at J2000 - * auto alt = vega.altitude_at(obs, now); + * using namespace siderust; + * ICRSTarget vega{ spherical::direction::ICRS{ 279.2348_deg, +38.7836_deg } }; + * auto alt = vega.altitude_at(obs, now); // → qtty::Degree + * std::cout << vega.ra() << "\n"; // qtty::Degree (equatorial frames) + * @endcode + * + * ### Example — Ecliptic target (auto-converted to ICRS internally) + * @code + * EclipticMeanJ2000Target ec{ + * spherical::direction::EclipticMeanJ2000{ 246.2_deg, 59.2_deg } }; + * auto alt = ec.altitude_at(obs, now); * @endcode */ +template class Target : public Trackable { + + static_assert(detail::is_spherical_direction_v, + "Target: C must be a specialisation of " + "siderust::spherical::Direction"); + + using Frame = detail::spherical_direction_frame_t; + + static_assert(frames::has_frame_transform_v, + "Target: frame F must support a transform to ICRS " + "(frames::has_frame_transform_v must be true). " + "Supported frames: ICRS, ICRF, EquatorialMeanJ2000, " + "EquatorialMeanOfDate, EquatorialTrueOfDate, EclipticMeanJ2000."); + public: // ------------------------------------------------------------------ // Construction / destruction // ------------------------------------------------------------------ /** - * @brief Create a target from ICRS [RA, Dec] and an epoch. + * @brief Construct from a strongly-typed spherical direction. + * + * For frames other than ICRS, the direction is converted to ICRS before + * being registered with the Rust FFI. The original `C` direction is + * retained for C++-side accessors. * - * @param ra_deg Right ascension, degrees [0, 360). - * @param dec_deg Declination, degrees [−90, +90]. - * @param epoch_jd Julian Date of the coordinate epoch (default J2000.0). + * @param dir Spherical direction (any supported frame). + * @param epoch Coordinate epoch (default J2000.0). */ - explicit Target(double ra_deg, double dec_deg, double epoch_jd = 2451545.0) { + explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) + : m_dir_(dir), m_epoch_(epoch) { + // Convert to ICRS for the FFI; identity transform when already ICRS. + if constexpr (std::is_same_v) { + m_icrs_ = dir; + } else { + m_icrs_ = dir.template to_frame(epoch); + } SiderustTarget* h = nullptr; - check_status(siderust_target_create(ra_deg, dec_deg, epoch_jd, &h), - "Target::Target"); + check_status( + siderust_target_create( + m_icrs_.ra().value(), m_icrs_.dec().value(), epoch.value(), &h), + "Target::Target"); handle_ = h; } @@ -60,7 +152,11 @@ class Target : public Trackable { } /// Move constructor. - Target(Target&& other) noexcept : handle_(other.handle_) { + Target(Target&& other) noexcept + : m_dir_(std::move(other.m_dir_)), + m_epoch_(other.m_epoch_), + m_icrs_(other.m_icrs_), + handle_(other.handle_) { other.handle_ = nullptr; } @@ -70,6 +166,9 @@ class Target : public Trackable { if (handle_) { siderust_target_free(handle_); } + m_dir_ = std::move(other.m_dir_); + m_epoch_ = other.m_epoch_; + m_icrs_ = other.m_icrs_; handle_ = other.handle_; other.handle_ = nullptr; } @@ -84,40 +183,39 @@ class Target : public Trackable { // Coordinate accessors // ------------------------------------------------------------------ - /// Right ascension of the target (degrees). - double ra_deg() const { - double out{}; - check_status(siderust_target_ra_deg(handle_, &out), "Target::ra_deg"); - return out; - } + /// The original typed direction as supplied at construction. + const C& direction() const { return m_dir_; } - /// Declination of the target (degrees). - double dec_deg() const { - double out{}; - check_status(siderust_target_dec_deg(handle_, &out), "Target::dec_deg"); - return out; - } + /// Epoch of the coordinate. + JulianDate epoch() const { return m_epoch_; } - /// Epoch of the coordinates (Julian Date). - double epoch_jd() const { - double out{}; - check_status(siderust_target_epoch_jd(handle_, &out), "Target::epoch_jd"); - return out; - } + /// The ICRS direction used for FFI calls (equals `direction()` when C is + /// already `spherical::direction::ICRS`). + const spherical::direction::ICRS& icrs_direction() const { return m_icrs_; } + + /// Right ascension — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree ra() const { return m_dir_.ra(); } + + /// Declination — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree dec() const { return m_dir_.dec(); } // ------------------------------------------------------------------ - // Altitude queries + // Altitude queries (implements Trackable) // ------------------------------------------------------------------ /** * @brief Compute altitude (degrees) at a given MJD instant. + * + * @note The Rust FFI returns radians; this method converts to degrees. */ qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { double out{}; check_status(siderust_target_altitude_at( handle_, obs.to_c(), mjd.value(), &out), "Target::altitude_at"); - return qtty::Degree(out); + return qtty::Radian(out).to(); } /** @@ -127,7 +225,7 @@ class Target : public Trackable { const Geodetic& obs, const Period& window, qtty::Degree threshold, const SearchOptions& opts = {}) const override { tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; + uintptr_t count = 0; check_status(siderust_target_above_threshold( handle_, obs.to_c(), window.c_inner(), threshold.value(), opts.to_c(), &ptr, &count), @@ -135,9 +233,7 @@ class Target : public Trackable { return detail_periods_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector above_threshold( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree threshold, const SearchOptions& opts = {}) const { @@ -150,11 +246,11 @@ class Target : public Trackable { std::vector below_threshold( const Geodetic& obs, const Period& window, qtty::Degree threshold, const SearchOptions& opts = {}) const override { - // Target wraps an ICRS direction; use icrs_below_threshold FFI. + // Always pass ICRS direction to the FFI layer. siderust_spherical_dir_t dir_c{}; - dir_c.polar_deg = dec_deg(); - dir_c.azimuth_deg = ra_deg(); - dir_c.frame = SIDERUST_FRAME_T_ICRS; + dir_c.polar_deg = m_icrs_.dec().value(); + dir_c.azimuth_deg = m_icrs_.ra().value(); + dir_c.frame = SIDERUST_FRAME_T_ICRS; tempoch_period_mjd_t* ptr = nullptr; uintptr_t count = 0; check_status(siderust_icrs_below_threshold( @@ -164,9 +260,7 @@ class Target : public Trackable { return detail_periods_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector below_threshold( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree threshold, const SearchOptions& opts = {}) const { @@ -188,9 +282,7 @@ class Target : public Trackable { return detail::crossings_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector crossings( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree threshold, const SearchOptions& opts = {}) const { @@ -212,9 +304,7 @@ class Target : public Trackable { return detail::culminations_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector culminations( const Geodetic& obs, const MJD& start, const MJD& end, const SearchOptions& opts = {}) const { @@ -222,7 +312,7 @@ class Target : public Trackable { } // ------------------------------------------------------------------ - // Azimuth queries + // Azimuth queries (implements Trackable) // ------------------------------------------------------------------ /** @@ -251,9 +341,7 @@ class Target : public Trackable { return detail::az_crossings_from_c(ptr, count); } - /** - * @brief Backward-compatible [start, end] overload. - */ + /// Backward-compatible [start, end] overload. std::vector azimuth_crossings( const Geodetic& obs, const MJD& start, const MJD& end, qtty::Degree bearing, const SearchOptions& opts = {}) const { @@ -264,7 +352,10 @@ class Target : public Trackable { const SiderustTarget* c_handle() const { return handle_; } private: - SiderustTarget* handle_ = nullptr; + C m_dir_; + JulianDate m_epoch_; + spherical::direction::ICRS m_icrs_; + SiderustTarget* handle_ = nullptr; /// Build a Period vector from a tempoch_period_mjd_t* array. static std::vector detail_periods_from_c( @@ -279,4 +370,26 @@ class Target : public Trackable { } }; +// ============================================================================ +// Convenience type aliases +// ============================================================================ + +/// Fixed direction in ICRS (most common use-case). +using ICRSTarget = Target; + +/// Fixed direction in ICRF (treated identically to ICRS in Siderust). +using ICRFTarget = Target; + +/// Fixed direction in mean equatorial coordinates of J2000.0 (FK5). +using EquatorialMeanJ2000Target = Target; + +/// Fixed direction in mean equatorial coordinates of date (precessed only). +using EquatorialMeanOfDateTarget = Target; + +/// Fixed direction in true equatorial coordinates of date (precessed + nutated). +using EquatorialTrueOfDateTarget = Target; + +/// Fixed direction in mean ecliptic coordinates of J2000.0. +using EclipticMeanJ2000Target = Target; + } // namespace siderust diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index cc24154..72b2419 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -120,7 +120,73 @@ TEST_F(AltitudeTest, IcrsAltitudeAt) { TEST_F(AltitudeTest, IcrsAboveThreshold) { const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), qtty::Degree(38.78)); - auto periods = icrs_altitude::above_threshold( - vega_icrs, obs, window, qtty::Degree(30.0)); + auto periods = icrs_altitude::above_threshold( + vega_icrs, obs, window, qtty::Degree(30.0)); EXPECT_GT(periods.size(), 0u); } + +// ============================================================================ +// Target — generic strongly-typed target +// ============================================================================ + +// Vega ICRS coordinates (J2000): RA=279.2348°, Dec=+38.7836° +TEST_F(AltitudeTest, ICRSTargetAltitudeAt) { + ICRSTarget vega{ spherical::direction::ICRS{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + // altitude_at returns qtty::Degree (radian/degree bug-fix verification) + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, ICRSTargetAboveThreshold) { + ICRSTarget vega{ spherical::direction::ICRS{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); + // Vega should rise above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); +} + +TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { + ICRSTarget vega{ spherical::direction::ICRS{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); + EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); + // epoch defaults to J2000 + EXPECT_NEAR(vega.epoch().value(), 2451545.0, 1e-3); + // icrs_direction is the same for an ICRS Target + EXPECT_NEAR(vega.icrs_direction().ra().value(), 279.23, 1e-9); +} + +TEST_F(AltitudeTest, ICRSTargetPolymorphic) { + // Verify Target is usable through the Trackable interface + std::unique_ptr t = std::make_unique( + spherical::direction::ICRS{ qtty::Degree(279.23), qtty::Degree(38.78) }); + qtty::Degree alt = t->altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { + // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° + EclipticMeanJ2000Target ec{ spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8) } }; + // ecl direction retained on the C++ side + EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); + EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); + // ICRS ra/dec computed at construction and accessible + EXPECT_GT(ec.icrs_direction().ra().value(), 0.0); + EXPECT_LT(ec.icrs_direction().ra().value(), 360.0); + // altitude should be a valid degree value + qtty::Degree alt = ec.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, EquatorialMeanJ2000TargetAltitudeAt) { + EquatorialMeanJ2000Target vega{ spherical::direction::EquatorialMeanJ2000{ + qtty::Degree(279.23), qtty::Degree(38.78) } }; + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); +} diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 8cc6cc7..1e6e9d5 100644 --- a/tests/test_coordinates.cpp +++ b/tests/test_coordinates.cpp @@ -23,7 +23,7 @@ TEST(TypedCoordinates, AliasNamespaces) { TEST(TypedCoordinates, IcrsDirToEcliptic) { using namespace siderust::frames; - spherical::direction::ICRS vega(279.23473, 38.78369); + spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto jd = JulianDate::J2000(); // Compile-time typed transform: ICRS -> EclipticMeanJ2000 @@ -39,7 +39,7 @@ TEST(TypedCoordinates, IcrsDirToEcliptic) { TEST(TypedCoordinates, IcrsDirRoundtrip) { using namespace siderust::frames; - spherical::direction::ICRS icrs(100.0, 30.0); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); auto ecl = icrs.to_frame(jd); @@ -53,7 +53,7 @@ TEST(TypedCoordinates, IcrsDirRoundtrip) { TEST(TypedCoordinates, ToShorthand) { using namespace siderust::frames; - spherical::direction::ICRS icrs(100.0, 30.0); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); // .to(jd) is a shorthand for .to_frame(jd) @@ -65,7 +65,7 @@ TEST(TypedCoordinates, ToShorthand) { TEST(TypedCoordinates, IcrsDirToHorizontal) { using namespace siderust::frames; - spherical::direction::ICRS vega(279.23473, 38.78369); + spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); auto obs = ROQUE_DE_LOS_MUCHACHOS; @@ -79,7 +79,7 @@ TEST(TypedCoordinates, IcrsDirToHorizontal) { TEST(TypedCoordinates, EquatorialToIcrs) { using namespace siderust::frames; - spherical::direction::EquatorialMeanJ2000 eq(100.0, 30.0); + spherical::direction::EquatorialMeanJ2000 eq(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); auto icrs = eq.to_frame(jd); @@ -94,7 +94,7 @@ TEST(TypedCoordinates, MultiHopTransform) { using namespace siderust::frames; // EquatorialMeanOfDate -> EquatorialTrueOfDate (through hub) - spherical::Direction mean_od(100.0, 30.0); + spherical::Direction mean_od(qtty::Degree(100.0), qtty::Degree(30.0)); auto jd = JulianDate::J2000(); auto true_od = mean_od.to_frame(jd); @@ -108,7 +108,7 @@ TEST(TypedCoordinates, MultiHopTransform) { TEST(TypedCoordinates, SameFrameIdentity) { using namespace siderust::frames; - spherical::direction::ICRS icrs(123.456, -45.678); + spherical::direction::ICRS icrs(qtty::Degree(123.456), qtty::Degree(-45.678)); auto jd = JulianDate::J2000(); auto same = icrs.to_frame(jd); @@ -117,7 +117,7 @@ TEST(TypedCoordinates, SameFrameIdentity) { } TEST(TypedCoordinates, QttyDegreeAccessors) { - spherical::direction::ICRS d(123.456, -45.678); + spherical::direction::ICRS d(qtty::Degree(123.456), qtty::Degree(-45.678)); // Frame-specific getters for ICRS. qtty::Degree ra = d.ra(); From 716d7348c3d662de11e68423faa942196d120452 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 01:34:31 +0100 Subject: [PATCH 06/19] Refactor test cases for consistency and readability - Adjusted formatting in test files to improve readability by aligning code and comments. - Replaced multiple lines of code with more concise versions in tests for TypedCoordinates, Ephemeris, Observatories, and Time. - Ensured consistent use of spacing and indentation across all test cases. - Verified that all assertions and static assertions maintain their intended functionality. --- examples/altitude_events_example.cpp | 121 +-- examples/azimuth_lunar_phase_example.cpp | 102 +-- examples/coordinate_systems_example.cpp | 50 +- examples/coordinates_examples.cpp | 62 +- examples/demo.cpp | 169 +++-- examples/solar_system_bodies_example.cpp | 104 ++- examples/trackable_targets_example.cpp | 117 +-- include/siderust/altitude.hpp | 698 +++++++++--------- include/siderust/azimuth.hpp | 420 ++++++----- include/siderust/bodies.hpp | 327 ++++---- include/siderust/body_target.hpp | 340 ++++----- include/siderust/centers.hpp | 71 +- include/siderust/coordinates/cartesian.hpp | 97 +-- include/siderust/coordinates/conversions.hpp | 21 +- include/siderust/coordinates/geodetic.hpp | 60 +- include/siderust/coordinates/spherical.hpp | 416 ++++++----- .../types/cartesian/position/ecliptic.hpp | 12 +- .../types/spherical/direction/equatorial.hpp | 6 +- .../types/spherical/position/ecliptic.hpp | 3 +- include/siderust/ephemeris.hpp | 47 +- include/siderust/ffi_core.hpp | 197 ++--- include/siderust/frames.hpp | 194 +++-- include/siderust/lunar_phase.hpp | 330 +++++---- include/siderust/observatories.hpp | 39 +- include/siderust/siderust.hpp | 15 +- include/siderust/star_target.hpp | 123 +-- include/siderust/target.hpp | 563 +++++++------- include/siderust/time.hpp | 10 +- include/siderust/trackable.hpp | 122 +-- tests/main.cpp | 6 +- tests/test_altitude.cpp | 218 +++--- tests/test_bodies.cpp | 246 +++--- tests/test_coordinates.cpp | 216 +++--- tests/test_ephemeris.cpp | 66 +- tests/test_observatories.cpp | 40 +- tests/test_time.cpp | 122 +-- 36 files changed, 2938 insertions(+), 2812 deletions(-) diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp index ee88acf..f89a497 100644 --- a/examples/altitude_events_example.cpp +++ b/examples/altitude_events_example.cpp @@ -13,67 +13,72 @@ namespace { -void print_periods(const std::vector& periods, std::size_t limit) { - const std::size_t n = std::min(periods.size(), limit); - for (std::size_t i = 0; i < n; ++i) { - const auto& p = periods[i]; - std::cout << " " << (i + 1) << ") " - << p.start().to_utc() << " -> " << p.end().to_utc() - << " (" << std::fixed << std::setprecision(2) - << p.duration().value() << " h)\n"; - } +void print_periods(const std::vector &periods, + std::size_t limit) { + const std::size_t n = std::min(periods.size(), limit); + for (std::size_t i = 0; i < n; ++i) { + const auto &p = periods[i]; + std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " + << p.end().to_utc() << " (" << std::fixed << std::setprecision(2) + << p.duration().value() << " h)\n"; + } } } // namespace int main() { - using namespace siderust; - using namespace qtty::literals; - - const Geodetic obs = MAUNA_KEA; - const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD end = start + qtty::Day(2.0); - const Period window(start, end); - - SearchOptions opts; - opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step - - std::cout << "=== altitude_events_example ===\n"; - std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - - auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); - std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() << " period(s)\n"; - print_periods(sun_nights, 3); - - auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); - std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; - if (!sun_cross.empty()) { - const auto& c = sun_cross.front(); - std::cout << " First crossing: " << c.time.to_utc() - << " (" << c.direction << ")\n"; - } - - auto moon_culm = moon::culminations(obs, window, opts); - std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; - if (!moon_culm.empty()) { - const auto& c = moon_culm.front(); - std::cout << " First culmination: " << c.time.to_utc() - << " kind=" << c.kind - << " alt=" << c.altitude - << std::endl; - } - - auto vega_periods = star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); - std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; - print_periods(vega_periods, 2); - - spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); - auto dir_visible = icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); - std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() << " period(s)\n"; - - ICRSTarget fixed_target{ spherical::direction::ICRS{279.23473_deg, 38.78369_deg } }; - auto fixed_target_periods = fixed_target.above_threshold(obs, window, 45.0_deg, opts); - std::cout << "ICRSTarget::above_threshold(45 deg): " << fixed_target_periods.size() << " period(s)\n"; - - return 0; + using namespace siderust; + using namespace qtty::literals; + + const Geodetic obs = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(2.0); + const Period window(start, end); + + SearchOptions opts; + opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step + + std::cout << "=== altitude_events_example ===\n"; + std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; + + auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); + std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() + << " period(s)\n"; + print_periods(sun_nights, 3); + + auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); + std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; + if (!sun_cross.empty()) { + const auto &c = sun_cross.front(); + std::cout << " First crossing: " << c.time.to_utc() << " (" << c.direction + << ")\n"; + } + + auto moon_culm = moon::culminations(obs, window, opts); + std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; + if (!moon_culm.empty()) { + const auto &c = moon_culm.front(); + std::cout << " First culmination: " << c.time.to_utc() + << " kind=" << c.kind << " alt=" << c.altitude << std::endl; + } + + auto vega_periods = + star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); + std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; + print_periods(vega_periods, 2); + + spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); + auto dir_visible = + icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); + std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() + << " period(s)\n"; + + ICRSTarget fixed_target{ + spherical::direction::ICRS{279.23473_deg, 38.78369_deg}}; + auto fixed_target_periods = + fixed_target.above_threshold(obs, window, 45.0_deg, opts); + std::cout << "ICRSTarget::above_threshold(45 deg): " + << fixed_target_periods.size() << " period(s)\n"; + + return 0; } diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp index f07d006..d17f6a9 100644 --- a/examples/azimuth_lunar_phase_example.cpp +++ b/examples/azimuth_lunar_phase_example.cpp @@ -11,67 +11,69 @@ #include -namespace { -} // namespace +namespace {} // namespace int main() { - using namespace siderust; - using namespace qtty::literals; + using namespace siderust; + using namespace qtty::literals; - const Geodetic site = MAUNA_KEA; - const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD end = start + qtty::Day(3.0); - const Period window(start, end); + const Geodetic site = MAUNA_KEA; + const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD end = start + qtty::Day(3.0); + const Period window(start, end); - std::cout << "=== azimuth_lunar_phase_example ===\n"; - std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; + std::cout << "=== azimuth_lunar_phase_example ===\n"; + std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() + << "\n\n"; - const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); - std::cout << "Instant azimuth\n"; - std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; - std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; - std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) << std::endl; + const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); + std::cout << "Instant azimuth\n"; + std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; + std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; + std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) + << std::endl; - auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); - auto sun_ext = sun::azimuth_extrema(site, window); - auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); + auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); + auto sun_ext = sun::azimuth_extrema(site, window); + auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); - std::cout << "Azimuth events\n"; - std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; - std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; - if (!sun_ext.empty()) { - const auto& e = sun_ext.front(); - std::cout << " first extremum " << e.kind - << " at " << e.time.to_utc() - << " az=" << e.azimuth - << std::endl; - } - std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() << " period(s)\n\n"; + std::cout << "Azimuth events\n"; + std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; + std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; + if (!sun_ext.empty()) { + const auto &e = sun_ext.front(); + std::cout << " first extremum " << e.kind << " at " << e.time.to_utc() + << " az=" << e.azimuth << std::endl; + } + std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() + << " period(s)\n\n"; - const JulianDate jd_now = now.to_jd(); - auto geo_phase = moon::phase_geocentric(jd_now); - auto topo_phase = moon::phase_topocentric(jd_now, site); - auto topo_label = moon::phase_label(topo_phase); + const JulianDate jd_now = now.to_jd(); + auto geo_phase = moon::phase_geocentric(jd_now); + auto topo_phase = moon::phase_topocentric(jd_now, site); + auto topo_label = moon::phase_label(topo_phase); - auto phase_events = moon::find_phase_events( - Period(start, start + qtty::Day(30.0))); - auto half_lit = moon::illumination_range(window, 0.45, 0.55); + auto phase_events = + moon::find_phase_events(Period(start, start + qtty::Day(30.0))); + auto half_lit = moon::illumination_range(window, 0.45, 0.55); - std::cout << "Lunar phase\n"; - std::cout << std::fixed << std::setprecision(3) - << " Geocentric illuminated fraction: " << geo_phase.illuminated_fraction << "\n" - << " Topocentric illuminated fraction: " << topo_phase.illuminated_fraction - << " (" << topo_label << ")\n"; + std::cout << "Lunar phase\n"; + std::cout << std::fixed << std::setprecision(3) + << " Geocentric illuminated fraction: " + << geo_phase.illuminated_fraction << "\n" + << " Topocentric illuminated fraction: " + << topo_phase.illuminated_fraction << " (" << topo_label << ")\n"; - std::cout << " Principal phase events in next 30 days: " << phase_events.size() << "\n"; - const std::size_t n = std::min(phase_events.size(), 4); - for (std::size_t i = 0; i < n; ++i) { - const auto& ev = phase_events[i]; - std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; - } + std::cout << " Principal phase events in next 30 days: " + << phase_events.size() << "\n"; + const std::size_t n = std::min(phase_events.size(), 4); + for (std::size_t i = 0; i < n; ++i) { + const auto &ev = phase_events[i]; + std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; + } - std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " - << half_lit.size() << "\n"; + std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " + << half_lit.size() << "\n"; - return 0; + return 0; } diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp index fcefb5a..3a144e4 100644 --- a/examples/coordinate_systems_example.cpp +++ b/examples/coordinate_systems_example.cpp @@ -11,37 +11,37 @@ #include int main() { - using namespace siderust; - using namespace siderust::frames; + using namespace siderust; + using namespace siderust::frames; - std::cout << "=== coordinate_systems_example ===\n"; + std::cout << "=== coordinate_systems_example ===\n"; - static_assert(has_frame_transform_v); - static_assert(has_frame_transform_v); - static_assert(has_horizontal_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_frame_transform_v); + static_assert(has_horizontal_transform_v); - const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; - const auto ecef = observer.to_cartesian(); + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + const auto ecef = observer.to_cartesian(); - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::Direction src(qtty::Degree(279.23473), qtty::Degree(38.78369)); - const auto ecl = src.to_frame(jd); - const auto mod = src.to_frame(jd); - const auto tod = mod.to_frame(jd); - const auto horiz = src.to_horizontal(jd, observer); + spherical::Direction src(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + const auto ecl = src.to_frame(jd); + const auto mod = src.to_frame(jd); + const auto tod = mod.to_frame(jd); + const auto horiz = src.to_horizontal(jd, observer); - std::cout << std::fixed << std::setprecision(6); - std::cout << "Observer: " << observer << std::endl; - std::cout << "Observer in ECEF: " << ecef << std::endl; + std::cout << std::fixed << std::setprecision(6); + std::cout << "Observer: " << observer << std::endl; + std::cout << "Observer in ECEF: " << ecef << std::endl; + std::cout << "Frame transforms for Vega-like direction\n"; + std::cout << " ICRS RA/Dec : " << src << "\n"; + std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; + std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; + std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; + std::cout << " Horizontal az/alt : " << horiz << "\n"; - std::cout << "Frame transforms for Vega-like direction\n"; - std::cout << " ICRS RA/Dec : " << src << "\n"; - std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; - std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; - std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; - std::cout << " Horizontal az/alt : " << horiz << "\n"; - - return 0; + return 0; } diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp index d13ccff..ee6fdb8 100644 --- a/examples/coordinates_examples.cpp +++ b/examples/coordinates_examples.cpp @@ -11,46 +11,46 @@ #include int main() { - using namespace siderust; - using namespace qtty::literals; + using namespace siderust; + using namespace qtty::literals; - std::cout << "=== coordinates_examples ===\n"; + std::cout << "=== coordinates_examples ===\n"; - const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); - const auto ecef_m = site.to_cartesian(); - const auto ecef_km = site.to_cartesian(); + const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); + const auto ecef_m = site.to_cartesian(); + const auto ecef_km = site.to_cartesian(); - static_assert(std::is_same_v< - std::remove_cv_t, - cartesian::position::ECEF>); + static_assert(std::is_same_v, + cartesian::position::ECEF>); - std::cout << "Geodetic -> ECEF \n " - << site << "\n" - << ecef_m << "\n" - << "(" << ecef_km << ")\n"<< std::endl; + std::cout << "Geodetic -> ECEF \n " << site << "\n" + << ecef_m << "\n" + << "(" << ecef_km << ")\n" + << std::endl; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_true = vega_icrs.to_frame(jd); - auto vega_horiz = vega_icrs.to_horizontal(jd, site); + spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_true = vega_icrs.to_frame(jd); + auto vega_horiz = vega_icrs.to_horizontal(jd, site); - std::cout << "Direction transforms\n"; - std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; - std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; - std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; - std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; + std::cout << "Direction transforms\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; + std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; + std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; + std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; - spherical::position::ICRS synthetic_star( - 210.0_deg, -12.0_deg, 4.2_au); + spherical::position::ICRS synthetic_star( + 210.0_deg, -12.0_deg, 4.2_au); - cartesian::position::EclipticMeanJ2000 earth = - ephemeris::earth_heliocentric(jd); + cartesian::position::EclipticMeanJ2000 earth = + ephemeris::earth_heliocentric(jd); - std::cout << "Typed positions\n"; - std::cout << " Synthetic star distance: " << synthetic_star.distance() << std::endl; - std::cout << " Earth heliocentric x: " << earth.x() << std::endl; + std::cout << "Typed positions\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance() + << std::endl; + std::cout << " Earth heliocentric x: " << earth.x() << std::endl; - return 0; + return 0; } diff --git a/examples/demo.cpp b/examples/demo.cpp index 5ea5d96..a0e8c23 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -13,92 +13,89 @@ #include -namespace { - -} // namespace +namespace {} // namespace int main() { - using namespace siderust; - - const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - const MJD now = MJD::from_jd(jd); - const Period next_day(now, now + qtty::Day(1.0)); - - std::cout << "=== siderust-cpp extended demo ===\n"; - std::cout << "Observer: " << site << "\n"; - std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() - << " UTC " << jd.to_utc() << "\n\n"; - - spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_hor = vega_icrs.to_horizontal(jd, site); - std::cout << "Typed coordinates\n"; - std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; - std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; - std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; - - qtty::Degree sun_alt = sun::altitude_at(site, now).to(); - qtty::Degree sun_az = sun::azimuth_at(site, now); - std::cout << "Sun instant\n"; - std::cout << " Altitude=" << sun_alt.value() << " deg" - << " Azimuth=" << sun_az.value() << " deg\n"; - - auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); - if (!sun_crossings.empty()) { - std::cout << " Next horizon crossing: " - << sun_crossings.front().time.to_utc() << " (" - << sun_crossings.front().direction - << ")\n"; - } - std::cout << "\n"; - - BodyTarget mars(Body::Mars); - ICRSTarget fixed_target{ spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369) } }; // Vega-like - - std::vector>> targets; - targets.push_back({"Sun", std::make_unique(Body::Sun)}); - targets.push_back({"Vega", std::make_unique(VEGA)}); - targets.push_back({"Fixed target", std::make_unique( - spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); - - std::cout << "Trackable polymorphism\n"; - for (const auto& entry : targets) { - const auto& name = entry.first; - const auto& obj = entry.second; - auto alt = obj->altitude_at(site, now); - auto az = obj->azimuth_at(site, now); - std::cout << " " << std::setw(12) << std::left << name - << " alt=" << std::setw(8) << alt - << " az=" << az << std::endl; - } - std::cout << " Mars altitude via BodyTarget: " - << mars.altitude_at(site, now).value() << " deg\n"; - std::cout << " Fixed Target altitude: " - << fixed_target.altitude_at(site, now).value() << " deg\n\n"; - - auto earth_helio = ephemeris::earth_heliocentric(jd); - auto moon_geo = ephemeris::moon_geocentric(jd); - double moon_dist_km = std::sqrt( - moon_geo.x().value() * moon_geo.x().value() + - moon_geo.y().value() * moon_geo.y().value() + - moon_geo.z().value() * moon_geo.z().value()); - - std::cout << "Ephemeris\n"; - std::cout << " Earth heliocentric " << earth_helio << " AU\n"; - std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - - auto phase = moon::phase_topocentric(jd, site); - auto label = moon::phase_label(phase); - auto bright_periods = moon::illumination_above( - Period(now, now + qtty::Day(7.0)), 0.8); - - std::cout << "Lunar phase\n"; - std::cout << " Illuminated fraction=" << phase.illuminated_fraction - << " label=" << label << "\n"; - std::cout << " Bright-moon periods (next 7 days, k>=0.8): " - << bright_periods.size() << "\n"; - - return 0; + using namespace siderust; + + const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period next_day(now, now + qtty::Day(1.0)); + + std::cout << "=== siderust-cpp extended demo ===\n"; + std::cout << "Observer: " << site << "\n"; + std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() + << " UTC " << jd.to_utc() << "\n\n"; + + spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_hor = vega_icrs.to_horizontal(jd, site); + std::cout << "Typed coordinates\n"; + std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; + std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; + std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; + + qtty::Degree sun_alt = sun::altitude_at(site, now).to(); + qtty::Degree sun_az = sun::azimuth_at(site, now); + std::cout << "Sun instant\n"; + std::cout << " Altitude=" << sun_alt.value() << " deg" + << " Azimuth=" << sun_az.value() << " deg\n"; + + auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); + if (!sun_crossings.empty()) { + std::cout << " Next horizon crossing: " + << sun_crossings.front().time.to_utc() << " (" + << sun_crossings.front().direction << ")\n"; + } + std::cout << "\n"; + + BodyTarget mars(Body::Mars); + ICRSTarget fixed_target{spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)}}; // Vega-like + + std::vector>> targets; + targets.push_back({"Sun", std::make_unique(Body::Sun)}); + targets.push_back({"Vega", std::make_unique(VEGA)}); + targets.push_back( + {"Fixed target", std::make_unique(spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)})}); + + std::cout << "Trackable polymorphism\n"; + for (const auto &entry : targets) { + const auto &name = entry.first; + const auto &obj = entry.second; + auto alt = obj->altitude_at(site, now); + auto az = obj->azimuth_at(site, now); + std::cout << " " << std::setw(12) << std::left << name + << " alt=" << std::setw(8) << alt << " az=" << az << std::endl; + } + std::cout << " Mars altitude via BodyTarget: " + << mars.altitude_at(site, now).value() << " deg\n"; + std::cout << " Fixed Target altitude: " + << fixed_target.altitude_at(site, now).value() << " deg\n\n"; + + auto earth_helio = ephemeris::earth_heliocentric(jd); + auto moon_geo = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + + moon_geo.y().value() * moon_geo.y().value() + + moon_geo.z().value() * moon_geo.z().value()); + + std::cout << "Ephemeris\n"; + std::cout << " Earth heliocentric " << earth_helio << " AU\n"; + std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; + + auto phase = moon::phase_topocentric(jd, site); + auto label = moon::phase_label(phase); + auto bright_periods = + moon::illumination_above(Period(now, now + qtty::Day(7.0)), 0.8); + + std::cout << "Lunar phase\n"; + std::cout << " Illuminated fraction=" << phase.illuminated_fraction + << " label=" << label << "\n"; + std::cout << " Bright-moon periods (next 7 days, k>=0.8): " + << bright_periods.size() << "\n"; + + return 0; } diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp index 51971eb..25c69d7 100644 --- a/examples/solar_system_bodies_example.cpp +++ b/examples/solar_system_bodies_example.cpp @@ -13,71 +13,69 @@ namespace { -const char* az_kind_name(siderust::AzimuthExtremumKind kind) { - using siderust::AzimuthExtremumKind; - switch (kind) { - case AzimuthExtremumKind::Max: - return "max"; - case AzimuthExtremumKind::Min: - return "min"; - } - return "unknown"; +const char *az_kind_name(siderust::AzimuthExtremumKind kind) { + using siderust::AzimuthExtremumKind; + switch (kind) { + case AzimuthExtremumKind::Max: + return "max"; + case AzimuthExtremumKind::Min: + return "min"; + } + return "unknown"; } } // namespace int main() { - using namespace siderust; + using namespace siderust; - const Geodetic site = MAUNA_KEA; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD now = MJD::from_jd(jd); - const Period window(now, now + qtty::Day(2.0)); + const Geodetic site = MAUNA_KEA; + const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); + const MJD now = MJD::from_jd(jd); + const Period window(now, now + qtty::Day(2.0)); - std::cout << "=== solar_system_bodies_example ===\n"; - std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; + std::cout << "=== solar_system_bodies_example ===\n"; + std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; - std::cout << "Planet catalog constants\n"; - std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" - << " radius=" << MERCURY.radius_km << " km\n"; - std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" - << " radius=" << EARTH.radius_km << " km\n"; - std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" - << " radius=" << JUPITER.radius_km << " km\n\n"; + std::cout << "Planet catalog constants\n"; + std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" + << " radius=" << MERCURY.radius_km << " km\n"; + std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" + << " radius=" << EARTH.radius_km << " km\n"; + std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" + << " radius=" << JUPITER.radius_km << " km\n\n"; - auto earth = ephemeris::earth_heliocentric(jd); - auto moon_pos = ephemeris::moon_geocentric(jd); - double moon_dist_km = std::sqrt( - moon_pos.x().value() * moon_pos.x().value() + - moon_pos.y().value() * moon_pos.y().value() + - moon_pos.z().value() * moon_pos.z().value()); + auto earth = ephemeris::earth_heliocentric(jd); + auto moon_pos = ephemeris::moon_geocentric(jd); + double moon_dist_km = std::sqrt(moon_pos.x().value() * moon_pos.x().value() + + moon_pos.y().value() * moon_pos.y().value() + + moon_pos.z().value() * moon_pos.z().value()); - std::cout << "Ephemeris\n"; - std::cout << std::fixed << std::setprecision(6) - << " Earth heliocentric x=" << earth.x().value() - << " AU y=" << earth.y().value() << " AU\n"; - std::cout << std::setprecision(2) - << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; + std::cout << "Ephemeris\n"; + std::cout << std::fixed << std::setprecision(6) + << " Earth heliocentric x=" << earth.x().value() + << " AU y=" << earth.y().value() << " AU\n"; + std::cout << std::setprecision(2) + << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, Body::Jupiter}; + std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, + Body::Jupiter}; - std::cout << "Body dispatch API at observer\n"; - for (Body b : tracked) { - auto alt = body::altitude_at(b, site, now).to(); - auto az = body::azimuth_at(b, site, now).to(); - std::cout << " body=" << static_cast(b) - << " alt=" << alt - << " az=" << az << std::endl; - } + std::cout << "Body dispatch API at observer\n"; + for (Body b : tracked) { + auto alt = body::altitude_at(b, site, now).to(); + auto az = body::azimuth_at(b, site, now).to(); + std::cout << " body=" << static_cast(b) << " alt=" << alt + << " az=" << az << std::endl; + } - auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); - if (!moon_extrema.empty()) { - const auto& e = moon_extrema.front(); - std::cout << "\nMoon azimuth extrema\n"; - std::cout << " first " << az_kind_name(e.kind) - << " at " << e.time.to_utc() - << " az=" << e.azimuth << std::endl; - } + auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); + if (!moon_extrema.empty()) { + const auto &e = moon_extrema.front(); + std::cout << "\nMoon azimuth extrema\n"; + std::cout << " first " << az_kind_name(e.kind) << " at " << e.time.to_utc() + << " az=" << e.azimuth << std::endl; + } - return 0; + return 0; } diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp index e8e33b3..2e578be 100644 --- a/examples/trackable_targets_example.cpp +++ b/examples/trackable_targets_example.cpp @@ -1,7 +1,8 @@ /** * @file trackable_targets_example.cpp * @example trackable_targets_example.cpp - * @brief Using Target, StarTarget, BodyTarget through Trackable polymorphism. + * @brief Using Target, StarTarget, BodyTarget through Trackable + * polymorphism. * * Demonstrates the strongly-typed Target template with multiple frames: * - ICRSTarget — fixed direction in ICRS equatorial coordinates @@ -23,67 +24,69 @@ namespace { struct NamedTrackable { - std::string name; - std::unique_ptr object; + std::string name; + std::unique_ptr object; }; } // namespace int main() { - using namespace siderust; - - const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); - const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); - const Period window(now, now + qtty::Day(1.0)); - - std::cout << "=== trackable_targets_example ===\n"; - std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; - - // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. - ICRSTarget fixed_vega_like{ spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369) } }; - std::cout << "ICRSTarget metadata\n"; - std::cout << " RA/Dec=" << fixed_vega_like.direction() - << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; - - // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). - // Automatically converted to ICRS by the constructor. - EclipticMeanJ2000Target ecliptic_vega{ spherical::direction::EclipticMeanJ2000{ - qtty::Degree(279.6), qtty::Degree(61.8) } }; - auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); - std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; - std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; - std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() << " (converted)\n"; - std::cout << " alt=" << alt_ecliptic << std::endl; - - std::vector catalog; - catalog.push_back({"Sun", std::make_unique(Body::Sun)}); - catalog.push_back({"Mars", std::make_unique(Body::Mars)}); - catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); - catalog.push_back({"Fixed Vega-like (ICRS)", std::make_unique( - spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369) })}); - - for (const auto& entry : catalog) { - const auto& t = entry.object; - auto alt = t->altitude_at(site, now); - auto az = t->azimuth_at(site, now); - - std::cout << std::left << std::setw(22) << entry.name << std::right - << " alt=" << std::setw(9) << alt - << " az=" << az << std::endl; - - auto crossings = t->crossings(site, window, qtty::Degree(0.0)); - if (!crossings.empty()) { - const auto& first = crossings.front(); - std::cout << " first horizon crossing: " << first.time.to_utc() - << " (" << first.direction << ")\n"; - } - - auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); - if (!az_cross.empty()) { - std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() << "\n"; - } + using namespace siderust; + + const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); + const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); + const Period window(now, now + qtty::Day(1.0)); + + std::cout << "=== trackable_targets_example ===\n"; + std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; + + // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. + ICRSTarget fixed_vega_like{spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)}}; + std::cout << "ICRSTarget metadata\n"; + std::cout << " RA/Dec=" << fixed_vega_like.direction() + << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; + + // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). + // Automatically converted to ICRS by the constructor. + EclipticMeanJ2000Target ecliptic_vega{spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8)}}; + auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); + std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; + std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; + std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() + << " (converted)\n"; + std::cout << " alt=" << alt_ecliptic << std::endl; + + std::vector catalog; + catalog.push_back({"Sun", std::make_unique(Body::Sun)}); + catalog.push_back({"Mars", std::make_unique(Body::Mars)}); + catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); + catalog.push_back({"Fixed Vega-like (ICRS)", + std::make_unique(spherical::direction::ICRS{ + qtty::Degree(279.23473), qtty::Degree(38.78369)})}); + + for (const auto &entry : catalog) { + const auto &t = entry.object; + auto alt = t->altitude_at(site, now); + auto az = t->azimuth_at(site, now); + + std::cout << std::left << std::setw(22) << entry.name << std::right + << " alt=" << std::setw(9) << alt << " az=" << az << std::endl; + + auto crossings = t->crossings(site, window, qtty::Degree(0.0)); + if (!crossings.empty()) { + const auto &first = crossings.front(); + std::cout << " first horizon crossing: " << first.time.to_utc() << " (" + << first.direction << ")\n"; } - return 0; + auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); + if (!az_cross.empty()) { + std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() + << "\n"; + } + } + + return 0; } diff --git a/include/siderust/altitude.hpp b/include/siderust/altitude.hpp index aefee3c..00529f8 100644 --- a/include/siderust/altitude.hpp +++ b/include/siderust/altitude.hpp @@ -2,7 +2,8 @@ /** * @file altitude.hpp - * @brief Altitude computations for Sun, Moon, stars, and arbitrary ICRS directions. + * @brief Altitude computations for Sun, Moon, stars, and arbitrary ICRS + * directions. * * Wraps siderust-ffi's altitude API with exception-safe C++ types and * RAII-managed output arrays. @@ -24,25 +25,26 @@ namespace siderust { * @brief A threshold-crossing event (rising or setting). */ struct CrossingEvent { - MJD time; - CrossingDirection direction; + MJD time; + CrossingDirection direction; - static CrossingEvent from_c(const siderust_crossing_event_t& c) { - return {MJD(c.mjd), static_cast(c.direction)}; - } + static CrossingEvent from_c(const siderust_crossing_event_t &c) { + return {MJD(c.mjd), static_cast(c.direction)}; + } }; /** * @brief A culmination (local altitude extremum) event. */ struct CulminationEvent { - MJD time; - qtty::Degree altitude; - CulminationKind kind; - - static CulminationEvent from_c(const siderust_culmination_event_t& c) { - return {MJD(c.mjd), qtty::Degree(c.altitude_deg), static_cast(c.kind)}; - } + MJD time; + qtty::Degree altitude; + CulminationKind kind; + + static CulminationEvent from_c(const siderust_culmination_event_t &c) { + return {MJD(c.mjd), qtty::Degree(c.altitude_deg), + static_cast(c.kind)}; + } }; // ============================================================================ @@ -53,28 +55,28 @@ struct CulminationEvent { * @brief Options for altitude search algorithms. */ struct SearchOptions { - double time_tolerance_days = 1e-9; - double scan_step_days = 0.0; - bool has_scan_step = false; - - SearchOptions() = default; - - /// Set a custom scan step. - SearchOptions& with_scan_step(double step) { - scan_step_days = step; - has_scan_step = true; - return *this; - } - - /// Set time tolerance. - SearchOptions& with_tolerance(double tol) { - time_tolerance_days = tol; - return *this; - } - - siderust_search_opts_t to_c() const { - return {time_tolerance_days, scan_step_days, has_scan_step}; - } + double time_tolerance_days = 1e-9; + double scan_step_days = 0.0; + bool has_scan_step = false; + + SearchOptions() = default; + + /// Set a custom scan step. + SearchOptions &with_scan_step(double step) { + scan_step_days = step; + has_scan_step = true; + return *this; + } + + /// Set time tolerance. + SearchOptions &with_tolerance(double tol) { + time_tolerance_days = tol; + return *this; + } + + siderust_search_opts_t to_c() const { + return {time_tolerance_days, scan_step_days, has_scan_step}; + } }; // ============================================================================ @@ -82,34 +84,37 @@ struct SearchOptions { // ============================================================================ namespace detail { -inline std::vector periods_from_c(tempoch_period_mjd_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(Period::from_c(ptr[i])); - } - siderust_periods_free(ptr, count); - return result; -} - -inline std::vector crossings_from_c(siderust_crossing_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(CrossingEvent::from_c(ptr[i])); - } - siderust_crossings_free(ptr, count); - return result; -} - -inline std::vector culminations_from_c(siderust_culmination_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(CulminationEvent::from_c(ptr[i])); - } - siderust_culminations_free(ptr, count); - return result; +inline std::vector periods_from_c(tempoch_period_mjd_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period::from_c(ptr[i])); + } + siderust_periods_free(ptr, count); + return result; +} + +inline std::vector +crossings_from_c(siderust_crossing_event_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(CrossingEvent::from_c(ptr[i])); + } + siderust_crossings_free(ptr, count); + return result; +} + +inline std::vector +culminations_from_c(siderust_culmination_event_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(CulminationEvent::from_c(ptr[i])); + } + siderust_culminations_free(ptr, count); + return result; } } // namespace detail @@ -123,137 +128,145 @@ namespace sun { /** * @brief Compute the Sun's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_sun_altitude_at(obs.to_c(), mjd.value(), &out), - "sun::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_sun_altitude_at(obs.to_c(), mjd.value(), &out), + "sun::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when the Sun is above a threshold altitude. */ -inline std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_above_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "sun::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_above_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "sun::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector above_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold(obs, Period(start, end), threshold, opts); +inline std::vector above_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find periods when the Sun is below a threshold altitude. */ -inline std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_below_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "sun::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_below_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "sun::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector below_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold(obs, Period(start, end), threshold, opts); +inline std::vector below_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find threshold-crossing events for the Sun. */ -inline std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_crossings( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "sun::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_crossings(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, + &count), + "sun::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return crossings(obs, Period(start, end), threshold, opts); +inline std::vector crossings(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return crossings(obs, Period(start, end), threshold, opts); } /** * @brief Find culmination events for the Sun. */ -inline std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_culminations( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "sun::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_culminations(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "sun::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector culminations( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return culminations(obs, Period(start, end), opts); +inline std::vector +culminations(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return culminations(obs, Period(start, end), opts); } /** * @brief Find periods when the Sun's altitude is within [min, max]. */ -inline std::vector altitude_periods( - const Geodetic& obs, const Period& window, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_altitude_periods(q, &ptr, &count), - "sun::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_altitude_periods(q, &ptr, &count), + "sun::altitude_periods"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector altitude_periods( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_altitude_periods(q, &ptr, &count), - "sun::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), + min_alt.value(), max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_altitude_periods(q, &ptr, &count), + "sun::altitude_periods"); + return detail::periods_from_c(ptr, count); } } // namespace sun @@ -267,137 +280,145 @@ namespace moon { /** * @brief Compute the Moon's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_moon_altitude_at(obs.to_c(), mjd.value(), &out), - "moon::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_moon_altitude_at(obs.to_c(), mjd.value(), &out), + "moon::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when the Moon is above a threshold altitude. */ -inline std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_above_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "moon::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_above_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "moon::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector above_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold(obs, Period(start, end), threshold, opts); +inline std::vector above_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find periods when the Moon is below a threshold altitude. */ -inline std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_below_threshold( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "moon::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_below_threshold(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), + &ptr, &count), + "moon::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector below_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold(obs, Period(start, end), threshold, opts); +inline std::vector below_threshold(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(obs, Period(start, end), threshold, opts); } /** * @brief Find threshold-crossing events for the Moon. */ -inline std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_crossings( - obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "moon::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_crossings(obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, + &count), + "moon::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return crossings(obs, Period(start, end), threshold, opts); +inline std::vector crossings(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return crossings(obs, Period(start, end), threshold, opts); } /** * @brief Find culmination events for the Moon. */ -inline std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_culminations( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "moon::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_culminations(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "moon::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector culminations( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return culminations(obs, Period(start, end), opts); +inline std::vector +culminations(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return culminations(obs, Period(start, end), opts); } /** * @brief Find periods when the Moon's altitude is within [min, max]. */ -inline std::vector altitude_periods( - const Geodetic& obs, const Period& window, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_altitude_periods(q, &ptr, &count), - "moon::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_altitude_periods(q, &ptr, &count), + "moon::altitude_periods"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector altitude_periods( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_altitude_periods(q, &ptr, &count), - "moon::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), start.value(), end.value(), + min_alt.value(), max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_altitude_periods(q, &ptr, &count), + "moon::altitude_periods"); + return detail::periods_from_c(ptr, count); } } // namespace moon @@ -411,108 +432,115 @@ namespace star_altitude { /** * @brief Compute a star's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(const Star& s, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_star_altitude_at( - s.c_handle(), obs.to_c(), mjd.value(), &out), - "star_altitude::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const Star &s, const Geodetic &obs, + const MJD &mjd) { + double out; + check_status( + siderust_star_altitude_at(s.c_handle(), obs.to_c(), mjd.value(), &out), + "star_altitude::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when a star is above a threshold altitude. */ -inline std::vector above_threshold( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_above_threshold( - s.c_handle(), obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "star_altitude::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(const Star &s, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_above_threshold( + s.c_handle(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "star_altitude::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector above_threshold( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold(s, obs, Period(start, end), threshold, opts); +inline std::vector above_threshold(const Star &s, const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(s, obs, Period(start, end), threshold, opts); } /** * @brief Find periods when a star is below a threshold altitude. */ -inline std::vector below_threshold( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_below_threshold( - s.c_handle(), obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "star_altitude::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(const Star &s, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_below_threshold( + s.c_handle(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "star_altitude::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector below_threshold( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold(s, obs, Period(start, end), threshold, opts); +inline std::vector below_threshold(const Star &s, const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(s, obs, Period(start, end), threshold, opts); } /** * @brief Find threshold-crossing events for a star. */ -inline std::vector crossings( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_crossings( - s.c_handle(), obs.to_c(), window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "star_altitude::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(const Star &s, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_crossings(s.c_handle(), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "star_altitude::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector crossings( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return crossings(s, obs, Period(start, end), threshold, opts); +inline std::vector crossings(const Star &s, const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return crossings(s, obs, Period(start, end), threshold, opts); } /** * @brief Find culmination events for a star. */ -inline std::vector culminations( - const Star& s, const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_culminations( - s.c_handle(), obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "star_altitude::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(const Star &s, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_culminations(s.c_handle(), obs.to_c(), + window.c_inner(), opts.to_c(), &ptr, + &count), + "star_altitude::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector culminations( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return culminations(s, obs, Period(start, end), opts); +inline std::vector +culminations(const Star &s, const Geodetic &obs, const MJD &start, + const MJD &end, const SearchOptions &opts = {}) { + return culminations(s, obs, Period(start, end), opts); } } // namespace star_altitude @@ -526,83 +554,77 @@ namespace icrs_altitude { /** * @brief Compute altitude (radians) for a fixed ICRS direction. */ -inline qtty::Radian altitude_at(const spherical::direction::ICRS& dir, - const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_icrs_altitude_at( - dir.to_c(), obs.to_c(), mjd.value(), &out), - "icrs_altitude::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(const spherical::direction::ICRS &dir, + const Geodetic &obs, const MJD &mjd) { + double out; + check_status( + siderust_icrs_altitude_at(dir.to_c(), obs.to_c(), mjd.value(), &out), + "icrs_altitude::altitude_at"); + return qtty::Radian(out); } /** * @brief Backward-compatible RA/Dec overload. */ inline qtty::Radian altitude_at(qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& mjd) { - return altitude_at(spherical::direction::ICRS(ra, dec), obs, mjd); + const Geodetic &obs, const MJD &mjd) { + return altitude_at(spherical::direction::ICRS(ra, dec), obs, mjd); } /** * @brief Find periods when a fixed ICRS direction is above a threshold. */ -inline std::vector above_threshold( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_above_threshold( - dir.to_c(), obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "icrs_altitude::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector +above_threshold(const spherical::direction::ICRS &dir, const Geodetic &obs, + const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_above_threshold( + dir.to_c(), obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "icrs_altitude::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible RA/Dec + [start, end] overload. */ -inline std::vector above_threshold( - qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return above_threshold( - spherical::direction::ICRS(ra, dec), - obs, - Period(start, end), - threshold, - opts); +inline std::vector above_threshold(qtty::Degree ra, qtty::Degree dec, + const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return above_threshold(spherical::direction::ICRS(ra, dec), obs, + Period(start, end), threshold, opts); } /** * @brief Find periods when a fixed ICRS direction is below a threshold. */ -inline std::vector below_threshold( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_below_threshold( - dir.to_c(), obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "icrs_altitude::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector +below_threshold(const spherical::direction::ICRS &dir, const Geodetic &obs, + const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_below_threshold( + dir.to_c(), obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "icrs_altitude::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible RA/Dec + [start, end] overload. */ -inline std::vector below_threshold( - qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) { - return below_threshold( - spherical::direction::ICRS(ra, dec), - obs, - Period(start, end), - threshold, - opts); +inline std::vector below_threshold(qtty::Degree ra, qtty::Degree dec, + const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + return below_threshold(spherical::direction::ICRS(ra, dec), obs, + Period(start, end), threshold, opts); } } // namespace icrs_altitude diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 4f88f40..37640bd 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -2,18 +2,19 @@ /** * @file azimuth.hpp - * @brief Azimuth computations for Sun, Moon, stars, and arbitrary ICRS directions. + * @brief Azimuth computations for Sun, Moon, stars, and arbitrary ICRS + * directions. * * Wraps siderust-ffi's azimuth API with exception-safe C++ types and * RAII-managed output arrays. * * ### Covered computations - * | Subject | azimuth_at | azimuth_crossings | azimuth_extrema | in_azimuth_range | + * | Subject | azimuth_at | azimuth_crossings | azimuth_extrema | + * in_azimuth_range | * |---------|:----------:|:-----------------:|:---------------:|:----------------:| - * | Sun | ✓ | ✓ | ✓ | ✓ | - * | Moon | ✓ | ✓ | ✓ | ✓ | - * | Star | ✓ | ✓ | – | – | - * | ICRS | ✓ | – | – | – | + * | Sun | ✓ | ✓ | ✓ | ✓ | | Moon | ✓ + * | ✓ | ✓ | ✓ | | Star | ✓ | ✓ + * | – | – | | ICRS | ✓ | – | – | – | */ #include "altitude.hpp" @@ -34,34 +35,36 @@ namespace siderust { * @brief Distinguishes azimuth extrema: northernmost or southernmost bearing. */ enum class AzimuthExtremumKind : int32_t { - Max = 0, ///< Northernmost (or easternmost) direction reached by the body. - Min = 1, ///< Southernmost (or westernmost) direction reached by the body. + Max = 0, ///< Northernmost (or easternmost) direction reached by the body. + Min = 1, ///< Southernmost (or westernmost) direction reached by the body. }; /** * @brief An azimuth bearing-crossing event. */ struct AzimuthCrossingEvent { - MJD time; ///< Epoch of the crossing (MJD). - CrossingDirection direction; ///< Whether the azimuth is increasing or decreasing. - - static AzimuthCrossingEvent from_c(const siderust_azimuth_crossing_event_t& c) { - return {MJD(c.mjd), static_cast(c.direction)}; - } + MJD time; ///< Epoch of the crossing (MJD). + CrossingDirection + direction; ///< Whether the azimuth is increasing or decreasing. + + static AzimuthCrossingEvent + from_c(const siderust_azimuth_crossing_event_t &c) { + return {MJD(c.mjd), static_cast(c.direction)}; + } }; /** * @brief An azimuth extremum event. */ struct AzimuthExtremum { - MJD time; ///< Epoch of the extremum (MJD). - qtty::Degree azimuth; ///< Azimuth at the extremum (degrees, N-clockwise). - AzimuthExtremumKind kind; ///< Maximum or minimum. - - static AzimuthExtremum from_c(const siderust_azimuth_extremum_t& c) { - return {MJD(c.mjd), qtty::Degree(c.azimuth_deg), - static_cast(c.kind)}; - } + MJD time; ///< Epoch of the extremum (MJD). + qtty::Degree azimuth; ///< Azimuth at the extremum (degrees, N-clockwise). + AzimuthExtremumKind kind; ///< Maximum or minimum. + + static AzimuthExtremum from_c(const siderust_azimuth_extremum_t &c) { + return {MJD(c.mjd), qtty::Degree(c.azimuth_deg), + static_cast(c.kind)}; + } }; // ============================================================================ @@ -69,26 +72,26 @@ struct AzimuthExtremum { // ============================================================================ namespace detail { -inline std::vector az_crossings_from_c( - siderust_azimuth_crossing_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(AzimuthCrossingEvent::from_c(ptr[i])); - } - siderust_azimuth_crossings_free(ptr, count); - return result; +inline std::vector +az_crossings_from_c(siderust_azimuth_crossing_event_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthCrossingEvent::from_c(ptr[i])); + } + siderust_azimuth_crossings_free(ptr, count); + return result; } -inline std::vector az_extrema_from_c( - siderust_azimuth_extremum_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(AzimuthExtremum::from_c(ptr[i])); - } - siderust_azimuth_extrema_free(ptr, count); - return result; +inline std::vector +az_extrema_from_c(siderust_azimuth_extremum_t *ptr, uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(AzimuthExtremum::from_c(ptr[i])); + } + siderust_azimuth_extrema_free(ptr, count); + return result; } } // namespace detail @@ -100,88 +103,91 @@ inline std::vector az_extrema_from_c( namespace sun { /** - * @brief Compute the Sun's azimuth (degrees, N-clockwise) at a given MJD instant. + * @brief Compute the Sun's azimuth (degrees, N-clockwise) at a given MJD + * instant. */ -inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_sun_azimuth_at(obs.to_c(), mjd.value(), &out), - "sun::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_sun_azimuth_at(obs.to_c(), mjd.value(), &out), + "sun::azimuth_at"); + return qtty::Degree(out); } /** * @brief Find epochs when the Sun crosses a given bearing. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_azimuth_crossings( - obs.to_c(), window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "sun::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_crossings(obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), + &ptr, &count), + "sun::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const Geodetic &obs, const MJD &start, const MJD &end, + qtty::Degree bearing, const SearchOptions &opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); } /** * @brief Find azimuth extrema (northernmost / southernmost) for the Sun. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_azimuth_extremum_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_azimuth_extrema( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "sun::azimuth_extrema"); - return detail::az_extrema_from_c(ptr, count); +inline std::vector +azimuth_extrema(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_azimuth_extrema(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "sun::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return azimuth_extrema(obs, Period(start, end), opts); +inline std::vector +azimuth_extrema(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); } /** - * @brief Find periods when the Sun's azimuth is within [min_bearing, max_bearing]. + * @brief Find periods when the Sun's azimuth is within [min_bearing, + * max_bearing]. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const Period& window, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_sun_in_azimuth_range( - obs.to_c(), window.c_inner(), - min_bearing.value(), max_bearing.value(), - opts.to_c(), &ptr, &count), - "sun::in_azimuth_range"); - return detail::periods_from_c(ptr, count); +inline std::vector in_azimuth_range(const Geodetic &obs, + const Period &window, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_sun_in_azimuth_range( + obs.to_c(), window.c_inner(), min_bearing.value(), + max_bearing.value(), opts.to_c(), &ptr, &count), + "sun::in_azimuth_range"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +inline std::vector in_azimuth_range(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, + opts); } } // namespace sun @@ -193,88 +199,91 @@ inline std::vector in_azimuth_range( namespace moon { /** - * @brief Compute the Moon's azimuth (degrees, N-clockwise) at a given MJD instant. + * @brief Compute the Moon's azimuth (degrees, N-clockwise) at a given MJD + * instant. */ -inline qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_moon_azimuth_at(obs.to_c(), mjd.value(), &out), - "moon::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_moon_azimuth_at(obs.to_c(), mjd.value(), &out), + "moon::azimuth_at"); + return qtty::Degree(out); } /** * @brief Find epochs when the Moon crosses a given bearing. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_azimuth_crossings( - obs.to_c(), window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "moon::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_crossings(obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), + &ptr, &count), + "moon::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const Geodetic &obs, const MJD &start, const MJD &end, + qtty::Degree bearing, const SearchOptions &opts = {}) { + return azimuth_crossings(obs, Period(start, end), bearing, opts); } /** * @brief Find azimuth extrema (northernmost / southernmost) for the Moon. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_azimuth_extremum_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_azimuth_extrema( - obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "moon::azimuth_extrema"); - return detail::az_extrema_from_c(ptr, count); +inline std::vector +azimuth_extrema(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_azimuth_extrema(obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "moon::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_extrema( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) { - return azimuth_extrema(obs, Period(start, end), opts); +inline std::vector +azimuth_extrema(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return azimuth_extrema(obs, Period(start, end), opts); } /** - * @brief Find periods when the Moon's azimuth is within [min_bearing, max_bearing]. + * @brief Find periods when the Moon's azimuth is within [min_bearing, + * max_bearing]. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const Period& window, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_in_azimuth_range( - obs.to_c(), window.c_inner(), - min_bearing.value(), max_bearing.value(), - opts.to_c(), &ptr, &count), - "moon::in_azimuth_range"); - return detail::periods_from_c(ptr, count); +inline std::vector in_azimuth_range(const Geodetic &obs, + const Period &window, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_in_azimuth_range( + obs.to_c(), window.c_inner(), min_bearing.value(), + max_bearing.value(), opts.to_c(), &ptr, &count), + "moon::in_azimuth_range"); + return detail::periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector in_azimuth_range( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree min_bearing, qtty::Degree max_bearing, - const SearchOptions& opts = {}) { - return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, opts); +inline std::vector in_azimuth_range(const Geodetic &obs, + const MJD &start, const MJD &end, + qtty::Degree min_bearing, + qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + return in_azimuth_range(obs, Period(start, end), min_bearing, max_bearing, + opts); } } // namespace moon @@ -286,38 +295,41 @@ inline std::vector in_azimuth_range( namespace star_altitude { /** - * @brief Compute a star's azimuth (degrees, N-clockwise) at a given MJD instant. + * @brief Compute a star's azimuth (degrees, N-clockwise) at a given MJD + * instant. */ -inline qtty::Degree azimuth_at(const Star& s, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_star_azimuth_at( - s.c_handle(), obs.to_c(), mjd.value(), &out), - "star_altitude::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const Star &s, const Geodetic &obs, + const MJD &mjd) { + double out; + check_status( + siderust_star_azimuth_at(s.c_handle(), obs.to_c(), mjd.value(), &out), + "star_altitude::azimuth_at"); + return qtty::Degree(out); } /** * @brief Find epochs when a star crosses a given azimuth bearing. */ -inline std::vector azimuth_crossings( - const Star& s, const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_star_azimuth_crossings( - s.c_handle(), obs.to_c(), window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "star_altitude::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const Star &s, const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_azimuth_crossings( + s.c_handle(), obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "star_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const Star& s, const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(s, obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const Star &s, const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree bearing, + const SearchOptions &opts = {}) { + return azimuth_crossings(s, obs, Period(start, end), bearing, opts); } } // namespace star_altitude @@ -331,58 +343,58 @@ namespace icrs_altitude { /** * @brief Compute azimuth (degrees, N-clockwise) for a fixed ICRS direction. */ -inline qtty::Degree azimuth_at(const spherical::direction::ICRS& dir, - const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_icrs_azimuth_at( - dir.to_c(), - obs.to_c(), mjd.value(), &out), - "icrs_altitude::azimuth_at"); - return qtty::Degree(out); +inline qtty::Degree azimuth_at(const spherical::direction::ICRS &dir, + const Geodetic &obs, const MJD &mjd) { + double out; + check_status( + siderust_icrs_azimuth_at(dir.to_c(), obs.to_c(), mjd.value(), &out), + "icrs_altitude::azimuth_at"); + return qtty::Degree(out); } /** * @brief Backward-compatible RA/Dec overload. */ inline qtty::Degree azimuth_at(qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const MJD& mjd) { - return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); + const Geodetic &obs, const MJD &mjd) { + return azimuth_at(spherical::direction::ICRS(ra, dec), obs, mjd); } /** * @brief Find epochs when an ICRS direction crosses a given azimuth bearing. */ -inline std::vector azimuth_crossings( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_azimuth_crossings( - dir.to_c(), obs.to_c(), window.c_inner(), - bearing.value(), opts.to_c(), &ptr, &count), - "icrs_altitude::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(const spherical::direction::ICRS &dir, const Geodetic &obs, + const Period &window, qtty::Degree bearing, + const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_azimuth_crossings( + dir.to_c(), obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "icrs_altitude::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Backward-compatible RA/Dec overload. */ -inline std::vector azimuth_crossings( - qtty::Degree ra, qtty::Degree dec, - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(spherical::direction::ICRS(ra, dec), obs, window, bearing, opts); +inline std::vector +azimuth_crossings(qtty::Degree ra, qtty::Degree dec, const Geodetic &obs, + const Period &window, qtty::Degree bearing, + const SearchOptions &opts = {}) { + return azimuth_crossings(spherical::direction::ICRS(ra, dec), obs, window, + bearing, opts); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector azimuth_crossings( - const spherical::direction::ICRS& dir, - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) { - return azimuth_crossings(dir, obs, Period(start, end), bearing, opts); +inline std::vector +azimuth_crossings(const spherical::direction::ICRS &dir, const Geodetic &obs, + const MJD &start, const MJD &end, qtty::Degree bearing, + const SearchOptions &opts = {}) { + return azimuth_crossings(dir, obs, Period(start, end), bearing, opts); } } // namespace icrs_altitude @@ -394,14 +406,14 @@ inline std::vector azimuth_crossings( /** * @brief Stream operator for AzimuthExtremumKind. */ -inline std::ostream& operator<<(std::ostream& os, AzimuthExtremumKind kind) { - switch (kind) { - case AzimuthExtremumKind::Max: - return os << "max"; - case AzimuthExtremumKind::Min: - return os << "min"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, AzimuthExtremumKind kind) { + switch (kind) { + case AzimuthExtremumKind::Max: + return os << "max"; + case AzimuthExtremumKind::Min: + return os << "min"; + } + return os << "unknown"; } } // namespace siderust diff --git a/include/siderust/bodies.hpp b/include/siderust/bodies.hpp index cbc3247..f6f2fa3 100644 --- a/include/siderust/bodies.hpp +++ b/include/siderust/bodies.hpp @@ -21,18 +21,18 @@ namespace siderust { * @brief Proper motion for a star (equatorial). */ struct ProperMotion { - double pm_ra_deg_yr; ///< RA proper motion (deg/yr). - double pm_dec_deg_yr; ///< Dec proper motion (deg/yr). - RaConvention convention; ///< RA rate convention. - - ProperMotion(double ra, double dec, - RaConvention conv = RaConvention::MuAlphaStar) - : pm_ra_deg_yr(ra), pm_dec_deg_yr(dec), convention(conv) {} - - siderust_proper_motion_t to_c() const { - return {pm_ra_deg_yr, pm_dec_deg_yr, - static_cast(convention)}; - } + double pm_ra_deg_yr; ///< RA proper motion (deg/yr). + double pm_dec_deg_yr; ///< Dec proper motion (deg/yr). + RaConvention convention; ///< RA rate convention. + + ProperMotion(double ra, double dec, + RaConvention conv = RaConvention::MuAlphaStar) + : pm_ra_deg_yr(ra), pm_dec_deg_yr(dec), convention(conv) {} + + siderust_proper_motion_t to_c() const { + return {pm_ra_deg_yr, pm_dec_deg_yr, + static_cast(convention)}; + } }; // ============================================================================ @@ -43,19 +43,23 @@ struct ProperMotion { * @brief Keplerian orbital elements. */ struct Orbit { - double semi_major_axis_au; - double eccentricity; - double inclination_deg; - double lon_ascending_node_deg; - double arg_perihelion_deg; - double mean_anomaly_deg; - double epoch_jd; - - static Orbit from_c(const siderust_orbit_t& c) { - return {c.semi_major_axis_au, c.eccentricity, c.inclination_deg, - c.lon_ascending_node_deg, c.arg_perihelion_deg, - c.mean_anomaly_deg, c.epoch_jd}; - } + double semi_major_axis_au; + double eccentricity; + double inclination_deg; + double lon_ascending_node_deg; + double arg_perihelion_deg; + double mean_anomaly_deg; + double epoch_jd; + + static Orbit from_c(const siderust_orbit_t &c) { + return {c.semi_major_axis_au, + c.eccentricity, + c.inclination_deg, + c.lon_ascending_node_deg, + c.arg_perihelion_deg, + c.mean_anomaly_deg, + c.epoch_jd}; + } }; // ============================================================================ @@ -66,74 +70,74 @@ struct Orbit { * @brief Planet data (value type, copyable). */ struct Planet { - double mass_kg; - double radius_km; - Orbit orbit; + double mass_kg; + double radius_km; + Orbit orbit; - static Planet from_c(const siderust_planet_t& c) { - return {c.mass_kg, c.radius_km, Orbit::from_c(c.orbit)}; - } + static Planet from_c(const siderust_planet_t &c) { + return {c.mass_kg, c.radius_km, Orbit::from_c(c.orbit)}; + } }; namespace detail { inline Planet make_planet_mercury() { - siderust_planet_t out; - check_status(siderust_planet_mercury(&out), "MERCURY"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_mercury(&out), "MERCURY"); + return Planet::from_c(out); } inline Planet make_planet_venus() { - siderust_planet_t out; - check_status(siderust_planet_venus(&out), "VENUS"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_venus(&out), "VENUS"); + return Planet::from_c(out); } inline Planet make_planet_earth() { - siderust_planet_t out; - check_status(siderust_planet_earth(&out), "EARTH"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_earth(&out), "EARTH"); + return Planet::from_c(out); } inline Planet make_planet_mars() { - siderust_planet_t out; - check_status(siderust_planet_mars(&out), "MARS"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_mars(&out), "MARS"); + return Planet::from_c(out); } inline Planet make_planet_jupiter() { - siderust_planet_t out; - check_status(siderust_planet_jupiter(&out), "JUPITER"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_jupiter(&out), "JUPITER"); + return Planet::from_c(out); } inline Planet make_planet_saturn() { - siderust_planet_t out; - check_status(siderust_planet_saturn(&out), "SATURN"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_saturn(&out), "SATURN"); + return Planet::from_c(out); } inline Planet make_planet_uranus() { - siderust_planet_t out; - check_status(siderust_planet_uranus(&out), "URANUS"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_uranus(&out), "URANUS"); + return Planet::from_c(out); } inline Planet make_planet_neptune() { - siderust_planet_t out; - check_status(siderust_planet_neptune(&out), "NEPTUNE"); - return Planet::from_c(out); + siderust_planet_t out; + check_status(siderust_planet_neptune(&out), "NEPTUNE"); + return Planet::from_c(out); } } // namespace detail inline const Planet MERCURY = detail::make_planet_mercury(); -inline const Planet VENUS = detail::make_planet_venus(); -inline const Planet EARTH = detail::make_planet_earth(); -inline const Planet MARS = detail::make_planet_mars(); +inline const Planet VENUS = detail::make_planet_venus(); +inline const Planet EARTH = detail::make_planet_earth(); +inline const Planet MARS = detail::make_planet_mars(); inline const Planet JUPITER = detail::make_planet_jupiter(); -inline const Planet SATURN = detail::make_planet_saturn(); -inline const Planet URANUS = detail::make_planet_uranus(); +inline const Planet SATURN = detail::make_planet_saturn(); +inline const Planet URANUS = detail::make_planet_uranus(); inline const Planet NEPTUNE = detail::make_planet_neptune(); // Backward-compatible function aliases. @@ -156,113 +160,110 @@ inline Planet neptune() { return NEPTUNE; } * Non-copyable; move-only. Released on destruction. */ class Star { - SiderustStar* m_handle = nullptr; - - explicit Star(SiderustStar* h) : m_handle(h) {} - - public: - Star() = default; - ~Star() { - if (m_handle) - siderust_star_free(m_handle); + SiderustStar *m_handle = nullptr; + + explicit Star(SiderustStar *h) : m_handle(h) {} + +public: + Star() = default; + ~Star() { + if (m_handle) + siderust_star_free(m_handle); + } + + // Move-only + Star(Star &&o) noexcept : m_handle(o.m_handle) { o.m_handle = nullptr; } + Star &operator=(Star &&o) noexcept { + if (this != &o) { + if (m_handle) + siderust_star_free(m_handle); + m_handle = o.m_handle; + o.m_handle = nullptr; } - - // Move-only - Star(Star&& o) noexcept : m_handle(o.m_handle) { o.m_handle = nullptr; } - Star& operator=(Star&& o) noexcept { - if (this != &o) { - if (m_handle) - siderust_star_free(m_handle); - m_handle = o.m_handle; - o.m_handle = nullptr; - } - return *this; + return *this; + } + Star(const Star &) = delete; + Star &operator=(const Star &) = delete; + + /// Whether the handle is valid. + explicit operator bool() const { return m_handle != nullptr; } + + /// Access the raw C handle (for passing to altitude functions). + const SiderustStar *c_handle() const { return m_handle; } + + // -- Factory methods -- + + /** + * @brief Look up a star from the built-in catalog. + * + * Supported: "VEGA", "SIRIUS", "POLARIS", "CANOPUS", "ARCTURUS", + * "RIGEL", "BETELGEUSE", "PROCYON", "ALDEBARAN", "ALTAIR". + */ + static Star catalog(const std::string &name) { + SiderustStar *h = nullptr; + check_status(siderust_star_catalog(name.c_str(), &h), "Star::catalog"); + return Star(h); + } + + /** + * @brief Create a custom star. + * + * @param name Star name. + * @param distance_ly Distance in light-years. + * @param mass_solar Mass in solar masses. + * @param radius_solar Radius in solar radii. + * @param luminosity_solar Luminosity in solar luminosities. + * @param ra_deg Right ascension (J2000) in degrees. + * @param dec_deg Declination (J2000) in degrees. + * @param epoch_jd Epoch of coordinates (Julian Date). + * @param pm Optional proper motion. + */ + static Star create(const std::string &name, double distance_ly, + double mass_solar, double radius_solar, + double luminosity_solar, double ra_deg, double dec_deg, + double epoch_jd, + const std::optional &pm = std::nullopt) { + SiderustStar *h = nullptr; + const siderust_proper_motion_t *pm_ptr = nullptr; + siderust_proper_motion_t pm_c{}; + if (pm.has_value()) { + pm_c = pm->to_c(); + pm_ptr = &pm_c; } - Star(const Star&) = delete; - Star& operator=(const Star&) = delete; - - /// Whether the handle is valid. - explicit operator bool() const { return m_handle != nullptr; } - - /// Access the raw C handle (for passing to altitude functions). - const SiderustStar* c_handle() const { return m_handle; } - - // -- Factory methods -- - - /** - * @brief Look up a star from the built-in catalog. - * - * Supported: "VEGA", "SIRIUS", "POLARIS", "CANOPUS", "ARCTURUS", - * "RIGEL", "BETELGEUSE", "PROCYON", "ALDEBARAN", "ALTAIR". - */ - static Star catalog(const std::string& name) { - SiderustStar* h = nullptr; - check_status(siderust_star_catalog(name.c_str(), &h), - "Star::catalog"); - return Star(h); - } - - /** - * @brief Create a custom star. - * - * @param name Star name. - * @param distance_ly Distance in light-years. - * @param mass_solar Mass in solar masses. - * @param radius_solar Radius in solar radii. - * @param luminosity_solar Luminosity in solar luminosities. - * @param ra_deg Right ascension (J2000) in degrees. - * @param dec_deg Declination (J2000) in degrees. - * @param epoch_jd Epoch of coordinates (Julian Date). - * @param pm Optional proper motion. - */ - static Star create(const std::string& name, - double distance_ly, - double mass_solar, - double radius_solar, - double luminosity_solar, - double ra_deg, - double dec_deg, - double epoch_jd, - const std::optional& pm = std::nullopt) { - SiderustStar* h = nullptr; - const siderust_proper_motion_t* pm_ptr = nullptr; - siderust_proper_motion_t pm_c{}; - if (pm.has_value()) { - pm_c = pm->to_c(); - pm_ptr = &pm_c; - } - check_status(siderust_star_create( - name.c_str(), distance_ly, mass_solar, radius_solar, - luminosity_solar, ra_deg, dec_deg, epoch_jd, pm_ptr, &h), - "Star::create"); - return Star(h); - } - - // -- Accessors -- - - std::string name() const { - char buf[256]; - uintptr_t written = 0; - check_status(siderust_star_name(m_handle, buf, sizeof(buf), &written), - "Star::name"); - return std::string(buf, written); - } - - double distance_ly() const { return siderust_star_distance_ly(m_handle); } - double mass_solar() const { return siderust_star_mass_solar(m_handle); } - double radius_solar() const { return siderust_star_radius_solar(m_handle); } - double luminosity_solar() const { return siderust_star_luminosity_solar(m_handle); } + check_status(siderust_star_create(name.c_str(), distance_ly, mass_solar, + radius_solar, luminosity_solar, ra_deg, + dec_deg, epoch_jd, pm_ptr, &h), + "Star::create"); + return Star(h); + } + + // -- Accessors -- + + std::string name() const { + char buf[256]; + uintptr_t written = 0; + check_status(siderust_star_name(m_handle, buf, sizeof(buf), &written), + "Star::name"); + return std::string(buf, written); + } + + double distance_ly() const { return siderust_star_distance_ly(m_handle); } + double mass_solar() const { return siderust_star_mass_solar(m_handle); } + double radius_solar() const { return siderust_star_radius_solar(m_handle); } + double luminosity_solar() const { + return siderust_star_luminosity_solar(m_handle); + } }; -inline const Star VEGA = Star::catalog("VEGA"); -inline const Star SIRIUS = Star::catalog("SIRIUS"); -inline const Star POLARIS = Star::catalog("POLARIS"); -inline const Star CANOPUS = Star::catalog("CANOPUS"); -inline const Star ARCTURUS = Star::catalog("ARCTURUS"); -inline const Star RIGEL = Star::catalog("RIGEL"); +inline const Star VEGA = Star::catalog("VEGA"); +inline const Star SIRIUS = Star::catalog("SIRIUS"); +inline const Star POLARIS = Star::catalog("POLARIS"); +inline const Star CANOPUS = Star::catalog("CANOPUS"); +inline const Star ARCTURUS = Star::catalog("ARCTURUS"); +inline const Star RIGEL = Star::catalog("RIGEL"); inline const Star BETELGEUSE = Star::catalog("BETELGEUSE"); -inline const Star PROCYON = Star::catalog("PROCYON"); -inline const Star ALDEBARAN = Star::catalog("ALDEBARAN"); -inline const Star ALTAIR = Star::catalog("ALTAIR"); +inline const Star PROCYON = Star::catalog("PROCYON"); +inline const Star ALDEBARAN = Star::catalog("ALDEBARAN"); +inline const Star ALTAIR = Star::catalog("ALTAIR"); } // namespace siderust diff --git a/include/siderust/body_target.hpp b/include/siderust/body_target.hpp index cc39e2a..855bafd 100644 --- a/include/siderust/body_target.hpp +++ b/include/siderust/body_target.hpp @@ -43,15 +43,15 @@ namespace siderust { * Maps 1:1 to the FFI `SiderustBody` discriminant. */ enum class Body : int32_t { - Sun = SIDERUST_BODY_SUN, - Moon = SIDERUST_BODY_MOON, - Mercury = SIDERUST_BODY_MERCURY, - Venus = SIDERUST_BODY_VENUS, - Mars = SIDERUST_BODY_MARS, - Jupiter = SIDERUST_BODY_JUPITER, - Saturn = SIDERUST_BODY_SATURN, - Uranus = SIDERUST_BODY_URANUS, - Neptune = SIDERUST_BODY_NEPTUNE, + Sun = SIDERUST_BODY_SUN, + Moon = SIDERUST_BODY_MOON, + Mercury = SIDERUST_BODY_MERCURY, + Venus = SIDERUST_BODY_VENUS, + Mars = SIDERUST_BODY_MARS, + Jupiter = SIDERUST_BODY_JUPITER, + Saturn = SIDERUST_BODY_SATURN, + Uranus = SIDERUST_BODY_URANUS, + Neptune = SIDERUST_BODY_NEPTUNE, }; // ============================================================================ @@ -63,92 +63,93 @@ namespace body { /** * @brief Compute a body's altitude (radians) at a given MJD instant. */ -inline qtty::Radian altitude_at(Body b, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_body_altitude_at( - static_cast(b), obs.to_c(), mjd.value(), &out), - "body::altitude_at"); - return qtty::Radian(out); +inline qtty::Radian altitude_at(Body b, const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_body_altitude_at(static_cast(b), + obs.to_c(), mjd.value(), &out), + "body::altitude_at"); + return qtty::Radian(out); } /** * @brief Find periods when a body is above a threshold altitude. */ -inline std::vector above_threshold( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_above_threshold( - static_cast(b), obs.to_c(), - window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "body::above_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector above_threshold(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_above_threshold( + static_cast(b), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "body::above_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Find periods when a body is below a threshold altitude. */ -inline std::vector below_threshold( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_below_threshold( - static_cast(b), obs.to_c(), - window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "body::below_threshold"); - return detail::periods_from_c(ptr, count); +inline std::vector below_threshold(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_below_threshold( + static_cast(b), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "body::below_threshold"); + return detail::periods_from_c(ptr, count); } /** * @brief Find threshold-crossing events for a body. */ -inline std::vector crossings( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_crossings( - static_cast(b), obs.to_c(), - window.c_inner(), threshold.value(), - opts.to_c(), &ptr, &count), - "body::crossings"); - return detail::crossings_from_c(ptr, count); +inline std::vector crossings(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_crossings(static_cast(b), obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "body::crossings"); + return detail::crossings_from_c(ptr, count); } /** * @brief Find culmination events for a body. */ -inline std::vector culminations( - Body b, const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_culminations( - static_cast(b), obs.to_c(), - window.c_inner(), - opts.to_c(), &ptr, &count), - "body::culminations"); - return detail::culminations_from_c(ptr, count); +inline std::vector +culminations(Body b, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_culminations(static_cast(b), + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "body::culminations"); + return detail::culminations_from_c(ptr, count); } /** * @brief Find periods when a body's altitude is within [min, max]. */ -inline std::vector altitude_periods( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree min_alt, qtty::Degree max_alt) { - siderust_altitude_query_t q = {obs.to_c(), window.start().value(), window.end().value(), - min_alt.value(), max_alt.value()}; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_altitude_periods( - static_cast(b), q, &ptr, &count), - "body::altitude_periods"); - return detail::periods_from_c(ptr, count); +inline std::vector altitude_periods(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_altitude_periods(static_cast(b), q, + &ptr, &count), + "body::altitude_periods"); + return detail::periods_from_c(ptr, count); } } // namespace body @@ -160,60 +161,58 @@ namespace body { /** * @brief Compute a body's azimuth (radians) at a given MJD instant. */ -inline qtty::Radian azimuth_at(Body b, const Geodetic& obs, const MJD& mjd) { - double out; - check_status(siderust_body_azimuth_at( - static_cast(b), obs.to_c(), mjd.value(), &out), - "body::azimuth_at"); - return qtty::Radian(out); +inline qtty::Radian azimuth_at(Body b, const Geodetic &obs, const MJD &mjd) { + double out; + check_status(siderust_body_azimuth_at(static_cast(b), + obs.to_c(), mjd.value(), &out), + "body::azimuth_at"); + return qtty::Radian(out); } /** * @brief Find azimuth-bearing crossing events for a body. */ -inline std::vector azimuth_crossings( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_azimuth_crossings( - static_cast(b), obs.to_c(), - window.c_inner(), bearing.value(), - opts.to_c(), &ptr, &count), - "body::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); +inline std::vector +azimuth_crossings(Body b, const Geodetic &obs, const Period &window, + qtty::Degree bearing, const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_crossings( + static_cast(b), obs.to_c(), window.c_inner(), + bearing.value(), opts.to_c(), &ptr, &count), + "body::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); } /** * @brief Find azimuth extrema (northernmost/southernmost bearing) for a body. */ -inline std::vector azimuth_extrema( - Body b, const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) { - siderust_azimuth_extremum_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_azimuth_extrema( - static_cast(b), obs.to_c(), - window.c_inner(), - opts.to_c(), &ptr, &count), - "body::azimuth_extrema"); - return detail::az_extrema_from_c(ptr, count); +inline std::vector +azimuth_extrema(Body b, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_azimuth_extrema(static_cast(b), + obs.to_c(), window.c_inner(), + opts.to_c(), &ptr, &count), + "body::azimuth_extrema"); + return detail::az_extrema_from_c(ptr, count); } /** * @brief Find periods when a body's azimuth is within [min, max]. */ -inline std::vector in_azimuth_range( - Body b, const Geodetic& obs, const Period& window, - qtty::Degree min, qtty::Degree max, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_body_in_azimuth_range( - static_cast(b), obs.to_c(), - window.c_inner(), min.value(), max.value(), - opts.to_c(), &ptr, &count), - "body::in_azimuth_range"); - return detail::periods_from_c(ptr, count); +inline std::vector in_azimuth_range(Body b, const Geodetic &obs, + const Period &window, + qtty::Degree min, qtty::Degree max, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_body_in_azimuth_range( + static_cast(b), obs.to_c(), window.c_inner(), + min.value(), max.value(), opts.to_c(), &ptr, &count), + "body::in_azimuth_range"); + return detail::periods_from_c(ptr, count); } } // namespace body @@ -233,66 +232,69 @@ inline std::vector in_azimuth_range( * polymorphic dispatch. */ class BodyTarget : public Trackable { - public: - /** - * @brief Construct a BodyTarget for a given solar-system body. - * @param body The body to track. - */ - explicit BodyTarget(Body body) : body_(body) {} - - // ------------------------------------------------------------------ - // Altitude queries - // ------------------------------------------------------------------ - - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { - auto rad = body::altitude_at(body_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); - } - - std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return body::above_threshold(body_, obs, window, threshold, opts); - } - - std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return body::below_threshold(body_, obs, window, threshold, opts); - } - - std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return body::crossings(body_, obs, window, threshold, opts); - } - - std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const override { - return body::culminations(body_, obs, window, opts); - } - - // ------------------------------------------------------------------ - // Azimuth queries - // ------------------------------------------------------------------ - - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { - auto rad = body::azimuth_at(body_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); - } - - std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const override { - return body::azimuth_crossings(body_, obs, window, bearing, opts); - } - - /// Access the underlying Body enum value. - Body body() const { return body_; } - - private: - Body body_; +public: + /** + * @brief Construct a BodyTarget for a given solar-system body. + * @param body The body to track. + */ + explicit BodyTarget(Body body) : body_(body) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { + auto rad = body::altitude_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return body::above_threshold(body_, obs, window, threshold, opts); + } + + std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return body::below_threshold(body_, obs, window, threshold, opts); + } + + std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return body::crossings(body_, obs, window, threshold, opts); + } + + std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const override { + return body::culminations(body_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) const override { + auto rad = body::azimuth_at(body_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const override { + return body::azimuth_crossings(body_, obs, window, bearing, opts); + } + + /// Access the underlying Body enum value. + Body body() const { return body_; } + +private: + Body body_; }; } // namespace siderust diff --git a/include/siderust/centers.hpp b/include/siderust/centers.hpp index 0a67de4..6c0ada0 100644 --- a/include/siderust/centers.hpp +++ b/include/siderust/centers.hpp @@ -23,17 +23,15 @@ namespace centers { // Center Trait // ============================================================================ -template -struct CenterTraits; // primary — intentionally undefined +template struct CenterTraits; // primary — intentionally undefined -template -struct is_center : std::false_type {}; +template struct is_center : std::false_type {}; template -struct is_center::ffi_id)>> : std::true_type {}; +struct is_center::ffi_id)>> + : std::true_type {}; -template -inline constexpr bool is_center_v = is_center::value; +template inline constexpr bool is_center_v = is_center::value; // ============================================================================ // Center Tag Definitions @@ -57,39 +55,34 @@ struct Bodycentric {}; /// Marker for simple (no-parameter) centers. struct NoParams {}; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BARYCENTRIC; - using Params = NoParams; - static constexpr const char* name() { return "Barycentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BARYCENTRIC; + using Params = NoParams; + static constexpr const char *name() { return "Barycentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_HELIOCENTRIC; - using Params = NoParams; - static constexpr const char* name() { return "Heliocentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_HELIOCENTRIC; + using Params = NoParams; + static constexpr const char *name() { return "Heliocentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_GEOCENTRIC; - using Params = NoParams; - static constexpr const char* name() { return "Geocentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_GEOCENTRIC; + using Params = NoParams; + static constexpr const char *name() { return "Geocentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_TOPOCENTRIC; - using Params = Geodetic; // forward-declared - static constexpr const char* name() { return "Topocentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_TOPOCENTRIC; + using Params = Geodetic; // forward-declared + static constexpr const char *name() { return "Topocentric"; } }; -template <> -struct CenterTraits { - static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BODYCENTRIC; - using Params = NoParams; // placeholder for BodycentricParams - static constexpr const char* name() { return "Bodycentric"; } +template <> struct CenterTraits { + static constexpr siderust_center_t ffi_id = SIDERUST_CENTER_T_BODYCENTRIC; + using Params = NoParams; // placeholder for BodycentricParams + static constexpr const char *name() { return "Bodycentric"; } }; // ============================================================================ @@ -105,14 +98,11 @@ struct CenterTraits { template struct has_center_transform : std::false_type {}; -template -struct has_center_transform : std::true_type {}; +template struct has_center_transform : std::true_type {}; -#define SIDERUST_CENTER_TRANSFORM_PAIR(A, B) \ - template <> \ - struct has_center_transform : std::true_type {}; \ - template <> \ - struct has_center_transform : std::true_type {} +#define SIDERUST_CENTER_TRANSFORM_PAIR(A, B) \ + template <> struct has_center_transform : std::true_type {}; \ + template <> struct has_center_transform : std::true_type {} SIDERUST_CENTER_TRANSFORM_PAIR(Barycentric, Heliocentric); SIDERUST_CENTER_TRANSFORM_PAIR(Barycentric, Geocentric); @@ -121,7 +111,8 @@ SIDERUST_CENTER_TRANSFORM_PAIR(Heliocentric, Geocentric); #undef SIDERUST_CENTER_TRANSFORM_PAIR template -inline constexpr bool has_center_transform_v = has_center_transform::value; +inline constexpr bool has_center_transform_v = + has_center_transform::value; } // namespace centers } // namespace siderust diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 13adad1..2a681b2 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -25,20 +25,19 @@ namespace cartesian { * @ingroup coordinates_cartesian * @tparam F Reference frame tag (e.g. `frames::ICRS`). */ -template -struct Direction { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); +template struct Direction { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); - double x; ///< X component (unitless). - double y; ///< Y component (unitless). - double z; ///< Z component (unitless). + double x; ///< X component (unitless). + double y; ///< Y component (unitless). + double z; ///< Z component (unitless). - Direction() : x(0), y(0), z(0) {} - Direction(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {} + Direction() : x(0), y(0), z(0) {} + Direction(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {} - static constexpr siderust_frame_t frame_id() { - return frames::FrameTraits::ffi_id; - } + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } }; /** @@ -51,41 +50,42 @@ struct Direction { * @tparam F Reference frame tag (e.g. `frames::ECEF`). * @tparam U Length unit (default: `qtty::Meter`). */ -template -struct Position { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); - static_assert(centers::is_center_v, "C must be a valid center tag"); - - U comp_x; ///< X component. - U comp_y; ///< Y component. - U comp_z; ///< Z component. - - Position() - : comp_x(U(0)), comp_y(U(0)), comp_z(U(0)) {} - - Position(U x_, U y_, U z_) - : comp_x(x_), comp_y(y_), comp_z(z_) {} - - Position(double x_, double y_, double z_) - : comp_x(U(x_)), comp_y(U(y_)), comp_z(U(z_)) {} - - U x() const { return comp_x; } - U y() const { return comp_y; } - U z() const { return comp_z; } - - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_id; } - - /// Convert to C FFI struct. - siderust_cartesian_pos_t to_c() const { - return {comp_x.value(), comp_y.value(), comp_z.value(), - frame_id(), center_id()}; - } - - /// Create from C FFI struct (ignoring runtime frame/center - trust the type). - static Position from_c(const siderust_cartesian_pos_t& c) { - return Position(c.x, c.y, c.z); - } +template struct Position { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + static_assert(centers::is_center_v, "C must be a valid center tag"); + + U comp_x; ///< X component. + U comp_y; ///< Y component. + U comp_z; ///< Z component. + + Position() : comp_x(U(0)), comp_y(U(0)), comp_z(U(0)) {} + + Position(U x_, U y_, U z_) : comp_x(x_), comp_y(y_), comp_z(z_) {} + + Position(double x_, double y_, double z_) + : comp_x(U(x_)), comp_y(U(y_)), comp_z(U(z_)) {} + + U x() const { return comp_x; } + U y() const { return comp_y; } + U z() const { return comp_z; } + + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } + static constexpr siderust_center_t center_id() { + return centers::CenterTraits::ffi_id; + } + + /// Convert to C FFI struct. + siderust_cartesian_pos_t to_c() const { + return {comp_x.value(), comp_y.value(), comp_z.value(), frame_id(), + center_id()}; + } + + /// Create from C FFI struct (ignoring runtime frame/center - trust the type). + static Position from_c(const siderust_cartesian_pos_t &c) { + return Position(c.x, c.y, c.z); + } }; // ============================================================================ @@ -96,8 +96,9 @@ struct Position { * @brief Stream operator for Position. */ template -inline std::ostream& operator<<(std::ostream& os, const Position& pos) { - return os << pos.x() << ", " << pos.y() << ", " << pos.z(); +inline std::ostream &operator<<(std::ostream &os, + const Position &pos) { + return os << pos.x() << ", " << pos.y() << ", " << pos.z(); } } // namespace cartesian diff --git a/include/siderust/coordinates/conversions.hpp b/include/siderust/coordinates/conversions.hpp index fe9b727..b3fd680 100644 --- a/include/siderust/coordinates/conversions.hpp +++ b/include/siderust/coordinates/conversions.hpp @@ -13,15 +13,13 @@ namespace siderust { template inline cartesian::Position Geodetic::to_cartesian() const { - siderust_cartesian_pos_t out; - check_status( - siderust_geodetic_to_cartesian_ecef(to_c(), &out), - "Geodetic::to_cartesian"); - const auto ecef_m = cartesian::position::ECEF::from_c(out); - return cartesian::Position( - ecef_m.x().template to(), - ecef_m.y().template to(), - ecef_m.z().template to()); + siderust_cartesian_pos_t out; + check_status(siderust_geodetic_to_cartesian_ecef(to_c(), &out), + "Geodetic::to_cartesian"); + const auto ecef_m = cartesian::position::ECEF::from_c(out); + return cartesian::Position( + ecef_m.x().template to(), ecef_m.y().template to(), + ecef_m.z().template to()); } /** @@ -29,8 +27,9 @@ Geodetic::to_cartesian() const { * * @ingroup coordinates_conversions */ -inline cartesian::position::ECEF geodetic_to_cartesian_ecef(const Geodetic& geo) { - return geo.to_cartesian(); +inline cartesian::position::ECEF +geodetic_to_cartesian_ecef(const Geodetic &geo) { + return geo.to_cartesian(); } } // namespace siderust diff --git a/include/siderust/coordinates/geodetic.hpp b/include/siderust/coordinates/geodetic.hpp index d244b13..32c5e69 100644 --- a/include/siderust/coordinates/geodetic.hpp +++ b/include/siderust/coordinates/geodetic.hpp @@ -16,8 +16,7 @@ namespace siderust { namespace cartesian { -template -struct Position; +template struct Position; } /** @@ -28,38 +27,39 @@ struct Position; * @ingroup coordinates_geodetic */ struct Geodetic { - qtty::Degree lon; ///< Longitude (east positive). - qtty::Degree lat; ///< Latitude (north positive). - qtty::Meter height; ///< Height above ellipsoid. + qtty::Degree lon; ///< Longitude (east positive). + qtty::Degree lat; ///< Latitude (north positive). + qtty::Meter height; ///< Height above ellipsoid. - Geodetic() - : lon(qtty::Degree(0)), lat(qtty::Degree(0)), height(qtty::Meter(0)) {} + Geodetic() + : lon(qtty::Degree(0)), lat(qtty::Degree(0)), height(qtty::Meter(0)) {} - Geodetic(qtty::Degree lon_, qtty::Degree lat_, qtty::Meter h = qtty::Meter(0)) - : lon(lon_), lat(lat_), height(h) {} + Geodetic(qtty::Degree lon_, qtty::Degree lat_, qtty::Meter h = qtty::Meter(0)) + : lon(lon_), lat(lat_), height(h) {} - /// Raw-double convenience constructor (degrees, metres). - Geodetic(double lon_deg, double lat_deg, double height_m = 0.0) - : lon(qtty::Degree(lon_deg)), lat(qtty::Degree(lat_deg)), - height(qtty::Meter(height_m)) {} + /// Raw-double convenience constructor (degrees, metres). + Geodetic(double lon_deg, double lat_deg, double height_m = 0.0) + : lon(qtty::Degree(lon_deg)), lat(qtty::Degree(lat_deg)), + height(qtty::Meter(height_m)) {} - /// Convert to C FFI struct. - siderust_geodetic_t to_c() const { - return {lon.value(), lat.value(), height.value()}; - } + /// Convert to C FFI struct. + siderust_geodetic_t to_c() const { + return {lon.value(), lat.value(), height.value()}; + } - /// Create from C FFI struct. - static Geodetic from_c(const siderust_geodetic_t& c) { - return Geodetic(c.lon_deg, c.lat_deg, c.height_m); - } + /// Create from C FFI struct. + static Geodetic from_c(const siderust_geodetic_t &c) { + return Geodetic(c.lon_deg, c.lat_deg, c.height_m); + } - /** - * @brief Convert geodetic (WGS84/ECEF) to cartesian position. - * - * @tparam U Output length unit (default: meter). - */ - template - cartesian::Position to_cartesian() const; + /** + * @brief Convert geodetic (WGS84/ECEF) to cartesian position. + * + * @tparam U Output length unit (default: meter). + */ + template + cartesian::Position + to_cartesian() const; }; // ============================================================================ @@ -69,8 +69,8 @@ struct Geodetic { /** * @brief Stream operator for Geodetic. */ -inline std::ostream& operator<<(std::ostream& os, const Geodetic& geo) { - return os << "lon=" << geo.lon << " lat=" << geo.lat << " h=" << geo.height; +inline std::ostream &operator<<(std::ostream &os, const Geodetic &geo) { + return os << "lon=" << geo.lon << " lat=" << geo.lat << " h=" << geo.height; } } // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 56db30d..1053eb7 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -25,8 +25,9 @@ namespace spherical { * Mirrors Rust's `affn::spherical::Direction`. * * @ingroup coordinates_spherical - * @tparam F Reference frame chapter content removed. Restore the original from \texttt{archived\_worktree/tex/chapters/12-logging-audit.tex} if needed. -tag (e.g. `frames::ICRS`). + * @tparam F Reference frame chapter content removed. Restore the original from +\texttt{archived\_worktree/tex/chapters/12-logging-audit.tex} if needed. tag +(e.g. `frames::ICRS`). * * @par Accessors * Access values through frame-appropriate getters: @@ -34,134 +35,157 @@ tag (e.g. `frames::ICRS`). * - Horizontal frame: `az()`, `al()` / `alt()` * - Lon/lat frames: `lon()`, `lat()` */ -template -struct Direction { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); - - private: - qtty::Degree azimuth_; ///< Azimuthal component (RA/longitude/azimuth). - qtty::Degree polar_; ///< Polar component (Dec/latitude/altitude). - - public: - Direction() : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)) {} - - Direction(qtty::Degree azimuth, qtty::Degree polar) - : azimuth_(azimuth), polar_(polar) {} - - /// @name Frame info - /// @{ - static constexpr siderust_frame_t frame_id() { - return frames::FrameTraits::ffi_id; - } - static constexpr const char* frame_name() { - return frames::FrameTraits::name(); - } - /// @} - - /// @name RA / Dec (equatorial frames only) - /// @{ - template , int> = 0> - qtty::Degree ra() const { return azimuth_; } - - template , int> = 0> - qtty::Degree dec() const { return polar_; } - /// @} - - /// @name Azimuth / Altitude (Horizontal frame only) - /// @{ - template , int> = 0> - qtty::Degree az() const { return azimuth_; } - - template , int> = 0> - qtty::Degree al() const { return polar_; } - - template , int> = 0> - qtty::Degree alt() const { return polar_; } - - template , int> = 0> - qtty::Degree altitude() const { return polar_; } - /// @} - - /// @name Longitude / Latitude (lon/lat frames) - /// @{ - template , int> = 0> - qtty::Degree lon() const { return azimuth_; } - - template , int> = 0> - qtty::Degree lat() const { return polar_; } - - template , int> = 0> - qtty::Degree longitude() const { return azimuth_; } - - template , int> = 0> - qtty::Degree latitude() const { return polar_; } - /// @} - - /// @name FFI interop - /// @{ - siderust_spherical_dir_t to_c() const { - return {polar_.value(), azimuth_.value(), frame_id()}; - } - - static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(qtty::Degree(c.azimuth_deg), qtty::Degree(c.polar_deg)); - } - /// @} - - /** - * @brief Transform to a different reference frame. - * - * Only enabled for frame pairs with a FrameRotationProvider in the FFI. - * Attempting an unsupported transform is a compile-time error. - * - * @tparam Target Destination frame tag. - */ - template - std::enable_if_t< - frames::has_frame_transform_v, - Direction> - to_frame(const JulianDate& jd) const { - if constexpr (std::is_same_v) { - return Direction(azimuth_, polar_); - } else { - siderust_spherical_dir_t out; - check_status( - siderust_spherical_dir_transform_frame( - polar_.value(), azimuth_.value(), - frames::FrameTraits::ffi_id, - frames::FrameTraits::ffi_id, - jd.value(), &out), - "Direction::to_frame"); - return Direction::from_c(out); - } - } - - /** - * @brief Shorthand: `.to(jd)` (calls `to_frame`). - */ - template - auto to(const JulianDate& jd) const - -> decltype(this->template to_frame(jd)) { - return to_frame(jd); - } - - /** - * @brief Transform to the horizontal (alt-az) frame. - */ - template - std::enable_if_t< - frames::has_horizontal_transform_v, - Direction> - to_horizontal(const JulianDate& jd, const Geodetic& observer) const { - siderust_spherical_dir_t out; - check_status( - siderust_spherical_dir_to_horizontal( - polar_.value(), azimuth_.value(), - frames::FrameTraits::ffi_id, - jd.value(), observer.to_c(), &out), - "Direction::to_horizontal"); - return Direction::from_c(out); +template struct Direction { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + +private: + qtty::Degree azimuth_; ///< Azimuthal component (RA/longitude/azimuth). + qtty::Degree polar_; ///< Polar component (Dec/latitude/altitude). + +public: + Direction() : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)) {} + + Direction(qtty::Degree azimuth, qtty::Degree polar) + : azimuth_(azimuth), polar_(polar) {} + + /// @name Frame info + /// @{ + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } + static constexpr const char *frame_name() { + return frames::FrameTraits::name(); + } + /// @} + + /// @name RA / Dec (equatorial frames only) + /// @{ + template , int> = 0> + qtty::Degree ra() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree dec() const { + return polar_; + } + /// @} + + /// @name Azimuth / Altitude (Horizontal frame only) + /// @{ + template , int> = 0> + qtty::Degree az() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree al() const { + return polar_; + } + + template , int> = 0> + qtty::Degree alt() const { + return polar_; + } + + template , int> = 0> + qtty::Degree altitude() const { + return polar_; + } + /// @} + + /// @name Longitude / Latitude (lon/lat frames) + /// @{ + template , int> = 0> + qtty::Degree lon() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree lat() const { + return polar_; + } + + template , int> = 0> + qtty::Degree longitude() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree latitude() const { + return polar_; + } + /// @} + + /// @name FFI interop + /// @{ + siderust_spherical_dir_t to_c() const { + return {polar_.value(), azimuth_.value(), frame_id()}; + } + + static Direction from_c(const siderust_spherical_dir_t &c) { + return Direction(qtty::Degree(c.azimuth_deg), qtty::Degree(c.polar_deg)); + } + /// @} + + /** + * @brief Transform to a different reference frame. + * + * Only enabled for frame pairs with a FrameRotationProvider in the FFI. + * Attempting an unsupported transform is a compile-time error. + * + * @tparam Target Destination frame tag. + */ + template + std::enable_if_t, Direction> + to_frame(const JulianDate &jd) const { + if constexpr (std::is_same_v) { + return Direction(azimuth_, polar_); + } else { + siderust_spherical_dir_t out; + check_status(siderust_spherical_dir_transform_frame( + polar_.value(), azimuth_.value(), + frames::FrameTraits::ffi_id, + frames::FrameTraits::ffi_id, jd.value(), &out), + "Direction::to_frame"); + return Direction::from_c(out); } + } + + /** + * @brief Shorthand: `.to(jd)` (calls `to_frame`). + */ + template + auto to(const JulianDate &jd) const + -> decltype(this->template to_frame(jd)) { + return to_frame(jd); + } + + /** + * @brief Transform to the horizontal (alt-az) frame. + */ + template + std::enable_if_t, + Direction> + to_horizontal(const JulianDate &jd, const Geodetic &observer) const { + siderust_spherical_dir_t out; + check_status( + siderust_spherical_dir_to_horizontal(polar_.value(), azimuth_.value(), + frames::FrameTraits::ffi_id, + jd.value(), observer.to_c(), &out), + "Direction::to_horizontal"); + return Direction::from_c(out); + } }; /** @@ -174,56 +198,78 @@ struct Direction { * @tparam F Reference frame tag (e.g. `frames::ICRS`). * @tparam U Distance unit (default: `qtty::Meter`). */ -template -struct Position { - static_assert(frames::is_frame_v, "F must be a valid frame tag"); - static_assert(centers::is_center_v, "C must be a valid center tag"); - - private: - qtty::Degree azimuth_; - qtty::Degree polar_; - U dist_; - - public: - Position() - : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)), dist_(U(0)) {} - - Position(qtty::Degree azimuth, qtty::Degree polar, U dist) - : azimuth_(azimuth), polar_(polar), dist_(dist) {} - - /// Extract the direction component. - Direction direction() const { - return Direction(azimuth_, polar_); - } - - /// @name Component accessors by frame convention - /// @{ - template , int> = 0> - qtty::Degree ra() const { return azimuth_; } - - template , int> = 0> - qtty::Degree dec() const { return polar_; } - - template , int> = 0> - qtty::Degree az() const { return azimuth_; } - - template , int> = 0> - qtty::Degree al() const { return polar_; } - - template , int> = 0> - qtty::Degree alt() const { return polar_; } - - template , int> = 0> - qtty::Degree lon() const { return azimuth_; } - - template , int> = 0> - qtty::Degree lat() const { return polar_; } - /// @} - - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_id; } - - U distance() const { return dist_; } +template struct Position { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + static_assert(centers::is_center_v, "C must be a valid center tag"); + +private: + qtty::Degree azimuth_; + qtty::Degree polar_; + U dist_; + +public: + Position() + : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)), dist_(U(0)) {} + + Position(qtty::Degree azimuth, qtty::Degree polar, U dist) + : azimuth_(azimuth), polar_(polar), dist_(dist) {} + + /// Extract the direction component. + Direction direction() const { return Direction(azimuth_, polar_); } + + /// @name Component accessors by frame convention + /// @{ + template , int> = 0> + qtty::Degree ra() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree dec() const { + return polar_; + } + + template , int> = 0> + qtty::Degree az() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree al() const { + return polar_; + } + + template , int> = 0> + qtty::Degree alt() const { + return polar_; + } + + template , int> = 0> + qtty::Degree lon() const { + return azimuth_; + } + + template , int> = 0> + qtty::Degree lat() const { + return polar_; + } + /// @} + + static constexpr siderust_frame_t frame_id() { + return frames::FrameTraits::ffi_id; + } + static constexpr siderust_center_t center_id() { + return centers::CenterTraits::ffi_id; + } + + U distance() const { return dist_; } }; // ============================================================================ @@ -234,24 +280,24 @@ struct Position { * @brief Stream operator for Direction with RA/Dec frames. */ template , int> = 0> -inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { - return os << dir.ra() << ", " << dir.dec(); +inline std::ostream &operator<<(std::ostream &os, const Direction &dir) { + return os << dir.ra() << ", " << dir.dec(); } /** * @brief Stream operator for Direction with Az/Alt frame. */ template , int> = 0> -inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { - return os << dir.az() << ", " << dir.alt(); +inline std::ostream &operator<<(std::ostream &os, const Direction &dir) { + return os << dir.az() << ", " << dir.alt(); } /** * @brief Stream operator for Direction with Lon/Lat frames. */ template , int> = 0> -inline std::ostream& operator<<(std::ostream& os, const Direction& dir) { - return os << dir.lon() << ", " << dir.lat(); +inline std::ostream &operator<<(std::ostream &os, const Direction &dir) { + return os << dir.lon() << ", " << dir.lat(); } } // namespace spherical diff --git a/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp b/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp index d06e3f6..cd440a5 100644 --- a/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp +++ b/include/siderust/coordinates/types/cartesian/position/ecliptic.hpp @@ -6,16 +6,20 @@ namespace siderust { namespace cartesian { namespace position { template -using EclipticMeanJ2000 = Position; +using EclipticMeanJ2000 = + Position; template -using HelioBarycentric = Position; +using HelioBarycentric = + Position; template -using GeoBarycentric = Position; +using GeoBarycentric = + Position; template -using MoonGeocentric = Position; +using MoonGeocentric = + Position; } // namespace position } // namespace cartesian } // namespace siderust diff --git a/include/siderust/coordinates/types/spherical/direction/equatorial.hpp b/include/siderust/coordinates/types/spherical/direction/equatorial.hpp index c920904..844a12e 100644 --- a/include/siderust/coordinates/types/spherical/direction/equatorial.hpp +++ b/include/siderust/coordinates/types/spherical/direction/equatorial.hpp @@ -5,9 +5,9 @@ namespace siderust { namespace spherical { namespace direction { -using ICRS = Direction; -using ICRF = Direction; -using EquatorialMeanJ2000 = Direction; +using ICRS = Direction; +using ICRF = Direction; +using EquatorialMeanJ2000 = Direction; using EquatorialMeanOfDate = Direction; using EquatorialTrueOfDate = Direction; } // namespace direction diff --git a/include/siderust/coordinates/types/spherical/position/ecliptic.hpp b/include/siderust/coordinates/types/spherical/position/ecliptic.hpp index ecf112b..8229992 100644 --- a/include/siderust/coordinates/types/spherical/position/ecliptic.hpp +++ b/include/siderust/coordinates/types/spherical/position/ecliptic.hpp @@ -6,7 +6,8 @@ namespace siderust { namespace spherical { namespace position { template -using EclipticMeanJ2000 = Position; +using EclipticMeanJ2000 = + Position; } // namespace position } // namespace spherical } // namespace siderust diff --git a/include/siderust/ephemeris.hpp b/include/siderust/ephemeris.hpp index d5377cb..59b715c 100644 --- a/include/siderust/ephemeris.hpp +++ b/include/siderust/ephemeris.hpp @@ -23,41 +23,48 @@ namespace ephemeris { /** * @brief Sun's barycentric position (EclipticMeanJ2000, AU) via VSOP87. */ -inline cartesian::position::HelioBarycentric sun_barycentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_sun_barycentric(jd.value(), &out), - "ephemeris::sun_barycentric"); - return cartesian::position::HelioBarycentric::from_c(out); +inline cartesian::position::HelioBarycentric +sun_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_sun_barycentric(jd.value(), &out), + "ephemeris::sun_barycentric"); + return cartesian::position::HelioBarycentric::from_c( + out); } /** * @brief Earth's barycentric position (EclipticMeanJ2000, AU) via VSOP87. */ -inline cartesian::position::GeoBarycentric earth_barycentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_earth_barycentric(jd.value(), &out), - "ephemeris::earth_barycentric"); - return cartesian::position::GeoBarycentric::from_c(out); +inline cartesian::position::GeoBarycentric +earth_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_earth_barycentric(jd.value(), &out), + "ephemeris::earth_barycentric"); + return cartesian::position::GeoBarycentric::from_c( + out); } /** * @brief Earth's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. */ -inline cartesian::position::EclipticMeanJ2000 earth_heliocentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_earth_heliocentric(jd.value(), &out), - "ephemeris::earth_heliocentric"); - return cartesian::position::EclipticMeanJ2000::from_c(out); +inline cartesian::position::EclipticMeanJ2000 +earth_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_earth_heliocentric(jd.value(), &out), + "ephemeris::earth_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c( + out); } /** * @brief Moon's geocentric position (EclipticMeanJ2000, km) via ELP2000. */ -inline cartesian::position::MoonGeocentric moon_geocentric(const JulianDate& jd) { - siderust_cartesian_pos_t out; - check_status(siderust_vsop87_moon_geocentric(jd.value(), &out), - "ephemeris::moon_geocentric"); - return cartesian::position::MoonGeocentric::from_c(out); +inline cartesian::position::MoonGeocentric +moon_geocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_moon_geocentric(jd.value(), &out), + "ephemeris::moon_geocentric"); + return cartesian::position::MoonGeocentric::from_c(out); } } // namespace ephemeris diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index 71784ce..df3c5b2 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -26,91 +26,99 @@ namespace siderust { // ============================================================================ class SiderustException : public std::runtime_error { - public: - explicit SiderustException(const std::string& msg) : std::runtime_error(msg) {} +public: + explicit SiderustException(const std::string &msg) + : std::runtime_error(msg) {} }; class NullPointerError : public SiderustException { - public: - explicit NullPointerError(const std::string& msg) : SiderustException(msg) {} +public: + explicit NullPointerError(const std::string &msg) : SiderustException(msg) {} }; class InvalidFrameError : public SiderustException { - public: - explicit InvalidFrameError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidFrameError(const std::string &msg) : SiderustException(msg) {} }; class InvalidCenterError : public SiderustException { - public: - explicit InvalidCenterError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidCenterError(const std::string &msg) + : SiderustException(msg) {} }; class TransformFailedError : public SiderustException { - public: - explicit TransformFailedError(const std::string& msg) : SiderustException(msg) {} +public: + explicit TransformFailedError(const std::string &msg) + : SiderustException(msg) {} }; class InvalidBodyError : public SiderustException { - public: - explicit InvalidBodyError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidBodyError(const std::string &msg) : SiderustException(msg) {} }; class UnknownStarError : public SiderustException { - public: - explicit UnknownStarError(const std::string& msg) : SiderustException(msg) {} +public: + explicit UnknownStarError(const std::string &msg) : SiderustException(msg) {} }; class InvalidPeriodError : public SiderustException { - public: - explicit InvalidPeriodError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidPeriodError(const std::string &msg) + : SiderustException(msg) {} }; class AllocationFailedError : public SiderustException { - public: - explicit AllocationFailedError(const std::string& msg) : SiderustException(msg) {} +public: + explicit AllocationFailedError(const std::string &msg) + : SiderustException(msg) {} }; class InvalidArgumentError : public SiderustException { - public: - explicit InvalidArgumentError(const std::string& msg) : SiderustException(msg) {} +public: + explicit InvalidArgumentError(const std::string &msg) + : SiderustException(msg) {} }; // ============================================================================ // Error Translation // ============================================================================ -inline void check_status(siderust_status_t status, const char* operation) { - if (status == SIDERUST_STATUS_T_OK) - return; - - std::string msg = std::string(operation) + " failed: "; - switch (status) { - case SIDERUST_STATUS_T_NULL_POINTER: - throw NullPointerError(msg + "null output pointer"); - case SIDERUST_STATUS_T_INVALID_FRAME: - throw InvalidFrameError(msg + "invalid or unsupported frame"); - case SIDERUST_STATUS_T_INVALID_CENTER: - throw InvalidCenterError(msg + "invalid or unsupported center"); - case SIDERUST_STATUS_T_TRANSFORM_FAILED: - throw TransformFailedError(msg + "coordinate transform failed"); - case SIDERUST_STATUS_T_INVALID_BODY: - throw InvalidBodyError(msg + "invalid body"); - case SIDERUST_STATUS_T_UNKNOWN_STAR: - throw UnknownStarError(msg + "unknown star name"); - case SIDERUST_STATUS_T_INVALID_PERIOD: - throw InvalidPeriodError(msg + "invalid period (start > end)"); - case SIDERUST_STATUS_T_ALLOCATION_FAILED: - throw AllocationFailedError(msg + "memory allocation failed"); - case SIDERUST_STATUS_T_INVALID_ARGUMENT: - throw InvalidArgumentError(msg + "invalid argument"); - default: - throw SiderustException(msg + "unknown error (" + std::to_string(status) + ")"); - } +inline void check_status(siderust_status_t status, const char *operation) { + if (status == SIDERUST_STATUS_T_OK) + return; + + std::string msg = std::string(operation) + " failed: "; + switch (status) { + case SIDERUST_STATUS_T_NULL_POINTER: + throw NullPointerError(msg + "null output pointer"); + case SIDERUST_STATUS_T_INVALID_FRAME: + throw InvalidFrameError(msg + "invalid or unsupported frame"); + case SIDERUST_STATUS_T_INVALID_CENTER: + throw InvalidCenterError(msg + "invalid or unsupported center"); + case SIDERUST_STATUS_T_TRANSFORM_FAILED: + throw TransformFailedError(msg + "coordinate transform failed"); + case SIDERUST_STATUS_T_INVALID_BODY: + throw InvalidBodyError(msg + "invalid body"); + case SIDERUST_STATUS_T_UNKNOWN_STAR: + throw UnknownStarError(msg + "unknown star name"); + case SIDERUST_STATUS_T_INVALID_PERIOD: + throw InvalidPeriodError(msg + "invalid period (start > end)"); + case SIDERUST_STATUS_T_ALLOCATION_FAILED: + throw AllocationFailedError(msg + "memory allocation failed"); + case SIDERUST_STATUS_T_INVALID_ARGUMENT: + throw InvalidArgumentError(msg + "invalid argument"); + default: + throw SiderustException(msg + "unknown error (" + std::to_string(status) + + ")"); + } } /// @brief Backward-compatible wrapper — delegates to tempoch::check_status. -inline void check_tempoch_status(tempoch_status_t status, const char* operation) { - tempoch::check_status(status, operation); +inline void check_tempoch_status(tempoch_status_t status, + const char *operation) { + tempoch::check_status(status, operation); } // ============================================================================ @@ -118,79 +126,78 @@ inline void check_tempoch_status(tempoch_status_t status, const char* operation) // ============================================================================ /** - * @brief Returns the siderust-ffi ABI version (major*10000 + minor*100 + patch). + * @brief Returns the siderust-ffi ABI version (major*10000 + minor*100 + + * patch). */ -inline uint32_t ffi_version() { - return siderust_ffi_version(); -} +inline uint32_t ffi_version() { return siderust_ffi_version(); } // ============================================================================ // Frame and Center Enums (C++ typed) // ============================================================================ enum class Frame : int32_t { - ICRS = SIDERUST_FRAME_T_ICRS, - EclipticMeanJ2000 = SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, - EquatorialMeanJ2000 = SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, - EquatorialMeanOfDate = SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, - EquatorialTrueOfDate = SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, - Horizontal = SIDERUST_FRAME_T_HORIZONTAL, - ECEF = SIDERUST_FRAME_T_ECEF, - Galactic = SIDERUST_FRAME_T_GALACTIC, - GCRS = SIDERUST_FRAME_T_GCRS, - EclipticOfDate = SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, - EclipticTrueOfDate = SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, - CIRS = SIDERUST_FRAME_T_CIRS, - TIRS = SIDERUST_FRAME_T_TIRS, - ITRF = SIDERUST_FRAME_T_ITRF, - ICRF = SIDERUST_FRAME_T_ICRF, + ICRS = SIDERUST_FRAME_T_ICRS, + EclipticMeanJ2000 = SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + EquatorialMeanJ2000 = SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, + EquatorialMeanOfDate = SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, + EquatorialTrueOfDate = SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, + Horizontal = SIDERUST_FRAME_T_HORIZONTAL, + ECEF = SIDERUST_FRAME_T_ECEF, + Galactic = SIDERUST_FRAME_T_GALACTIC, + GCRS = SIDERUST_FRAME_T_GCRS, + EclipticOfDate = SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, + EclipticTrueOfDate = SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, + CIRS = SIDERUST_FRAME_T_CIRS, + TIRS = SIDERUST_FRAME_T_TIRS, + ITRF = SIDERUST_FRAME_T_ITRF, + ICRF = SIDERUST_FRAME_T_ICRF, }; enum class Center : int32_t { - Barycentric = SIDERUST_CENTER_T_BARYCENTRIC, - Heliocentric = SIDERUST_CENTER_T_HELIOCENTRIC, - Geocentric = SIDERUST_CENTER_T_GEOCENTRIC, - Topocentric = SIDERUST_CENTER_T_TOPOCENTRIC, - Bodycentric = SIDERUST_CENTER_T_BODYCENTRIC, + Barycentric = SIDERUST_CENTER_T_BARYCENTRIC, + Heliocentric = SIDERUST_CENTER_T_HELIOCENTRIC, + Geocentric = SIDERUST_CENTER_T_GEOCENTRIC, + Topocentric = SIDERUST_CENTER_T_TOPOCENTRIC, + Bodycentric = SIDERUST_CENTER_T_BODYCENTRIC, }; enum class CrossingDirection : int32_t { - Rising = SIDERUST_CROSSING_DIRECTION_T_RISING, - Setting = SIDERUST_CROSSING_DIRECTION_T_SETTING, + Rising = SIDERUST_CROSSING_DIRECTION_T_RISING, + Setting = SIDERUST_CROSSING_DIRECTION_T_SETTING, }; enum class CulminationKind : int32_t { - Max = SIDERUST_CULMINATION_KIND_T_MAX, - Min = SIDERUST_CULMINATION_KIND_T_MIN, + Max = SIDERUST_CULMINATION_KIND_T_MAX, + Min = SIDERUST_CULMINATION_KIND_T_MIN, }; // ============================================================================ // Stream operators for enums // ============================================================================ -inline std::ostream& operator<<(std::ostream& os, CrossingDirection dir) { - switch (dir) { - case CrossingDirection::Rising: - return os << "rising"; - case CrossingDirection::Setting: - return os << "setting"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, CrossingDirection dir) { + switch (dir) { + case CrossingDirection::Rising: + return os << "rising"; + case CrossingDirection::Setting: + return os << "setting"; + } + return os << "unknown"; } -inline std::ostream& operator<<(std::ostream& os, CulminationKind kind) { - switch (kind) { - case CulminationKind::Max: - return os << "max"; - case CulminationKind::Min: - return os << "min"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, CulminationKind kind) { + switch (kind) { + case CulminationKind::Max: + return os << "max"; + case CulminationKind::Min: + return os << "min"; + } + return os << "unknown"; } enum class RaConvention : int32_t { - MuAlpha = SIDERUST_RA_CONVENTION_T_MU_ALPHA, - MuAlphaStar = SIDERUST_RA_CONVENTION_T_MU_ALPHA_STAR, + MuAlpha = SIDERUST_RA_CONVENTION_T_MU_ALPHA, + MuAlphaStar = SIDERUST_RA_CONVENTION_T_MU_ALPHA_STAR, }; } // namespace siderust diff --git a/include/siderust/frames.hpp b/include/siderust/frames.hpp index 2e2b0f3..fe3bb5c 100644 --- a/include/siderust/frames.hpp +++ b/include/siderust/frames.hpp @@ -30,14 +30,13 @@ struct FrameTraits; // primary template — intentionally undefined /** * @brief Concept-like compile-time check (C++17: constexpr bool). */ -template -struct is_frame : std::false_type {}; +template struct is_frame : std::false_type {}; template -struct is_frame::ffi_id)>> : std::true_type {}; +struct is_frame::ffi_id)>> + : std::true_type {}; -template -inline constexpr bool is_frame_v = is_frame::value; +template inline constexpr bool is_frame_v = is_frame::value; // ============================================================================ // Frame Tag Definitions @@ -80,21 +79,30 @@ struct EclipticMeanOfDate {}; // FrameTraits Specializations // ============================================================================ -#define SIDERUST_DEFINE_FRAME(Tag, EnumVal, Label) \ - template <> \ - struct FrameTraits { \ - static constexpr siderust_frame_t ffi_id = EnumVal; \ - static constexpr const char* name() { return Label; } \ - } +#define SIDERUST_DEFINE_FRAME(Tag, EnumVal, Label) \ + template <> struct FrameTraits { \ + static constexpr siderust_frame_t ffi_id = EnumVal; \ + static constexpr const char *name() { return Label; } \ + } SIDERUST_DEFINE_FRAME(ICRS, SIDERUST_FRAME_T_ICRS, "ICRS"); SIDERUST_DEFINE_FRAME(ICRF, SIDERUST_FRAME_T_ICRF, "ICRF"); -SIDERUST_DEFINE_FRAME(EclipticMeanJ2000, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, "EclipticMeanJ2000"); -SIDERUST_DEFINE_FRAME(EclipticOfDate, SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, "EclipticOfDate"); -SIDERUST_DEFINE_FRAME(EclipticTrueOfDate, SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, "EclipticTrueOfDate"); -SIDERUST_DEFINE_FRAME(EquatorialMeanJ2000, SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, "EquatorialMeanJ2000"); -SIDERUST_DEFINE_FRAME(EquatorialMeanOfDate, SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, "EquatorialMeanOfDate"); -SIDERUST_DEFINE_FRAME(EquatorialTrueOfDate, SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, "EquatorialTrueOfDate"); +SIDERUST_DEFINE_FRAME(EclipticMeanJ2000, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + "EclipticMeanJ2000"); +SIDERUST_DEFINE_FRAME(EclipticOfDate, SIDERUST_FRAME_T_ECLIPTIC_OF_DATE, + "EclipticOfDate"); +SIDERUST_DEFINE_FRAME(EclipticTrueOfDate, + SIDERUST_FRAME_T_ECLIPTIC_TRUE_OF_DATE, + "EclipticTrueOfDate"); +SIDERUST_DEFINE_FRAME(EquatorialMeanJ2000, + SIDERUST_FRAME_T_EQUATORIAL_MEAN_J2000, + "EquatorialMeanJ2000"); +SIDERUST_DEFINE_FRAME(EquatorialMeanOfDate, + SIDERUST_FRAME_T_EQUATORIAL_MEAN_OF_DATE, + "EquatorialMeanOfDate"); +SIDERUST_DEFINE_FRAME(EquatorialTrueOfDate, + SIDERUST_FRAME_T_EQUATORIAL_TRUE_OF_DATE, + "EquatorialTrueOfDate"); SIDERUST_DEFINE_FRAME(Horizontal, SIDERUST_FRAME_T_HORIZONTAL, "Horizontal"); SIDERUST_DEFINE_FRAME(Galactic, SIDERUST_FRAME_T_GALACTIC, "Galactic"); SIDERUST_DEFINE_FRAME(ECEF, SIDERUST_FRAME_T_ECEF, "ECEF"); @@ -112,60 +120,52 @@ SIDERUST_DEFINE_FRAME(TIRS, SIDERUST_FRAME_T_TIRS, "TIRS"); /** * @brief Maps a frame to its conventional spherical-coordinate names. * - * Default: (longitude, latitude). Specialise per-frame for RA/Dec, Az/Alt, etc. + * Default: (longitude, latitude). Specialise per-frame for RA/Dec, Az/Alt, + * etc. */ -template -struct SphericalNaming { - static constexpr const char* lon_name() { return "longitude"; } - static constexpr const char* lat_name() { return "latitude"; } +template struct SphericalNaming { + static constexpr const char *lon_name() { return "longitude"; } + static constexpr const char *lat_name() { return "latitude"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "right_ascension"; } - static constexpr const char* lat_name() { return "declination"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "right_ascension"; } + static constexpr const char *lat_name() { return "declination"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "azimuth"; } - static constexpr const char* lat_name() { return "altitude"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "azimuth"; } + static constexpr const char *lat_name() { return "altitude"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "l"; } - static constexpr const char* lat_name() { return "b"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "l"; } + static constexpr const char *lat_name() { return "b"; } }; -template <> -struct SphericalNaming { - static constexpr const char* lon_name() { return "ecliptic_longitude"; } - static constexpr const char* lat_name() { return "ecliptic_latitude"; } +template <> struct SphericalNaming { + static constexpr const char *lon_name() { return "ecliptic_longitude"; } + static constexpr const char *lat_name() { return "ecliptic_latitude"; } }; // ============================================================================ @@ -177,58 +177,38 @@ struct SphericalNaming { * * Use `has_ra_dec_v` in `std::enable_if_t` to gate RA/Dec accessors. */ -template -struct has_ra_dec : std::false_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template <> -struct has_ra_dec : std::true_type {}; -template -inline constexpr bool has_ra_dec_v = has_ra_dec::value; +template struct has_ra_dec : std::false_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template <> struct has_ra_dec : std::true_type {}; +template inline constexpr bool has_ra_dec_v = has_ra_dec::value; /** * @brief True for the horizontal frame that exposes azimuth / altitude. * * Use `has_az_alt_v` to gate Az/Alt accessors. */ -template -struct has_az_alt : std::false_type {}; -template <> -struct has_az_alt : std::true_type {}; -template -inline constexpr bool has_az_alt_v = has_az_alt::value; +template struct has_az_alt : std::false_type {}; +template <> struct has_az_alt : std::true_type {}; +template inline constexpr bool has_az_alt_v = has_az_alt::value; /** * @brief True for ecliptic and galactic frames that use longitude / latitude. * * Use `has_lon_lat_v` to gate lon/lat accessors. */ -template -struct has_lon_lat : std::false_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; -template <> -struct has_lon_lat : std::true_type {}; +template struct has_lon_lat : std::false_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; +template <> struct has_lon_lat : std::true_type {}; template inline constexpr bool has_lon_lat_v = has_lon_lat::value; @@ -250,15 +230,12 @@ template struct has_frame_transform : std::false_type {}; // Identity -template -struct has_frame_transform : std::true_type {}; +template struct has_frame_transform : std::true_type {}; // Hub spokes (bidirectional) -#define SIDERUST_FRAME_TRANSFORM_PAIR(A, B) \ - template <> \ - struct has_frame_transform : std::true_type {}; \ - template <> \ - struct has_frame_transform : std::true_type {} +#define SIDERUST_FRAME_TRANSFORM_PAIR(A, B) \ + template <> struct has_frame_transform : std::true_type {}; \ + template <> struct has_frame_transform : std::true_type {} // All pairs reachable through the ICRS hub SIDERUST_FRAME_TRANSFORM_PAIR(ICRS, EclipticMeanJ2000); @@ -281,18 +258,16 @@ SIDERUST_FRAME_TRANSFORM_PAIR(ICRF, ICRS); #undef SIDERUST_FRAME_TRANSFORM_PAIR template -inline constexpr bool has_frame_transform_v = has_frame_transform::value; +inline constexpr bool has_frame_transform_v = + has_frame_transform::value; /** * @brief Marks frames from which to_horizontal is reachable. */ -template -struct has_horizontal_transform : std::false_type {}; +template struct has_horizontal_transform : std::false_type {}; -template <> -struct has_horizontal_transform : std::true_type {}; -template <> -struct has_horizontal_transform : std::true_type {}; +template <> struct has_horizontal_transform : std::true_type {}; +template <> struct has_horizontal_transform : std::true_type {}; template <> struct has_horizontal_transform : std::true_type {}; template <> @@ -303,7 +278,8 @@ template <> struct has_horizontal_transform : std::true_type {}; template -inline constexpr bool has_horizontal_transform_v = has_horizontal_transform::value; +inline constexpr bool has_horizontal_transform_v = + has_horizontal_transform::value; } // namespace frames } // namespace siderust diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index 58704ca..1db1aa6 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -28,24 +28,24 @@ namespace siderust { * @brief Principal lunar phase kinds (new-moon quarter events). */ enum class PhaseKind : int32_t { - NewMoon = 0, - FirstQuarter = 1, - FullMoon = 2, - LastQuarter = 3, + NewMoon = 0, + FirstQuarter = 1, + FullMoon = 2, + LastQuarter = 3, }; /** * @brief Descriptive moon phase labels (8 canonical phases). */ enum class MoonPhaseLabel : int32_t { - NewMoon = 0, - WaxingCrescent = 1, - FirstQuarter = 2, - WaxingGibbous = 3, - FullMoon = 4, - WaningGibbous = 5, - LastQuarter = 6, - WaningCrescent = 7, + NewMoon = 0, + WaxingCrescent = 1, + FirstQuarter = 2, + WaxingGibbous = 3, + FullMoon = 4, + WaningGibbous = 5, + LastQuarter = 6, + WaningCrescent = 7, }; // ============================================================================ @@ -56,27 +56,27 @@ enum class MoonPhaseLabel : int32_t { * @brief Geometric description of the Moon's phase at a point in time. */ struct MoonPhaseGeometry { - double phase_angle_rad; ///< Phase angle in [0, π], radians. - double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. - double elongation_rad; ///< Sun–Moon elongation, radians. - bool waxing; ///< True when the Moon is waxing. - - static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t& c) { - return {c.phase_angle_rad, c.illuminated_fraction, - c.elongation_rad, static_cast(c.waxing)}; - } + double phase_angle_rad; ///< Phase angle in [0, π], radians. + double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. + double elongation_rad; ///< Sun–Moon elongation, radians. + bool waxing; ///< True when the Moon is waxing. + + static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t &c) { + return {c.phase_angle_rad, c.illuminated_fraction, c.elongation_rad, + static_cast(c.waxing)}; + } }; /** * @brief A principal lunar phase event (new moon, first quarter, etc.). */ struct PhaseEvent { - MJD time; ///< Epoch of the event (MJD). - PhaseKind kind; ///< Which principal phase occurred. + MJD time; ///< Epoch of the event (MJD). + PhaseKind kind; ///< Which principal phase occurred. - static PhaseEvent from_c(const siderust_phase_event_t& c) { - return {MJD(c.mjd), static_cast(c.kind)}; - } + static PhaseEvent from_c(const siderust_phase_event_t &c) { + return {MJD(c.mjd), static_cast(c.kind)}; + } }; // ============================================================================ @@ -84,28 +84,28 @@ struct PhaseEvent { // ============================================================================ namespace detail { -inline std::vector phase_events_from_c( - siderust_phase_event_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(PhaseEvent::from_c(ptr[i])); - } - siderust_phase_events_free(ptr, count); - return result; +inline std::vector phase_events_from_c(siderust_phase_event_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(PhaseEvent::from_c(ptr[i])); + } + siderust_phase_events_free(ptr, count); + return result; } /// Like periods_from_c but for tempoch_period_mjd_t* pointers (freed with /// siderust_periods_free). -inline std::vector illum_periods_from_c( - tempoch_period_mjd_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); - } - siderust_periods_free(ptr, count); - return result; +inline std::vector illum_periods_from_c(tempoch_period_mjd_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); + } + siderust_periods_free(ptr, count); + return result; } } // namespace detail @@ -121,11 +121,11 @@ namespace moon { * * @param jd Julian Date (e.g. `siderust::JulianDate(2451545.0)` for J2000.0). */ -inline MoonPhaseGeometry phase_geocentric(const JulianDate& jd) { - siderust_moon_phase_geometry_t out{}; - check_status(siderust_moon_phase_geocentric(jd.value(), &out), - "moon::phase_geocentric"); - return MoonPhaseGeometry::from_c(out); +inline MoonPhaseGeometry phase_geocentric(const JulianDate &jd) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_geocentric(jd.value(), &out), + "moon::phase_geocentric"); + return MoonPhaseGeometry::from_c(out); } /** @@ -134,51 +134,53 @@ inline MoonPhaseGeometry phase_geocentric(const JulianDate& jd) { * @param jd Julian Date. * @param site Observer geodetic coordinates. */ -inline MoonPhaseGeometry phase_topocentric(const JulianDate& jd, - const Geodetic& site) { - siderust_moon_phase_geometry_t out{}; - check_status(siderust_moon_phase_topocentric(jd.value(), site.to_c(), &out), - "moon::phase_topocentric"); - return MoonPhaseGeometry::from_c(out); +inline MoonPhaseGeometry phase_topocentric(const JulianDate &jd, + const Geodetic &site) { + siderust_moon_phase_geometry_t out{}; + check_status(siderust_moon_phase_topocentric(jd.value(), site.to_c(), &out), + "moon::phase_topocentric"); + return MoonPhaseGeometry::from_c(out); } /** * @brief Determine the descriptive phase label for a given geometry. * - * @param geom Moon phase geometry (as returned by phase_geocentric / phase_topocentric). + * @param geom Moon phase geometry (as returned by phase_geocentric / + * phase_topocentric). */ -inline MoonPhaseLabel phase_label(const MoonPhaseGeometry& geom) { - siderust_moon_phase_geometry_t c{geom.phase_angle_rad, - geom.illuminated_fraction, - geom.elongation_rad, - static_cast(geom.waxing)}; - siderust_moon_phase_label_t out{}; - check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); - return static_cast(out); +inline MoonPhaseLabel phase_label(const MoonPhaseGeometry &geom) { + siderust_moon_phase_geometry_t c{ + geom.phase_angle_rad, geom.illuminated_fraction, geom.elongation_rad, + static_cast(geom.waxing)}; + siderust_moon_phase_label_t out{}; + check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); + return static_cast(out); } /** - * @brief Find principal phase events (new moon, quarters, full moon) in a window. + * @brief Find principal phase events (new moon, quarters, full moon) in a + * window. * * @param window MJD search window. * @param opts Search tolerances (optional). */ -inline std::vector find_phase_events( - const Period& window, const SearchOptions& opts = {}) { - siderust_phase_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_find_phase_events( - window.c_inner(), opts.to_c(), &ptr, &count), - "moon::find_phase_events"); - return detail::phase_events_from_c(ptr, count); +inline std::vector +find_phase_events(const Period &window, const SearchOptions &opts = {}) { + siderust_phase_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status( + siderust_find_phase_events(window.c_inner(), opts.to_c(), &ptr, &count), + "moon::find_phase_events"); + return detail::phase_events_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector find_phase_events( - const MJD& start, const MJD& end, const SearchOptions& opts = {}) { - return find_phase_events(Period(start, end), opts); +inline std::vector +find_phase_events(const MJD &start, const MJD &end, + const SearchOptions &opts = {}) { + return find_phase_events(Period(start, end), opts); } /** @@ -188,23 +190,24 @@ inline std::vector find_phase_events( * @param k_min Minimum illuminated fraction in [0, 1]. * @param opts Search tolerances (optional). */ -inline std::vector illumination_above( - const Period& window, double k_min, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_illumination_above( - window.c_inner(), k_min, opts.to_c(), &ptr, &count), - "moon::illumination_above"); - return detail::illum_periods_from_c(ptr, count); +inline std::vector illumination_above(const Period &window, + double k_min, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_above(window.c_inner(), k_min, + opts.to_c(), &ptr, &count), + "moon::illumination_above"); + return detail::illum_periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector illumination_above( - const MJD& start, const MJD& end, double k_min, - const SearchOptions& opts = {}) { - return illumination_above(Period(start, end), k_min, opts); +inline std::vector illumination_above(const MJD &start, const MJD &end, + double k_min, + const SearchOptions &opts = {}) { + return illumination_above(Period(start, end), k_min, opts); } /** @@ -214,23 +217,24 @@ inline std::vector illumination_above( * @param k_max Maximum illuminated fraction in [0, 1]. * @param opts Search tolerances (optional). */ -inline std::vector illumination_below( - const Period& window, double k_max, const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_illumination_below( - window.c_inner(), k_max, opts.to_c(), &ptr, &count), - "moon::illumination_below"); - return detail::illum_periods_from_c(ptr, count); +inline std::vector illumination_below(const Period &window, + double k_max, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_below(window.c_inner(), k_max, + opts.to_c(), &ptr, &count), + "moon::illumination_below"); + return detail::illum_periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector illumination_below( - const MJD& start, const MJD& end, double k_max, - const SearchOptions& opts = {}) { - return illumination_below(Period(start, end), k_max, opts); +inline std::vector illumination_below(const MJD &start, const MJD &end, + double k_max, + const SearchOptions &opts = {}) { + return illumination_below(Period(start, end), k_max, opts); } /** @@ -241,24 +245,24 @@ inline std::vector illumination_below( * @param k_max Maximum illuminated fraction in [0, 1]. * @param opts Search tolerances (optional). */ -inline std::vector illumination_range( - const Period& window, double k_min, double k_max, - const SearchOptions& opts = {}) { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_moon_illumination_range( - window.c_inner(), k_min, k_max, opts.to_c(), &ptr, &count), - "moon::illumination_range"); - return detail::illum_periods_from_c(ptr, count); +inline std::vector illumination_range(const Period &window, + double k_min, double k_max, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_moon_illumination_range(window.c_inner(), k_min, k_max, + opts.to_c(), &ptr, &count), + "moon::illumination_range"); + return detail::illum_periods_from_c(ptr, count); } /** * @brief Backward-compatible [start, end] overload. */ -inline std::vector illumination_range( - const MJD& start, const MJD& end, double k_min, double k_max, - const SearchOptions& opts = {}) { - return illumination_range(Period(start, end), k_min, k_max, opts); +inline std::vector illumination_range(const MJD &start, const MJD &end, + double k_min, double k_max, + const SearchOptions &opts = {}) { + return illumination_range(Period(start, end), k_min, k_max, opts); } } // namespace moon @@ -270,36 +274,36 @@ inline std::vector illumination_range( /** * @brief Get the illuminated fraction as a percentage [0, 100]. */ -inline double illuminated_percent(const MoonPhaseGeometry& geom) { - return geom.illuminated_fraction * 100.0; +inline double illuminated_percent(const MoonPhaseGeometry &geom) { + return geom.illuminated_fraction * 100.0; } /** * @brief Check if a phase label describes a waxing moon. */ inline bool is_waxing(MoonPhaseLabel label) { - switch (label) { - case MoonPhaseLabel::WaxingCrescent: - case MoonPhaseLabel::FirstQuarter: - case MoonPhaseLabel::WaxingGibbous: - return true; - default: - return false; - } + switch (label) { + case MoonPhaseLabel::WaxingCrescent: + case MoonPhaseLabel::FirstQuarter: + case MoonPhaseLabel::WaxingGibbous: + return true; + default: + return false; + } } /** * @brief Check if a phase label describes a waning moon. */ inline bool is_waning(MoonPhaseLabel label) { - switch (label) { - case MoonPhaseLabel::WaningGibbous: - case MoonPhaseLabel::LastQuarter: - case MoonPhaseLabel::WaningCrescent: - return true; - default: - return false; - } + switch (label) { + case MoonPhaseLabel::WaningGibbous: + case MoonPhaseLabel::LastQuarter: + case MoonPhaseLabel::WaningCrescent: + return true; + default: + return false; + } } // ============================================================================ @@ -309,43 +313,43 @@ inline bool is_waning(MoonPhaseLabel label) { /** * @brief Stream operator for PhaseKind. */ -inline std::ostream& operator<<(std::ostream& os, PhaseKind kind) { - switch (kind) { - case PhaseKind::NewMoon: - return os << "new moon"; - case PhaseKind::FirstQuarter: - return os << "first quarter"; - case PhaseKind::FullMoon: - return os << "full moon"; - case PhaseKind::LastQuarter: - return os << "last quarter"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, PhaseKind kind) { + switch (kind) { + case PhaseKind::NewMoon: + return os << "new moon"; + case PhaseKind::FirstQuarter: + return os << "first quarter"; + case PhaseKind::FullMoon: + return os << "full moon"; + case PhaseKind::LastQuarter: + return os << "last quarter"; + } + return os << "unknown"; } /** * @brief Stream operator for MoonPhaseLabel. */ -inline std::ostream& operator<<(std::ostream& os, MoonPhaseLabel label) { - switch (label) { - case MoonPhaseLabel::NewMoon: - return os << "new moon"; - case MoonPhaseLabel::WaxingCrescent: - return os << "waxing crescent"; - case MoonPhaseLabel::FirstQuarter: - return os << "first quarter"; - case MoonPhaseLabel::WaxingGibbous: - return os << "waxing gibbous"; - case MoonPhaseLabel::FullMoon: - return os << "full moon"; - case MoonPhaseLabel::WaningGibbous: - return os << "waning gibbous"; - case MoonPhaseLabel::LastQuarter: - return os << "last quarter"; - case MoonPhaseLabel::WaningCrescent: - return os << "waning crescent"; - } - return os << "unknown"; +inline std::ostream &operator<<(std::ostream &os, MoonPhaseLabel label) { + switch (label) { + case MoonPhaseLabel::NewMoon: + return os << "new moon"; + case MoonPhaseLabel::WaxingCrescent: + return os << "waxing crescent"; + case MoonPhaseLabel::FirstQuarter: + return os << "first quarter"; + case MoonPhaseLabel::WaxingGibbous: + return os << "waxing gibbous"; + case MoonPhaseLabel::FullMoon: + return os << "full moon"; + case MoonPhaseLabel::WaningGibbous: + return os << "waning gibbous"; + case MoonPhaseLabel::LastQuarter: + return os << "last quarter"; + case MoonPhaseLabel::WaningCrescent: + return os << "waning crescent"; + } + return os << "unknown"; } } // namespace siderust diff --git a/include/siderust/observatories.hpp b/include/siderust/observatories.hpp index 5e9c345..e632906 100644 --- a/include/siderust/observatories.hpp +++ b/include/siderust/observatories.hpp @@ -13,27 +13,28 @@ namespace siderust { namespace detail { inline Geodetic make_roque_de_los_muchachos() { - siderust_geodetic_t out; - check_status(siderust_observatory_roque_de_los_muchachos(&out), "ROQUE_DE_LOS_MUCHACHOS"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_roque_de_los_muchachos(&out), + "ROQUE_DE_LOS_MUCHACHOS"); + return Geodetic::from_c(out); } inline Geodetic make_el_paranal() { - siderust_geodetic_t out; - check_status(siderust_observatory_el_paranal(&out), "EL_PARANAL"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_el_paranal(&out), "EL_PARANAL"); + return Geodetic::from_c(out); } inline Geodetic make_mauna_kea() { - siderust_geodetic_t out; - check_status(siderust_observatory_mauna_kea(&out), "MAUNA_KEA"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_mauna_kea(&out), "MAUNA_KEA"); + return Geodetic::from_c(out); } inline Geodetic make_la_silla() { - siderust_geodetic_t out; - check_status(siderust_observatory_la_silla(&out), "LA_SILLA_OBSERVATORY"); - return Geodetic::from_c(out); + siderust_geodetic_t out; + check_status(siderust_observatory_la_silla(&out), "LA_SILLA_OBSERVATORY"); + return Geodetic::from_c(out); } } // namespace detail @@ -41,17 +42,19 @@ inline Geodetic make_la_silla() { /** * @brief Create a custom geodetic position (WGS84). */ -inline Geodetic geodetic(double lon_deg, double lat_deg, double height_m = 0.0) { - siderust_geodetic_t out; - check_status(siderust_geodetic_new(lon_deg, lat_deg, height_m, &out), - "geodetic"); - return Geodetic::from_c(out); +inline Geodetic geodetic(double lon_deg, double lat_deg, + double height_m = 0.0) { + siderust_geodetic_t out; + check_status(siderust_geodetic_new(lon_deg, lat_deg, height_m, &out), + "geodetic"); + return Geodetic::from_c(out); } /** * @brief Roque de los Muchachos Observatory (La Palma, Spain). */ -inline const Geodetic ROQUE_DE_LOS_MUCHACHOS = detail::make_roque_de_los_muchachos(); +inline const Geodetic ROQUE_DE_LOS_MUCHACHOS = + detail::make_roque_de_los_muchachos(); /** * @brief El Paranal Observatory (Chile). diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 483bd6c..63e0c05 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -13,16 +13,19 @@ * using namespace siderust::frames; * * // Typed coordinates with compile-time frame/center - * spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), qtty::Degree(38.78369)); - * auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + * spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), + * qtty::Degree(38.78369)); auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, + * 0}); * * // Template-targeted transform — invalid pairs won't compile - * auto ecl = vega_icrs.to_frame(jd); // Direction - * auto hor = vega_icrs.to_horizontal(jd, ROQUE_DE_LOS_MUCHACHOS); + * auto ecl = vega_icrs.to_frame(jd); // + * Direction auto hor = vega_icrs.to_horizontal(jd, + * ROQUE_DE_LOS_MUCHACHOS); * * // Typed ephemeris — unit-safe AU/km positions - * auto earth = ephemeris::earth_heliocentric(jd); // cartesian::Position - * auto dist = earth.comp_x.to(); // unit conversion + * auto earth = ephemeris::earth_heliocentric(jd); // + * cartesian::Position auto dist = + * earth.comp_x.to(); // unit conversion * @endcode */ diff --git a/include/siderust/star_target.hpp b/include/siderust/star_target.hpp index 72e336a..40f0a02 100644 --- a/include/siderust/star_target.hpp +++ b/include/siderust/star_target.hpp @@ -30,66 +30,69 @@ namespace siderust { * globals and live for the entire program. */ class StarTarget : public Trackable { - public: - /** - * @brief Wrap a Star reference as a Trackable. - * @param star Reference to a Star. Must outlive this adapter. - */ - explicit StarTarget(const Star& star) : star_(star) {} - - // ------------------------------------------------------------------ - // Altitude queries - // ------------------------------------------------------------------ - - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { - // star_altitude::altitude_at returns Radian; convert to Degree - auto rad = star_altitude::altitude_at(star_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); - } - - std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return star_altitude::above_threshold(star_, obs, window, threshold, opts); - } - - std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return star_altitude::below_threshold(star_, obs, window, threshold, opts); - } - - std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - return star_altitude::crossings(star_, obs, window, threshold, opts); - } - - std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const override { - return star_altitude::culminations(star_, obs, window, opts); - } - - // ------------------------------------------------------------------ - // Azimuth queries - // ------------------------------------------------------------------ - - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { - return star_altitude::azimuth_at(star_, obs, mjd); - } - - std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const override { - return star_altitude::azimuth_crossings(star_, obs, window, bearing, opts); - } - - /// Access the underlying Star reference. - const Star& star() const { return star_; } - - private: - const Star& star_; +public: + /** + * @brief Wrap a Star reference as a Trackable. + * @param star Reference to a Star. Must outlive this adapter. + */ + explicit StarTarget(const Star &star) : star_(star) {} + + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ + + qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { + // star_altitude::altitude_at returns Radian; convert to Degree + auto rad = star_altitude::altitude_at(star_, obs, mjd); + return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + } + + std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return star_altitude::above_threshold(star_, obs, window, threshold, opts); + } + + std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return star_altitude::below_threshold(star_, obs, window, threshold, opts); + } + + std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + return star_altitude::crossings(star_, obs, window, threshold, opts); + } + + std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const override { + return star_altitude::culminations(star_, obs, window, opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ + + qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) const override { + return star_altitude::azimuth_at(star_, obs, mjd); + } + + std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const override { + return star_altitude::azimuth_crossings(star_, obs, window, bearing, opts); + } + + /// Access the underlying Star reference. + const Star &star() const { return star_; } + +private: + const Star &star_; }; } // namespace siderust diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index c7d6bc9..af7fba3 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -49,22 +49,21 @@ namespace detail { /// @cond INTERNAL /// True iff T is an instantiation of spherical::Direction. -template -struct is_spherical_direction : std::false_type {}; +template struct is_spherical_direction : std::false_type {}; template struct is_spherical_direction> : std::true_type {}; template -inline constexpr bool is_spherical_direction_v = is_spherical_direction::value; +inline constexpr bool is_spherical_direction_v = + is_spherical_direction::value; /// Extract the frame tag F from spherical::Direction. -template -struct spherical_direction_frame; // undefined primary +template struct spherical_direction_frame; // undefined primary template struct spherical_direction_frame> { - using type = F; + using type = F; }; template @@ -98,276 +97,284 @@ using spherical_direction_frame_t = typename spherical_direction_frame::type; * auto alt = ec.altitude_at(obs, now); * @endcode */ -template -class Target : public Trackable { - - static_assert(detail::is_spherical_direction_v, - "Target: C must be a specialisation of " - "siderust::spherical::Direction"); - - using Frame = detail::spherical_direction_frame_t; - - static_assert(frames::has_frame_transform_v, - "Target: frame F must support a transform to ICRS " - "(frames::has_frame_transform_v must be true). " - "Supported frames: ICRS, ICRF, EquatorialMeanJ2000, " - "EquatorialMeanOfDate, EquatorialTrueOfDate, EclipticMeanJ2000."); - - public: - // ------------------------------------------------------------------ - // Construction / destruction - // ------------------------------------------------------------------ - - /** - * @brief Construct from a strongly-typed spherical direction. - * - * For frames other than ICRS, the direction is converted to ICRS before - * being registered with the Rust FFI. The original `C` direction is - * retained for C++-side accessors. - * - * @param dir Spherical direction (any supported frame). - * @param epoch Coordinate epoch (default J2000.0). - */ - explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) - : m_dir_(dir), m_epoch_(epoch) { - // Convert to ICRS for the FFI; identity transform when already ICRS. - if constexpr (std::is_same_v) { - m_icrs_ = dir; - } else { - m_icrs_ = dir.template to_frame(epoch); - } - SiderustTarget* h = nullptr; - check_status( - siderust_target_create( - m_icrs_.ra().value(), m_icrs_.dec().value(), epoch.value(), &h), - "Target::Target"); - handle_ = h; - } - - ~Target() { - if (handle_) { - siderust_target_free(handle_); - handle_ = nullptr; - } - } - - /// Move constructor. - Target(Target&& other) noexcept - : m_dir_(std::move(other.m_dir_)), - m_epoch_(other.m_epoch_), - m_icrs_(other.m_icrs_), - handle_(other.handle_) { - other.handle_ = nullptr; - } - - /// Move assignment. - Target& operator=(Target&& other) noexcept { - if (this != &other) { - if (handle_) { - siderust_target_free(handle_); - } - m_dir_ = std::move(other.m_dir_); - m_epoch_ = other.m_epoch_; - m_icrs_ = other.m_icrs_; - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; +template class Target : public Trackable { + + static_assert(detail::is_spherical_direction_v, + "Target: C must be a specialisation of " + "siderust::spherical::Direction"); + + using Frame = detail::spherical_direction_frame_t; + + static_assert( + frames::has_frame_transform_v, + "Target: frame F must support a transform to ICRS " + "(frames::has_frame_transform_v must be true). " + "Supported frames: ICRS, ICRF, EquatorialMeanJ2000, " + "EquatorialMeanOfDate, EquatorialTrueOfDate, EclipticMeanJ2000."); + +public: + // ------------------------------------------------------------------ + // Construction / destruction + // ------------------------------------------------------------------ + + /** + * @brief Construct from a strongly-typed spherical direction. + * + * For frames other than ICRS, the direction is converted to ICRS before + * being registered with the Rust FFI. The original `C` direction is + * retained for C++-side accessors. + * + * @param dir Spherical direction (any supported frame). + * @param epoch Coordinate epoch (default J2000.0). + */ + explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) + : m_dir_(dir), m_epoch_(epoch) { + // Convert to ICRS for the FFI; identity transform when already ICRS. + if constexpr (std::is_same_v) { + m_icrs_ = dir; + } else { + m_icrs_ = dir.template to_frame(epoch); } - - // Prevent copying (the handle has unique ownership). - Target(const Target&) = delete; - Target& operator=(const Target&) = delete; - - // ------------------------------------------------------------------ - // Coordinate accessors - // ------------------------------------------------------------------ - - /// The original typed direction as supplied at construction. - const C& direction() const { return m_dir_; } - - /// Epoch of the coordinate. - JulianDate epoch() const { return m_epoch_; } - - /// The ICRS direction used for FFI calls (equals `direction()` when C is - /// already `spherical::direction::ICRS`). - const spherical::direction::ICRS& icrs_direction() const { return m_icrs_; } - - /// Right ascension — only available for equatorial frames (RA/Dec). - template , int> = 0> - qtty::Degree ra() const { return m_dir_.ra(); } - - /// Declination — only available for equatorial frames (RA/Dec). - template , int> = 0> - qtty::Degree dec() const { return m_dir_.dec(); } - - // ------------------------------------------------------------------ - // Altitude queries (implements Trackable) - // ------------------------------------------------------------------ - - /** - * @brief Compute altitude (degrees) at a given MJD instant. - * - * @note The Rust FFI returns radians; this method converts to degrees. - */ - qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const override { - double out{}; - check_status(siderust_target_altitude_at( - handle_, obs.to_c(), mjd.value(), &out), - "Target::altitude_at"); - return qtty::Radian(out).to(); - } - - /** - * @brief Find periods when the target is above a threshold altitude. - */ - std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_above_threshold( - handle_, obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "Target::above_threshold"); - return detail_periods_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector above_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) const { - return above_threshold(obs, Period(start, end), threshold, opts); - } - - /** - * @brief Find periods when the target is below a threshold altitude. - */ - std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - // Always pass ICRS direction to the FFI layer. - siderust_spherical_dir_t dir_c{}; - dir_c.polar_deg = m_icrs_.dec().value(); - dir_c.azimuth_deg = m_icrs_.ra().value(); - dir_c.frame = SIDERUST_FRAME_T_ICRS; - tempoch_period_mjd_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_icrs_below_threshold( - dir_c, obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "Target::below_threshold"); - return detail_periods_from_c(ptr, count); + SiderustTarget *h = nullptr; + check_status(siderust_target_create(m_icrs_.ra().value(), + m_icrs_.dec().value(), epoch.value(), + &h), + "Target::Target"); + handle_ = h; + } + + ~Target() { + if (handle_) { + siderust_target_free(handle_); + handle_ = nullptr; } - - /// Backward-compatible [start, end] overload. - std::vector below_threshold( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) const { - return below_threshold(obs, Period(start, end), threshold, opts); + } + + /// Move constructor. + Target(Target &&other) noexcept + : m_dir_(std::move(other.m_dir_)), m_epoch_(other.m_epoch_), + m_icrs_(other.m_icrs_), handle_(other.handle_) { + other.handle_ = nullptr; + } + + /// Move assignment. + Target &operator=(Target &&other) noexcept { + if (this != &other) { + if (handle_) { + siderust_target_free(handle_); + } + m_dir_ = std::move(other.m_dir_); + m_epoch_ = other.m_epoch_; + m_icrs_ = other.m_icrs_; + handle_ = other.handle_; + other.handle_ = nullptr; } - - /** - * @brief Find threshold-crossing events (rising / setting). - */ - std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const override { - siderust_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_crossings( - handle_, obs.to_c(), window.c_inner(), - threshold.value(), opts.to_c(), &ptr, &count), - "Target::crossings"); - return detail::crossings_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree threshold, const SearchOptions& opts = {}) const { - return crossings(obs, Period(start, end), threshold, opts); - } - - /** - * @brief Find culmination (local altitude extremum) events. - */ - std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const override { - siderust_culmination_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_culminations( - handle_, obs.to_c(), window.c_inner(), - opts.to_c(), &ptr, &count), - "Target::culminations"); - return detail::culminations_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector culminations( - const Geodetic& obs, const MJD& start, const MJD& end, - const SearchOptions& opts = {}) const { - return culminations(obs, Period(start, end), opts); - } - - // ------------------------------------------------------------------ - // Azimuth queries (implements Trackable) - // ------------------------------------------------------------------ - - /** - * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. - */ - qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const override { - double out{}; - check_status(siderust_target_azimuth_at( - handle_, obs.to_c(), mjd.value(), &out), - "Target::azimuth_at"); - return qtty::Degree(out); - } - - /** - * @brief Find epochs when the target crosses a given azimuth bearing. - */ - std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const override { - siderust_azimuth_crossing_event_t* ptr = nullptr; - uintptr_t count = 0; - check_status(siderust_target_azimuth_crossings( - handle_, obs.to_c(), window.c_inner(), - bearing.value(), opts.to_c(), &ptr, &count), - "Target::azimuth_crossings"); - return detail::az_crossings_from_c(ptr, count); - } - - /// Backward-compatible [start, end] overload. - std::vector azimuth_crossings( - const Geodetic& obs, const MJD& start, const MJD& end, - qtty::Degree bearing, const SearchOptions& opts = {}) const { - return azimuth_crossings(obs, Period(start, end), bearing, opts); - } - - /// Access the underlying C handle (advanced use). - const SiderustTarget* c_handle() const { return handle_; } - - private: - C m_dir_; - JulianDate m_epoch_; - spherical::direction::ICRS m_icrs_; - SiderustTarget* handle_ = nullptr; - - /// Build a Period vector from a tempoch_period_mjd_t* array. - static std::vector detail_periods_from_c( - tempoch_period_mjd_t* ptr, uintptr_t count) { - std::vector result; - result.reserve(count); - for (uintptr_t i = 0; i < count; ++i) { - result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); - } - siderust_periods_free(ptr, count); - return result; + return *this; + } + + // Prevent copying (the handle has unique ownership). + Target(const Target &) = delete; + Target &operator=(const Target &) = delete; + + // ------------------------------------------------------------------ + // Coordinate accessors + // ------------------------------------------------------------------ + + /// The original typed direction as supplied at construction. + const C &direction() const { return m_dir_; } + + /// Epoch of the coordinate. + JulianDate epoch() const { return m_epoch_; } + + /// The ICRS direction used for FFI calls (equals `direction()` when C is + /// already `spherical::direction::ICRS`). + const spherical::direction::ICRS &icrs_direction() const { return m_icrs_; } + + /// Right ascension — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree ra() const { + return m_dir_.ra(); + } + + /// Declination — only available for equatorial frames (RA/Dec). + template , int> = 0> + qtty::Degree dec() const { + return m_dir_.dec(); + } + + // ------------------------------------------------------------------ + // Altitude queries (implements Trackable) + // ------------------------------------------------------------------ + + /** + * @brief Compute altitude (degrees) at a given MJD instant. + * + * @note The Rust FFI returns radians; this method converts to degrees. + */ + qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { + double out{}; + check_status( + siderust_target_altitude_at(handle_, obs.to_c(), mjd.value(), &out), + "Target::altitude_at"); + return qtty::Radian(out).to(); + } + + /** + * @brief Find periods when the target is above a threshold altitude. + */ + std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_above_threshold( + handle_, obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "Target::above_threshold"); + return detail_periods_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector above_threshold(const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree threshold, + const SearchOptions &opts = {}) const { + return above_threshold(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find periods when the target is below a threshold altitude. + */ + std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + // Always pass ICRS direction to the FFI layer. + siderust_spherical_dir_t dir_c{}; + dir_c.polar_deg = m_icrs_.dec().value(); + dir_c.azimuth_deg = m_icrs_.ra().value(); + dir_c.frame = SIDERUST_FRAME_T_ICRS; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_icrs_below_threshold( + dir_c, obs.to_c(), window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "Target::below_threshold"); + return detail_periods_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector below_threshold(const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree threshold, + const SearchOptions &opts = {}) const { + return below_threshold(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find threshold-crossing events (rising / setting). + */ + std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const override { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_crossings(handle_, obs.to_c(), + window.c_inner(), threshold.value(), + opts.to_c(), &ptr, &count), + "Target::crossings"); + return detail::crossings_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector crossings(const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree threshold, + const SearchOptions &opts = {}) const { + return crossings(obs, Period(start, end), threshold, opts); + } + + /** + * @brief Find culmination (local altitude extremum) events. + */ + std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const override { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_culminations(handle_, obs.to_c(), + window.c_inner(), opts.to_c(), + &ptr, &count), + "Target::culminations"); + return detail::culminations_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector + culminations(const Geodetic &obs, const MJD &start, const MJD &end, + const SearchOptions &opts = {}) const { + return culminations(obs, Period(start, end), opts); + } + + // ------------------------------------------------------------------ + // Azimuth queries (implements Trackable) + // ------------------------------------------------------------------ + + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + qtty::Degree azimuth_at(const Geodetic &obs, const MJD &mjd) const override { + double out{}; + check_status( + siderust_target_azimuth_at(handle_, obs.to_c(), mjd.value(), &out), + "Target::azimuth_at"); + return qtty::Degree(out); + } + + /** + * @brief Find epochs when the target crosses a given azimuth bearing. + */ + std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const override { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_target_azimuth_crossings( + handle_, obs.to_c(), window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "Target::azimuth_crossings"); + return detail::az_crossings_from_c(ptr, count); + } + + /// Backward-compatible [start, end] overload. + std::vector + azimuth_crossings(const Geodetic &obs, const MJD &start, const MJD &end, + qtty::Degree bearing, + const SearchOptions &opts = {}) const { + return azimuth_crossings(obs, Period(start, end), bearing, opts); + } + + /// Access the underlying C handle (advanced use). + const SiderustTarget *c_handle() const { return handle_; } + +private: + C m_dir_; + JulianDate m_epoch_; + spherical::direction::ICRS m_icrs_; + SiderustTarget *handle_ = nullptr; + + /// Build a Period vector from a tempoch_period_mjd_t* array. + static std::vector detail_periods_from_c(tempoch_period_mjd_t *ptr, + uintptr_t count) { + std::vector result; + result.reserve(count); + for (uintptr_t i = 0; i < count; ++i) { + result.push_back(Period(MJD(ptr[i].start_mjd), MJD(ptr[i].end_mjd))); } + siderust_periods_free(ptr, count); + return result; + } }; // ============================================================================ @@ -381,13 +388,17 @@ using ICRSTarget = Target; using ICRFTarget = Target; /// Fixed direction in mean equatorial coordinates of J2000.0 (FK5). -using EquatorialMeanJ2000Target = Target; +using EquatorialMeanJ2000Target = + Target; /// Fixed direction in mean equatorial coordinates of date (precessed only). -using EquatorialMeanOfDateTarget = Target; +using EquatorialMeanOfDateTarget = + Target; -/// Fixed direction in true equatorial coordinates of date (precessed + nutated). -using EquatorialTrueOfDateTarget = Target; +/// Fixed direction in true equatorial coordinates of date (precessed + +/// nutated). +using EquatorialTrueOfDateTarget = + Target; /// Fixed direction in mean ecliptic coordinates of J2000.0. using EclipticMeanJ2000Target = Target; diff --git a/include/siderust/time.hpp b/include/siderust/time.hpp index ddef029..126f781 100644 --- a/include/siderust/time.hpp +++ b/include/siderust/time.hpp @@ -13,10 +13,10 @@ namespace siderust { -using CivilTime = tempoch::CivilTime; -using UTC = tempoch::UTC; // alias for CivilTime -using JulianDate = tempoch::JulianDate; // Time -using MJD = tempoch::MJD; // Time -using Period = tempoch::Period; +using CivilTime = tempoch::CivilTime; +using UTC = tempoch::UTC; // alias for CivilTime +using JulianDate = tempoch::JulianDate; // Time +using MJD = tempoch::MJD; // Time +using Period = tempoch::Period; } // namespace siderust diff --git a/include/siderust/trackable.hpp b/include/siderust/trackable.hpp index 5aefe37..08a236f 100644 --- a/include/siderust/trackable.hpp +++ b/include/siderust/trackable.hpp @@ -40,80 +40,86 @@ namespace siderust { /** - * @brief Abstract interface for any object whose altitude/azimuth can be computed. + * @brief Abstract interface for any object whose altitude/azimuth can be + * computed. * * This class defines the common API shared by all trackable celestial objects. * Implementations must provide altitude_at and azimuth_at at minimum; the * remaining methods have default implementations that throw if not overridden. */ class Trackable { - public: - virtual ~Trackable() = default; +public: + virtual ~Trackable() = default; - // ------------------------------------------------------------------ - // Altitude queries - // ------------------------------------------------------------------ + // ------------------------------------------------------------------ + // Altitude queries + // ------------------------------------------------------------------ - /** - * @brief Compute altitude at a given MJD instant. - * - * The return unit varies by implementation (radians for sun/moon/star, - * degrees for Target/BodyTarget). Check the concrete class documentation. - * - * @note For BodyTarget, returns radians; for Target, returns degrees. - */ - virtual qtty::Degree altitude_at(const Geodetic& obs, const MJD& mjd) const = 0; + /** + * @brief Compute altitude at a given MJD instant. + * + * The return unit varies by implementation (radians for sun/moon/star, + * degrees for Target/BodyTarget). Check the concrete class documentation. + * + * @note For BodyTarget, returns radians; for Target, returns degrees. + */ + virtual qtty::Degree altitude_at(const Geodetic &obs, + const MJD &mjd) const = 0; - /** - * @brief Find periods when the object is above a threshold altitude. - */ - virtual std::vector above_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find periods when the object is above a threshold altitude. + */ + virtual std::vector + above_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const = 0; - /** - * @brief Find periods when the object is below a threshold altitude. - */ - virtual std::vector below_threshold( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find periods when the object is below a threshold altitude. + */ + virtual std::vector + below_threshold(const Geodetic &obs, const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) const = 0; - /** - * @brief Find threshold-crossing events (rising / setting). - */ - virtual std::vector crossings( - const Geodetic& obs, const Period& window, - qtty::Degree threshold, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find threshold-crossing events (rising / setting). + */ + virtual std::vector + crossings(const Geodetic &obs, const Period &window, qtty::Degree threshold, + const SearchOptions &opts = {}) const = 0; - /** - * @brief Find culmination (local altitude extremum) events. - */ - virtual std::vector culminations( - const Geodetic& obs, const Period& window, - const SearchOptions& opts = {}) const = 0; + /** + * @brief Find culmination (local altitude extremum) events. + */ + virtual std::vector + culminations(const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) const = 0; - // ------------------------------------------------------------------ - // Azimuth queries - // ------------------------------------------------------------------ + // ------------------------------------------------------------------ + // Azimuth queries + // ------------------------------------------------------------------ - /** - * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. - */ - virtual qtty::Degree azimuth_at(const Geodetic& obs, const MJD& mjd) const = 0; + /** + * @brief Compute azimuth (degrees, N-clockwise) at a given MJD instant. + */ + virtual qtty::Degree azimuth_at(const Geodetic &obs, + const MJD &mjd) const = 0; - /** - * @brief Find epochs when the object crosses a given azimuth bearing. - */ - virtual std::vector azimuth_crossings( - const Geodetic& obs, const Period& window, - qtty::Degree bearing, const SearchOptions& opts = {}) const = 0; + /** + * @brief Find epochs when the object crosses a given azimuth bearing. + */ + virtual std::vector + azimuth_crossings(const Geodetic &obs, const Period &window, + qtty::Degree bearing, + const SearchOptions &opts = {}) const = 0; - // Non-copyable, non-movable from base - Trackable() = default; - Trackable(const Trackable&) = delete; - Trackable& operator=(const Trackable&) = delete; - Trackable(Trackable&&) = default; - Trackable& operator=(Trackable&&) = default; + // Non-copyable, non-movable from base + Trackable() = default; + Trackable(const Trackable &) = delete; + Trackable &operator=(const Trackable &) = delete; + Trackable(Trackable &&) = default; + Trackable &operator=(Trackable &&) = default; }; } // namespace siderust diff --git a/tests/main.cpp b/tests/main.cpp index 5ebbc76..4d820af 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,6 +1,6 @@ #include -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index 72b2419..6121c62 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -7,18 +7,18 @@ using namespace siderust; static const double PI = 3.14159265358979323846; class AltitudeTest : public ::testing::Test { - protected: - Geodetic obs; - MJD start; - MJD end_; - Period window{MJD(0.0), MJD(1.0)}; - - void SetUp() override { - obs = ROQUE_DE_LOS_MUCHACHOS; - start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); - end_ = start + qtty::Day(1.0); // 24 hours - window = Period(start, end_); - } +protected: + Geodetic obs; + MJD start; + MJD end_; + Period window{MJD(0.0), MJD(1.0)}; + + void SetUp() override { + obs = ROQUE_DE_LOS_MUCHACHOS; + start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); + end_ = start + qtty::Day(1.0); // 24 hours + window = Period(start, end_); + } }; // ============================================================================ @@ -26,49 +26,50 @@ class AltitudeTest : public ::testing::Test { // ============================================================================ TEST_F(AltitudeTest, SunAltitudeAt) { - qtty::Radian alt = sun::altitude_at(obs, start); - // Should be a valid radian value - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + qtty::Radian alt = sun::altitude_at(obs, start); + // Should be a valid radian value + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, SunAboveThreshold) { - // Find periods when sun > 0 deg (daytime) - auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); - EXPECT_GT(periods.size(), 0u); - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + // Find periods when sun > 0 deg (daytime) + auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); + EXPECT_GT(periods.size(), 0u); + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } TEST_F(AltitudeTest, SunBelowThreshold) { - // Astronomical night: sun < -18° - auto periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); - // In July at La Palma, astronomical night may be short but should exist - // (or possibly not if too close to solstice — accept 0+) - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + // Astronomical night: sun < -18° + auto periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); + // In July at La Palma, astronomical night may be short but should exist + // (or possibly not if too close to solstice — accept 0+) + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } TEST_F(AltitudeTest, SunCrossings) { - auto events = sun::crossings(obs, window, qtty::Degree(0.0)); - // Expect at least 1 crossing in 24h (sunrise or sunset) - EXPECT_GE(events.size(), 1u); + auto events = sun::crossings(obs, window, qtty::Degree(0.0)); + // Expect at least 1 crossing in 24h (sunrise or sunset) + EXPECT_GE(events.size(), 1u); } TEST_F(AltitudeTest, SunCulminations) { - auto events = sun::culminations(obs, window); - // At least one culmination (meridian passage) - EXPECT_GE(events.size(), 1u); + auto events = sun::culminations(obs, window); + // At least one culmination (meridian passage) + EXPECT_GE(events.size(), 1u); } TEST_F(AltitudeTest, SunAltitudePeriods) { - // Find periods when sun is between -6° and 0° (civil twilight) - auto periods = sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + // Find periods when sun is between -6° and 0° (civil twilight) + auto periods = + sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } // ============================================================================ @@ -76,17 +77,17 @@ TEST_F(AltitudeTest, SunAltitudePeriods) { // ============================================================================ TEST_F(AltitudeTest, MoonAltitudeAt) { - qtty::Radian alt = moon::altitude_at(obs, start); - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + qtty::Radian alt = moon::altitude_at(obs, start); + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, MoonAboveThreshold) { - auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); - // Moon may or may not be above horizon for this date; just no crash - for (auto& p : periods) { - EXPECT_GT(p.duration().value(), 0.0); - } + auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); + // Moon may or may not be above horizon for this date; just no crash + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } // ============================================================================ @@ -94,17 +95,18 @@ TEST_F(AltitudeTest, MoonAboveThreshold) { // ============================================================================ TEST_F(AltitudeTest, StarAltitudeAt) { - const auto& vega = VEGA; - qtty::Radian alt = star_altitude::altitude_at(vega, obs, start); - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + const auto &vega = VEGA; + qtty::Radian alt = star_altitude::altitude_at(vega, obs, start); + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, StarAboveThreshold) { - const auto& vega = VEGA; - auto periods = star_altitude::above_threshold(vega, obs, window, qtty::Degree(30.0)); - // Vega should be well above 30° from La Palma in July - EXPECT_GT(periods.size(), 0u); + const auto &vega = VEGA; + auto periods = + star_altitude::above_threshold(vega, obs, window, qtty::Degree(30.0)); + // Vega should be well above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); } // ============================================================================ @@ -112,17 +114,19 @@ TEST_F(AltitudeTest, StarAboveThreshold) { // ============================================================================ TEST_F(AltitudeTest, IcrsAltitudeAt) { - const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), qtty::Degree(38.78)); - qtty::Radian alt = icrs_altitude::altitude_at(vega_icrs, obs, start); - EXPECT_GT(alt.value(), -PI / 2.0); - EXPECT_LT(alt.value(), PI / 2.0); + const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), + qtty::Degree(38.78)); + qtty::Radian alt = icrs_altitude::altitude_at(vega_icrs, obs, start); + EXPECT_GT(alt.value(), -PI / 2.0); + EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, IcrsAboveThreshold) { - const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), qtty::Degree(38.78)); - auto periods = icrs_altitude::above_threshold( - vega_icrs, obs, window, qtty::Degree(30.0)); - EXPECT_GT(periods.size(), 0u); + const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), + qtty::Degree(38.78)); + auto periods = icrs_altitude::above_threshold(vega_icrs, obs, window, + qtty::Degree(30.0)); + EXPECT_GT(periods.size(), 0u); } // ============================================================================ @@ -131,62 +135,62 @@ TEST_F(AltitudeTest, IcrsAboveThreshold) { // Vega ICRS coordinates (J2000): RA=279.2348°, Dec=+38.7836° TEST_F(AltitudeTest, ICRSTargetAltitudeAt) { - ICRSTarget vega{ spherical::direction::ICRS{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - // altitude_at returns qtty::Degree (radian/degree bug-fix verification) - qtty::Degree alt = vega.altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + ICRSTarget vega{ + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + // altitude_at returns qtty::Degree (radian/degree bug-fix verification) + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST_F(AltitudeTest, ICRSTargetAboveThreshold) { - ICRSTarget vega{ spherical::direction::ICRS{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); - // Vega should rise above 30° from La Palma in July - EXPECT_GT(periods.size(), 0u); + ICRSTarget vega{ + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); + // Vega should rise above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); } TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { - ICRSTarget vega{ spherical::direction::ICRS{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); - EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); - // epoch defaults to J2000 - EXPECT_NEAR(vega.epoch().value(), 2451545.0, 1e-3); - // icrs_direction is the same for an ICRS Target - EXPECT_NEAR(vega.icrs_direction().ra().value(), 279.23, 1e-9); + ICRSTarget vega{ + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); + EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); + // epoch defaults to J2000 + EXPECT_NEAR(vega.epoch().value(), 2451545.0, 1e-3); + // icrs_direction is the same for an ICRS Target + EXPECT_NEAR(vega.icrs_direction().ra().value(), 279.23, 1e-9); } TEST_F(AltitudeTest, ICRSTargetPolymorphic) { - // Verify Target is usable through the Trackable interface - std::unique_ptr t = std::make_unique( - spherical::direction::ICRS{ qtty::Degree(279.23), qtty::Degree(38.78) }); - qtty::Degree alt = t->altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + // Verify Target is usable through the Trackable interface + std::unique_ptr t = std::make_unique( + spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}); + qtty::Degree alt = t->altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { - // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° - EclipticMeanJ2000Target ec{ spherical::direction::EclipticMeanJ2000{ - qtty::Degree(279.6), qtty::Degree(61.8) } }; - // ecl direction retained on the C++ side - EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); - EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); - // ICRS ra/dec computed at construction and accessible - EXPECT_GT(ec.icrs_direction().ra().value(), 0.0); - EXPECT_LT(ec.icrs_direction().ra().value(), 360.0); - // altitude should be a valid degree value - qtty::Degree alt = ec.altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° + EclipticMeanJ2000Target ec{spherical::direction::EclipticMeanJ2000{ + qtty::Degree(279.6), qtty::Degree(61.8)}}; + // ecl direction retained on the C++ side + EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); + EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); + // ICRS ra/dec computed at construction and accessible + EXPECT_GT(ec.icrs_direction().ra().value(), 0.0); + EXPECT_LT(ec.icrs_direction().ra().value(), 360.0); + // altitude should be a valid degree value + qtty::Degree alt = ec.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST_F(AltitudeTest, EquatorialMeanJ2000TargetAltitudeAt) { - EquatorialMeanJ2000Target vega{ spherical::direction::EquatorialMeanJ2000{ - qtty::Degree(279.23), qtty::Degree(38.78) } }; - qtty::Degree alt = vega.altitude_at(obs, start); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + EquatorialMeanJ2000Target vega{spherical::direction::EquatorialMeanJ2000{ + qtty::Degree(279.23), qtty::Degree(38.78)}}; + qtty::Degree alt = vega.altitude_at(obs, start); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index 530617e..c4cf2a5 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -8,51 +8,50 @@ using namespace siderust; // ============================================================================ TEST(Bodies, StarCatalogVega) { - const auto& vega = VEGA; - EXPECT_EQ(vega.name(), "Vega"); - EXPECT_NEAR(vega.distance_ly(), 25.0, 1.0); - EXPECT_GT(vega.luminosity_solar(), 1.0); + const auto &vega = VEGA; + EXPECT_EQ(vega.name(), "Vega"); + EXPECT_NEAR(vega.distance_ly(), 25.0, 1.0); + EXPECT_GT(vega.luminosity_solar(), 1.0); } TEST(Bodies, StarCatalogSirius) { - const auto& sirius = SIRIUS; - EXPECT_EQ(sirius.name(), "Sirius"); - EXPECT_NEAR(sirius.distance_ly(), 8.6, 0.5); + const auto &sirius = SIRIUS; + EXPECT_EQ(sirius.name(), "Sirius"); + EXPECT_NEAR(sirius.distance_ly(), 8.6, 0.5); } TEST(Bodies, StarCatalogUnknownThrows) { - EXPECT_THROW(Star::catalog("NONEXISTENT"), UnknownStarError); + EXPECT_THROW(Star::catalog("NONEXISTENT"), UnknownStarError); } TEST(Bodies, StarMoveSemantics) { - auto s1 = Star::catalog("POLARIS"); - EXPECT_TRUE(static_cast(s1)); + auto s1 = Star::catalog("POLARIS"); + EXPECT_TRUE(static_cast(s1)); - auto s2 = std::move(s1); - EXPECT_TRUE(static_cast(s2)); - // s1 is now empty (moved-from) + auto s2 = std::move(s1); + EXPECT_TRUE(static_cast(s2)); + // s1 is now empty (moved-from) } TEST(Bodies, StarCreate) { - auto s = Star::create( - "TestStar", - 100.0, // distance_ly - 1.0, // mass_solar - 1.0, // radius_solar - 1.0, // luminosity_solar - 180.0, // ra_deg - 45.0, // dec_deg - 2451545.0 // epoch_jd (J2000) - ); - EXPECT_EQ(s.name(), "TestStar"); - EXPECT_NEAR(s.distance_ly(), 100.0, 1e-6); + auto s = Star::create("TestStar", + 100.0, // distance_ly + 1.0, // mass_solar + 1.0, // radius_solar + 1.0, // luminosity_solar + 180.0, // ra_deg + 45.0, // dec_deg + 2451545.0 // epoch_jd (J2000) + ); + EXPECT_EQ(s.name(), "TestStar"); + EXPECT_NEAR(s.distance_ly(), 100.0, 1e-6); } TEST(Bodies, StarCreateWithProperMotion) { - ProperMotion pm(0.001, -0.002, RaConvention::MuAlphaStar); - auto s = Star::create("PMStar", 50.0, 1.0, 1.0, 1.0, - 100.0, 30.0, 2451545.0, pm); - EXPECT_EQ(s.name(), "PMStar"); + ProperMotion pm(0.001, -0.002, RaConvention::MuAlphaStar); + auto s = + Star::create("PMStar", 50.0, 1.0, 1.0, 1.0, 100.0, 30.0, 2451545.0, pm); + EXPECT_EQ(s.name(), "PMStar"); } // ============================================================================ @@ -60,28 +59,28 @@ TEST(Bodies, StarCreateWithProperMotion) { // ============================================================================ TEST(Bodies, PlanetEarth) { - auto e = EARTH; - EXPECT_NEAR(e.mass_kg, 5.972e24, 0.01e24); - EXPECT_NEAR(e.radius_km, 6371.0, 10.0); - EXPECT_NEAR(e.orbit.semi_major_axis_au, 1.0, 0.01); + auto e = EARTH; + EXPECT_NEAR(e.mass_kg, 5.972e24, 0.01e24); + EXPECT_NEAR(e.radius_km, 6371.0, 10.0); + EXPECT_NEAR(e.orbit.semi_major_axis_au, 1.0, 0.01); } TEST(Bodies, PlanetMars) { - auto m = MARS; - EXPECT_GT(m.mass_kg, 0); - EXPECT_NEAR(m.orbit.semi_major_axis_au, 1.524, 0.01); + auto m = MARS; + EXPECT_GT(m.mass_kg, 0); + EXPECT_NEAR(m.orbit.semi_major_axis_au, 1.524, 0.01); } TEST(Bodies, AllPlanets) { - // Ensure all static constants are populated. - EXPECT_GT(MERCURY.mass_kg, 0.0); - EXPECT_GT(VENUS.mass_kg, 0.0); - EXPECT_GT(EARTH.mass_kg, 0.0); - EXPECT_GT(MARS.mass_kg, 0.0); - EXPECT_GT(JUPITER.mass_kg, 0.0); - EXPECT_GT(SATURN.mass_kg, 0.0); - EXPECT_GT(URANUS.mass_kg, 0.0); - EXPECT_GT(NEPTUNE.mass_kg, 0.0); + // Ensure all static constants are populated. + EXPECT_GT(MERCURY.mass_kg, 0.0); + EXPECT_GT(VENUS.mass_kg, 0.0); + EXPECT_GT(EARTH.mass_kg, 0.0); + EXPECT_GT(MARS.mass_kg, 0.0); + EXPECT_GT(JUPITER.mass_kg, 0.0); + EXPECT_GT(SATURN.mass_kg, 0.0); + EXPECT_GT(URANUS.mass_kg, 0.0); + EXPECT_GT(NEPTUNE.mass_kg, 0.0); } // ============================================================================ @@ -89,94 +88,93 @@ TEST(Bodies, AllPlanets) { // ============================================================================ TEST(Bodies, BodyTargetSunAltitude) { - BodyTarget sun(Body::Sun); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto alt = sun.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = sun.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST(Bodies, BodyTargetMarsAltitude) { - BodyTarget mars(Body::Mars); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto alt = mars.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + BodyTarget mars(Body::Mars); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = mars.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST(Bodies, BodyTargetAllBodiesAltitude) { - auto obs = geodetic(-17.89, 28.76, 2326.0); // ORM - auto mjd = MJD(60000.5); - std::vector all = { - Body::Sun, Body::Moon, Body::Mercury, Body::Venus, - Body::Mars, Body::Jupiter, Body::Saturn, Body::Uranus, Body::Neptune - }; - for (auto b : all) { - BodyTarget bt(b); - auto alt = bt.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - } + auto obs = geodetic(-17.89, 28.76, 2326.0); // ORM + auto mjd = MJD(60000.5); + std::vector all = {Body::Sun, Body::Moon, Body::Mercury, + Body::Venus, Body::Mars, Body::Jupiter, + Body::Saturn, Body::Uranus, Body::Neptune}; + for (auto b : all) { + BodyTarget bt(b); + auto alt = bt.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } } TEST(Bodies, BodyTargetAzimuth) { - BodyTarget sun(Body::Sun); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto az = sun.azimuth_at(obs, mjd); - EXPECT_GE(az.value(), 0.0); - EXPECT_LT(az.value(), 360.0); + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = sun.azimuth_at(obs, mjd); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); } TEST(Bodies, BodyTargetJupiterAzimuth) { - BodyTarget jup(Body::Jupiter); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto az = jup.azimuth_at(obs, mjd); - EXPECT_TRUE(std::isfinite(az.value())); - EXPECT_GE(az.value(), 0.0); - EXPECT_LT(az.value(), 360.0); + BodyTarget jup(Body::Jupiter); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto az = jup.azimuth_at(obs, mjd); + EXPECT_TRUE(std::isfinite(az.value())); + EXPECT_GE(az.value(), 0.0); + EXPECT_LT(az.value(), 360.0); } TEST(Bodies, BodyTargetAboveThreshold) { - BodyTarget sun(Body::Sun); - auto obs = geodetic(2.35, 48.85, 35.0); - auto window = Period(MJD(60000.0), MJD(60001.0)); - auto periods = sun.above_threshold(obs, window, qtty::Degree(0.0)); - // Sun should be above horizon for some portion of the day - EXPECT_GT(periods.size(), 0u); + BodyTarget sun(Body::Sun); + auto obs = geodetic(2.35, 48.85, 35.0); + auto window = Period(MJD(60000.0), MJD(60001.0)); + auto periods = sun.above_threshold(obs, window, qtty::Degree(0.0)); + // Sun should be above horizon for some portion of the day + EXPECT_GT(periods.size(), 0u); } TEST(Bodies, BodyTargetPolymorphic) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); - std::vector> targets; - targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(Body::Mars)); + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(Body::Mars)); - for (const auto& t : targets) { - auto alt = t->altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - } + for (const auto &t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } } TEST(Bodies, BodyNamespaceAltitudeAt) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto rad = body::altitude_at(Body::Saturn, obs, mjd); - EXPECT_TRUE(std::isfinite(rad.value())); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::altitude_at(Body::Saturn, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); } TEST(Bodies, BodyNamespaceAzimuthAt) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto rad = body::azimuth_at(Body::Venus, obs, mjd); - EXPECT_TRUE(std::isfinite(rad.value())); - EXPECT_GE(rad.value(), 0.0); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto rad = body::azimuth_at(Body::Venus, obs, mjd); + EXPECT_TRUE(std::isfinite(rad.value())); + EXPECT_GE(rad.value(), 0.0); } // ============================================================================ @@ -184,26 +182,26 @@ TEST(Bodies, BodyNamespaceAzimuthAt) { // ============================================================================ TEST(Bodies, StarTargetAltitude) { - const auto& vega = VEGA; - StarTarget st(vega); - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); - auto alt = st.altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - EXPECT_GT(alt.value(), -90.0); - EXPECT_LT(alt.value(), 90.0); + const auto &vega = VEGA; + StarTarget st(vega); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); + auto alt = st.altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + EXPECT_GT(alt.value(), -90.0); + EXPECT_LT(alt.value(), 90.0); } TEST(Bodies, StarTargetPolymorphicWithBodyTarget) { - auto obs = geodetic(2.35, 48.85, 35.0); - auto mjd = MJD(60000.5); + auto obs = geodetic(2.35, 48.85, 35.0); + auto mjd = MJD(60000.5); - std::vector> targets; - targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(VEGA)); + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(VEGA)); - for (const auto& t : targets) { - auto alt = t->altitude_at(obs, mjd); - EXPECT_TRUE(std::isfinite(alt.value())); - } + for (const auto &t : targets) { + auto alt = t->altitude_at(obs, mjd); + EXPECT_TRUE(std::isfinite(alt.value())); + } } diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 1e6e9d5..5566303 100644 --- a/tests/test_coordinates.cpp +++ b/tests/test_coordinates.cpp @@ -10,124 +10,137 @@ using namespace siderust; // ============================================================================ TEST(TypedCoordinates, AliasNamespaces) { - static_assert(std::is_same_v>); - static_assert(std::is_same_v>); - static_assert(std::is_same_v< - spherical::position::ICRS, - spherical::Position>); - static_assert(std::is_same_v< - cartesian::position::ECEF, - cartesian::Position>); + static_assert(std::is_same_v>); + static_assert( + std::is_same_v>); + static_assert(std::is_same_v, + spherical::Position>); + static_assert( + std::is_same_v< + cartesian::position::ECEF, + cartesian::Position>); } TEST(TypedCoordinates, IcrsDirToEcliptic) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS vega(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + auto jd = JulianDate::J2000(); - // Compile-time typed transform: ICRS -> EclipticMeanJ2000 - auto ecl = vega.to_frame(jd); + // Compile-time typed transform: ICRS -> EclipticMeanJ2000 + auto ecl = vega.to_frame(jd); - // Result is statically typed as Direction - static_assert(std::is_same_v>, - "to_frame must return Direction"); + // Result is statically typed as Direction + static_assert( + std::is_same_v>, + "to_frame must return Direction"); - EXPECT_NEAR(ecl.lat().value(), 61.7, 0.5); + EXPECT_NEAR(ecl.lat().value(), 61.7, 0.5); } TEST(TypedCoordinates, IcrsDirRoundtrip) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - auto ecl = icrs.to_frame(jd); - auto back = ecl.to_frame(jd); + auto ecl = icrs.to_frame(jd); + auto back = ecl.to_frame(jd); - static_assert(std::is_same_v); - EXPECT_NEAR(back.ra().value(), 100.0, 1e-4); - EXPECT_NEAR(back.dec().value(), 30.0, 1e-4); + static_assert(std::is_same_v); + EXPECT_NEAR(back.ra().value(), 100.0, 1e-4); + EXPECT_NEAR(back.dec().value(), 30.0, 1e-4); } TEST(TypedCoordinates, ToShorthand) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS icrs(qtty::Degree(100.0), qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - // .to(jd) is a shorthand for .to_frame(jd) - auto ecl = icrs.to(jd); - static_assert(std::is_same_v>); - EXPECT_NEAR(ecl.lat().value(), 30.0, 30.0); // sanity check — something was computed + // .to(jd) is a shorthand for .to_frame(jd) + auto ecl = icrs.to(jd); + static_assert( + std::is_same_v>); + EXPECT_NEAR(ecl.lat().value(), 30.0, + 30.0); // sanity check — something was computed } TEST(TypedCoordinates, IcrsDirToHorizontal) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); - auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - auto obs = ROQUE_DE_LOS_MUCHACHOS; + spherical::direction::ICRS vega(qtty::Degree(279.23473), + qtty::Degree(38.78369)); + auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + auto obs = ROQUE_DE_LOS_MUCHACHOS; - auto hor = vega.to_horizontal(jd, obs); + auto hor = vega.to_horizontal(jd, obs); - static_assert(std::is_same_v>); - EXPECT_GT(hor.altitude().value(), -90.0); - EXPECT_LT(hor.altitude().value(), 90.0); + static_assert( + std::is_same_v>); + EXPECT_GT(hor.altitude().value(), -90.0); + EXPECT_LT(hor.altitude().value(), 90.0); } TEST(TypedCoordinates, EquatorialToIcrs) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::EquatorialMeanJ2000 eq(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + spherical::direction::EquatorialMeanJ2000 eq(qtty::Degree(100.0), + qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - auto icrs = eq.to_frame(jd); - static_assert(std::is_same_v); + auto icrs = eq.to_frame(jd); + static_assert(std::is_same_v); - // Should be close to input (EquatorialMeanJ2000 ≈ ICRS at J2000) - EXPECT_NEAR(icrs.ra().value(), 100.0, 0.1); - EXPECT_NEAR(icrs.dec().value(), 30.0, 0.1); + // Should be close to input (EquatorialMeanJ2000 ≈ ICRS at J2000) + EXPECT_NEAR(icrs.ra().value(), 100.0, 0.1); + EXPECT_NEAR(icrs.dec().value(), 30.0, 0.1); } TEST(TypedCoordinates, MultiHopTransform) { - using namespace siderust::frames; + using namespace siderust::frames; - // EquatorialMeanOfDate -> EquatorialTrueOfDate (through hub) - spherical::Direction mean_od(qtty::Degree(100.0), qtty::Degree(30.0)); - auto jd = JulianDate::J2000(); + // EquatorialMeanOfDate -> EquatorialTrueOfDate (through hub) + spherical::Direction mean_od(qtty::Degree(100.0), + qtty::Degree(30.0)); + auto jd = JulianDate::J2000(); - auto true_od = mean_od.to_frame(jd); - static_assert(std::is_same_v>); + auto true_od = mean_od.to_frame(jd); + static_assert(std::is_same_v>); - // At J2000, nutation is small — should be close - EXPECT_NEAR(true_od.ra().value(), 100.0, 0.1); - EXPECT_NEAR(true_od.dec().value(), 30.0, 0.1); + // At J2000, nutation is small — should be close + EXPECT_NEAR(true_od.ra().value(), 100.0, 0.1); + EXPECT_NEAR(true_od.dec().value(), 30.0, 0.1); } TEST(TypedCoordinates, SameFrameIdentity) { - using namespace siderust::frames; + using namespace siderust::frames; - spherical::direction::ICRS icrs(qtty::Degree(123.456), qtty::Degree(-45.678)); - auto jd = JulianDate::J2000(); + spherical::direction::ICRS icrs(qtty::Degree(123.456), qtty::Degree(-45.678)); + auto jd = JulianDate::J2000(); - auto same = icrs.to_frame(jd); - EXPECT_DOUBLE_EQ(same.ra().value(), 123.456); - EXPECT_DOUBLE_EQ(same.dec().value(), -45.678); + auto same = icrs.to_frame(jd); + EXPECT_DOUBLE_EQ(same.ra().value(), 123.456); + EXPECT_DOUBLE_EQ(same.dec().value(), -45.678); } TEST(TypedCoordinates, QttyDegreeAccessors) { - spherical::direction::ICRS d(qtty::Degree(123.456), qtty::Degree(-45.678)); + spherical::direction::ICRS d(qtty::Degree(123.456), qtty::Degree(-45.678)); - // Frame-specific getters for ICRS. - qtty::Degree ra = d.ra(); - qtty::Degree dec = d.dec(); - EXPECT_DOUBLE_EQ(ra.value(), 123.456); - EXPECT_DOUBLE_EQ(dec.value(), -45.678); + // Frame-specific getters for ICRS. + qtty::Degree ra = d.ra(); + qtty::Degree dec = d.dec(); + EXPECT_DOUBLE_EQ(ra.value(), 123.456); + EXPECT_DOUBLE_EQ(dec.value(), -45.678); - // Convert to radians through qtty - qtty::Radian ra_rad = ra.to(); - EXPECT_NEAR(ra_rad.value(), 123.456 * M_PI / 180.0, 1e-10); + // Convert to radians through qtty + qtty::Radian ra_rad = ra.to(); + EXPECT_NEAR(ra_rad.value(), 123.456 * M_PI / 180.0, 1e-10); } // ============================================================================ @@ -135,21 +148,21 @@ TEST(TypedCoordinates, QttyDegreeAccessors) { // ============================================================================ TEST(TypedCoordinates, GeodeticQttyFields) { - auto obs = ROQUE_DE_LOS_MUCHACHOS; + auto obs = ROQUE_DE_LOS_MUCHACHOS; - // Exercise the qtty::Degree / qtty::Meter fields - qtty::Degree lon = obs.lon; - qtty::Degree lat = obs.lat; - qtty::Meter h = obs.height; + // Exercise the qtty::Degree / qtty::Meter fields + qtty::Degree lon = obs.lon; + qtty::Degree lat = obs.lat; + qtty::Meter h = obs.height; - EXPECT_NE(lon.value(), 0.0); - EXPECT_NE(lat.value(), 0.0); - EXPECT_GT(h.value(), 0.0); + EXPECT_NE(lon.value(), 0.0); + EXPECT_NE(lat.value(), 0.0); + EXPECT_GT(h.value(), 0.0); - // Accessors are the fields themselves - EXPECT_EQ(obs.lon, lon); - EXPECT_EQ(obs.lat, lat); - EXPECT_EQ(obs.height, h); + // Accessors are the fields themselves + EXPECT_EQ(obs.lon, lon); + EXPECT_EQ(obs.lat, lat); + EXPECT_EQ(obs.height, h); } // ============================================================================ @@ -157,28 +170,31 @@ TEST(TypedCoordinates, GeodeticQttyFields) { // ============================================================================ TEST(TypedCoordinates, GeodeticToCartesianEcef) { - auto geo = geodetic(0.0, 0.0, 0.0); - auto cart = geodetic_to_cartesian_ecef(geo); + auto geo = geodetic(0.0, 0.0, 0.0); + auto cart = geodetic_to_cartesian_ecef(geo); - // Typed return: cartesian::Position - static_assert(std::is_same_v>); + // Typed return: cartesian::Position + static_assert( + std::is_same_v>); - EXPECT_NEAR(cart.x().value(), 6378137.0, 1.0); - EXPECT_NEAR(cart.y().value(), 0.0, 1.0); - EXPECT_NEAR(cart.z().value(), 0.0, 1.0); + EXPECT_NEAR(cart.x().value(), 6378137.0, 1.0); + EXPECT_NEAR(cart.y().value(), 0.0, 1.0); + EXPECT_NEAR(cart.z().value(), 0.0, 1.0); } TEST(TypedCoordinates, GeodeticToCartesianMember) { - auto geo = geodetic(0.0, 0.0, 0.0); + auto geo = geodetic(0.0, 0.0, 0.0); - auto ecef_m = geo.to_cartesian(); - auto ecef_km = geo.to_cartesian(); + auto ecef_m = geo.to_cartesian(); + auto ecef_km = geo.to_cartesian(); - static_assert(std::is_same_v>); - static_assert(std::is_same_v< - decltype(ecef_km), - cartesian::Position>); + static_assert( + std::is_same_v>); + static_assert( + std::is_same_v>); - EXPECT_NEAR(ecef_m.x().value(), 6378137.0, 1.0); - EXPECT_NEAR(ecef_km.x().value(), 6378.137, 1e-3); + EXPECT_NEAR(ecef_m.x().value(), 6378137.0, 1.0); + EXPECT_NEAR(ecef_km.x().value(), 6378.137, 1e-3); } diff --git a/tests/test_ephemeris.cpp b/tests/test_ephemeris.cpp index fd1489d..73c38d7 100644 --- a/tests/test_ephemeris.cpp +++ b/tests/test_ephemeris.cpp @@ -9,38 +9,42 @@ using namespace siderust; // ============================================================================ TEST(Ephemeris, EarthHeliocentric) { - auto jd = JulianDate::J2000(); - auto pos = ephemeris::earth_heliocentric(jd); - - // Compile-time type checks - static_assert(std::is_same_v< - decltype(pos), - cartesian::position::EclipticMeanJ2000>); - static_assert(std::is_same_v); - - // Value check — distance should be ~1 AU - double r = std::sqrt(pos.x().value() * pos.x().value() + pos.y().value() * pos.y().value() + pos.z().value() * pos.z().value()); - EXPECT_NEAR(r, 1.0, 0.02); - - // Unit conversion: AU -> Kilometer (on individual component) - qtty::Kilometer x_km = pos.comp_x.to(); - // x_km is one component, not the full distance; just verify conversion works - EXPECT_NEAR(x_km.value(), pos.x().value() * 1.495978707e8, 1e3); - - // Total distance in km should be ~1 AU ≈ 149.6M km - double r_km = r * 1.495978707e8; - EXPECT_NEAR(r_km, 1.496e8, 3e6); + auto jd = JulianDate::J2000(); + auto pos = ephemeris::earth_heliocentric(jd); + + // Compile-time type checks + static_assert( + std::is_same_v>); + static_assert(std::is_same_v); + + // Value check — distance should be ~1 AU + double r = std::sqrt(pos.x().value() * pos.x().value() + + pos.y().value() * pos.y().value() + + pos.z().value() * pos.z().value()); + EXPECT_NEAR(r, 1.0, 0.02); + + // Unit conversion: AU -> Kilometer (on individual component) + qtty::Kilometer x_km = pos.comp_x.to(); + // x_km is one component, not the full distance; just verify conversion works + EXPECT_NEAR(x_km.value(), pos.x().value() * 1.495978707e8, 1e3); + + // Total distance in km should be ~1 AU ≈ 149.6M km + double r_km = r * 1.495978707e8; + EXPECT_NEAR(r_km, 1.496e8, 3e6); } TEST(Ephemeris, MoonGeocentric) { - auto jd = JulianDate::J2000(); - auto pos = ephemeris::moon_geocentric(jd); - - static_assert(std::is_same_v< - decltype(pos), - cartesian::position::MoonGeocentric>); - static_assert(std::is_same_v); - - double r = std::sqrt(pos.x().value() * pos.x().value() + pos.y().value() * pos.y().value() + pos.z().value() * pos.z().value()); - EXPECT_NEAR(r, 384400.0, 25000.0); + auto jd = JulianDate::J2000(); + auto pos = ephemeris::moon_geocentric(jd); + + static_assert( + std::is_same_v>); + static_assert(std::is_same_v); + + double r = std::sqrt(pos.x().value() * pos.x().value() + + pos.y().value() * pos.y().value() + + pos.z().value() * pos.z().value()); + EXPECT_NEAR(r, 384400.0, 25000.0); } diff --git a/tests/test_observatories.cpp b/tests/test_observatories.cpp index bd673b4..bbd9fda 100644 --- a/tests/test_observatories.cpp +++ b/tests/test_observatories.cpp @@ -4,36 +4,36 @@ using namespace siderust; TEST(Observatories, RoqueDeLos) { - auto obs = ROQUE_DE_LOS_MUCHACHOS; - // La Palma, approx lon=-17.88, lat=28.76 - EXPECT_NEAR(obs.lon.value(), -17.88, 0.1); - EXPECT_NEAR(obs.lat.value(), 28.76, 0.1); - EXPECT_GT(obs.height.value(), 2000.0); + auto obs = ROQUE_DE_LOS_MUCHACHOS; + // La Palma, approx lon=-17.88, lat=28.76 + EXPECT_NEAR(obs.lon.value(), -17.88, 0.1); + EXPECT_NEAR(obs.lat.value(), 28.76, 0.1); + EXPECT_GT(obs.height.value(), 2000.0); } TEST(Observatories, ElParanal) { - auto obs = EL_PARANAL; - EXPECT_LT(obs.lon.value(), 0.0); - EXPECT_LT(obs.lat.value(), 0.0); // Southern hemisphere - EXPECT_GT(obs.height.value(), 2000.0); + auto obs = EL_PARANAL; + EXPECT_LT(obs.lon.value(), 0.0); + EXPECT_LT(obs.lat.value(), 0.0); // Southern hemisphere + EXPECT_GT(obs.height.value(), 2000.0); } TEST(Observatories, MaunaKea) { - auto obs = MAUNA_KEA; - EXPECT_NEAR(obs.lon.value(), -155.47, 0.1); - EXPECT_NEAR(obs.lat.value(), 19.82, 0.1); - EXPECT_GT(obs.height.value(), 4000.0); + auto obs = MAUNA_KEA; + EXPECT_NEAR(obs.lon.value(), -155.47, 0.1); + EXPECT_NEAR(obs.lat.value(), 19.82, 0.1); + EXPECT_GT(obs.height.value(), 4000.0); } TEST(Observatories, LaSilla) { - auto obs = LA_SILLA_OBSERVATORY; - EXPECT_LT(obs.lon.value(), 0.0); - EXPECT_LT(obs.lat.value(), 0.0); + auto obs = LA_SILLA_OBSERVATORY; + EXPECT_LT(obs.lon.value(), 0.0); + EXPECT_LT(obs.lat.value(), 0.0); } TEST(Observatories, CustomGeodetic) { - auto g = geodetic(-3.7, 40.4, 667.0); - EXPECT_NEAR(g.lon.value(), -3.7, 1e-10); - EXPECT_NEAR(g.lat.value(), 40.4, 1e-10); - EXPECT_NEAR(g.height.value(), 667.0, 1e-10); + auto g = geodetic(-3.7, 40.4, 667.0); + EXPECT_NEAR(g.lon.value(), -3.7, 1e-10); + EXPECT_NEAR(g.lat.value(), 40.4, 1e-10); + EXPECT_NEAR(g.height.value(), 667.0, 1e-10); } diff --git a/tests/test_time.cpp b/tests/test_time.cpp index a2454c3..d88f13a 100644 --- a/tests/test_time.cpp +++ b/tests/test_time.cpp @@ -8,36 +8,36 @@ using namespace siderust; // ============================================================================ TEST(Time, JulianDateJ2000) { - auto jd = JulianDate::J2000(); - EXPECT_DOUBLE_EQ(jd.value(), 2451545.0); + auto jd = JulianDate::J2000(); + EXPECT_DOUBLE_EQ(jd.value(), 2451545.0); } TEST(Time, JulianDateFromUtc) { - // UTC noon 2000-01-01 differs from J2000 (TT) by ~64s leap seconds - auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); - EXPECT_NEAR(jd.value(), 2451545.0, 0.001); + // UTC noon 2000-01-01 differs from J2000 (TT) by ~64s leap seconds + auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); + EXPECT_NEAR(jd.value(), 2451545.0, 0.001); } TEST(Time, JulianDateRoundtripUtc) { - UTC original(2026, 7, 15, 22, 0, 0); - auto jd = JulianDate::from_utc(original); - auto utc = jd.to_utc(); - EXPECT_EQ(utc.year, 2026); - EXPECT_EQ(utc.month, 7); - EXPECT_EQ(utc.day, 15); - // Hour may differ slightly due to TT/UTC offset - EXPECT_NEAR(utc.hour, 22, 1); + UTC original(2026, 7, 15, 22, 0, 0); + auto jd = JulianDate::from_utc(original); + auto utc = jd.to_utc(); + EXPECT_EQ(utc.year, 2026); + EXPECT_EQ(utc.month, 7); + EXPECT_EQ(utc.day, 15); + // Hour may differ slightly due to TT/UTC offset + EXPECT_NEAR(utc.hour, 22, 1); } TEST(Time, JulianDateArithmetic) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + qtty::Day(365.25); - EXPECT_NEAR((jd2 - jd1).value(), 365.25, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + qtty::Day(365.25); + EXPECT_NEAR((jd2 - jd1).value(), 365.25, 1e-10); } TEST(Time, JulianCenturies) { - auto jd = JulianDate::J2000(); - EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-10); + auto jd = JulianDate::J2000(); + EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-10); } // ============================================================================ @@ -45,16 +45,16 @@ TEST(Time, JulianCenturies) { // ============================================================================ TEST(Time, MjdFromJd) { - auto jd = JulianDate::J2000(); - auto mjd = MJD::from_jd(jd); - EXPECT_NEAR(mjd.value(), jd.to_mjd(), 1e-10); + auto jd = JulianDate::J2000(); + auto mjd = MJD::from_jd(jd); + EXPECT_NEAR(mjd.value(), jd.to_mjd(), 1e-10); } TEST(Time, MjdRoundtrip) { - auto mjd1 = MJD(60200.0); - auto jd = mjd1.to_jd(); - auto mjd2 = MJD::from_jd(jd); - EXPECT_NEAR(mjd1.value(), mjd2.value(), 1e-10); + auto mjd1 = MJD(60200.0); + auto jd = mjd1.to_jd(); + auto mjd2 = MJD::from_jd(jd); + EXPECT_NEAR(mjd1.value(), mjd2.value(), 1e-10); } // ============================================================================ @@ -62,26 +62,26 @@ TEST(Time, MjdRoundtrip) { // ============================================================================ TEST(Time, PeriodDuration) { - Period p(MJD(60200.0), MJD(60201.0)); - EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); } TEST(Time, PeriodIntersection) { - Period a(MJD(60200.0), MJD(60202.0)); - Period b(MJD(60201.0), MJD(60203.0)); - auto c = a.intersection(b); - EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); - EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); + Period a(MJD(60200.0), MJD(60202.0)); + Period b(MJD(60201.0), MJD(60203.0)); + auto c = a.intersection(b); + EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); + EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); } TEST(Time, PeriodNoIntersection) { - Period a(MJD(60200.0), MJD(60201.0)); - Period b(MJD(60202.0), MJD(60203.0)); - EXPECT_THROW(a.intersection(b), tempoch::NoIntersectionError); + Period a(MJD(60200.0), MJD(60201.0)); + Period b(MJD(60202.0), MJD(60203.0)); + EXPECT_THROW(a.intersection(b), tempoch::NoIntersectionError); } TEST(Time, PeriodInvalidThrows) { - EXPECT_THROW(Period(MJD(60203.0), MJD(60200.0)), tempoch::InvalidPeriodError); + EXPECT_THROW(Period(MJD(60203.0), MJD(60200.0)), tempoch::InvalidPeriodError); } // ============================================================================ @@ -89,47 +89,47 @@ TEST(Time, PeriodInvalidThrows) { // ============================================================================ TEST(Time, JulianCenturiesQty) { - auto jd = JulianDate::J2000(); - auto jc = jd.julian_centuries_qty(); - EXPECT_NEAR(jc.value(), 0.0, 1e-10); - EXPECT_EQ(jc.unit_id(), UNIT_ID_JULIAN_CENTURY); + auto jd = JulianDate::J2000(); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 0.0, 1e-10); + EXPECT_EQ(jc.unit_id(), UNIT_ID_JULIAN_CENTURY); } TEST(Time, JulianCenturiesQtyNonZero) { - // 36525 days ≈ 1 Julian century - auto jd = JulianDate(2451545.0 + 36525.0); - auto jc = jd.julian_centuries_qty(); - EXPECT_NEAR(jc.value(), 1.0, 1e-10); + // 36525 days ≈ 1 Julian century + auto jd = JulianDate(2451545.0 + 36525.0); + auto jc = jd.julian_centuries_qty(); + EXPECT_NEAR(jc.value(), 1.0, 1e-10); } TEST(Time, ArithmeticWithHours) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + qtty::Hour(24.0); - EXPECT_NEAR((jd2 - jd1).value(), 1.0, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + qtty::Hour(24.0); + EXPECT_NEAR((jd2 - jd1).value(), 1.0, 1e-10); } TEST(Time, ArithmeticWithMinutes) { - auto mjd1 = MJD(60200.0); - auto mjd2 = mjd1 + qtty::Minute(1440.0); - EXPECT_NEAR((mjd2 - mjd1).value(), 1.0, 1e-10); + auto mjd1 = MJD(60200.0); + auto mjd2 = mjd1 + qtty::Minute(1440.0); + EXPECT_NEAR((mjd2 - mjd1).value(), 1.0, 1e-10); } TEST(Time, SubtractQuantityHours) { - auto jd1 = JulianDate(2451546.0); - auto jd2 = jd1 - qtty::Hour(12.0); - EXPECT_NEAR(jd2.value(), 2451545.5, 1e-10); + auto jd1 = JulianDate(2451546.0); + auto jd2 = jd1 - qtty::Hour(12.0); + EXPECT_NEAR(jd2.value(), 2451545.5, 1e-10); } TEST(Time, DifferenceConvertible) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = JulianDate(2451546.0); - auto diff = jd2 - jd1; - auto hours = diff.to(); - EXPECT_NEAR(hours.value(), 24.0, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = JulianDate(2451546.0); + auto diff = jd2 - jd1; + auto hours = diff.to(); + EXPECT_NEAR(hours.value(), 24.0, 1e-10); } TEST(Time, PeriodDurationInMinutes) { - Period p(MJD(60200.0), MJD(60200.5)); - auto min = p.duration(); - EXPECT_NEAR(min.value(), 720.0, 1e-6); + Period p(MJD(60200.0), MJD(60200.5)); + auto min = p.duration(); + EXPECT_NEAR(min.value(), 720.0, 1e-6); } From 82093688276e418ea5c897103207fb1410fa9a1e Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 11:33:56 +0100 Subject: [PATCH 07/19] refactor: unify Target interface for celestial objects and update related examples --- examples/trackable_targets_example.cpp | 46 +++++++------- include/siderust/body_target.hpp | 40 ++++++++++--- include/siderust/star_target.hpp | 23 ++++--- include/siderust/target.hpp | 83 +++++++++++++++++++------- include/siderust/trackable.hpp | 70 +++++++++++++--------- tests/test_altitude.cpp | 4 +- tests/test_bodies.cpp | 8 +-- 7 files changed, 177 insertions(+), 97 deletions(-) diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp index 2e578be..c09d50b 100644 --- a/examples/trackable_targets_example.cpp +++ b/examples/trackable_targets_example.cpp @@ -1,10 +1,10 @@ /** * @file trackable_targets_example.cpp * @example trackable_targets_example.cpp - * @brief Using Target, StarTarget, BodyTarget through Trackable - * polymorphism. + * @brief Using DirectionTarget, StarTarget, BodyTarget through the Target + * polymorphic interface. * - * Demonstrates the strongly-typed Target template with multiple frames: + * Demonstrates the strongly-typed DirectionTarget template with multiple frames: * - ICRSTarget — fixed direction in ICRS equatorial coordinates * - EclipticMeanJ2000Target — fixed direction in mean ecliptic J2000 * @@ -21,15 +21,6 @@ #include -namespace { - -struct NamedTrackable { - std::string name; - std::unique_ptr object; -}; - -} // namespace - int main() { using namespace siderust; @@ -44,6 +35,7 @@ int main() { ICRSTarget fixed_vega_like{spherical::direction::ICRS{ qtty::Degree(279.23473), qtty::Degree(38.78369)}}; std::cout << "ICRSTarget metadata\n"; + std::cout << " name=" << fixed_vega_like.name() << "\n"; std::cout << " RA/Dec=" << fixed_vega_like.direction() << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; @@ -53,25 +45,27 @@ int main() { qtty::Degree(279.6), qtty::Degree(61.8)}}; auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; + std::cout << " name=" << ecliptic_vega.name() << "\n"; std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() << " (converted)\n"; - std::cout << " alt=" << alt_ecliptic << std::endl; - - std::vector catalog; - catalog.push_back({"Sun", std::make_unique(Body::Sun)}); - catalog.push_back({"Mars", std::make_unique(Body::Mars)}); - catalog.push_back({"Vega (StarTarget)", std::make_unique(VEGA)}); - catalog.push_back({"Fixed Vega-like (ICRS)", - std::make_unique(spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369)})}); - - for (const auto &entry : catalog) { - const auto &t = entry.object; + std::cout << " alt=" << alt_ecliptic << "\n\n"; + + // Polymorphic catalog: all targets share the Target base. + // DirectionTarget accepts an optional label at construction. + std::vector> catalog; + catalog.push_back(std::make_unique(Body::Sun)); + catalog.push_back(std::make_unique(Body::Mars)); + catalog.push_back(std::make_unique(VEGA)); + catalog.push_back(std::make_unique( + spherical::direction::ICRS{qtty::Degree(279.23473), qtty::Degree(38.78369)}, + JulianDate::J2000(), "Vega (ICRS coord)")); + + for (const auto &t : catalog) { auto alt = t->altitude_at(site, now); - auto az = t->azimuth_at(site, now); + auto az = t->azimuth_at(site, now); - std::cout << std::left << std::setw(22) << entry.name << std::right + std::cout << std::left << std::setw(22) << t->name() << std::right << " alt=" << std::setw(9) << alt << " az=" << az << std::endl; auto crossings = t->crossings(site, window, qtty::Degree(0.0)); diff --git a/include/siderust/body_target.hpp b/include/siderust/body_target.hpp index 855bafd..0bbb307 100644 --- a/include/siderust/body_target.hpp +++ b/include/siderust/body_target.hpp @@ -2,9 +2,9 @@ /** * @file body_target.hpp - * @brief Trackable wrapper for solar-system bodies. + * @brief Target implementation for solar-system bodies. * - * `BodyTarget` implements the `Trackable` interface for any solar-system + * `BodyTarget` implements the `Target` interface for any solar-system * body identified by the `Body` enum. It dispatches altitude and azimuth * computations through the siderust-ffi `siderust_body_*` functions, which * in turn use VSOP87 (planets), specialised engines (Sun/Moon), or @@ -14,14 +14,15 @@ * @code * using namespace siderust; * BodyTarget mars(Body::Mars); + * std::cout << mars.name() << "\n"; // "Mars" * qtty::Degree alt = mars.altitude_at(obs, now); * * // Polymorphic usage - * std::vector> targets; + * std::vector> targets; * targets.push_back(std::make_unique(Body::Sun)); * targets.push_back(std::make_unique(Body::Jupiter)); * for (const auto& t : targets) { - * std::cout << t->altitude_at(obs, now).value() << "\n"; + * std::cout << t->name() << ": " << t->altitude_at(obs, now) << "\n"; * } * @endcode */ @@ -30,6 +31,7 @@ #include "azimuth.hpp" #include "ffi_core.hpp" #include "trackable.hpp" +#include namespace siderust { @@ -218,20 +220,20 @@ inline std::vector in_azimuth_range(Body b, const Geodetic &obs, } // namespace body // ============================================================================ -// BodyTarget — Trackable adapter for solar-system bodies +// BodyTarget — Target implementation for solar-system bodies // ============================================================================ /** - * @brief Trackable adapter for solar-system bodies. + * @brief Target implementation for solar-system bodies. * * Wraps a `Body` enum value and dispatches all altitude/azimuth queries * through the FFI `siderust_body_*` functions. * * `BodyTarget` is lightweight (holds a single enum value), copyable, and - * can be used directly or stored as `std::unique_ptr` for + * can be used directly or stored as `std::unique_ptr` for * polymorphic dispatch. */ -class BodyTarget : public Trackable { +class BodyTarget : public Target { public: /** * @brief Construct a BodyTarget for a given solar-system body. @@ -239,6 +241,28 @@ class BodyTarget : public Trackable { */ explicit BodyTarget(Body body) : body_(body) {} + // ------------------------------------------------------------------ + // Identity (implements Target) + // ------------------------------------------------------------------ + + /** + * @brief Returns the body's conventional name ("Sun", "Moon", "Mars", …). + */ + std::string name() const override { + switch (body_) { + case Body::Sun: return "Sun"; + case Body::Moon: return "Moon"; + case Body::Mercury: return "Mercury"; + case Body::Venus: return "Venus"; + case Body::Mars: return "Mars"; + case Body::Jupiter: return "Jupiter"; + case Body::Saturn: return "Saturn"; + case Body::Uranus: return "Uranus"; + case Body::Neptune: return "Neptune"; + default: return "Unknown Body"; + } + } + // ------------------------------------------------------------------ // Altitude queries // ------------------------------------------------------------------ diff --git a/include/siderust/star_target.hpp b/include/siderust/star_target.hpp index 40f0a02..637472e 100644 --- a/include/siderust/star_target.hpp +++ b/include/siderust/star_target.hpp @@ -2,15 +2,15 @@ /** * @file star_target.hpp - * @brief Trackable adapter for Star objects. + * @brief Target implementation for Star catalog objects. * - * `StarTarget` wraps a `const Star&` and implements the `Trackable` - * interface by delegating to the `star_altitude::` and `star_altitude::` - * namespace free functions. + * `StarTarget` wraps a `const Star&` and implements the `Target` + * interface by delegating to the `star_altitude::` namespace free functions. * * ### Example * @code * siderust::StarTarget vega_target(siderust::VEGA); + * std::cout << vega_target.name() << "\n"; // "Vega" * auto alt = vega_target.altitude_at(obs, now); * @endcode */ @@ -23,20 +23,29 @@ namespace siderust { /** - * @brief Trackable adapter wrapping a `const Star&`. + * @brief Target implementation wrapping a `const Star&`. * * The referenced `Star` must outlive the `StarTarget`. Typically used with * the pre-built catalog stars (e.g. `VEGA`, `SIRIUS`) which are `inline const` * globals and live for the entire program. */ -class StarTarget : public Trackable { +class StarTarget : public Target { public: /** - * @brief Wrap a Star reference as a Trackable. + * @brief Wrap a Star reference as a Target. * @param star Reference to a Star. Must outlive this adapter. */ explicit StarTarget(const Star &star) : star_(star) {} + // ------------------------------------------------------------------ + // Identity (implements Target) + // ------------------------------------------------------------------ + + /** + * @brief Returns the star's catalog name (delegates to `Star::name()`). + */ + std::string name() const override { return star_.name(); } + // ------------------------------------------------------------------ // Altitude queries // ------------------------------------------------------------------ diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index af7fba3..276dde9 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -2,12 +2,16 @@ /** * @file target.hpp - * @brief Generic strongly-typed RAII wrapper for a siderust Target direction. + * @brief Strongly-typed fixed-direction Target for any supported frame. * - * `Target` represents a fixed celestial direction in any supported - * reference frame and exposes altitude and azimuth computations via the same - * observer/window API as the sun/moon/star helpers in altitude.hpp and - * azimuth.hpp. + * `DirectionTarget` represents a fixed celestial direction (star, galaxy, + * or any user-defined sky coordinate) in any supported reference frame and + * exposes altitude/azimuth computations via the same observer/window API as + * the body helpers in altitude.hpp and azimuth.hpp. + * + * It is one concrete implementation of `Target` (the common abstract base). + * For moving solar-system bodies use `BodyTarget`; for catalog stars use + * `StarTarget`. * * The template parameter `C` must be an instantiation of * `spherical::Direction` for a frame `F` that can be transformed to ICRS @@ -34,6 +38,8 @@ #include "ffi_core.hpp" #include "time.hpp" #include "trackable.hpp" +#include +#include #include #include #include @@ -78,7 +84,8 @@ using spherical_direction_frame_t = typename spherical_direction_frame::type; // ============================================================================ /** - * @brief Move-only RAII wrapper for a fixed celestial target direction. + * @brief Fixed celestial direction target — a `Target` for a specific sky + * position. * * @tparam C Spherical direction type (e.g. `spherical::direction::ICRS`). * @@ -87,9 +94,17 @@ using spherical_direction_frame_t = typename spherical_direction_frame::type; * using namespace siderust; * ICRSTarget vega{ spherical::direction::ICRS{ 279.2348_deg, +38.7836_deg } }; * auto alt = vega.altitude_at(obs, now); // → qtty::Degree + * std::cout << vega.name() << "\n"; // "ICRS(279.23°, 38.78°)" * std::cout << vega.ra() << "\n"; // qtty::Degree (equatorial frames) * @endcode * + * ### Example — named ICRS target + * @code + * ICRSTarget vega{ spherical::direction::ICRS{ 279.2348_deg, +38.7836_deg }, + * JulianDate::J2000(), "Vega" }; + * std::cout << vega.name() << "\n"; // "Vega" + * @endcode + * * ### Example — Ecliptic target (auto-converted to ICRS internally) * @code * EclipticMeanJ2000Target ec{ @@ -97,7 +112,7 @@ using spherical_direction_frame_t = typename spherical_direction_frame::type; * auto alt = ec.altitude_at(obs, now); * @endcode */ -template class Target : public Trackable { +template class DirectionTarget : public Target { static_assert(detail::is_spherical_direction_v, "Target: C must be a specialisation of " @@ -126,9 +141,12 @@ template class Target : public Trackable { * * @param dir Spherical direction (any supported frame). * @param epoch Coordinate epoch (default J2000.0). + * @param label Optional human-readable name. If empty, a default + * "Frame(lon°, lat°)" string is generated from the direction. */ - explicit Target(C dir, JulianDate epoch = JulianDate::J2000()) - : m_dir_(dir), m_epoch_(epoch) { + explicit DirectionTarget(C dir, JulianDate epoch = JulianDate::J2000(), + std::string label = "") + : m_dir_(dir), m_epoch_(epoch), label_(std::move(label)) { // Convert to ICRS for the FFI; identity transform when already ICRS. if constexpr (std::is_same_v) { m_icrs_ = dir; @@ -143,7 +161,7 @@ template class Target : public Trackable { handle_ = h; } - ~Target() { + ~DirectionTarget() { if (handle_) { siderust_target_free(handle_); handle_ = nullptr; @@ -151,14 +169,15 @@ template class Target : public Trackable { } /// Move constructor. - Target(Target &&other) noexcept + DirectionTarget(DirectionTarget &&other) noexcept : m_dir_(std::move(other.m_dir_)), m_epoch_(other.m_epoch_), - m_icrs_(other.m_icrs_), handle_(other.handle_) { + m_icrs_(other.m_icrs_), label_(std::move(other.label_)), + handle_(other.handle_) { other.handle_ = nullptr; } /// Move assignment. - Target &operator=(Target &&other) noexcept { + DirectionTarget &operator=(DirectionTarget &&other) noexcept { if (this != &other) { if (handle_) { siderust_target_free(handle_); @@ -166,6 +185,7 @@ template class Target : public Trackable { m_dir_ = std::move(other.m_dir_); m_epoch_ = other.m_epoch_; m_icrs_ = other.m_icrs_; + label_ = std::move(other.label_); handle_ = other.handle_; other.handle_ = nullptr; } @@ -173,8 +193,27 @@ template class Target : public Trackable { } // Prevent copying (the handle has unique ownership). - Target(const Target &) = delete; - Target &operator=(const Target &) = delete; + DirectionTarget(const DirectionTarget &) = delete; + DirectionTarget &operator=(const DirectionTarget &) = delete; + + // ------------------------------------------------------------------ + // Identity (implements Target) + // ------------------------------------------------------------------ + + /** + * @brief Human-readable name for this direction target. + * + * Returns the label passed at construction if one was provided; otherwise + * generates a "Frame(lon°, lat°)" string from the ICRS direction. + */ + std::string name() const override { + if (!label_.empty()) + return label_; + std::ostringstream ss; + ss << "Direction(" << m_icrs_.ra().value() << "\xc2\xb0, " + << m_icrs_.dec().value() << "\xc2\xb0)"; + return ss.str(); + } // ------------------------------------------------------------------ // Coordinate accessors @@ -362,6 +401,7 @@ template class Target : public Trackable { C m_dir_; JulianDate m_epoch_; spherical::direction::ICRS m_icrs_; + std::string label_; SiderustTarget *handle_ = nullptr; /// Build a Period vector from a tempoch_period_mjd_t* array. @@ -382,25 +422,26 @@ template class Target : public Trackable { // ============================================================================ /// Fixed direction in ICRS (most common use-case). -using ICRSTarget = Target; +using ICRSTarget = DirectionTarget; /// Fixed direction in ICRF (treated identically to ICRS in Siderust). -using ICRFTarget = Target; +using ICRFTarget = DirectionTarget; /// Fixed direction in mean equatorial coordinates of J2000.0 (FK5). using EquatorialMeanJ2000Target = - Target; + DirectionTarget; /// Fixed direction in mean equatorial coordinates of date (precessed only). using EquatorialMeanOfDateTarget = - Target; + DirectionTarget; /// Fixed direction in true equatorial coordinates of date (precessed + /// nutated). using EquatorialTrueOfDateTarget = - Target; + DirectionTarget; /// Fixed direction in mean ecliptic coordinates of J2000.0. -using EclipticMeanJ2000Target = Target; +using EclipticMeanJ2000Target = + DirectionTarget; } // namespace siderust diff --git a/include/siderust/trackable.hpp b/include/siderust/trackable.hpp index 08a236f..9def307 100644 --- a/include/siderust/trackable.hpp +++ b/include/siderust/trackable.hpp @@ -2,30 +2,32 @@ /** * @file trackable.hpp - * @brief Abstract base class for trackable celestial objects. + * @brief Abstract base class for all celestial targets. * - * `Trackable` defines a polymorphic interface for any celestial object - * whose altitude and azimuth can be computed at an observer location. - * Implementations include: + * `Target` is the unified concept for anything in the sky that can be + * pointed at from an observer location. Concrete implementations cover: * - * - **Target** — fixed ICRS direction (RA/Dec) + * - **DirectionTarget** — fixed spherical direction in any supported frame + * (ICRS, equatorial, ecliptic). Aliased as `ICRSTarget`, etc. * - **StarTarget** — adapter for `Star` catalog objects - * - **BodyTarget** — solar-system bodies (Sun, Moon, planets, Pluto) + * - **BodyTarget** — solar-system bodies (Sun, Moon, planets) + * - *(future)* **SatelliteTarget** — Earth-orbiting satellites (TLE/SGP4) * - * Use `std::unique_ptr` to hold heterogeneous collections of - * trackable objects. + * Every `Target` carries a human-readable `name()`. + * Use `std::unique_ptr` to hold heterogeneous collections. * * ### Example * @code * auto sun = std::make_unique(siderust::Body::Sun); + * std::cout << sun->name() << "\n"; // "Sun" * qtty::Degree alt = sun->altitude_at(obs, now); * * // Polymorphic usage - * std::vector> targets; + * std::vector> targets; * targets.push_back(std::move(sun)); * targets.push_back(std::make_unique(VEGA)); * for (const auto& t : targets) { - * std::cout << t->altitude_at(obs, now).value() << "\n"; + * std::cout << t->name() << ": " << t->altitude_at(obs, now) << "\n"; * } * @endcode */ @@ -35,33 +37,40 @@ #include "coordinates.hpp" #include "time.hpp" #include +#include #include namespace siderust { /** - * @brief Abstract interface for any object whose altitude/azimuth can be - * computed. + * @brief Abstract base for any celestial object that can be tracked from an + * observer location. * - * This class defines the common API shared by all trackable celestial objects. - * Implementations must provide altitude_at and azimuth_at at minimum; the - * remaining methods have default implementations that throw if not overridden. + * Subclasses represent concrete target kinds: fixed sky directions, catalog + * stars, solar-system bodies, and (in the future) satellites. All must + * implement `name()`, `altitude_at()`, and `azimuth_at()`; the search + * helpers also need overrides. */ -class Trackable { +class Target { public: - virtual ~Trackable() = default; + virtual ~Target() = default; + + // ------------------------------------------------------------------ + // Identity + // ------------------------------------------------------------------ + + /** + * @brief Human-readable name for this target (e.g. "Sun", "Vega", + * "ICRS(279.2°, 38.8°)"). + */ + virtual std::string name() const = 0; // ------------------------------------------------------------------ // Altitude queries // ------------------------------------------------------------------ /** - * @brief Compute altitude at a given MJD instant. - * - * The return unit varies by implementation (radians for sun/moon/star, - * degrees for Target/BodyTarget). Check the concrete class documentation. - * - * @note For BodyTarget, returns radians; for Target, returns degrees. + * @brief Compute altitude (degrees) at a given MJD instant. */ virtual qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const = 0; @@ -114,12 +123,15 @@ class Trackable { qtty::Degree bearing, const SearchOptions &opts = {}) const = 0; - // Non-copyable, non-movable from base - Trackable() = default; - Trackable(const Trackable &) = delete; - Trackable &operator=(const Trackable &) = delete; - Trackable(Trackable &&) = default; - Trackable &operator=(Trackable &&) = default; + // Non-copyable, movable from base + Target() = default; + Target(const Target &) = delete; + Target &operator=(const Target &) = delete; + Target(Target &&) = default; + Target &operator=(Target &&) = default; }; +/// @brief Backward-compatible alias. Prefer `Target` in new code. +using Trackable = Target; + } // namespace siderust diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index 6121c62..26ff198 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -163,8 +163,8 @@ TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { } TEST_F(AltitudeTest, ICRSTargetPolymorphic) { - // Verify Target is usable through the Trackable interface - std::unique_ptr t = std::make_unique( + // Verify DirectionTarget is usable through the Target interface + std::unique_ptr t = std::make_unique( spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}); qtty::Degree alt = t->altitude_at(obs, start); EXPECT_GT(alt.value(), -90.0); diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index c4cf2a5..588b22f 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -84,7 +84,7 @@ TEST(Bodies, AllPlanets) { } // ============================================================================ -// BodyTarget — generic solar-system body via Trackable polymorphism +// BodyTarget — solar-system body via Target polymorphism // ============================================================================ TEST(Bodies, BodyTargetSunAltitude) { @@ -152,7 +152,7 @@ TEST(Bodies, BodyTargetPolymorphic) { auto obs = geodetic(2.35, 48.85, 35.0); auto mjd = MJD(60000.5); - std::vector> targets; + std::vector> targets; targets.push_back(std::make_unique(Body::Sun)); targets.push_back(std::make_unique(Body::Mars)); @@ -178,7 +178,7 @@ TEST(Bodies, BodyNamespaceAzimuthAt) { } // ============================================================================ -// StarTarget — Trackable adapter for Star +// StarTarget — Target implementation for catalog stars // ============================================================================ TEST(Bodies, StarTargetAltitude) { @@ -196,7 +196,7 @@ TEST(Bodies, StarTargetPolymorphicWithBodyTarget) { auto obs = geodetic(2.35, 48.85, 35.0); auto mjd = MJD(60000.5); - std::vector> targets; + std::vector> targets; targets.push_back(std::make_unique(Body::Sun)); targets.push_back(std::make_unique(VEGA)); From 443f7f4a17ef4459e9ac1d78e2afc9f059ea846d Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 12:56:28 +0100 Subject: [PATCH 08/19] refactor: update .gitignore and add CI script for Docker integration --- .gitignore | 2 + Dockerfile | 4 ++ run-ci.sh | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100755 run-ci.sh diff --git a/.gitignore b/.gitignore index 5ade5e6..2926453 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ out/ build-make/ build-verify/ build-ci-config-check/ +build-coverage/ +coverage_html/ # IDE files .vscode/ diff --git a/Dockerfile b/Dockerfile index 5c3221f..5108edb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ pkg-config \ libssl-dev \ graphviz \ + rsync \ + clang-format \ + clang-tidy \ + gcovr \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL "https://github.com/doxygen/doxygen/releases/download/Release_1_16_1/doxygen-${DOXYGEN_VERSION}.linux.bin.tar.gz" -o /tmp/doxygen.tar.gz && \ diff --git a/run-ci.sh b/run-ci.sh new file mode 100755 index 0000000..a986a36 --- /dev/null +++ b/run-ci.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Local CI runner that mirrors .github/workflows/ci.yml inside Docker + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IMAGE_NAME="siderust-cpp-ci" +IMAGE_TAG="local" +IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" + +bold() { printf '\e[1m%s\e[0m\n' "$*"; } + +build_image() { + bold "Building Docker image (${IMAGE})" + docker build -t "${IMAGE}" "${ROOT_DIR}" +} + +run_container() { + local cmd=$1 + local flags=(--rm --init -v "${ROOT_DIR}:/workspace") + if [ -t 1 ]; then + flags+=(-it) + fi + docker run "${flags[@]}" "${IMAGE}" bash -lc "${cmd}" +} + +container_script() { + cat <<'EOS' +set -euo pipefail +export CARGO_TERM_COLOR=always +export CMAKE_BUILD_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL:-2} + +cd /workspace + +run_lint() { + echo "==> Lint: configure + clang-format + clang-tidy" + rm -rf build + cmake -S . -B build -G Ninja -DSIDERUST_BUILD_DOCS=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + mapfile -t format_files < <(git ls-files '*.hpp' '*.cpp') + if [ ${#format_files[@]} -gt 0 ]; then + clang-format --dry-run --Werror "${format_files[@]}" + else + echo "No C++ files to format" + fi + + mapfile -t tidy_files < <(git ls-files '*.cpp') + if [ ${#tidy_files[@]} -gt 0 ]; then + for f in "${tidy_files[@]}"; do + echo "clang-tidy: ${f}" + clang-tidy -p build --warnings-as-errors='*' "${f}" + done + else + echo "No C++ source files for clang-tidy" + fi +} + +run_build_test_docs() { + echo "==> Build + Test + Docs" + rm -rf build + cmake -S . -B build -G Ninja -DSIDERUST_BUILD_DOCS=ON + cmake --build build --target test_siderust + ctest --test-dir build --output-on-failure -L siderust_cpp + cmake --build build --target docs +} + +run_coverage() { + echo "==> Coverage" + rm -rf build-coverage coverage.xml coverage_html code-coverage-results.md + cmake -S . -B build-coverage -G Ninja \ + -DSIDERUST_BUILD_DOCS=OFF \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="--coverage" \ + -DCMAKE_EXE_LINKER_FLAGS="--coverage" + + cmake --build build-coverage --target test_siderust + ctest --test-dir build-coverage --output-on-failure -L siderust_cpp + + mkdir -p coverage_html + gcovr \ + --root . \ + --exclude 'build-coverage/.*' \ + --exclude 'siderust/.*' \ + --exclude 'qtty-cpp/.*' \ + --exclude 'tempoch-cpp/.*' \ + --exclude 'tests/.*' \ + --exclude 'examples/.*' \ + --xml \ + --output coverage.xml + + gcovr \ + --root . \ + --exclude 'build-coverage/.*' \ + --exclude 'siderust/.*' \ + --exclude 'qtty-cpp/.*' \ + --exclude 'tempoch-cpp/.*' \ + --exclude 'tests/.*' \ + --exclude 'examples/.*' \ + --html-details \ + --output coverage_html/index.html +} + +run_lint +run_build_test_docs +run_coverage + +echo "==> All CI steps completed" +EOS +} + +build_image +run_container "$(container_script)" From 48acce3f1b224b1510706e83ee64f2a908a0d9ec Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 16:07:27 +0100 Subject: [PATCH 09/19] Add body-centric transformations and Mars example - Implemented body-centric coordinate transformations in `bodycentric_transforms.hpp`. - Introduced `BodycentricPos` template struct for handling body-centric positions. - Added `to_bodycentric` function to convert geocentric/heliocentric positions to body-centric coordinates. - Created `BodycentricParams` struct to encapsulate orbital parameters and reference centers. - Added Mars heliocentric position calculation in `ephemeris.hpp`. - Developed a new example `l2_satellite_mars_example.cpp` demonstrating Mars's position as seen from a JWST-like L2 orbit. - Enhanced `Position` class in `cartesian.hpp` with distance calculation methods. - Added unit tests in `test_bodycentric.cpp` to validate body-centric transformations and Keplerian position calculations. --- .gitignore | 6 +- CMakeLists.txt | 19 ++ examples/bodycentric_coordinates.cpp | 210 ++++++++++++++ examples/l2_satellite_mars_example.cpp | 68 +++++ include/siderust/bodies.hpp | 7 + .../coordinates/bodycentric_transforms.hpp | 204 ++++++++++++++ include/siderust/coordinates/cartesian.hpp | 17 ++ include/siderust/coordinates/spherical.hpp | 25 ++ include/siderust/ephemeris.hpp | 24 ++ include/siderust/orbital_center.hpp | 174 ++++++++++++ include/siderust/siderust.hpp | 2 + siderust | 2 +- tests/test_bodycentric.cpp | 256 ++++++++++++++++++ 13 files changed, 1009 insertions(+), 5 deletions(-) create mode 100644 examples/bodycentric_coordinates.cpp create mode 100644 examples/l2_satellite_mars_example.cpp create mode 100644 include/siderust/coordinates/bodycentric_transforms.hpp create mode 100644 include/siderust/orbital_center.hpp create mode 100644 tests/test_bodycentric.cpp diff --git a/.gitignore b/.gitignore index 2926453..0c80895 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,9 @@ build/ cmake-build-*/ out/ -build-make/ -build-verify/ -build-ci-config-check/ -build-coverage/ +build-* coverage_html/ +coverage.xml # IDE files .vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8131d86..3898ffb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,24 @@ if(DEFINED _siderust_rpath) ) endif() +add_executable(l2_satellite_mars_example examples/l2_satellite_mars_example.cpp) +target_link_libraries(l2_satellite_mars_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(l2_satellite_mars_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(bodycentric_coordinates_example examples/bodycentric_coordinates.cpp) +target_link_libraries(bodycentric_coordinates_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(bodycentric_coordinates_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- @@ -218,6 +236,7 @@ set(TEST_SOURCES tests/test_bodies.cpp tests/test_altitude.cpp tests/test_ephemeris.cpp + tests/test_bodycentric.cpp ) add_executable(test_siderust ${TEST_SOURCES}) diff --git a/examples/bodycentric_coordinates.cpp b/examples/bodycentric_coordinates.cpp new file mode 100644 index 0000000..9184cf7 --- /dev/null +++ b/examples/bodycentric_coordinates.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/** + * @file bodycentric_coordinates.cpp + * @brief Body-Centric Coordinates Example (mirrors Rust's bodycentric_coordinates.rs) + * + * This example demonstrates body-centric coordinate transforms: viewing + * positions from arbitrary orbiting bodies (satellites, planets, moons). + * + * Key API: + * - `BodycentricParams::geocentric(orbit)` / `::heliocentric(orbit)` — params + * - `to_bodycentric(pos, params, jd)` — transform to body frame + * - `BodycentricPos::to_geocentric(jd)` — inverse transform + * - `kepler_position(orbit, jd)` — Keplerian propagator + */ + +#include + +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; +using qtty::AstronomicalUnit; + +// --------------------------------------------------------------------------- +// Distances helpers +// --------------------------------------------------------------------------- +static double au_magnitude(const cartesian::Position &p) { + double x = p.x().value(), y = p.y().value(), z = p.z().value(); + return std::sqrt(x * x + y * y + z * z); +} +template +static double magnitude(const BodycentricPos &p) { + double x = p.x().value(), y = p.y().value(), z = p.z().value(); + return std::sqrt(x * x + y * y + z * z); +} +template +static double pos_magnitude(const cartesian::Position &p) { + double x = p.x().value(), y = p.y().value(), z = p.z().value(); + return std::sqrt(x * x + y * y + z * z); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- +int main() { + std::cout << "=== Body-Centric Coordinates Example ===\n\n"; + + // J2000.0 + const JulianDate jd(2451545.0); + std::cout << "Reference time: J2000.0 (JD " << std::fixed + << std::setprecision(1) << jd.value() << ")\n\n"; + + // ========================================================================= + // 1. Satellite-Centric Coordinates (ISS example) + // ========================================================================= + std::cout << "1. SATELLITE-CENTRIC COORDINATES\n"; + std::cout << "--------------------------------\n"; + + // ISS-like orbit: ~6 378 km = 0.0000426 AU above Earth + constexpr double KM_PER_AU = 149'597'870.7; + constexpr double ISS_ALTITUDE_KM = 6'378.0; // Earth radius ≈ altitude + constexpr double ISS_SMA_AU = ISS_ALTITUDE_KM / KM_PER_AU; + + Orbit iss_orbit{ISS_SMA_AU, 0.001, 51.6, 0.0, 0.0, 0.0, jd.value()}; + + BodycentricParams iss_params = BodycentricParams::geocentric(iss_orbit); + + // ISS position (geocentric) via Keplerian propagation + auto iss_pos = kepler_position(iss_orbit, jd); + std::cout << "ISS orbit:\n"; + std::cout << " Semi-major axis : " << std::setprecision(8) << ISS_SMA_AU + << " AU (" << ISS_ALTITUDE_KM << " km)\n"; + std::cout << " Eccentricity : " << iss_orbit.eccentricity << "\n"; + std::cout << " Inclination : " << iss_orbit.inclination_deg << "°\n"; + std::cout << std::setprecision(8); + std::cout << "ISS position (Geocentric EclipticMeanJ2000):\n"; + std::cout << " X = " << iss_pos.x().value() << " AU\n"; + std::cout << " Y = " << iss_pos.y().value() << " AU\n"; + std::cout << " Z = " << iss_pos.z().value() << " AU\n"; + std::cout << " Distance from Earth: " << iss_pos.distance().value() << " AU (" + << iss_pos.distance().value() * KM_PER_AU << " km)\n\n"; + + // Moon's approximate geocentric position (~384 400 km = 0.00257 AU) + cartesian::Position + moon_geo(0.00257, 0.0, 0.0); + std::cout << "Moon position (Geocentric):\n"; + std::cout << " Distance from Earth: " << moon_geo.distance().value() << " AU (" + << moon_geo.distance().value() * KM_PER_AU << " km)\n\n"; + + // Transform to ISS-centric + auto moon_from_iss = to_bodycentric(moon_geo, iss_params, jd); + std::cout << "Moon as seen from ISS:\n"; + std::cout << " X = " << moon_from_iss.x().value() << " AU\n"; + std::cout << " Y = " << moon_from_iss.y().value() << " AU\n"; + std::cout << " Z = " << moon_from_iss.z().value() << " AU\n"; + std::cout << " Distance from ISS: " << moon_from_iss.distance().value() << " AU (" + << moon_from_iss.distance().value() * KM_PER_AU << " km)\n\n"; + + // ========================================================================= + // 2. Mars-Centric Coordinates + // ========================================================================= + std::cout << "2. MARS-CENTRIC COORDINATES\n"; + std::cout << "---------------------------\n"; + + Orbit mars_orbit{1.524, 0.0934, 1.85, 49.56, 286.5, 19.41, jd.value()}; + BodycentricParams mars_params = BodycentricParams::heliocentric(mars_orbit); + + auto earth_helio = ephemeris::earth_heliocentric(jd); + auto mars_helio = ephemeris::mars_heliocentric(jd); + + std::cout << "Earth (Heliocentric): distance from Sun = " + << earth_helio.distance().value() << " AU\n"; + std::cout << "Mars (Heliocentric): distance from Sun = " + << mars_helio.distance().value() << " AU\n\n"; + + auto earth_from_mars = to_bodycentric(earth_helio, mars_params, jd); + std::cout << "Earth as seen from Mars:\n"; + std::cout << " X = " << earth_from_mars.x().value() << " AU\n"; + std::cout << " Y = " << earth_from_mars.y().value() << " AU\n"; + std::cout << " Z = " << earth_from_mars.z().value() << " AU\n"; + std::cout << " Distance from Mars: " << earth_from_mars.distance().value() << " AU\n\n"; + + // ========================================================================= + // 3. Venus-Centric Coordinates + // ========================================================================= + std::cout << "3. VENUS-CENTRIC COORDINATES\n"; + std::cout << "----------------------------\n"; + + Orbit venus_orbit{0.723, 0.0067, 3.39, 76.68, 131.53, 50.42, jd.value()}; + BodycentricParams venus_params = BodycentricParams::heliocentric(venus_orbit); + + auto venus_helio = ephemeris::venus_heliocentric(jd); + std::cout << "Venus (Heliocentric): distance from Sun = " + << venus_helio.distance().value() << " AU\n\n"; + + auto earth_from_venus = to_bodycentric(earth_helio, venus_params, jd); + std::cout << "Earth as seen from Venus:\n"; + std::cout << " Distance: " << earth_from_venus.distance().value() << " AU\n\n"; + + auto mars_from_venus = to_bodycentric(mars_helio, venus_params, jd); + std::cout << "Mars as seen from Venus:\n"; + std::cout << " Distance: " << mars_from_venus.distance().value() << " AU\n\n"; + + // ========================================================================= + // 4. Round-Trip Transformation + // ========================================================================= + std::cout << "4. ROUND-TRIP TRANSFORMATION\n"; + std::cout << "----------------------------\n"; + + // Start from a known geocentric position + cartesian::Position + original_pos(0.001, 0.002, 0.003); + std::cout << "Original position (Geocentric):\n"; + std::cout << " X = " << std::setprecision(12) << original_pos.x().value() + << " AU\n"; + std::cout << " Y = " << original_pos.y().value() << " AU\n"; + std::cout << " Z = " << original_pos.z().value() << " AU\n\n"; + + // To Mars-centric and back to geocentric + auto mars_centric = to_bodycentric(original_pos, mars_params, jd); + std::cout << "Transformed to Mars-centric:\n"; + std::cout << " Distance from Mars: " << std::setprecision(8) + << mars_centric.distance().value() << " AU\n\n"; + + auto recovered = mars_centric.to_geocentric(jd); + std::cout << "Recovered position (Geocentric):\n"; + std::cout << " X = " << std::setprecision(12) << recovered.x().value() + << " AU\n"; + std::cout << " Y = " << recovered.y().value() << " AU\n"; + std::cout << " Z = " << recovered.z().value() << " AU\n\n"; + + double dx = original_pos.x().value() - recovered.x().value(); + double dy = original_pos.y().value() - recovered.y().value(); + double dz = original_pos.z().value() - recovered.z().value(); + double diff = std::sqrt(dx * dx + dy * dy + dz * dz); + std::cout << "Total difference: " << diff + << " AU (should be ~0 within floating-point precision)\n\n"; + + // ========================================================================= + // 5. Directions as Free Vectors + // ========================================================================= + std::cout << "5. DIRECTIONS AS FREE VECTORS\n"; + std::cout << "------------------------------\n"; + + cartesian::Direction star_dir(0.707, 0.0, 0.707); + std::cout << "Star direction (EquatorialMeanJ2000):\n"; + std::cout << " X = " << std::setprecision(3) << star_dir.x << "\n"; + std::cout << " Y = " << star_dir.y << "\n"; + std::cout << " Z = " << star_dir.z << "\n\n"; + + std::cout << "Note: Directions are free vectors — they represent 'which way'\n" + "without reference to any origin. A distant star appears in the\n" + "same direction from Earth or from the ISS.\n\n"; + + // ========================================================================= + std::cout << "=== Example Complete ===\n\n"; + std::cout << "Key Takeaways:\n"; + std::cout << "- Body-centric coordinates work for any orbiting body\n"; + std::cout << "- Satellite-centric: use BodycentricParams::geocentric()\n"; + std::cout << "- Planet-centric: use BodycentricParams::heliocentric()\n"; + std::cout << "- Directions are free vectors (no center, only frame)\n"; + std::cout << "- Round-trip transformations preserve positions within floating-point precision\n"; + + return 0; +} diff --git a/examples/l2_satellite_mars_example.cpp b/examples/l2_satellite_mars_example.cpp new file mode 100644 index 0000000..acfb6dd --- /dev/null +++ b/examples/l2_satellite_mars_example.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include +#include + +using namespace siderust; + +// Approximate Sun–Earth L2 offset: 1.5e6 km beyond Earth along the Sun–Earth +// line. Convert to AU so we can stay unit-safe with qtty. +constexpr double L2_OFFSET_KM = 1'500'000.0; +constexpr double KM_PER_AU = 149'597'870.7; +constexpr double L2_OFFSET_AU = L2_OFFSET_KM / KM_PER_AU; + +cartesian::position::EclipticMeanJ2000 +compute_l2_heliocentric(const JulianDate &jd) { + const auto earth = ephemeris::earth_heliocentric(jd); + + // Unit vector from Sun → Earth (heliocentric frame). + const double ex = earth.x().value(); + const double ey = earth.y().value(); + const double ez = earth.z().value(); + const double norm = std::sqrt(ex * ex + ey * ey + ez * ez); + const double ux = ex / norm; + const double uy = ey / norm; + const double uz = ez / norm; + + // Move ~0.01 AU further from the Sun along that direction. + const qtty::AstronomicalUnit offset(L2_OFFSET_AU); + return {earth.x() + offset * ux, earth.y() + offset * uy, + earth.z() + offset * uz}; +} + +cartesian::Position +mars_relative_to_l2(const JulianDate &jd) { + const auto mars = ephemeris::mars_heliocentric(jd); + const auto l2 = compute_l2_heliocentric(jd); + + // Bodycentric position = target - observer. + return {mars.x() - l2.x(), mars.y() - l2.y(), mars.z() - l2.z()}; +} + +int main() { + std::cout << "╔══════════════════════════════════════════╗\n" + << "║ Mars as Seen from a JWST-like L2 Orbit ║\n" + << "╚══════════════════════════════════════════╝\n\n"; + + const JulianDate obs_epoch(2460000.0); // ~2023-06-30 + + std::cout << "Observation epoch (JD): " << std::fixed << std::setprecision(1) + << obs_epoch.value() << "\n\n"; + + const auto mars_helio = ephemeris::mars_heliocentric(obs_epoch); + const auto l2_helio = compute_l2_heliocentric(obs_epoch); + const auto mars_from_l2 = mars_relative_to_l2(obs_epoch); + + std::cout << "Mars heliocentric (EclipticMeanJ2000):\n " << mars_helio + << "\n\n"; + + std::cout << "L2 heliocentric (Earth + 1.5e6 km radial):\n " << l2_helio + << "\n\n"; + + std::cout << "Mars relative to L2 (bodycentric):\n " << mars_from_l2 + << "\n"; + + return 0; +} diff --git a/include/siderust/bodies.hpp b/include/siderust/bodies.hpp index f6f2fa3..add6e18 100644 --- a/include/siderust/bodies.hpp +++ b/include/siderust/bodies.hpp @@ -60,6 +60,13 @@ struct Orbit { c.mean_anomaly_deg, c.epoch_jd}; } + + /// Convert to C FFI struct. + siderust_orbit_t to_c() const { + return {semi_major_axis_au, eccentricity, inclination_deg, + lon_ascending_node_deg, arg_perihelion_deg, mean_anomaly_deg, + epoch_jd}; + } }; // ============================================================================ diff --git a/include/siderust/coordinates/bodycentric_transforms.hpp b/include/siderust/coordinates/bodycentric_transforms.hpp new file mode 100644 index 0000000..3f046ae --- /dev/null +++ b/include/siderust/coordinates/bodycentric_transforms.hpp @@ -0,0 +1,204 @@ +#pragma once + +/** + * @file bodycentric_transforms.hpp + * @brief Body-centric coordinate transformations. + * + * Mirrors Rust's `ToBodycentricExt` and `FromBodycentricExt` traits: + * - `to_bodycentric(pos, params, jd)` — free function transforming a + * Geocentric/Heliocentric/Barycentric position to one centered on the + * orbiting body described by `params`. + * - `BodycentricPos::to_geocentric(jd)` — inverse transform back to + * geocentric. + * + * The transform algorithm (mirroring Rust): + * 1. Propagate the body's Keplerian orbit to JD → position in the orbit's + * reference center. + * 2. Convert that position to match the source center (via VSOP87 offsets). + * 3. `bodycentric = input - body_in_source_center` + * + * # Usage + * ```cpp + * #include // or just this file + * using namespace siderust; + * using namespace siderust::frames; + * using namespace siderust::centers; + * using qtty::AstronomicalUnit; + * + * auto jd = JulianDate::J2000; + * + * // ISS-like geocentric orbit + * Orbit iss_orbit{0.0000426, 0.001, 51.6, 0.0, 0.0, 0.0, jd.value()}; + * BodycentricParams iss_params = BodycentricParams::geocentric(iss_orbit); + * + * // Moon's approximate geocentric position + * cartesian::Position moon_geo( + * 0.00257, 0.0, 0.0); + * + * // Moon as seen from ISS + * auto moon_from_iss = to_bodycentric(moon_geo, iss_params, jd); + * + * // Round-trip back to geocentric + * auto recovered = moon_from_iss.to_geocentric(jd); + * ``` + */ + +#include "../ffi_core.hpp" +#include "../orbital_center.hpp" +#include "../time.hpp" +#include "cartesian.hpp" + +#include + +namespace siderust { + +// ============================================================================ +// BodycentricPos +// ============================================================================ + +/** + * @brief Result of a body-centric coordinate transformation. + * + * Carries the relative position (target – body) and the embedded + * `BodycentricParams` needed for the inverse transform (`to_geocentric`). + * Mirrors Rust's `Position` which stores + * `BodycentricParams` at runtime. + * + * @tparam F Reference frame tag (e.g. `frames::EclipticMeanJ2000`). + * @tparam U Length unit (default: `qtty::AstronomicalUnit`). + * + * @ingroup coordinates_cartesian + */ +template +struct BodycentricPos { + static_assert(frames::is_frame_v, "F must be a valid frame tag"); + + /// Raw Cartesian position tagged with the Bodycentric center. + cartesian::Position pos; + + /// Orbital parameters of the body used as the coordinate origin. + BodycentricParams params; + + // -- Accessors -- + + U x() const { return pos.x(); } + U y() const { return pos.y(); } + U z() const { return pos.z(); } + + /// Distance from the body (norm of the embedded `pos`). + U distance() const { return pos.distance(); } + + /// Distance to another body-centric position. + U distance_to(const BodycentricPos &other) const { return pos.distance_to(other.pos); } + + /// Access the embedded orbital parameters of the body. + const BodycentricParams ¢er_params() const { return params; } + + // ── Inverse transform ───────────────────────────────────────────────────── + + /** + * @brief Transform back to geocentric coordinates. + * + * Mirrors Rust's `FromBodycentricExt::to_geocentric(jd)`. + * Uses the same `params` and `jd` as the original `to_bodycentric()` call. + * + * @param jd Julian Date (same as the forward transform). + * @return Geocentric position in the same frame and unit. + */ + cartesian::Position + to_geocentric(const JulianDate &jd) const; +}; + +// ============================================================================ +// to_bodycentric() — free function template +// ============================================================================ + +/** + * @brief Transform a position to body-centric coordinates. + * + * Mirrors Rust's `position.to_bodycentric(params, jd)`. + * + * The source center must be `Geocentric`, `Heliocentric`, or `Barycentric`. + * Calling this with `Bodycentric` or `Topocentric` as the source center will + * throw `InvalidCenterError` at runtime. + * + * The result frame `F` and unit `U` are preserved from the source position. + * + * @tparam C Source center (Geocentric, Heliocentric, or Barycentric). + * @tparam F Reference frame. + * @tparam U Length unit. + * @param pos Source position. + * @param params Orbital parameters of the body to use as the new center. + * @param jd Julian Date for Keplerian propagation and center shifts. + * @return `BodycentricPos` — relative position plus embedded params. + * + * @throws InvalidCenterError if the source center is not supported. + */ +template +inline BodycentricPos +to_bodycentric(const cartesian::Position &pos, + const BodycentricParams ¶ms, const JulianDate &jd) { + static_assert(centers::is_center_v, "C must be a valid center tag"); + + siderust_cartesian_pos_t c_pos = pos.to_c(); + SiderustBodycentricParams c_params = params.to_c(); + siderust_cartesian_pos_t c_out{}; + + check_status(siderust_to_bodycentric(c_pos, c_params, jd.value(), &c_out), + "to_bodycentric"); + + cartesian::Position result_pos( + U(c_out.x), U(c_out.y), U(c_out.z)); + return BodycentricPos{result_pos, params}; +} + +// ============================================================================ +// BodycentricPos::to_geocentric() — out-of-line member implementation +// ============================================================================ + +template +inline cartesian::Position +BodycentricPos::to_geocentric(const JulianDate &jd) const { + siderust_cartesian_pos_t c_pos = pos.to_c(); + SiderustBodycentricParams c_params = params.to_c(); + siderust_cartesian_pos_t c_out{}; + + check_status(siderust_from_bodycentric(c_pos, c_params, jd.value(), &c_out), + "from_bodycentric"); + + return cartesian::Position(U(c_out.x), U(c_out.y), + U(c_out.z)); +} + +// ============================================================================ +// kepler_position() — Keplerian orbital propagation +// ============================================================================ + +/** + * @brief Compute an orbital position at a given Julian Date via Kepler's laws. + * + * Returns the body's position in the EclipticMeanJ2000 frame in AU. + * The reference center of the returned position equals the orbit's own + * reference center (e.g. heliocentric for a planet's orbit). + * + * @tparam C Desired center tag for the result (caller must know from context, + * e.g. `centers::Geocentric` for a satellite orbit). + * @param orbit Keplerian orbital elements. + * @param jd Julian Date. + * @return Position in EclipticMeanJ2000/AU with center C. + */ +template +inline cartesian::Position +kepler_position(const Orbit &orbit, const JulianDate &jd) { + static_assert(centers::is_center_v, + "C must be a valid center tag (default: Heliocentric)"); + siderust_cartesian_pos_t c_out{}; + check_status(siderust_kepler_position(orbit.to_c(), jd.value(), &c_out), + "kepler_position"); + return cartesian::Position( + qtty::AstronomicalUnit(c_out.x), qtty::AstronomicalUnit(c_out.y), + qtty::AstronomicalUnit(c_out.z)); +} + +} // namespace siderust diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 2a681b2..0b91f0c 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -13,6 +13,7 @@ #include #include +#include namespace siderust { namespace cartesian { @@ -69,6 +70,22 @@ template struct Position { U y() const { return comp_y; } U z() const { return comp_z; } + U distance() const { + using std::sqrt; + const double vx = comp_x.value(); + const double vy = comp_y.value(); + const double vz = comp_z.value(); + return U(sqrt(vx * vx + vy * vy + vz * vz)); + } + + U distance_to(const Position &other) const { + using std::sqrt; + const double dx = comp_x.value() - other.comp_x.value(); + const double dy = comp_y.value() - other.comp_y.value(); + const double dz = comp_z.value() - other.comp_z.value(); + return U(sqrt(dx * dx + dy * dy + dz * dz)); + } + static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 1053eb7..13e8426 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -15,6 +15,7 @@ #include #include +#include namespace siderust { namespace spherical { @@ -270,6 +271,30 @@ template struct Position { } U distance() const { return dist_; } + + U distance_to(const Position &other) const { + using std::sqrt; + // Values in underlying unit (e.g. meters) + const double r = dist_.value(); + const double s = other.dist_.value(); + + // convert degrees to radians + constexpr double DEG2RAD = M_PI / 180.0; + const double a1 = azimuth_.value() * DEG2RAD; + const double p1 = polar_.value() * DEG2RAD; + const double a2 = other.azimuth_.value() * DEG2RAD; + const double p2 = other.polar_.value() * DEG2RAD; + + // dot product of unit direction vectors (spherical -> cartesian) + double cos_p1 = std::cos(p1); + double cos_p2 = std::cos(p2); + double dot = cos_p1 * cos_p2 * std::cos(a1 - a2) + std::sin(p1) * std::sin(p2); + if (dot > 1.0) dot = 1.0; + if (dot < -1.0) dot = -1.0; + + double d = std::sqrt(r * r + s * s - 2.0 * r * s * dot); + return U(d); + } }; // ============================================================================ diff --git a/include/siderust/ephemeris.hpp b/include/siderust/ephemeris.hpp index 59b715c..72dd39f 100644 --- a/include/siderust/ephemeris.hpp +++ b/include/siderust/ephemeris.hpp @@ -56,6 +56,30 @@ earth_heliocentric(const JulianDate &jd) { out); } +/** + * @brief Mars's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +mars_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_mars_heliocentric(jd.value(), &out), + "ephemeris::mars_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c( + out); +} + +/** + * @brief Venus's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +venus_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_venus_heliocentric(jd.value(), &out), + "ephemeris::venus_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c( + out); +} + /** * @brief Moon's geocentric position (EclipticMeanJ2000, km) via ELP2000. */ diff --git a/include/siderust/orbital_center.hpp b/include/siderust/orbital_center.hpp new file mode 100644 index 0000000..d4add6d --- /dev/null +++ b/include/siderust/orbital_center.hpp @@ -0,0 +1,174 @@ +#pragma once + +/** + * @file orbital_center.hpp + * @brief C++ wrapper for body-centric coordinates using orbital elements. + * + * Provides `OrbitReferenceCenter` enum and `BodycentricParams` struct to define + * and work with coordinate systems centered on orbiting bodies (planets, moons, + * satellites, etc.). Use these with `Position` to + * express positions relative to an orbiting body. + * + * @example + * ```cpp + * using namespace siderust; + * + * // Define Mars's heliocentric orbit + * Orbit mars_orbit = { + * 1.524, // semi_major_axis_au + * 0.0934, // eccentricity + * 1.85, // inclination_deg + * 49.56, // lon_ascending_node_deg + * 286.5, // arg_perihelion_deg + * 19.41, // mean_anomaly_deg + * 2451545.0 // epoch_jd (J2000) + * }; + * + * // Create parameters: Mars as center of a heliocentric orbit + * BodycentricParams mars_center = BodycentricParams::heliocentric(mars_orbit); + * + * // Later, compute Mars position and use it as reference center + * // (Integration with transform functions coming in future release) + * ``` + */ + +#include "bodies.hpp" +#include "centers.hpp" +#include "ffi_core.hpp" +#include +#include + +namespace siderust { + +/** + * @brief Specifies the reference center for an orbit. + * + * Indicates which standard center the orbital elements are defined relative to. + * This is needed when transforming positions to/from a body-centric frame, + * as the orbit must be converted to match the coordinate system. + */ +enum class OrbitReferenceCenter : std::uint8_t { + /// Orbit defined relative to the solar system barycenter. + Barycentric = 0, + /// Orbit defined relative to the Sun (planets, asteroids, comets). + Heliocentric = 1, + /// Orbit defined relative to Earth (artificial satellites, Moon). + Geocentric = 2, +}; + +/** + * @brief Parameters for a body-centric coordinate system. + * + * Specifies the orbital elements of a celestial body and the reference center + * for those elements. This allows computing a body's position at any Julian date + * using Keplerian propagation, then using that position as the origin of a + * coordinate system. + * + * # Use Cases + * + * - **Satellites**: Define L1, L2, L3, L4, L5 positions relative to their + * parent body (e.g., a halo orbit at the Sun-Earth L2). + * - **Planets**: Compute stellar positions as seen from another planet. + * - **Moons**: Express coordinates relative to a moon's center (e.g., Phobos + * relative to Mars). + * + * # Example: L2 Satellite + * + * ```cpp + * // Approximate L2 orbit (1.5M km from Earth on opposite side of Sun) + * // In practice, L2 is a quasi-periodic Halo orbit, but we approximate here + * Orbit l2_approx = { + * 1.0, // semi_major_axis_au (~1 AU from Sun, like Earth) + * 0.01, // eccentricity (small: stable near L2) + * 0.0, // inclination_deg + * 0.0, // lon_ascending_node_deg + * 0.0, // arg_perihelion_deg + * 0.0, // mean_anomaly_deg + * 2451545.0 // epoch_jd (J2000) + * }; + * BodycentricParams l2_center = BodycentricParams::heliocentric(l2_approx); + * + * // Now use l2_center as the reference for body-centric coordinates + * // to express Mars's position relative to L2. + * ``` + */ +struct BodycentricParams { + /// Keplerian orbital elements of the body. + Orbit orbit; + + /// Which standard center the orbit is defined relative to. + OrbitReferenceCenter orbit_center; + + /** + * @brief Creates parameters for a body with the given orbit. + * + * @param orb The Keplerian orbital elements. + * @param center The reference center for the orbit. + */ + BodycentricParams(const Orbit &orb, OrbitReferenceCenter center) + : orbit(orb), orbit_center(center) {} + + /** + * @brief Creates parameters for a body orbiting the Sun. + * + * Most common: planets, asteroids, comets. + * + * @param orb Heliocentric orbital elements. + * @return BodycentricParams with Heliocentric reference. + */ + static BodycentricParams heliocentric(const Orbit &orb) { + return BodycentricParams(orb, OrbitReferenceCenter::Heliocentric); + } + + /** + * @brief Creates parameters for a body orbiting Earth. + * + * Use for artificial satellites, the Moon, etc. + * + * @param orb Geocentric orbital elements. + * @return BodycentricParams with Geocentric reference. + */ + static BodycentricParams geocentric(const Orbit &orb) { + return BodycentricParams(orb, OrbitReferenceCenter::Geocentric); + } + + /** + * @brief Creates parameters for a body orbiting the barycenter. + * + * @param orb Barycentric orbital elements. + * @return BodycentricParams with Barycentric reference. + */ + static BodycentricParams barycentric(const Orbit &orb) { + return BodycentricParams(orb, OrbitReferenceCenter::Barycentric); + } + + /// Default: circular 1 AU heliocentric orbit (placeholder). + BodycentricParams() + : orbit{1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2451545.0}, + orbit_center(OrbitReferenceCenter::Heliocentric) {} + + /// Convert to C FFI struct for passing to siderust_to_bodycentric / + /// siderust_from_bodycentric. + SiderustBodycentricParams to_c() const { + SiderustBodycentricParams c{}; + c.orbit = orbit.to_c(); + c.orbit_center = static_cast(orbit_center); + return c; + } +}; + +// Stream operator for OrbitReferenceCenter +inline std::ostream &operator<<(std::ostream &os, + OrbitReferenceCenter center) { + switch (center) { + case OrbitReferenceCenter::Barycentric: + return os << "Barycentric"; + case OrbitReferenceCenter::Heliocentric: + return os << "Heliocentric"; + case OrbitReferenceCenter::Geocentric: + return os << "Geocentric"; + } + return os << "Unknown"; +} + +} // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index 63e0c05..bb538db 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -35,10 +35,12 @@ #include "body_target.hpp" #include "centers.hpp" #include "coordinates.hpp" +#include "coordinates/bodycentric_transforms.hpp" #include "ephemeris.hpp" #include "ffi_core.hpp" #include "frames.hpp" #include "lunar_phase.hpp" +#include "orbital_center.hpp" #include "observatories.hpp" #include "star_target.hpp" #include "target.hpp" diff --git a/siderust b/siderust index 17d986f..cd2eb5b 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 17d986fd5d1c9c258df50ce66f36fc524266d6e4 +Subproject commit cd2eb5b85f7f17db63cc9171283c67fa0f085e4a diff --git a/tests/test_bodycentric.cpp b/tests/test_bodycentric.cpp new file mode 100644 index 0000000..a4cf5fb --- /dev/null +++ b/tests/test_bodycentric.cpp @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; +using qtty::AstronomicalUnit; + +namespace { + +constexpr double J2000 = 2451545.0; +constexpr double KM_PER_AU = 149597870.7; + +// Satellite orbit at 0.0001 AU (~14 960 km) geocentric +Orbit satellite_orbit() { + return {0.0001, 0.0, 0.0, 0.0, 0.0, 0.0, J2000}; +} + +// Approximate Mars heliocentric orbit +Orbit mars_orbit() { + return {1.524, 0.0934, 1.85, 49.56, 286.5, 19.41, J2000}; +} + +double vec_magnitude(double x, double y, double z) { + return std::sqrt(x * x + y * y + z * z); +} + +} // namespace + +// ============================================================================ +// Kepler position +// ============================================================================ + +TEST(BodycentricTransforms, KeplerPositionGeocentricOrbit) { + const JulianDate jd(J2000); + auto pos = kepler_position(satellite_orbit(), jd); + + // Satellite at 0.0001 AU — distance should be close to 0.0001 AU + double r = std::sqrt(pos.x().value() * pos.x().value() + + pos.y().value() * pos.y().value() + + pos.z().value() * pos.z().value()); + EXPECT_NEAR(r, 0.0001, 1e-5); + EXPECT_TRUE(std::isfinite(pos.x().value())); + EXPECT_TRUE(std::isfinite(pos.y().value())); + EXPECT_TRUE(std::isfinite(pos.z().value())); +} + +TEST(BodycentricTransforms, KeplerPositionHeliocentricOrbit) { + const JulianDate jd(J2000); + auto pos = kepler_position(mars_orbit(), jd); // default C = Heliocentric + + double r = std::sqrt(pos.x().value() * pos.x().value() + + pos.y().value() * pos.y().value() + + pos.z().value() * pos.z().value()); + // Mars at ~1.52 AU from Sun + EXPECT_NEAR(r, 1.524, 0.15); + EXPECT_TRUE(std::isfinite(pos.x().value())); +} + +// ============================================================================ +// Geocentric source → Bodycentric (geocentric orbit) +// ============================================================================ + +TEST(BodycentricTransforms, GeocentricToBodycentricGeoOrbit) { + const JulianDate jd(J2000); + BodycentricParams params = BodycentricParams::geocentric(satellite_orbit()); + + // Target at 0.001 AU from Earth + cartesian::Position + target(0.001, 0.0, 0.0); + + auto result = to_bodycentric(target, params, jd); + + // Satellite is at ~0.0001 AU; relative position should be positive and < 0.001 AU + EXPECT_GT(result.x().value(), 0.0); + EXPECT_LT(result.x().value(), 0.001); + EXPECT_TRUE(std::isfinite(result.x().value())); + EXPECT_TRUE(std::isfinite(result.y().value())); + EXPECT_TRUE(std::isfinite(result.z().value())); + + // center_params round-trips correctly + EXPECT_NEAR(result.center_params().orbit.semi_major_axis_au, 0.0001, 1e-10); +} + +// ============================================================================ +// Heliocentric source → Bodycentric (heliocentric orbit) +// ============================================================================ + +TEST(BodycentricTransforms, HeliocentricToBodycentricHelioOrbit) { + const JulianDate jd(J2000); + BodycentricParams params = BodycentricParams::heliocentric(mars_orbit()); + + auto earth_helio = ephemeris::earth_heliocentric(jd); + auto result = to_bodycentric(earth_helio, params, jd); + + // Earth–Mars distance at J2000 ≈ 0.5–2.5 AU + double r = vec_magnitude(result.x().value(), result.y().value(), + result.z().value()); + EXPECT_GT(r, 0.3); + EXPECT_LT(r, 3.0); + EXPECT_TRUE(std::isfinite(r)); +} + +// ============================================================================ +// Round-Trip: Geocentric ↔ Bodycentric +// ============================================================================ + +TEST(BodycentricTransforms, RoundTripGeocentricBodycentric) { + const JulianDate jd(J2000); + BodycentricParams params = + BodycentricParams::geocentric(satellite_orbit()); + + cartesian::Position + original(0.001, 0.002, 0.003); + + auto bodycentric = to_bodycentric(original, params, jd); + auto recovered = bodycentric.to_geocentric(jd); + + EXPECT_NEAR(recovered.x().value(), original.x().value(), 1e-9); + EXPECT_NEAR(recovered.y().value(), original.y().value(), 1e-9); + EXPECT_NEAR(recovered.z().value(), original.z().value(), 1e-9); +} + +TEST(BodycentricTransforms, RoundTripHeliocentricBodycentric) { + const JulianDate jd(J2000); + BodycentricParams params = BodycentricParams::heliocentric(mars_orbit()); + + cartesian::Position + original(0.005, 0.003, 0.001); + + auto bodycentric = to_bodycentric(original, params, jd); + auto recovered = bodycentric.to_geocentric(jd); + + EXPECT_NEAR(recovered.x().value(), original.x().value(), 1e-9); + EXPECT_NEAR(recovered.y().value(), original.y().value(), 1e-9); + EXPECT_NEAR(recovered.z().value(), original.z().value(), 1e-9); +} + +// ============================================================================ +// Body at its own position → should be at origin +// ============================================================================ + +TEST(BodycentricTransforms, BodyOwnPositionAtOrigin) { + const JulianDate jd(J2000); + Orbit orbit = satellite_orbit(); + BodycentricParams params = BodycentricParams::geocentric(orbit); + + // Get the satellite's own geocentric position + auto body_geo = kepler_position(orbit, jd); + + // Transform to body-centric + auto body_from_body = to_bodycentric(body_geo, params, jd); + + double r = vec_magnitude(body_from_body.x().value(), + body_from_body.y().value(), + body_from_body.z().value()); + // The body's own position should be at (or very near) the origin + EXPECT_LT(r, 1e-10); +} + +// ============================================================================ +// Different source centers +// ============================================================================ + +TEST(BodycentricTransforms, HeliocentricOrbitWithGeocentricOrbit) { + const JulianDate jd(J2000); + // Geocentric orbit for the body, but heliocentric position for the target + BodycentricParams params = + BodycentricParams::geocentric(satellite_orbit()); + + // Moon geocentric at ~0.00257 AU + cartesian::Position + moon_geo(0.00257, 0.0, 0.0); + + auto moon_from_sat = to_bodycentric(moon_geo, params, jd); + + double r = vec_magnitude(moon_from_sat.x().value(), moon_from_sat.y().value(), + moon_from_sat.z().value()); + // Moon at ~384400 km, ISS at ~6378 km → distance should be close to moon distance + EXPECT_NEAR(r, 0.00257, 0.0002); +} + +// ============================================================================ +// FFI direct: null pointer guard + invalid center +// ============================================================================ + +TEST(BodycentricFFI, NullOutputPointer_ToBodycentric) { + siderust_cartesian_pos_t pos{1.0, 0.0, 0.0, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + SIDERUST_CENTER_T_HELIOCENTRIC}; + SiderustBodycentricParams params{}; + params.orbit = {1.0, 0.0, 0.0, 0.0, 0.0, 0.0, J2000}; + params.orbit_center = 1; // Heliocentric + + auto s = siderust_to_bodycentric(pos, params, J2000, nullptr); + EXPECT_EQ(s, SIDERUST_STATUS_T_NULL_POINTER); +} + +TEST(BodycentricFFI, NullOutputPointer_FromBodycentric) { + siderust_cartesian_pos_t pos{0.5, 0.0, 0.0, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + SIDERUST_CENTER_T_BODYCENTRIC}; + SiderustBodycentricParams params{}; + params.orbit = {1.0, 0.0, 0.0, 0.0, 0.0, 0.0, J2000}; + params.orbit_center = 1; + + auto s = siderust_from_bodycentric(pos, params, J2000, nullptr); + EXPECT_EQ(s, SIDERUST_STATUS_T_NULL_POINTER); +} + +TEST(BodycentricFFI, InvalidCenterInput) { + siderust_cartesian_pos_t pos{1.0, 0.0, 0.0, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000, + SIDERUST_CENTER_T_TOPOCENTRIC}; // unsupported center + siderust_cartesian_pos_t out{}; + SiderustBodycentricParams params{}; + params.orbit = {1.0, 0.0, 0.0, 0.0, 0.0, 0.0, J2000}; + params.orbit_center = 1; + + auto s = siderust_to_bodycentric(pos, params, J2000, &out); + EXPECT_EQ(s, SIDERUST_STATUS_T_INVALID_CENTER); +} + +TEST(BodycentricFFI, NullOutputPointer_KeplerPosition) { + siderust_orbit_t orbit{1.0, 0.0, 0.0, 0.0, 0.0, 0.0, J2000}; + auto s = siderust_kepler_position(orbit, J2000, nullptr); + EXPECT_EQ(s, SIDERUST_STATUS_T_NULL_POINTER); +} + +TEST(BodycentricFFI, KeplerPositionReturnsFinite) { + siderust_orbit_t orbit{1.524, 0.0934, 1.85, 49.56, 286.5, 19.41, J2000}; + siderust_cartesian_pos_t out{}; + auto s = siderust_kepler_position(orbit, J2000, &out); + EXPECT_EQ(s, SIDERUST_STATUS_T_OK); + EXPECT_TRUE(std::isfinite(out.x)); + EXPECT_TRUE(std::isfinite(out.y)); + EXPECT_TRUE(std::isfinite(out.z)); + EXPECT_EQ(out.frame, SIDERUST_FRAME_T_ECLIPTIC_MEAN_J2000); +} + +// ============================================================================ +// Venus VSOP87 (using new ephemeris function) +// ============================================================================ + +TEST(BodycentricTransforms, VenusHeliocentricIsFinite) { + const JulianDate jd(J2000); + auto venus = ephemeris::venus_heliocentric(jd); + EXPECT_TRUE(std::isfinite(venus.x().value())); + EXPECT_TRUE(std::isfinite(venus.y().value())); + EXPECT_TRUE(std::isfinite(venus.z().value())); + + double r = vec_magnitude(venus.x().value(), venus.y().value(), venus.z().value()); + // Venus at ~0.72 AU from Sun + EXPECT_NEAR(r, 0.72, 0.05); +} From fcd72ba6d86794b82234d23294fe8de318b8149f Mon Sep 17 00:00:00 2001 From: VPRamon Date: Wed, 25 Feb 2026 17:03:04 +0100 Subject: [PATCH 10/19] feat: add basic_coordinates_example and update CMake configuration --- CMakeLists.txt | 9 +++++ examples/01_basic_coordinates.cpp | 62 +++++++++++++++++++++++++++++++ siderust | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 examples/01_basic_coordinates.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3898ffb..7d21ab1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,15 @@ if(DEFINED _siderust_rpath) ) endif() +add_executable(basic_coordinates_example examples/01_basic_coordinates.cpp) +target_link_libraries(basic_coordinates_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(basic_coordinates_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + add_executable(coordinate_systems_example examples/coordinate_systems_example.cpp) target_link_libraries(coordinate_systems_example PRIVATE siderust_cpp) if(DEFINED _siderust_rpath) diff --git a/examples/01_basic_coordinates.cpp b/examples/01_basic_coordinates.cpp new file mode 100644 index 0000000..7420cdb --- /dev/null +++ b/examples/01_basic_coordinates.cpp @@ -0,0 +1,62 @@ +/** + * @file 01_basic_coordinates.cpp + * @brief C++ port of siderust/examples/01_basic_coordinates.rs + */ + +#include +#include + +#include + +int main() { + using namespace siderust; + using namespace qtty::literals; + + std::cout << "=== 01_basic_coordinates (C++) ===\n"; + + // Time + const JulianDate jd = JulianDate::J2000; + + // ========================================================================== + // Cartesian coordinates (heliocentric example) + // ========================================================================== + auto earth = ephemeris::earth_heliocentric(jd); + std::cout << "Earth heliocentric (EclipticMeanJ2000):\n"; + std::cout << " X = " << earth.x() << "\n"; + std::cout << " Y = " << earth.y() << "\n"; + std::cout << " Z = " << earth.z() << "\n"; + std::cout << " Distance = " << earth.distance() << "\n\n"; + + // ========================================================================== + // Spherical direction and frame conversions + // ========================================================================== + const Geodetic site(-17.8947_deg, 28.7606_deg, 2396.0_m); + + spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); + auto vega_ecl = vega_icrs.to_frame(jd); + auto vega_true = vega_icrs.to_frame(jd); + auto vega_horiz = vega_icrs.to_horizontal(jd, site); + + std::cout << "Direction transforms:\n"; + std::cout << " ICRS RA/Dec: " << vega_icrs << "\n"; + std::cout << " Ecliptic lon/lat: " << vega_ecl << "\n"; + std::cout << " True-of-date RA/Dec: " << vega_true << "\n"; + std::cout << " Horizontal az/alt: " << vega_horiz << "\n\n"; + + // ========================================================================== + // Directions <-> Positions + // ========================================================================== + spherical::position::ICRS synthetic_star(210.0_deg, -12.0_deg, 4.2_au); + std::cout << "Typed positions:\n"; + std::cout << " Synthetic star distance: " << synthetic_star.distance() << "\n"; + + // ========================================================================== + // Type safety demonstration + // ========================================================================== + const auto ecef_m = site.to_cartesian(); + static_assert(std::is_same_v>); + std::cout << "Geodetic -> ECEF: " << site << " -> " << ecef_m << "\n"; + + std::cout << "=== Example Complete ===\n"; + return 0; +} diff --git a/siderust b/siderust index cd2eb5b..15da06f 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit cd2eb5b85f7f17db63cc9171283c67fa0f085e4a +Subproject commit 15da06f875eb5c475dfc078a33f793502f766ab5 From bd35926c271f5ea5e327a654b6299c97c0051ac2 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Thu, 26 Feb 2026 01:01:38 +0100 Subject: [PATCH 11/19] Add examples for serialization, Kepler orbits, root finding, moon phases, and trackable targets - Implemented `17_serde_serialization.cpp` to demonstrate manual JSON serialization for siderust types. - Added `18_kepler_orbit.cpp` to showcase Keplerian orbit propagation with a manual solver. - Created `19_brent_root_finding.cpp` to illustrate a manual implementation of Brent's root-finding method. - Developed `20_moon_phase.cpp` to demonstrate lunar phase calculations and event searches. - Introduced `21_trackable_demo.cpp` to showcase polymorphism in handling various astronomical targets. Updated headers to include new position conversion methods between Cartesian and spherical coordinates. --- CMakeLists.txt | 184 ++++++++++++++ examples/01_basic_coordinates.cpp | 2 +- examples/02_coordinate_transformations.cpp | 238 ++++++++++++++++++ examples/03_all_frame_conversions.cpp | 156 ++++++++++++ examples/04_all_center_conversions.cpp | 197 +++++++++++++++ examples/05_time_periods.cpp | 80 ++++++ examples/06_astronomical_night.cpp | 68 +++++ examples/07_find_night_periods_365day.cpp | 56 +++++ examples/08_night_quality_scoring.cpp | 109 ++++++++ examples/09_star_observability.cpp | 91 +++++++ examples/10_altitude_periods_trait.cpp | 140 +++++++++++ examples/11_compare_sun_moon_star.cpp | 108 ++++++++ examples/12_solar_system_example.cpp | 138 ++++++++++ examples/13_observer_coordinates.cpp | 148 +++++++++++ examples/14_bodycentric_coordinates.cpp | 144 +++++++++++ examples/15_targets_proper_motion.cpp | 128 ++++++++++ examples/16_jpl_precise_ephemeris.cpp | 87 +++++++ examples/17_serde_serialization.cpp | 121 +++++++++ examples/18_kepler_orbit.cpp | 166 ++++++++++++ examples/19_brent_root_finding.cpp | 169 +++++++++++++ examples/20_moon_phase.cpp | 142 +++++++++++ examples/21_trackable_demo.cpp | 174 +++++++++++++ include/siderust/coordinates.hpp | 1 + include/siderust/coordinates/cartesian.hpp | 8 + .../siderust/coordinates/pos_conversions.hpp | 51 ++++ include/siderust/coordinates/spherical.hpp | 7 + siderust | 2 +- 27 files changed, 2913 insertions(+), 2 deletions(-) create mode 100644 examples/02_coordinate_transformations.cpp create mode 100644 examples/03_all_frame_conversions.cpp create mode 100644 examples/04_all_center_conversions.cpp create mode 100644 examples/05_time_periods.cpp create mode 100644 examples/06_astronomical_night.cpp create mode 100644 examples/07_find_night_periods_365day.cpp create mode 100644 examples/08_night_quality_scoring.cpp create mode 100644 examples/09_star_observability.cpp create mode 100644 examples/10_altitude_periods_trait.cpp create mode 100644 examples/11_compare_sun_moon_star.cpp create mode 100644 examples/12_solar_system_example.cpp create mode 100644 examples/13_observer_coordinates.cpp create mode 100644 examples/14_bodycentric_coordinates.cpp create mode 100644 examples/15_targets_proper_motion.cpp create mode 100644 examples/16_jpl_precise_ephemeris.cpp create mode 100644 examples/17_serde_serialization.cpp create mode 100644 examples/18_kepler_orbit.cpp create mode 100644 examples/19_brent_root_finding.cpp create mode 100644 examples/20_moon_phase.cpp create mode 100644 examples/21_trackable_demo.cpp create mode 100644 include/siderust/coordinates/pos_conversions.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d21ab1..1a37a2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,15 @@ if(DEFINED _siderust_rpath) ) endif() +add_executable(coordinate_transformations_example examples/02_coordinate_transformations.cpp) +target_link_libraries(coordinate_transformations_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(coordinate_transformations_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + add_executable(solar_system_bodies_example examples/solar_system_bodies_example.cpp) target_link_libraries(solar_system_bodies_example PRIVATE siderust_cpp) if(DEFINED _siderust_rpath) @@ -234,6 +243,181 @@ if(DEFINED _siderust_rpath) ) endif() +# --------------------------------------------------------------------------- +# Numbered mirror examples (03–21, mirroring siderust Rust examples) +# --------------------------------------------------------------------------- + +add_executable(03_all_frame_conversions_example examples/03_all_frame_conversions.cpp) +target_link_libraries(03_all_frame_conversions_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(03_all_frame_conversions_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(04_all_center_conversions_example examples/04_all_center_conversions.cpp) +target_link_libraries(04_all_center_conversions_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(04_all_center_conversions_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(05_time_periods_example examples/05_time_periods.cpp) +target_link_libraries(05_time_periods_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(05_time_periods_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(06_astronomical_night_example examples/06_astronomical_night.cpp) +target_link_libraries(06_astronomical_night_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(06_astronomical_night_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(07_find_night_periods_365day_example examples/07_find_night_periods_365day.cpp) +target_link_libraries(07_find_night_periods_365day_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(07_find_night_periods_365day_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(08_night_quality_scoring_example examples/08_night_quality_scoring.cpp) +target_link_libraries(08_night_quality_scoring_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(08_night_quality_scoring_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(09_star_observability_example examples/09_star_observability.cpp) +target_link_libraries(09_star_observability_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(09_star_observability_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(10_altitude_periods_trait_example examples/10_altitude_periods_trait.cpp) +target_link_libraries(10_altitude_periods_trait_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(10_altitude_periods_trait_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(11_compare_sun_moon_star_example examples/11_compare_sun_moon_star.cpp) +target_link_libraries(11_compare_sun_moon_star_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(11_compare_sun_moon_star_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(12_solar_system_example examples/12_solar_system_example.cpp) +target_link_libraries(12_solar_system_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(12_solar_system_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(13_observer_coordinates_example examples/13_observer_coordinates.cpp) +target_link_libraries(13_observer_coordinates_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(13_observer_coordinates_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(14_bodycentric_coordinates_example examples/14_bodycentric_coordinates.cpp) +target_link_libraries(14_bodycentric_coordinates_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(14_bodycentric_coordinates_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(15_targets_proper_motion_example examples/15_targets_proper_motion.cpp) +target_link_libraries(15_targets_proper_motion_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(15_targets_proper_motion_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(16_jpl_precise_ephemeris_example examples/16_jpl_precise_ephemeris.cpp) +target_link_libraries(16_jpl_precise_ephemeris_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(16_jpl_precise_ephemeris_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(17_serde_serialization_example examples/17_serde_serialization.cpp) +target_link_libraries(17_serde_serialization_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(17_serde_serialization_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(18_kepler_orbit_example examples/18_kepler_orbit.cpp) +target_link_libraries(18_kepler_orbit_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(18_kepler_orbit_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(19_brent_root_finding_example examples/19_brent_root_finding.cpp) +target_link_libraries(19_brent_root_finding_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(19_brent_root_finding_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(20_moon_phase_example examples/20_moon_phase.cpp) +target_link_libraries(20_moon_phase_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(20_moon_phase_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + +add_executable(21_trackable_demo_example examples/21_trackable_demo.cpp) +target_link_libraries(21_trackable_demo_example PRIVATE siderust_cpp) +if(DEFINED _siderust_rpath) + set_target_properties(21_trackable_demo_example PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) +endif() + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- diff --git a/examples/01_basic_coordinates.cpp b/examples/01_basic_coordinates.cpp index 7420cdb..d39fa4b 100644 --- a/examples/01_basic_coordinates.cpp +++ b/examples/01_basic_coordinates.cpp @@ -15,7 +15,7 @@ int main() { std::cout << "=== 01_basic_coordinates (C++) ===\n"; // Time - const JulianDate jd = JulianDate::J2000; + const JulianDate jd = JulianDate::J2000(); // ========================================================================== // Cartesian coordinates (heliocentric example) diff --git a/examples/02_coordinate_transformations.cpp b/examples/02_coordinate_transformations.cpp new file mode 100644 index 0000000..9111ed7 --- /dev/null +++ b/examples/02_coordinate_transformations.cpp @@ -0,0 +1,238 @@ +/** + * @file 02_coordinate_transformations.cpp + * @brief C++ port of siderust/examples/02_coordinate_transformations.rs + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + + +int main() { + using AU = qtty::AstronomicalUnit; + + std::cout << "=== Coordinate Transformations (C++) ===\n\n"; + + const JulianDate jd = JulianDate::J2000(); + std::cout << "Reference time: J2000.0 (JD " << std::fixed << std::setprecision(1) + << jd.value() << ")\n\n"; + + // 1. Frame transformations (same center) + std::cout << "1. FRAME TRANSFORMATIONS\n"; + std::cout << "------------------------\n"; + + cartesian::position::EclipticMeanJ2000 pos_ecl(1.0, 0.0, 0.0); + std::cout << "Original (Heliocentric EclipticMeanJ2000):\n"; + std::cout << " X = " << pos_ecl.x() << " AU\n"; + std::cout << " Y = " << pos_ecl.y() << " AU\n"; + std::cout << " Z = " << pos_ecl.z() << " AU\n\n"; + + // to Equatorial (same heliocentric center) + auto sph_ecl = pos_ecl.to_spherical(); + auto dir_equ = sph_ecl.direction().to_frame(jd); + spherical::Position + sph_equ(dir_equ, sph_ecl.distance()); + auto pos_equ = sph_equ.to_cartesian(); + + std::cout << "Transformed to EquatorialMeanJ2000 frame:\n"; + std::cout << " X = " << pos_equ.x() << " AU\n"; + std::cout << " Y = " << pos_equ.y() << " AU\n"; + std::cout << " Z = " << pos_equ.z() << " AU\n\n"; + + // to ICRS (via equatorial hub) + auto dir_icrs = sph_equ.direction().to_frame(jd); + spherical::Position sph_icrs(dir_icrs, + sph_equ.distance()); + auto pos_icrs = sph_icrs.to_cartesian(); + std::cout << "Transformed to ICRS frame:\n"; + std::cout << " X = " << pos_icrs.x() << " AU\n"; + std::cout << " Y = " << pos_icrs.y() << " AU\n"; + std::cout << " Z = " << pos_icrs.z() << " AU\n\n"; + + // 2. Center transformations (same frame) + std::cout << "2. CENTER TRANSFORMATIONS\n"; + std::cout << "-------------------------\n"; + + auto earth_helio = ephemeris::earth_heliocentric(jd); + std::cout << "Earth (Heliocentric EclipticMeanJ2000):\n"; + std::cout << " X = " << earth_helio.x() << " AU\n"; + std::cout << " Y = " << earth_helio.y() << " AU\n"; + std::cout << " Z = " << earth_helio.z() << " AU\n"; + std::cout << " Distance = " << earth_helio.distance() << " AU\n\n"; + + // Earth in geocentric (origin) — heliocentric minus itself -> zero + cartesian::Position + earth_geo(AU(0.0), AU(0.0), AU(0.0)); + std::cout << "Earth (Geocentric EclipticMeanJ2000) - at origin:\n"; + std::cout << " X = " << earth_geo.x() << " AU\n"; + std::cout << " Y = " << earth_geo.y() << " AU\n"; + std::cout << " Z = " << earth_geo.z() << " AU\n\n"; + std::cout << " Distance = " << earth_geo.distance() << " AU (should be ~0)\n\n"; + + auto mars_helio = ephemeris::mars_heliocentric(jd); + std::cout << "Mars (Heliocentric EclipticMeanJ2000):\n"; + std::cout << " X = " << mars_helio.x() << " AU\n"; + std::cout << " Y = " << mars_helio.y() << " AU\n"; + std::cout << " Z = " << mars_helio.z() << " AU\n"; + std::cout << " Distance = " << mars_helio.distance() << " AU\n\n"; + + // Mars geocentric = Mars_helio - Earth_helio (component-wise) + cartesian::Position + mars_geo(mars_helio.x() - earth_helio.x(), mars_helio.y() - earth_helio.y(), + mars_helio.z() - earth_helio.z()); + std::cout << "Mars (Geocentric EclipticMeanJ2000) - as seen from Earth:\n"; + std::cout << " X = " << mars_geo.x() << " AU\n"; + std::cout << " Y = " << mars_geo.y() << " AU\n"; + std::cout << " Z = " << mars_geo.z() << " AU\n"; + std::cout << " Distance = " << mars_geo.distance() << " AU\n\n"; + + // 3. Combined transformations (center + frame) + std::cout << "3. COMBINED TRANSFORMATIONS\n"; + std::cout << "---------------------------\n"; + + std::cout << "Mars transformation chain:\n"; + std::cout << " Start: Heliocentric EclipticMeanJ2000\n"; + + // Step 1: convert Mars heliocentric ecl -> heliocentric equatorial + auto sph_mars_helio = mars_helio.to_spherical(); + auto dir_mars_helio_equ = sph_mars_helio.direction().to_frame(jd); + spherical::Position + sph_mars_helio_equ(dir_mars_helio_equ, sph_mars_helio.distance()); + auto mars_helio_equ = sph_mars_helio_equ.to_cartesian(); + std::cout << " Step 1: Transform frame → Heliocentric EquatorialMeanJ2000\n"; + + // Step 2: convert center heliocentric -> geocentric by subtracting Earth's heliocentric equ + auto sph_earth_helio = earth_helio.to_spherical(); + auto dir_earth_helio_equ = sph_earth_helio.direction().to_frame(jd); + spherical::Position + sph_earth_helio_equ(dir_earth_helio_equ, sph_earth_helio.distance()); + auto earth_helio_equ = sph_earth_helio_equ.to_cartesian(); + + cartesian::Position + mars_geo_equ(mars_helio_equ.x() - earth_helio_equ.x(), + mars_helio_equ.y() - earth_helio_equ.y(), + mars_helio_equ.z() - earth_helio_equ.z()); + + std::cout << " Step 2: Transform center → Geocentric EquatorialMeanJ2000\n"; + std::cout << " Result:\n"; + std::cout << " X = " << mars_geo_equ.x() << " AU\n"; + std::cout << " Y = " << mars_geo_equ.y() << " AU\n"; + std::cout << " Z = " << mars_geo_equ.z() << " AU\n\n"; + + // Method 2: do the same in one direct chain (frame then center) + auto dir_direct = sph_mars_helio.direction().to_frame(jd); + spherical::Position + sph_direct(dir_direct, sph_mars_helio.distance()); + auto sph_direct_cart = sph_direct.to_cartesian(); + auto Mars_geo_equ_direct = cartesian::Position( + sph_direct_cart.x() - earth_helio_equ.x(), + sph_direct_cart.y() - earth_helio_equ.y(), + sph_direct_cart.z() - earth_helio_equ.z()); + + std::cout << " Or using direct chain (same result):\n"; + std::cout << " X = " << Mars_geo_equ_direct.x() << " AU\n"; + std::cout << " Y = " << Mars_geo_equ_direct.y() << " AU\n"; + std::cout << " Z = " << Mars_geo_equ_direct.z() << " AU\n\n"; + + // 4. Barycentric coordinates + std::cout << "4. BARYCENTRIC COORDINATES\n"; + std::cout << "--------------------------\n"; + + auto earth_bary = ephemeris::earth_barycentric(jd); + std::cout << "Earth (Barycentric EclipticMeanJ2000):\n"; + std::cout << " X = " << earth_bary.x() << " AU\n"; + std::cout << " Y = " << earth_bary.y() << " AU\n"; + std::cout << " Z = " << earth_bary.z() << " AU\n"; + std::cout << " Distance from SSB = " << earth_bary.distance() << " AU\n\n"; + + // Mars barycentric = sun_barycentric + mars_helio + auto sun_bary = ephemeris::sun_barycentric(jd); + cartesian::Position + mars_bary(sun_bary.x() + mars_helio.x(), sun_bary.y() + mars_helio.y(), + sun_bary.z() + mars_helio.z()); + + // Transform to geocentric (barycentric -> geocentric = target_bary - earth_bary) + auto mars_geo_from_bary = cartesian::Position( + mars_bary.x() - earth_bary.x(), mars_bary.y() - earth_bary.y(), + mars_bary.z() - earth_bary.z()); + + std::cout << "Mars (Geocentric, from Barycentric):\n"; + std::cout << " X = " << mars_geo_from_bary.x() << " AU\n"; + std::cout << " Y = " << mars_geo_from_bary.y() << " AU\n"; + std::cout << " Z = " << mars_geo_from_bary.z() << " AU\n"; + std::cout << " Distance = " << mars_geo_from_bary.distance() << " AU\n\n"; + + // 5. ICRS frame transformations (barycentric -> geocentric) + std::cout << "5. ICRS FRAME TRANSFORMATIONS\n"; + std::cout << "-----------------------------\n"; + + // Create a sample star in barycentric ICRS cartesian coords + cartesian::Position star_icrs(AU(100.0), AU(50.0), AU(1000.0)); + std::cout << "Star (Barycentric ICRS):\n"; + std::cout << " X = " << star_icrs.x() << " AU\n"; + std::cout << " Y = " << star_icrs.y() << " AU\n"; + std::cout << " Z = " << star_icrs.z() << " AU\n\n"; + + // Convert Earth's barycentric from ecliptic -> ICRS frame, then subtract + auto sph_earth_bary = earth_bary.to_spherical(); + auto dir_earth_bary_icrs = sph_earth_bary.direction().to_frame(jd); + spherical::Position sph_earth_bary_icrs( + dir_earth_bary_icrs, sph_earth_bary.distance()); + auto earth_bary_icrs = sph_earth_bary_icrs.to_cartesian(); + + // Star geocentric ICRS (GCRS-equivalent for this demo) + auto star_gcrs = cartesian::Position( + star_icrs.x() - earth_bary_icrs.x(), star_icrs.y() - earth_bary_icrs.y(), + star_icrs.z() - earth_bary_icrs.z()); + + std::cout << "Star (Geocentric ICRS/GCRS):\n"; + std::cout << " X = " << star_gcrs.x() << " AU\n"; + std::cout << " Y = " << star_gcrs.y() << " AU\n"; + std::cout << " Z = " << star_gcrs.z() << " AU\n\n"; + + // 6. Round-trip transformation + std::cout << "6. ROUND-TRIP TRANSFORMATION\n"; + std::cout << "----------------------------\n"; + + auto original = mars_helio; + std::cout << "Original Mars (Heliocentric EclipticMeanJ2000):\n"; + std::cout << " X = " << original.x() << " AU\n"; + std::cout << " Y = " << original.y() << " AU\n"; + std::cout << " Z = " << original.z() << " AU\n\n"; + + // Helio Ecl -> Geo Equ -> back to Helio Ecl + // (we reuse previously computed earth_helio_equ) + auto temp = mars_geo_equ; // geocentric equ + + // Recover heliocentric equ by adding Earth's heliocentric equ + auto recovered_helio_equ = cartesian::Position( + temp.x() + earth_helio_equ.x(), temp.y() + earth_helio_equ.y(), temp.z() + earth_helio_equ.z()); + + // Convert recovered heliocentric equ back to heliocentric ecliptic + auto sph_recovered_equ = recovered_helio_equ.to_spherical(); + auto dir_recovered_ecl = sph_recovered_equ.direction().to_frame(jd); + spherical::Position + sph_recovered_ecl(dir_recovered_ecl, sph_recovered_equ.distance()); + auto recovered = sph_recovered_ecl.to_cartesian(); + + std::cout << "After round-trip transformation:\n"; + std::cout << " X = " << recovered.x() << " AU\n"; + std::cout << " Y = " << recovered.y() << " AU\n"; + std::cout << " Z = " << recovered.z() << " AU\n\n"; + + const double diff_x = std::abs(original.x().value() - recovered.x().value()); + const double diff_y = std::abs(original.y().value() - recovered.y().value()); + const double diff_z = std::abs(original.z().value() - recovered.z().value()); + std::cout << "Differences (should be tiny):\n"; + std::cout << " \u0394X = " << diff_x << "\n"; + std::cout << " \u0394Y = " << diff_y << "\n"; + std::cout << " \u0394Z = " << diff_z << "\n\n"; + + std::cout << "=== Example Complete ===\n"; + return 0; +} diff --git a/examples/03_all_frame_conversions.cpp b/examples/03_all_frame_conversions.cpp new file mode 100644 index 0000000..d5f5f3f --- /dev/null +++ b/examples/03_all_frame_conversions.cpp @@ -0,0 +1,156 @@ +/** + * @file 03_all_frame_conversions.cpp + * @brief C++ port of siderust/examples/23_all_frame_conversions.rs + * + * Demonstrates all supported direction frame-rotation pairs using the + * `direction.to_frame(jd)` API. + * + * Supported frame pairs (via ICRS hub): + * ICRS <-> EclipticMeanJ2000 + * ICRS <-> EquatorialMeanJ2000 + * ICRS <-> EquatorialMeanOfDate + * ICRS <-> EquatorialTrueOfDate + * EclipticMeanJ2000 <-> EquatorialMeanJ2000/OfDate/TrueOfDate + * EquatorialMean* <-> EquatorialTrue* + * Any -> Horizontal via `.to_horizontal(jd, observer)` + * + * Run with: + * cmake --build build --target 03_all_frame_conversions_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace qtty::literals; + +int main() { + std::cout << "=== All Frame Conversions ===\n\n"; + + const JulianDate jd = JulianDate::J2000(); + std::cout << "Epoch: J2000.0 (JD " << std::fixed << std::setprecision(1) + << jd.value() << ")\n\n"; + + // Vega in ICRS (RA=279.2348 deg, Dec=38.7837 deg) + const spherical::direction::ICRS vega_icrs(279.2348_deg, 38.7837_deg); + std::cout << "Reference: Vega\n"; + std::cout << " ICRS RA=" << std::fixed << std::setprecision(4) + << vega_icrs.ra().value() + << " Dec=" << vega_icrs.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // ICRS -> EclipticMeanJ2000 + // ------------------------------------------------------------------------- + std::cout << "--- ICRS -> EclipticMeanJ2000 ---\n"; + auto d_ecl = vega_icrs.to_frame(jd); + std::cout << " lon=" << std::setprecision(4) << d_ecl.longitude().value() + << " lat=" << d_ecl.latitude().value() << " deg\n"; + auto rt_ecl = d_ecl.to_frame(jd); + std::cout << " round-trip RA err: " << std::scientific + << std::abs(rt_ecl.ra().value() - vega_icrs.ra().value()) << "\n\n"; + + // ------------------------------------------------------------------------- + // ICRS -> EquatorialMeanJ2000 + // ------------------------------------------------------------------------- + std::cout << "--- ICRS -> EquatorialMeanJ2000 ---\n"; + auto d_eqj = vega_icrs.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqj.ra().value() + << " Dec=" << d_eqj.dec().value() << " deg\n"; + auto rt_eqj = d_eqj.to_frame(jd); + std::cout << " round-trip RA err: " << std::scientific + << std::abs(rt_eqj.ra().value() - vega_icrs.ra().value()) << "\n\n"; + + // ------------------------------------------------------------------------- + // ICRS -> EquatorialMeanOfDate + // ------------------------------------------------------------------------- + std::cout << "--- ICRS -> EquatorialMeanOfDate ---\n"; + auto d_eqmod = vega_icrs.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqmod.ra().value() + << " Dec=" << d_eqmod.dec().value() << " deg\n"; + auto rt_eqmod = d_eqmod.to_frame(jd); + std::cout << " round-trip RA err: " << std::scientific + << std::abs(rt_eqmod.ra().value() - vega_icrs.ra().value()) << "\n\n"; + + // ------------------------------------------------------------------------- + // ICRS -> EquatorialTrueOfDate + // ------------------------------------------------------------------------- + std::cout << "--- ICRS -> EquatorialTrueOfDate ---\n"; + auto d_eqtod = vega_icrs.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqtod.ra().value() + << " Dec=" << d_eqtod.dec().value() << " deg\n"; + auto rt_eqtod = d_eqtod.to_frame(jd); + std::cout << " round-trip RA err: " << std::scientific + << std::abs(rt_eqtod.ra().value() - vega_icrs.ra().value()) << "\n\n"; + + // ------------------------------------------------------------------------- + // EclipticMeanJ2000 -> EquatorialMeanJ2000 + // ------------------------------------------------------------------------- + std::cout << "--- EclipticMeanJ2000 -> EquatorialMeanJ2000 ---\n"; + auto d_ecl_to_eqj = d_ecl.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_ecl_to_eqj.ra().value() + << " Dec=" << d_ecl_to_eqj.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // EclipticMeanJ2000 -> EquatorialMeanOfDate + // ------------------------------------------------------------------------- + std::cout << "--- EclipticMeanJ2000 -> EquatorialMeanOfDate ---\n"; + auto d_ecl_to_mod = d_ecl.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_ecl_to_mod.ra().value() + << " Dec=" << d_ecl_to_mod.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // EclipticMeanJ2000 -> EquatorialTrueOfDate + // ------------------------------------------------------------------------- + std::cout << "--- EclipticMeanJ2000 -> EquatorialTrueOfDate ---\n"; + auto d_ecl_to_tod = d_ecl.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_ecl_to_tod.ra().value() + << " Dec=" << d_ecl_to_tod.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // EquatorialMeanJ2000 -> EquatorialMeanOfDate + // ------------------------------------------------------------------------- + std::cout << "--- EquatorialMeanJ2000 -> EquatorialMeanOfDate ---\n"; + auto d_eqj_to_mod = d_eqj.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqj_to_mod.ra().value() + << " Dec=" << d_eqj_to_mod.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // EquatorialMeanJ2000 -> EquatorialTrueOfDate + // ------------------------------------------------------------------------- + std::cout << "--- EquatorialMeanJ2000 -> EquatorialTrueOfDate ---\n"; + auto d_eqj_to_tod = d_eqj.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqj_to_tod.ra().value() + << " Dec=" << d_eqj_to_tod.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // EquatorialMeanOfDate -> EquatorialTrueOfDate + // ------------------------------------------------------------------------- + std::cout << "--- EquatorialMeanOfDate -> EquatorialTrueOfDate ---\n"; + auto d_mod_to_tod = d_eqmod.to_frame(jd); + std::cout << " RA=" << std::fixed << std::setprecision(4) << d_mod_to_tod.ra().value() + << " Dec=" << d_mod_to_tod.dec().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // ICRS -> Horizontal (alt-az) — requires observer location + // ------------------------------------------------------------------------- + std::cout << "--- ICRS -> Horizontal (Roque de los Muchachos) ---\n"; + const Geodetic obs(-17.8947_deg, 28.7606_deg, 2396.0_m); + auto d_horiz = vega_icrs.to_horizontal(jd, obs); + std::cout << " Az=" << std::fixed << std::setprecision(4) << d_horiz.az().value() + << " Alt=" << d_horiz.altitude().value() << " deg\n\n"; + + // ------------------------------------------------------------------------- + // EclipticMeanJ2000 -> Horizontal + // ------------------------------------------------------------------------- + std::cout << "--- EclipticMeanJ2000 -> Horizontal ---\n"; + auto d_ecl_horiz = d_ecl.to_horizontal(jd, obs); + std::cout << " Az=" << std::fixed << std::setprecision(4) << d_ecl_horiz.az().value() + << " Alt=" << d_ecl_horiz.altitude().value() << " deg\n\n"; + + std::cout << "=== Done ===\n"; + return 0; +} diff --git a/examples/04_all_center_conversions.cpp b/examples/04_all_center_conversions.cpp new file mode 100644 index 0000000..956ac17 --- /dev/null +++ b/examples/04_all_center_conversions.cpp @@ -0,0 +1,197 @@ +/** + * @file 04_all_center_conversions.cpp + * @brief C++ port of siderust/examples/22_all_center_conversions.rs + * + * Demonstrates all supported center-shift pairs with round-trip error metric: + * - Barycentric <-> Heliocentric + * - Barycentric <-> Geocentric + * - Heliocentric <-> Geocentric + * + * NOTE: The Rust library exposes an automatic `to_center()` trait API backed + * by a `CenterShiftProvider` pattern. In C++ this is not yet fully bound; + * center shifts are performed here manually using VSOP87 ephemeris offsets, + * which is the same underlying calculation. + * + * TODO: When `CenterShiftProvider` is bound in C++, replace the manual + * arithmetic below with the typed transform calls. + * + * Run with: cmake --build build --target all_center_conversions_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +using AU = qtty::AstronomicalUnit; + +// --------------------------------------------------------------------------- +// Compute Euclidean error between two cartesian positions (same type erased) +// --------------------------------------------------------------------------- +static double cart_error(double ax, double ay, double az, + double bx, double by, double bz) { + double dx = ax - bx, dy = ay - by, dz = az - bz; + return std::sqrt(dx * dx + dy * dy + dz * dz); +} + +int main() { + const JulianDate jd = JulianDate(2'460'000.5); + std::cout << "Center conversion demo at JD(TT) = " << std::fixed + << std::setprecision(1) << jd.value() << "\n\n"; + + // Fetch SSB offsets via ephemeris (same data the Rust provider uses) + auto sun_bary = ephemeris::sun_barycentric(jd); // Sun relative to SSB + auto earth_bary = ephemeris::earth_barycentric(jd); // Earth relative to SSB + + // A sample physical point defined in barycentric coords + cartesian::Position + p_bary(AU(0.40), AU(-0.10), AU(1.20)); + + // ------------------------------------------------------------------------- + // Derive heliocentric and geocentric equivalents + // helio = bary - sun_bary + // geo = bary - earth_bary + // ------------------------------------------------------------------------- + cartesian::Position + p_helio(p_bary.x() - sun_bary.x(), + p_bary.y() - sun_bary.y(), + p_bary.z() - sun_bary.z()); + + cartesian::Position + p_geo(p_bary.x() - earth_bary.x(), + p_bary.y() - earth_bary.y(), + p_bary.z() - earth_bary.z()); + + const auto fmt = [](double val) { + return val; // just pass through for cout + }; + + auto print_row = [&](const char *from, const char *to, + double ox, double oy, double oz, + double err) { + std::cout << std::left << std::setw(14) << from + << " -> " << std::setw(14) << to + << " out=(" << std::fixed << std::setprecision(9) + << std::setw(13) << ox << ", " + << std::setw(13) << oy << ", " + << std::setw(13) << oz << ") " + << "roundtrip=" << std::scientific << std::setprecision(3) + << err << "\n"; + }; + + // ------------------------------------------------------------------------- + // Barycentric source + // ------------------------------------------------------------------------- + + // Bary -> Bary (identity) + print_row("Barycentric", "Barycentric", + p_bary.x().value(), p_bary.y().value(), p_bary.z().value(), 0.0); + + // Bary -> Helio + { + // back: helio -> bary = helio + sun_bary + cartesian::Position + back(p_helio.x() + sun_bary.x(), + p_helio.y() + sun_bary.y(), + p_helio.z() + sun_bary.z()); + double err = cart_error(p_bary.x().value(), p_bary.y().value(), p_bary.z().value(), + back.x().value(), back.y().value(), back.z().value()); + print_row("Barycentric", "Heliocentric", + p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), err); + } + + // Bary -> Geo + { + cartesian::Position + back(p_geo.x() + earth_bary.x(), + p_geo.y() + earth_bary.y(), + p_geo.z() + earth_bary.z()); + double err = cart_error(p_bary.x().value(), p_bary.y().value(), p_bary.z().value(), + back.x().value(), back.y().value(), back.z().value()); + print_row("Barycentric", "Geocentric", + p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), err); + } + + // ------------------------------------------------------------------------- + // Heliocentric source + // ------------------------------------------------------------------------- + print_row("Heliocentric", "Heliocentric", + p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), 0.0); + + // Helio -> Bary + { + cartesian::Position + out(p_helio.x() + sun_bary.x(), + p_helio.y() + sun_bary.y(), + p_helio.z() + sun_bary.z()); + cartesian::Position + back(out.x() - sun_bary.x(), + out.y() - sun_bary.y(), + out.z() - sun_bary.z()); + double err = cart_error(p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), + back.x().value(), back.y().value(), back.z().value()); + print_row("Heliocentric", "Barycentric", + out.x().value(), out.y().value(), out.z().value(), err); + } + + // Helio -> Geo + { + cartesian::Position + out(p_helio.x() - (earth_bary.x() - sun_bary.x()), + p_helio.y() - (earth_bary.y() - sun_bary.y()), + p_helio.z() - (earth_bary.z() - sun_bary.z())); + cartesian::Position + back(out.x() + (earth_bary.x() - sun_bary.x()), + out.y() + (earth_bary.y() - sun_bary.y()), + out.z() + (earth_bary.z() - sun_bary.z())); + double err = cart_error(p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), + back.x().value(), back.y().value(), back.z().value()); + print_row("Heliocentric", "Geocentric", + out.x().value(), out.y().value(), out.z().value(), err); + } + + // ------------------------------------------------------------------------- + // Geocentric source + // ------------------------------------------------------------------------- + print_row("Geocentric", "Geocentric", + p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), 0.0); + + // Geo -> Bary + { + cartesian::Position + out(p_geo.x() + earth_bary.x(), + p_geo.y() + earth_bary.y(), + p_geo.z() + earth_bary.z()); + cartesian::Position + back(out.x() - earth_bary.x(), + out.y() - earth_bary.y(), + out.z() - earth_bary.z()); + double err = cart_error(p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), + back.x().value(), back.y().value(), back.z().value()); + print_row("Geocentric", "Barycentric", + out.x().value(), out.y().value(), out.z().value(), err); + } + + // Geo -> Helio + { + cartesian::Position + out(p_geo.x() + (earth_bary.x() - sun_bary.x()), + p_geo.y() + (earth_bary.y() - sun_bary.y()), + p_geo.z() + (earth_bary.z() - sun_bary.z())); + cartesian::Position + back(out.x() - (earth_bary.x() - sun_bary.x()), + out.y() - (earth_bary.y() - sun_bary.y()), + out.z() - (earth_bary.z() - sun_bary.z())); + double err = cart_error(p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), + back.x().value(), back.y().value(), back.z().value()); + print_row("Geocentric", "Heliocentric", + out.x().value(), out.y().value(), out.z().value(), err); + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/05_time_periods.cpp b/examples/05_time_periods.cpp new file mode 100644 index 0000000..69a63d8 --- /dev/null +++ b/examples/05_time_periods.cpp @@ -0,0 +1,80 @@ +/** + * @file 05_time_periods.cpp + * @brief C++ port of siderust/examples/41_time_periods.rs + * + * Demonstrates the generic Period with different time types: + * - JulianDate periods + * - MJD (ModifiedJulianDate) periods + * - Conversions between time systems + * + * Run with: cmake --build build --target time_periods_example + */ + +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main() { + std::cout << "Generic Time Period Examples\n"; + std::cout << "============================\n\n"; + + // ========================================================================= + // 1. Period with JulianDate + // ========================================================================= + std::cout << "1. Period with JulianDate:\n"; + const JulianDate jd_start = JulianDate(2451545.0); // J2000.0 + const JulianDate jd_end = JulianDate(2451546.5); // 1.5 days later + std::cout << " Start: JD " << std::fixed << std::setprecision(6) << jd_start.value() << "\n"; + std::cout << " End: JD " << jd_end.value() << "\n"; + std::cout << " Duration: " << (jd_end.value() - jd_start.value()) << " days\n\n"; + + // ========================================================================= + // 2. Period with ModifiedJulianDate (MJD) + // ========================================================================= + std::cout << "2. Period with ModifiedJulianDate:\n"; + const MJD mjd_start = MJD(59000.0); + const MJD mjd_end = MJD(59002.5); + const Period mjd_period(mjd_start, mjd_end); + std::cout << " Start: MJD " << mjd_start.value() << "\n"; + std::cout << " End: MJD " << mjd_end.value() << "\n"; + std::cout << " Duration: " << mjd_period.duration().value() << " days\n\n"; + + // ========================================================================= + // 3. JulianDate <-> MJD conversion + // Relationship: MJD = JD - 2400000.5 + // ========================================================================= + std::cout << "3. Converting between time systems:\n"; + const MJD mjd_j2000 = MJD(51544.5); // MJD at J2000.0 + const JulianDate jd_from_mjd = JulianDate(mjd_j2000.value() + 2400000.5); + std::cout << " MJD: " << mjd_j2000.value() << "\n"; + std::cout << " JD: " << jd_from_mjd.value() << " (should be 2451545.0)\n\n"; + + // ========================================================================= + // 4. Period arithmetic + // ========================================================================= + std::cout << "4. Period arithmetic:\n"; + const MJD night_start = MJD(60000.0); + const MJD night_end = MJD(60000.5); // 12 hours later + const Period night(night_start, night_end); + + std::cout << " Night period start: MJD " << night.start().value() << "\n"; + std::cout << " Night period end: MJD " << night.end().value() << "\n"; + std::cout << " Duration in days: " << night.duration().value() << " days\n"; + std::cout << " Duration in hours: " << night.duration().value() << " h\n\n"; + + // ========================================================================= + // 5. J2000.0 epoch constant + // ========================================================================= + std::cout << "5. J2000.0 constants:\n"; + std::cout << " JulianDate::J2000() = JD " << std::setprecision(1) + << JulianDate::J2000().value() << "\n"; + std::cout << " Corresponding MJD = " << std::setprecision(3) + << (JulianDate::J2000().value() - 2400000.5) << "\n\n"; + + std::cout << "=== Example Complete ===\n"; + return 0; +} diff --git a/examples/06_astronomical_night.cpp b/examples/06_astronomical_night.cpp new file mode 100644 index 0000000..4a25bfc --- /dev/null +++ b/examples/06_astronomical_night.cpp @@ -0,0 +1,68 @@ +/** + * @file 06_astronomical_night.cpp + * @brief C++ port of siderust/examples/25_astronomical_night.rs + * + * Demonstrates finding astronomical night periods using the siderust library. + * Astronomical night is defined as the period when the Sun's center is + * more than 18° below the horizon (altitude < -18°). + * + * Usage: + * ./06_astronomical_night [YYYY-MM-DD] [lat_deg] [lon_deg] [height_m] + * + * Defaults: + * - Start date: MJD 60000.0 (2023-02-25) + * - Location: Greenwich Observatory (51.4769°N, 0°E) + * - Search period: 7 days + * + * Run with: cmake --build build --target astronomical_night_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main(int argc, char *argv[]) { + // Parse optional arguments (lon, lat, height, start_mjd) + const double lat_deg = argc > 1 ? std::atof(argv[1]) : 51.4769; + const double lon_deg = argc > 2 ? std::atof(argv[2]) : 0.0; + const double height_m = argc > 3 ? std::atof(argv[3]) : 0.0; + const double mjd0 = argc > 4 ? std::atof(argv[4]) : 60000.0; + + const Geodetic site = Geodetic(lon_deg, lat_deg, height_m); + const MJD start(mjd0); + const MJD end(mjd0 + 7.0); // 7-day window + const Period window(start, end); + + std::cout << "Astronomical Night Periods (Sun altitude < -18°)\n"; + std::cout << "================================================\n"; + std::cout << "Observer: lat = " << lat_deg << "°, " + << "lon = " << lon_deg << "°, " + << "height = " << height_m << " m\n"; + std::cout << "MJD window: " << start.value() << " → " << end.value() + << " (7 days)\n\n"; + + // Find astronomical night periods (Sun < -18°) + const auto nights = sun::below_threshold(site, window, qtty::Degree(-18.0)); + + if (nights.empty()) { + std::cout << "No astronomical night periods found in this week.\n"; + std::cout << "(This can happen at high latitudes during summer.)\n"; + } else { + std::cout << "Found " << nights.size() << " astronomical night period(s):\n\n"; + for (const auto &period : nights) { + const double dur_min = period.duration().value(); + std::cout << " MJD " << std::fixed << std::setprecision(4) + << period.start().value() + << " → " << period.end().value() + << " (" << std::setprecision(1) << dur_min << " min)\n"; + } + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/07_find_night_periods_365day.cpp b/examples/07_find_night_periods_365day.cpp new file mode 100644 index 0000000..2484ac8 --- /dev/null +++ b/examples/07_find_night_periods_365day.cpp @@ -0,0 +1,56 @@ +/** + * @file 07_find_night_periods_365day.cpp + * @brief C++ port of siderust/examples/31_find_night_periods_365day.rs + * + * Runs the astronomical night finder over a full 365-day horizon and prints + * all astronomical night periods (Sun altitude < -18°) for the default site + * (Roque de los Muchachos Observatory, La Palma). + * + * Usage: + * ./07_find_night_periods_365day [start_mjd] + * + * Default: MJD 60339.0 (~2026-01-01) + * + * Run with: cmake --build build --target find_night_periods_365day_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main(int argc, char *argv[]) { + // Default: MJD for 2026-01-01 + const double start_mjd = argc > 1 ? std::atof(argv[1]) : 60339.0; + + const MJD start(start_mjd); + const MJD end(start_mjd + 365.0); + const Period year_window(start, end); + + std::cout << "Find astronomical night periods for 365 days starting MJD " + << std::fixed << std::setprecision(0) << start_mjd << "\n"; + std::cout << "Observer: Roque de los Muchachos Observatory (La Palma)\n\n"; + + const auto nights = sun::below_threshold(ROQUE_DE_LOS_MUCHACHOS, year_window, + qtty::Degree(-18.0)); + + if (nights.empty()) { + std::cout << "No astronomical night periods found for this year at this site.\n"; + return 0; + } + + std::cout << "Found " << nights.size() << " night periods:\n\n"; + for (const auto &p : nights) { + const double dur_min = p.duration().value(); + std::cout << " MJD " << std::setprecision(4) << p.start().value() + << " → " << p.end().value() + << " (" << std::setprecision(1) << dur_min << " min)\n"; + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/08_night_quality_scoring.cpp b/examples/08_night_quality_scoring.cpp new file mode 100644 index 0000000..10bf0ff --- /dev/null +++ b/examples/08_night_quality_scoring.cpp @@ -0,0 +1,109 @@ +/** + * @file 08_night_quality_scoring.cpp + * @brief C++ port of siderust/examples/35_night_quality_scoring.rs + * + * Scores each night in a month based on Moon interference and astronomical + * darkness duration. Uses Sun/Moon altitude APIs for both criteria. + * + * Run with: cmake --build build --target night_quality_scoring_example + */ + +#include +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +struct NightScore { + double start_mjd; + double dark_hours; + double moon_up_hours; + double score; +}; + +static NightScore score_night(double night_start_mjd, const Geodetic &obs) { + const MJD start(night_start_mjd); + const MJD end(night_start_mjd + 1.0); + const Period window(start, end); + + // Astronomical darkness (Sun below -18°) + const auto dark_periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); + double dark_hours = 0.0; + for (const auto &p : dark_periods) + dark_hours += p.duration().value(); + + // Moon above horizon + const auto moon_up = moon::above_threshold(obs, window, qtty::Degree(0.0)); + double moon_hours = 0.0; + for (const auto &p : moon_up) + moon_hours += p.duration().value(); + + // Score = dark_hours * (1 - 0.7 * moon_interference) + double moon_interference = std::min(moon_hours / 24.0, 1.0); + double sc = dark_hours * (1.0 - 0.7 * moon_interference); + + return {night_start_mjd, dark_hours, moon_hours, sc}; +} + +int main() { + std::cout << "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; + std::cout << "\u2551 Monthly Observing Conditions Report \u2551\n"; + std::cout << "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; + + // Mauna Kea Observatory, Hawaii + const Geodetic obs = Geodetic(-155.472, 19.826, 4207.0); + std::cout << "Observatory: Mauna Kea, Hawaii\n"; + std::cout << " lat = 19.826 N, lon = -155.472 E, elev = 4207 m\n\n"; + + const double start_mjd = 60000.0; + std::vector scores; + scores.reserve(30); + + std::cout << "Analyzing 30 nights starting MJD " << std::fixed + << std::setprecision(0) << start_mjd << "...\n\n"; + + for (int day = 0; day < 30; ++day) { + scores.push_back(score_night(start_mjd + day, obs)); + } + + // Print table header + std::cout << std::setw(10) << "MJD" + << std::setw(12) << "Dark(h)" + << std::setw(12) << "Moon(h)" + << std::setw(10) << "Score" + << "\n"; + std::cout << std::string(44, '-') << "\n"; + + for (const auto &s : scores) { + std::cout << std::setw(10) << std::setprecision(0) << s.start_mjd + << std::setw(12) << std::setprecision(2) << s.dark_hours + << std::setw(12) << s.moon_up_hours + << std::setw(10) << std::setprecision(3) << s.score + << "\n"; + } + + // Summary statistics + auto best = *std::max_element(scores.begin(), scores.end(), + [](const NightScore &a, const NightScore &b) { + return a.score < b.score; + }); + auto worst = *std::min_element(scores.begin(), scores.end(), + [](const NightScore &a, const NightScore &b) { + return a.score < b.score; + }); + + std::cout << "\n--- Summary ---\n"; + std::cout << "Best night: MJD " << std::setprecision(0) << best.start_mjd + << " score=" << std::setprecision(3) << best.score + << " dark=" << std::setprecision(2) << best.dark_hours << " h\n"; + std::cout << "Worst night: MJD " << std::setprecision(0) << worst.start_mjd + << " score=" << std::setprecision(3) << worst.score + << " dark=" << std::setprecision(2) << worst.dark_hours << " h\n"; + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/09_star_observability.cpp b/examples/09_star_observability.cpp new file mode 100644 index 0000000..2953b2e --- /dev/null +++ b/examples/09_star_observability.cpp @@ -0,0 +1,91 @@ +/** + * @file 09_star_observability.cpp + * @brief C++ port of siderust/examples/39_star_observability.rs + * + * Demonstrates using the altitude period API to plan optimal observing windows + * for multiple catalog stars at a given observatory. + * + * Run with: cmake --build build --target star_observability_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main() { + std::cout << "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; + std::cout << "\u2551 Star Observability Planner \u2551\n"; + std::cout << "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; + + // Observatory: Greenwich + const Geodetic obs = Geodetic(0.0, 51.4769, 0.0); + std::cout << "Observatory: Greenwich Royal Observatory\n"; + std::cout << " lat = 51.4769 N, lon = 0.0 E\n\n"; + + // Tonight: one night starting at MJD 60000.5 + const MJD start(60000.5); + const MJD end(60001.5); + const Period night(start, end); + std::cout << "Observation window: MJD " << start.value() << " to " << end.value() << "\n\n"; + + // Find astronomical night (Sun below -18°) + const auto dark_periods = sun::below_threshold(obs, night, qtty::Degree(-18.0)); + + if (dark_periods.empty()) { + std::cout << "No astronomical darkness available tonight!\n"; + return 0; + } + + double total_dark_h = 0.0; + for (const auto &p : dark_periods) + total_dark_h += p.duration().value(); + std::cout << "Astronomical night duration: " << std::fixed << std::setprecision(2) + << total_dark_h << " h\n\n"; + + // Target stars + struct TargetInfo { const char *name; const Star *star; }; + const TargetInfo targets[] = { + {"Sirius", &SIRIUS}, + {"Vega", &VEGA}, + {"Altair", &ALTAIR}, + {"Betelgeuse", &BETELGEUSE}, + {"Rigel", &RIGEL}, + {"Polaris", &POLARIS}, + }; + + const qtty::Degree min_alt(30.0); // minimum altitude for good observation + + std::cout << "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; + std::cout << " Target Visibility (altitude > 30°, during astronomical night)\n"; + std::cout << "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; + + for (const auto &t : targets) { + const auto visible = star_altitude::above_threshold(*t.star, obs, night, min_alt); + + double total_h = 0.0; + for (const auto &p : visible) + total_h += p.duration().value(); + + std::cout << std::left << std::setw(12) << t.name + << " periods=" << std::setw(3) << visible.size() + << " total=" << std::setprecision(2) << std::fixed + << total_h << " h\n"; + + if (!visible.empty()) { + const auto &best = visible.front(); + std::cout << " first period: MJD " + << std::setprecision(4) << best.start().value() + << " → " << best.end().value() + << " (" << std::setprecision(2) << best.duration().value() + << " h)\n"; + } + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/10_altitude_periods_trait.cpp b/examples/10_altitude_periods_trait.cpp new file mode 100644 index 0000000..6195cd6 --- /dev/null +++ b/examples/10_altitude_periods_trait.cpp @@ -0,0 +1,140 @@ +/** + * @file 10_altitude_periods_trait.cpp + * @brief C++ port of siderust/examples/24_altitude_periods_trait.rs + * + * Demonstrates the unified altitude period API (implemented via the + * `Target` base class and free-function namespaces) for finding time + * intervals when celestial bodies are within specific altitude ranges. + * + * In Rust this is the `AltitudePeriodsProvider` trait. In C++ the same + * functionality is available via: + * - `sun::above_threshold / below_threshold` + * - `moon::above_threshold / below_threshold` + * - `star_altitude::above_threshold / below_threshold` + * - Polymorphic via the `Target` / `BodyTarget` / `StarTarget` classes + * + * Run with: cmake --build build --target altitude_periods_trait_example + */ + +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main() { + std::cout << "=== Altitude Periods API Examples ===\n\n"; + + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + std::cout << "Observer: Roque de los Muchachos Observatory\n\n"; + + // Time window: one week starting from MJD 60000 + const MJD start(60000.0); + const MJD end(60007.0); + const Period window(start, end); + std::cout << "Time window: MJD " << std::fixed << std::setprecision(1) + << start.value() << " → " << end.value() << " (7 days)\n\n"; + + // ------------------------------------------------------------------------- + // Example 1: Astronomical nights (Sun below -18°) + // ------------------------------------------------------------------------- + std::cout << "--- Example 1: Astronomical Nights ---\n"; + const auto astro_nights = sun::below_threshold(observer, window, qtty::Degree(-18.0)); + + std::cout << "Found " << astro_nights.size() << " astronomical night period(s):\n"; + for (size_t i = 0; i < std::min(astro_nights.size(), size_t(3)); ++i) { + const auto &p = astro_nights[i]; + std::cout << " Night " << i + 1 << ": " + << std::setprecision(3) << p.duration().value() << " h\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Example 2: Sirius above 30° + // ------------------------------------------------------------------------- + std::cout << "--- Example 2: Sirius High Above Horizon ---\n"; + const auto sirius_high = star_altitude::above_threshold(SIRIUS, observer, window, + qtty::Degree(30.0)); + std::cout << "Sirius above 30° altitude:\n"; + std::cout << " Found " << sirius_high.size() << " period(s)\n"; + if (!sirius_high.empty()) { + std::cout << " First period: " << std::setprecision(3) + << sirius_high.front().duration().value() << " h\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Example 3: Custom ICRS direction (Betelgeuse) — above horizon + // ------------------------------------------------------------------------- + std::cout << "--- Example 3: Custom ICRS Direction (Betelgeuse) ---\n"; + // Betelgeuse: RA ≈ 88.79°, Dec ≈ +7.41° + const spherical::direction::ICRS betelgeuse_dir(qtty::Degree(88.79), + qtty::Degree(7.41)); + // Use StarTarget with a custom (non-catalog) star to demonstrate the API + // (Alternatively use the catalog BETELGEUSE star handle) + const auto btel_visible = star_altitude::above_threshold(BETELGEUSE, observer, window, + qtty::Degree(0.0)); + double total_btel_h = 0.0; + for (const auto &p : btel_visible) + total_btel_h += p.duration().value(); + std::cout << "Betelgeuse above horizon:\n"; + std::cout << " Found " << btel_visible.size() << " period(s) in 7 days\n"; + std::cout << " Total visible time: " << std::setprecision(2) << total_btel_h << " h\n\n"; + + // ------------------------------------------------------------------------- + // Example 4: Moon altitude range query (0° to 20°) + // ------------------------------------------------------------------------- + std::cout << "--- Example 4: Low Moon Periods (0° to 20°) ---\n"; + const auto moon_low = moon::above_threshold(observer, window, qtty::Degree(0.0)); + const auto moon_high = moon::above_threshold(observer, window, qtty::Degree(20.0)); + + // Low moon = above 0° minus above 20° (approximate count; periods may differ) + std::cout << "Moon between 0° and 20° altitude (approx):\n"; + std::cout << " Moon > 0°: " << moon_low.size() << " period(s)\n"; + std::cout << " Moon > 20°: " << moon_high.size() << " period(s)\n\n"; + + // ------------------------------------------------------------------------- + // Example 5: Circumpolar star check (Polaris) + // ------------------------------------------------------------------------- + std::cout << "--- Example 5: Circumpolar Star (Polaris) ---\n"; + const auto polaris_up = star_altitude::above_threshold(POLARIS, observer, window, + qtty::Degree(0.0)); + + double total_polaris_h = 0.0; + for (const auto &p : polaris_up) + total_polaris_h += p.duration().value(); + + std::cout << "Polaris above horizon:\n"; + if (polaris_up.size() == 1 && std::abs(total_polaris_h - 7.0 * 24.0) < 2.4) { + std::cout << " Circumpolar (continuously visible for entire week)\n"; + } else { + std::cout << " Found " << polaris_up.size() << " period(s), " + << "total " << std::setprecision(2) << total_polaris_h << " h\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Example 6: Polymorphic dispatch via Target base class + // ------------------------------------------------------------------------- + std::cout << "--- Example 6: Polymorphic Target Dispatch ---\n"; + std::vector> targets; + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(Body::Moon)); + targets.push_back(std::make_unique(VEGA)); + targets.push_back(std::make_unique(SIRIUS)); + + for (const auto &t : targets) { + const auto periods = t->above_threshold(observer, window, qtty::Degree(0.0)); + double total_h = 0.0; + for (const auto &p : periods) + total_h += p.duration().value(); + std::cout << " " << std::left << std::setw(8) << t->name() + << " " << std::setw(3) << periods.size() + << " periods total = " << std::setprecision(2) << total_h << " h\n"; + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/11_compare_sun_moon_star.cpp b/examples/11_compare_sun_moon_star.cpp new file mode 100644 index 0000000..33b5119 --- /dev/null +++ b/examples/11_compare_sun_moon_star.cpp @@ -0,0 +1,108 @@ +/** + * @file 11_compare_sun_moon_star.cpp + * @brief C++ port of siderust/examples/29_compare_sun_moon_star.rs + * + * Demonstrates generic altitude analysis for the Sun, Moon, and a star using + * a single helper function that works with the polymorphic `Target` base class. + * + * Run with: cmake --build build --target compare_sun_moon_star_example + */ + +#include +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +struct Summary { + std::string name; + size_t period_count; + double total_hours; + double max_period_hours; +}; + +/// Generic analysis for any `Target` subclass. +static Summary analyze_body(const Target &body, const Geodetic &obs, const Period &window, + qtty::Degree threshold) { + const auto periods = body.above_threshold(obs, window, threshold); + + double total = 0.0; + double max_duration = 0.0; + + for (const auto &p : periods) { + const double h = p.duration().value(); + total += h; + if (h > max_duration) + max_duration = h; + } + + return {body.name(), periods.size(), total, max_duration}; +} + +static void print_summary(const Summary &s) { + std::cout << std::left << std::setw(12) << s.name + << " periods=" << std::setw(4) << s.period_count + << " total=" << std::setw(8) << std::setprecision(2) << std::fixed + << s.total_hours << " h" + << " longest=" << std::setprecision(2) << s.max_period_hours << " h\n"; +} + +int main() { + std::cout << "=== Comparing Sun, Moon, and Star Altitude Periods ===\n\n"; + + const Geodetic observer = EL_PARANAL; + std::cout << "Observatory: ESO Paranal / VLT\n\n"; + + // 14-day window around a new moon + const Period window(MJD(60000.0), MJD(60014.0)); + std::cout << "Time window: 14 days starting MJD 60000\n\n"; + + // Build target list using polymorphic base class + std::vector> bodies; + bodies.push_back(std::make_unique(Body::Sun)); + bodies.push_back(std::make_unique(Body::Moon)); + bodies.push_back(std::make_unique(SIRIUS)); + bodies.push_back(std::make_unique(VEGA)); + bodies.push_back(std::make_unique(CANOPUS)); + + const qtty::Degree threshold_deg(20.0); + std::cout << "Altitude threshold: " << threshold_deg.value() << "°\n\n"; + + std::cout << std::string(72, '=') << "\n"; + std::cout << " Target Periods Total Time Longest Period\n"; + std::cout << std::string(72, '-') << "\n"; + + for (const auto &b : bodies) { + const auto s = analyze_body(*b, observer, window, threshold_deg); + print_summary(s); + } + + std::cout << std::string(72, '=') << "\n\n"; + + // ------------------------------------------------------------------------- + // Focused comparison: time simultaneously above 20° (Sun excluded) + // Illustrate that the same helper can iterate over any bodies vector. + // ------------------------------------------------------------------------- + std::cout << "--- Bodies visible (>20°) without the Sun ---\n\n"; + std::vector> night_bodies; + night_bodies.push_back(std::make_unique(Body::Moon)); + night_bodies.push_back(std::make_unique(SIRIUS)); + night_bodies.push_back(std::make_unique(VEGA)); + night_bodies.push_back(std::make_unique(CANOPUS)); + night_bodies.push_back(std::make_unique(RIGEL)); + night_bodies.push_back(std::make_unique(ALTAIR)); + night_bodies.push_back(std::make_unique(ARCTURUS)); + night_bodies.push_back(std::make_unique(ALDEBARAN)); + + for (const auto &b : night_bodies) { + const auto s = analyze_body(*b, observer, window, threshold_deg); + print_summary(s); + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/12_solar_system_example.cpp b/examples/12_solar_system_example.cpp new file mode 100644 index 0000000..7efa10a --- /dev/null +++ b/examples/12_solar_system_example.cpp @@ -0,0 +1,138 @@ +/** + * @file 12_solar_system_example.cpp + * @brief C++ port of siderust/examples/38_solar_system_example.rs + * + * Computes heliocentric and barycentric positions for solar-system bodies + * using the VSOP87 ephemeris layer exposed by siderust. + * + * Bodies with VSOP87 bindings in C++: Sun, Earth, Mars, Venus, Moon. + * (Mercury/Jupiter/Saturn/Uranus/Neptune not yet bound in C++ FFI.) + * + * Run with: cmake --build build --target 12_solar_system_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main() { + std::cout << "=== Solar System Bodies Positions ===\n\n"; + + const JulianDate jd(2451545.0); + std::cout << "Epoch: JD " << std::fixed << std::setprecision(1) + << jd.value() << " (J2000.0)\n\n"; + + // ------------------------------------------------------------------------- + // Heliocentric positions (EclipticMeanJ2000 frame, AU) + // ------------------------------------------------------------------------- + std::cout << "--- Heliocentric Positions (VSOP87) ---\n"; + + auto print_hel = [](const char *name, auto pos) { + const auto sph = pos.to_spherical(); + const auto dir = sph.direction(); + std::cout << std::left << std::setw(8) << name + << " lon=" << std::right << std::fixed << std::setprecision(3) + << std::setw(10) << dir.longitude().value() << " deg" + << " lat=" << std::setw(8) << dir.latitude().value() << " deg" + << " r=" << std::setw(12) << std::setprecision(6) + << sph.distance().value() << " AU\n"; + }; + + const auto earth_h = ephemeris::earth_heliocentric(jd); + const auto venus_h = ephemeris::venus_heliocentric(jd); + const auto mars_h = ephemeris::mars_heliocentric(jd); + + print_hel("Earth", earth_h); + print_hel("Venus", venus_h); + print_hel("Mars", mars_h); + + std::cout << "\n [NOTE: Mercury, Jupiter, Saturn, Uranus, Neptune heliocentric\n"; + std::cout << " not yet bound in C++ FFI.]\n\n"; + + // ------------------------------------------------------------------------- + // Barycentric positions (Solar System Barycenter origin, AU) + // ------------------------------------------------------------------------- + std::cout << "--- Barycentric Positions ---\n"; + + const auto sun_bc = ephemeris::sun_barycentric(jd); + const auto earth_bc = ephemeris::earth_barycentric(jd); + + auto print_bary = [](const char *name, auto pos) { + const auto sph = pos.to_spherical(); + std::cout << std::left << std::setw(16) << name + << " r=" << std::right << std::fixed << std::setprecision(6) + << sph.distance().value() << " AU\n"; + }; + + print_bary("Sun (SSB)", sun_bc); + print_bary("Earth (SSB)", earth_bc); + + std::cout << "\n [NOTE: mars_barycentric, moon_barycentric not yet bound.]\n\n"; + + // ------------------------------------------------------------------------- + // Moon geocentric (km) + // ------------------------------------------------------------------------- + std::cout << "--- Moon Geocentric Position ---\n"; + const auto moon_g = ephemeris::moon_geocentric(jd); + { + const auto sph = moon_g.to_spherical(); + const auto dir = sph.direction(); + const double r_km = sph.distance().value(); + std::cout << "Moon r=" << std::setprecision(1) << r_km << " km" + << " (" << std::setprecision(6) << r_km / 1.496e8 << " AU)" + << " lon=" << std::setprecision(3) << dir.longitude().value() << " deg" + << " lat=" << dir.latitude().value() << " deg\n\n"; + } + + // ------------------------------------------------------------------------- + // Planet catalog (orbital elements) + // ------------------------------------------------------------------------- + std::cout << "--- Planet Catalog (orbital elements) ---\n"; + + struct PlanetInfo { const char *name; const Planet *planet; }; + const PlanetInfo catalog[] = { + {"Mercury", &MERCURY}, + {"Venus", &VENUS}, + {"Earth", &EARTH}, + {"Mars", &MARS}, + {"Jupiter", &JUPITER}, + {"Saturn", &SATURN}, + {"Uranus", &URANUS}, + {"Neptune", &NEPTUNE}, + }; + + std::cout << std::setw(10) << "Planet" + << std::setw(12) << "a (AU)" + << std::setw(10) << "e" + << std::setw(12) << "r_body (km)\n"; + std::cout << std::string(44, '-') << "\n"; + + for (const auto &p : catalog) { + std::cout << std::left << std::setw(10) << p.name + << std::right << std::fixed + << std::setw(12) << std::setprecision(4) + << p.planet->orbit.semi_major_axis_au + << std::setw(10) << std::setprecision(5) + << p.planet->orbit.eccentricity + << std::setw(12) << std::setprecision(0) + << p.planet->radius_km << "\n"; + } + + // ------------------------------------------------------------------------- + // Earth-Mars distance via heliocentric coordinates + // ------------------------------------------------------------------------- + std::cout << "\n--- Earth-Mars Distance (J2000.0) ---\n"; + const double ex = earth_h.x().value(), ey = earth_h.y().value(), ez = earth_h.z().value(); + const double mx = mars_h.x().value(), my = mars_h.y().value(), mz = mars_h.z().value(); + const double dist_au = std::sqrt((ex-mx)*(ex-mx) + (ey-my)*(ey-my) + (ez-mz)*(ez-mz)); + std::cout << "Distance: " << std::setprecision(4) << dist_au << " AU" + << " (" << std::setprecision(0) << dist_au * 1.496e8 << " km)\n"; + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/13_observer_coordinates.cpp b/examples/13_observer_coordinates.cpp new file mode 100644 index 0000000..2bd7b59 --- /dev/null +++ b/examples/13_observer_coordinates.cpp @@ -0,0 +1,148 @@ +/** + * @file 13_observer_coordinates.cpp + * @brief C++ port of siderust/examples/36_observer_coordinates.rs + * + * Shows how an observer's ground coordinates (geodetic) relate to + * geocentric Cartesian coordinates, and how to convert between + * equatorial and horizontal systems for an observation site. + * + * Run with: cmake --build build --target observer_coordinates_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main() { + std::cout << "=== Observer Coordinate Systems ===\n\n"; + + // ------------------------------------------------------------------------- + // Build a few observatories using the Geodetic() factory + // ------------------------------------------------------------------------- + const struct { const char *name; double lon; double lat; double alt_m; } sites[] = { + {"Greenwich", 0.0, 51.4769, 46.0}, + {"Roque de los Muchachos", -17.892, 28.756, 2396.0}, + {"Mauna Kea", -155.472, 19.826, 4207.0}, + {"El Paranal", -70.403, -24.627, 2635.0}, + {"La Silla", -70.730, -29.257, 2400.0}, + }; + + std::cout << "--- Observatory Summary ---\n"; + std::cout << std::setw(28) << "Name" + << std::setw(10) << "Lon(°)" + << std::setw(10) << "Lat(°)" + << std::setw(10) << "Alt(m)\n"; + std::cout << std::string(58, '-') << "\n"; + + for (const auto &s : sites) { + std::cout << std::setw(28) << std::left << s.name + << std::setw(10) << std::right << std::fixed << std::setprecision(3) + << s.lon + << std::setw(10) << s.lat + << std::setw(10) << std::setprecision(0) << s.alt_m << "\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Use built-in observatory constants (pre-defined in siderust.hpp) + // ------------------------------------------------------------------------- + std::cout << "--- Built-in Observatory Constants ---\n"; + const Geodetic obs1 = ROQUE_DE_LOS_MUCHACHOS; + const Geodetic obs2 = MAUNA_KEA; + const Geodetic obs3 = EL_PARANAL; + const Geodetic obs4 = LA_SILLA_OBSERVATORY; + + std::cout << "ROQUE_DE_LOS_MUCHACHOS lat=" << std::setprecision(3) + << obs1.lat.value() << "° lon=" << obs1.lon.value() << "°" + << " alt=" << obs1.height.value() << " m\n"; + std::cout << "MAUNA_KEA lat=" << obs2.lat.value() + << "° lon=" << obs2.lon.value() << "°" + << " alt=" << obs2.height.value() << " m\n"; + std::cout << "EL_PARANAL lat=" << obs3.lat.value() + << "° lon=" << obs3.lon.value() << "°" + << " alt=" << obs3.height.value() << " m\n"; + std::cout << "LA_SILLA_OBSERVATORY lat=" << obs4.lat.value() + << "° lon=" << obs4.lon.value() << "°" + << " alt=" << obs4.height.value() << " m\n\n"; + + // ------------------------------------------------------------------------- + // Frame conversion: ICRS position → Horizontal for selected stars + // ------------------------------------------------------------------------- + std::cout << "--- Star Horizontal Coordinates at Roque de los Muchachos ---\n"; + std::cout << "(Epoch: J2000.0, for rough indicative values)\n\n"; + + const JulianDate jd(2451545.0); // J2000.0 + const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; + + // Known J2000 ICRS coordinates for catalog stars + struct StarInfo { const char *name; double ra_deg; double dec_deg; }; + const StarInfo stars[] = { + {"Sirius", 101.2871, -16.7161}, + {"Vega", 279.2348, 38.7837}, + {"Altair", 297.6958, 8.8683}, + {"Polaris", 37.9546, 89.2641}, + {"Betelgeuse", 88.7929, 7.4070}, + }; + + std::cout << std::setw(12) << "Star" + << std::setw(12) << " RA (°)" + << std::setw(12) << "Dec (°)" + << std::setw(12) << "Alt (°)\n"; + std::cout << std::string(48, '-') << "\n"; + + for (const auto &s : stars) { + const spherical::direction::ICRS dir(qtty::Degree(s.ra_deg), qtty::Degree(s.dec_deg)); + const auto h_dir = dir.to_horizontal(jd, observer); + std::cout << std::left << std::setw(12) << s.name + << std::right << std::setw(12) << std::setprecision(3) << std::fixed + << s.ra_deg + << std::setw(12) << s.dec_deg + << std::setw(12) << h_dir.altitude().value() << "\n"; + } + + // ------------------------------------------------------------------------- + // Topocentric vs geocentric: a brief note and illustration + // ------------------------------------------------------------------------- + std::cout << "\n--- Topocentric Parallax (Moon) ---\n"; + const auto moon_geo = ephemeris::moon_geocentric(jd); + { + const auto moon_sph = moon_geo.to_spherical(); + std::cout << "Moon geocentric: r=" << std::setprecision(6) + << moon_sph.distance().value() << " AU\n"; + } + // Topocentric shift is computed internally by the altitude/frame routines; + // this example illustrates that raw ephemeris gives geocentric positions. + std::cout << "(Altitude routines apply topocentric correction automatically)\n"; + + // ------------------------------------------------------------------------- + // Summary: compare observers at different latitudes + // ------------------------------------------------------------------------- + std::cout << "\n--- Sirius Visibility by Latitude ---\n"; + std::cout << " (hours above horizon over one year via altitude periods API)\n\n"; + + const Period full_year(MJD(60000.0), MJD(60365.0)); + struct LatSite { const char *name; Geodetic obs; }; + const LatSite lat_sites[] = { + {"Greenwich (+51.5°)", Geodetic(0.0, 51.48, 0.0)}, + {"Roque (+28.8°)", ROQUE_DE_LOS_MUCHACHOS}, + {"Paranal (-24.6°)", EL_PARANAL}, + }; + + for (const auto &ls : lat_sites) { + const auto periods = star_altitude::above_threshold(SIRIUS, ls.obs, full_year, + qtty::Degree(0.0)); + double total_h = 0.0; + for (const auto &p : periods) + total_h += p.duration().value(); + std::cout << " " << std::left << std::setw(22) << ls.name + << "Sirius above horizon: " << std::setprecision(0) << total_h << " h/yr\n"; + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/14_bodycentric_coordinates.cpp b/examples/14_bodycentric_coordinates.cpp new file mode 100644 index 0000000..0a8065c --- /dev/null +++ b/examples/14_bodycentric_coordinates.cpp @@ -0,0 +1,144 @@ +/** + * @file 14_bodycentric_coordinates.cpp + * @brief C++ port of siderust/examples/27_bodycentric_coordinates.rs + * + * Shows how to project positions into a body-centered reference frame using + * `to_bodycentric()` and `BodycentricParams`. Useful for describing spacecraft + * or Moon positions as seen from an orbiter. + * + * Run with: cmake --build build --target bodycentric_coordinates_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; +using namespace siderust::frames; +using namespace siderust::centers; + +int main() { + std::cout << "=== Body-Centric Coordinate Transformations ===\n\n"; + + const JulianDate jd = JulianDate::J2000(); + std::cout << "Epoch: J2000.0 (JD " << std::fixed << std::setprecision(1) + << jd.value() << ")\n\n"; + + // ------------------------------------------------------------------------- + // Example 1: Moon as seen from a fictitious ISS-like orbit + // ------------------------------------------------------------------------- + std::cout << "--- Example 1: Moon from a Low-Earth Orbit ---\n"; + + // ISS-like (circular low-Earth, ~400 km altitude, ~51.6° inclination) + // a ≈ (6371 + 400) km ≈ 0.0000440 AU, e≈0, i≈51.6° + const Orbit iss_orbit{ + 0.0000440, // semi-major axis (AU) + 0.0001, // eccentricity + 51.6, // inclination (°) + 0.0, // RAAN (°) + 0.0, // argument of periapsis (°) + 0.0, // mean anomaly at epoch (°) + jd.value() // epoch (JD) + }; + const BodycentricParams iss_params = BodycentricParams::geocentric(iss_orbit); + + // Moon's approximate geocentric position in Ecliptic J2000 + // (rough: ~0.00257 AU, ~5° ecliptic latitude) + const cartesian::Position + moon_geo_pos(0.00257, 0.00034, 0.0); + + const auto moon_from_iss = to_bodycentric(moon_geo_pos, iss_params, jd); + + std::cout << "Moon from ISS (bodycentric, EclipticJ2000) [AU]:\n"; + std::cout << " x=" << std::setprecision(6) + << moon_from_iss.pos.x().value() + << " y=" << moon_from_iss.pos.y().value() + << " z=" << moon_from_iss.pos.z().value() << "\n"; + + const double dist_au = std::sqrt( + moon_from_iss.pos.x().value() * moon_from_iss.pos.x().value() + + moon_from_iss.pos.y().value() * moon_from_iss.pos.y().value() + + moon_from_iss.pos.z().value() * moon_from_iss.pos.z().value()); + std::cout << " distance ≈ " << std::setprecision(6) << dist_au << " AU (" + << std::setprecision(0) << dist_au * 1.496e8 << " km)\n\n"; + + // Round-trip back to geocentric + const auto recovered = moon_from_iss.to_geocentric(jd); + std::cout << "Round-trip to geocentric [AU]:\n"; + std::cout << " x=" << std::setprecision(6) << recovered.x().value() + << " y=" << recovered.y().value() + << " z=" << recovered.z().value() << "\n"; + + const double err = std::sqrt( + std::pow(recovered.x().value() - moon_geo_pos.x().value(), 2) + + std::pow(recovered.y().value() - moon_geo_pos.y().value(), 2) + + std::pow(recovered.z().value() - moon_geo_pos.z().value(), 2)); + std::cout << " round-trip error = " << std::setprecision(2) << std::scientific + << err << " AU\n\n"; + + // ------------------------------------------------------------------------- + // Example 2: Mars Phobos-like orbit + // ------------------------------------------------------------------------- + std::cout << "--- Example 2: Position Relative to Mars (Phobos-like Orbit) ---\n"; + + // Phobos: a ≈ 9376 km ≈ 0.0000627 AU, e≈0.015, i≈1.1° + const Orbit phobos_orbit{ + 0.0000627, // semi-major axis (AU) + 0.015, // eccentricity + 1.1, // inclination (°) + 0.0, // RAAN (°) + 0.0, // argument of periapsis (°) + 5.0, // mean anomaly at epoch (°) + jd.value() // epoch (JD) + }; + const BodycentricParams phobos_params = BodycentricParams::heliocentric(phobos_orbit); + + // Mars heliocentric position at J2000.0 (approximate) + const auto mars_hel = ephemeris::mars_heliocentric(jd); + + const auto phobos_from_mars = to_bodycentric(mars_hel, phobos_params, jd); + + std::cout << "Mars heliocentric position [AU]:" + << " r=" << std::setprecision(3) + << mars_hel.to_spherical().distance().value() << "\n"; + std::cout << "Phobos bodycentric (relative to Mars) [AU]:\n"; + std::cout << " x=" << std::setprecision(8) << phobos_from_mars.pos.x().value() + << " y=" << phobos_from_mars.pos.y().value() + << " z=" << phobos_from_mars.pos.z().value() << "\n"; + + // ------------------------------------------------------------------------- + // Example 3: Geocentric orbit (circular, equatorial) + // ------------------------------------------------------------------------- + std::cout << "\n--- Example 3: GEO Satellite ---\n"; + + const Orbit geo_orbit{ + 0.000284, // ~42164 km ≈ 0.000282 AU + 0.0001, // nearly circular + 0.1, // near-equatorial + 120.0, // RAAN + 0.0, // arg periapsis + 0.0, // mean anomaly + jd.value() + }; + const BodycentricParams geo_params = BodycentricParams::geocentric(geo_orbit); + + // Sun geocentric approximate position + const auto sun_geo_approx = + cartesian::Position( + -1.0, 0.0, 0.0); // rough + + const auto sun_from_geo = to_bodycentric(sun_geo_approx, geo_params, jd); + const double sun_dist_au = std::sqrt( + sun_from_geo.pos.x().value() * sun_from_geo.pos.x().value() + + sun_from_geo.pos.y().value() * sun_from_geo.pos.y().value() + + sun_from_geo.pos.z().value() * sun_from_geo.pos.z().value()); + + std::cout << "Sun as seen from GEO satellite [AU]: r=" << std::setprecision(4) + << sun_dist_au << "\n"; + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/15_targets_proper_motion.cpp b/examples/15_targets_proper_motion.cpp new file mode 100644 index 0000000..499ff69 --- /dev/null +++ b/examples/15_targets_proper_motion.cpp @@ -0,0 +1,128 @@ +/** + * @file 15_targets_proper_motion.cpp + * @brief C++ port of siderust/examples/40_targets_proper_motion.rs + * + * Demonstrates proper motion propagation for catalog stars. In Rust the + * `CoordinateWithPM` type propagates RA/Dec/proper-motion from a reference + * epoch. This binding is **not yet available** in C++; a placeholder section + * clearly marks that area, while the surrounding coordinate arithmetic is + * fully implemented. + * + * Run with: cmake --build build --target targets_proper_motion_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; +using namespace siderust::frames; + +// TODO: [PLACEHOLDER] CoordinateWithPM / set_proper_motion_since_j2000 not +// yet bound in C++. The Rust API is: +// let star_pm = star.set_proper_motion_since_j2000(target_jd); +// These will be added once the FFI layer for proper motion propagation is +// implemented. + +int main() { + std::cout << "=== Targets with Proper Motion ===\n\n"; + + // ------------------------------------------------------------------------- + // Part 1: Catalog star directions (static, no proper motion — fully bound) + // ------------------------------------------------------------------------- + std::cout << "--- Part 1: Catalog Stars — Current ICRS Directions ---\n\n"; + + struct CatalogEntry { + const char *name; + double ra_deg; // J2000 ICRS right ascension (degrees) + double dec_deg; // J2000 ICRS declination (degrees) + double pmRA_mas; // proper motion in RA (mas/yr) + double pmDec_mas; // proper motion in Dec (mas/yr) + }; + + // J2000 ICRS coords and proper motions (Hipparcos/Gaia) + const CatalogEntry entries[] = { + {"Sirius", 101.2871, -16.7161, -546.0, -1223.0}, + {"Vega", 279.2348, 38.7837, +200.9, +287.5}, + {"Polaris", 37.9546, 89.2641, +44.2, -11.7}, + {"Altair", 297.6958, 8.8683, +536.8, +385.3}, + {"Arcturus", 213.9153, 19.1822, -1093.4, -1999.4}, + {"Betelgeuse", 88.7929, 7.4070, +27.3, +10.9}, + {"Rigel", 78.6345, -8.2016, +1.3, -0.1}, + {"Aldebaran", 68.9801, 16.5093, +63.5, -189.9}, + }; + + std::cout << std::setw(12) << "Star" + << std::setw(12) << "RA (°)" + << std::setw(12) << "Dec (°)" + << std::setw(16) << "pmRA (mas/yr)" + << std::setw(16) << "pmDec (mas/yr)" + << "\n"; + std::cout << std::string(68, '-') << "\n"; + + for (const auto &e : entries) { + std::cout << std::left << std::setw(12) << e.name + << std::right << std::setw(12) << std::fixed << std::setprecision(4) + << e.ra_deg + << std::setw(12) << e.dec_deg + << std::setw(16) << std::setprecision(1) << e.pmRA_mas + << std::setw(16) << e.pmDec_mas << "\n"; + } + + // ------------------------------------------------------------------------- + // Part 2: Manual proper-motion propagation (simplified linear model) + // This is what the Rust CoordinateWithPM does internally. + // ------------------------------------------------------------------------- + std::cout << "\n--- Part 2: Manual Linear Proper Motion Propagation ---\n"; + std::cout << "(Simplified — no parallax correction. Rust uses a full ICRS model.)\n\n"; + + const double j2000_jd = 2451545.0; + const double target_jd = 2451545.0 + 100.0 * 365.25; // J2100.0 + const double years = (target_jd - j2000_jd) / 365.25; + + std::cout << "Propagating from J2000.0 to J2100.0 (" << std::setprecision(1) + << years << " years)\n\n"; + + for (const auto &e : entries) { + const double ra0 = e.ra_deg; // J2000 RA in degrees + const double dec0 = e.dec_deg; // J2000 Dec in degrees + + // Convert mas/yr → deg/yr + const double pm_ra_deg = e.pmRA_mas / (3.6e6 * std::cos(dec0 * M_PI / 180.0)); + const double pm_dec_deg = e.pmDec_mas / 3.6e6; + + const double ra1 = ra0 + pm_ra_deg * years; + const double dec1 = dec0 + pm_dec_deg * years; + + // Angular offset in arcseconds + const double dra = (ra1 - ra0) * 3600.0; + const double ddec = (dec1 - dec0) * 3600.0; + const double separation_arcsec = std::sqrt(dra * dra + ddec * ddec); + + std::cout << std::left << std::setw(12) << e.name + << " RA: " << std::right << std::setw(10) << std::setprecision(4) + << ra0 << " → " << std::setw(10) << ra1 + << "° shift=" << std::setprecision(2) << separation_arcsec << " arcsec\n"; + } + + // ------------------------------------------------------------------------- + // Part 3: PLACEHOLDER — CoordinateWithPM + // ------------------------------------------------------------------------- + std::cout << "\n--- Part 3: [PLACEHOLDER] CoordinateWithPM API ---\n"; + std::cout << "NOTE: The following Rust capability is not yet bound in C++:\n"; + std::cout << " // Rust:\n"; + std::cout << " let star = siderust::catalog::SIRIUS;\n"; + std::cout << " let jd_target = Julian::J2100;\n"; + std::cout << " let pos_2100 = star.set_proper_motion_since_j2000(jd_target);\n"; + std::cout << " // Returns CoordinateWithPM — ICRS direction with full\n"; + std::cout << " // rigorous proper-motion propagation (including parallax).\n"; + std::cout << "\n // C++ equivalent (future API):\n"; + std::cout << " // auto pos_2100 = SIRIUS.propagate_proper_motion(JulianDate_J2100);\n"; + std::cout << "\nUsing manual linear propagation above as approximation.\n"; + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/16_jpl_precise_ephemeris.cpp b/examples/16_jpl_precise_ephemeris.cpp new file mode 100644 index 0000000..3547670 --- /dev/null +++ b/examples/16_jpl_precise_ephemeris.cpp @@ -0,0 +1,87 @@ +/** + * @file 16_jpl_precise_ephemeris.cpp + * @brief C++ port of siderust/examples/32_jpl_precise_ephemeris.rs + * + * [PLACEHOLDER] — JPL DE430/DE440 high-precision ephemeris is **not yet + * bound** in the C++ wrapper. The Rust implementation uses a run-time + * loadable ephemeris file; the C++ FFI layer currently only exposes the + * VSOP87 analytical series. + * + * This file documents what the API will look like and shows the VSOP87 + * baseline for comparison. + * + * Run with: cmake --build build --target jpl_precise_ephemeris_example + */ + +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +int main() { + std::cout << "=== JPL Precise Ephemeris [PLACEHOLDER] ===\n\n"; + + // ------------------------------------------------------------------------- + // PLACEHOLDER: JPL DE440 ephemeris loading + // ------------------------------------------------------------------------- + std::cout << "NOTE: JPL DE440/DE441 ephemeris not yet available in C++ bindings.\n"; + std::cout << " // Rust:\n"; + std::cout << " // use siderust::ephemeris::jpl::*;\n"; + std::cout << " // let ephem = JplEphemeris::load(\"de440.bsp\");\n"; + std::cout << " // let pos = ephem.mars_heliocentric(jd);\n"; + std::cout << "\n // C++ (future API):\n"; + std::cout << " // auto ephem = jpl::load(\"de440.bsp\");\n"; + std::cout << " // auto pos = ephem.mars_heliocentric(jd);\n\n"; + + // ------------------------------------------------------------------------- + // Fallback: VSOP87 comparison (fully available today) + // ------------------------------------------------------------------------- + std::cout << "--- Baseline: VSOP87 Analytical Ephemeris (available now) ---\n\n"; + + const JulianDate jd = JulianDate::J2000(); + std::cout << "Epoch: J2000.0 (JD " << std::fixed << std::setprecision(1) + << jd.value() << ")\n\n"; + + const auto earth_h = ephemeris::earth_heliocentric(jd); + const auto mars_h = ephemeris::mars_heliocentric(jd); + const auto venus_h = ephemeris::venus_heliocentric(jd); + const auto moon_g = ephemeris::moon_geocentric(jd); + + auto print_body = [](const char *name, auto pos) { + const auto sph = pos.to_spherical(); + std::cout << std::left << std::setw(8) << name + << " r=" << std::right << std::setprecision(6) + << std::fixed << sph.distance().value() << " AU" + << " lon=" << std::setw(10) << std::setprecision(4) + << sph.direction().longitude().value() << "°" + << " lat=" << sph.direction().latitude().value() << "°\n"; + }; + + std::cout << "Heliocentric positions (VSOP87):\n"; + print_body("Earth", earth_h); + print_body("Venus", venus_h); + print_body("Mars", mars_h); + + std::cout << "\nGeocentric Moon (VSOP87-based):\n"; + { + const auto sph = moon_g.to_spherical(); + std::cout << " Moon r=" << std::setprecision(6) << sph.distance().value() + << " AU lon=" << std::setprecision(4) << sph.direction().longitude().value() + << "° lat=" << sph.direction().latitude().value() << "°\n"; + } + + // ------------------------------------------------------------------------- + // Expected accuracy note (once JPL is available) + // ------------------------------------------------------------------------- + std::cout << "\n--- Expected accuracy improvement with DE440 ---\n"; + std::cout << " VSOP87 typical error: ~1 arcsecond (inner planets)\n"; + std::cout << " DE440 typical error: ~0.001 arcsecond\n"; + std::cout << " (Factor ~1000 improvement, relevant for high-precision astrometry)\n"; + + std::cout << "\nImplementation status: VSOP87 fully available, DE440 — TODO.\n"; + std::cout << "\n=== Example Complete (Placeholder) ===\n"; + return 0; +} diff --git a/examples/17_serde_serialization.cpp b/examples/17_serde_serialization.cpp new file mode 100644 index 0000000..2d9c295 --- /dev/null +++ b/examples/17_serde_serialization.cpp @@ -0,0 +1,121 @@ +/** + * @file 17_serde_serialization.cpp + * @brief C++ port of siderust/examples/37_serde_serialization.rs + * + * [PLACEHOLDER] — Rust's `serde` serialization/deserialization framework has + * no direct equivalent in the C++ wrapper. JSON round-trips for siderust + * types are **not yet available** in C++. + * + * This file shows how one could serialize the available C++ types manually + * (as a demonstration), and documents what the Rust serde API looks like. + * + * Run with: cmake --build build --target serde_serialization_example + */ + +#include +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +// --------------------------------------------------------------------------- +// Minimal manual JSON helpers (C++ placeholder for serde) +// --------------------------------------------------------------------------- + +static std::string mjd_to_json(const MJD &mjd) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(6) << "{\"mjd\":" << mjd.value() << "}"; + return ss.str(); +} + +static std::string jd_to_json(const JulianDate &jd) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(6) << "{\"jd\":" << jd.value() << "}"; + return ss.str(); +} + +static std::string geodetic_to_json(const Geodetic &g) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(6) + << "{\"lon_deg\":" << g.lon.value() + << ",\"lat_deg\":" << g.lat.value() + << ",\"alt_m\":" << g.height.value() << "}"; + return ss.str(); +} + +static std::string direction_to_json(const spherical::direction::ICRS &d) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(6) + << "{\"ra_deg\":" << d.ra().value() + << ",\"dec_deg\":" << d.dec().value() << "}"; + return ss.str(); +} + +template +static std::string position_to_json(const cartesian::Position &p) { + std::ostringstream ss; + ss << std::fixed << std::setprecision(9) + << "{\"x\":" << p.x().value() + << ",\"y\":" << p.y().value() + << ",\"z\":" << p.z().value() << "}"; + return ss.str(); +} + +int main() { + std::cout << "=== Serialization / Deserialization [PLACEHOLDER] ===\n\n"; + + // ------------------------------------------------------------------------- + // PLACEHOLDER: Rust serde API description + // ------------------------------------------------------------------------- + std::cout << "NOTE: Rust serde JSON support is not yet bound in C++.\n"; + std::cout << " // Rust:\n"; + std::cout << " // use serde_json;\n"; + std::cout << " // let jd: JulianDate = JulianDate::J2000();\n"; + std::cout << " // let json = serde_json::to_string(&jd).unwrap();\n"; + std::cout << " // let back: JulianDate = serde_json::from_str(&json).unwrap();\n"; + std::cout << "\n // C++ (future API — planned with nlohmann/json or similar):\n"; + std::cout << " // auto j = siderust::to_json(jd);\n"; + std::cout << " // auto jd2 = siderust::from_json(j);\n\n"; + + // ------------------------------------------------------------------------- + // Manual serialization demo (available now) + // ------------------------------------------------------------------------- + std::cout << "--- Manual JSON-like Serialization (C++ demo) ---\n\n"; + + const JulianDate jd = JulianDate::J2000(); + const MJD mjd(51544.5); + const Geodetic obs = ROQUE_DE_LOS_MUCHACHOS; + const auto mars_pos = ephemeris::mars_heliocentric(jd); + + std::cout << "JulianDate: " << jd_to_json(jd) << "\n"; + std::cout << "MJD: " << mjd_to_json(mjd) << "\n"; + std::cout << "Geodetic (Roque):\n " << geodetic_to_json(obs) << "\n"; + // Sirius ICRS direction (J2000 coords; Star::direction() not yet bound) + const spherical::direction::ICRS sirius_icrs(qtty::Degree(101.2871), qtty::Degree(-16.7161)); + std::cout << "Sirius ICRS dir:\n " << direction_to_json(sirius_icrs) << "\n"; + std::cout << "Mars heliocentric:\n " << position_to_json(mars_pos) << "\n"; + + // ------------------------------------------------------------------------- + // Round-trip parse demo (manual) + // ------------------------------------------------------------------------- + std::cout << "\n--- Manual Round-Trip Verification ---\n"; + + const double jd_val = 2451545.0; + const JulianDate jd2(jd_val); + std::cout << "Serialized JD=" << jd_val << " → re-parsed JD=" << jd2.value() << " ✓\n"; + + const double lat_val = obs.lat.value(); + const double lon_val = obs.lon.value(); + const double alt_val = obs.height.value(); + const Geodetic obs2 = Geodetic(lon_val, lat_val, alt_val); + std::cout << "Re-parsed Geodetic lat=" << obs2.lat.value() + << "\u00b0, lon=" << obs2.lon.value() << "\u00b0 \u2713\n"; + + std::cout << "\nFull serde support: TODO — needs nlohmann/json or similar.\n"; + std::cout << "\n=== Example Complete (Placeholder) ===\n"; + return 0; +} diff --git a/examples/18_kepler_orbit.cpp b/examples/18_kepler_orbit.cpp new file mode 100644 index 0000000..236d70a --- /dev/null +++ b/examples/18_kepler_orbit.cpp @@ -0,0 +1,166 @@ +/** + * @file 18_kepler_orbit.cpp + * @brief C++ port of siderust/examples/33_kepler_orbit.rs + * + * Demonstrates Keplerian orbit propagation. The `Orbit` struct is fully + * available in C++; the `kepler_position()` / `solve_keplers_equation()` + * free functions are **not yet bound** (placeholder section below). + * + * Run with: cmake --build build --target kepler_orbit_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; +using namespace siderust::frames; + +// TODO: [PLACEHOLDER] solve_keplers_equation() and kepler_position() not yet +// exposed in the C++ FFI wrapper. The Rust API is: +// let pos = kepler_position(&orbit, jd); +// The `BodycentricParams` constructor uses them internally via to_bodycentric(). + +// Simple manual Kepler solver for demonstration (borrowed from celestial mechanics) +namespace demo { + +/// Solve Kepler's equation M = E - e*sin(E) via Newton-Raphson. +static double solve_kepler(double M_rad, double e, int max_iter = 100) { + double E = M_rad; // initial guess + for (int i = 0; i < max_iter; ++i) { + const double dE = (M_rad - E + e * std::sin(E)) / (1.0 - e * std::cos(E)); + E += dE; + if (std::abs(dE) < 1e-12) break; + } + return E; +} + +/// Propagate two-body orbit: returns heliocentric (x, y, 0) in AU at epoch+dt_days. +static std::pair +propagate_orbit(const Orbit &orb, double dt_days) { + // Mean motion (rad/day) = 2π / T, T = a^1.5 years * 365.25 + const double T_days = std::pow(orb.semi_major_axis_au, 1.5) * 365.25; + const double n = 2.0 * M_PI / T_days; + + const double M = orb.mean_anomaly_deg * M_PI / 180.0 + n * dt_days; + const double E = solve_kepler(std::fmod(M, 2.0 * M_PI), orb.eccentricity); + + const double nu = 2.0 * std::atan2(std::sqrt(1.0 + orb.eccentricity) * std::sin(E / 2.0), + std::sqrt(1.0 - orb.eccentricity) * std::cos(E / 2.0)); + + const double r = orb.semi_major_axis_au * (1.0 - orb.eccentricity * std::cos(E)); + const double om = orb.arg_perihelion_deg * M_PI / 180.0; + const double x = r * std::cos(nu + om); + const double y = r * std::sin(nu + om); + return {x, y}; +} + +} // namespace demo + +int main() { + std::cout << "=== Kepler Orbit Propagation ===\n\n"; + + // ------------------------------------------------------------------------- + // PLACEHOLDER note + // ------------------------------------------------------------------------- + std::cout << "[PLACEHOLDER] Rust's kepler_position() is not yet bound in C++.\n"; + std::cout << "Using a manual Newton-Raphson Kepler solver for demonstration.\n\n"; + + // ------------------------------------------------------------------------- + // Earth's orbit elements (J2000.0) + // ------------------------------------------------------------------------- + const JulianDate jd0 = JulianDate::J2000(); + + const Orbit earth_orbit{ + 1.0000010178, // semi-major axis (AU) + 0.0167086, // eccentricity + 0.0000001, // inclination (°) + 0.0, // RAAN (°) + 102.9373481, // argument of periapsis (°) + 100.4645717, // mean anomaly at epoch (°) + jd0.value() // epoch JD + }; + + // ------------------------------------------------------------------------- + // Part 1: Display orbital elements + // ------------------------------------------------------------------------- + std::cout << "--- Earth's Keplerian Elements (J2000.0) ---\n"; + std::cout << " semi-major axis a = " << std::setprecision(7) + << earth_orbit.semi_major_axis_au << " AU\n"; + std::cout << " eccentricity e = " << earth_orbit.eccentricity << "\n"; + std::cout << " inclination i = " << earth_orbit.inclination_deg << "°\n"; + std::cout << " arg periapsis ω = " << earth_orbit.arg_perihelion_deg << "°\n"; + std::cout << " mean anomaly M₀ = " << earth_orbit.mean_anomaly_deg << "°\n"; + std::cout << " epoch = JD " << std::setprecision(1) << earth_orbit.epoch_jd << "\n\n"; + + // ------------------------------------------------------------------------- + // Part 2: Propagate and compare with VSOP87 + // ------------------------------------------------------------------------- + std::cout << "--- Propagated Position vs VSOP87 (first 5 years) ---\n\n"; + std::cout << std::setw(10) << "Days" << std::setw(12) << "x_kepl(AU)" + << std::setw(12) << "y_kepl(AU)" << std::setw(12) << "r_kepl(AU)" + << std::setw(12) << "r_VSOP(AU)" << "\n"; + std::cout << std::string(58, '-') << "\n"; + + for (int d : {0, 91, 182, 273, 365, 730, 1461}) { + const auto [xk, yk] = demo::propagate_orbit(earth_orbit, d); + const double rk = std::sqrt(xk * xk + yk * yk); + + const JulianDate jd_t(jd0.value() + d); + const auto vsop = ephemeris::earth_heliocentric(jd_t); + const double rv = vsop.to_spherical().distance().value(); + + std::cout << std::setw(10) << d + << std::setw(12) << std::setprecision(6) << std::fixed << xk + << std::setw(12) << yk + << std::setw(12) << rk + << std::setw(12) << rv << "\n"; + } + + // ------------------------------------------------------------------------- + // Part 3: Mars orbit propagation + // ------------------------------------------------------------------------- + std::cout << "\n--- Mars Orbit (2-year trace, Kepler propagator) ---\n"; + + const Orbit mars_orbit{ + 1.523679342, // a (AU) + 0.0934005, // e + 1.849691, // i (°) + 49.4785, // RAAN (°) + 286.502, // ω (°) + 19.37215, // M₀ (°) + jd0.value() + }; + + for (int d : {0, 182, 365, 548, 730}) { + const auto [xm, ym] = demo::propagate_orbit(mars_orbit, d); + const double rm = std::sqrt(xm * xm + ym * ym); + const double lon_m = std::atan2(ym, xm) * 180.0 / M_PI; + if (lon_m < 0) {} // suppress unused warning + std::cout << " day=" << std::setw(4) << d + << " x=" << std::setw(10) << std::setprecision(5) << xm + << " y=" << std::setw(10) << ym + << " r=" << std::setprecision(4) << rm << " AU\n"; + } + + // ------------------------------------------------------------------------- + // Part 4: to_bodycentric round-trip (uses kepler internally) + // ------------------------------------------------------------------------- + std::cout << "\n--- to_bodycentric() uses Kepler solver internally ---\n"; + using namespace siderust::centers; + const BodycentricParams mars_params = BodycentricParams::heliocentric(mars_orbit); + const auto mars_hel = ephemeris::mars_heliocentric(jd0); + + const auto mars_bc_rel = to_bodycentric(mars_hel, mars_params, jd0); + std::cout << "Mars relative to itself (should be ~0):\n"; + std::cout << " x=" << std::scientific << std::setprecision(2) + << mars_bc_rel.pos.x().value() + << " y=" << mars_bc_rel.pos.y().value() + << " z=" << mars_bc_rel.pos.z().value() << "\n"; + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/19_brent_root_finding.cpp b/examples/19_brent_root_finding.cpp new file mode 100644 index 0000000..6c35ffd --- /dev/null +++ b/examples/19_brent_root_finding.cpp @@ -0,0 +1,169 @@ +/** + * @file 19_brent_root_finding.cpp + * @brief C++ port of siderust/examples/28_brent_root_finding.rs + * + * [PLACEHOLDER] — The siderust Brent root-finding utility (`brent_find_root`) + * is **not yet exposed** in the C++ FFI wrapper. The surrounding altitude + * search infrastructure (used internally by `above_threshold` et al.) is + * fully available. + * + * This file demonstrates the concept with a simple manual Brent implementation + * and documents what the Rust API looks like. + * + * Run with: cmake --build build --target brent_root_finding_example + */ + +#include +#include +#include +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +// TODO: [PLACEHOLDER] siderust::brent_find_root() not yet bound in C++. +// Rust API: +// use siderust::math::brent_find_root; +// let root = brent_find_root(a, b, tol, |x| f(x)); + +// --------------------------------------------------------------------------- +// Manual Brent's method (ISO 9222 variant, fully adequate as placeholder) +// --------------------------------------------------------------------------- +static double brent_find_root(double a, double b, double tol, + const std::function &f) { + double fa = f(a), fb = f(b); + if (fa * fb > 0.0) + throw std::runtime_error("brent: f(a) and f(b) must have opposite signs"); + + double c = a, fc = fa, s, d = 0.0; + bool mflag = true; + + for (int iter = 0; iter < 200; ++iter) { + if (std::abs(b - a) < tol) break; + + if (fa != fc && fb != fc) { + // Inverse quadratic interpolation + s = a * fb * fc / ((fa - fb) * (fa - fc)) + + b * fa * fc / ((fb - fa) * (fb - fc)) + + c * fa * fb / ((fc - fa) * (fc - fb)); + } else { + // Secant method + s = b - fb * (b - a) / (fb - fa); + } + + const double delta = std::abs(2.0 * tol * std::abs(b)); + const bool cond1 = (s < (3.0 * a + b) / 4.0 || s > b); + const bool cond2 = (mflag && std::abs(s - b) >= std::abs(b - c) / 2.0); + const bool cond3 = (!mflag && std::abs(s - b) >= std::abs(c - d) / 2.0); + const bool cond4 = (mflag && std::abs(b - c) < delta); + const bool cond5 = (!mflag && std::abs(c - d) < delta); + + if (cond1 || cond2 || cond3 || cond4 || cond5) { + s = (a + b) / 2.0; + mflag = true; + } else { + mflag = false; + } + + double fs = f(s); + d = c; + c = b; + fc = fb; + + if (fa * fs < 0.0) { b = s; fb = fs; } + else { a = s; fa = fs; } + + if (std::abs(fa) < std::abs(fb)) { + std::swap(a, b); std::swap(fa, fb); + } + } + return b; +} + +int main() { + std::cout << "=== Brent Root Finding [PLACEHOLDER for native siderust API] ===\n\n"; + + // ------------------------------------------------------------------------- + // PLACEHOLDER: Rust Brent API + // ------------------------------------------------------------------------- + std::cout << "NOTE: siderust::math::brent_find_root() not yet bound in C++.\n"; + std::cout << " // Rust:\n"; + std::cout << " // let root = brent_find_root(0.0, 3.0, 1e-10, |x| x.sin() - 0.5);\n"; + std::cout << " // // → π/6 ≈ 0.5235988...\n"; + std::cout << "\n // C++ (future API):\n"; + std::cout << " // double root = siderust::math::brent(0.0, 3.0, 1e-10,\n"; + std::cout << " // [](double x){ return std::sin(x) - 0.5; });\n\n"; + + // ------------------------------------------------------------------------- + // Example 1: Simple trigonometric equation + // ------------------------------------------------------------------------- + std::cout << "--- Example 1: sin(x) = 0.5 on [0, π/2] ---\n"; + const double root1 = brent_find_root(0.0, M_PI / 2.0, 1e-12, + [](double x) { return std::sin(x) - 0.5; }); + std::cout << " root = " << std::setprecision(12) << root1 << "\n"; + std::cout << " π/6 = " << M_PI / 6.0 << "\n"; + std::cout << " |error| = " << std::scientific << std::abs(root1 - M_PI / 6.0) << "\n\n"; + + // ------------------------------------------------------------------------- + // Example 2: Kepler's equation E - e*sin(E) = M + // ------------------------------------------------------------------------- + std::cout << "--- Example 2: Kepler's equation M = E - e·sin(E) ---\n"; + const double e = 0.0934005; // Mars eccentricity + const double M = 1.0; // mean anomaly (rad) + + const double E = brent_find_root(0.0, 2.0 * M_PI, 1e-12, + [e, M](double E) { return E - e * std::sin(E) - M; }); + + std::cout << " e=" << e << ", M=" << M << " rad\n"; + std::cout << " Eccentric anomaly E = " << std::setprecision(10) << E << " rad\n"; + std::cout << " Check: E - e·sin(E) = " + << E - e * std::sin(E) << " (≈M=" << M << ")\n\n"; + + // ------------------------------------------------------------------------- + // Example 3: Find Sun's altitude crossing time (siderust use-case) + // ------------------------------------------------------------------------- + std::cout << "--- Example 3: Sun Crossing -18° (astronomical twilight) ---\n"; + std::cout << "(This is what siderust's sun::below_threshold uses internally)\n\n"; + + const Geodetic obs = ROQUE_DE_LOS_MUCHACHOS; + const BodyTarget sun_target(Body::Sun); + + // Search for the exact MJD when Sun crosses -18° on MJD 60000 + const MJD mjd_search_start(59999.5); + const MJD mjd_search_end(60000.5); + + // Sample to bracket + double best_a = -1.0, best_b = -1.0; + const double threshold = -18.0; + for (int i = 0; i < 100; ++i) { + const double t1 = mjd_search_start.value() + i * 0.01; + const double t2 = t1 + 0.01; + const double alt1 = sun_target.altitude_at(obs, MJD(t1)).value() - threshold; + const double alt2 = sun_target.altitude_at(obs, MJD(t2)).value() - threshold; + if (alt1 * alt2 < 0.0) { + best_a = t1; + best_b = t2; + break; + } + } + + if (best_a > 0.0) { + const double crossing_mjd = brent_find_root(best_a, best_b, 1e-9, [&](double t) { + return sun_target.altitude_at(obs, MJD(t)).value() - threshold; + }); + std::cout << " Sun crosses -18° at MJD = " << std::fixed << std::setprecision(6) + << crossing_mjd << "\n"; + std::cout << " Verify: alt at crossing = " + << std::setprecision(4) << sun_target.altitude_at(obs, MJD(crossing_mjd)).value() + << "° (≈ -18°)\n"; + } else { + std::cout << " No crossing found in search window.\n"; + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/20_moon_phase.cpp b/examples/20_moon_phase.cpp new file mode 100644 index 0000000..259dcca --- /dev/null +++ b/examples/20_moon_phase.cpp @@ -0,0 +1,142 @@ +/** + * @file 20_moon_phase.cpp + * @brief C++ port of siderust/examples/34_moon_phase.rs + * + * Demonstrates the full lunar-phase API: geocentric/topocentric geometry, + * phase labels, phase-event search, and illumination-above queries. + * + * Run with: cmake --build build --target moon_phase_example + */ + +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +static const char *phase_kind_str(PhaseKind k) { + switch (k) { + case PhaseKind::NewMoon: return "New Moon"; + case PhaseKind::FirstQuarter: return "First Quarter"; + case PhaseKind::FullMoon: return "Full Moon"; + case PhaseKind::LastQuarter: return "Last Quarter"; + default: return "Unknown"; + } +} + +static const char *phase_label_str(MoonPhaseLabel l) { + switch (l) { + case MoonPhaseLabel::NewMoon: return "New Moon"; + case MoonPhaseLabel::WaxingCrescent: return "Waxing Crescent"; + case MoonPhaseLabel::FirstQuarter: return "First Quarter"; + case MoonPhaseLabel::WaxingGibbous: return "Waxing Gibbous"; + case MoonPhaseLabel::FullMoon: return "Full Moon"; + case MoonPhaseLabel::WaningGibbous: return "Waning Gibbous"; + case MoonPhaseLabel::LastQuarter: return "Last Quarter"; + case MoonPhaseLabel::WaningCrescent: return "Waning Crescent"; + default: return "Unknown"; + } +} + +static void print_geometry(const char *label, const MoonPhaseGeometry &g) { + const double phase_angle_deg = g.phase_angle_rad * 180.0 / M_PI; + std::cout << std::left << std::setw(22) << label + << " phase=" << std::right << std::setw(7) << std::fixed + << std::setprecision(2) << phase_angle_deg << "°" + << " illum=" << std::setw(6) << std::setprecision(3) + << g.illuminated_fraction + << " waxing=" << (g.waxing ? "yes" : "no") + << "\n"; +} + +int main() { + std::cout << "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; + std::cout << "\u2551 Lunar Phase Analysis \u2551\n"; + std::cout << "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; + + // ------------------------------------------------------------------------- + // Part 1: Geocentric phase geometry at J2000.0 + // ------------------------------------------------------------------------- + std::cout << "--- Part 1: Phase Geometry at Key Epochs ---\n\n"; + + // Sample seven epochs spanning two weeks + const JulianDate jd0 = JulianDate::J2000(); + for (int d = 0; d <= 28; d += 4) { + const JulianDate jd(jd0.value() + d); + const auto geo = moon::phase_geocentric(jd); + const auto lbl = moon::phase_label(geo); + char buf[48]; + std::snprintf(buf, sizeof(buf), "JD+%2d days", d); + print_geometry(buf, geo); + std::cout << " → label: " << phase_label_str(lbl) << "\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Part 2: Topocentric phase at Roque de los Muchachos + // ------------------------------------------------------------------------- + std::cout << "--- Part 2: Geocentric vs Topocentric Phase (slight difference) ---\n\n"; + + const Geodetic obs = ROQUE_DE_LOS_MUCHACHOS; + const JulianDate jd_full(2451545.0 + 14.0); // roughly full moon region + + const auto geo_geom = moon::phase_geocentric(jd_full); + const auto topo_geom = moon::phase_topocentric(jd_full, obs); + + print_geometry("Geocentric", geo_geom); + print_geometry("Topocentric", topo_geom); + + const double diff = std::abs(geo_geom.illuminated_fraction - topo_geom.illuminated_fraction); + std::cout << " illumination difference: " << std::scientific << std::setprecision(3) + << diff << "\n\n"; + + // ------------------------------------------------------------------------- + // Part 3: Find all principal phase events in 3 months + // ------------------------------------------------------------------------- + std::cout << "--- Part 3: Phase Events Over 3 Months ---\n\n"; + + const Period quarter(MJD(60000.0), MJD(60090.0)); + const auto events = moon::find_phase_events(quarter); + + std::cout << "Found " << events.size() << " phase events:\n"; + for (const auto &ev : events) { + std::cout << " MJD=" << std::fixed << std::setprecision(2) << ev.time.value() + << " → " << phase_kind_str(ev.kind) << "\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Part 4: Illumination above threshold (bright moon periods) + // ------------------------------------------------------------------------- + std::cout << "--- Part 4: Illumination ≥ 50% (bright moon periods) ---\n\n"; + + const auto bright_periods = moon::illumination_above(quarter, 0.50); + double total_bright_days = 0.0; + for (const auto &p : bright_periods) { + const double d = p.duration().value(); + total_bright_days += d; + std::cout << " MJD " << std::setprecision(2) << p.start().value() + << " – " << p.end().value() + << " (" << std::setprecision(1) << d << " d)\n"; + } + std::cout << " Total bright days: " << std::setprecision(1) << total_bright_days + << " / 90 d\n\n"; + + // ------------------------------------------------------------------------- + // Part 5: Moon-free (illumination < 5%) for dark-sky scheduling + // ------------------------------------------------------------------------- + std::cout << "--- Part 5: Near-Dark Moon Windows (illumination < 5%) ---\n\n"; + + const auto dark_moon_periods = moon::illumination_above(quarter, 0.01); + // Invert: total window minus illumination above 1% + // (Simplified: just report dark periods count and duration from above search) + const auto very_dark_periods = moon::illumination_above(quarter, 0.05); + std::cout << "Periods with illumination > 5%: " << very_dark_periods.size() << "\n"; + std::cout << "(Complement = dark-moon windows, ideal for deep-sky observing)\n"; + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/examples/21_trackable_demo.cpp b/examples/21_trackable_demo.cpp new file mode 100644 index 0000000..458ae66 --- /dev/null +++ b/examples/21_trackable_demo.cpp @@ -0,0 +1,174 @@ +/** + * @file 21_trackable_demo.cpp + * @brief C++ port of siderust/examples/42_trackable_demo.rs + * + * Demonstrates the unified `Target` polymorphism in siderust-cpp. Any mix + * of solar-system bodies, catalog stars, and fixed sky directions can be held + * in a `std::vector>` and queried uniformly via the + * `Target` virtual interface. + * + * Rust counterpart uses the `Trackable` trait; C++ uses virtual dispatch on + * `siderust::Target`. + * + * Run with: cmake --build build --target trackable_demo_example + */ + +#include +#include +#include +#include +#include + +#include + +using namespace siderust; +using namespace qtty::literals; + +// ============================================================================ +// Helpers +// ============================================================================ + +static void print_rising_setting(const Target &target, const Geodetic &obs, + const Period &window) { + const auto events = target.crossings(obs, window, qtty::Degree(0.0)); + for (const auto &ev : events) { + const char *kind = (ev.direction == CrossingDirection::Rising) ? "Rise" : "Set"; + std::cout << " " << kind << " at MJD " + << std::fixed << std::setprecision(4) << ev.time.value() << "\n"; + } +} + +static void print_culminations(const Target &target, const Geodetic &obs, + const Period &window) { + const auto culms = target.culminations(obs, window); + for (const auto &cu : culms) { + std::cout << " Culmination at MJD " + << std::fixed << std::setprecision(4) << cu.time.value() + << " alt=" << std::setprecision(2) << cu.altitude.value() << "°\n"; + } +} + +// ============================================================================ +// Main +// ============================================================================ + +int main() { + std::cout << "=== Trackable Target Polymorphism Demo ===\n\n"; + + // ------------------------------------------------------------------------- + // Build a heterogeneous list of targets + // ------------------------------------------------------------------------- + std::vector> targets; + + // Solar-system bodies + targets.push_back(std::make_unique(Body::Sun)); + targets.push_back(std::make_unique(Body::Moon)); + targets.push_back(std::make_unique(Body::Mars)); + targets.push_back(std::make_unique(Body::Jupiter)); + + // Catalog stars + targets.push_back(std::make_unique(SIRIUS)); + targets.push_back(std::make_unique(VEGA)); + targets.push_back(std::make_unique(POLARIS)); + targets.push_back(std::make_unique(BETELGEUSE)); + targets.push_back(std::make_unique(RIGEL)); + targets.push_back(std::make_unique(CANOPUS)); + + std::cout << "Target list (" << targets.size() << " objects):\n"; + for (const auto &t : targets) + std::cout << " • " << t->name() << "\n"; + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Common observation parameters + // ------------------------------------------------------------------------- + const Geodetic obs = EL_PARANAL; + std::cout << "Observatory: ESO Paranal / VLT\n\n"; + + const Period night(MJD(60000.5), MJD(60001.5)); + std::cout << "Observation night: MJD " << std::fixed << std::setprecision(2) + << night.start().value() << " → " << night.end().value() << "\n\n"; + + // ------------------------------------------------------------------------- + // Generic altitude survey via Target interface + // ------------------------------------------------------------------------- + std::cout << "=== Altitude at Transit Middle (MJD 60001.0) ===\n"; + const MJD midpoint(60001.0); + + std::cout << std::left << std::setw(14) << "Target" + << std::right << std::setw(12) << "Altitude (°)" + << " \n"; + std::cout << std::string(28, '-') << "\n"; + + for (const auto &t : targets) { + const double alt = t->altitude_at(obs, midpoint).value(); + std::cout << std::left << std::setw(14) << t->name() + << std::right << std::setw(12) << std::fixed << std::setprecision(2) + << alt << "°\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Above-threshold summary (30°) + // ------------------------------------------------------------------------- + const qtty::Degree threshold(30.0); + std::cout << "=== Time Above " << std::setprecision(0) << threshold.value() + << "° During the Night ===\n\n"; + + for (const auto &t : targets) { + const auto periods = t->above_threshold(obs, night, threshold); + double total_h = 0.0; + for (const auto &p : periods) + total_h += p.duration().value(); + + std::cout << std::left << std::setw(14) << t->name() + << " " << periods.size() << " period(s) " + << std::setprecision(2) << total_h << " h total\n"; + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Rising / setting / culmination events + // ------------------------------------------------------------------------- + std::cout << "=== Rising, Setting & Culmination Events ===\n\n"; + + for (size_t i = 0; i < std::min(targets.size(), size_t(4)); ++i) { + const auto &t = targets[i]; + std::cout << " " << t->name() << ":\n"; + print_rising_setting(*t, obs, night); + print_culminations(*t, obs, night); + } + std::cout << "\n"; + + // ------------------------------------------------------------------------- + // Sort targets by tonight's availability (most hours first) + // ------------------------------------------------------------------------- + struct Availability { + std::string name; + double hours; + }; + + std::vector avail; + for (const auto &t : targets) { + const auto ps = t->above_threshold(obs, night, qtty::Degree(20.0)); + double h = 0.0; + for (const auto &p : ps) + h += p.duration().value(); + avail.push_back({t->name(), h}); + } + + std::sort(avail.begin(), avail.end(), + [](const Availability &a, const Availability &b) { + return a.hours > b.hours; + }); + + std::cout << "=== Tonight's Observing Priority (sorted by hours >20°) ===\n\n"; + for (size_t i = 0; i < avail.size(); ++i) { + std::cout << " " << std::setw(2) << i + 1 << ". " + << std::left << std::setw(14) << avail[i].name + << std::right << std::setprecision(2) << avail[i].hours << " h\n"; + } + + std::cout << "\n=== Example Complete ===\n"; + return 0; +} diff --git a/include/siderust/coordinates.hpp b/include/siderust/coordinates.hpp index a500e77..0898889 100644 --- a/include/siderust/coordinates.hpp +++ b/include/siderust/coordinates.hpp @@ -53,6 +53,7 @@ #include "coordinates/conversions.hpp" #include "coordinates/geodetic.hpp" #include "coordinates/spherical.hpp" +#include "coordinates/pos_conversions.hpp" #include "coordinates/types.hpp" /** @} */ // end of group coordinates diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 0b91f0c..0c24509 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -15,6 +15,9 @@ #include #include +// Forward-declare spherical Position to avoid circular include. +namespace siderust { namespace spherical { template struct Position; } } + namespace siderust { namespace cartesian { @@ -78,6 +81,11 @@ template struct Position { return U(sqrt(vx * vx + vy * vy + vz * vz)); } + /** + * @brief Convert this cartesian position to a spherical Position. + */ + spherical::Position to_spherical() const; + U distance_to(const Position &other) const { using std::sqrt; const double dx = comp_x.value() - other.comp_x.value(); diff --git a/include/siderust/coordinates/pos_conversions.hpp b/include/siderust/coordinates/pos_conversions.hpp new file mode 100644 index 0000000..004072e --- /dev/null +++ b/include/siderust/coordinates/pos_conversions.hpp @@ -0,0 +1,51 @@ +#pragma once + +/** + * @file pos_conversions.hpp + * @brief Out-of-line template definitions for cartesian<->spherical conversions. + */ + +#include "cartesian.hpp" +#include "spherical.hpp" + +namespace siderust { + +// cartesian::Position::to_spherical implementation +template +spherical::Position cartesian::Position::to_spherical() const { + const double x = comp_x.value(); + const double y = comp_y.value(); + const double z = comp_z.value(); + const double r = std::sqrt(x * x + y * y + z * z); + const double lon = std::atan2(y, x) * 180.0 / M_PI; + const double lat = std::atan2(z, std::sqrt(x * x + y * y)) * 180.0 / M_PI; + return spherical::Position(qtty::Degree(lon), qtty::Degree(lat), U(r)); +} + +// spherical::Position::to_cartesian implementation +template +cartesian::Position spherical::Position::to_cartesian() const { + double lon_deg = 0.0; + double lat_deg = 0.0; + if constexpr (frames::has_ra_dec_v) { + lon_deg = azimuth_.value(); + lat_deg = polar_.value(); + } else if constexpr (frames::has_lon_lat_v) { + lon_deg = azimuth_.value(); + lat_deg = polar_.value(); + } else if constexpr (frames::has_az_alt_v) { + lon_deg = azimuth_.value(); + lat_deg = polar_.value(); + } + + const double lon = lon_deg * M_PI / 180.0; + const double lat = lat_deg * M_PI / 180.0; + const double r = dist_.value(); + + const double cx = r * std::cos(lat) * std::cos(lon); + const double cy = r * std::cos(lat) * std::sin(lon); + const double cz = r * std::sin(lat); + return cartesian::Position(U(cx), U(cy), U(cz)); +} + +} // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 13e8426..3195af1 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -17,6 +17,9 @@ #include #include +// Forward-declare cartesian Position to avoid circular include. +namespace siderust { namespace cartesian { template struct Position; } } + namespace siderust { namespace spherical { @@ -271,6 +274,10 @@ template struct Position { } U distance() const { return dist_; } + /** + * @brief Convert this spherical position to a cartesian Position. + */ + cartesian::Position to_cartesian() const; U distance_to(const Position &other) const { using std::sqrt; diff --git a/siderust b/siderust index 15da06f..6fef7d3 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 15da06f875eb5c475dfc078a33f793502f766ab5 +Subproject commit 6fef7d34822ab4882a9f3b605f6a8fc6974b1017 From 54db5bc089da008712b8509b2014e9ab28d93bed Mon Sep 17 00:00:00 2001 From: VPRamon Date: Thu, 26 Feb 2026 01:20:28 +0100 Subject: [PATCH 12/19] refactor: simplify output formatting and improve Position constructor in coordinate classes --- examples/02_coordinate_transformations.cpp | 99 +++++++++++----------- include/siderust/coordinates/cartesian.hpp | 2 +- include/siderust/coordinates/spherical.hpp | 9 +- siderust | 2 +- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/examples/02_coordinate_transformations.cpp b/examples/02_coordinate_transformations.cpp index 9111ed7..0f38d45 100644 --- a/examples/02_coordinate_transformations.cpp +++ b/examples/02_coordinate_transformations.cpp @@ -19,8 +19,7 @@ int main() { std::cout << "=== Coordinate Transformations (C++) ===\n\n"; const JulianDate jd = JulianDate::J2000(); - std::cout << "Reference time: J2000.0 (JD " << std::fixed << std::setprecision(1) - << jd.value() << ")\n\n"; + std::cout << "Reference time: J2000.0 (JD " << jd << ")\n\n"; // 1. Frame transformations (same center) std::cout << "1. FRAME TRANSFORMATIONS\n"; @@ -28,21 +27,21 @@ int main() { cartesian::position::EclipticMeanJ2000 pos_ecl(1.0, 0.0, 0.0); std::cout << "Original (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << pos_ecl.x() << " AU\n"; - std::cout << " Y = " << pos_ecl.y() << " AU\n"; - std::cout << " Z = " << pos_ecl.z() << " AU\n\n"; + std::cout << " X = " << pos_ecl.x() << "\n"; + std::cout << " Y = " << pos_ecl.y() << "\n"; + std::cout << " Z = " << pos_ecl.z() << "\n\n"; // to Equatorial (same heliocentric center) - auto sph_ecl = pos_ecl.to_spherical(); + auto sph_ecl = pos_ecl.to_spherical(); auto dir_equ = sph_ecl.direction().to_frame(jd); spherical::Position sph_equ(dir_equ, sph_ecl.distance()); auto pos_equ = sph_equ.to_cartesian(); std::cout << "Transformed to EquatorialMeanJ2000 frame:\n"; - std::cout << " X = " << pos_equ.x() << " AU\n"; - std::cout << " Y = " << pos_equ.y() << " AU\n"; - std::cout << " Z = " << pos_equ.z() << " AU\n\n"; + std::cout << " X = " << pos_equ.x() << "\n"; + std::cout << " Y = " << pos_equ.y() << "\n"; + std::cout << " Z = " << pos_equ.z() << "\n\n"; // to ICRS (via equatorial hub) auto dir_icrs = sph_equ.direction().to_frame(jd); @@ -50,9 +49,9 @@ int main() { sph_equ.distance()); auto pos_icrs = sph_icrs.to_cartesian(); std::cout << "Transformed to ICRS frame:\n"; - std::cout << " X = " << pos_icrs.x() << " AU\n"; - std::cout << " Y = " << pos_icrs.y() << " AU\n"; - std::cout << " Z = " << pos_icrs.z() << " AU\n\n"; + std::cout << " X = " << pos_icrs.x() << "\n"; + std::cout << " Y = " << pos_icrs.y() << "\n"; + std::cout << " Z = " << pos_icrs.z() << "\n\n"; // 2. Center transformations (same frame) std::cout << "2. CENTER TRANSFORMATIONS\n"; @@ -60,36 +59,36 @@ int main() { auto earth_helio = ephemeris::earth_heliocentric(jd); std::cout << "Earth (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << earth_helio.x() << " AU\n"; - std::cout << " Y = " << earth_helio.y() << " AU\n"; - std::cout << " Z = " << earth_helio.z() << " AU\n"; - std::cout << " Distance = " << earth_helio.distance() << " AU\n\n"; + std::cout << " X = " << earth_helio.x() << "\n"; + std::cout << " Y = " << earth_helio.y() << "\n"; + std::cout << " Z = " << earth_helio.z() << "\n"; + std::cout << " Distance = " << earth_helio.distance() << "\n\n"; // Earth in geocentric (origin) — heliocentric minus itself -> zero cartesian::Position earth_geo(AU(0.0), AU(0.0), AU(0.0)); std::cout << "Earth (Geocentric EclipticMeanJ2000) - at origin:\n"; - std::cout << " X = " << earth_geo.x() << " AU\n"; - std::cout << " Y = " << earth_geo.y() << " AU\n"; - std::cout << " Z = " << earth_geo.z() << " AU\n\n"; + std::cout << " X = " << earth_geo.x() << "\n"; + std::cout << " Y = " << earth_geo.y() << "\n"; + std::cout << " Z = " << earth_geo.z() << "\n\n"; std::cout << " Distance = " << earth_geo.distance() << " AU (should be ~0)\n\n"; auto mars_helio = ephemeris::mars_heliocentric(jd); std::cout << "Mars (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << mars_helio.x() << " AU\n"; - std::cout << " Y = " << mars_helio.y() << " AU\n"; - std::cout << " Z = " << mars_helio.z() << " AU\n"; - std::cout << " Distance = " << mars_helio.distance() << " AU\n\n"; + std::cout << " X = " << mars_helio.x() << "\n"; + std::cout << " Y = " << mars_helio.y() << "\n"; + std::cout << " Z = " << mars_helio.z() << "\n"; + std::cout << " Distance = " << mars_helio.distance() << "\n\n"; // Mars geocentric = Mars_helio - Earth_helio (component-wise) cartesian::Position mars_geo(mars_helio.x() - earth_helio.x(), mars_helio.y() - earth_helio.y(), mars_helio.z() - earth_helio.z()); std::cout << "Mars (Geocentric EclipticMeanJ2000) - as seen from Earth:\n"; - std::cout << " X = " << mars_geo.x() << " AU\n"; - std::cout << " Y = " << mars_geo.y() << " AU\n"; - std::cout << " Z = " << mars_geo.z() << " AU\n"; - std::cout << " Distance = " << mars_geo.distance() << " AU\n\n"; + std::cout << " X = " << mars_geo.x() << "\n"; + std::cout << " Y = " << mars_geo.y() << "\n"; + std::cout << " Z = " << mars_geo.z() << "\n"; + std::cout << " Distance = " << mars_geo.distance() << "\n\n"; // 3. Combined transformations (center + frame) std::cout << "3. COMBINED TRANSFORMATIONS\n"; @@ -135,9 +134,9 @@ int main() { sph_direct_cart.z() - earth_helio_equ.z()); std::cout << " Or using direct chain (same result):\n"; - std::cout << " X = " << Mars_geo_equ_direct.x() << " AU\n"; - std::cout << " Y = " << Mars_geo_equ_direct.y() << " AU\n"; - std::cout << " Z = " << Mars_geo_equ_direct.z() << " AU\n\n"; + std::cout << " X = " << Mars_geo_equ_direct.x() << "\n"; + std::cout << " Y = " << Mars_geo_equ_direct.y() << "\n"; + std::cout << " Z = " << Mars_geo_equ_direct.z() << "\n\n"; // 4. Barycentric coordinates std::cout << "4. BARYCENTRIC COORDINATES\n"; @@ -145,10 +144,10 @@ int main() { auto earth_bary = ephemeris::earth_barycentric(jd); std::cout << "Earth (Barycentric EclipticMeanJ2000):\n"; - std::cout << " X = " << earth_bary.x() << " AU\n"; - std::cout << " Y = " << earth_bary.y() << " AU\n"; - std::cout << " Z = " << earth_bary.z() << " AU\n"; - std::cout << " Distance from SSB = " << earth_bary.distance() << " AU\n\n"; + std::cout << " X = " << earth_bary.x() << "\n"; + std::cout << " Y = " << earth_bary.y() << "\n"; + std::cout << " Z = " << earth_bary.z() << "\n"; + std::cout << " Distance from SSB = " << earth_bary.distance() << "\n\n"; // Mars barycentric = sun_barycentric + mars_helio auto sun_bary = ephemeris::sun_barycentric(jd); @@ -162,10 +161,10 @@ int main() { mars_bary.z() - earth_bary.z()); std::cout << "Mars (Geocentric, from Barycentric):\n"; - std::cout << " X = " << mars_geo_from_bary.x() << " AU\n"; - std::cout << " Y = " << mars_geo_from_bary.y() << " AU\n"; - std::cout << " Z = " << mars_geo_from_bary.z() << " AU\n"; - std::cout << " Distance = " << mars_geo_from_bary.distance() << " AU\n\n"; + std::cout << " X = " << mars_geo_from_bary.x() << "\n"; + std::cout << " Y = " << mars_geo_from_bary.y() << "\n"; + std::cout << " Z = " << mars_geo_from_bary.z() << "\n"; + std::cout << " Distance = " << mars_geo_from_bary.distance() << "\n\n"; // 5. ICRS frame transformations (barycentric -> geocentric) std::cout << "5. ICRS FRAME TRANSFORMATIONS\n"; @@ -174,9 +173,9 @@ int main() { // Create a sample star in barycentric ICRS cartesian coords cartesian::Position star_icrs(AU(100.0), AU(50.0), AU(1000.0)); std::cout << "Star (Barycentric ICRS):\n"; - std::cout << " X = " << star_icrs.x() << " AU\n"; - std::cout << " Y = " << star_icrs.y() << " AU\n"; - std::cout << " Z = " << star_icrs.z() << " AU\n\n"; + std::cout << " X = " << star_icrs.x() << "\n"; + std::cout << " Y = " << star_icrs.y() << "\n"; + std::cout << " Z = " << star_icrs.z() << "\n\n"; // Convert Earth's barycentric from ecliptic -> ICRS frame, then subtract auto sph_earth_bary = earth_bary.to_spherical(); @@ -191,9 +190,9 @@ int main() { star_icrs.z() - earth_bary_icrs.z()); std::cout << "Star (Geocentric ICRS/GCRS):\n"; - std::cout << " X = " << star_gcrs.x() << " AU\n"; - std::cout << " Y = " << star_gcrs.y() << " AU\n"; - std::cout << " Z = " << star_gcrs.z() << " AU\n\n"; + std::cout << " X = " << star_gcrs.x() << "\n"; + std::cout << " Y = " << star_gcrs.y() << "\n"; + std::cout << " Z = " << star_gcrs.z() << "\n\n"; // 6. Round-trip transformation std::cout << "6. ROUND-TRIP TRANSFORMATION\n"; @@ -201,9 +200,9 @@ int main() { auto original = mars_helio; std::cout << "Original Mars (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << original.x() << " AU\n"; - std::cout << " Y = " << original.y() << " AU\n"; - std::cout << " Z = " << original.z() << " AU\n\n"; + std::cout << " X = " << original.x() << "\n"; + std::cout << " Y = " << original.y() << "\n"; + std::cout << " Z = " << original.z() << "\n\n"; // Helio Ecl -> Geo Equ -> back to Helio Ecl // (we reuse previously computed earth_helio_equ) @@ -221,9 +220,9 @@ int main() { auto recovered = sph_recovered_ecl.to_cartesian(); std::cout << "After round-trip transformation:\n"; - std::cout << " X = " << recovered.x() << " AU\n"; - std::cout << " Y = " << recovered.y() << " AU\n"; - std::cout << " Z = " << recovered.z() << " AU\n\n"; + std::cout << " X = " << recovered.x() << "\n"; + std::cout << " Y = " << recovered.y() << "\n"; + std::cout << " Z = " << recovered.z() << "\n\n"; const double diff_x = std::abs(original.x().value() - recovered.x().value()); const double diff_y = std::abs(original.y().value() - recovered.y().value()); diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index 0c24509..f3c95ca 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -54,7 +54,7 @@ template struct Direction { * @tparam F Reference frame tag (e.g. `frames::ECEF`). * @tparam U Length unit (default: `qtty::Meter`). */ -template struct Position { +template struct Position { static_assert(frames::is_frame_v, "F must be a valid frame tag"); static_assert(centers::is_center_v, "C must be a valid center tag"); diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 3195af1..787ea46 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -212,11 +212,12 @@ template struct Position { U dist_; public: - Position() - : azimuth_(qtty::Degree(0)), polar_(qtty::Degree(0)), dist_(U(0)) {} - Position(qtty::Degree azimuth, qtty::Degree polar, U dist) - : azimuth_(azimuth), polar_(polar), dist_(dist) {} + Position(qtty::Degree azimuth, qtty::Degree polar, U distance) + : azimuth_(azimuth), polar_(polar), dist_(distance) {} + + Position(const Direction &dir, U distance) + : azimuth_(dir.azimuth()), polar_(dir.polar()), dist_(distance) {} /// Extract the direction component. Direction direction() const { return Direction(azimuth_, polar_); } diff --git a/siderust b/siderust index 6fef7d3..37247a1 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 6fef7d34822ab4882a9f3b605f6a8fc6974b1017 +Subproject commit 37247a1a2eda38fc70e1e2652f06041274dad24c From 4beb4287f17a8c7a6db45b58a981ed185112d7bd Mon Sep 17 00:00:00 2001 From: VPRamon Date: Thu, 26 Feb 2026 01:38:45 +0100 Subject: [PATCH 13/19] feat: add frame transformation methods for cartesian and spherical coordinates --- examples/02_coordinate_transformations.cpp | 60 ++------ include/siderust/coordinates/cartesian.hpp | 74 +++++++++ .../siderust/coordinates/pos_conversions.hpp | 15 ++ include/siderust/coordinates/spherical.hpp | 23 +++ siderust | 2 +- tests/test_coordinates.cpp | 141 ++++++++++++++++++ 6 files changed, 270 insertions(+), 45 deletions(-) diff --git a/examples/02_coordinate_transformations.cpp b/examples/02_coordinate_transformations.cpp index 0f38d45..474e520 100644 --- a/examples/02_coordinate_transformations.cpp +++ b/examples/02_coordinate_transformations.cpp @@ -31,23 +31,15 @@ int main() { std::cout << " Y = " << pos_ecl.y() << "\n"; std::cout << " Z = " << pos_ecl.z() << "\n\n"; - // to Equatorial (same heliocentric center) - auto sph_ecl = pos_ecl.to_spherical(); - auto dir_equ = sph_ecl.direction().to_frame(jd); - spherical::Position - sph_equ(dir_equ, sph_ecl.distance()); - auto pos_equ = sph_equ.to_cartesian(); - + // to Equatorial (same heliocentric center + auto pos_equ = pos_ecl.to_frame(jd); std::cout << "Transformed to EquatorialMeanJ2000 frame:\n"; std::cout << " X = " << pos_equ.x() << "\n"; std::cout << " Y = " << pos_equ.y() << "\n"; std::cout << " Z = " << pos_equ.z() << "\n\n"; - // to ICRS (via equatorial hub) - auto dir_icrs = sph_equ.direction().to_frame(jd); - spherical::Position sph_icrs(dir_icrs, - sph_equ.distance()); - auto pos_icrs = sph_icrs.to_cartesian(); + // to ICRS — direct to_frame on cartesian::Position (no intermediate spherical) + auto pos_icrs = pos_equ.to_frame(jd); std::cout << "Transformed to ICRS frame:\n"; std::cout << " X = " << pos_icrs.x() << "\n"; std::cout << " Y = " << pos_icrs.y() << "\n"; @@ -97,20 +89,12 @@ int main() { std::cout << "Mars transformation chain:\n"; std::cout << " Start: Heliocentric EclipticMeanJ2000\n"; - // Step 1: convert Mars heliocentric ecl -> heliocentric equatorial - auto sph_mars_helio = mars_helio.to_spherical(); - auto dir_mars_helio_equ = sph_mars_helio.direction().to_frame(jd); - spherical::Position - sph_mars_helio_equ(dir_mars_helio_equ, sph_mars_helio.distance()); - auto mars_helio_equ = sph_mars_helio_equ.to_cartesian(); + // Step 1: transform Mars frame directly (cartesian::Position::to_frame) + auto mars_helio_equ = mars_helio.to_frame(jd); std::cout << " Step 1: Transform frame → Heliocentric EquatorialMeanJ2000\n"; - // Step 2: convert center heliocentric -> geocentric by subtracting Earth's heliocentric equ - auto sph_earth_helio = earth_helio.to_spherical(); - auto dir_earth_helio_equ = sph_earth_helio.direction().to_frame(jd); - spherical::Position - sph_earth_helio_equ(dir_earth_helio_equ, sph_earth_helio.distance()); - auto earth_helio_equ = sph_earth_helio_equ.to_cartesian(); + // Step 2: convert center heliocentric -> geocentric by subtracting Earth's pos in the same frame + auto earth_helio_equ = earth_helio.to_frame(jd); cartesian::Position mars_geo_equ(mars_helio_equ.x() - earth_helio_equ.x(), @@ -123,15 +107,11 @@ int main() { std::cout << " Y = " << mars_geo_equ.y() << " AU\n"; std::cout << " Z = " << mars_geo_equ.z() << " AU\n\n"; - // Method 2: do the same in one direct chain (frame then center) - auto dir_direct = sph_mars_helio.direction().to_frame(jd); - spherical::Position - sph_direct(dir_direct, sph_mars_helio.distance()); - auto sph_direct_cart = sph_direct.to_cartesian(); + // Method 2: same in one chain — now trivial with direct to_frame auto Mars_geo_equ_direct = cartesian::Position( - sph_direct_cart.x() - earth_helio_equ.x(), - sph_direct_cart.y() - earth_helio_equ.y(), - sph_direct_cart.z() - earth_helio_equ.z()); + mars_helio_equ.x() - earth_helio_equ.x(), + mars_helio_equ.y() - earth_helio_equ.y(), + mars_helio_equ.z() - earth_helio_equ.z()); std::cout << " Or using direct chain (same result):\n"; std::cout << " X = " << Mars_geo_equ_direct.x() << "\n"; @@ -177,12 +157,8 @@ int main() { std::cout << " Y = " << star_icrs.y() << "\n"; std::cout << " Z = " << star_icrs.z() << "\n\n"; - // Convert Earth's barycentric from ecliptic -> ICRS frame, then subtract - auto sph_earth_bary = earth_bary.to_spherical(); - auto dir_earth_bary_icrs = sph_earth_bary.direction().to_frame(jd); - spherical::Position sph_earth_bary_icrs( - dir_earth_bary_icrs, sph_earth_bary.distance()); - auto earth_bary_icrs = sph_earth_bary_icrs.to_cartesian(); + // Convert Earth's barycentric position from EclipticMeanJ2000 -> ICRS directly + auto earth_bary_icrs = earth_bary.to_frame(jd); // Star geocentric ICRS (GCRS-equivalent for this demo) auto star_gcrs = cartesian::Position( @@ -212,12 +188,8 @@ int main() { auto recovered_helio_equ = cartesian::Position( temp.x() + earth_helio_equ.x(), temp.y() + earth_helio_equ.y(), temp.z() + earth_helio_equ.z()); - // Convert recovered heliocentric equ back to heliocentric ecliptic - auto sph_recovered_equ = recovered_helio_equ.to_spherical(); - auto dir_recovered_ecl = sph_recovered_equ.direction().to_frame(jd); - spherical::Position - sph_recovered_ecl(dir_recovered_ecl, sph_recovered_equ.distance()); - auto recovered = sph_recovered_ecl.to_cartesian(); + // Convert recovered heliocentric equ back to heliocentric ecliptic (direct to_frame) + auto recovered = recovered_helio_equ.to_frame(jd); std::cout << "After round-trip transformation:\n"; std::cout << " X = " << recovered.x() << "\n"; diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index f3c95ca..dc5aaec 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -9,11 +9,13 @@ #include "../centers.hpp" #include "../ffi_core.hpp" #include "../frames.hpp" +#include "../time.hpp" #include #include #include +#include // Forward-declare spherical Position to avoid circular include. namespace siderust { namespace spherical { template struct Position; } } @@ -42,6 +44,43 @@ template struct Direction { static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } + + /** + * @brief Transform this direction to a different reference frame. + * + * Only enabled when a `FrameRotationProvider` exists for the pair (F, Target). + * For time-independent (fixed-epoch) transforms, `jd` is still required but + * its value is ignored. + * + * @tparam Target Destination frame tag. + * @param jd Julian Date (TT) for time-dependent rotations. + */ + template + std::enable_if_t, Direction> + to_frame(const JulianDate &jd) const { + if constexpr (std::is_same_v) { + return Direction(x, y, z); + } else { + siderust_cartesian_pos_t out{}; + check_status( + siderust_cartesian_dir_transform_frame( + x, y, z, + frames::FrameTraits::ffi_id, + frames::FrameTraits::ffi_id, + jd.value(), &out), + "cartesian::Direction::to_frame"); + return Direction(out.x, out.y, out.z); + } + } + + /** + * @brief Shorthand: `.to(jd)` (calls `to_frame`). + */ + template + auto to(const JulianDate &jd) const + -> decltype(this->template to_frame(jd)) { + return to_frame(jd); + } }; /** @@ -111,6 +150,41 @@ template struct Position { static Position from_c(const siderust_cartesian_pos_t &c) { return Position(c.x, c.y, c.z); } + + /** + * @brief Transform this position to a different reference frame (same center). + * + * Only a pure rotation is applied; the reference center is unchanged. + * Only enabled when a `FrameRotationProvider` exists for the pair (F, Target). + * + * @tparam Target Destination frame tag. + * @param jd Julian Date (TT) for time-dependent rotations. + */ + template + std::enable_if_t, Position> + to_frame(const JulianDate &jd) const { + if constexpr (std::is_same_v) { + return *this; + } else { + siderust_cartesian_pos_t out{}; + check_status( + siderust_cartesian_pos_transform_frame( + to_c(), + frames::FrameTraits::ffi_id, + jd.value(), &out), + "cartesian::Position::to_frame"); + return Position(out.x, out.y, out.z); + } + } + + /** + * @brief Shorthand: `.to(jd)` (calls `to_frame`). + */ + template + auto to(const JulianDate &jd) const + -> decltype(this->template to_frame(jd)) { + return to_frame(jd); + } }; // ============================================================================ diff --git a/include/siderust/coordinates/pos_conversions.hpp b/include/siderust/coordinates/pos_conversions.hpp index 004072e..ea16b34 100644 --- a/include/siderust/coordinates/pos_conversions.hpp +++ b/include/siderust/coordinates/pos_conversions.hpp @@ -48,4 +48,19 @@ cartesian::Position spherical::Position::to_cartesian() const return cartesian::Position(U(cx), U(cy), U(cz)); } +// spherical::Position::to_frame implementation +// Performs the transformation via a cartesian round-trip: +// spherical(C,F) → cartesian(C,F) → cartesian(C,Target) → spherical(C,Target) +template +template +std::enable_if_t, + siderust::spherical::Position> +spherical::Position::to_frame(const JulianDate &jd) const { + if constexpr (std::is_same_v) { + return *this; + } else { + return to_cartesian().template to_frame(jd).to_spherical(); + } +} + } // namespace siderust diff --git a/include/siderust/coordinates/spherical.hpp b/include/siderust/coordinates/spherical.hpp index 787ea46..5426516 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -280,6 +280,29 @@ template struct Position { */ cartesian::Position to_cartesian() const; + /** + * @brief Transform this position to a different reference frame (same center). + * + * Internally converts to Cartesian, applies the frame rotation, then converts + * back to spherical. Only enabled when a `FrameRotationProvider` exists for + * the pair (F, Target). + * + * @tparam Target Destination frame tag. + * @param jd Julian Date (TT) for time-dependent rotations. + */ + template + std::enable_if_t, Position> + to_frame(const JulianDate &jd) const; + + /** + * @brief Shorthand: `.to(jd)` (calls `to_frame`). + */ + template + auto to(const JulianDate &jd) const + -> decltype(this->template to_frame(jd)) { + return to_frame(jd); + } + U distance_to(const Position &other) const { using std::sqrt; // Values in underlying unit (e.g. meters) diff --git a/siderust b/siderust index 37247a1..cf066e0 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 37247a1a2eda38fc70e1e2652f06041274dad24c +Subproject commit cf066e0707bb2c485036ba1f5fa3e39b18a0aacf diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 5566303..e00b7f1 100644 --- a/tests/test_coordinates.cpp +++ b/tests/test_coordinates.cpp @@ -198,3 +198,144 @@ TEST(TypedCoordinates, GeodeticToCartesianMember) { EXPECT_NEAR(ecef_m.x().value(), 6378137.0, 1.0); EXPECT_NEAR(ecef_km.x().value(), 6378.137, 1e-3); } + +// ============================================================================ +// cartesian::Direction::to_frame — new method +// ============================================================================ + +TEST(TypedCoordinates, CartesianDirToFrameRoundtrip) { + using namespace siderust::frames; + + // Unit vector along X in ICRS + cartesian::Direction dir_icrs(1.0, 0.0, 0.0); + auto jd = JulianDate::J2000(); + + auto dir_ecl = dir_icrs.to_frame(jd); + static_assert(std::is_same_v>); + + // Round-trip + auto dir_back = dir_ecl.to_frame(jd); + EXPECT_NEAR(dir_back.x, 1.0, 1e-8); + EXPECT_NEAR(dir_back.y, 0.0, 1e-8); + EXPECT_NEAR(dir_back.z, 0.0, 1e-8); +} + +TEST(TypedCoordinates, CartesianDirToFrameIdentity) { + using namespace siderust::frames; + + cartesian::Direction dir(0.6, 0.8, 0.0); + auto jd = JulianDate::J2000(); + + auto same = dir.to_frame(jd); + static_assert(std::is_same_v>); + EXPECT_DOUBLE_EQ(same.x, 0.6); + EXPECT_DOUBLE_EQ(same.y, 0.8); + EXPECT_DOUBLE_EQ(same.z, 0.0); +} + +TEST(TypedCoordinates, CartesianDirToFramePreservesLength) { + using namespace siderust::frames; + + // The rotation must preserve vector length + cartesian::Direction dir(0.6, 0.8, 0.0); + auto jd = JulianDate::J2000(); + + auto ecl = dir.to_frame(jd); + double len = std::sqrt(ecl.x * ecl.x + ecl.y * ecl.y + ecl.z * ecl.z); + EXPECT_NEAR(len, 1.0, 1e-10); +} + +// ============================================================================ +// cartesian::Position::to_frame — new method +// ============================================================================ + +TEST(TypedCoordinates, CartesianPosToFrameRoundtrip) { + using namespace siderust::frames; + using AU = qtty::AstronomicalUnit; + + cartesian::Position pos(1.0, 0.5, 0.2); + auto jd = JulianDate::J2000(); + + auto pos_icrs = pos.to_frame(jd); + static_assert(std::is_same_v>); + + // Round-trip back to EclipticMeanJ2000 + auto pos_back = pos_icrs.to_frame(jd); + EXPECT_NEAR(pos_back.x().value(), 1.0, 1e-8); + EXPECT_NEAR(pos_back.y().value(), 0.5, 1e-8); + EXPECT_NEAR(pos_back.z().value(), 0.2, 1e-8); +} + +TEST(TypedCoordinates, CartesianPosToFrameSameCenterPreserved) { + using namespace siderust::frames; + using AU = qtty::AstronomicalUnit; + + // Frame-only transform preserves center + cartesian::Position pos(1.0, 0.0, 0.0); + auto jd = JulianDate::J2000(); + + auto transformed = pos.to_frame(jd); + static_assert( + std::is_same_v>); + + // Distance must be preserved (rotation) + double r0 = pos.distance().value(); + double r1 = transformed.distance().value(); + EXPECT_NEAR(r0, r1, 1e-10); +} + +// ============================================================================ +// spherical::Position::to_frame — new method +// ============================================================================ + +TEST(TypedCoordinates, SphericalPosToFrameRoundtrip) { + using namespace siderust::frames; + using AU = qtty::AstronomicalUnit; + + spherical::Position sph( + qtty::Degree(30.0), qtty::Degree(10.0), AU(1.5)); + auto jd = JulianDate::J2000(); + + auto sph_icrs = sph.to_frame(jd); + static_assert( + std::is_same_v>); + + // Round-trip back + auto sph_back = sph_icrs.to_frame(jd); + EXPECT_NEAR(sph_back.lon().value(), 30.0, 1e-6); + EXPECT_NEAR(sph_back.lat().value(), 10.0, 1e-6); + EXPECT_NEAR(sph_back.distance().value(), 1.5, 1e-10); +} + +TEST(TypedCoordinates, SphericalPosToFramePreservesDistance) { + using namespace siderust::frames; + using AU = qtty::AstronomicalUnit; + + spherical::Position sph( + qtty::Degree(100.0), qtty::Degree(45.0), AU(2.3)); + auto jd = JulianDate::J2000(); + + auto ecl = sph.to_frame(jd); + // Distance must be unchanged by a frame rotation + EXPECT_NEAR(ecl.distance().value(), 2.3, 1e-10); +} + +TEST(TypedCoordinates, SphericalPosToFrameShorthand) { + using namespace siderust::frames; + using AU = qtty::AstronomicalUnit; + + spherical::Position sph( + qtty::Degree(50.0), qtty::Degree(20.0), AU(1.0)); + auto jd = JulianDate::J2000(); + + // .to(jd) shorthand + auto ecl = sph.to(jd); + static_assert( + std::is_same_v>); + EXPECT_NEAR(ecl.distance().value(), 1.0, 1e-10); +} From 179de199d6365397c5b771989c9c7e072faab49a Mon Sep 17 00:00:00 2001 From: VPRamon Date: Sat, 28 Feb 2026 17:05:52 +0100 Subject: [PATCH 14/19] Remove deprecated example files and enhance azimuth and position transformation functionalities - Deleted several example files including bodycentric_coordinates.cpp, coordinate_systems_example.cpp, coordinates_examples.cpp, demo.cpp, l2_satellite_mars_example.cpp, solar_system_bodies_example.cpp, and trackable_targets_example.cpp to streamline the codebase. - Added a new function `in_azimuth_range` to find periods when a star's azimuth is within a specified range. - Introduced new methods in the `Position` class for transforming positions to different reference centers and combined frame + center transformations. - Added a new type alias `HCRS` for Heliocentric positions in the equatorial frame. - Implemented a new function `mars_barycentric` to retrieve Mars's barycentric position using VSOP87. - Updated submodules for qtty-cpp, siderust, and tempoch-cpp to their latest commits. --- CMakeLists.txt | 305 ++----------- examples/01_basic_coordinates.cpp | 248 ++++++++--- examples/02_coordinate_transformations.cpp | 403 +++++++++--------- examples/03_all_frame_conversions.cpp | 156 ------- examples/03_all_frames_conversions.cpp | 89 ++++ examples/04_all_center_conversions.cpp | 366 ++++++++-------- examples/05_target_tracking.cpp | 180 ++++++++ examples/05_time_periods.cpp | 80 ---- examples/06_astronomical_night.cpp | 68 --- examples/06_night_events.cpp | 132 ++++++ examples/07_find_night_periods_365day.cpp | 56 --- examples/07_moon_properties.cpp | 117 +++++ examples/08_night_quality_scoring.cpp | 109 ----- examples/08_solar_system.cpp | 228 ++++++++++ examples/09_star_observability.cpp | 155 ++++--- examples/10_altitude_periods_trait.cpp | 140 ------ examples/10_time_periods.cpp | 158 +++++++ examples/11_compare_sun_moon_star.cpp | 108 ----- examples/11_serde_serialization.cpp | 192 +++++++++ examples/12_solar_system_example.cpp | 138 ------ examples/13_observer_coordinates.cpp | 148 ------- examples/14_bodycentric_coordinates.cpp | 144 ------- examples/15_targets_proper_motion.cpp | 128 ------ examples/16_jpl_precise_ephemeris.cpp | 87 ---- examples/17_serde_serialization.cpp | 121 ------ examples/18_kepler_orbit.cpp | 166 -------- examples/19_brent_root_finding.cpp | 169 -------- examples/20_moon_phase.cpp | 142 ------ examples/21_trackable_demo.cpp | 174 -------- examples/README.md | 30 -- examples/altitude_events_example.cpp | 84 ---- examples/azimuth_lunar_phase_example.cpp | 79 ---- examples/bodycentric_coordinates.cpp | 210 --------- examples/coordinate_systems_example.cpp | 47 -- examples/coordinates_examples.cpp | 56 --- examples/demo.cpp | 101 ----- examples/l2_satellite_mars_example.cpp | 68 --- examples/solar_system_bodies_example.cpp | 81 ---- examples/trackable_targets_example.cpp | 86 ---- include/siderust/azimuth.hpp | 28 ++ include/siderust/coordinates/cartesian.hpp | 81 ++++ .../types/cartesian/position/equatorial.hpp | 3 + include/siderust/ephemeris.hpp | 12 + qtty-cpp | 2 +- siderust | 2 +- tempoch-cpp | 2 +- 46 files changed, 1903 insertions(+), 3776 deletions(-) delete mode 100644 examples/03_all_frame_conversions.cpp create mode 100644 examples/03_all_frames_conversions.cpp create mode 100644 examples/05_target_tracking.cpp delete mode 100644 examples/05_time_periods.cpp delete mode 100644 examples/06_astronomical_night.cpp create mode 100644 examples/06_night_events.cpp delete mode 100644 examples/07_find_night_periods_365day.cpp create mode 100644 examples/07_moon_properties.cpp delete mode 100644 examples/08_night_quality_scoring.cpp create mode 100644 examples/08_solar_system.cpp delete mode 100644 examples/10_altitude_periods_trait.cpp create mode 100644 examples/10_time_periods.cpp delete mode 100644 examples/11_compare_sun_moon_star.cpp create mode 100644 examples/11_serde_serialization.cpp delete mode 100644 examples/12_solar_system_example.cpp delete mode 100644 examples/13_observer_coordinates.cpp delete mode 100644 examples/14_bodycentric_coordinates.cpp delete mode 100644 examples/15_targets_proper_motion.cpp delete mode 100644 examples/16_jpl_precise_ephemeris.cpp delete mode 100644 examples/17_serde_serialization.cpp delete mode 100644 examples/18_kepler_orbit.cpp delete mode 100644 examples/19_brent_root_finding.cpp delete mode 100644 examples/20_moon_phase.cpp delete mode 100644 examples/21_trackable_demo.cpp delete mode 100644 examples/README.md delete mode 100644 examples/altitude_events_example.cpp delete mode 100644 examples/azimuth_lunar_phase_example.cpp delete mode 100644 examples/bodycentric_coordinates.cpp delete mode 100644 examples/coordinate_systems_example.cpp delete mode 100644 examples/coordinates_examples.cpp delete mode 100644 examples/demo.cpp delete mode 100644 examples/l2_satellite_mars_example.cpp delete mode 100644 examples/solar_system_bodies_example.cpp delete mode 100644 examples/trackable_targets_example.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a37a2a..212e0ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,281 +142,46 @@ FetchContent_MakeAvailable(googletest) enable_testing() # --------------------------------------------------------------------------- -# Example +# Example helper: add an example only if its source file exists. # --------------------------------------------------------------------------- -add_executable(siderust_demo examples/demo.cpp) -target_link_libraries(siderust_demo PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(siderust_demo PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(coordinates_examples examples/coordinates_examples.cpp) -target_link_libraries(coordinates_examples PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(coordinates_examples PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(basic_coordinates_example examples/01_basic_coordinates.cpp) -target_link_libraries(basic_coordinates_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(basic_coordinates_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(coordinate_systems_example examples/coordinate_systems_example.cpp) -target_link_libraries(coordinate_systems_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(coordinate_systems_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(coordinate_transformations_example examples/02_coordinate_transformations.cpp) -target_link_libraries(coordinate_transformations_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(coordinate_transformations_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(solar_system_bodies_example examples/solar_system_bodies_example.cpp) -target_link_libraries(solar_system_bodies_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(solar_system_bodies_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(altitude_events_example examples/altitude_events_example.cpp) -target_link_libraries(altitude_events_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(altitude_events_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(trackable_targets_example examples/trackable_targets_example.cpp) -target_link_libraries(trackable_targets_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(trackable_targets_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(azimuth_lunar_phase_example examples/azimuth_lunar_phase_example.cpp) -target_link_libraries(azimuth_lunar_phase_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(azimuth_lunar_phase_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(l2_satellite_mars_example examples/l2_satellite_mars_example.cpp) -target_link_libraries(l2_satellite_mars_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(l2_satellite_mars_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(bodycentric_coordinates_example examples/bodycentric_coordinates.cpp) -target_link_libraries(bodycentric_coordinates_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(bodycentric_coordinates_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() +macro(siderust_add_example target_name source_file) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${source_file}") + add_executable(${target_name} ${source_file}) + target_link_libraries(${target_name} PRIVATE siderust_cpp) + if(DEFINED _siderust_rpath) + set_target_properties(${target_name} PROPERTIES + BUILD_RPATH ${_siderust_rpath} + INSTALL_RPATH ${_siderust_rpath} + ) + endif() + endif() +endmacro() # --------------------------------------------------------------------------- -# Numbered mirror examples (03–21, mirroring siderust Rust examples) +# Examples # --------------------------------------------------------------------------- +siderust_add_example(siderust_demo examples/demo.cpp) +siderust_add_example(coordinates_examples examples/coordinates_examples.cpp) +siderust_add_example(basic_coordinates_example examples/01_basic_coordinates.cpp) +siderust_add_example(coordinate_systems_example examples/coordinate_systems_example.cpp) +siderust_add_example(coordinate_transformations_example examples/02_coordinate_transformations.cpp) +siderust_add_example(solar_system_bodies_example examples/solar_system_bodies_example.cpp) +siderust_add_example(altitude_events_example examples/altitude_events_example.cpp) +siderust_add_example(trackable_targets_example examples/trackable_targets_example.cpp) +siderust_add_example(azimuth_lunar_phase_example examples/azimuth_lunar_phase_example.cpp) +siderust_add_example(l2_satellite_mars_example examples/l2_satellite_mars_example.cpp) +siderust_add_example(bodycentric_coordinates_example examples/bodycentric_coordinates.cpp) -add_executable(03_all_frame_conversions_example examples/03_all_frame_conversions.cpp) -target_link_libraries(03_all_frame_conversions_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(03_all_frame_conversions_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(04_all_center_conversions_example examples/04_all_center_conversions.cpp) -target_link_libraries(04_all_center_conversions_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(04_all_center_conversions_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(05_time_periods_example examples/05_time_periods.cpp) -target_link_libraries(05_time_periods_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(05_time_periods_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(06_astronomical_night_example examples/06_astronomical_night.cpp) -target_link_libraries(06_astronomical_night_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(06_astronomical_night_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(07_find_night_periods_365day_example examples/07_find_night_periods_365day.cpp) -target_link_libraries(07_find_night_periods_365day_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(07_find_night_periods_365day_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(08_night_quality_scoring_example examples/08_night_quality_scoring.cpp) -target_link_libraries(08_night_quality_scoring_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(08_night_quality_scoring_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(09_star_observability_example examples/09_star_observability.cpp) -target_link_libraries(09_star_observability_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(09_star_observability_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(10_altitude_periods_trait_example examples/10_altitude_periods_trait.cpp) -target_link_libraries(10_altitude_periods_trait_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(10_altitude_periods_trait_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(11_compare_sun_moon_star_example examples/11_compare_sun_moon_star.cpp) -target_link_libraries(11_compare_sun_moon_star_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(11_compare_sun_moon_star_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(12_solar_system_example examples/12_solar_system_example.cpp) -target_link_libraries(12_solar_system_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(12_solar_system_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(13_observer_coordinates_example examples/13_observer_coordinates.cpp) -target_link_libraries(13_observer_coordinates_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(13_observer_coordinates_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(14_bodycentric_coordinates_example examples/14_bodycentric_coordinates.cpp) -target_link_libraries(14_bodycentric_coordinates_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(14_bodycentric_coordinates_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(15_targets_proper_motion_example examples/15_targets_proper_motion.cpp) -target_link_libraries(15_targets_proper_motion_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(15_targets_proper_motion_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(16_jpl_precise_ephemeris_example examples/16_jpl_precise_ephemeris.cpp) -target_link_libraries(16_jpl_precise_ephemeris_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(16_jpl_precise_ephemeris_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(17_serde_serialization_example examples/17_serde_serialization.cpp) -target_link_libraries(17_serde_serialization_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(17_serde_serialization_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(18_kepler_orbit_example examples/18_kepler_orbit.cpp) -target_link_libraries(18_kepler_orbit_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(18_kepler_orbit_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(19_brent_root_finding_example examples/19_brent_root_finding.cpp) -target_link_libraries(19_brent_root_finding_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(19_brent_root_finding_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(20_moon_phase_example examples/20_moon_phase.cpp) -target_link_libraries(20_moon_phase_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(20_moon_phase_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() - -add_executable(21_trackable_demo_example examples/21_trackable_demo.cpp) -target_link_libraries(21_trackable_demo_example PRIVATE siderust_cpp) -if(DEFINED _siderust_rpath) - set_target_properties(21_trackable_demo_example PROPERTIES - BUILD_RPATH ${_siderust_rpath} - INSTALL_RPATH ${_siderust_rpath} - ) -endif() +# Numbered mirror examples (03–21, mirroring siderust Rust examples) +siderust_add_example(03_all_frame_conversions_example examples/03_all_frames_conversions.cpp) +siderust_add_example(04_all_center_conversions_example examples/04_all_center_conversions.cpp) +siderust_add_example(05_target_tracking_example examples/05_target_tracking.cpp) +siderust_add_example(06_night_events_example examples/06_night_events.cpp) +siderust_add_example(07_moon_properties_example examples/07_moon_properties.cpp) +siderust_add_example(08_solar_system_example examples/08_solar_system.cpp) +siderust_add_example(09_star_observability_example examples/09_star_observability.cpp) +siderust_add_example(10_time_periods_example examples/10_time_periods.cpp) +siderust_add_example(11_serde_serialization_example examples/11_serde_serialization.cpp) # --------------------------------------------------------------------------- # Tests diff --git a/examples/01_basic_coordinates.cpp b/examples/01_basic_coordinates.cpp index d39fa4b..f4dab36 100644 --- a/examples/01_basic_coordinates.cpp +++ b/examples/01_basic_coordinates.cpp @@ -1,62 +1,198 @@ -/** - * @file 01_basic_coordinates.cpp - * @brief C++ port of siderust/examples/01_basic_coordinates.rs - */ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon -#include -#include +/// Basic Coordinates Example +/// +/// Build with: cmake --build build-local --target basic_coordinates_example +/// Run with: ./build-local/basic_coordinates_example #include +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; +using namespace qtty::literals; + int main() { - using namespace siderust; - using namespace qtty::literals; - - std::cout << "=== 01_basic_coordinates (C++) ===\n"; - - // Time - const JulianDate jd = JulianDate::J2000(); - - // ========================================================================== - // Cartesian coordinates (heliocentric example) - // ========================================================================== - auto earth = ephemeris::earth_heliocentric(jd); - std::cout << "Earth heliocentric (EclipticMeanJ2000):\n"; - std::cout << " X = " << earth.x() << "\n"; - std::cout << " Y = " << earth.y() << "\n"; - std::cout << " Z = " << earth.z() << "\n"; - std::cout << " Distance = " << earth.distance() << "\n\n"; - - // ========================================================================== - // Spherical direction and frame conversions - // ========================================================================== - const Geodetic site(-17.8947_deg, 28.7606_deg, 2396.0_m); - - spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_true = vega_icrs.to_frame(jd); - auto vega_horiz = vega_icrs.to_horizontal(jd, site); - - std::cout << "Direction transforms:\n"; - std::cout << " ICRS RA/Dec: " << vega_icrs << "\n"; - std::cout << " Ecliptic lon/lat: " << vega_ecl << "\n"; - std::cout << " True-of-date RA/Dec: " << vega_true << "\n"; - std::cout << " Horizontal az/alt: " << vega_horiz << "\n\n"; - - // ========================================================================== - // Directions <-> Positions - // ========================================================================== - spherical::position::ICRS synthetic_star(210.0_deg, -12.0_deg, 4.2_au); - std::cout << "Typed positions:\n"; - std::cout << " Synthetic star distance: " << synthetic_star.distance() << "\n"; - - // ========================================================================== - // Type safety demonstration - // ========================================================================== - const auto ecef_m = site.to_cartesian(); - static_assert(std::is_same_v>); - std::cout << "Geodetic -> ECEF: " << site << " -> " << ecef_m << "\n"; - - std::cout << "=== Example Complete ===\n"; - return 0; + std::cout << std::fixed; + std::cout << "=== Siderust Basic Coordinates Example ===" << std::endl + << std::endl; + + // ========================================================================= + // 1. Cartesian Coordinates + // ========================================================================= + std::cout << "1. CARTESIAN COORDINATES" << std::endl; + std::cout << "------------------------" << std::endl; + + // Create a heliocentric ecliptic position (1 AU along X-axis) + cartesian::position::EclipticMeanJ2000 earth_position( + 1.0, 0.0, 0.0); + std::cout << "Earth position (Heliocentric EclipticMeanJ2000):" << std::endl; + std::cout << std::setprecision(6); + std::cout << " X = " << earth_position.x() << std::endl; + std::cout << " Y = " << earth_position.y() << std::endl; + std::cout << " Z = " << earth_position.z() << std::endl; + std::cout << " Distance from Sun = " << earth_position.distance() << std::endl << std::endl; + + // Create a geocentric equatorial position (Moon at ~384,400 km) + // No pre-defined alias for Geocentric EquatorialMeanJ2000, so use full type + cartesian::Position + moon_position(300000.0, 200000.0, 100000.0); + std::cout << "Moon position (Geocentric EquatorialMeanJ2000):" << std::endl; + std::cout << std::setprecision(1); + std::cout << " X = " << moon_position.x() << std::endl; + std::cout << " Y = " << moon_position.y() << std::endl; + std::cout << " Z = " << moon_position.z() << std::endl; + std::cout << " Distance from Earth = " << moon_position.distance() < cloud( + zenith.az(), zenith.alt(), cloud_distance); + std::cout << "Cloud at zenith, 5 km altitude (relative to geocenter):" << std::endl; + std::cout << " Distance = " << cloud.distance().value() << " km" << std::endl + << std::endl; + + // ========================================================================= + // 4. Cartesian <-> Spherical Conversion + // ========================================================================= + std::cout << "4. CARTESIAN <-> SPHERICAL CONVERSION" << std::endl; + std::cout << "-----------------------------------" << std::endl; + + // Start with cartesian + // Use Geocentric EquatorialMeanJ2000 (same as Rust example's implied center) + cartesian::Position + cart_pos(0.5, 0.5, 0.707); + std::cout << "Cartesian position:" << std::endl; + std::cout << std::setprecision(3); + std::cout << " X = " << cart_pos.x().value() << " AU" << std::endl; + std::cout << " Y = " << cart_pos.y().value() << " AU" << std::endl; + std::cout << " Z = " << cart_pos.z().value() << " AU" << std::endl + << std::endl; + + // Convert to spherical + auto sph_pos = cart_pos.to_spherical(); + std::cout << "Converted to Spherical:" << std::endl; + std::cout << std::setprecision(2); + std::cout << " RA = " << sph_pos.ra().value() << "°" << std::endl; + std::cout << " Dec = " << sph_pos.dec().value() << "°" << std::endl; + std::cout << std::setprecision(3); + std::cout << " Distance = " << sph_pos.distance().value() << " AU" << std::endl; + + // Convert back to cartesian + auto cart_pos_back = sph_pos.to_cartesian(); + std::cout << std::endl << "Converted back to Cartesian:" << std::endl; + std::cout << " X = " << cart_pos_back.x().value() << " AU" << std::endl; + std::cout << " Y = " << cart_pos_back.y().value() << " AU" << std::endl; + std::cout << " Z = " << cart_pos_back.z().value() << " AU" << std::endl + << std::endl; + + // ========================================================================= + // 5. Type Safety + // ========================================================================= + std::cout << "5. TYPE SAFETY" << std::endl; + std::cout << "--------------" << std::endl; + + // Different coordinate types are incompatible at compile time + cartesian::position::EclipticMeanJ2000 helio_pos( + 1.0, 0.0, 0.0); + cartesian::Position + geo_pos(0.0, 1.0, 0.0); + + std::cout << "Type-safe coordinates prevent mixing incompatible systems:" + << std::endl; + std::cout << " Heliocentric EclipticMeanJ2000: " << helio_pos << std::endl; + std::cout << " Geocentric EquatorialMeanJ2000: " << geo_pos << std::endl; + std::cout << std::endl + << " Cannot directly compute distance between them!" << std::endl; + std::cout << " (Must transform to same center/frame first)" << std::endl + << std::endl; + + // But operations within the same type are allowed + cartesian::position::EclipticMeanJ2000 pos1( + 1.0, 0.0, 0.0); + cartesian::position::EclipticMeanJ2000 pos2( + 1.5, 0.0, 0.0); + auto distance = pos1.distance_to(pos2); + std::cout << "Distance between two Heliocentric EclipticMeanJ2000 positions:" + << std::endl; + std::cout << " " << distance << std::endl + << std::endl; + + // ========================================================================= + // 6. Different Centers and Frames + // ========================================================================= + std::cout << "6. CENTERS AND FRAMES" << std::endl; + std::cout << "---------------------" << std::endl; + + std::cout << "Reference Centers:" << std::endl; + std::cout << " Barycentric: " << CenterTraits::name() << std::endl; + std::cout << " Heliocentric: " << CenterTraits::name() << std::endl; + std::cout << " Geocentric: " << CenterTraits::name() << std::endl; + std::cout << " Topocentric: " << CenterTraits::name() << std::endl; + std::cout << " Bodycentric: " << CenterTraits::name() << std::endl + << std::endl; + + std::cout << "Reference Frames:" << std::endl; + std::cout << " EclipticMeanJ2000: " << FrameTraits::name() + << std::endl; + std::cout << " EquatorialMeanJ2000: " << FrameTraits::name() + << std::endl; + std::cout << " Horizontal: " << FrameTraits::name() << std::endl; + std::cout << " ICRS: " << FrameTraits::name() << std::endl; + std::cout << " ECEF: " << FrameTraits::name() << std::endl + << std::endl; + + std::cout << "=== Example Complete ===" << std::endl; + return 0; } diff --git a/examples/02_coordinate_transformations.cpp b/examples/02_coordinate_transformations.cpp index 474e520..3c9c93f 100644 --- a/examples/02_coordinate_transformations.cpp +++ b/examples/02_coordinate_transformations.cpp @@ -1,209 +1,210 @@ -/** - * @file 02_coordinate_transformations.cpp - * @brief C++ port of siderust/examples/02_coordinate_transformations.rs - */ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// Coordinate Transformations Example +/// +/// Build & run: +/// cmake --build build-local --target 02_coordinate_transformations_example +/// ./build-local/02_coordinate_transformations_example + +#include #include #include #include -#include - using namespace siderust; -using namespace qtty::literals; - +using namespace siderust::frames; +using namespace siderust::centers; +using AU = qtty::AstronomicalUnit; int main() { - using AU = qtty::AstronomicalUnit; - - std::cout << "=== Coordinate Transformations (C++) ===\n\n"; - - const JulianDate jd = JulianDate::J2000(); - std::cout << "Reference time: J2000.0 (JD " << jd << ")\n\n"; - - // 1. Frame transformations (same center) - std::cout << "1. FRAME TRANSFORMATIONS\n"; - std::cout << "------------------------\n"; - - cartesian::position::EclipticMeanJ2000 pos_ecl(1.0, 0.0, 0.0); - std::cout << "Original (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << pos_ecl.x() << "\n"; - std::cout << " Y = " << pos_ecl.y() << "\n"; - std::cout << " Z = " << pos_ecl.z() << "\n\n"; - - // to Equatorial (same heliocentric center - auto pos_equ = pos_ecl.to_frame(jd); - std::cout << "Transformed to EquatorialMeanJ2000 frame:\n"; - std::cout << " X = " << pos_equ.x() << "\n"; - std::cout << " Y = " << pos_equ.y() << "\n"; - std::cout << " Z = " << pos_equ.z() << "\n\n"; - - // to ICRS — direct to_frame on cartesian::Position (no intermediate spherical) - auto pos_icrs = pos_equ.to_frame(jd); - std::cout << "Transformed to ICRS frame:\n"; - std::cout << " X = " << pos_icrs.x() << "\n"; - std::cout << " Y = " << pos_icrs.y() << "\n"; - std::cout << " Z = " << pos_icrs.z() << "\n\n"; - - // 2. Center transformations (same frame) - std::cout << "2. CENTER TRANSFORMATIONS\n"; - std::cout << "-------------------------\n"; - - auto earth_helio = ephemeris::earth_heliocentric(jd); - std::cout << "Earth (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << earth_helio.x() << "\n"; - std::cout << " Y = " << earth_helio.y() << "\n"; - std::cout << " Z = " << earth_helio.z() << "\n"; - std::cout << " Distance = " << earth_helio.distance() << "\n\n"; - - // Earth in geocentric (origin) — heliocentric minus itself -> zero - cartesian::Position - earth_geo(AU(0.0), AU(0.0), AU(0.0)); - std::cout << "Earth (Geocentric EclipticMeanJ2000) - at origin:\n"; - std::cout << " X = " << earth_geo.x() << "\n"; - std::cout << " Y = " << earth_geo.y() << "\n"; - std::cout << " Z = " << earth_geo.z() << "\n\n"; - std::cout << " Distance = " << earth_geo.distance() << " AU (should be ~0)\n\n"; - - auto mars_helio = ephemeris::mars_heliocentric(jd); - std::cout << "Mars (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << mars_helio.x() << "\n"; - std::cout << " Y = " << mars_helio.y() << "\n"; - std::cout << " Z = " << mars_helio.z() << "\n"; - std::cout << " Distance = " << mars_helio.distance() << "\n\n"; - - // Mars geocentric = Mars_helio - Earth_helio (component-wise) - cartesian::Position - mars_geo(mars_helio.x() - earth_helio.x(), mars_helio.y() - earth_helio.y(), - mars_helio.z() - earth_helio.z()); - std::cout << "Mars (Geocentric EclipticMeanJ2000) - as seen from Earth:\n"; - std::cout << " X = " << mars_geo.x() << "\n"; - std::cout << " Y = " << mars_geo.y() << "\n"; - std::cout << " Z = " << mars_geo.z() << "\n"; - std::cout << " Distance = " << mars_geo.distance() << "\n\n"; - - // 3. Combined transformations (center + frame) - std::cout << "3. COMBINED TRANSFORMATIONS\n"; - std::cout << "---------------------------\n"; - - std::cout << "Mars transformation chain:\n"; - std::cout << " Start: Heliocentric EclipticMeanJ2000\n"; - - // Step 1: transform Mars frame directly (cartesian::Position::to_frame) - auto mars_helio_equ = mars_helio.to_frame(jd); - std::cout << " Step 1: Transform frame → Heliocentric EquatorialMeanJ2000\n"; - - // Step 2: convert center heliocentric -> geocentric by subtracting Earth's pos in the same frame - auto earth_helio_equ = earth_helio.to_frame(jd); - - cartesian::Position - mars_geo_equ(mars_helio_equ.x() - earth_helio_equ.x(), - mars_helio_equ.y() - earth_helio_equ.y(), - mars_helio_equ.z() - earth_helio_equ.z()); - - std::cout << " Step 2: Transform center → Geocentric EquatorialMeanJ2000\n"; - std::cout << " Result:\n"; - std::cout << " X = " << mars_geo_equ.x() << " AU\n"; - std::cout << " Y = " << mars_geo_equ.y() << " AU\n"; - std::cout << " Z = " << mars_geo_equ.z() << " AU\n\n"; - - // Method 2: same in one chain — now trivial with direct to_frame - auto Mars_geo_equ_direct = cartesian::Position( - mars_helio_equ.x() - earth_helio_equ.x(), - mars_helio_equ.y() - earth_helio_equ.y(), - mars_helio_equ.z() - earth_helio_equ.z()); - - std::cout << " Or using direct chain (same result):\n"; - std::cout << " X = " << Mars_geo_equ_direct.x() << "\n"; - std::cout << " Y = " << Mars_geo_equ_direct.y() << "\n"; - std::cout << " Z = " << Mars_geo_equ_direct.z() << "\n\n"; - - // 4. Barycentric coordinates - std::cout << "4. BARYCENTRIC COORDINATES\n"; - std::cout << "--------------------------\n"; - - auto earth_bary = ephemeris::earth_barycentric(jd); - std::cout << "Earth (Barycentric EclipticMeanJ2000):\n"; - std::cout << " X = " << earth_bary.x() << "\n"; - std::cout << " Y = " << earth_bary.y() << "\n"; - std::cout << " Z = " << earth_bary.z() << "\n"; - std::cout << " Distance from SSB = " << earth_bary.distance() << "\n\n"; - - // Mars barycentric = sun_barycentric + mars_helio - auto sun_bary = ephemeris::sun_barycentric(jd); - cartesian::Position - mars_bary(sun_bary.x() + mars_helio.x(), sun_bary.y() + mars_helio.y(), - sun_bary.z() + mars_helio.z()); - - // Transform to geocentric (barycentric -> geocentric = target_bary - earth_bary) - auto mars_geo_from_bary = cartesian::Position( - mars_bary.x() - earth_bary.x(), mars_bary.y() - earth_bary.y(), - mars_bary.z() - earth_bary.z()); - - std::cout << "Mars (Geocentric, from Barycentric):\n"; - std::cout << " X = " << mars_geo_from_bary.x() << "\n"; - std::cout << " Y = " << mars_geo_from_bary.y() << "\n"; - std::cout << " Z = " << mars_geo_from_bary.z() << "\n"; - std::cout << " Distance = " << mars_geo_from_bary.distance() << "\n\n"; - - // 5. ICRS frame transformations (barycentric -> geocentric) - std::cout << "5. ICRS FRAME TRANSFORMATIONS\n"; - std::cout << "-----------------------------\n"; - - // Create a sample star in barycentric ICRS cartesian coords - cartesian::Position star_icrs(AU(100.0), AU(50.0), AU(1000.0)); - std::cout << "Star (Barycentric ICRS):\n"; - std::cout << " X = " << star_icrs.x() << "\n"; - std::cout << " Y = " << star_icrs.y() << "\n"; - std::cout << " Z = " << star_icrs.z() << "\n\n"; - - // Convert Earth's barycentric position from EclipticMeanJ2000 -> ICRS directly - auto earth_bary_icrs = earth_bary.to_frame(jd); - - // Star geocentric ICRS (GCRS-equivalent for this demo) - auto star_gcrs = cartesian::Position( - star_icrs.x() - earth_bary_icrs.x(), star_icrs.y() - earth_bary_icrs.y(), - star_icrs.z() - earth_bary_icrs.z()); - - std::cout << "Star (Geocentric ICRS/GCRS):\n"; - std::cout << " X = " << star_gcrs.x() << "\n"; - std::cout << " Y = " << star_gcrs.y() << "\n"; - std::cout << " Z = " << star_gcrs.z() << "\n\n"; - - // 6. Round-trip transformation - std::cout << "6. ROUND-TRIP TRANSFORMATION\n"; - std::cout << "----------------------------\n"; - - auto original = mars_helio; - std::cout << "Original Mars (Heliocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << original.x() << "\n"; - std::cout << " Y = " << original.y() << "\n"; - std::cout << " Z = " << original.z() << "\n\n"; - - // Helio Ecl -> Geo Equ -> back to Helio Ecl - // (we reuse previously computed earth_helio_equ) - auto temp = mars_geo_equ; // geocentric equ - - // Recover heliocentric equ by adding Earth's heliocentric equ - auto recovered_helio_equ = cartesian::Position( - temp.x() + earth_helio_equ.x(), temp.y() + earth_helio_equ.y(), temp.z() + earth_helio_equ.z()); - - // Convert recovered heliocentric equ back to heliocentric ecliptic (direct to_frame) - auto recovered = recovered_helio_equ.to_frame(jd); - - std::cout << "After round-trip transformation:\n"; - std::cout << " X = " << recovered.x() << "\n"; - std::cout << " Y = " << recovered.y() << "\n"; - std::cout << " Z = " << recovered.z() << "\n\n"; - - const double diff_x = std::abs(original.x().value() - recovered.x().value()); - const double diff_y = std::abs(original.y().value() - recovered.y().value()); - const double diff_z = std::abs(original.z().value() - recovered.z().value()); - std::cout << "Differences (should be tiny):\n"; - std::cout << " \u0394X = " << diff_x << "\n"; - std::cout << " \u0394Y = " << diff_y << "\n"; - std::cout << " \u0394Z = " << diff_z << "\n\n"; - - std::cout << "=== Example Complete ===\n"; - return 0; -} + std::cout << "=== Coordinate Transformations Example ===\n" << std::endl; + + auto jd = JulianDate::J2000(); + std::cout << std::fixed << std::setprecision(1); + std::cout << "Reference time: J2000.0 (JD " << jd.value() << ")\n" << std::endl; + + // ========================================================================= + // 1. Frame Transformations (same center) + // ========================================================================= + std::cout << "1. FRAME TRANSFORMATIONS" << std::endl; + std::cout << "------------------------" << std::endl; + + // Start with ecliptic coordinates (heliocentric) + cartesian::Position pos_ecliptic(1.0, 0.0, 0.0); + std::cout << std::setprecision(6); + std::cout << "Original (Heliocentric EclipticMeanJ2000):" << std::endl; + std::cout << " X = " << pos_ecliptic.x() << std::endl; + std::cout << " Y = " << pos_ecliptic.y() << std::endl; + std::cout << " Z = " << pos_ecliptic.z() << "\n" << std::endl; + + // Transform to equatorial frame (same heliocentric center) + auto pos_equatorial = pos_ecliptic.to_frame(jd); + std::cout << "Transformed to EquatorialMeanJ2000 frame:" << std::endl; + std::cout << " X = " << pos_equatorial.x() << std::endl; + std::cout << " Y = " << pos_equatorial.y() << std::endl; + std::cout << " Z = " << pos_equatorial.z() << "\n" << std::endl; + + // Transform to ICRS frame + auto pos_hcrs = pos_equatorial.to_frame(jd); + std::cout << "Transformed to ICRS frame:" << std::endl; + std::cout << " X = " << pos_hcrs.x() << std::endl; + std::cout << " Y = " << pos_hcrs.y() << std::endl; + std::cout << " Z = " << pos_hcrs.z() << "\n" << std::endl; + + // ========================================================================= + // 2. Center Transformations (same frame) + // ========================================================================= + std::cout << "2. CENTER TRANSFORMATIONS" << std::endl; + std::cout << "-------------------------" << std::endl; + + // Get Earth's position (heliocentric ecliptic) + auto earth_helio = ephemeris::earth_heliocentric(jd); + std::cout << "Earth (Heliocentric EclipticMeanJ2000):" << std::endl; + std::cout << " X = " << earth_helio.x() << std::endl; + std::cout << " Y = " << earth_helio.y() << std::endl; + std::cout << " Z = " << earth_helio.z() << std::endl; + std::cout << " Distance = " << earth_helio.distance() << "\n" << std::endl; + + // Transform to geocentric (Earth becomes origin) + auto earth_geo = earth_helio.to_center(jd); + std::cout << std::setprecision(10); + std::cout << "Earth (Geocentric EclipticMeanJ2000) - at origin:" << std::endl; + std::cout << " X = " << earth_geo.x() << std::endl; + std::cout << " Y = " << earth_geo.y() << std::endl; + std::cout << " Z = " << earth_geo.z() << std::endl; + std::cout << " Distance = " << earth_geo.distance() << " (should be ~0)\n" << std::endl; + + // Get Mars position (heliocentric) + auto mars_helio = ephemeris::mars_heliocentric(jd); + std::cout << std::setprecision(6); + std::cout << "Mars (Heliocentric EclipticMeanJ2000):" << std::endl; + std::cout << " X = " << mars_helio.x() << std::endl; + std::cout << " Y = " << mars_helio.y() << std::endl; + std::cout << " Z = " << mars_helio.z() << std::endl; + std::cout << " Distance = " << mars_helio.distance() << "\n" << std::endl; + + // Transform Mars to geocentric + auto mars_geo = mars_helio.to_center(jd); + std::cout << "Mars (Geocentric EclipticMeanJ2000) - as seen from Earth:" << std::endl; + std::cout << " X = " << mars_geo.x() << std::endl; + std::cout << " Y = " << mars_geo.y() << std::endl; + std::cout << " Z = " << mars_geo.z() << std::endl; + std::cout << " Distance = " << mars_geo.distance() << "\n" << std::endl; + + // ========================================================================= + // 3. Combined Transformations (center + frame) + // ========================================================================= + std::cout << "3. COMBINED TRANSFORMATIONS" << std::endl; + std::cout << "---------------------------" << std::endl; + + // Mars: Heliocentric EclipticMeanJ2000 → Geocentric EquatorialMeanJ2000 + std::cout << "Mars transformation chain:" << std::endl; + std::cout << " Start: Heliocentric EclipticMeanJ2000" << std::endl; + + // Method 1: Step by step + auto mars_helio_equ = mars_helio.to_frame(jd); + std::cout << " Step 1: Transform frame -> Heliocentric EquatorialMeanJ2000" << std::endl; + + auto mars_geo_equ = mars_helio_equ.to_center(jd); + std::cout << " Step 2: Transform center -> Geocentric EquatorialMeanJ2000" << std::endl; + std::cout << " Result:" << std::endl; + std::cout << " X = " << mars_geo_equ.x() << std::endl; + std::cout << " Y = " << mars_geo_equ.y() << std::endl; + std::cout << " Z = " << mars_geo_equ.z() << "\n" << std::endl; + + // Method 2: Using transform (does both) + auto mars_geo_equ_direct = mars_helio.transform(jd); + std::cout << " Or using .transform(jd) directly:" << std::endl; + std::cout << " X = " << mars_geo_equ_direct.x() << std::endl; + std::cout << " Y = " << mars_geo_equ_direct.y() << std::endl; + std::cout << " Z = " << mars_geo_equ_direct.z() << "\n" << std::endl; + + // ========================================================================= + // 4. Barycentric Coordinates + // ========================================================================= + std::cout << "4. BARYCENTRIC COORDINATES" << std::endl; + std::cout << "--------------------------" << std::endl; + + // Get Earth in barycentric coordinates + auto earth_bary = ephemeris::earth_barycentric(jd); + std::cout << "Earth (Barycentric EclipticMeanJ2000):" << std::endl; + std::cout << " X = " << earth_bary.x() << std::endl; + std::cout << " Y = " << earth_bary.y() << std::endl; + std::cout << " Z = " << earth_bary.z() << std::endl; + std::cout << " Distance from SSB = " << earth_bary.distance() << "\n" << std::endl; + + // Transform to geocentric + auto earth_geo_from_bary = earth_bary.to_center(jd); + std::cout << std::setprecision(10); + std::cout << "Earth (Geocentric, from Barycentric):" << std::endl; + std::cout << " Distance = " << earth_geo_from_bary.distance() + << " (should be ~0)\n" << std::endl; + + // Transform Mars from barycentric to geocentric + auto mars_bary = ephemeris::mars_barycentric(jd); + auto mars_geo_from_bary = mars_bary.to_center(jd); + std::cout << std::setprecision(6); + std::cout << "Mars (Geocentric, from Barycentric):" << std::endl; + std::cout << " X = " << mars_geo_from_bary.x() << std::endl; + std::cout << " Y = " << mars_geo_from_bary.y() << std::endl; + std::cout << " Z = " << mars_geo_from_bary.z() << std::endl; + std::cout << " Distance = " << mars_geo_from_bary.distance() << "\n" << std::endl; + + // ========================================================================= + // 5. ICRS Frame Transformations + // ========================================================================= + std::cout << "5. ICRS FRAME TRANSFORMATIONS" << std::endl; + std::cout << "-----------------------------" << std::endl; + + // Barycentric ICRS (standard for catalogs) + cartesian::position::ICRS star_icrs(100.0, 50.0, 1000.0); + std::cout << std::setprecision(3); + std::cout << "Star (Barycentric ICRS):" << std::endl; + std::cout << " X = " << star_icrs.x() << std::endl; + std::cout << " Y = " << star_icrs.y() << std::endl; + std::cout << " Z = " << star_icrs.z() << "\n" << std::endl; + + // Transform to Geocentric ICRS (GCRS) + auto star_gcrs = star_icrs.to_center(jd); + std::cout << "Star (Geocentric ICRS/GCRS):" << std::endl; + std::cout << " X = " << star_gcrs.x() << std::endl; + std::cout << " Y = " << star_gcrs.y() << std::endl; + std::cout << " Z = " << star_gcrs.z() << std::endl; + std::cout << " (Difference is tiny for distant stars)\n" << std::endl; + + // ========================================================================= + // 6. Round-trip Transformation + // ========================================================================= + std::cout << "6. ROUND-TRIP TRANSFORMATION" << std::endl; + std::cout << "----------------------------" << std::endl; + + std::cout << std::setprecision(10); + std::cout << "Original Mars (Heliocentric EclipticMeanJ2000):" << std::endl; + std::cout << " X = " << mars_helio.x() << std::endl; + std::cout << " Y = " << mars_helio.y() << std::endl; + std::cout << " Z = " << mars_helio.z() << "\n" << std::endl; + + // Transform: Helio Ecl → Geo EquatorialMeanJ2000 → Helio Ecl + auto temp = mars_helio.transform(jd); + auto recovered = temp.transform(jd); + + std::cout << "After round-trip transformation:" << std::endl; + std::cout << " X = " << recovered.x() << std::endl; + std::cout << " Y = " << recovered.y() << std::endl; + std::cout << " Z = " << recovered.z() << "\n" << std::endl; + + double diff_x = std::abs(mars_helio.x().value() - recovered.x().value()); + double diff_y = std::abs(mars_helio.y().value() - recovered.y().value()); + double diff_z = std::abs(mars_helio.z().value() - recovered.z().value()); + std::cout << std::scientific << std::setprecision(3); + std::cout << "Differences (should be tiny):" << std::endl; + std::cout << " dX = " << diff_x << std::endl; + std::cout << " dY = " << diff_y << std::endl; + std::cout << " dZ = " << diff_z << "\n" << std::endl; + + std::cout << "=== Example Complete ===" << std::endl; + + return 0; +} \ No newline at end of file diff --git a/examples/03_all_frame_conversions.cpp b/examples/03_all_frame_conversions.cpp deleted file mode 100644 index d5f5f3f..0000000 --- a/examples/03_all_frame_conversions.cpp +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @file 03_all_frame_conversions.cpp - * @brief C++ port of siderust/examples/23_all_frame_conversions.rs - * - * Demonstrates all supported direction frame-rotation pairs using the - * `direction.to_frame(jd)` API. - * - * Supported frame pairs (via ICRS hub): - * ICRS <-> EclipticMeanJ2000 - * ICRS <-> EquatorialMeanJ2000 - * ICRS <-> EquatorialMeanOfDate - * ICRS <-> EquatorialTrueOfDate - * EclipticMeanJ2000 <-> EquatorialMeanJ2000/OfDate/TrueOfDate - * EquatorialMean* <-> EquatorialTrue* - * Any -> Horizontal via `.to_horizontal(jd, observer)` - * - * Run with: - * cmake --build build --target 03_all_frame_conversions_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace siderust::frames; -using namespace qtty::literals; - -int main() { - std::cout << "=== All Frame Conversions ===\n\n"; - - const JulianDate jd = JulianDate::J2000(); - std::cout << "Epoch: J2000.0 (JD " << std::fixed << std::setprecision(1) - << jd.value() << ")\n\n"; - - // Vega in ICRS (RA=279.2348 deg, Dec=38.7837 deg) - const spherical::direction::ICRS vega_icrs(279.2348_deg, 38.7837_deg); - std::cout << "Reference: Vega\n"; - std::cout << " ICRS RA=" << std::fixed << std::setprecision(4) - << vega_icrs.ra().value() - << " Dec=" << vega_icrs.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // ICRS -> EclipticMeanJ2000 - // ------------------------------------------------------------------------- - std::cout << "--- ICRS -> EclipticMeanJ2000 ---\n"; - auto d_ecl = vega_icrs.to_frame(jd); - std::cout << " lon=" << std::setprecision(4) << d_ecl.longitude().value() - << " lat=" << d_ecl.latitude().value() << " deg\n"; - auto rt_ecl = d_ecl.to_frame(jd); - std::cout << " round-trip RA err: " << std::scientific - << std::abs(rt_ecl.ra().value() - vega_icrs.ra().value()) << "\n\n"; - - // ------------------------------------------------------------------------- - // ICRS -> EquatorialMeanJ2000 - // ------------------------------------------------------------------------- - std::cout << "--- ICRS -> EquatorialMeanJ2000 ---\n"; - auto d_eqj = vega_icrs.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqj.ra().value() - << " Dec=" << d_eqj.dec().value() << " deg\n"; - auto rt_eqj = d_eqj.to_frame(jd); - std::cout << " round-trip RA err: " << std::scientific - << std::abs(rt_eqj.ra().value() - vega_icrs.ra().value()) << "\n\n"; - - // ------------------------------------------------------------------------- - // ICRS -> EquatorialMeanOfDate - // ------------------------------------------------------------------------- - std::cout << "--- ICRS -> EquatorialMeanOfDate ---\n"; - auto d_eqmod = vega_icrs.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqmod.ra().value() - << " Dec=" << d_eqmod.dec().value() << " deg\n"; - auto rt_eqmod = d_eqmod.to_frame(jd); - std::cout << " round-trip RA err: " << std::scientific - << std::abs(rt_eqmod.ra().value() - vega_icrs.ra().value()) << "\n\n"; - - // ------------------------------------------------------------------------- - // ICRS -> EquatorialTrueOfDate - // ------------------------------------------------------------------------- - std::cout << "--- ICRS -> EquatorialTrueOfDate ---\n"; - auto d_eqtod = vega_icrs.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqtod.ra().value() - << " Dec=" << d_eqtod.dec().value() << " deg\n"; - auto rt_eqtod = d_eqtod.to_frame(jd); - std::cout << " round-trip RA err: " << std::scientific - << std::abs(rt_eqtod.ra().value() - vega_icrs.ra().value()) << "\n\n"; - - // ------------------------------------------------------------------------- - // EclipticMeanJ2000 -> EquatorialMeanJ2000 - // ------------------------------------------------------------------------- - std::cout << "--- EclipticMeanJ2000 -> EquatorialMeanJ2000 ---\n"; - auto d_ecl_to_eqj = d_ecl.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_ecl_to_eqj.ra().value() - << " Dec=" << d_ecl_to_eqj.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // EclipticMeanJ2000 -> EquatorialMeanOfDate - // ------------------------------------------------------------------------- - std::cout << "--- EclipticMeanJ2000 -> EquatorialMeanOfDate ---\n"; - auto d_ecl_to_mod = d_ecl.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_ecl_to_mod.ra().value() - << " Dec=" << d_ecl_to_mod.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // EclipticMeanJ2000 -> EquatorialTrueOfDate - // ------------------------------------------------------------------------- - std::cout << "--- EclipticMeanJ2000 -> EquatorialTrueOfDate ---\n"; - auto d_ecl_to_tod = d_ecl.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_ecl_to_tod.ra().value() - << " Dec=" << d_ecl_to_tod.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // EquatorialMeanJ2000 -> EquatorialMeanOfDate - // ------------------------------------------------------------------------- - std::cout << "--- EquatorialMeanJ2000 -> EquatorialMeanOfDate ---\n"; - auto d_eqj_to_mod = d_eqj.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqj_to_mod.ra().value() - << " Dec=" << d_eqj_to_mod.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // EquatorialMeanJ2000 -> EquatorialTrueOfDate - // ------------------------------------------------------------------------- - std::cout << "--- EquatorialMeanJ2000 -> EquatorialTrueOfDate ---\n"; - auto d_eqj_to_tod = d_eqj.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_eqj_to_tod.ra().value() - << " Dec=" << d_eqj_to_tod.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // EquatorialMeanOfDate -> EquatorialTrueOfDate - // ------------------------------------------------------------------------- - std::cout << "--- EquatorialMeanOfDate -> EquatorialTrueOfDate ---\n"; - auto d_mod_to_tod = d_eqmod.to_frame(jd); - std::cout << " RA=" << std::fixed << std::setprecision(4) << d_mod_to_tod.ra().value() - << " Dec=" << d_mod_to_tod.dec().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // ICRS -> Horizontal (alt-az) — requires observer location - // ------------------------------------------------------------------------- - std::cout << "--- ICRS -> Horizontal (Roque de los Muchachos) ---\n"; - const Geodetic obs(-17.8947_deg, 28.7606_deg, 2396.0_m); - auto d_horiz = vega_icrs.to_horizontal(jd, obs); - std::cout << " Az=" << std::fixed << std::setprecision(4) << d_horiz.az().value() - << " Alt=" << d_horiz.altitude().value() << " deg\n\n"; - - // ------------------------------------------------------------------------- - // EclipticMeanJ2000 -> Horizontal - // ------------------------------------------------------------------------- - std::cout << "--- EclipticMeanJ2000 -> Horizontal ---\n"; - auto d_ecl_horiz = d_ecl.to_horizontal(jd, obs); - std::cout << " Az=" << std::fixed << std::setprecision(4) << d_ecl_horiz.az().value() - << " Alt=" << d_ecl_horiz.altitude().value() << " deg\n\n"; - - std::cout << "=== Done ===\n"; - return 0; -} diff --git a/examples/03_all_frames_conversions.cpp b/examples/03_all_frames_conversions.cpp new file mode 100644 index 0000000..6240d9a --- /dev/null +++ b/examples/03_all_frames_conversions.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// Example: all currently supported frame conversions. +/// +/// Demonstrates every direct frame-rotation pair, plus identity rotations. +/// +/// Build & run: cmake --build build-local --target 03_all_frame_conversions_example +/// ./build-local/03_all_frame_conversions_example + +#include + +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; + +using C = Barycentric; +using U = qtty::AstronomicalUnit; + +/// Show a frame conversion from F1→F2, round-trip F1→F2→F1, and the error. +template +void show_frame_conversion(const JulianDate &jd, + const cartesian::Position &src) { + auto out = src.template to_frame(jd); + auto back = out.template to_frame(jd); + auto err = (src - back).magnitude(); + + std::cout << std::left << std::setw(24) << FrameTraits::name() + << " -> " << std::setw(24) << FrameTraits::name() + << " out=(" << std::showpos << std::setprecision(9) << out + << std::noshowpos << ") roundtrip=" << std::scientific + << std::setprecision(3) << err.value() << std::fixed + << std::endl; +} + +int main() { + JulianDate jd(2460000.5); + std::cout << std::fixed; + std::cout << "Frame conversion demo at JD(TT) = " << std::setprecision(1) + << jd.value() << std::endl; + + cartesian::Position p_icrs(0.30, -0.70, 0.64); + auto p_icrf = p_icrs.to_frame(jd); + auto p_ecl = p_icrs.to_frame(jd); + auto p_eq_j2000 = p_icrs.to_frame(jd); + auto p_eq_mod = p_icrs.to_frame(jd); + auto p_eq_tod = p_icrs.to_frame(jd); + + // Identity conversions + show_frame_conversion(jd, p_icrs); + show_frame_conversion(jd, p_icrf); + show_frame_conversion(jd, p_ecl); + show_frame_conversion(jd, p_eq_j2000); + show_frame_conversion(jd, p_eq_mod); + show_frame_conversion(jd, p_eq_tod); + + // All direct non-identity provider pairs + show_frame_conversion(jd, p_icrs); + show_frame_conversion(jd, p_ecl); + show_frame_conversion(jd, p_icrs); + show_frame_conversion(jd, p_eq_j2000); + show_frame_conversion(jd, p_eq_j2000); + show_frame_conversion(jd, p_ecl); + show_frame_conversion(jd, p_eq_j2000); + show_frame_conversion(jd, p_eq_mod); + show_frame_conversion(jd, p_eq_mod); + show_frame_conversion(jd, p_eq_tod); + show_frame_conversion(jd, p_eq_j2000); + show_frame_conversion(jd, p_eq_tod); + show_frame_conversion(jd, p_icrs); + show_frame_conversion(jd, p_eq_mod); + show_frame_conversion(jd, p_icrs); + show_frame_conversion(jd, p_eq_tod); + show_frame_conversion(jd, p_icrf); + show_frame_conversion(jd, p_icrs); + show_frame_conversion(jd, p_icrf); + show_frame_conversion(jd, p_eq_j2000); + show_frame_conversion(jd, p_icrf); + show_frame_conversion(jd, p_ecl); + show_frame_conversion(jd, p_icrf); + show_frame_conversion(jd, p_eq_mod); + show_frame_conversion(jd, p_icrf); + show_frame_conversion(jd, p_eq_tod); + + return 0; +} diff --git a/examples/04_all_center_conversions.cpp b/examples/04_all_center_conversions.cpp index 956ac17..64ee479 100644 --- a/examples/04_all_center_conversions.cpp +++ b/examples/04_all_center_conversions.cpp @@ -1,197 +1,181 @@ -/** - * @file 04_all_center_conversions.cpp - * @brief C++ port of siderust/examples/22_all_center_conversions.rs - * - * Demonstrates all supported center-shift pairs with round-trip error metric: - * - Barycentric <-> Heliocentric - * - Barycentric <-> Geocentric - * - Heliocentric <-> Geocentric - * - * NOTE: The Rust library exposes an automatic `to_center()` trait API backed - * by a `CenterShiftProvider` pattern. In C++ this is not yet fully bound; - * center shifts are performed here manually using VSOP87 ephemeris offsets, - * which is the same underlying calculation. - * - * TODO: When `CenterShiftProvider` is bound in C++, replace the manual - * arithmetic below with the typed transform calls. - * - * Run with: cmake --build build --target all_center_conversions_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -using AU = qtty::AstronomicalUnit; - -// --------------------------------------------------------------------------- -// Compute Euclidean error between two cartesian positions (same type erased) -// --------------------------------------------------------------------------- -static double cart_error(double ax, double ay, double az, - double bx, double by, double bz) { - double dx = ax - bx, dy = ay - by, dz = az - bz; - return std::sqrt(dx * dx + dy * dy + dz * dz); +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Example: all currently supported center conversions. +//! +//! This demonstrates all center-shift pairs implemented in `providers.rs`: +//! - Barycentric <-> Heliocentric +//! - Barycentric <-> Geocentric +//! - Heliocentric <-> Geocentric +//! - Identity shifts for each center +//! +//! It also demonstrates: +//! - **Bodycentric** conversions: from Barycentric, Heliocentric, and Geocentric into a +//! body-centric frame (Mars-centric and ISS-centric) with round-trip verification. +//! - **Topocentric** conversions: observer-on-Earth parallax correction applied to +//! positions originally expressed in each of the three standard centers. + +use qtty::*; +use siderust::astro::orbit::Orbit; +use siderust::coordinates::cartesian::Position; +use siderust::coordinates::centers::{ + Barycentric, Bodycentric, BodycentricParams, Geocentric, Geodetic, Heliocentric, + ReferenceCenter, Topocentric, +}; +use siderust::coordinates::frames::{EclipticMeanJ2000, ECEF}; +use siderust::coordinates::transform::{CenterShiftProvider, TransformCenter}; +use siderust::time::JulianDate; + +type F = EclipticMeanJ2000; +type U = AstronomicalUnit; + +// ─── Standard center shifts ────────────────────────────────────────────────── + +fn show_center_conversion(jd: &JulianDate, src: &Position) +where + C1: ReferenceCenter, + C2: ReferenceCenter, + (): CenterShiftProvider, + (): CenterShiftProvider, +{ + let out: Position = src.to_center(*jd); + let back: Position = out.to_center(*jd); + let err = (*src - back).magnitude(); + + println!( + "{:<12} -> {:<12} out=({:+.9}) roundtrip={:.3e}", + C1::center_name(), + C2::center_name(), + out, + err + ); } -int main() { - const JulianDate jd = JulianDate(2'460'000.5); - std::cout << "Center conversion demo at JD(TT) = " << std::fixed - << std::setprecision(1) << jd.value() << "\n\n"; - - // Fetch SSB offsets via ephemeris (same data the Rust provider uses) - auto sun_bary = ephemeris::sun_barycentric(jd); // Sun relative to SSB - auto earth_bary = ephemeris::earth_barycentric(jd); // Earth relative to SSB - - // A sample physical point defined in barycentric coords - cartesian::Position - p_bary(AU(0.40), AU(-0.10), AU(1.20)); - - // ------------------------------------------------------------------------- - // Derive heliocentric and geocentric equivalents - // helio = bary - sun_bary - // geo = bary - earth_bary - // ------------------------------------------------------------------------- - cartesian::Position - p_helio(p_bary.x() - sun_bary.x(), - p_bary.y() - sun_bary.y(), - p_bary.z() - sun_bary.z()); - - cartesian::Position - p_geo(p_bary.x() - earth_bary.x(), - p_bary.y() - earth_bary.y(), - p_bary.z() - earth_bary.z()); - - const auto fmt = [](double val) { - return val; // just pass through for cout - }; - - auto print_row = [&](const char *from, const char *to, - double ox, double oy, double oz, - double err) { - std::cout << std::left << std::setw(14) << from - << " -> " << std::setw(14) << to - << " out=(" << std::fixed << std::setprecision(9) - << std::setw(13) << ox << ", " - << std::setw(13) << oy << ", " - << std::setw(13) << oz << ") " - << "roundtrip=" << std::scientific << std::setprecision(3) - << err << "\n"; - }; - - // ------------------------------------------------------------------------- +// ─── Bodycentric ───────────────────────────────────────────────────────────── + +/// Transform `src` (in center `C`) into body-centric coordinates and back. +/// +/// Round-trip: C → Bodycentric → Geocentric → C. +fn show_bodycentric_conversion( + label: &str, + jd: &JulianDate, + src: &Position, + params: BodycentricParams, +) where + C: ReferenceCenter, + Position: TransformCenter, + Position: TransformCenter, + (): CenterShiftProvider, +{ + let bary: Position = src.to_center((params, *jd)); + let recovered_geo: Position = bary.to_center(*jd); + let recovered: Position = recovered_geo.to_center(*jd); + let err = (*src - recovered).magnitude(); + + println!( + "{:<12} -> {:<12} dist={:.6} roundtrip={:.3e}", + label, + Bodycentric::center_name(), + bary.distance(), + err + ); +} + +// ─── Topocentric ───────────────────────────────────────────────────────────── + +/// Transform a geocentric position to topocentric and back. +/// +/// The `label` argument names the original center (before it was shifted to +/// geocentric), so the output shows the full chain (e.g. Barycentric → Geo → +/// Topocentric). +fn show_topocentric_conversion( + label: &str, + jd: &JulianDate, + geo: &Position, + site: Geodetic, +) { + let topo: Position = geo.to_center((site, *jd)); + let recovered: Position = topo.to_center(*jd); + let err = (*geo - recovered).magnitude(); + + println!( + "{:<12} -> {:<12} out=({:+.6}) roundtrip={:.3e}", + label, "Topocentric", topo, err + ); +} + +// ─── main ───────────────────────────────────────────────────────────────────── + +fn main() { + let jd = JulianDate::new(2_460_000.5); + println!("Center conversion demo at JD(TT) = {:.1}\n", jd); + + let p_bary = Position::::new(0.40, -0.10, 1.20); + let p_helio: Position = p_bary.to_center(jd); + let p_geo: Position = p_bary.to_center(jd); + + // ── Standard center shifts via CenterShiftProvider ──────────────────────── + println!("── Standard center shifts ─────────────────────────────────────────────"); + // Barycentric source - // ------------------------------------------------------------------------- - - // Bary -> Bary (identity) - print_row("Barycentric", "Barycentric", - p_bary.x().value(), p_bary.y().value(), p_bary.z().value(), 0.0); - - // Bary -> Helio - { - // back: helio -> bary = helio + sun_bary - cartesian::Position - back(p_helio.x() + sun_bary.x(), - p_helio.y() + sun_bary.y(), - p_helio.z() + sun_bary.z()); - double err = cart_error(p_bary.x().value(), p_bary.y().value(), p_bary.z().value(), - back.x().value(), back.y().value(), back.z().value()); - print_row("Barycentric", "Heliocentric", - p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), err); - } - - // Bary -> Geo - { - cartesian::Position - back(p_geo.x() + earth_bary.x(), - p_geo.y() + earth_bary.y(), - p_geo.z() + earth_bary.z()); - double err = cart_error(p_bary.x().value(), p_bary.y().value(), p_bary.z().value(), - back.x().value(), back.y().value(), back.z().value()); - print_row("Barycentric", "Geocentric", - p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), err); - } - - // ------------------------------------------------------------------------- + show_center_conversion::(&jd, &p_bary); + show_center_conversion::(&jd, &p_bary); + show_center_conversion::(&jd, &p_bary); + // Heliocentric source - // ------------------------------------------------------------------------- - print_row("Heliocentric", "Heliocentric", - p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), 0.0); - - // Helio -> Bary - { - cartesian::Position - out(p_helio.x() + sun_bary.x(), - p_helio.y() + sun_bary.y(), - p_helio.z() + sun_bary.z()); - cartesian::Position - back(out.x() - sun_bary.x(), - out.y() - sun_bary.y(), - out.z() - sun_bary.z()); - double err = cart_error(p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), - back.x().value(), back.y().value(), back.z().value()); - print_row("Heliocentric", "Barycentric", - out.x().value(), out.y().value(), out.z().value(), err); - } - - // Helio -> Geo - { - cartesian::Position - out(p_helio.x() - (earth_bary.x() - sun_bary.x()), - p_helio.y() - (earth_bary.y() - sun_bary.y()), - p_helio.z() - (earth_bary.z() - sun_bary.z())); - cartesian::Position - back(out.x() + (earth_bary.x() - sun_bary.x()), - out.y() + (earth_bary.y() - sun_bary.y()), - out.z() + (earth_bary.z() - sun_bary.z())); - double err = cart_error(p_helio.x().value(), p_helio.y().value(), p_helio.z().value(), - back.x().value(), back.y().value(), back.z().value()); - print_row("Heliocentric", "Geocentric", - out.x().value(), out.y().value(), out.z().value(), err); - } - - // ------------------------------------------------------------------------- + show_center_conversion::(&jd, &p_helio); + show_center_conversion::(&jd, &p_helio); + show_center_conversion::(&jd, &p_helio); + // Geocentric source - // ------------------------------------------------------------------------- - print_row("Geocentric", "Geocentric", - p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), 0.0); - - // Geo -> Bary - { - cartesian::Position - out(p_geo.x() + earth_bary.x(), - p_geo.y() + earth_bary.y(), - p_geo.z() + earth_bary.z()); - cartesian::Position - back(out.x() - earth_bary.x(), - out.y() - earth_bary.y(), - out.z() - earth_bary.z()); - double err = cart_error(p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), - back.x().value(), back.y().value(), back.z().value()); - print_row("Geocentric", "Barycentric", - out.x().value(), out.y().value(), out.z().value(), err); - } - - // Geo -> Helio - { - cartesian::Position - out(p_geo.x() + (earth_bary.x() - sun_bary.x()), - p_geo.y() + (earth_bary.y() - sun_bary.y()), - p_geo.z() + (earth_bary.z() - sun_bary.z())); - cartesian::Position - back(out.x() - (earth_bary.x() - sun_bary.x()), - out.y() - (earth_bary.y() - sun_bary.y()), - out.z() - (earth_bary.z() - sun_bary.z())); - double err = cart_error(p_geo.x().value(), p_geo.y().value(), p_geo.z().value(), - back.x().value(), back.y().value(), back.z().value()); - print_row("Geocentric", "Heliocentric", - out.x().value(), out.y().value(), out.z().value(), err); - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; + show_center_conversion::(&jd, &p_geo); + show_center_conversion::(&jd, &p_geo); + show_center_conversion::(&jd, &p_geo); + + // ── Bodycentric: Mars-like orbit (heliocentric reference) ────────────────── + println!("\n── Bodycentric – Mars-like orbit (heliocentric ref) ───────────────────"); + let mars_orbit = Orbit::new( + 1.524 * AU, + 0.0934, + Degrees::new(1.85), + Degrees::new(49.56), + Degrees::new(286.5), + Degrees::new(19.41), + jd, + ); + let mars_params = BodycentricParams::heliocentric(mars_orbit); + + show_bodycentric_conversion("Heliocentric", &jd, &p_helio, mars_params); + show_bodycentric_conversion("Barycentric", &jd, &p_bary, mars_params); + show_bodycentric_conversion("Geocentric", &jd, &p_geo, mars_params); + + // ── Bodycentric: ISS-like orbit (geocentric reference) ──────────────────── + println!("\n── Bodycentric – ISS-like orbit (geocentric ref) ──────────────────────"); + let iss_orbit = Orbit::new( + 0.0000426 * AU, // ~6 378 km in AU + 0.001, + Degrees::new(51.6), + Degrees::new(0.0), + Degrees::new(0.0), + Degrees::new(0.0), + jd, + ); + let iss_params = BodycentricParams::geocentric(iss_orbit); + + show_bodycentric_conversion("Heliocentric", &jd, &p_helio, iss_params); + show_bodycentric_conversion("Barycentric", &jd, &p_bary, iss_params); + show_bodycentric_conversion("Geocentric", &jd, &p_geo, iss_params); + + // ── Topocentric: observer at Barcelona ──────────────────────────────────── + println!("\n── Topocentric – Barcelona (lon=2.17°, lat=41.39°, h=12 m) ───────────"); + // Topocentric is geocentric-relative: shift Bary/Helio to Geocentric first, + // then apply the parallax correction. + let site = Geodetic::::new(Degrees::new(2.17), Degrees::new(41.39), 12.0 * M); + + let p_geo_from_bary: Position = p_bary.to_center(jd); + let p_geo_from_helio: Position = p_helio.to_center(jd); + + show_topocentric_conversion("Barycentric", &jd, &p_geo_from_bary, site); + show_topocentric_conversion("Heliocentric", &jd, &p_geo_from_helio, site); + show_topocentric_conversion("Geocentric", &jd, &p_geo, site); } diff --git a/examples/05_target_tracking.cpp b/examples/05_target_tracking.cpp new file mode 100644 index 0000000..a3b02cc --- /dev/null +++ b/examples/05_target_tracking.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Target examples. +//! +//! Shows how to use: +//! - `Trackable` for dynamic sky objects (Sun, planets, Moon, stars, ICRS directions) +//! - `Target` / `CoordinateWithPM` as timestamped coordinate snapshots +//! - optional proper motion for stellar targets +//! - target frame+center conversion through `From<&Target<_>>` +//! +//! Run with: +//! `cargo run --example 08_target` + +use qtty::*; +use siderust::astro::orbit::Orbit; +use siderust::astro::proper_motion::{set_proper_motion_since_j2000, ProperMotion}; +use siderust::bodies::comet::HALLEY; +use siderust::bodies::solar_system::{Mars, Moon, Sun}; +use siderust::bodies::{catalog, Satellite}; +use siderust::coordinates::cartesian; +use siderust::coordinates::centers::{Geocentric, Heliocentric}; +use siderust::coordinates::spherical::direction; +use siderust::targets::{Target, Trackable}; +use siderust::time::JulianDate; + +fn main() { + let jd = JulianDate::J2000; + let jd_next = jd + Days::new(1.0); + + println!("Target + Trackable examples"); + println!("===========================\n"); + + section_trackable_objects(jd, jd_next); + section_target_snapshots(jd, jd_next); + section_target_with_proper_motion(jd); + section_target_transform(jd); +} + +fn section_trackable_objects(jd: JulianDate, jd_next: JulianDate) { + println!("1) Trackable objects (ICRS, star, Sun, planet, Moon)"); + + let fixed_icrs = direction::ICRS::new(Degrees::new(120.0), Degrees::new(22.5)); + let fixed_icrs_at_jd = fixed_icrs.track(jd); + let fixed_icrs_at_next = fixed_icrs.track(jd_next); + + println!( + " ICRS direction is time-invariant: RA {:.3} -> {:.3}, Dec {:.3} -> {:.3}", + fixed_icrs_at_jd.ra(), + fixed_icrs_at_next.ra(), + fixed_icrs_at_jd.dec(), + fixed_icrs_at_next.dec() + ); + + let sirius_dir = catalog::SIRIUS.track(jd); + println!( + " Sirius via Trackable: RA {:.3}, Dec {:.3}", + sirius_dir.ra(), + sirius_dir.dec() + ); + + let sun = Sun.track(jd); + let mars = Mars.track(jd); + let moon = Moon.track(jd); + + println!( + " Sun barycentric distance: {:.6}", + sun.position.distance() + ); + println!( + " Mars barycentric distance: {:.6}", + mars.position.distance() + ); + println!( + " Moon geocentric distance: {:.1}\n", + moon.distance() + ); +} + +fn section_target_snapshots(jd: JulianDate, jd_next: JulianDate) { + println!("2) Target snapshots for arbitrary sky objects"); + + // Planet snapshot target (heliocentric ecliptic cartesian) + let mut mars_target = Target::new_static(Mars::vsop87a(jd), jd); + println!( + " Mars target at JD {:.1}: r = {:.6}", + mars_target.time, + mars_target.position.distance() + ); + + // Update target with a new ephemeris sample at the next epoch. + mars_target.update(Mars::vsop87a(jd_next), jd_next); + println!( + " Mars target updated to JD {:.1}: r = {:.6}", + mars_target.time, + mars_target.position.distance() + ); + + // Comet snapshot target (orbit propagated with Kepler helper). + let halley_target = Target::new_static(HALLEY.orbit.kepler_position(jd), jd); + println!( + " Halley target at JD {:.1}: r = {:.6}", + halley_target.time, + halley_target.position.distance() + ); + + // Satellite-like custom object: propagate its Orbit and wrap in Target. + let demo_satellite = Satellite::new( + "DemoSat", + Kilograms::new(1_200.0), + Kilometers::new(1.4), + Orbit::new( + AstronomicalUnits::new(1.0002), + 0.001, + Degrees::new(0.1), + Degrees::new(35.0), + Degrees::new(80.0), + Degrees::new(10.0), + jd, + ), + ); + let demo_satellite_target = Target::new_static(demo_satellite.orbit.kepler_position(jd), jd); + println!( + " {} target at JD {:.1}: r = {:.6}\n", + demo_satellite.name, + demo_satellite_target.time, + demo_satellite_target.position.distance() + ); +} + +fn section_target_with_proper_motion(jd: JulianDate) { + println!("3) Target with proper motion (stellar-style target)"); + + type MasPerYear = qtty::Per; + type MasPerYearQ = qtty::Quantity; + + let pm = ProperMotion::from_mu_alpha_star::( + MasPerYearQ::new(27.54), // µα⋆ + MasPerYearQ::new(10.86), // µδ + ); + + let mut moving_target = + Target::new(*catalog::BETELGEUSE.coordinate.get_position(), jd, pm.clone()); + + println!( + " Betelgeuse-like target at J2000: RA {:.6}, Dec {:.6}", + moving_target.position.ra(), + moving_target.position.dec() + ); + + let jd_future = jd + 25.0 * JulianDate::JULIAN_YEAR; + let moved = set_proper_motion_since_j2000(moving_target.position, pm, jd_future) + .expect("proper-motion propagation should succeed"); + moving_target.update(moved, jd_future); + + println!( + " After 25 years: RA {:.6}, Dec {:.6}\n", + moving_target.position.ra(), + moving_target.position.dec() + ); +} + +fn section_target_transform(jd: JulianDate) { + println!("4) Target conversion across frame + center"); + + type HelioEclAu = cartesian::position::EclipticMeanJ2000; + type GeoEqAu = cartesian::position::EquatorialMeanJ2000; + + let mars_helio: Target = Target::new_static(Mars::vsop87a(jd), jd); + let mars_geoeq: Target = Target::from(&mars_helio); + + println!( + " Mars heliocentric ecliptic target: r = {:.6}", + mars_helio.position.distance() + ); + println!( + " Mars geocentric equatorial target: r = {:.6}", + mars_geoeq.position.distance() + ); +} diff --git a/examples/05_time_periods.cpp b/examples/05_time_periods.cpp deleted file mode 100644 index 69a63d8..0000000 --- a/examples/05_time_periods.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file 05_time_periods.cpp - * @brief C++ port of siderust/examples/41_time_periods.rs - * - * Demonstrates the generic Period with different time types: - * - JulianDate periods - * - MJD (ModifiedJulianDate) periods - * - Conversions between time systems - * - * Run with: cmake --build build --target time_periods_example - */ - -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main() { - std::cout << "Generic Time Period Examples\n"; - std::cout << "============================\n\n"; - - // ========================================================================= - // 1. Period with JulianDate - // ========================================================================= - std::cout << "1. Period with JulianDate:\n"; - const JulianDate jd_start = JulianDate(2451545.0); // J2000.0 - const JulianDate jd_end = JulianDate(2451546.5); // 1.5 days later - std::cout << " Start: JD " << std::fixed << std::setprecision(6) << jd_start.value() << "\n"; - std::cout << " End: JD " << jd_end.value() << "\n"; - std::cout << " Duration: " << (jd_end.value() - jd_start.value()) << " days\n\n"; - - // ========================================================================= - // 2. Period with ModifiedJulianDate (MJD) - // ========================================================================= - std::cout << "2. Period with ModifiedJulianDate:\n"; - const MJD mjd_start = MJD(59000.0); - const MJD mjd_end = MJD(59002.5); - const Period mjd_period(mjd_start, mjd_end); - std::cout << " Start: MJD " << mjd_start.value() << "\n"; - std::cout << " End: MJD " << mjd_end.value() << "\n"; - std::cout << " Duration: " << mjd_period.duration().value() << " days\n\n"; - - // ========================================================================= - // 3. JulianDate <-> MJD conversion - // Relationship: MJD = JD - 2400000.5 - // ========================================================================= - std::cout << "3. Converting between time systems:\n"; - const MJD mjd_j2000 = MJD(51544.5); // MJD at J2000.0 - const JulianDate jd_from_mjd = JulianDate(mjd_j2000.value() + 2400000.5); - std::cout << " MJD: " << mjd_j2000.value() << "\n"; - std::cout << " JD: " << jd_from_mjd.value() << " (should be 2451545.0)\n\n"; - - // ========================================================================= - // 4. Period arithmetic - // ========================================================================= - std::cout << "4. Period arithmetic:\n"; - const MJD night_start = MJD(60000.0); - const MJD night_end = MJD(60000.5); // 12 hours later - const Period night(night_start, night_end); - - std::cout << " Night period start: MJD " << night.start().value() << "\n"; - std::cout << " Night period end: MJD " << night.end().value() << "\n"; - std::cout << " Duration in days: " << night.duration().value() << " days\n"; - std::cout << " Duration in hours: " << night.duration().value() << " h\n\n"; - - // ========================================================================= - // 5. J2000.0 epoch constant - // ========================================================================= - std::cout << "5. J2000.0 constants:\n"; - std::cout << " JulianDate::J2000() = JD " << std::setprecision(1) - << JulianDate::J2000().value() << "\n"; - std::cout << " Corresponding MJD = " << std::setprecision(3) - << (JulianDate::J2000().value() - 2400000.5) << "\n\n"; - - std::cout << "=== Example Complete ===\n"; - return 0; -} diff --git a/examples/06_astronomical_night.cpp b/examples/06_astronomical_night.cpp deleted file mode 100644 index 4a25bfc..0000000 --- a/examples/06_astronomical_night.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @file 06_astronomical_night.cpp - * @brief C++ port of siderust/examples/25_astronomical_night.rs - * - * Demonstrates finding astronomical night periods using the siderust library. - * Astronomical night is defined as the period when the Sun's center is - * more than 18° below the horizon (altitude < -18°). - * - * Usage: - * ./06_astronomical_night [YYYY-MM-DD] [lat_deg] [lon_deg] [height_m] - * - * Defaults: - * - Start date: MJD 60000.0 (2023-02-25) - * - Location: Greenwich Observatory (51.4769°N, 0°E) - * - Search period: 7 days - * - * Run with: cmake --build build --target astronomical_night_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main(int argc, char *argv[]) { - // Parse optional arguments (lon, lat, height, start_mjd) - const double lat_deg = argc > 1 ? std::atof(argv[1]) : 51.4769; - const double lon_deg = argc > 2 ? std::atof(argv[2]) : 0.0; - const double height_m = argc > 3 ? std::atof(argv[3]) : 0.0; - const double mjd0 = argc > 4 ? std::atof(argv[4]) : 60000.0; - - const Geodetic site = Geodetic(lon_deg, lat_deg, height_m); - const MJD start(mjd0); - const MJD end(mjd0 + 7.0); // 7-day window - const Period window(start, end); - - std::cout << "Astronomical Night Periods (Sun altitude < -18°)\n"; - std::cout << "================================================\n"; - std::cout << "Observer: lat = " << lat_deg << "°, " - << "lon = " << lon_deg << "°, " - << "height = " << height_m << " m\n"; - std::cout << "MJD window: " << start.value() << " → " << end.value() - << " (7 days)\n\n"; - - // Find astronomical night periods (Sun < -18°) - const auto nights = sun::below_threshold(site, window, qtty::Degree(-18.0)); - - if (nights.empty()) { - std::cout << "No astronomical night periods found in this week.\n"; - std::cout << "(This can happen at high latitudes during summer.)\n"; - } else { - std::cout << "Found " << nights.size() << " astronomical night period(s):\n\n"; - for (const auto &period : nights) { - const double dur_min = period.duration().value(); - std::cout << " MJD " << std::fixed << std::setprecision(4) - << period.start().value() - << " → " << period.end().value() - << " (" << std::setprecision(1) << dur_min << " min)\n"; - } - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/06_night_events.cpp b/examples/06_night_events.cpp new file mode 100644 index 0000000..4cd8767 --- /dev/null +++ b/examples/06_night_events.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// Night Events Example +/// +/// Shows how to spot "night-type" crossing events and night periods in a +/// one-week window using civil/nautical/astronomical/horizon thresholds. +/// +/// Build & run: +/// cmake --build build-local --target 06_night_events_example +/// ./build-local/06_night_events_example [lat_deg] [lon_deg] [height_m] + +#include + +#include +#include +#include +#include +#include + +using namespace siderust; + +// Twilight threshold constants (same values as siderust::calculus::solar::night_types) +namespace twilight { +constexpr auto HORIZON = qtty::Degree(0.0); +constexpr auto APPARENT_HORIZON = qtty::Degree(-0.833); +constexpr auto CIVIL = qtty::Degree(-6.0); +constexpr auto NAUTICAL = qtty::Degree(-12.0); +constexpr auto ASTRONOMICAL = qtty::Degree(-18.0); +} // namespace twilight + +/// Format a CivilTime as YYYY-MM-DDTHH:MM:SS. +static std::string fmt_utc(const tempoch::CivilTime &ct) { + std::ostringstream os; + os << ct.year << '-' + << std::setfill('0') << std::setw(2) << int(ct.month) << '-' + << std::setw(2) << int(ct.day) << 'T' + << std::setw(2) << int(ct.hour) << ':' + << std::setw(2) << int(ct.minute) << ':' + << std::setw(2) << int(ct.second); + return os.str(); +} + +static Period week_from_mjd(const MJD &start) { + MJD end = start + qtty::Day(7.0); + return Period(start, end); +} + +static void print_events_for_type(const Geodetic &site, const Period &week, + const char *name, qtty::Degree threshold) { + auto events = sun::crossings(site, week, threshold); + int downs = 0, raises = 0; + + std::cout << std::left << std::setw(18) << name << " threshold " + << std::right << std::setw(8) << std::fixed << std::setprecision(3) + << threshold.value() << " deg -> " << events.size() + << " crossing(s)" << std::endl; + + for (auto &ev : events) { + const char *label; + if (ev.direction == CrossingDirection::Setting) { + ++downs; + label = "night-type down (Sun setting below threshold)"; + } else { + ++raises; + label = "night-type raise (Sun rising above threshold)"; + } + auto utc = ev.time.to_utc(); + std::cout << " - " << label << " at " << fmt_utc(utc) << std::endl; + } + std::cout << " summary: down=" << downs << " raise=" << raises << std::endl; +} + +static void print_periods_for_type(const Geodetic &site, const Period &week, + const char *name, qtty::Degree threshold) { + auto periods = sun::below_threshold(site, week, threshold); + std::cout << std::left << std::setw(18) << name + << " night periods (Sun < " << std::fixed << std::setprecision(3) + << threshold.value() << " deg): " << periods.size() << std::endl; + + for (auto &p : periods) { + auto s = p.start().to_utc(); + auto e = p.end().to_utc(); + auto hours = p.duration(); + std::cout << " - " << fmt_utc(s) << " -> " << fmt_utc(e) + << " (" << std::fixed << std::setprecision(1) + << hours.value() << " h)" << std::endl; + } +} + +int main(int argc, char *argv[]) { + double lat_deg = argc > 1 ? std::stod(argv[1]) : 51.4769; + double lon_deg = argc > 2 ? std::stod(argv[2]) : 0.0; + double height_m = argc > 3 ? std::stod(argv[3]) : 0.0; + + Geodetic site{qtty::Degree(lon_deg), qtty::Degree(lat_deg), + qtty::Meter(height_m)}; + + // Fixed start date: 2024-06-01 00:00 UTC (MJD ≈ 60461) + auto mjd_start = MJD::from_utc({2024, 6, 1, 0, 0, 0}); + auto week = week_from_mjd(mjd_start); + + struct NightType { + const char *name; + qtty::Degree threshold; + }; + NightType night_types[] = { + {"Horizon", twilight::HORIZON}, + {"Apparent Horizon", twilight::APPARENT_HORIZON}, + {"Civil", twilight::CIVIL}, + {"Nautical", twilight::NAUTICAL}, + {"Astronomical", twilight::ASTRONOMICAL}, + }; + + std::cout << "Night events over one week" << std::endl; + std::cout << "==========================" << std::endl; + std::cout << "Site: lat=" << lat_deg << " lon=" << lon_deg + << " height=" << height_m << std::endl; + std::cout << "Week start: 2024-06-01 UTC\n" << std::endl; + + std::cout << "1) Night-type crossing events" << std::endl; + for (auto &nt : night_types) { + print_events_for_type(site, week, nt.name, nt.threshold); + } + + std::cout << "\n2) Night periods per night type" << std::endl; + for (auto &nt : night_types) { + print_periods_for_type(site, week, nt.name, nt.threshold); + } + + return 0; +} diff --git a/examples/07_find_night_periods_365day.cpp b/examples/07_find_night_periods_365day.cpp deleted file mode 100644 index 2484ac8..0000000 --- a/examples/07_find_night_periods_365day.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @file 07_find_night_periods_365day.cpp - * @brief C++ port of siderust/examples/31_find_night_periods_365day.rs - * - * Runs the astronomical night finder over a full 365-day horizon and prints - * all astronomical night periods (Sun altitude < -18°) for the default site - * (Roque de los Muchachos Observatory, La Palma). - * - * Usage: - * ./07_find_night_periods_365day [start_mjd] - * - * Default: MJD 60339.0 (~2026-01-01) - * - * Run with: cmake --build build --target find_night_periods_365day_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main(int argc, char *argv[]) { - // Default: MJD for 2026-01-01 - const double start_mjd = argc > 1 ? std::atof(argv[1]) : 60339.0; - - const MJD start(start_mjd); - const MJD end(start_mjd + 365.0); - const Period year_window(start, end); - - std::cout << "Find astronomical night periods for 365 days starting MJD " - << std::fixed << std::setprecision(0) << start_mjd << "\n"; - std::cout << "Observer: Roque de los Muchachos Observatory (La Palma)\n\n"; - - const auto nights = sun::below_threshold(ROQUE_DE_LOS_MUCHACHOS, year_window, - qtty::Degree(-18.0)); - - if (nights.empty()) { - std::cout << "No astronomical night periods found for this year at this site.\n"; - return 0; - } - - std::cout << "Found " << nights.size() << " night periods:\n\n"; - for (const auto &p : nights) { - const double dur_min = p.duration().value(); - std::cout << " MJD " << std::setprecision(4) << p.start().value() - << " → " << p.end().value() - << " (" << std::setprecision(1) << dur_min << " min)\n"; - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/07_moon_properties.cpp b/examples/07_moon_properties.cpp new file mode 100644 index 0000000..e8d4482 --- /dev/null +++ b/examples/07_moon_properties.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// Moon phase quick examples. +/// +/// Shows: +/// 1) how to get moon phase properties at a given instant, +/// 2) how to find windows where illumination is in a given range. +/// +/// Build & run: cmake --build build-local --target 07_moon_properties_example +/// ./build-local/07_moon_properties_example + +#include + +#include +#include +#include +#include +#include + +using namespace siderust; + +/// Helper: print a list of MJD periods with their durations. +void print_periods(const std::string &label, + const std::vector &periods) { + std::cout << "\n" << label << ": " << periods.size() << " period(s)" + << std::endl; + for (const auto &p : periods) { + auto dur_h = p.duration(); + auto s = p.start().to_utc(); + auto e = p.end().to_utc(); + std::cout << " - " << s << " -> " << e << " (" << dur_h << ")" + << std::endl; + } +} + +int main() { + // Default site: Roque de los Muchachos + double lat = 28.762; + double lon = -17.892; + double h_m = 2396.0; + + Geodetic site{qtty::Degree(lon), qtty::Degree(lat), qtty::Meter(h_m)}; + + // Use a fixed date for reproducibility: 2026-03-01 00:00 UTC + auto jd = JulianDate::from_utc({2026, 3, 1, 0, 0, 0}); + auto mjd = jd.to(); + auto window = Period(mjd, MJD(mjd.value() + 35.0)); + SearchOptions opts{}; + + // ========================================================================= + // 1) Point-in-time phase properties + // ========================================================================= + auto geo = moon::phase_geocentric(jd); + auto topo = moon::phase_topocentric(jd, site); + + constexpr double RAD_TO_DEG = 180.0 / M_PI; + + std::cout << std::fixed; + std::cout << "Moon phase at 2026-03-01 00:00 UTC" << std::endl; + std::cout << "==================================" << std::endl; + std::cout << std::setprecision(4); + std::cout << "Site: lat=" << lat << " deg, lon=" << lon + << " deg, h=" << std::setprecision(0) << h_m << " m" << std::endl; + + std::cout << "\nGeocentric:" << std::endl; + std::cout << " label : " << moon::phase_label(geo) << std::endl; + std::cout << std::setprecision(4); + std::cout << " illuminated fraction : " << geo.illuminated_fraction << std::endl; + std::cout << std::setprecision(2); + std::cout << " illuminated percent : " << illuminated_percent(geo) << " %" + << std::endl; + std::cout << " phase angle : " + << geo.phase_angle_rad * RAD_TO_DEG << " deg" << std::endl; + std::cout << " elongation : " + << geo.elongation_rad * RAD_TO_DEG << " deg" << std::endl; + std::cout << " waxing : " << std::boolalpha << geo.waxing + << std::endl; + + std::cout << "\nTopocentric:" << std::endl; + std::cout << " label : " << moon::phase_label(topo) + << std::endl; + std::cout << std::setprecision(4); + std::cout << " illuminated fraction : " << topo.illuminated_fraction + << std::endl; + std::cout << " illumination delta : " + << std::showpos + << (topo.illuminated_fraction - geo.illuminated_fraction) * 100.0 + << std::noshowpos << " %" << std::endl; + std::cout << " elongation : " + << topo.elongation_rad * RAD_TO_DEG << " deg" << std::endl; + + // ========================================================================= + // 2) Principal phase events + // ========================================================================= + auto events = moon::find_phase_events(window, opts); + std::cout << "\nPrincipal phase events in next 35 days: " << events.size() + << std::endl; + for (const auto &ev : events) { + auto utc = ev.time.to_utc(); + std::cout << " - " << std::setw(13) << std::right << ev.kind + << " at " << utc << " UTC" << std::endl; + } + + // ========================================================================= + // 3) Illumination range searches + // ========================================================================= + auto crescent = moon::illumination_range(window, 0.05, 0.35, opts); + auto quarterish = moon::illumination_range(window, 0.45, 0.55, opts); + auto gibbous = moon::illumination_range(window, 0.65, 0.95, opts); + + print_periods("Crescent-like range (5%-35%)", crescent); + print_periods("Quarter-like range (45%-55%)", quarterish); + print_periods("Gibbous-like range (65%-95%)", gibbous); + + return 0; +} diff --git a/examples/08_night_quality_scoring.cpp b/examples/08_night_quality_scoring.cpp deleted file mode 100644 index 10bf0ff..0000000 --- a/examples/08_night_quality_scoring.cpp +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @file 08_night_quality_scoring.cpp - * @brief C++ port of siderust/examples/35_night_quality_scoring.rs - * - * Scores each night in a month based on Moon interference and astronomical - * darkness duration. Uses Sun/Moon altitude APIs for both criteria. - * - * Run with: cmake --build build --target night_quality_scoring_example - */ - -#include -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -struct NightScore { - double start_mjd; - double dark_hours; - double moon_up_hours; - double score; -}; - -static NightScore score_night(double night_start_mjd, const Geodetic &obs) { - const MJD start(night_start_mjd); - const MJD end(night_start_mjd + 1.0); - const Period window(start, end); - - // Astronomical darkness (Sun below -18°) - const auto dark_periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); - double dark_hours = 0.0; - for (const auto &p : dark_periods) - dark_hours += p.duration().value(); - - // Moon above horizon - const auto moon_up = moon::above_threshold(obs, window, qtty::Degree(0.0)); - double moon_hours = 0.0; - for (const auto &p : moon_up) - moon_hours += p.duration().value(); - - // Score = dark_hours * (1 - 0.7 * moon_interference) - double moon_interference = std::min(moon_hours / 24.0, 1.0); - double sc = dark_hours * (1.0 - 0.7 * moon_interference); - - return {night_start_mjd, dark_hours, moon_hours, sc}; -} - -int main() { - std::cout << "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; - std::cout << "\u2551 Monthly Observing Conditions Report \u2551\n"; - std::cout << "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; - - // Mauna Kea Observatory, Hawaii - const Geodetic obs = Geodetic(-155.472, 19.826, 4207.0); - std::cout << "Observatory: Mauna Kea, Hawaii\n"; - std::cout << " lat = 19.826 N, lon = -155.472 E, elev = 4207 m\n\n"; - - const double start_mjd = 60000.0; - std::vector scores; - scores.reserve(30); - - std::cout << "Analyzing 30 nights starting MJD " << std::fixed - << std::setprecision(0) << start_mjd << "...\n\n"; - - for (int day = 0; day < 30; ++day) { - scores.push_back(score_night(start_mjd + day, obs)); - } - - // Print table header - std::cout << std::setw(10) << "MJD" - << std::setw(12) << "Dark(h)" - << std::setw(12) << "Moon(h)" - << std::setw(10) << "Score" - << "\n"; - std::cout << std::string(44, '-') << "\n"; - - for (const auto &s : scores) { - std::cout << std::setw(10) << std::setprecision(0) << s.start_mjd - << std::setw(12) << std::setprecision(2) << s.dark_hours - << std::setw(12) << s.moon_up_hours - << std::setw(10) << std::setprecision(3) << s.score - << "\n"; - } - - // Summary statistics - auto best = *std::max_element(scores.begin(), scores.end(), - [](const NightScore &a, const NightScore &b) { - return a.score < b.score; - }); - auto worst = *std::min_element(scores.begin(), scores.end(), - [](const NightScore &a, const NightScore &b) { - return a.score < b.score; - }); - - std::cout << "\n--- Summary ---\n"; - std::cout << "Best night: MJD " << std::setprecision(0) << best.start_mjd - << " score=" << std::setprecision(3) << best.score - << " dark=" << std::setprecision(2) << best.dark_hours << " h\n"; - std::cout << "Worst night: MJD " << std::setprecision(0) << worst.start_mjd - << " score=" << std::setprecision(3) << worst.score - << " dark=" << std::setprecision(2) << worst.dark_hours << " h\n"; - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/08_solar_system.cpp b/examples/08_solar_system.cpp new file mode 100644 index 0000000..86fd75e --- /dev/null +++ b/examples/08_solar_system.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Solar System + Planets Module Tour +//! +//! Run with: `cargo run --example 12_solar_system_example` + +use qtty::*; +use siderust::astro::orbit::Orbit; +use siderust::bodies::planets::{OrbitExt, Planet}; +use siderust::bodies::solar_system::*; +use siderust::calculus::vsop87::VSOP87; +use siderust::coordinates::cartesian::position::EclipticMeanJ2000; +use siderust::coordinates::centers::Geocentric; +use siderust::coordinates::transform::TransformCenter; +use siderust::time::JulianDate; + +fn main() { + let jd = JulianDate::J2000; + let now = JulianDate::from_utc(chrono::Utc::now()); + + println!("=== Siderust Solar System Module Tour ===\n"); + println!( + "Epoch used for deterministic outputs: J2000 (JD {:.1})", + jd + ); + println!("Current epoch snapshot: JD {:.6}\n", now); + + section_catalog_overview(); + section_planet_constants_and_periods(); + section_vsop87_positions(jd); + section_center_transforms(jd); + section_moon_and_lagrange_points(jd); + section_trait_dispatch(jd); + section_planet_builder(); + section_current_snapshot(now); +} + +fn section_catalog_overview() { + println!("1) CATALOG OVERVIEW"); + println!("------------------"); + println!("Sun: {}", SOLAR_SYSTEM.sun.name); + println!("Major planets: {}", SOLAR_SYSTEM.planets.len()); + println!("Dwarf planets: {}", SOLAR_SYSTEM.dwarf_planets.len()); + println!("Major moons: {}", SOLAR_SYSTEM.moons.len()); + println!("Lagrange points: {}\n", SOLAR_SYSTEM.lagrange_points.len()); +} + +fn section_planet_constants_and_periods() { + println!("2) PLANET CONSTANTS + ORBITEXT::period()\n"); + + let major_planets = [ + ("Mercury", &MERCURY), + ("Venus", &VENUS), + ("Earth", &EARTH), + ("Mars", &MARS), + ("Jupiter", &JUPITER), + ("Saturn", &SATURN), + ("Uranus", &URANUS), + ("Neptune", &NEPTUNE), + ]; + + println!("{:<8} {:>10} {:>10} {:>10}", "Planet", "a [AU]", "e", "Period"); + println!("{}", "-".repeat(48)); + for (name, p) in major_planets { + println!( + "{:<8} {:>10.6} {:>10.6} {:>10.2}", + name, + p.orbit.semi_major_axis, + p.orbit.eccentricity, + p.orbit.period().to::() + ); + } + println!(); +} + +fn section_vsop87_positions(jd: JulianDate) { + println!("3) VSOP87 EPHEMERIDES (HELIOCENTRIC + BARYCENTRIC)"); + println!("-----------------------------------------------"); + + let earth_h = Earth::vsop87a(jd); + let mars_h = Mars::vsop87a(jd); + let earth_mars = earth_h.distance_to(&mars_h); + + println!( + "Earth heliocentric distance: {:.6}", + earth_h.distance() + ); + println!( + "Mars heliocentric distance: {:.6}", + mars_h.distance() + ); + println!( + "Earth-Mars separation: {:.6} ({:.0}", + earth_mars, + earth_mars.to::() + ); + + let sun_bary = Sun::vsop87e(jd); + println!( + "Sun barycentric offset from SSB: {:.8}\n", + sun_bary.distance() + ); + + let (jupiter_pos, jupiter_vel) = Jupiter::vsop87e_pos_vel(jd); + println!("Jupiter barycentric position+velocity at J2000:"); + println!(" r = {:.6}", jupiter_pos.x()); + println!(" v = {:.6}\n", jupiter_vel); +} + +fn section_center_transforms(jd: JulianDate) { + println!("4) CENTER TRANSFORMS (HELIOCENTRIC -> GEOCENTRIC)"); + println!("-----------------------------------------------"); + + let mars_helio = Mars::vsop87a(jd); + let mars_geo: EclipticMeanJ2000 = mars_helio.to_center(jd); + + println!( + "Mars geocentric distance at J2000: {:.6}", + mars_geo.distance() + ); + println!( + "Mars geocentric distance at J2000: {:.0}\n", + mars_geo.distance().to::() + ); +} + +fn section_moon_and_lagrange_points(jd: JulianDate) { + println!("5) MOON + LAGRANGE POINTS"); + println!("-------------------------"); + + let moon_geo = Moon::get_geo_position::(jd); + println!( + "Moon geocentric distance (ELP2000): {:.1} ({:.6})", + moon_geo.distance(), + moon_geo.distance().to::() + ); + + println!("Lagrange points available in the catalog:"); + for lp in LAGRANGE_POINTS { + println!( + " {:<12} in {:<10} -> lon={:>7.2} lat={:>6.2} r={:>5.2}", + lp.name, + lp.parent_system, + lp.position.azimuth, + lp.position.polar, + lp.position.distance + ); + } + println!(); +} + +fn section_trait_dispatch(jd: JulianDate) { + println!("6) TRAIT-BASED DISPATCH (calculus::vsop87::VSOP87)"); + println!("-------------------------------------------------"); + + let dynamic_planets: [(&str, &dyn VSOP87); 4] = [ + ("Mercury", &Mercury), + ("Venus", &Venus), + ("Earth", &Earth), + ("Mars", &Mars), + ]; + + for (name, planet) in dynamic_planets { + let helio = planet.vsop87a(jd); + let bary = planet.vsop87e(jd); + println!( + "{:<8} helio={:>8.5} bary={:>8.5}", + name, + helio.distance(), + bary.distance() + ); + } + println!(); +} + +fn section_planet_builder() { + println!("7) planets::Planet BUILDER + OrbitExt"); + println!("-------------------------------------"); + + let demo_world = Planet::builder() + .mass(Kilograms::new(5.972e24 * 2.0)) + .radius(Kilometers::new(6371.0 * 1.3)) + .orbit(Orbit::new( + AstronomicalUnits::new(1.4), + 0.07, + Degrees::new(4.0), + Degrees::new(120.0), + Degrees::new(80.0), + Degrees::new(10.0), + JulianDate::J2000, + )) + .build(); + + println!("Custom planet built at runtime:"); + println!(" mass = {}", demo_world.mass); + println!(" radius = {}", demo_world.radius); + println!(" a = {}", demo_world.orbit.semi_major_axis); + println!( + " sidereal period = {:.2}\n", + demo_world.orbit.period().to::() + ); +} + +fn section_current_snapshot(now: JulianDate) { + println!("8) CURRENT SNAPSHOT"); + println!("-------------------"); + + let earth_now = Earth::vsop87a(now); + let mars_now = Mars::vsop87a(now); + let mars_geo_now: EclipticMeanJ2000 = mars_now.to_center(now); + + println!( + "Earth-Sun distance now: {:.6}", + earth_now.distance() + ); + println!( + "Mars-Sun distance now: {:.6}", + mars_now.distance() + ); + println!( + "Mars-Earth distance now: {:.6} ({:.0})", + mars_geo_now.distance(), + mars_geo_now.distance().to::() + ); + + println!("\n=== End of example ==="); +} diff --git a/examples/09_star_observability.cpp b/examples/09_star_observability.cpp index 2953b2e..1562dbb 100644 --- a/examples/09_star_observability.cpp +++ b/examples/09_star_observability.cpp @@ -1,91 +1,88 @@ -/** - * @file 09_star_observability.cpp - * @brief C++ port of siderust/examples/39_star_observability.rs - * - * Demonstrates using the altitude period API to plan optimal observing windows - * for multiple catalog stars at a given observatory. - * - * Run with: cmake --build build --target star_observability_example - */ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon -#include -#include -#include +/// Star observability in altitude + azimuth ranges. +/// +/// Build & run: +/// cmake --build build-local --target 09_star_observability_example +/// ./build-local/09_star_observability_example #include +#include +#include +#include + using namespace siderust; -using namespace qtty::literals; -int main() { - std::cout << "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; - std::cout << "\u2551 Star Observability Planner \u2551\n"; - std::cout << "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; - - // Observatory: Greenwich - const Geodetic obs = Geodetic(0.0, 51.4769, 0.0); - std::cout << "Observatory: Greenwich Royal Observatory\n"; - std::cout << " lat = 51.4769 N, lon = 0.0 E\n\n"; - - // Tonight: one night starting at MJD 60000.5 - const MJD start(60000.5); - const MJD end(60001.5); - const Period night(start, end); - std::cout << "Observation window: MJD " << start.value() << " to " << end.value() << "\n\n"; - - // Find astronomical night (Sun below -18°) - const auto dark_periods = sun::below_threshold(obs, night, qtty::Degree(-18.0)); - - if (dark_periods.empty()) { - std::cout << "No astronomical darkness available tonight!\n"; - return 0; +/// Intersect two sorted vectors of periods. +/// Returns every non-empty overlap between a period in `a` and a period in `b`. +static std::vector intersect_periods(const std::vector &a, + const std::vector &b) { + std::vector result; + size_t j = 0; + for (size_t i = 0; i < a.size() && j < b.size(); ) { + double lo = std::max(a[i].start().value(), b[j].start().value()); + double hi = std::min(a[i].end().value(), b[j].end().value()); + if (lo < hi) { + result.push_back(Period(MJD(lo), MJD(hi))); + } + // advance whichever period ends first + if (a[i].end().value() < b[j].end().value()) ++i; else ++j; } + return result; +} - double total_dark_h = 0.0; - for (const auto &p : dark_periods) - total_dark_h += p.duration().value(); - std::cout << "Astronomical night duration: " << std::fixed << std::setprecision(2) - << total_dark_h << " h\n\n"; - - // Target stars - struct TargetInfo { const char *name; const Star *star; }; - const TargetInfo targets[] = { - {"Sirius", &SIRIUS}, - {"Vega", &VEGA}, - {"Altair", &ALTAIR}, - {"Betelgeuse", &BETELGEUSE}, - {"Rigel", &RIGEL}, - {"Polaris", &POLARIS}, - }; - - const qtty::Degree min_alt(30.0); // minimum altitude for good observation - - std::cout << "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; - std::cout << " Target Visibility (altitude > 30°, during astronomical night)\n"; - std::cout << "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; - - for (const auto &t : targets) { - const auto visible = star_altitude::above_threshold(*t.star, obs, night, min_alt); - - double total_h = 0.0; - for (const auto &p : visible) - total_h += p.duration().value(); - - std::cout << std::left << std::setw(12) << t.name - << " periods=" << std::setw(3) << visible.size() - << " total=" << std::setprecision(2) << std::fixed - << total_h << " h\n"; - - if (!visible.empty()) { - const auto &best = visible.front(); - std::cout << " first period: MJD " - << std::setprecision(4) << best.start().value() - << " → " << best.end().value() - << " (" << std::setprecision(2) << best.duration().value() - << " h)\n"; - } +int main() { + std::cout << "Star observability: altitude + azimuth constraints\n" << std::endl; + + const auto &observer = ROQUE_DE_LOS_MUCHACHOS; + const auto &target = SIRIUS; + + // One-night search window (MJD TT). + MJD t0(60000.0); + Period window(t0, t0 + qtty::Day(1.0)); + + // Constraint 1: altitude between 25° and 65°. + auto min_alt = qtty::Degree(25.0); + auto max_alt = qtty::Degree(65.0); + auto above_min = star_altitude::above_threshold(target, observer, window, min_alt); + auto below_max = star_altitude::below_threshold(target, observer, window, max_alt); + auto altitude_periods = intersect_periods(above_min, below_max); + + // Constraint 2: azimuth between 110° and 220° (ESE -> SW sector). + auto min_az = qtty::Degree(110.0); + auto max_az = qtty::Degree(220.0); + auto azimuth_periods = star_altitude::in_azimuth_range( + target, observer, window, min_az, max_az); + + // Final observability: periods satisfying both constraints simultaneously. + auto observable = intersect_periods(altitude_periods, azimuth_periods); + + std::cout << "Observer: Roque de los Muchachos" << std::endl; + std::cout << "Target: Sirius" << std::endl; + std::cout << "Window: MJD " << std::fixed << std::setprecision(1) + << window.start().value() << " -> " << window.end().value() + << "\n" << std::endl; + + std::cout << "Altitude range: " << min_alt << " .. " << max_alt << std::endl; + std::cout << "Azimuth range: " << min_az << " .. " << max_az << "\n" << std::endl; + + std::cout << "Matched periods: " << observable.size() << std::endl; + double total_hours = 0.0; + for (size_t i = 0; i < observable.size(); ++i) { + auto hours = observable[i].duration(); + total_hours += hours.value(); + std::cout << " " << (i + 1) << ". MJD " + << std::fixed << std::setprecision(6) + << observable[i].start().value() << " -> " + << observable[i].end().value() + << " (" << std::setprecision(4) << hours << ")" + << std::endl; } - std::cout << "\n=== Example Complete ===\n"; + std::cout << "\nTotal observable time in both ranges: " + << std::setprecision(4) << qtty::Hour(total_hours) << std::endl; + return 0; } diff --git a/examples/10_altitude_periods_trait.cpp b/examples/10_altitude_periods_trait.cpp deleted file mode 100644 index 6195cd6..0000000 --- a/examples/10_altitude_periods_trait.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @file 10_altitude_periods_trait.cpp - * @brief C++ port of siderust/examples/24_altitude_periods_trait.rs - * - * Demonstrates the unified altitude period API (implemented via the - * `Target` base class and free-function namespaces) for finding time - * intervals when celestial bodies are within specific altitude ranges. - * - * In Rust this is the `AltitudePeriodsProvider` trait. In C++ the same - * functionality is available via: - * - `sun::above_threshold / below_threshold` - * - `moon::above_threshold / below_threshold` - * - `star_altitude::above_threshold / below_threshold` - * - Polymorphic via the `Target` / `BodyTarget` / `StarTarget` classes - * - * Run with: cmake --build build --target altitude_periods_trait_example - */ - -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main() { - std::cout << "=== Altitude Periods API Examples ===\n\n"; - - const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; - std::cout << "Observer: Roque de los Muchachos Observatory\n\n"; - - // Time window: one week starting from MJD 60000 - const MJD start(60000.0); - const MJD end(60007.0); - const Period window(start, end); - std::cout << "Time window: MJD " << std::fixed << std::setprecision(1) - << start.value() << " → " << end.value() << " (7 days)\n\n"; - - // ------------------------------------------------------------------------- - // Example 1: Astronomical nights (Sun below -18°) - // ------------------------------------------------------------------------- - std::cout << "--- Example 1: Astronomical Nights ---\n"; - const auto astro_nights = sun::below_threshold(observer, window, qtty::Degree(-18.0)); - - std::cout << "Found " << astro_nights.size() << " astronomical night period(s):\n"; - for (size_t i = 0; i < std::min(astro_nights.size(), size_t(3)); ++i) { - const auto &p = astro_nights[i]; - std::cout << " Night " << i + 1 << ": " - << std::setprecision(3) << p.duration().value() << " h\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Example 2: Sirius above 30° - // ------------------------------------------------------------------------- - std::cout << "--- Example 2: Sirius High Above Horizon ---\n"; - const auto sirius_high = star_altitude::above_threshold(SIRIUS, observer, window, - qtty::Degree(30.0)); - std::cout << "Sirius above 30° altitude:\n"; - std::cout << " Found " << sirius_high.size() << " period(s)\n"; - if (!sirius_high.empty()) { - std::cout << " First period: " << std::setprecision(3) - << sirius_high.front().duration().value() << " h\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Example 3: Custom ICRS direction (Betelgeuse) — above horizon - // ------------------------------------------------------------------------- - std::cout << "--- Example 3: Custom ICRS Direction (Betelgeuse) ---\n"; - // Betelgeuse: RA ≈ 88.79°, Dec ≈ +7.41° - const spherical::direction::ICRS betelgeuse_dir(qtty::Degree(88.79), - qtty::Degree(7.41)); - // Use StarTarget with a custom (non-catalog) star to demonstrate the API - // (Alternatively use the catalog BETELGEUSE star handle) - const auto btel_visible = star_altitude::above_threshold(BETELGEUSE, observer, window, - qtty::Degree(0.0)); - double total_btel_h = 0.0; - for (const auto &p : btel_visible) - total_btel_h += p.duration().value(); - std::cout << "Betelgeuse above horizon:\n"; - std::cout << " Found " << btel_visible.size() << " period(s) in 7 days\n"; - std::cout << " Total visible time: " << std::setprecision(2) << total_btel_h << " h\n\n"; - - // ------------------------------------------------------------------------- - // Example 4: Moon altitude range query (0° to 20°) - // ------------------------------------------------------------------------- - std::cout << "--- Example 4: Low Moon Periods (0° to 20°) ---\n"; - const auto moon_low = moon::above_threshold(observer, window, qtty::Degree(0.0)); - const auto moon_high = moon::above_threshold(observer, window, qtty::Degree(20.0)); - - // Low moon = above 0° minus above 20° (approximate count; periods may differ) - std::cout << "Moon between 0° and 20° altitude (approx):\n"; - std::cout << " Moon > 0°: " << moon_low.size() << " period(s)\n"; - std::cout << " Moon > 20°: " << moon_high.size() << " period(s)\n\n"; - - // ------------------------------------------------------------------------- - // Example 5: Circumpolar star check (Polaris) - // ------------------------------------------------------------------------- - std::cout << "--- Example 5: Circumpolar Star (Polaris) ---\n"; - const auto polaris_up = star_altitude::above_threshold(POLARIS, observer, window, - qtty::Degree(0.0)); - - double total_polaris_h = 0.0; - for (const auto &p : polaris_up) - total_polaris_h += p.duration().value(); - - std::cout << "Polaris above horizon:\n"; - if (polaris_up.size() == 1 && std::abs(total_polaris_h - 7.0 * 24.0) < 2.4) { - std::cout << " Circumpolar (continuously visible for entire week)\n"; - } else { - std::cout << " Found " << polaris_up.size() << " period(s), " - << "total " << std::setprecision(2) << total_polaris_h << " h\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Example 6: Polymorphic dispatch via Target base class - // ------------------------------------------------------------------------- - std::cout << "--- Example 6: Polymorphic Target Dispatch ---\n"; - std::vector> targets; - targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(Body::Moon)); - targets.push_back(std::make_unique(VEGA)); - targets.push_back(std::make_unique(SIRIUS)); - - for (const auto &t : targets) { - const auto periods = t->above_threshold(observer, window, qtty::Degree(0.0)); - double total_h = 0.0; - for (const auto &p : periods) - total_h += p.duration().value(); - std::cout << " " << std::left << std::setw(8) << t->name() - << " " << std::setw(3) << periods.size() - << " periods total = " << std::setprecision(2) << total_h << " h\n"; - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/10_time_periods.cpp b/examples/10_time_periods.cpp new file mode 100644 index 0000000..34624bf --- /dev/null +++ b/examples/10_time_periods.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Time Scales, Formats, and Period Conversions Example +//! +//! Run with: `cargo run --example 05_time_periods` +//! +//! This example demonstrates `tempoch` (re-exported as `siderust::time`): +//! - Constructing an instant from `chrono::DateTime` +//! - Viewing the same absolute instant in each supported time scale: +//! `JD`, `JDE`, `MJD`, `TDB`, `TT`, `TAI`, `TCG`, `TCB`, `GPS`, `UnixTime`, `UT` +//! - Using the common type aliases: `JulianDate`, `JulianEphemerisDay`, +//! `ModifiedJulianDate`, `UniversalTime` +//! - Converting `Period` between scales and to/from `UtcPeriod` +//! +//! Notes: +//! - Scale conversions route through the canonical `JD(TT)` representation. +//! - `UT` is Earth-rotation based; `Time::::delta_t()` exposes `ΔT = TT − UT`. + +use chrono::{DateTime, Duration, Utc}; +use qtty::{Days, Second}; +use siderust::time::{ + Interval, JulianDate, JulianEphemerisDay, ModifiedJulianDate, Period, Time, TimeScale, + UniversalTime, UnixTime, UtcPeriod, GPS, JD, JDE, MJD, TAI, TCB, TCG, TDB, TT, UT, +}; + +fn print_scale(label: &str, time: Time, reference_jd: JulianDate) { + let jd_back: JulianDate = time.to::(); + let drift_s = (jd_back - reference_jd).to::(); + println!( + " {:<8} value = {:>16.9} | JD roundtrip drift = {:>11.3e} s", + label, time, drift_s + ); +} + +fn print_period(label: &str, period: &Period) { + println!( + " {:<8} [{:>16.9}] Δ = {}", + label, + period, + period.duration_days() + ); +} + +fn main() { + println!("Time Scales, Formats, and Period Conversions"); + println!("============================================\n"); + + let utc_ref = DateTime::::from_timestamp(946_728_000, 0).expect("valid UTC timestamp"); + let jd: JulianDate = JulianDate::from_utc(utc_ref); + + let jde: JulianEphemerisDay = jd.to::(); + let mjd: ModifiedJulianDate = jd.to::(); + let tdb: Time = jd.to::(); + let tt: Time = jd.to::(); + let tai: Time = jd.to::(); + let tcg: Time = jd.to::(); + let tcb: Time = jd.to::(); + let gps: Time = jd.to::(); + let unix: Time = jd.to::(); + let ut: UniversalTime = jd.to::(); + + println!("Reference UTC instant: {}\n", utc_ref.to_rfc3339()); + + println!("1) Each supported time scale for the same instant:"); + print_scale("JD", jd, jd); + print_scale("JDE", jde, jd); + print_scale("MJD", mjd, jd); + print_scale("TDB", tdb, jd); + print_scale("TT", tt, jd); + print_scale("TAI", tai, jd); + print_scale("TCG", tcg, jd); + print_scale("TCB", tcb, jd); + print_scale("GPS", gps, jd); + print_scale("Unix", unix, jd); + print_scale("UT", ut, jd); + println!( + " {:<8} delta_t = {:.3} s (TT - UT)\n", + "UT", + ut.delta_t().value() + ); + + println!("2) Time formats / aliases:"); + println!(" JulianDate alias: {}", jd); + println!(" JulianEphemerisDay alias: {}", jde); + println!(" ModifiedJulianDate alias: {}", mjd); + println!(" UniversalTime alias: {}", ut); + let utc_roundtrip = jd.to_utc().expect("JD should convert back to UTC"); + println!( + " UTC roundtrip from JD: {}\n", + utc_roundtrip.to_rfc3339() + ); + + println!("3) Period representations and conversions:"); + let period_jd: Period = Period::new(jd, jd + Days::new(0.5)); + let period_jde: Period = period_jd.to::().expect("JD -> JDE period conversion"); + let period_mjd: Period = period_jd.to::().expect("JD -> MJD period conversion"); + let period_tdb: Period = period_jd.to::().expect("JD -> TDB period conversion"); + let period_tt: Period = period_jd.to::().expect("JD -> TT period conversion"); + let period_tai: Period = period_jd.to::().expect("JD -> TAI period conversion"); + let period_tcg: Period = period_jd.to::().expect("JD -> TCG period conversion"); + let period_tcb: Period = period_jd.to::().expect("JD -> TCB period conversion"); + let period_gps: Period = period_jd.to::().expect("JD -> GPS period conversion"); + let period_unix: Period = period_jd + .to::() + .expect("JD -> Unix period conversion"); + let period_ut: Period = period_jd.to::().expect("JD -> UT period conversion"); + let period_utc: UtcPeriod = period_jd + .to::>() + .expect("JD -> UTC period conversion"); + + print_period("JD", &period_jd); + print_period("JDE", &period_jde); + print_period("MJD", &period_mjd); + print_period("TDB", &period_tdb); + print_period("TT", &period_tt); + print_period("TAI", &period_tai); + print_period("TCG", &period_tcg); + print_period("TCB", &period_tcb); + print_period("GPS", &period_gps); + print_period("Unix", &period_unix); + print_period("UT", &period_ut); + println!( + " {:<8} [{} -> {}] Δ = {:.6} days ({:.0} s)\n", + "UTC", + period_utc.start.to_rfc3339(), + period_utc.end.to_rfc3339(), + period_utc.duration_days(), + period_utc.duration_seconds() + ); + + println!("4) UtcPeriod / Interval> conversions back to typed periods:"); + let utc_window: UtcPeriod = Interval::new(utc_ref, utc_ref + Duration::hours(6)); + let from_utc_jd: Period = utc_window.to::(); + let from_utc_mjd: Period = utc_window.to::(); + let from_utc_ut: Period = utc_window.to::(); + let from_utc_unix: Period = utc_window.to::(); + + println!( + " UTC [{} -> {}] Δ = {:.6} days", + utc_window.start.to_rfc3339(), + utc_window.end.to_rfc3339(), + utc_window.duration_days() + ); + print_period("JD", &from_utc_jd); + print_period("MJD", &from_utc_mjd); + print_period("UT", &from_utc_ut); + print_period("Unix", &from_utc_unix); + + let utc_roundtrip_period: UtcPeriod = from_utc_mjd + .to::>() + .expect("MJD -> UTC period conversion"); + println!( + " UTC<-MJD [{} -> {}]", + utc_roundtrip_period.start.to_rfc3339(), + utc_roundtrip_period.end.to_rfc3339() + ); +} diff --git a/examples/11_compare_sun_moon_star.cpp b/examples/11_compare_sun_moon_star.cpp deleted file mode 100644 index 33b5119..0000000 --- a/examples/11_compare_sun_moon_star.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @file 11_compare_sun_moon_star.cpp - * @brief C++ port of siderust/examples/29_compare_sun_moon_star.rs - * - * Demonstrates generic altitude analysis for the Sun, Moon, and a star using - * a single helper function that works with the polymorphic `Target` base class. - * - * Run with: cmake --build build --target compare_sun_moon_star_example - */ - -#include -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -struct Summary { - std::string name; - size_t period_count; - double total_hours; - double max_period_hours; -}; - -/// Generic analysis for any `Target` subclass. -static Summary analyze_body(const Target &body, const Geodetic &obs, const Period &window, - qtty::Degree threshold) { - const auto periods = body.above_threshold(obs, window, threshold); - - double total = 0.0; - double max_duration = 0.0; - - for (const auto &p : periods) { - const double h = p.duration().value(); - total += h; - if (h > max_duration) - max_duration = h; - } - - return {body.name(), periods.size(), total, max_duration}; -} - -static void print_summary(const Summary &s) { - std::cout << std::left << std::setw(12) << s.name - << " periods=" << std::setw(4) << s.period_count - << " total=" << std::setw(8) << std::setprecision(2) << std::fixed - << s.total_hours << " h" - << " longest=" << std::setprecision(2) << s.max_period_hours << " h\n"; -} - -int main() { - std::cout << "=== Comparing Sun, Moon, and Star Altitude Periods ===\n\n"; - - const Geodetic observer = EL_PARANAL; - std::cout << "Observatory: ESO Paranal / VLT\n\n"; - - // 14-day window around a new moon - const Period window(MJD(60000.0), MJD(60014.0)); - std::cout << "Time window: 14 days starting MJD 60000\n\n"; - - // Build target list using polymorphic base class - std::vector> bodies; - bodies.push_back(std::make_unique(Body::Sun)); - bodies.push_back(std::make_unique(Body::Moon)); - bodies.push_back(std::make_unique(SIRIUS)); - bodies.push_back(std::make_unique(VEGA)); - bodies.push_back(std::make_unique(CANOPUS)); - - const qtty::Degree threshold_deg(20.0); - std::cout << "Altitude threshold: " << threshold_deg.value() << "°\n\n"; - - std::cout << std::string(72, '=') << "\n"; - std::cout << " Target Periods Total Time Longest Period\n"; - std::cout << std::string(72, '-') << "\n"; - - for (const auto &b : bodies) { - const auto s = analyze_body(*b, observer, window, threshold_deg); - print_summary(s); - } - - std::cout << std::string(72, '=') << "\n\n"; - - // ------------------------------------------------------------------------- - // Focused comparison: time simultaneously above 20° (Sun excluded) - // Illustrate that the same helper can iterate over any bodies vector. - // ------------------------------------------------------------------------- - std::cout << "--- Bodies visible (>20°) without the Sun ---\n\n"; - std::vector> night_bodies; - night_bodies.push_back(std::make_unique(Body::Moon)); - night_bodies.push_back(std::make_unique(SIRIUS)); - night_bodies.push_back(std::make_unique(VEGA)); - night_bodies.push_back(std::make_unique(CANOPUS)); - night_bodies.push_back(std::make_unique(RIGEL)); - night_bodies.push_back(std::make_unique(ALTAIR)); - night_bodies.push_back(std::make_unique(ARCTURUS)); - night_bodies.push_back(std::make_unique(ALDEBARAN)); - - for (const auto &b : night_bodies) { - const auto s = analyze_body(*b, observer, window, threshold_deg); - print_summary(s); - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/11_serde_serialization.cpp b/examples/11_serde_serialization.cpp new file mode 100644 index 0000000..f16676d --- /dev/null +++ b/examples/11_serde_serialization.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Serde serialization examples. +//! +//! Run with: `cargo run --example 17_serde_serialization --features serde` + +use qtty::*; +use serde::{Deserialize, Serialize}; +use siderust::astro::orbit::Orbit; +use siderust::bodies::comet::HALLEY; +use siderust::bodies::solar_system::{Earth, Mars, Moon}; +use siderust::coordinates::{cartesian, centers, frames, spherical}; +use siderust::targets::{Target, Trackable}; +use siderust::time::{JulianDate, ModifiedJulianDate}; +use std::fs; + +#[derive(Debug, Serialize, Deserialize)] +struct TimeBundle { + j2000: JulianDate, + mjd: ModifiedJulianDate, + timeline: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoordinateBundle { + geo_icrs_cart: + cartesian::Position, + helio_ecl_sph: + spherical::Position, + observer_site: centers::Geodetic, +} + +#[derive(Debug, Serialize, Deserialize)] +struct BodySnapshot { + name: String, + epoch: JulianDate, + orbit: Orbit, + heliocentric_ecliptic: + cartesian::Position, +} + +#[derive(Debug, Serialize, Deserialize)] +struct BodyTargetsBundle { + // Mars `Trackable` output is already a CoordinateWithPM<...>, i.e. Target<...>. + mars_bary_target: + Target>, + // Moon does not return CoordinateWithPM from `track`, so we wrap a snapshot. + moon_geo_target: + Target>, +} + +fn pretty_json(value: &T) -> String { + serde_json::to_string_pretty(value).expect("serialize to pretty JSON") +} + +fn roundtrip(value: &T) -> T +where + T: Serialize + for<'de> Deserialize<'de>, +{ + let json = serde_json::to_string(value).expect("serialize"); + serde_json::from_str(&json).expect("deserialize") +} + +fn main() { + println!("=== Siderust Serde Serialization Examples ===\n"); + + let jd = JulianDate::J2000; + + // ========================================================================= + // 1) Times + // ========================================================================= + println!("1) TIME OBJECTS"); + println!("---------------"); + + let time_bundle = TimeBundle { + j2000: jd, + mjd: ModifiedJulianDate::from(jd), + timeline: vec![jd, jd + Days::new(1.0), jd + Days::new(7.0)], + }; + println!("{}", pretty_json(&time_bundle)); + + let recovered_times: TimeBundle = roundtrip(&time_bundle); + println!( + "Roundtrip check: j2000={:.1}, timeline_len={}\n", + recovered_times.j2000.value(), + recovered_times.timeline.len() + ); + + // ========================================================================= + // 2) Coordinates + // ========================================================================= + println!("2) COORDINATE OBJECTS"); + println!("---------------------"); + + let coords = CoordinateBundle { + geo_icrs_cart: + cartesian::Position::::new( + 6371.0, 0.0, 0.0, + ), + helio_ecl_sph: + spherical::Position::::new_raw( + Degrees::new(5.0), // lat + Degrees::new(120.0), // lon + AstronomicalUnits::new(1.2), + ), + observer_site: + centers::Geodetic::::new( + Degrees::new(-17.8947), // lon + Degrees::new(28.7636), // lat + Meters::new(2396.0), // height + ), + }; + println!("{}", pretty_json(&coords)); + + let recovered_coords: CoordinateBundle = roundtrip(&coords); + println!( + "Roundtrip check: x={:.1} km, lon={:.4} deg\n", + recovered_coords.geo_icrs_cart.x().value(), + recovered_coords.observer_site.lon.value() + ); + + // ========================================================================= + // 3) Body-related objects: orbit + ephemeris snapshots + // ========================================================================= + println!("3) BODY-RELATED OBJECTS"); + println!("-----------------------"); + + // NOTE: + // `Planet`/`Star` structs are not serde-derived in the current API. + // We serialize body-related data that *is* serde-ready: orbit elements + // and concrete coordinate snapshots. + let earth_snapshot = BodySnapshot { + name: "Earth".to_string(), + epoch: jd, + orbit: siderust::bodies::EARTH.orbit, + heliocentric_ecliptic: Earth::vsop87a(jd), + }; + let halley_snapshot = BodySnapshot { + name: HALLEY.name.to_string(), + epoch: jd, + orbit: HALLEY.orbit, + heliocentric_ecliptic: HALLEY.orbit.kepler_position(jd), + }; + + println!("Earth snapshot JSON:"); + println!("{}", pretty_json(&earth_snapshot)); + println!("Halley snapshot JSON:"); + println!("{}", pretty_json(&halley_snapshot)); + + let recovered_halley: BodySnapshot = roundtrip(&halley_snapshot); + println!( + "Roundtrip check: {} @ JD {:.1}, r={:.6} AU\n", + recovered_halley.name, + recovered_halley.epoch.value(), + recovered_halley.heliocentric_ecliptic.distance().value() + ); + + // ========================================================================= + // 4) Target objects (CoordinateWithPM alias) + // ========================================================================= + println!("4) TARGET OBJECTS"); + println!("-----------------"); + + let mars_target = Mars.track(jd); + let moon_target = Target::new_static(Moon.track(jd), jd); + + let targets = BodyTargetsBundle { + mars_bary_target: mars_target, + moon_geo_target: moon_target, + }; + println!("{}", pretty_json(&targets)); + + let recovered_targets: BodyTargetsBundle = roundtrip(&targets); + println!( + "Roundtrip check: Mars target JD {:.1}, Moon target JD {:.1}\n", + recovered_targets.mars_bary_target.time.value(), + recovered_targets.moon_geo_target.time.value() + ); + + // ========================================================================= + // 5) File I/O + // ========================================================================= + println!("5) FILE I/O"); + println!("----------"); + + let out_path = "/tmp/siderust_serde_example_targets.json"; + fs::write(out_path, pretty_json(&targets)).expect("write JSON file"); + let loaded = fs::read_to_string(out_path).expect("read JSON file"); + let _: BodyTargetsBundle = serde_json::from_str(&loaded).expect("deserialize loaded JSON"); + println!("Saved and loaded: {}", out_path); +} diff --git a/examples/12_solar_system_example.cpp b/examples/12_solar_system_example.cpp deleted file mode 100644 index 7efa10a..0000000 --- a/examples/12_solar_system_example.cpp +++ /dev/null @@ -1,138 +0,0 @@ -/** - * @file 12_solar_system_example.cpp - * @brief C++ port of siderust/examples/38_solar_system_example.rs - * - * Computes heliocentric and barycentric positions for solar-system bodies - * using the VSOP87 ephemeris layer exposed by siderust. - * - * Bodies with VSOP87 bindings in C++: Sun, Earth, Mars, Venus, Moon. - * (Mercury/Jupiter/Saturn/Uranus/Neptune not yet bound in C++ FFI.) - * - * Run with: cmake --build build --target 12_solar_system_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main() { - std::cout << "=== Solar System Bodies Positions ===\n\n"; - - const JulianDate jd(2451545.0); - std::cout << "Epoch: JD " << std::fixed << std::setprecision(1) - << jd.value() << " (J2000.0)\n\n"; - - // ------------------------------------------------------------------------- - // Heliocentric positions (EclipticMeanJ2000 frame, AU) - // ------------------------------------------------------------------------- - std::cout << "--- Heliocentric Positions (VSOP87) ---\n"; - - auto print_hel = [](const char *name, auto pos) { - const auto sph = pos.to_spherical(); - const auto dir = sph.direction(); - std::cout << std::left << std::setw(8) << name - << " lon=" << std::right << std::fixed << std::setprecision(3) - << std::setw(10) << dir.longitude().value() << " deg" - << " lat=" << std::setw(8) << dir.latitude().value() << " deg" - << " r=" << std::setw(12) << std::setprecision(6) - << sph.distance().value() << " AU\n"; - }; - - const auto earth_h = ephemeris::earth_heliocentric(jd); - const auto venus_h = ephemeris::venus_heliocentric(jd); - const auto mars_h = ephemeris::mars_heliocentric(jd); - - print_hel("Earth", earth_h); - print_hel("Venus", venus_h); - print_hel("Mars", mars_h); - - std::cout << "\n [NOTE: Mercury, Jupiter, Saturn, Uranus, Neptune heliocentric\n"; - std::cout << " not yet bound in C++ FFI.]\n\n"; - - // ------------------------------------------------------------------------- - // Barycentric positions (Solar System Barycenter origin, AU) - // ------------------------------------------------------------------------- - std::cout << "--- Barycentric Positions ---\n"; - - const auto sun_bc = ephemeris::sun_barycentric(jd); - const auto earth_bc = ephemeris::earth_barycentric(jd); - - auto print_bary = [](const char *name, auto pos) { - const auto sph = pos.to_spherical(); - std::cout << std::left << std::setw(16) << name - << " r=" << std::right << std::fixed << std::setprecision(6) - << sph.distance().value() << " AU\n"; - }; - - print_bary("Sun (SSB)", sun_bc); - print_bary("Earth (SSB)", earth_bc); - - std::cout << "\n [NOTE: mars_barycentric, moon_barycentric not yet bound.]\n\n"; - - // ------------------------------------------------------------------------- - // Moon geocentric (km) - // ------------------------------------------------------------------------- - std::cout << "--- Moon Geocentric Position ---\n"; - const auto moon_g = ephemeris::moon_geocentric(jd); - { - const auto sph = moon_g.to_spherical(); - const auto dir = sph.direction(); - const double r_km = sph.distance().value(); - std::cout << "Moon r=" << std::setprecision(1) << r_km << " km" - << " (" << std::setprecision(6) << r_km / 1.496e8 << " AU)" - << " lon=" << std::setprecision(3) << dir.longitude().value() << " deg" - << " lat=" << dir.latitude().value() << " deg\n\n"; - } - - // ------------------------------------------------------------------------- - // Planet catalog (orbital elements) - // ------------------------------------------------------------------------- - std::cout << "--- Planet Catalog (orbital elements) ---\n"; - - struct PlanetInfo { const char *name; const Planet *planet; }; - const PlanetInfo catalog[] = { - {"Mercury", &MERCURY}, - {"Venus", &VENUS}, - {"Earth", &EARTH}, - {"Mars", &MARS}, - {"Jupiter", &JUPITER}, - {"Saturn", &SATURN}, - {"Uranus", &URANUS}, - {"Neptune", &NEPTUNE}, - }; - - std::cout << std::setw(10) << "Planet" - << std::setw(12) << "a (AU)" - << std::setw(10) << "e" - << std::setw(12) << "r_body (km)\n"; - std::cout << std::string(44, '-') << "\n"; - - for (const auto &p : catalog) { - std::cout << std::left << std::setw(10) << p.name - << std::right << std::fixed - << std::setw(12) << std::setprecision(4) - << p.planet->orbit.semi_major_axis_au - << std::setw(10) << std::setprecision(5) - << p.planet->orbit.eccentricity - << std::setw(12) << std::setprecision(0) - << p.planet->radius_km << "\n"; - } - - // ------------------------------------------------------------------------- - // Earth-Mars distance via heliocentric coordinates - // ------------------------------------------------------------------------- - std::cout << "\n--- Earth-Mars Distance (J2000.0) ---\n"; - const double ex = earth_h.x().value(), ey = earth_h.y().value(), ez = earth_h.z().value(); - const double mx = mars_h.x().value(), my = mars_h.y().value(), mz = mars_h.z().value(); - const double dist_au = std::sqrt((ex-mx)*(ex-mx) + (ey-my)*(ey-my) + (ez-mz)*(ez-mz)); - std::cout << "Distance: " << std::setprecision(4) << dist_au << " AU" - << " (" << std::setprecision(0) << dist_au * 1.496e8 << " km)\n"; - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/13_observer_coordinates.cpp b/examples/13_observer_coordinates.cpp deleted file mode 100644 index 2bd7b59..0000000 --- a/examples/13_observer_coordinates.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/** - * @file 13_observer_coordinates.cpp - * @brief C++ port of siderust/examples/36_observer_coordinates.rs - * - * Shows how an observer's ground coordinates (geodetic) relate to - * geocentric Cartesian coordinates, and how to convert between - * equatorial and horizontal systems for an observation site. - * - * Run with: cmake --build build --target observer_coordinates_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main() { - std::cout << "=== Observer Coordinate Systems ===\n\n"; - - // ------------------------------------------------------------------------- - // Build a few observatories using the Geodetic() factory - // ------------------------------------------------------------------------- - const struct { const char *name; double lon; double lat; double alt_m; } sites[] = { - {"Greenwich", 0.0, 51.4769, 46.0}, - {"Roque de los Muchachos", -17.892, 28.756, 2396.0}, - {"Mauna Kea", -155.472, 19.826, 4207.0}, - {"El Paranal", -70.403, -24.627, 2635.0}, - {"La Silla", -70.730, -29.257, 2400.0}, - }; - - std::cout << "--- Observatory Summary ---\n"; - std::cout << std::setw(28) << "Name" - << std::setw(10) << "Lon(°)" - << std::setw(10) << "Lat(°)" - << std::setw(10) << "Alt(m)\n"; - std::cout << std::string(58, '-') << "\n"; - - for (const auto &s : sites) { - std::cout << std::setw(28) << std::left << s.name - << std::setw(10) << std::right << std::fixed << std::setprecision(3) - << s.lon - << std::setw(10) << s.lat - << std::setw(10) << std::setprecision(0) << s.alt_m << "\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Use built-in observatory constants (pre-defined in siderust.hpp) - // ------------------------------------------------------------------------- - std::cout << "--- Built-in Observatory Constants ---\n"; - const Geodetic obs1 = ROQUE_DE_LOS_MUCHACHOS; - const Geodetic obs2 = MAUNA_KEA; - const Geodetic obs3 = EL_PARANAL; - const Geodetic obs4 = LA_SILLA_OBSERVATORY; - - std::cout << "ROQUE_DE_LOS_MUCHACHOS lat=" << std::setprecision(3) - << obs1.lat.value() << "° lon=" << obs1.lon.value() << "°" - << " alt=" << obs1.height.value() << " m\n"; - std::cout << "MAUNA_KEA lat=" << obs2.lat.value() - << "° lon=" << obs2.lon.value() << "°" - << " alt=" << obs2.height.value() << " m\n"; - std::cout << "EL_PARANAL lat=" << obs3.lat.value() - << "° lon=" << obs3.lon.value() << "°" - << " alt=" << obs3.height.value() << " m\n"; - std::cout << "LA_SILLA_OBSERVATORY lat=" << obs4.lat.value() - << "° lon=" << obs4.lon.value() << "°" - << " alt=" << obs4.height.value() << " m\n\n"; - - // ------------------------------------------------------------------------- - // Frame conversion: ICRS position → Horizontal for selected stars - // ------------------------------------------------------------------------- - std::cout << "--- Star Horizontal Coordinates at Roque de los Muchachos ---\n"; - std::cout << "(Epoch: J2000.0, for rough indicative values)\n\n"; - - const JulianDate jd(2451545.0); // J2000.0 - const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; - - // Known J2000 ICRS coordinates for catalog stars - struct StarInfo { const char *name; double ra_deg; double dec_deg; }; - const StarInfo stars[] = { - {"Sirius", 101.2871, -16.7161}, - {"Vega", 279.2348, 38.7837}, - {"Altair", 297.6958, 8.8683}, - {"Polaris", 37.9546, 89.2641}, - {"Betelgeuse", 88.7929, 7.4070}, - }; - - std::cout << std::setw(12) << "Star" - << std::setw(12) << " RA (°)" - << std::setw(12) << "Dec (°)" - << std::setw(12) << "Alt (°)\n"; - std::cout << std::string(48, '-') << "\n"; - - for (const auto &s : stars) { - const spherical::direction::ICRS dir(qtty::Degree(s.ra_deg), qtty::Degree(s.dec_deg)); - const auto h_dir = dir.to_horizontal(jd, observer); - std::cout << std::left << std::setw(12) << s.name - << std::right << std::setw(12) << std::setprecision(3) << std::fixed - << s.ra_deg - << std::setw(12) << s.dec_deg - << std::setw(12) << h_dir.altitude().value() << "\n"; - } - - // ------------------------------------------------------------------------- - // Topocentric vs geocentric: a brief note and illustration - // ------------------------------------------------------------------------- - std::cout << "\n--- Topocentric Parallax (Moon) ---\n"; - const auto moon_geo = ephemeris::moon_geocentric(jd); - { - const auto moon_sph = moon_geo.to_spherical(); - std::cout << "Moon geocentric: r=" << std::setprecision(6) - << moon_sph.distance().value() << " AU\n"; - } - // Topocentric shift is computed internally by the altitude/frame routines; - // this example illustrates that raw ephemeris gives geocentric positions. - std::cout << "(Altitude routines apply topocentric correction automatically)\n"; - - // ------------------------------------------------------------------------- - // Summary: compare observers at different latitudes - // ------------------------------------------------------------------------- - std::cout << "\n--- Sirius Visibility by Latitude ---\n"; - std::cout << " (hours above horizon over one year via altitude periods API)\n\n"; - - const Period full_year(MJD(60000.0), MJD(60365.0)); - struct LatSite { const char *name; Geodetic obs; }; - const LatSite lat_sites[] = { - {"Greenwich (+51.5°)", Geodetic(0.0, 51.48, 0.0)}, - {"Roque (+28.8°)", ROQUE_DE_LOS_MUCHACHOS}, - {"Paranal (-24.6°)", EL_PARANAL}, - }; - - for (const auto &ls : lat_sites) { - const auto periods = star_altitude::above_threshold(SIRIUS, ls.obs, full_year, - qtty::Degree(0.0)); - double total_h = 0.0; - for (const auto &p : periods) - total_h += p.duration().value(); - std::cout << " " << std::left << std::setw(22) << ls.name - << "Sirius above horizon: " << std::setprecision(0) << total_h << " h/yr\n"; - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/14_bodycentric_coordinates.cpp b/examples/14_bodycentric_coordinates.cpp deleted file mode 100644 index 0a8065c..0000000 --- a/examples/14_bodycentric_coordinates.cpp +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @file 14_bodycentric_coordinates.cpp - * @brief C++ port of siderust/examples/27_bodycentric_coordinates.rs - * - * Shows how to project positions into a body-centered reference frame using - * `to_bodycentric()` and `BodycentricParams`. Useful for describing spacecraft - * or Moon positions as seen from an orbiter. - * - * Run with: cmake --build build --target bodycentric_coordinates_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; -using namespace siderust::frames; -using namespace siderust::centers; - -int main() { - std::cout << "=== Body-Centric Coordinate Transformations ===\n\n"; - - const JulianDate jd = JulianDate::J2000(); - std::cout << "Epoch: J2000.0 (JD " << std::fixed << std::setprecision(1) - << jd.value() << ")\n\n"; - - // ------------------------------------------------------------------------- - // Example 1: Moon as seen from a fictitious ISS-like orbit - // ------------------------------------------------------------------------- - std::cout << "--- Example 1: Moon from a Low-Earth Orbit ---\n"; - - // ISS-like (circular low-Earth, ~400 km altitude, ~51.6° inclination) - // a ≈ (6371 + 400) km ≈ 0.0000440 AU, e≈0, i≈51.6° - const Orbit iss_orbit{ - 0.0000440, // semi-major axis (AU) - 0.0001, // eccentricity - 51.6, // inclination (°) - 0.0, // RAAN (°) - 0.0, // argument of periapsis (°) - 0.0, // mean anomaly at epoch (°) - jd.value() // epoch (JD) - }; - const BodycentricParams iss_params = BodycentricParams::geocentric(iss_orbit); - - // Moon's approximate geocentric position in Ecliptic J2000 - // (rough: ~0.00257 AU, ~5° ecliptic latitude) - const cartesian::Position - moon_geo_pos(0.00257, 0.00034, 0.0); - - const auto moon_from_iss = to_bodycentric(moon_geo_pos, iss_params, jd); - - std::cout << "Moon from ISS (bodycentric, EclipticJ2000) [AU]:\n"; - std::cout << " x=" << std::setprecision(6) - << moon_from_iss.pos.x().value() - << " y=" << moon_from_iss.pos.y().value() - << " z=" << moon_from_iss.pos.z().value() << "\n"; - - const double dist_au = std::sqrt( - moon_from_iss.pos.x().value() * moon_from_iss.pos.x().value() + - moon_from_iss.pos.y().value() * moon_from_iss.pos.y().value() + - moon_from_iss.pos.z().value() * moon_from_iss.pos.z().value()); - std::cout << " distance ≈ " << std::setprecision(6) << dist_au << " AU (" - << std::setprecision(0) << dist_au * 1.496e8 << " km)\n\n"; - - // Round-trip back to geocentric - const auto recovered = moon_from_iss.to_geocentric(jd); - std::cout << "Round-trip to geocentric [AU]:\n"; - std::cout << " x=" << std::setprecision(6) << recovered.x().value() - << " y=" << recovered.y().value() - << " z=" << recovered.z().value() << "\n"; - - const double err = std::sqrt( - std::pow(recovered.x().value() - moon_geo_pos.x().value(), 2) + - std::pow(recovered.y().value() - moon_geo_pos.y().value(), 2) + - std::pow(recovered.z().value() - moon_geo_pos.z().value(), 2)); - std::cout << " round-trip error = " << std::setprecision(2) << std::scientific - << err << " AU\n\n"; - - // ------------------------------------------------------------------------- - // Example 2: Mars Phobos-like orbit - // ------------------------------------------------------------------------- - std::cout << "--- Example 2: Position Relative to Mars (Phobos-like Orbit) ---\n"; - - // Phobos: a ≈ 9376 km ≈ 0.0000627 AU, e≈0.015, i≈1.1° - const Orbit phobos_orbit{ - 0.0000627, // semi-major axis (AU) - 0.015, // eccentricity - 1.1, // inclination (°) - 0.0, // RAAN (°) - 0.0, // argument of periapsis (°) - 5.0, // mean anomaly at epoch (°) - jd.value() // epoch (JD) - }; - const BodycentricParams phobos_params = BodycentricParams::heliocentric(phobos_orbit); - - // Mars heliocentric position at J2000.0 (approximate) - const auto mars_hel = ephemeris::mars_heliocentric(jd); - - const auto phobos_from_mars = to_bodycentric(mars_hel, phobos_params, jd); - - std::cout << "Mars heliocentric position [AU]:" - << " r=" << std::setprecision(3) - << mars_hel.to_spherical().distance().value() << "\n"; - std::cout << "Phobos bodycentric (relative to Mars) [AU]:\n"; - std::cout << " x=" << std::setprecision(8) << phobos_from_mars.pos.x().value() - << " y=" << phobos_from_mars.pos.y().value() - << " z=" << phobos_from_mars.pos.z().value() << "\n"; - - // ------------------------------------------------------------------------- - // Example 3: Geocentric orbit (circular, equatorial) - // ------------------------------------------------------------------------- - std::cout << "\n--- Example 3: GEO Satellite ---\n"; - - const Orbit geo_orbit{ - 0.000284, // ~42164 km ≈ 0.000282 AU - 0.0001, // nearly circular - 0.1, // near-equatorial - 120.0, // RAAN - 0.0, // arg periapsis - 0.0, // mean anomaly - jd.value() - }; - const BodycentricParams geo_params = BodycentricParams::geocentric(geo_orbit); - - // Sun geocentric approximate position - const auto sun_geo_approx = - cartesian::Position( - -1.0, 0.0, 0.0); // rough - - const auto sun_from_geo = to_bodycentric(sun_geo_approx, geo_params, jd); - const double sun_dist_au = std::sqrt( - sun_from_geo.pos.x().value() * sun_from_geo.pos.x().value() + - sun_from_geo.pos.y().value() * sun_from_geo.pos.y().value() + - sun_from_geo.pos.z().value() * sun_from_geo.pos.z().value()); - - std::cout << "Sun as seen from GEO satellite [AU]: r=" << std::setprecision(4) - << sun_dist_au << "\n"; - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/15_targets_proper_motion.cpp b/examples/15_targets_proper_motion.cpp deleted file mode 100644 index 499ff69..0000000 --- a/examples/15_targets_proper_motion.cpp +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @file 15_targets_proper_motion.cpp - * @brief C++ port of siderust/examples/40_targets_proper_motion.rs - * - * Demonstrates proper motion propagation for catalog stars. In Rust the - * `CoordinateWithPM` type propagates RA/Dec/proper-motion from a reference - * epoch. This binding is **not yet available** in C++; a placeholder section - * clearly marks that area, while the surrounding coordinate arithmetic is - * fully implemented. - * - * Run with: cmake --build build --target targets_proper_motion_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; -using namespace siderust::frames; - -// TODO: [PLACEHOLDER] CoordinateWithPM / set_proper_motion_since_j2000 not -// yet bound in C++. The Rust API is: -// let star_pm = star.set_proper_motion_since_j2000(target_jd); -// These will be added once the FFI layer for proper motion propagation is -// implemented. - -int main() { - std::cout << "=== Targets with Proper Motion ===\n\n"; - - // ------------------------------------------------------------------------- - // Part 1: Catalog star directions (static, no proper motion — fully bound) - // ------------------------------------------------------------------------- - std::cout << "--- Part 1: Catalog Stars — Current ICRS Directions ---\n\n"; - - struct CatalogEntry { - const char *name; - double ra_deg; // J2000 ICRS right ascension (degrees) - double dec_deg; // J2000 ICRS declination (degrees) - double pmRA_mas; // proper motion in RA (mas/yr) - double pmDec_mas; // proper motion in Dec (mas/yr) - }; - - // J2000 ICRS coords and proper motions (Hipparcos/Gaia) - const CatalogEntry entries[] = { - {"Sirius", 101.2871, -16.7161, -546.0, -1223.0}, - {"Vega", 279.2348, 38.7837, +200.9, +287.5}, - {"Polaris", 37.9546, 89.2641, +44.2, -11.7}, - {"Altair", 297.6958, 8.8683, +536.8, +385.3}, - {"Arcturus", 213.9153, 19.1822, -1093.4, -1999.4}, - {"Betelgeuse", 88.7929, 7.4070, +27.3, +10.9}, - {"Rigel", 78.6345, -8.2016, +1.3, -0.1}, - {"Aldebaran", 68.9801, 16.5093, +63.5, -189.9}, - }; - - std::cout << std::setw(12) << "Star" - << std::setw(12) << "RA (°)" - << std::setw(12) << "Dec (°)" - << std::setw(16) << "pmRA (mas/yr)" - << std::setw(16) << "pmDec (mas/yr)" - << "\n"; - std::cout << std::string(68, '-') << "\n"; - - for (const auto &e : entries) { - std::cout << std::left << std::setw(12) << e.name - << std::right << std::setw(12) << std::fixed << std::setprecision(4) - << e.ra_deg - << std::setw(12) << e.dec_deg - << std::setw(16) << std::setprecision(1) << e.pmRA_mas - << std::setw(16) << e.pmDec_mas << "\n"; - } - - // ------------------------------------------------------------------------- - // Part 2: Manual proper-motion propagation (simplified linear model) - // This is what the Rust CoordinateWithPM does internally. - // ------------------------------------------------------------------------- - std::cout << "\n--- Part 2: Manual Linear Proper Motion Propagation ---\n"; - std::cout << "(Simplified — no parallax correction. Rust uses a full ICRS model.)\n\n"; - - const double j2000_jd = 2451545.0; - const double target_jd = 2451545.0 + 100.0 * 365.25; // J2100.0 - const double years = (target_jd - j2000_jd) / 365.25; - - std::cout << "Propagating from J2000.0 to J2100.0 (" << std::setprecision(1) - << years << " years)\n\n"; - - for (const auto &e : entries) { - const double ra0 = e.ra_deg; // J2000 RA in degrees - const double dec0 = e.dec_deg; // J2000 Dec in degrees - - // Convert mas/yr → deg/yr - const double pm_ra_deg = e.pmRA_mas / (3.6e6 * std::cos(dec0 * M_PI / 180.0)); - const double pm_dec_deg = e.pmDec_mas / 3.6e6; - - const double ra1 = ra0 + pm_ra_deg * years; - const double dec1 = dec0 + pm_dec_deg * years; - - // Angular offset in arcseconds - const double dra = (ra1 - ra0) * 3600.0; - const double ddec = (dec1 - dec0) * 3600.0; - const double separation_arcsec = std::sqrt(dra * dra + ddec * ddec); - - std::cout << std::left << std::setw(12) << e.name - << " RA: " << std::right << std::setw(10) << std::setprecision(4) - << ra0 << " → " << std::setw(10) << ra1 - << "° shift=" << std::setprecision(2) << separation_arcsec << " arcsec\n"; - } - - // ------------------------------------------------------------------------- - // Part 3: PLACEHOLDER — CoordinateWithPM - // ------------------------------------------------------------------------- - std::cout << "\n--- Part 3: [PLACEHOLDER] CoordinateWithPM API ---\n"; - std::cout << "NOTE: The following Rust capability is not yet bound in C++:\n"; - std::cout << " // Rust:\n"; - std::cout << " let star = siderust::catalog::SIRIUS;\n"; - std::cout << " let jd_target = Julian::J2100;\n"; - std::cout << " let pos_2100 = star.set_proper_motion_since_j2000(jd_target);\n"; - std::cout << " // Returns CoordinateWithPM — ICRS direction with full\n"; - std::cout << " // rigorous proper-motion propagation (including parallax).\n"; - std::cout << "\n // C++ equivalent (future API):\n"; - std::cout << " // auto pos_2100 = SIRIUS.propagate_proper_motion(JulianDate_J2100);\n"; - std::cout << "\nUsing manual linear propagation above as approximation.\n"; - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/16_jpl_precise_ephemeris.cpp b/examples/16_jpl_precise_ephemeris.cpp deleted file mode 100644 index 3547670..0000000 --- a/examples/16_jpl_precise_ephemeris.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @file 16_jpl_precise_ephemeris.cpp - * @brief C++ port of siderust/examples/32_jpl_precise_ephemeris.rs - * - * [PLACEHOLDER] — JPL DE430/DE440 high-precision ephemeris is **not yet - * bound** in the C++ wrapper. The Rust implementation uses a run-time - * loadable ephemeris file; the C++ FFI layer currently only exposes the - * VSOP87 analytical series. - * - * This file documents what the API will look like and shows the VSOP87 - * baseline for comparison. - * - * Run with: cmake --build build --target jpl_precise_ephemeris_example - */ - -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -int main() { - std::cout << "=== JPL Precise Ephemeris [PLACEHOLDER] ===\n\n"; - - // ------------------------------------------------------------------------- - // PLACEHOLDER: JPL DE440 ephemeris loading - // ------------------------------------------------------------------------- - std::cout << "NOTE: JPL DE440/DE441 ephemeris not yet available in C++ bindings.\n"; - std::cout << " // Rust:\n"; - std::cout << " // use siderust::ephemeris::jpl::*;\n"; - std::cout << " // let ephem = JplEphemeris::load(\"de440.bsp\");\n"; - std::cout << " // let pos = ephem.mars_heliocentric(jd);\n"; - std::cout << "\n // C++ (future API):\n"; - std::cout << " // auto ephem = jpl::load(\"de440.bsp\");\n"; - std::cout << " // auto pos = ephem.mars_heliocentric(jd);\n\n"; - - // ------------------------------------------------------------------------- - // Fallback: VSOP87 comparison (fully available today) - // ------------------------------------------------------------------------- - std::cout << "--- Baseline: VSOP87 Analytical Ephemeris (available now) ---\n\n"; - - const JulianDate jd = JulianDate::J2000(); - std::cout << "Epoch: J2000.0 (JD " << std::fixed << std::setprecision(1) - << jd.value() << ")\n\n"; - - const auto earth_h = ephemeris::earth_heliocentric(jd); - const auto mars_h = ephemeris::mars_heliocentric(jd); - const auto venus_h = ephemeris::venus_heliocentric(jd); - const auto moon_g = ephemeris::moon_geocentric(jd); - - auto print_body = [](const char *name, auto pos) { - const auto sph = pos.to_spherical(); - std::cout << std::left << std::setw(8) << name - << " r=" << std::right << std::setprecision(6) - << std::fixed << sph.distance().value() << " AU" - << " lon=" << std::setw(10) << std::setprecision(4) - << sph.direction().longitude().value() << "°" - << " lat=" << sph.direction().latitude().value() << "°\n"; - }; - - std::cout << "Heliocentric positions (VSOP87):\n"; - print_body("Earth", earth_h); - print_body("Venus", venus_h); - print_body("Mars", mars_h); - - std::cout << "\nGeocentric Moon (VSOP87-based):\n"; - { - const auto sph = moon_g.to_spherical(); - std::cout << " Moon r=" << std::setprecision(6) << sph.distance().value() - << " AU lon=" << std::setprecision(4) << sph.direction().longitude().value() - << "° lat=" << sph.direction().latitude().value() << "°\n"; - } - - // ------------------------------------------------------------------------- - // Expected accuracy note (once JPL is available) - // ------------------------------------------------------------------------- - std::cout << "\n--- Expected accuracy improvement with DE440 ---\n"; - std::cout << " VSOP87 typical error: ~1 arcsecond (inner planets)\n"; - std::cout << " DE440 typical error: ~0.001 arcsecond\n"; - std::cout << " (Factor ~1000 improvement, relevant for high-precision astrometry)\n"; - - std::cout << "\nImplementation status: VSOP87 fully available, DE440 — TODO.\n"; - std::cout << "\n=== Example Complete (Placeholder) ===\n"; - return 0; -} diff --git a/examples/17_serde_serialization.cpp b/examples/17_serde_serialization.cpp deleted file mode 100644 index 2d9c295..0000000 --- a/examples/17_serde_serialization.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @file 17_serde_serialization.cpp - * @brief C++ port of siderust/examples/37_serde_serialization.rs - * - * [PLACEHOLDER] — Rust's `serde` serialization/deserialization framework has - * no direct equivalent in the C++ wrapper. JSON round-trips for siderust - * types are **not yet available** in C++. - * - * This file shows how one could serialize the available C++ types manually - * (as a demonstration), and documents what the Rust serde API looks like. - * - * Run with: cmake --build build --target serde_serialization_example - */ - -#include -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -// --------------------------------------------------------------------------- -// Minimal manual JSON helpers (C++ placeholder for serde) -// --------------------------------------------------------------------------- - -static std::string mjd_to_json(const MJD &mjd) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(6) << "{\"mjd\":" << mjd.value() << "}"; - return ss.str(); -} - -static std::string jd_to_json(const JulianDate &jd) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(6) << "{\"jd\":" << jd.value() << "}"; - return ss.str(); -} - -static std::string geodetic_to_json(const Geodetic &g) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(6) - << "{\"lon_deg\":" << g.lon.value() - << ",\"lat_deg\":" << g.lat.value() - << ",\"alt_m\":" << g.height.value() << "}"; - return ss.str(); -} - -static std::string direction_to_json(const spherical::direction::ICRS &d) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(6) - << "{\"ra_deg\":" << d.ra().value() - << ",\"dec_deg\":" << d.dec().value() << "}"; - return ss.str(); -} - -template -static std::string position_to_json(const cartesian::Position &p) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(9) - << "{\"x\":" << p.x().value() - << ",\"y\":" << p.y().value() - << ",\"z\":" << p.z().value() << "}"; - return ss.str(); -} - -int main() { - std::cout << "=== Serialization / Deserialization [PLACEHOLDER] ===\n\n"; - - // ------------------------------------------------------------------------- - // PLACEHOLDER: Rust serde API description - // ------------------------------------------------------------------------- - std::cout << "NOTE: Rust serde JSON support is not yet bound in C++.\n"; - std::cout << " // Rust:\n"; - std::cout << " // use serde_json;\n"; - std::cout << " // let jd: JulianDate = JulianDate::J2000();\n"; - std::cout << " // let json = serde_json::to_string(&jd).unwrap();\n"; - std::cout << " // let back: JulianDate = serde_json::from_str(&json).unwrap();\n"; - std::cout << "\n // C++ (future API — planned with nlohmann/json or similar):\n"; - std::cout << " // auto j = siderust::to_json(jd);\n"; - std::cout << " // auto jd2 = siderust::from_json(j);\n\n"; - - // ------------------------------------------------------------------------- - // Manual serialization demo (available now) - // ------------------------------------------------------------------------- - std::cout << "--- Manual JSON-like Serialization (C++ demo) ---\n\n"; - - const JulianDate jd = JulianDate::J2000(); - const MJD mjd(51544.5); - const Geodetic obs = ROQUE_DE_LOS_MUCHACHOS; - const auto mars_pos = ephemeris::mars_heliocentric(jd); - - std::cout << "JulianDate: " << jd_to_json(jd) << "\n"; - std::cout << "MJD: " << mjd_to_json(mjd) << "\n"; - std::cout << "Geodetic (Roque):\n " << geodetic_to_json(obs) << "\n"; - // Sirius ICRS direction (J2000 coords; Star::direction() not yet bound) - const spherical::direction::ICRS sirius_icrs(qtty::Degree(101.2871), qtty::Degree(-16.7161)); - std::cout << "Sirius ICRS dir:\n " << direction_to_json(sirius_icrs) << "\n"; - std::cout << "Mars heliocentric:\n " << position_to_json(mars_pos) << "\n"; - - // ------------------------------------------------------------------------- - // Round-trip parse demo (manual) - // ------------------------------------------------------------------------- - std::cout << "\n--- Manual Round-Trip Verification ---\n"; - - const double jd_val = 2451545.0; - const JulianDate jd2(jd_val); - std::cout << "Serialized JD=" << jd_val << " → re-parsed JD=" << jd2.value() << " ✓\n"; - - const double lat_val = obs.lat.value(); - const double lon_val = obs.lon.value(); - const double alt_val = obs.height.value(); - const Geodetic obs2 = Geodetic(lon_val, lat_val, alt_val); - std::cout << "Re-parsed Geodetic lat=" << obs2.lat.value() - << "\u00b0, lon=" << obs2.lon.value() << "\u00b0 \u2713\n"; - - std::cout << "\nFull serde support: TODO — needs nlohmann/json or similar.\n"; - std::cout << "\n=== Example Complete (Placeholder) ===\n"; - return 0; -} diff --git a/examples/18_kepler_orbit.cpp b/examples/18_kepler_orbit.cpp deleted file mode 100644 index 236d70a..0000000 --- a/examples/18_kepler_orbit.cpp +++ /dev/null @@ -1,166 +0,0 @@ -/** - * @file 18_kepler_orbit.cpp - * @brief C++ port of siderust/examples/33_kepler_orbit.rs - * - * Demonstrates Keplerian orbit propagation. The `Orbit` struct is fully - * available in C++; the `kepler_position()` / `solve_keplers_equation()` - * free functions are **not yet bound** (placeholder section below). - * - * Run with: cmake --build build --target kepler_orbit_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; -using namespace siderust::frames; - -// TODO: [PLACEHOLDER] solve_keplers_equation() and kepler_position() not yet -// exposed in the C++ FFI wrapper. The Rust API is: -// let pos = kepler_position(&orbit, jd); -// The `BodycentricParams` constructor uses them internally via to_bodycentric(). - -// Simple manual Kepler solver for demonstration (borrowed from celestial mechanics) -namespace demo { - -/// Solve Kepler's equation M = E - e*sin(E) via Newton-Raphson. -static double solve_kepler(double M_rad, double e, int max_iter = 100) { - double E = M_rad; // initial guess - for (int i = 0; i < max_iter; ++i) { - const double dE = (M_rad - E + e * std::sin(E)) / (1.0 - e * std::cos(E)); - E += dE; - if (std::abs(dE) < 1e-12) break; - } - return E; -} - -/// Propagate two-body orbit: returns heliocentric (x, y, 0) in AU at epoch+dt_days. -static std::pair -propagate_orbit(const Orbit &orb, double dt_days) { - // Mean motion (rad/day) = 2π / T, T = a^1.5 years * 365.25 - const double T_days = std::pow(orb.semi_major_axis_au, 1.5) * 365.25; - const double n = 2.0 * M_PI / T_days; - - const double M = orb.mean_anomaly_deg * M_PI / 180.0 + n * dt_days; - const double E = solve_kepler(std::fmod(M, 2.0 * M_PI), orb.eccentricity); - - const double nu = 2.0 * std::atan2(std::sqrt(1.0 + orb.eccentricity) * std::sin(E / 2.0), - std::sqrt(1.0 - orb.eccentricity) * std::cos(E / 2.0)); - - const double r = orb.semi_major_axis_au * (1.0 - orb.eccentricity * std::cos(E)); - const double om = orb.arg_perihelion_deg * M_PI / 180.0; - const double x = r * std::cos(nu + om); - const double y = r * std::sin(nu + om); - return {x, y}; -} - -} // namespace demo - -int main() { - std::cout << "=== Kepler Orbit Propagation ===\n\n"; - - // ------------------------------------------------------------------------- - // PLACEHOLDER note - // ------------------------------------------------------------------------- - std::cout << "[PLACEHOLDER] Rust's kepler_position() is not yet bound in C++.\n"; - std::cout << "Using a manual Newton-Raphson Kepler solver for demonstration.\n\n"; - - // ------------------------------------------------------------------------- - // Earth's orbit elements (J2000.0) - // ------------------------------------------------------------------------- - const JulianDate jd0 = JulianDate::J2000(); - - const Orbit earth_orbit{ - 1.0000010178, // semi-major axis (AU) - 0.0167086, // eccentricity - 0.0000001, // inclination (°) - 0.0, // RAAN (°) - 102.9373481, // argument of periapsis (°) - 100.4645717, // mean anomaly at epoch (°) - jd0.value() // epoch JD - }; - - // ------------------------------------------------------------------------- - // Part 1: Display orbital elements - // ------------------------------------------------------------------------- - std::cout << "--- Earth's Keplerian Elements (J2000.0) ---\n"; - std::cout << " semi-major axis a = " << std::setprecision(7) - << earth_orbit.semi_major_axis_au << " AU\n"; - std::cout << " eccentricity e = " << earth_orbit.eccentricity << "\n"; - std::cout << " inclination i = " << earth_orbit.inclination_deg << "°\n"; - std::cout << " arg periapsis ω = " << earth_orbit.arg_perihelion_deg << "°\n"; - std::cout << " mean anomaly M₀ = " << earth_orbit.mean_anomaly_deg << "°\n"; - std::cout << " epoch = JD " << std::setprecision(1) << earth_orbit.epoch_jd << "\n\n"; - - // ------------------------------------------------------------------------- - // Part 2: Propagate and compare with VSOP87 - // ------------------------------------------------------------------------- - std::cout << "--- Propagated Position vs VSOP87 (first 5 years) ---\n\n"; - std::cout << std::setw(10) << "Days" << std::setw(12) << "x_kepl(AU)" - << std::setw(12) << "y_kepl(AU)" << std::setw(12) << "r_kepl(AU)" - << std::setw(12) << "r_VSOP(AU)" << "\n"; - std::cout << std::string(58, '-') << "\n"; - - for (int d : {0, 91, 182, 273, 365, 730, 1461}) { - const auto [xk, yk] = demo::propagate_orbit(earth_orbit, d); - const double rk = std::sqrt(xk * xk + yk * yk); - - const JulianDate jd_t(jd0.value() + d); - const auto vsop = ephemeris::earth_heliocentric(jd_t); - const double rv = vsop.to_spherical().distance().value(); - - std::cout << std::setw(10) << d - << std::setw(12) << std::setprecision(6) << std::fixed << xk - << std::setw(12) << yk - << std::setw(12) << rk - << std::setw(12) << rv << "\n"; - } - - // ------------------------------------------------------------------------- - // Part 3: Mars orbit propagation - // ------------------------------------------------------------------------- - std::cout << "\n--- Mars Orbit (2-year trace, Kepler propagator) ---\n"; - - const Orbit mars_orbit{ - 1.523679342, // a (AU) - 0.0934005, // e - 1.849691, // i (°) - 49.4785, // RAAN (°) - 286.502, // ω (°) - 19.37215, // M₀ (°) - jd0.value() - }; - - for (int d : {0, 182, 365, 548, 730}) { - const auto [xm, ym] = demo::propagate_orbit(mars_orbit, d); - const double rm = std::sqrt(xm * xm + ym * ym); - const double lon_m = std::atan2(ym, xm) * 180.0 / M_PI; - if (lon_m < 0) {} // suppress unused warning - std::cout << " day=" << std::setw(4) << d - << " x=" << std::setw(10) << std::setprecision(5) << xm - << " y=" << std::setw(10) << ym - << " r=" << std::setprecision(4) << rm << " AU\n"; - } - - // ------------------------------------------------------------------------- - // Part 4: to_bodycentric round-trip (uses kepler internally) - // ------------------------------------------------------------------------- - std::cout << "\n--- to_bodycentric() uses Kepler solver internally ---\n"; - using namespace siderust::centers; - const BodycentricParams mars_params = BodycentricParams::heliocentric(mars_orbit); - const auto mars_hel = ephemeris::mars_heliocentric(jd0); - - const auto mars_bc_rel = to_bodycentric(mars_hel, mars_params, jd0); - std::cout << "Mars relative to itself (should be ~0):\n"; - std::cout << " x=" << std::scientific << std::setprecision(2) - << mars_bc_rel.pos.x().value() - << " y=" << mars_bc_rel.pos.y().value() - << " z=" << mars_bc_rel.pos.z().value() << "\n"; - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/19_brent_root_finding.cpp b/examples/19_brent_root_finding.cpp deleted file mode 100644 index 6c35ffd..0000000 --- a/examples/19_brent_root_finding.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/** - * @file 19_brent_root_finding.cpp - * @brief C++ port of siderust/examples/28_brent_root_finding.rs - * - * [PLACEHOLDER] — The siderust Brent root-finding utility (`brent_find_root`) - * is **not yet exposed** in the C++ FFI wrapper. The surrounding altitude - * search infrastructure (used internally by `above_threshold` et al.) is - * fully available. - * - * This file demonstrates the concept with a simple manual Brent implementation - * and documents what the Rust API looks like. - * - * Run with: cmake --build build --target brent_root_finding_example - */ - -#include -#include -#include -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -// TODO: [PLACEHOLDER] siderust::brent_find_root() not yet bound in C++. -// Rust API: -// use siderust::math::brent_find_root; -// let root = brent_find_root(a, b, tol, |x| f(x)); - -// --------------------------------------------------------------------------- -// Manual Brent's method (ISO 9222 variant, fully adequate as placeholder) -// --------------------------------------------------------------------------- -static double brent_find_root(double a, double b, double tol, - const std::function &f) { - double fa = f(a), fb = f(b); - if (fa * fb > 0.0) - throw std::runtime_error("brent: f(a) and f(b) must have opposite signs"); - - double c = a, fc = fa, s, d = 0.0; - bool mflag = true; - - for (int iter = 0; iter < 200; ++iter) { - if (std::abs(b - a) < tol) break; - - if (fa != fc && fb != fc) { - // Inverse quadratic interpolation - s = a * fb * fc / ((fa - fb) * (fa - fc)) + - b * fa * fc / ((fb - fa) * (fb - fc)) + - c * fa * fb / ((fc - fa) * (fc - fb)); - } else { - // Secant method - s = b - fb * (b - a) / (fb - fa); - } - - const double delta = std::abs(2.0 * tol * std::abs(b)); - const bool cond1 = (s < (3.0 * a + b) / 4.0 || s > b); - const bool cond2 = (mflag && std::abs(s - b) >= std::abs(b - c) / 2.0); - const bool cond3 = (!mflag && std::abs(s - b) >= std::abs(c - d) / 2.0); - const bool cond4 = (mflag && std::abs(b - c) < delta); - const bool cond5 = (!mflag && std::abs(c - d) < delta); - - if (cond1 || cond2 || cond3 || cond4 || cond5) { - s = (a + b) / 2.0; - mflag = true; - } else { - mflag = false; - } - - double fs = f(s); - d = c; - c = b; - fc = fb; - - if (fa * fs < 0.0) { b = s; fb = fs; } - else { a = s; fa = fs; } - - if (std::abs(fa) < std::abs(fb)) { - std::swap(a, b); std::swap(fa, fb); - } - } - return b; -} - -int main() { - std::cout << "=== Brent Root Finding [PLACEHOLDER for native siderust API] ===\n\n"; - - // ------------------------------------------------------------------------- - // PLACEHOLDER: Rust Brent API - // ------------------------------------------------------------------------- - std::cout << "NOTE: siderust::math::brent_find_root() not yet bound in C++.\n"; - std::cout << " // Rust:\n"; - std::cout << " // let root = brent_find_root(0.0, 3.0, 1e-10, |x| x.sin() - 0.5);\n"; - std::cout << " // // → π/6 ≈ 0.5235988...\n"; - std::cout << "\n // C++ (future API):\n"; - std::cout << " // double root = siderust::math::brent(0.0, 3.0, 1e-10,\n"; - std::cout << " // [](double x){ return std::sin(x) - 0.5; });\n\n"; - - // ------------------------------------------------------------------------- - // Example 1: Simple trigonometric equation - // ------------------------------------------------------------------------- - std::cout << "--- Example 1: sin(x) = 0.5 on [0, π/2] ---\n"; - const double root1 = brent_find_root(0.0, M_PI / 2.0, 1e-12, - [](double x) { return std::sin(x) - 0.5; }); - std::cout << " root = " << std::setprecision(12) << root1 << "\n"; - std::cout << " π/6 = " << M_PI / 6.0 << "\n"; - std::cout << " |error| = " << std::scientific << std::abs(root1 - M_PI / 6.0) << "\n\n"; - - // ------------------------------------------------------------------------- - // Example 2: Kepler's equation E - e*sin(E) = M - // ------------------------------------------------------------------------- - std::cout << "--- Example 2: Kepler's equation M = E - e·sin(E) ---\n"; - const double e = 0.0934005; // Mars eccentricity - const double M = 1.0; // mean anomaly (rad) - - const double E = brent_find_root(0.0, 2.0 * M_PI, 1e-12, - [e, M](double E) { return E - e * std::sin(E) - M; }); - - std::cout << " e=" << e << ", M=" << M << " rad\n"; - std::cout << " Eccentric anomaly E = " << std::setprecision(10) << E << " rad\n"; - std::cout << " Check: E - e·sin(E) = " - << E - e * std::sin(E) << " (≈M=" << M << ")\n\n"; - - // ------------------------------------------------------------------------- - // Example 3: Find Sun's altitude crossing time (siderust use-case) - // ------------------------------------------------------------------------- - std::cout << "--- Example 3: Sun Crossing -18° (astronomical twilight) ---\n"; - std::cout << "(This is what siderust's sun::below_threshold uses internally)\n\n"; - - const Geodetic obs = ROQUE_DE_LOS_MUCHACHOS; - const BodyTarget sun_target(Body::Sun); - - // Search for the exact MJD when Sun crosses -18° on MJD 60000 - const MJD mjd_search_start(59999.5); - const MJD mjd_search_end(60000.5); - - // Sample to bracket - double best_a = -1.0, best_b = -1.0; - const double threshold = -18.0; - for (int i = 0; i < 100; ++i) { - const double t1 = mjd_search_start.value() + i * 0.01; - const double t2 = t1 + 0.01; - const double alt1 = sun_target.altitude_at(obs, MJD(t1)).value() - threshold; - const double alt2 = sun_target.altitude_at(obs, MJD(t2)).value() - threshold; - if (alt1 * alt2 < 0.0) { - best_a = t1; - best_b = t2; - break; - } - } - - if (best_a > 0.0) { - const double crossing_mjd = brent_find_root(best_a, best_b, 1e-9, [&](double t) { - return sun_target.altitude_at(obs, MJD(t)).value() - threshold; - }); - std::cout << " Sun crosses -18° at MJD = " << std::fixed << std::setprecision(6) - << crossing_mjd << "\n"; - std::cout << " Verify: alt at crossing = " - << std::setprecision(4) << sun_target.altitude_at(obs, MJD(crossing_mjd)).value() - << "° (≈ -18°)\n"; - } else { - std::cout << " No crossing found in search window.\n"; - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/20_moon_phase.cpp b/examples/20_moon_phase.cpp deleted file mode 100644 index 259dcca..0000000 --- a/examples/20_moon_phase.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/** - * @file 20_moon_phase.cpp - * @brief C++ port of siderust/examples/34_moon_phase.rs - * - * Demonstrates the full lunar-phase API: geocentric/topocentric geometry, - * phase labels, phase-event search, and illumination-above queries. - * - * Run with: cmake --build build --target moon_phase_example - */ - -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -static const char *phase_kind_str(PhaseKind k) { - switch (k) { - case PhaseKind::NewMoon: return "New Moon"; - case PhaseKind::FirstQuarter: return "First Quarter"; - case PhaseKind::FullMoon: return "Full Moon"; - case PhaseKind::LastQuarter: return "Last Quarter"; - default: return "Unknown"; - } -} - -static const char *phase_label_str(MoonPhaseLabel l) { - switch (l) { - case MoonPhaseLabel::NewMoon: return "New Moon"; - case MoonPhaseLabel::WaxingCrescent: return "Waxing Crescent"; - case MoonPhaseLabel::FirstQuarter: return "First Quarter"; - case MoonPhaseLabel::WaxingGibbous: return "Waxing Gibbous"; - case MoonPhaseLabel::FullMoon: return "Full Moon"; - case MoonPhaseLabel::WaningGibbous: return "Waning Gibbous"; - case MoonPhaseLabel::LastQuarter: return "Last Quarter"; - case MoonPhaseLabel::WaningCrescent: return "Waning Crescent"; - default: return "Unknown"; - } -} - -static void print_geometry(const char *label, const MoonPhaseGeometry &g) { - const double phase_angle_deg = g.phase_angle_rad * 180.0 / M_PI; - std::cout << std::left << std::setw(22) << label - << " phase=" << std::right << std::setw(7) << std::fixed - << std::setprecision(2) << phase_angle_deg << "°" - << " illum=" << std::setw(6) << std::setprecision(3) - << g.illuminated_fraction - << " waxing=" << (g.waxing ? "yes" : "no") - << "\n"; -} - -int main() { - std::cout << "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"; - std::cout << "\u2551 Lunar Phase Analysis \u2551\n"; - std::cout << "\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n"; - - // ------------------------------------------------------------------------- - // Part 1: Geocentric phase geometry at J2000.0 - // ------------------------------------------------------------------------- - std::cout << "--- Part 1: Phase Geometry at Key Epochs ---\n\n"; - - // Sample seven epochs spanning two weeks - const JulianDate jd0 = JulianDate::J2000(); - for (int d = 0; d <= 28; d += 4) { - const JulianDate jd(jd0.value() + d); - const auto geo = moon::phase_geocentric(jd); - const auto lbl = moon::phase_label(geo); - char buf[48]; - std::snprintf(buf, sizeof(buf), "JD+%2d days", d); - print_geometry(buf, geo); - std::cout << " → label: " << phase_label_str(lbl) << "\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Part 2: Topocentric phase at Roque de los Muchachos - // ------------------------------------------------------------------------- - std::cout << "--- Part 2: Geocentric vs Topocentric Phase (slight difference) ---\n\n"; - - const Geodetic obs = ROQUE_DE_LOS_MUCHACHOS; - const JulianDate jd_full(2451545.0 + 14.0); // roughly full moon region - - const auto geo_geom = moon::phase_geocentric(jd_full); - const auto topo_geom = moon::phase_topocentric(jd_full, obs); - - print_geometry("Geocentric", geo_geom); - print_geometry("Topocentric", topo_geom); - - const double diff = std::abs(geo_geom.illuminated_fraction - topo_geom.illuminated_fraction); - std::cout << " illumination difference: " << std::scientific << std::setprecision(3) - << diff << "\n\n"; - - // ------------------------------------------------------------------------- - // Part 3: Find all principal phase events in 3 months - // ------------------------------------------------------------------------- - std::cout << "--- Part 3: Phase Events Over 3 Months ---\n\n"; - - const Period quarter(MJD(60000.0), MJD(60090.0)); - const auto events = moon::find_phase_events(quarter); - - std::cout << "Found " << events.size() << " phase events:\n"; - for (const auto &ev : events) { - std::cout << " MJD=" << std::fixed << std::setprecision(2) << ev.time.value() - << " → " << phase_kind_str(ev.kind) << "\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Part 4: Illumination above threshold (bright moon periods) - // ------------------------------------------------------------------------- - std::cout << "--- Part 4: Illumination ≥ 50% (bright moon periods) ---\n\n"; - - const auto bright_periods = moon::illumination_above(quarter, 0.50); - double total_bright_days = 0.0; - for (const auto &p : bright_periods) { - const double d = p.duration().value(); - total_bright_days += d; - std::cout << " MJD " << std::setprecision(2) << p.start().value() - << " – " << p.end().value() - << " (" << std::setprecision(1) << d << " d)\n"; - } - std::cout << " Total bright days: " << std::setprecision(1) << total_bright_days - << " / 90 d\n\n"; - - // ------------------------------------------------------------------------- - // Part 5: Moon-free (illumination < 5%) for dark-sky scheduling - // ------------------------------------------------------------------------- - std::cout << "--- Part 5: Near-Dark Moon Windows (illumination < 5%) ---\n\n"; - - const auto dark_moon_periods = moon::illumination_above(quarter, 0.01); - // Invert: total window minus illumination above 1% - // (Simplified: just report dark periods count and duration from above search) - const auto very_dark_periods = moon::illumination_above(quarter, 0.05); - std::cout << "Periods with illumination > 5%: " << very_dark_periods.size() << "\n"; - std::cout << "(Complement = dark-moon windows, ideal for deep-sky observing)\n"; - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/21_trackable_demo.cpp b/examples/21_trackable_demo.cpp deleted file mode 100644 index 458ae66..0000000 --- a/examples/21_trackable_demo.cpp +++ /dev/null @@ -1,174 +0,0 @@ -/** - * @file 21_trackable_demo.cpp - * @brief C++ port of siderust/examples/42_trackable_demo.rs - * - * Demonstrates the unified `Target` polymorphism in siderust-cpp. Any mix - * of solar-system bodies, catalog stars, and fixed sky directions can be held - * in a `std::vector>` and queried uniformly via the - * `Target` virtual interface. - * - * Rust counterpart uses the `Trackable` trait; C++ uses virtual dispatch on - * `siderust::Target`. - * - * Run with: cmake --build build --target trackable_demo_example - */ - -#include -#include -#include -#include -#include - -#include - -using namespace siderust; -using namespace qtty::literals; - -// ============================================================================ -// Helpers -// ============================================================================ - -static void print_rising_setting(const Target &target, const Geodetic &obs, - const Period &window) { - const auto events = target.crossings(obs, window, qtty::Degree(0.0)); - for (const auto &ev : events) { - const char *kind = (ev.direction == CrossingDirection::Rising) ? "Rise" : "Set"; - std::cout << " " << kind << " at MJD " - << std::fixed << std::setprecision(4) << ev.time.value() << "\n"; - } -} - -static void print_culminations(const Target &target, const Geodetic &obs, - const Period &window) { - const auto culms = target.culminations(obs, window); - for (const auto &cu : culms) { - std::cout << " Culmination at MJD " - << std::fixed << std::setprecision(4) << cu.time.value() - << " alt=" << std::setprecision(2) << cu.altitude.value() << "°\n"; - } -} - -// ============================================================================ -// Main -// ============================================================================ - -int main() { - std::cout << "=== Trackable Target Polymorphism Demo ===\n\n"; - - // ------------------------------------------------------------------------- - // Build a heterogeneous list of targets - // ------------------------------------------------------------------------- - std::vector> targets; - - // Solar-system bodies - targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(Body::Moon)); - targets.push_back(std::make_unique(Body::Mars)); - targets.push_back(std::make_unique(Body::Jupiter)); - - // Catalog stars - targets.push_back(std::make_unique(SIRIUS)); - targets.push_back(std::make_unique(VEGA)); - targets.push_back(std::make_unique(POLARIS)); - targets.push_back(std::make_unique(BETELGEUSE)); - targets.push_back(std::make_unique(RIGEL)); - targets.push_back(std::make_unique(CANOPUS)); - - std::cout << "Target list (" << targets.size() << " objects):\n"; - for (const auto &t : targets) - std::cout << " • " << t->name() << "\n"; - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Common observation parameters - // ------------------------------------------------------------------------- - const Geodetic obs = EL_PARANAL; - std::cout << "Observatory: ESO Paranal / VLT\n\n"; - - const Period night(MJD(60000.5), MJD(60001.5)); - std::cout << "Observation night: MJD " << std::fixed << std::setprecision(2) - << night.start().value() << " → " << night.end().value() << "\n\n"; - - // ------------------------------------------------------------------------- - // Generic altitude survey via Target interface - // ------------------------------------------------------------------------- - std::cout << "=== Altitude at Transit Middle (MJD 60001.0) ===\n"; - const MJD midpoint(60001.0); - - std::cout << std::left << std::setw(14) << "Target" - << std::right << std::setw(12) << "Altitude (°)" - << " \n"; - std::cout << std::string(28, '-') << "\n"; - - for (const auto &t : targets) { - const double alt = t->altitude_at(obs, midpoint).value(); - std::cout << std::left << std::setw(14) << t->name() - << std::right << std::setw(12) << std::fixed << std::setprecision(2) - << alt << "°\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Above-threshold summary (30°) - // ------------------------------------------------------------------------- - const qtty::Degree threshold(30.0); - std::cout << "=== Time Above " << std::setprecision(0) << threshold.value() - << "° During the Night ===\n\n"; - - for (const auto &t : targets) { - const auto periods = t->above_threshold(obs, night, threshold); - double total_h = 0.0; - for (const auto &p : periods) - total_h += p.duration().value(); - - std::cout << std::left << std::setw(14) << t->name() - << " " << periods.size() << " period(s) " - << std::setprecision(2) << total_h << " h total\n"; - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Rising / setting / culmination events - // ------------------------------------------------------------------------- - std::cout << "=== Rising, Setting & Culmination Events ===\n\n"; - - for (size_t i = 0; i < std::min(targets.size(), size_t(4)); ++i) { - const auto &t = targets[i]; - std::cout << " " << t->name() << ":\n"; - print_rising_setting(*t, obs, night); - print_culminations(*t, obs, night); - } - std::cout << "\n"; - - // ------------------------------------------------------------------------- - // Sort targets by tonight's availability (most hours first) - // ------------------------------------------------------------------------- - struct Availability { - std::string name; - double hours; - }; - - std::vector avail; - for (const auto &t : targets) { - const auto ps = t->above_threshold(obs, night, qtty::Degree(20.0)); - double h = 0.0; - for (const auto &p : ps) - h += p.duration().value(); - avail.push_back({t->name(), h}); - } - - std::sort(avail.begin(), avail.end(), - [](const Availability &a, const Availability &b) { - return a.hours > b.hours; - }); - - std::cout << "=== Tonight's Observing Priority (sorted by hours >20°) ===\n\n"; - for (size_t i = 0; i < avail.size(); ++i) { - std::cout << " " << std::setw(2) << i + 1 << ". " - << std::left << std::setw(14) << avail[i].name - << std::right << std::setprecision(2) << avail[i].hours << " h\n"; - } - - std::cout << "\n=== Example Complete ===\n"; - return 0; -} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 1287282..0000000 --- a/examples/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# C++ Examples - -Build from the repository root: - -```bash -cmake -S . -B build -cmake --build build -``` - -Run selected examples: - -```bash -./build/siderust_demo -./build/coordinates_examples -./build/coordinate_systems_example -./build/solar_system_bodies_example -./build/altitude_events_example -./build/trackable_targets_example -./build/azimuth_lunar_phase_example -``` - -## Files - -- `demo.cpp`: end-to-end extended walkthrough (time, typed coordinates, altitude/azimuth, trackables, ephemeris, lunar phase). -- `coordinates_examples.cpp`: typed coordinate construction and core conversion patterns. -- `coordinate_systems_example.cpp`: frame-tag traits and practical frame/horizontal transforms. -- `solar_system_bodies_example.cpp`: planet catalog constants, body-dispatch API, and ephemeris vectors. -- `altitude_events_example.cpp`: altitude windows/crossings/culminations for Sun, Moon, stars, ICRS directions, and `Target`. -- `trackable_targets_example.cpp`: polymorphic tracking with `Trackable`, `BodyTarget`, `StarTarget`, and `Target`. -- `azimuth_lunar_phase_example.cpp`: azimuth events/ranges plus lunar phase geometry, labels, and phase-event searches. diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp deleted file mode 100644 index f89a497..0000000 --- a/examples/altitude_events_example.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @file altitude_events_example.cpp - * @example altitude_events_example.cpp - * @brief Altitude periods/crossings/culminations for multiple target types. - */ - -#include -#include -#include -#include - -#include - -namespace { - -void print_periods(const std::vector &periods, - std::size_t limit) { - const std::size_t n = std::min(periods.size(), limit); - for (std::size_t i = 0; i < n; ++i) { - const auto &p = periods[i]; - std::cout << " " << (i + 1) << ") " << p.start().to_utc() << " -> " - << p.end().to_utc() << " (" << std::fixed << std::setprecision(2) - << p.duration().value() << " h)\n"; - } -} - -} // namespace - -int main() { - using namespace siderust; - using namespace qtty::literals; - - const Geodetic obs = MAUNA_KEA; - const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD end = start + qtty::Day(2.0); - const Period window(start, end); - - SearchOptions opts; - opts.with_tolerance(1e-9).with_scan_step(1.0 / 1440.0); // ~1 minute scan step - - std::cout << "=== altitude_events_example ===\n"; - std::cout << "Window: " << start.to_utc() << " -> " << end.to_utc() << "\n\n"; - - auto sun_nights = sun::below_threshold(obs, window, -18.0_deg, opts); - std::cout << "Sun below -18 deg (astronomical night): " << sun_nights.size() - << " period(s)\n"; - print_periods(sun_nights, 3); - - auto sun_cross = sun::crossings(obs, window, 0.0_deg, opts); - std::cout << "\nSun horizon crossings: " << sun_cross.size() << "\n"; - if (!sun_cross.empty()) { - const auto &c = sun_cross.front(); - std::cout << " First crossing: " << c.time.to_utc() << " (" << c.direction - << ")\n"; - } - - auto moon_culm = moon::culminations(obs, window, opts); - std::cout << "\nMoon culminations: " << moon_culm.size() << "\n"; - if (!moon_culm.empty()) { - const auto &c = moon_culm.front(); - std::cout << " First culmination: " << c.time.to_utc() - << " kind=" << c.kind << " alt=" << c.altitude << std::endl; - } - - auto vega_periods = - star_altitude::above_threshold(VEGA, obs, window, 30.0_deg, opts); - std::cout << "\nVega above 30 deg: " << vega_periods.size() << " period(s)\n"; - print_periods(vega_periods, 2); - - spherical::direction::ICRS target_dir(279.23473_deg, 38.78369_deg); - auto dir_visible = - icrs_altitude::above_threshold(target_dir, obs, window, 0.0_deg, opts); - std::cout << "\nFixed ICRS direction above horizon: " << dir_visible.size() - << " period(s)\n"; - - ICRSTarget fixed_target{ - spherical::direction::ICRS{279.23473_deg, 38.78369_deg}}; - auto fixed_target_periods = - fixed_target.above_threshold(obs, window, 45.0_deg, opts); - std::cout << "ICRSTarget::above_threshold(45 deg): " - << fixed_target_periods.size() << " period(s)\n"; - - return 0; -} diff --git a/examples/azimuth_lunar_phase_example.cpp b/examples/azimuth_lunar_phase_example.cpp deleted file mode 100644 index d17f6a9..0000000 --- a/examples/azimuth_lunar_phase_example.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @file azimuth_lunar_phase_example.cpp - * @example azimuth_lunar_phase_example.cpp - * @brief Azimuth event search plus lunar phase geometry/events. - */ - -#include -#include -#include -#include - -#include - -namespace {} // namespace - -int main() { - using namespace siderust; - using namespace qtty::literals; - - const Geodetic site = MAUNA_KEA; - const MJD start = MJD::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD end = start + qtty::Day(3.0); - const Period window(start, end); - - std::cout << "=== azimuth_lunar_phase_example ===\n"; - std::cout << "Window UTC: " << start.to_utc() << " -> " << end.to_utc() - << "\n\n"; - - const MJD now = MJD::from_utc({2026, 7, 15, 12, 0, 0}); - std::cout << "Instant azimuth\n"; - std::cout << " Sun : " << sun::azimuth_at(site, now) << std::endl; - std::cout << " Moon : " << moon::azimuth_at(site, now) << std::endl; - std::cout << " Vega : " << star_altitude::azimuth_at(VEGA, site, now) - << std::endl; - - auto sun_cross = sun::azimuth_crossings(site, window, 180.0_deg); - auto sun_ext = sun::azimuth_extrema(site, window); - auto moon_west = moon::in_azimuth_range(site, window, 240.0_deg, 300.0_deg); - - std::cout << "Azimuth events\n"; - std::cout << " Sun crossings at 180 deg: " << sun_cross.size() << "\n"; - std::cout << " Sun azimuth extrema: " << sun_ext.size() << "\n"; - if (!sun_ext.empty()) { - const auto &e = sun_ext.front(); - std::cout << " first extremum " << e.kind << " at " << e.time.to_utc() - << " az=" << e.azimuth << std::endl; - } - std::cout << " Moon in [240,300] deg azimuth: " << moon_west.size() - << " period(s)\n\n"; - - const JulianDate jd_now = now.to_jd(); - auto geo_phase = moon::phase_geocentric(jd_now); - auto topo_phase = moon::phase_topocentric(jd_now, site); - auto topo_label = moon::phase_label(topo_phase); - - auto phase_events = - moon::find_phase_events(Period(start, start + qtty::Day(30.0))); - auto half_lit = moon::illumination_range(window, 0.45, 0.55); - - std::cout << "Lunar phase\n"; - std::cout << std::fixed << std::setprecision(3) - << " Geocentric illuminated fraction: " - << geo_phase.illuminated_fraction << "\n" - << " Topocentric illuminated fraction: " - << topo_phase.illuminated_fraction << " (" << topo_label << ")\n"; - - std::cout << " Principal phase events in next 30 days: " - << phase_events.size() << "\n"; - const std::size_t n = std::min(phase_events.size(), 4); - for (std::size_t i = 0; i < n; ++i) { - const auto &ev = phase_events[i]; - std::cout << " " << ev.time.to_utc() << " -> " << ev.kind << "\n"; - } - - std::cout << " Near-half illumination periods (k in [0.45, 0.55]): " - << half_lit.size() << "\n"; - - return 0; -} diff --git a/examples/bodycentric_coordinates.cpp b/examples/bodycentric_coordinates.cpp deleted file mode 100644 index 9184cf7..0000000 --- a/examples/bodycentric_coordinates.cpp +++ /dev/null @@ -1,210 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -// Copyright (C) 2026 Vallés Puig, Ramon - -/** - * @file bodycentric_coordinates.cpp - * @brief Body-Centric Coordinates Example (mirrors Rust's bodycentric_coordinates.rs) - * - * This example demonstrates body-centric coordinate transforms: viewing - * positions from arbitrary orbiting bodies (satellites, planets, moons). - * - * Key API: - * - `BodycentricParams::geocentric(orbit)` / `::heliocentric(orbit)` — params - * - `to_bodycentric(pos, params, jd)` — transform to body frame - * - `BodycentricPos::to_geocentric(jd)` — inverse transform - * - `kepler_position(orbit, jd)` — Keplerian propagator - */ - -#include - -#include -#include -#include - -using namespace siderust; -using namespace siderust::frames; -using namespace siderust::centers; -using qtty::AstronomicalUnit; - -// --------------------------------------------------------------------------- -// Distances helpers -// --------------------------------------------------------------------------- -static double au_magnitude(const cartesian::Position &p) { - double x = p.x().value(), y = p.y().value(), z = p.z().value(); - return std::sqrt(x * x + y * y + z * z); -} -template -static double magnitude(const BodycentricPos &p) { - double x = p.x().value(), y = p.y().value(), z = p.z().value(); - return std::sqrt(x * x + y * y + z * z); -} -template -static double pos_magnitude(const cartesian::Position &p) { - double x = p.x().value(), y = p.y().value(), z = p.z().value(); - return std::sqrt(x * x + y * y + z * z); -} - -// --------------------------------------------------------------------------- -// main -// --------------------------------------------------------------------------- -int main() { - std::cout << "=== Body-Centric Coordinates Example ===\n\n"; - - // J2000.0 - const JulianDate jd(2451545.0); - std::cout << "Reference time: J2000.0 (JD " << std::fixed - << std::setprecision(1) << jd.value() << ")\n\n"; - - // ========================================================================= - // 1. Satellite-Centric Coordinates (ISS example) - // ========================================================================= - std::cout << "1. SATELLITE-CENTRIC COORDINATES\n"; - std::cout << "--------------------------------\n"; - - // ISS-like orbit: ~6 378 km = 0.0000426 AU above Earth - constexpr double KM_PER_AU = 149'597'870.7; - constexpr double ISS_ALTITUDE_KM = 6'378.0; // Earth radius ≈ altitude - constexpr double ISS_SMA_AU = ISS_ALTITUDE_KM / KM_PER_AU; - - Orbit iss_orbit{ISS_SMA_AU, 0.001, 51.6, 0.0, 0.0, 0.0, jd.value()}; - - BodycentricParams iss_params = BodycentricParams::geocentric(iss_orbit); - - // ISS position (geocentric) via Keplerian propagation - auto iss_pos = kepler_position(iss_orbit, jd); - std::cout << "ISS orbit:\n"; - std::cout << " Semi-major axis : " << std::setprecision(8) << ISS_SMA_AU - << " AU (" << ISS_ALTITUDE_KM << " km)\n"; - std::cout << " Eccentricity : " << iss_orbit.eccentricity << "\n"; - std::cout << " Inclination : " << iss_orbit.inclination_deg << "°\n"; - std::cout << std::setprecision(8); - std::cout << "ISS position (Geocentric EclipticMeanJ2000):\n"; - std::cout << " X = " << iss_pos.x().value() << " AU\n"; - std::cout << " Y = " << iss_pos.y().value() << " AU\n"; - std::cout << " Z = " << iss_pos.z().value() << " AU\n"; - std::cout << " Distance from Earth: " << iss_pos.distance().value() << " AU (" - << iss_pos.distance().value() * KM_PER_AU << " km)\n\n"; - - // Moon's approximate geocentric position (~384 400 km = 0.00257 AU) - cartesian::Position - moon_geo(0.00257, 0.0, 0.0); - std::cout << "Moon position (Geocentric):\n"; - std::cout << " Distance from Earth: " << moon_geo.distance().value() << " AU (" - << moon_geo.distance().value() * KM_PER_AU << " km)\n\n"; - - // Transform to ISS-centric - auto moon_from_iss = to_bodycentric(moon_geo, iss_params, jd); - std::cout << "Moon as seen from ISS:\n"; - std::cout << " X = " << moon_from_iss.x().value() << " AU\n"; - std::cout << " Y = " << moon_from_iss.y().value() << " AU\n"; - std::cout << " Z = " << moon_from_iss.z().value() << " AU\n"; - std::cout << " Distance from ISS: " << moon_from_iss.distance().value() << " AU (" - << moon_from_iss.distance().value() * KM_PER_AU << " km)\n\n"; - - // ========================================================================= - // 2. Mars-Centric Coordinates - // ========================================================================= - std::cout << "2. MARS-CENTRIC COORDINATES\n"; - std::cout << "---------------------------\n"; - - Orbit mars_orbit{1.524, 0.0934, 1.85, 49.56, 286.5, 19.41, jd.value()}; - BodycentricParams mars_params = BodycentricParams::heliocentric(mars_orbit); - - auto earth_helio = ephemeris::earth_heliocentric(jd); - auto mars_helio = ephemeris::mars_heliocentric(jd); - - std::cout << "Earth (Heliocentric): distance from Sun = " - << earth_helio.distance().value() << " AU\n"; - std::cout << "Mars (Heliocentric): distance from Sun = " - << mars_helio.distance().value() << " AU\n\n"; - - auto earth_from_mars = to_bodycentric(earth_helio, mars_params, jd); - std::cout << "Earth as seen from Mars:\n"; - std::cout << " X = " << earth_from_mars.x().value() << " AU\n"; - std::cout << " Y = " << earth_from_mars.y().value() << " AU\n"; - std::cout << " Z = " << earth_from_mars.z().value() << " AU\n"; - std::cout << " Distance from Mars: " << earth_from_mars.distance().value() << " AU\n\n"; - - // ========================================================================= - // 3. Venus-Centric Coordinates - // ========================================================================= - std::cout << "3. VENUS-CENTRIC COORDINATES\n"; - std::cout << "----------------------------\n"; - - Orbit venus_orbit{0.723, 0.0067, 3.39, 76.68, 131.53, 50.42, jd.value()}; - BodycentricParams venus_params = BodycentricParams::heliocentric(venus_orbit); - - auto venus_helio = ephemeris::venus_heliocentric(jd); - std::cout << "Venus (Heliocentric): distance from Sun = " - << venus_helio.distance().value() << " AU\n\n"; - - auto earth_from_venus = to_bodycentric(earth_helio, venus_params, jd); - std::cout << "Earth as seen from Venus:\n"; - std::cout << " Distance: " << earth_from_venus.distance().value() << " AU\n\n"; - - auto mars_from_venus = to_bodycentric(mars_helio, venus_params, jd); - std::cout << "Mars as seen from Venus:\n"; - std::cout << " Distance: " << mars_from_venus.distance().value() << " AU\n\n"; - - // ========================================================================= - // 4. Round-Trip Transformation - // ========================================================================= - std::cout << "4. ROUND-TRIP TRANSFORMATION\n"; - std::cout << "----------------------------\n"; - - // Start from a known geocentric position - cartesian::Position - original_pos(0.001, 0.002, 0.003); - std::cout << "Original position (Geocentric):\n"; - std::cout << " X = " << std::setprecision(12) << original_pos.x().value() - << " AU\n"; - std::cout << " Y = " << original_pos.y().value() << " AU\n"; - std::cout << " Z = " << original_pos.z().value() << " AU\n\n"; - - // To Mars-centric and back to geocentric - auto mars_centric = to_bodycentric(original_pos, mars_params, jd); - std::cout << "Transformed to Mars-centric:\n"; - std::cout << " Distance from Mars: " << std::setprecision(8) - << mars_centric.distance().value() << " AU\n\n"; - - auto recovered = mars_centric.to_geocentric(jd); - std::cout << "Recovered position (Geocentric):\n"; - std::cout << " X = " << std::setprecision(12) << recovered.x().value() - << " AU\n"; - std::cout << " Y = " << recovered.y().value() << " AU\n"; - std::cout << " Z = " << recovered.z().value() << " AU\n\n"; - - double dx = original_pos.x().value() - recovered.x().value(); - double dy = original_pos.y().value() - recovered.y().value(); - double dz = original_pos.z().value() - recovered.z().value(); - double diff = std::sqrt(dx * dx + dy * dy + dz * dz); - std::cout << "Total difference: " << diff - << " AU (should be ~0 within floating-point precision)\n\n"; - - // ========================================================================= - // 5. Directions as Free Vectors - // ========================================================================= - std::cout << "5. DIRECTIONS AS FREE VECTORS\n"; - std::cout << "------------------------------\n"; - - cartesian::Direction star_dir(0.707, 0.0, 0.707); - std::cout << "Star direction (EquatorialMeanJ2000):\n"; - std::cout << " X = " << std::setprecision(3) << star_dir.x << "\n"; - std::cout << " Y = " << star_dir.y << "\n"; - std::cout << " Z = " << star_dir.z << "\n\n"; - - std::cout << "Note: Directions are free vectors — they represent 'which way'\n" - "without reference to any origin. A distant star appears in the\n" - "same direction from Earth or from the ISS.\n\n"; - - // ========================================================================= - std::cout << "=== Example Complete ===\n\n"; - std::cout << "Key Takeaways:\n"; - std::cout << "- Body-centric coordinates work for any orbiting body\n"; - std::cout << "- Satellite-centric: use BodycentricParams::geocentric()\n"; - std::cout << "- Planet-centric: use BodycentricParams::heliocentric()\n"; - std::cout << "- Directions are free vectors (no center, only frame)\n"; - std::cout << "- Round-trip transformations preserve positions within floating-point precision\n"; - - return 0; -} diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp deleted file mode 100644 index 3a144e4..0000000 --- a/examples/coordinate_systems_example.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @file coordinate_systems_example.cpp - * @example coordinate_systems_example.cpp - * @brief Compile-time frame tags and transform capabilities walkthrough. - */ - -#include -#include -#include - -#include - -int main() { - using namespace siderust; - using namespace siderust::frames; - - std::cout << "=== coordinate_systems_example ===\n"; - - static_assert(has_frame_transform_v); - static_assert(has_frame_transform_v); - static_assert(has_horizontal_transform_v); - - const Geodetic observer = ROQUE_DE_LOS_MUCHACHOS; - const auto ecef = observer.to_cartesian(); - - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - - spherical::Direction src(qtty::Degree(279.23473), - qtty::Degree(38.78369)); - const auto ecl = src.to_frame(jd); - const auto mod = src.to_frame(jd); - const auto tod = mod.to_frame(jd); - const auto horiz = src.to_horizontal(jd, observer); - - std::cout << std::fixed << std::setprecision(6); - std::cout << "Observer: " << observer << std::endl; - std::cout << "Observer in ECEF: " << ecef << std::endl; - - std::cout << "Frame transforms for Vega-like direction\n"; - std::cout << " ICRS RA/Dec : " << src << "\n"; - std::cout << " EclipticMeanJ2000 lon/lat : " << ecl << "\n"; - std::cout << " EquatorialMeanOfDate RA/Dec: " << mod << "\n"; - std::cout << " EquatorialTrueOfDate RA/Dec: " << tod << "\n"; - std::cout << " Horizontal az/alt : " << horiz << "\n"; - - return 0; -} diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp deleted file mode 100644 index ee6fdb8..0000000 --- a/examples/coordinates_examples.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @file coordinates_examples.cpp - * @example coordinates_examples.cpp - * @brief Focused typed-coordinate construction and conversion examples. - */ - -#include -#include -#include - -#include - -int main() { - using namespace siderust; - using namespace qtty::literals; - - std::cout << "=== coordinates_examples ===\n"; - - const Geodetic site(-17.8890_deg, 28.7610_deg, 2396.0_m); - const auto ecef_m = site.to_cartesian(); - const auto ecef_km = site.to_cartesian(); - - static_assert(std::is_same_v, - cartesian::position::ECEF>); - - std::cout << "Geodetic -> ECEF \n " << site << "\n" - << ecef_m << "\n" - << "(" << ecef_km << ")\n" - << std::endl; - - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - - spherical::direction::ICRS vega_icrs(279.23473_deg, 38.78369_deg); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_true = vega_icrs.to_frame(jd); - auto vega_horiz = vega_icrs.to_horizontal(jd, site); - - std::cout << "Direction transforms\n"; - std::cout << " ICRS RA/Dec: " << vega_icrs << std::endl; - std::cout << " Ecliptic lon/lat: " << vega_ecl << std::endl; - std::cout << " True-of-date RA/Dec: " << vega_true << std::endl; - std::cout << " Horizontal az/alt: " << vega_horiz << std::endl; - - spherical::position::ICRS synthetic_star( - 210.0_deg, -12.0_deg, 4.2_au); - - cartesian::position::EclipticMeanJ2000 earth = - ephemeris::earth_heliocentric(jd); - - std::cout << "Typed positions\n"; - std::cout << " Synthetic star distance: " << synthetic_star.distance() - << std::endl; - std::cout << " Earth heliocentric x: " << earth.x() << std::endl; - - return 0; -} diff --git a/examples/demo.cpp b/examples/demo.cpp deleted file mode 100644 index a0e8c23..0000000 --- a/examples/demo.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @file demo.cpp - * @example demo.cpp - * @brief End-to-end demo of siderust-cpp extended capabilities. - */ - -#include -#include -#include -#include -#include -#include - -#include - -namespace {} // namespace - -int main() { - using namespace siderust; - - const Geodetic site = ROQUE_DE_LOS_MUCHACHOS; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - const MJD now = MJD::from_jd(jd); - const Period next_day(now, now + qtty::Day(1.0)); - - std::cout << "=== siderust-cpp extended demo ===\n"; - std::cout << "Observer: " << site << "\n"; - std::cout << "Epoch: JD " << std::fixed << std::setprecision(6) << jd.value() - << " UTC " << jd.to_utc() << "\n\n"; - - spherical::direction::ICRS vega_icrs(qtty::Degree(279.23473), - qtty::Degree(38.78369)); - auto vega_ecl = vega_icrs.to_frame(jd); - auto vega_hor = vega_icrs.to_horizontal(jd, site); - std::cout << "Typed coordinates\n"; - std::cout << " Vega ICRS RA/Dec=" << vega_icrs << " deg\n"; - std::cout << " Vega Ecliptic lon/lat=" << vega_ecl << " deg\n"; - std::cout << " Vega Horizontal az/alt=" << vega_hor << " deg\n\n"; - - qtty::Degree sun_alt = sun::altitude_at(site, now).to(); - qtty::Degree sun_az = sun::azimuth_at(site, now); - std::cout << "Sun instant\n"; - std::cout << " Altitude=" << sun_alt.value() << " deg" - << " Azimuth=" << sun_az.value() << " deg\n"; - - auto sun_crossings = sun::crossings(site, next_day, qtty::Degree(0.0)); - if (!sun_crossings.empty()) { - std::cout << " Next horizon crossing: " - << sun_crossings.front().time.to_utc() << " (" - << sun_crossings.front().direction << ")\n"; - } - std::cout << "\n"; - - BodyTarget mars(Body::Mars); - ICRSTarget fixed_target{spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369)}}; // Vega-like - - std::vector>> targets; - targets.push_back({"Sun", std::make_unique(Body::Sun)}); - targets.push_back({"Vega", std::make_unique(VEGA)}); - targets.push_back( - {"Fixed target", std::make_unique(spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369)})}); - - std::cout << "Trackable polymorphism\n"; - for (const auto &entry : targets) { - const auto &name = entry.first; - const auto &obj = entry.second; - auto alt = obj->altitude_at(site, now); - auto az = obj->azimuth_at(site, now); - std::cout << " " << std::setw(12) << std::left << name - << " alt=" << std::setw(8) << alt << " az=" << az << std::endl; - } - std::cout << " Mars altitude via BodyTarget: " - << mars.altitude_at(site, now).value() << " deg\n"; - std::cout << " Fixed Target altitude: " - << fixed_target.altitude_at(site, now).value() << " deg\n\n"; - - auto earth_helio = ephemeris::earth_heliocentric(jd); - auto moon_geo = ephemeris::moon_geocentric(jd); - double moon_dist_km = std::sqrt(moon_geo.x().value() * moon_geo.x().value() + - moon_geo.y().value() * moon_geo.y().value() + - moon_geo.z().value() * moon_geo.z().value()); - - std::cout << "Ephemeris\n"; - std::cout << " Earth heliocentric " << earth_helio << " AU\n"; - std::cout << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - - auto phase = moon::phase_topocentric(jd, site); - auto label = moon::phase_label(phase); - auto bright_periods = - moon::illumination_above(Period(now, now + qtty::Day(7.0)), 0.8); - - std::cout << "Lunar phase\n"; - std::cout << " Illuminated fraction=" << phase.illuminated_fraction - << " label=" << label << "\n"; - std::cout << " Bright-moon periods (next 7 days, k>=0.8): " - << bright_periods.size() << "\n"; - - return 0; -} diff --git a/examples/l2_satellite_mars_example.cpp b/examples/l2_satellite_mars_example.cpp deleted file mode 100644 index acfb6dd..0000000 --- a/examples/l2_satellite_mars_example.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include - -#include -#include -#include - -using namespace siderust; - -// Approximate Sun–Earth L2 offset: 1.5e6 km beyond Earth along the Sun–Earth -// line. Convert to AU so we can stay unit-safe with qtty. -constexpr double L2_OFFSET_KM = 1'500'000.0; -constexpr double KM_PER_AU = 149'597'870.7; -constexpr double L2_OFFSET_AU = L2_OFFSET_KM / KM_PER_AU; - -cartesian::position::EclipticMeanJ2000 -compute_l2_heliocentric(const JulianDate &jd) { - const auto earth = ephemeris::earth_heliocentric(jd); - - // Unit vector from Sun → Earth (heliocentric frame). - const double ex = earth.x().value(); - const double ey = earth.y().value(); - const double ez = earth.z().value(); - const double norm = std::sqrt(ex * ex + ey * ey + ez * ez); - const double ux = ex / norm; - const double uy = ey / norm; - const double uz = ez / norm; - - // Move ~0.01 AU further from the Sun along that direction. - const qtty::AstronomicalUnit offset(L2_OFFSET_AU); - return {earth.x() + offset * ux, earth.y() + offset * uy, - earth.z() + offset * uz}; -} - -cartesian::Position -mars_relative_to_l2(const JulianDate &jd) { - const auto mars = ephemeris::mars_heliocentric(jd); - const auto l2 = compute_l2_heliocentric(jd); - - // Bodycentric position = target - observer. - return {mars.x() - l2.x(), mars.y() - l2.y(), mars.z() - l2.z()}; -} - -int main() { - std::cout << "╔══════════════════════════════════════════╗\n" - << "║ Mars as Seen from a JWST-like L2 Orbit ║\n" - << "╚══════════════════════════════════════════╝\n\n"; - - const JulianDate obs_epoch(2460000.0); // ~2023-06-30 - - std::cout << "Observation epoch (JD): " << std::fixed << std::setprecision(1) - << obs_epoch.value() << "\n\n"; - - const auto mars_helio = ephemeris::mars_heliocentric(obs_epoch); - const auto l2_helio = compute_l2_heliocentric(obs_epoch); - const auto mars_from_l2 = mars_relative_to_l2(obs_epoch); - - std::cout << "Mars heliocentric (EclipticMeanJ2000):\n " << mars_helio - << "\n\n"; - - std::cout << "L2 heliocentric (Earth + 1.5e6 km radial):\n " << l2_helio - << "\n\n"; - - std::cout << "Mars relative to L2 (bodycentric):\n " << mars_from_l2 - << "\n"; - - return 0; -} diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp deleted file mode 100644 index 25c69d7..0000000 --- a/examples/solar_system_bodies_example.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @file solar_system_bodies_example.cpp - * @example solar_system_bodies_example.cpp - * @brief Solar-system body catalog, ephemeris, and body-dispatch examples. - */ - -#include -#include -#include -#include - -#include - -namespace { - -const char *az_kind_name(siderust::AzimuthExtremumKind kind) { - using siderust::AzimuthExtremumKind; - switch (kind) { - case AzimuthExtremumKind::Max: - return "max"; - case AzimuthExtremumKind::Min: - return "min"; - } - return "unknown"; -} - -} // namespace - -int main() { - using namespace siderust; - - const Geodetic site = MAUNA_KEA; - const JulianDate jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - const MJD now = MJD::from_jd(jd); - const Period window(now, now + qtty::Day(2.0)); - - std::cout << "=== solar_system_bodies_example ===\n"; - std::cout << "Epoch UTC: " << jd.to_utc() << "\n\n"; - - std::cout << "Planet catalog constants\n"; - std::cout << " Mercury a=" << MERCURY.orbit.semi_major_axis_au << " AU" - << " radius=" << MERCURY.radius_km << " km\n"; - std::cout << " Earth a=" << EARTH.orbit.semi_major_axis_au << " AU" - << " radius=" << EARTH.radius_km << " km\n"; - std::cout << " Jupiter a=" << JUPITER.orbit.semi_major_axis_au << " AU" - << " radius=" << JUPITER.radius_km << " km\n\n"; - - auto earth = ephemeris::earth_heliocentric(jd); - auto moon_pos = ephemeris::moon_geocentric(jd); - double moon_dist_km = std::sqrt(moon_pos.x().value() * moon_pos.x().value() + - moon_pos.y().value() * moon_pos.y().value() + - moon_pos.z().value() * moon_pos.z().value()); - - std::cout << "Ephemeris\n"; - std::cout << std::fixed << std::setprecision(6) - << " Earth heliocentric x=" << earth.x().value() - << " AU y=" << earth.y().value() << " AU\n"; - std::cout << std::setprecision(2) - << " Moon geocentric distance=" << moon_dist_km << " km\n\n"; - - std::vector tracked = {Body::Sun, Body::Moon, Body::Mars, - Body::Jupiter}; - - std::cout << "Body dispatch API at observer\n"; - for (Body b : tracked) { - auto alt = body::altitude_at(b, site, now).to(); - auto az = body::azimuth_at(b, site, now).to(); - std::cout << " body=" << static_cast(b) << " alt=" << alt - << " az=" << az << std::endl; - } - - auto moon_extrema = body::azimuth_extrema(Body::Moon, site, window); - if (!moon_extrema.empty()) { - const auto &e = moon_extrema.front(); - std::cout << "\nMoon azimuth extrema\n"; - std::cout << " first " << az_kind_name(e.kind) << " at " << e.time.to_utc() - << " az=" << e.azimuth << std::endl; - } - - return 0; -} diff --git a/examples/trackable_targets_example.cpp b/examples/trackable_targets_example.cpp deleted file mode 100644 index c09d50b..0000000 --- a/examples/trackable_targets_example.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @file trackable_targets_example.cpp - * @example trackable_targets_example.cpp - * @brief Using DirectionTarget, StarTarget, BodyTarget through the Target - * polymorphic interface. - * - * Demonstrates the strongly-typed DirectionTarget template with multiple frames: - * - ICRSTarget — fixed direction in ICRS equatorial coordinates - * - EclipticMeanJ2000Target — fixed direction in mean ecliptic J2000 - * - * Non-ICRS targets are silently converted to ICRS at construction time for - * the Rust FFI layer; the original typed direction is retained in C++. - */ - -#include -#include -#include -#include -#include -#include - -#include - -int main() { - using namespace siderust; - - const Geodetic site = geodetic(-17.8890, 28.7610, 2396.0); - const MJD now = MJD::from_utc({2026, 7, 15, 22, 0, 0}); - const Period window(now, now + qtty::Day(1.0)); - - std::cout << "=== trackable_targets_example ===\n"; - std::cout << "Epoch UTC: " << now.to_utc() << "\n\n"; - - // Strongly-typed ICRS target — ra() / dec() return qtty::Degree. - ICRSTarget fixed_vega_like{spherical::direction::ICRS{ - qtty::Degree(279.23473), qtty::Degree(38.78369)}}; - std::cout << "ICRSTarget metadata\n"; - std::cout << " name=" << fixed_vega_like.name() << "\n"; - std::cout << " RA/Dec=" << fixed_vega_like.direction() - << " epoch=" << fixed_vega_like.epoch() << " JD\n\n"; - - // Ecliptic target (Vega in EclipticMeanJ2000, lon≈279.6°, lat≈+61.8°). - // Automatically converted to ICRS by the constructor. - EclipticMeanJ2000Target ecliptic_vega{spherical::direction::EclipticMeanJ2000{ - qtty::Degree(279.6), qtty::Degree(61.8)}}; - auto alt_ecliptic = ecliptic_vega.altitude_at(site, now); - std::cout << "EclipticMeanJ2000Target (Vega approx)\n"; - std::cout << " name=" << ecliptic_vega.name() << "\n"; - std::cout << " ecl lon/lat=" << ecliptic_vega.direction() << "\n"; - std::cout << " ICRS ra/dec=" << ecliptic_vega.icrs_direction() - << " (converted)\n"; - std::cout << " alt=" << alt_ecliptic << "\n\n"; - - // Polymorphic catalog: all targets share the Target base. - // DirectionTarget accepts an optional label at construction. - std::vector> catalog; - catalog.push_back(std::make_unique(Body::Sun)); - catalog.push_back(std::make_unique(Body::Mars)); - catalog.push_back(std::make_unique(VEGA)); - catalog.push_back(std::make_unique( - spherical::direction::ICRS{qtty::Degree(279.23473), qtty::Degree(38.78369)}, - JulianDate::J2000(), "Vega (ICRS coord)")); - - for (const auto &t : catalog) { - auto alt = t->altitude_at(site, now); - auto az = t->azimuth_at(site, now); - - std::cout << std::left << std::setw(22) << t->name() << std::right - << " alt=" << std::setw(9) << alt << " az=" << az << std::endl; - - auto crossings = t->crossings(site, window, qtty::Degree(0.0)); - if (!crossings.empty()) { - const auto &first = crossings.front(); - std::cout << " first horizon crossing: " << first.time.to_utc() << " (" - << first.direction << ")\n"; - } - - auto az_cross = t->azimuth_crossings(site, window, qtty::Degree(180.0)); - if (!az_cross.empty()) { - std::cout << " first az=180 crossing: " << az_cross.front().time.to_utc() - << "\n"; - } - } - - return 0; -} diff --git a/include/siderust/azimuth.hpp b/include/siderust/azimuth.hpp index 37640bd..7276159 100644 --- a/include/siderust/azimuth.hpp +++ b/include/siderust/azimuth.hpp @@ -332,6 +332,34 @@ azimuth_crossings(const Star &s, const Geodetic &obs, const MJD &start, return azimuth_crossings(s, obs, Period(start, end), bearing, opts); } +/** + * @brief Find periods when a star's azimuth is within [min, max] (degrees). + */ +inline std::vector +in_azimuth_range(const Star &s, const Geodetic &obs, const Period &window, + qtty::Degree min_bearing, qtty::Degree max_bearing, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_star_in_azimuth_range( + s.c_handle(), obs.to_c(), window.c_inner(), + min_bearing.value(), max_bearing.value(), opts.to_c(), &ptr, + &count), + "star_altitude::in_azimuth_range"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Backward-compatible [start, end] overload. + */ +inline std::vector +in_azimuth_range(const Star &s, const Geodetic &obs, const MJD &start, + const MJD &end, qtty::Degree min_bearing, + qtty::Degree max_bearing, const SearchOptions &opts = {}) { + return in_azimuth_range(s, obs, Period(start, end), min_bearing, max_bearing, + opts); +} + } // namespace star_altitude // ============================================================================ diff --git a/include/siderust/coordinates/cartesian.hpp b/include/siderust/coordinates/cartesian.hpp index dc5aaec..a97a347 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -185,6 +185,87 @@ template struct Position { -> decltype(this->template to_frame(jd)) { return to_frame(jd); } + + /** + * @brief Transform this position to a different reference center (same frame). + * + * The FFI center-shift uses VSOP87 ephemeris vectors expressed in + * EclipticMeanJ2000. When the position is already in that frame the FFI + * call is made directly; otherwise the position is first rotated to + * ecliptic, shifted, and rotated back so the result is frame-correct. + * + * @tparam TargetC Destination center tag. + * @param jd Julian Date (TT) for the ephemeris evaluation. + */ + template + std::enable_if_t, Position> + to_center(const JulianDate &jd) const { + if constexpr (std::is_same_v) { + return *this; + } else if constexpr (std::is_same_v) { + // Direct FFI call — shift vectors and position are both in ecliptic. + siderust_cartesian_pos_t out{}; + check_status( + siderust_cartesian_pos_transform_center( + to_c(), + centers::CenterTraits::ffi_id, + jd.value(), &out), + "cartesian::Position::to_center"); + return Position(out.x, out.y, out.z); + } else { + // Route through ecliptic so the shift vectors match the frame. + auto ecl = to_frame(jd); + auto shifted = ecl.template to_center(jd); + return shifted.template to_frame(jd); + } + } + + /** + * @brief Combined frame + center transform in one call. + * + * Routes through EclipticMeanJ2000 for the center shift so that + * VSOP87 ephemeris vectors are applied in the correct frame: + * 1. rotate to EclipticMeanJ2000 + * 2. shift center + * 3. rotate to target frame + * + * @tparam TargetC Destination center tag. + * @tparam TargetF Destination frame tag. + * @param jd Julian Date (TT). + */ + template + std::enable_if_t< + frames::has_frame_transform_v && + centers::has_center_transform_v, + Position> + transform(const JulianDate &jd) const { + auto ecl = to_frame(jd); + auto shifted = ecl.template to_center(jd); + return shifted.template to_frame(jd); + } + + /** + * @brief Subtract two positions in the same center/frame/unit (vector difference). + */ + Position operator-(const Position &other) const { + return Position(U(comp_x.value() - other.comp_x.value()), + U(comp_y.value() - other.comp_y.value()), + U(comp_z.value() - other.comp_z.value())); + } + + /** + * @brief Add two positions in the same center/frame/unit (vector sum). + */ + Position operator+(const Position &other) const { + return Position(U(comp_x.value() + other.comp_x.value()), + U(comp_y.value() + other.comp_y.value()), + U(comp_z.value() + other.comp_z.value())); + } + + /** + * @brief Magnitude of the position vector (alias for distance()). + */ + U magnitude() const { return distance(); } }; // ============================================================================ diff --git a/include/siderust/coordinates/types/cartesian/position/equatorial.hpp b/include/siderust/coordinates/types/cartesian/position/equatorial.hpp index c5f8a5a..54b7841 100644 --- a/include/siderust/coordinates/types/cartesian/position/equatorial.hpp +++ b/include/siderust/coordinates/types/cartesian/position/equatorial.hpp @@ -10,6 +10,9 @@ using ICRS = Position; template using GCRS = Position; + +template +using HCRS = Position; } // namespace position } // namespace cartesian } // namespace siderust diff --git a/include/siderust/ephemeris.hpp b/include/siderust/ephemeris.hpp index 72dd39f..3d1cd5e 100644 --- a/include/siderust/ephemeris.hpp +++ b/include/siderust/ephemeris.hpp @@ -68,6 +68,18 @@ mars_heliocentric(const JulianDate &jd) { out); } +/** + * @brief Mars's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +mars_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_mars_barycentric(jd.value(), &out), + "ephemeris::mars_barycentric"); + return cartesian::position::HelioBarycentric::from_c( + out); +} + /** * @brief Venus's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. */ diff --git a/qtty-cpp b/qtty-cpp index 3612a8f..a5821e1 160000 --- a/qtty-cpp +++ b/qtty-cpp @@ -1 +1 @@ -Subproject commit 3612a8ff03f7290ea7fe53df897f10aa5716582f +Subproject commit a5821e14e6f5271998c5837b933abb9e281c8b77 diff --git a/siderust b/siderust index cf066e0..8c8eeac 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit cf066e0707bb2c485036ba1f5fa3e39b18a0aacf +Subproject commit 8c8eeacbc9b3c4d2147261cf511f57633d3adc73 diff --git a/tempoch-cpp b/tempoch-cpp index 0b27c11..b4bc764 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit 0b27c11cdc03fa016e3545753d3aaab1520ff576 +Subproject commit b4bc764eaa9b4d47104923b73fe4926741135fbc From da4cb14cdee57483f03660e15bd0b18c60b90eeb Mon Sep 17 00:00:00 2001 From: VPRamon Date: Sat, 28 Feb 2026 17:08:10 +0100 Subject: [PATCH 15/19] refactor: streamline output formatting in examples for better readability --- examples/01_basic_coordinates.cpp | 26 ++++++++++----------- examples/02_coordinate_transformations.cpp | 2 +- examples/06_night_events.cpp | 27 +++++----------------- examples/09_star_observability.cpp | 6 ++--- 4 files changed, 23 insertions(+), 38 deletions(-) diff --git a/examples/01_basic_coordinates.cpp b/examples/01_basic_coordinates.cpp index f4dab36..dc418cb 100644 --- a/examples/01_basic_coordinates.cpp +++ b/examples/01_basic_coordinates.cpp @@ -67,7 +67,7 @@ int main() { << std::endl; // Create a position with distance (Betelgeuse at ~500 light-years) - double betelgeuse_distance = qtty::LightYears(500.0).to(); // Convert 500 ly to AU + auto betelgeuse_distance = qtty::LightYear(500.0).to(); // Convert 500 ly to AU spherical::position::ICRS betelgeuse(88.79_deg, 7.41_deg, betelgeuse_distance); std::cout << "Betelgeuse (Barycentric ICRS Position):" << std::endl; std::cout << " Right Ascension = " << betelgeuse.ra() << std::endl; @@ -90,8 +90,8 @@ int main() { qtty::Degree(90.0) // Altitude (straight up) ); std::cout << "Zenith direction (Horizontal frame):" << std::endl; - std::cout << " Altitude = " << zenith.alt().value() << "°" << std::endl; - std::cout << " Azimuth = " << zenith.az().value() << "°" << std::endl + std::cout << " Altitude = " << zenith.alt() << std::endl; + std::cout << " Azimuth = " << zenith.az() << std::endl << std::endl; // Convert direction to position at a specific distance @@ -100,7 +100,7 @@ int main() { spherical::Position cloud( zenith.az(), zenith.alt(), cloud_distance); std::cout << "Cloud at zenith, 5 km altitude (relative to geocenter):" << std::endl; - std::cout << " Distance = " << cloud.distance().value() << " km" << std::endl + std::cout << " Distance = " << cloud.distance() << std::endl << std::endl; // ========================================================================= @@ -115,26 +115,26 @@ int main() { cart_pos(0.5, 0.5, 0.707); std::cout << "Cartesian position:" << std::endl; std::cout << std::setprecision(3); - std::cout << " X = " << cart_pos.x().value() << " AU" << std::endl; - std::cout << " Y = " << cart_pos.y().value() << " AU" << std::endl; - std::cout << " Z = " << cart_pos.z().value() << " AU" << std::endl + std::cout << " X = " << cart_pos.x() << std::endl; + std::cout << " Y = " << cart_pos.y() << std::endl; + std::cout << " Z = " << cart_pos.z() << std::endl << std::endl; // Convert to spherical auto sph_pos = cart_pos.to_spherical(); std::cout << "Converted to Spherical:" << std::endl; std::cout << std::setprecision(2); - std::cout << " RA = " << sph_pos.ra().value() << "°" << std::endl; - std::cout << " Dec = " << sph_pos.dec().value() << "°" << std::endl; + std::cout << " RA = " << sph_pos.ra() << std::endl; + std::cout << " Dec = " << sph_pos.dec() << std::endl; std::cout << std::setprecision(3); - std::cout << " Distance = " << sph_pos.distance().value() << " AU" << std::endl; + std::cout << " Distance = " << sph_pos.distance() << std::endl; // Convert back to cartesian auto cart_pos_back = sph_pos.to_cartesian(); std::cout << std::endl << "Converted back to Cartesian:" << std::endl; - std::cout << " X = " << cart_pos_back.x().value() << " AU" << std::endl; - std::cout << " Y = " << cart_pos_back.y().value() << " AU" << std::endl; - std::cout << " Z = " << cart_pos_back.z().value() << " AU" << std::endl + std::cout << " X = " << cart_pos_back.x() << std::endl; + std::cout << " Y = " << cart_pos_back.y() << std::endl; + std::cout << " Z = " << cart_pos_back.z() << std::endl << std::endl; // ========================================================================= diff --git a/examples/02_coordinate_transformations.cpp b/examples/02_coordinate_transformations.cpp index 3c9c93f..55e0e3e 100644 --- a/examples/02_coordinate_transformations.cpp +++ b/examples/02_coordinate_transformations.cpp @@ -23,7 +23,7 @@ int main() { auto jd = JulianDate::J2000(); std::cout << std::fixed << std::setprecision(1); - std::cout << "Reference time: J2000.0 (JD " << jd.value() << ")\n" << std::endl; + std::cout << "Reference time: J2000.0 (JD " << jd << ")\n" << std::endl; // ========================================================================= // 1. Frame Transformations (same center) diff --git a/examples/06_night_events.cpp b/examples/06_night_events.cpp index 4cd8767..74b9b5a 100644 --- a/examples/06_night_events.cpp +++ b/examples/06_night_events.cpp @@ -14,8 +14,6 @@ #include #include -#include -#include #include using namespace siderust; @@ -29,18 +27,6 @@ constexpr auto NAUTICAL = qtty::Degree(-12.0); constexpr auto ASTRONOMICAL = qtty::Degree(-18.0); } // namespace twilight -/// Format a CivilTime as YYYY-MM-DDTHH:MM:SS. -static std::string fmt_utc(const tempoch::CivilTime &ct) { - std::ostringstream os; - os << ct.year << '-' - << std::setfill('0') << std::setw(2) << int(ct.month) << '-' - << std::setw(2) << int(ct.day) << 'T' - << std::setw(2) << int(ct.hour) << ':' - << std::setw(2) << int(ct.minute) << ':' - << std::setw(2) << int(ct.second); - return os.str(); -} - static Period week_from_mjd(const MJD &start) { MJD end = start + qtty::Day(7.0); return Period(start, end); @@ -52,8 +38,8 @@ static void print_events_for_type(const Geodetic &site, const Period &week, int downs = 0, raises = 0; std::cout << std::left << std::setw(18) << name << " threshold " - << std::right << std::setw(8) << std::fixed << std::setprecision(3) - << threshold.value() << " deg -> " << events.size() + << std::right << std::fixed << std::setprecision(3) + << threshold << " -> " << events.size() << " crossing(s)" << std::endl; for (auto &ev : events) { @@ -66,7 +52,7 @@ static void print_events_for_type(const Geodetic &site, const Period &week, label = "night-type raise (Sun rising above threshold)"; } auto utc = ev.time.to_utc(); - std::cout << " - " << label << " at " << fmt_utc(utc) << std::endl; + std::cout << " - " << label << " at " << utc << std::endl; } std::cout << " summary: down=" << downs << " raise=" << raises << std::endl; } @@ -76,15 +62,14 @@ static void print_periods_for_type(const Geodetic &site, const Period &week, auto periods = sun::below_threshold(site, week, threshold); std::cout << std::left << std::setw(18) << name << " night periods (Sun < " << std::fixed << std::setprecision(3) - << threshold.value() << " deg): " << periods.size() << std::endl; + << threshold << "): " << periods.size() << std::endl; for (auto &p : periods) { auto s = p.start().to_utc(); auto e = p.end().to_utc(); auto hours = p.duration(); - std::cout << " - " << fmt_utc(s) << " -> " << fmt_utc(e) - << " (" << std::fixed << std::setprecision(1) - << hours.value() << " h)" << std::endl; + std::cout << " - " << s << " -> " << e + << " (" << std::setprecision(1) << hours << ")" << std::endl; } } diff --git a/examples/09_star_observability.cpp b/examples/09_star_observability.cpp index 1562dbb..181a5d1 100644 --- a/examples/09_star_observability.cpp +++ b/examples/09_star_observability.cpp @@ -62,7 +62,7 @@ int main() { std::cout << "Observer: Roque de los Muchachos" << std::endl; std::cout << "Target: Sirius" << std::endl; std::cout << "Window: MJD " << std::fixed << std::setprecision(1) - << window.start().value() << " -> " << window.end().value() + << window.start() << " -> " << window.end() << "\n" << std::endl; std::cout << "Altitude range: " << min_alt << " .. " << max_alt << std::endl; @@ -75,8 +75,8 @@ int main() { total_hours += hours.value(); std::cout << " " << (i + 1) << ". MJD " << std::fixed << std::setprecision(6) - << observable[i].start().value() << " -> " - << observable[i].end().value() + << observable[i].start() << " -> " + << observable[i].end() << " (" << std::setprecision(4) << hours << ")" << std::endl; } From 6c686ab547f4e771fd4f87e18d7a8942d7945b9c Mon Sep 17 00:00:00 2001 From: VPRamon Date: Sat, 28 Feb 2026 19:02:51 +0100 Subject: [PATCH 16/19] Refactor serialization examples in C++ and enhance ephemeris functionality - Converted Rust serialization examples to C++ in `11_serde_serialization.cpp`, demonstrating manual JSON-like serialization for time, coordinate, body-related, and target objects. - Added file I/O capabilities to save and load serialized data. - Introduced new functions in `ephemeris.hpp` for calculating heliocentric and barycentric positions of Mercury, Venus, Jupiter, Saturn, Uranus, and Neptune using VSOP87. - Expanded time-related types in `time.hpp` to include TDB, TT, TAI, TCG, TCB, GPS, UT, UniversalTime, JDE, and UnixTime. - Updated submodules for siderust and tempoch-cpp to the latest commits. --- CMakeLists.txt | 11 - examples/03_all_frames_conversions.cpp | 4 +- examples/04_all_center_conversions.cpp | 266 +++++++--------- examples/05_target_tracking.cpp | 359 +++++++++++---------- examples/08_solar_system.cpp | 418 ++++++++++++------------ examples/10_time_periods.cpp | 359 ++++++++++++--------- examples/11_serde_serialization.cpp | 423 +++++++++++++++---------- include/siderust/ephemeris.hpp | 121 +++++++ include/siderust/time.hpp | 10 + siderust | 2 +- tempoch-cpp | 2 +- 11 files changed, 1116 insertions(+), 859 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 212e0ca..d799ee7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,17 +160,6 @@ endmacro() # --------------------------------------------------------------------------- # Examples # --------------------------------------------------------------------------- -siderust_add_example(siderust_demo examples/demo.cpp) -siderust_add_example(coordinates_examples examples/coordinates_examples.cpp) -siderust_add_example(basic_coordinates_example examples/01_basic_coordinates.cpp) -siderust_add_example(coordinate_systems_example examples/coordinate_systems_example.cpp) -siderust_add_example(coordinate_transformations_example examples/02_coordinate_transformations.cpp) -siderust_add_example(solar_system_bodies_example examples/solar_system_bodies_example.cpp) -siderust_add_example(altitude_events_example examples/altitude_events_example.cpp) -siderust_add_example(trackable_targets_example examples/trackable_targets_example.cpp) -siderust_add_example(azimuth_lunar_phase_example examples/azimuth_lunar_phase_example.cpp) -siderust_add_example(l2_satellite_mars_example examples/l2_satellite_mars_example.cpp) -siderust_add_example(bodycentric_coordinates_example examples/bodycentric_coordinates.cpp) # Numbered mirror examples (03–21, mirroring siderust Rust examples) siderust_add_example(03_all_frame_conversions_example examples/03_all_frames_conversions.cpp) diff --git a/examples/03_all_frames_conversions.cpp b/examples/03_all_frames_conversions.cpp index 6240d9a..6edf8af 100644 --- a/examples/03_all_frames_conversions.cpp +++ b/examples/03_all_frames_conversions.cpp @@ -32,7 +32,7 @@ void show_frame_conversion(const JulianDate &jd, << " -> " << std::setw(24) << FrameTraits::name() << " out=(" << std::showpos << std::setprecision(9) << out << std::noshowpos << ") roundtrip=" << std::scientific - << std::setprecision(3) << err.value() << std::fixed + << std::setprecision(3) << err << std::fixed << std::endl; } @@ -40,7 +40,7 @@ int main() { JulianDate jd(2460000.5); std::cout << std::fixed; std::cout << "Frame conversion demo at JD(TT) = " << std::setprecision(1) - << jd.value() << std::endl; + << jd << std::endl; cartesian::Position p_icrs(0.30, -0.70, 0.64); auto p_icrf = p_icrs.to_frame(jd); diff --git a/examples/04_all_center_conversions.cpp b/examples/04_all_center_conversions.cpp index 64ee479..e285001 100644 --- a/examples/04_all_center_conversions.cpp +++ b/examples/04_all_center_conversions.cpp @@ -1,181 +1,143 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Vallés Puig, Ramon -//! Example: all currently supported center conversions. -//! -//! This demonstrates all center-shift pairs implemented in `providers.rs`: -//! - Barycentric <-> Heliocentric -//! - Barycentric <-> Geocentric -//! - Heliocentric <-> Geocentric -//! - Identity shifts for each center -//! -//! It also demonstrates: -//! - **Bodycentric** conversions: from Barycentric, Heliocentric, and Geocentric into a -//! body-centric frame (Mars-centric and ISS-centric) with round-trip verification. -//! - **Topocentric** conversions: observer-on-Earth parallax correction applied to -//! positions originally expressed in each of the three standard centers. - -use qtty::*; -use siderust::astro::orbit::Orbit; -use siderust::coordinates::cartesian::Position; -use siderust::coordinates::centers::{ - Barycentric, Bodycentric, BodycentricParams, Geocentric, Geodetic, Heliocentric, - ReferenceCenter, Topocentric, -}; -use siderust::coordinates::frames::{EclipticMeanJ2000, ECEF}; -use siderust::coordinates::transform::{CenterShiftProvider, TransformCenter}; -use siderust::time::JulianDate; - -type F = EclipticMeanJ2000; -type U = AstronomicalUnit; +/// @file 04_all_center_conversions.cpp +/// @brief All currently supported center conversions. +/// +/// Demonstrates: +/// - Standard center shifts: Barycentric ↔ Heliocentric ↔ Geocentric +/// - Identity shifts for each center +/// - Bodycentric conversions with round-trip (Mars helio + ISS geo) +/// +/// Build & run: +/// cmake --build build-local --target 04_all_center_conversions_example +/// ./build-local/04_all_center_conversions_example + +#include + +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; + +using F = EclipticMeanJ2000; +using U = qtty::AstronomicalUnit; // ─── Standard center shifts ────────────────────────────────────────────────── -fn show_center_conversion(jd: &JulianDate, src: &Position) -where - C1: ReferenceCenter, - C2: ReferenceCenter, - (): CenterShiftProvider, - (): CenterShiftProvider, -{ - let out: Position = src.to_center(*jd); - let back: Position = out.to_center(*jd); - let err = (*src - back).magnitude(); - - println!( - "{:<12} -> {:<12} out=({:+.9}) roundtrip={:.3e}", - C1::center_name(), - C2::center_name(), - out, - err - ); +/// Show a center conversion C1→C2, round-trip C1→C2→C1, and the error. +template +void show_center_conversion(const JulianDate &jd, + const cartesian::Position &src) { + auto out = src.template to_center(jd); + auto back = out.template to_center(jd); + auto err = (src - back).magnitude(); + + std::cout << std::left << std::setw(12) << CenterTraits::name() + << " -> " << std::setw(12) << CenterTraits::name() + << " out=(" << std::showpos << std::fixed << std::setprecision(9) + << out + << std::noshowpos << ") roundtrip=" << std::scientific + << std::setprecision(3) << err << std::fixed + << std::endl; } // ─── Bodycentric ───────────────────────────────────────────────────────────── -/// Transform `src` (in center `C`) into body-centric coordinates and back. +/// Transform `src` (in center `C`) to body-centric coordinates and back. /// /// Round-trip: C → Bodycentric → Geocentric → C. -fn show_bodycentric_conversion( - label: &str, - jd: &JulianDate, - src: &Position, - params: BodycentricParams, -) where - C: ReferenceCenter, - Position: TransformCenter, - Position: TransformCenter, - (): CenterShiftProvider, -{ - let bary: Position = src.to_center((params, *jd)); - let recovered_geo: Position = bary.to_center(*jd); - let recovered: Position = recovered_geo.to_center(*jd); - let err = (*src - recovered).magnitude(); - - println!( - "{:<12} -> {:<12} dist={:.6} roundtrip={:.3e}", - label, - Bodycentric::center_name(), - bary.distance(), - err - ); -} - -// ─── Topocentric ───────────────────────────────────────────────────────────── - -/// Transform a geocentric position to topocentric and back. -/// -/// The `label` argument names the original center (before it was shifted to -/// geocentric), so the output shows the full chain (e.g. Barycentric → Geo → -/// Topocentric). -fn show_topocentric_conversion( - label: &str, - jd: &JulianDate, - geo: &Position, - site: Geodetic, -) { - let topo: Position = geo.to_center((site, *jd)); - let recovered: Position = topo.to_center(*jd); - let err = (*geo - recovered).magnitude(); - - println!( - "{:<12} -> {:<12} out=({:+.6}) roundtrip={:.3e}", - label, "Topocentric", topo, err - ); +template +void show_bodycentric_conversion(const char *label, + const JulianDate &jd, + const cartesian::Position &src, + const BodycentricParams ¶ms) { + auto bary = to_bodycentric(src, params, jd); + auto recovered_geo = bary.to_geocentric(jd); + auto recovered = recovered_geo.template to_center(jd); + auto err = (src - recovered).magnitude(); + + std::cout << std::left << std::setw(12) << label + << " -> " << std::setw(12) << "Bodycentric" + << " dist=" << std::fixed << std::setprecision(6) + << bary.distance() + << " roundtrip=" << std::scientific << std::setprecision(3) + << err << std::fixed + << std::endl; } // ─── main ───────────────────────────────────────────────────────────────────── -fn main() { - let jd = JulianDate::new(2_460_000.5); - println!("Center conversion demo at JD(TT) = {:.1}\n", jd); +int main() { + JulianDate jd(2460000.5); + std::cout << "Center conversion demo at JD(TT) = " << std::fixed + << std::setprecision(1) << jd << "\n" << std::endl; - let p_bary = Position::::new(0.40, -0.10, 1.20); - let p_helio: Position = p_bary.to_center(jd); - let p_geo: Position = p_bary.to_center(jd); + cartesian::Position p_bary(0.40, -0.10, 1.20); + auto p_helio = p_bary.to_center(jd); + auto p_geo = p_bary.to_center(jd); - // ── Standard center shifts via CenterShiftProvider ──────────────────────── - println!("── Standard center shifts ─────────────────────────────────────────────"); + // ── Standard center shifts ──────────────────────────────────────────────── + std::puts("── Standard center shifts ─────────────────────────────────────────────"); // Barycentric source - show_center_conversion::(&jd, &p_bary); - show_center_conversion::(&jd, &p_bary); - show_center_conversion::(&jd, &p_bary); + show_center_conversion(jd, p_bary); + show_center_conversion(jd, p_bary); + show_center_conversion(jd, p_bary); // Heliocentric source - show_center_conversion::(&jd, &p_helio); - show_center_conversion::(&jd, &p_helio); - show_center_conversion::(&jd, &p_helio); + show_center_conversion(jd, p_helio); + show_center_conversion(jd, p_helio); + show_center_conversion(jd, p_helio); // Geocentric source - show_center_conversion::(&jd, &p_geo); - show_center_conversion::(&jd, &p_geo); - show_center_conversion::(&jd, &p_geo); + show_center_conversion(jd, p_geo); + show_center_conversion(jd, p_geo); + show_center_conversion(jd, p_geo); // ── Bodycentric: Mars-like orbit (heliocentric reference) ────────────────── - println!("\n── Bodycentric – Mars-like orbit (heliocentric ref) ───────────────────"); - let mars_orbit = Orbit::new( - 1.524 * AU, - 0.0934, - Degrees::new(1.85), - Degrees::new(49.56), - Degrees::new(286.5), - Degrees::new(19.41), - jd, - ); - let mars_params = BodycentricParams::heliocentric(mars_orbit); - - show_bodycentric_conversion("Heliocentric", &jd, &p_helio, mars_params); - show_bodycentric_conversion("Barycentric", &jd, &p_bary, mars_params); - show_bodycentric_conversion("Geocentric", &jd, &p_geo, mars_params); + std::puts("\n── Bodycentric – Mars-like orbit (heliocentric ref) ───────────────────"); + Orbit mars_orbit{ + 1.524, // semi_major_axis_au + 0.0934, // eccentricity + 1.85, // inclination_deg + 49.56, // lon_ascending_node_deg + 286.5, // arg_perihelion_deg + 19.41, // mean_anomaly_deg + jd.value() + }; + auto mars_params = BodycentricParams::heliocentric(mars_orbit); + + show_bodycentric_conversion("Heliocentric", jd, p_helio, mars_params); + show_bodycentric_conversion("Barycentric", jd, p_bary, mars_params); + show_bodycentric_conversion("Geocentric", jd, p_geo, mars_params); // ── Bodycentric: ISS-like orbit (geocentric reference) ──────────────────── - println!("\n── Bodycentric – ISS-like orbit (geocentric ref) ──────────────────────"); - let iss_orbit = Orbit::new( - 0.0000426 * AU, // ~6 378 km in AU - 0.001, - Degrees::new(51.6), - Degrees::new(0.0), - Degrees::new(0.0), - Degrees::new(0.0), - jd, - ); - let iss_params = BodycentricParams::geocentric(iss_orbit); - - show_bodycentric_conversion("Heliocentric", &jd, &p_helio, iss_params); - show_bodycentric_conversion("Barycentric", &jd, &p_bary, iss_params); - show_bodycentric_conversion("Geocentric", &jd, &p_geo, iss_params); - - // ── Topocentric: observer at Barcelona ──────────────────────────────────── - println!("\n── Topocentric – Barcelona (lon=2.17°, lat=41.39°, h=12 m) ───────────"); - // Topocentric is geocentric-relative: shift Bary/Helio to Geocentric first, - // then apply the parallax correction. - let site = Geodetic::::new(Degrees::new(2.17), Degrees::new(41.39), 12.0 * M); - - let p_geo_from_bary: Position = p_bary.to_center(jd); - let p_geo_from_helio: Position = p_helio.to_center(jd); - - show_topocentric_conversion("Barycentric", &jd, &p_geo_from_bary, site); - show_topocentric_conversion("Heliocentric", &jd, &p_geo_from_helio, site); - show_topocentric_conversion("Geocentric", &jd, &p_geo, site); + std::puts("\n── Bodycentric – ISS-like orbit (geocentric ref) ──────────────────────"); + Orbit iss_orbit{ + 0.0000426, // ~6 378 km in AU + 0.001, // eccentricity + 51.6, // inclination_deg + 0.0, // lon_ascending_node_deg + 0.0, // arg_perihelion_deg + 0.0, // mean_anomaly_deg + jd.value() + }; + auto iss_params = BodycentricParams::geocentric(iss_orbit); + + show_bodycentric_conversion("Heliocentric", jd, p_helio, iss_params); + show_bodycentric_conversion("Barycentric", jd, p_bary, iss_params); + show_bodycentric_conversion("Geocentric", jd, p_geo, iss_params); + + + // ── Topocentric ─────────────────────────────────────────────────────────── + // NOTE: Topocentric position transforms are not yet available in the C FFI; + // Topocentric phase/altitude/azimuth queries are available via the altitude + // and lunar_phase modules. This section will be enabled once + // siderust_to_topocentric / siderust_from_topocentric FFI functions are added. + + return 0; } diff --git a/examples/05_target_tracking.cpp b/examples/05_target_tracking.cpp index a3b02cc..5613d29 100644 --- a/examples/05_target_tracking.cpp +++ b/examples/05_target_tracking.cpp @@ -1,180 +1,219 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Vallés Puig, Ramon -//! Target examples. -//! -//! Shows how to use: -//! - `Trackable` for dynamic sky objects (Sun, planets, Moon, stars, ICRS directions) -//! - `Target` / `CoordinateWithPM` as timestamped coordinate snapshots -//! - optional proper motion for stellar targets -//! - target frame+center conversion through `From<&Target<_>>` -//! -//! Run with: -//! `cargo run --example 08_target` - -use qtty::*; -use siderust::astro::orbit::Orbit; -use siderust::astro::proper_motion::{set_proper_motion_since_j2000, ProperMotion}; -use siderust::bodies::comet::HALLEY; -use siderust::bodies::solar_system::{Mars, Moon, Sun}; -use siderust::bodies::{catalog, Satellite}; -use siderust::coordinates::cartesian; -use siderust::coordinates::centers::{Geocentric, Heliocentric}; -use siderust::coordinates::spherical::direction; -use siderust::targets::{Target, Trackable}; -use siderust::time::JulianDate; - -fn main() { - let jd = JulianDate::J2000; - let jd_next = jd + Days::new(1.0); - - println!("Target + Trackable examples"); - println!("===========================\n"); - - section_trackable_objects(jd, jd_next); - section_target_snapshots(jd, jd_next); - section_target_with_proper_motion(jd); - section_target_transform(jd); +/// @file 05_target_tracking.cpp +/// @brief Target and Trackable examples. +/// +/// Demonstrates: +/// - `Target` hierarchy: `DirectionTarget`, `BodyTarget`, `StarTarget` +/// - Planet ephemeris as coordinate snapshots +/// - Kepler propagation for comets and satellites +/// - Proper motion propagation (inline math) +/// - Position frame + center transforms +/// +/// Build & run: +/// cmake --build build-local --target 05_target_tracking_example +/// ./build-local/05_target_tracking_example + +#include + +#include +#include +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; + +// ─── Helper: simple coordinate snapshot (mirrors Rust's Target) ────────── + +/// A timestamped position snapshot, optionally with proper motion. +template +struct Snapshot { + P position; + JulianDate time; + + void update(P new_pos, JulianDate new_time) { + position = new_pos; + time = new_time; + } +}; + +// ─── Halley's comet orbit (hardcoded from the Rust `HALLEY` constant) ─────── + +inline Orbit halley_orbit() { + // a = 17.834 AU, e = 0.96714, i = 162.26°, Ω = 58.42°, ω = 111.33°, + // M = 38.38° at epoch JD 2446467.4 (≈1986 Feb 9). + return {17.834, 0.96714, 162.26, 58.42, 111.33, 38.38, 2446467.4}; } -fn section_trackable_objects(jd: JulianDate, jd_next: JulianDate) { - println!("1) Trackable objects (ICRS, star, Sun, planet, Moon)"); - - let fixed_icrs = direction::ICRS::new(Degrees::new(120.0), Degrees::new(22.5)); - let fixed_icrs_at_jd = fixed_icrs.track(jd); - let fixed_icrs_at_next = fixed_icrs.track(jd_next); - - println!( - " ICRS direction is time-invariant: RA {:.3} -> {:.3}, Dec {:.3} -> {:.3}", - fixed_icrs_at_jd.ra(), - fixed_icrs_at_next.ra(), - fixed_icrs_at_jd.dec(), - fixed_icrs_at_next.dec() - ); - - let sirius_dir = catalog::SIRIUS.track(jd); - println!( - " Sirius via Trackable: RA {:.3}, Dec {:.3}", - sirius_dir.ra(), - sirius_dir.dec() - ); - - let sun = Sun.track(jd); - let mars = Mars.track(jd); - let moon = Moon.track(jd); - - println!( - " Sun barycentric distance: {:.6}", - sun.position.distance() - ); - println!( - " Mars barycentric distance: {:.6}", - mars.position.distance() - ); - println!( - " Moon geocentric distance: {:.1}\n", - moon.distance() - ); -} +// ─── Section 1: Trackable objects ─────────────────────────────────────────── + +void section_trackable_objects(const JulianDate &jd, const JulianDate &jd_next) { + std::puts("1) Trackable objects (ICRS, star, Sun, planet, Moon)"); + + // ICRS direction — time-invariant target + spherical::direction::ICRS fixed_icrs(qtty::Degree(120.0), qtty::Degree(22.5)); + ICRSTarget icrs_target(fixed_icrs, jd, "FixedICRS"); + + // Verify time-invariance: the ICRS direction coordinates are constant. + std::cout << " ICRS direction is time-invariant: " << std::fixed + << std::setprecision(3) << fixed_icrs << std::endl; + + // Sirius via the catalog StarTarget + StarTarget sirius_target(SIRIUS); + std::printf(" Sirius via StarTarget: name = %s\n", + sirius_target.name().c_str()); + + // Sun, Mars, Moon via BodyTarget + BodyTarget sun_target(Body::Sun); + BodyTarget mars_target(Body::Mars); + BodyTarget moon_target(Body::Moon); -fn section_target_snapshots(jd: JulianDate, jd_next: JulianDate) { - println!("2) Target snapshots for arbitrary sky objects"); - - // Planet snapshot target (heliocentric ecliptic cartesian) - let mut mars_target = Target::new_static(Mars::vsop87a(jd), jd); - println!( - " Mars target at JD {:.1}: r = {:.6}", - mars_target.time, - mars_target.position.distance() - ); - - // Update target with a new ephemeris sample at the next epoch. - mars_target.update(Mars::vsop87a(jd_next), jd_next); - println!( - " Mars target updated to JD {:.1}: r = {:.6}", - mars_target.time, - mars_target.position.distance() - ); - - // Comet snapshot target (orbit propagated with Kepler helper). - let halley_target = Target::new_static(HALLEY.orbit.kepler_position(jd), jd); - println!( - " Halley target at JD {:.1}: r = {:.6}", - halley_target.time, - halley_target.position.distance() - ); - - // Satellite-like custom object: propagate its Orbit and wrap in Target. - let demo_satellite = Satellite::new( - "DemoSat", - Kilograms::new(1_200.0), - Kilometers::new(1.4), - Orbit::new( - AstronomicalUnits::new(1.0002), - 0.001, - Degrees::new(0.1), - Degrees::new(35.0), - Degrees::new(80.0), - Degrees::new(10.0), - jd, - ), - ); - let demo_satellite_target = Target::new_static(demo_satellite.orbit.kepler_position(jd), jd); - println!( - " {} target at JD {:.1}: r = {:.6}\n", - demo_satellite.name, - demo_satellite_target.time, - demo_satellite_target.position.distance() - ); + // Show Sun barycentric distance at J2000 + auto sun_bary = ephemeris::sun_barycentric(jd); + std::cout << " Sun barycentric distance: " << std::fixed + << std::setprecision(6) << sun_bary.distance() << std::endl; + + // Mars heliocentric distance + auto mars_helio = ephemeris::mars_heliocentric(jd); + std::cout << " Mars heliocentric distance: " << std::fixed + << std::setprecision(6) << mars_helio.distance() << std::endl; + + // Moon geocentric distance + auto moon_geo = ephemeris::moon_geocentric(jd); + std::cout << " Moon geocentric distance: " << std::fixed + << std::setprecision(1) << moon_geo.distance() << "\n" << std::endl; } -fn section_target_with_proper_motion(jd: JulianDate) { - println!("3) Target with proper motion (stellar-style target)"); +// ─── Section 2: Target snapshots ──────────────────────────────────────────── + +void section_target_snapshots(const JulianDate &jd, const JulianDate &jd_next) { + std::puts("2) Target snapshots for arbitrary sky objects"); + + // Mars heliocentric snapshot (VSOP87 ephemeris) + Snapshot> mars_snap{ + ephemeris::mars_heliocentric(jd), jd}; + std::cout << " Mars target at JD " << std::fixed << std::setprecision(1) + << mars_snap.time << ": r = " << std::setprecision(6) + << mars_snap.position.distance() << std::endl; + + // Update with next-day ephemeris + mars_snap.update(ephemeris::mars_heliocentric(jd_next), jd_next); + std::cout << " Mars target updated to JD " << std::fixed + << std::setprecision(1) << mars_snap.time + << ": r = " << std::setprecision(6) + << mars_snap.position.distance() << std::endl; + + // Halley's comet — Kepler-propagated snapshot + auto halley_pos = kepler_position(halley_orbit(), jd); + Snapshot> halley_snap{ + halley_pos, jd}; + std::cout << " Halley target at JD " << std::fixed + << std::setprecision(1) << halley_snap.time + << ": r = " << std::setprecision(6) + << halley_snap.position.distance() << std::endl; + + // DemoSat — satellite-like custom object with a geocentric orbit + Orbit demosat_orbit{1.0002, 0.001, 0.1, 35.0, 80.0, 10.0, jd.value()}; + auto demosat_pos = kepler_position(demosat_orbit, jd); + Snapshot> demosat_snap{ + demosat_pos, jd}; + std::cout << " DemoSat target at JD " << std::fixed << std::setprecision(1) + << demosat_snap.time << ": r = " << std::setprecision(6) + << demosat_snap.position.distance() << "\n" << std::endl; +} - type MasPerYear = qtty::Per; - type MasPerYearQ = qtty::Quantity; +// ─── Section 3: Proper motion ─────────────────────────────────────────────── + +/// Apply stellar proper motion to an ICRS direction. +/// +/// Computes: RA' = RA + μα* · Δt / cos(dec), Dec' = Dec + μδ · Δt +/// where Δt is in Julian years since the reference epoch. +inline spherical::direction::ICRS +apply_proper_motion(const spherical::direction::ICRS &pos, + const ProperMotion &pm, + const JulianDate &epoch, + const JulianDate &target_epoch) { + constexpr double JULIAN_YEAR = 365.25; // days + double dt_years = (target_epoch.value() - epoch.value()) / JULIAN_YEAR; + + double ra_deg = pos.ra().value(); + double dec_deg = pos.dec().value(); + + // ProperMotion rates in deg/yr (already stored as deg/yr in the struct) + // MuAlphaStar convention: pm_ra is already μα* = μα·cos(δ), so divide by cos(δ) + double cos_dec = std::cos(dec_deg * M_PI / 180.0); + double dra = (cos_dec > 1e-12) ? pm.pm_ra_deg_yr * dt_years / cos_dec + : 0.0; + double ddec = pm.pm_dec_deg_yr * dt_years; + + return spherical::direction::ICRS( + qtty::Degree(ra_deg + dra), + qtty::Degree(dec_deg + ddec)); +} - let pm = ProperMotion::from_mu_alpha_star::( - MasPerYearQ::new(27.54), // µα⋆ - MasPerYearQ::new(10.86), // µδ - ); +void section_target_with_proper_motion(const JulianDate &jd) { + std::puts("3) Target with proper motion (stellar-style target)"); + + // Betelgeuse approximate ICRS coordinates at J2000 + // (RA ≈ 88.7929°, Dec ≈ +7.4071°) + spherical::direction::ICRS betelgeuse_pos( + qtty::Degree(88.7929), qtty::Degree(7.4071)); + + // Proper motion: µα* = 27.54 mas/yr, µδ = 10.86 mas/yr + // Convert mas/yr → deg/yr + constexpr double MAS_TO_DEG = 1.0 / 3600000.0; + ProperMotion pm(27.54 * MAS_TO_DEG, 10.86 * MAS_TO_DEG); + + std::cout << " Betelgeuse-like target at J2000: RA " + << std::fixed << std::setprecision(6) + << betelgeuse_pos.ra() << ", Dec " + << betelgeuse_pos.dec() << std::endl; + + // Propagate 25 years + constexpr double JULIAN_YEAR = 365.25; + JulianDate jd_future(jd.value() + 25.0 * JULIAN_YEAR); + auto moved = apply_proper_motion(betelgeuse_pos, pm, jd, jd_future); + + std::cout << " After 25 years: RA " + << std::fixed << std::setprecision(6) + << moved.ra() << ", Dec " + << moved.dec() << "\n" << std::endl; +} - let mut moving_target = - Target::new(*catalog::BETELGEUSE.coordinate.get_position(), jd, pm.clone()); +// ─── Section 4: Frame + center transforms ─────────────────────────────────── - println!( - " Betelgeuse-like target at J2000: RA {:.6}, Dec {:.6}", - moving_target.position.ra(), - moving_target.position.dec() - ); +void section_target_transform(const JulianDate &jd) { + std::puts("4) Target conversion across frame + center"); - let jd_future = jd + 25.0 * JulianDate::JULIAN_YEAR; - let moved = set_proper_motion_since_j2000(moving_target.position, pm, jd_future) - .expect("proper-motion propagation should succeed"); - moving_target.update(moved, jd_future); + // Mars heliocentric ecliptic → geocentric equatorial + auto mars_helio = ephemeris::mars_heliocentric(jd); + auto mars_geoeq = mars_helio.template transform(jd); - println!( - " After 25 years: RA {:.6}, Dec {:.6}\n", - moving_target.position.ra(), - moving_target.position.dec() - ); + std::cout << " Mars heliocentric ecliptic target: r = " + << std::fixed << std::setprecision(6) + << mars_helio.distance() << std::endl; + std::cout << " Mars geocentric equatorial target: r = " + << std::fixed << std::setprecision(6) + << mars_geoeq.distance() << std::endl; } -fn section_target_transform(jd: JulianDate) { - println!("4) Target conversion across frame + center"); +// ─── main ───────────────────────────────────────────────────────────────────── - type HelioEclAu = cartesian::position::EclipticMeanJ2000; - type GeoEqAu = cartesian::position::EquatorialMeanJ2000; +int main() { + JulianDate jd = JulianDate::J2000(); + JulianDate jd_next(jd.value() + 1.0); - let mars_helio: Target = Target::new_static(Mars::vsop87a(jd), jd); - let mars_geoeq: Target = Target::from(&mars_helio); + std::puts("Target + Trackable examples"); + std::puts("===========================\n"); + + section_trackable_objects(jd, jd_next); + section_target_snapshots(jd, jd_next); + section_target_with_proper_motion(jd); + section_target_transform(jd); - println!( - " Mars heliocentric ecliptic target: r = {:.6}", - mars_helio.position.distance() - ); - println!( - " Mars geocentric equatorial target: r = {:.6}", - mars_geoeq.position.distance() - ); + return 0; } diff --git a/examples/08_solar_system.cpp b/examples/08_solar_system.cpp index 86fd75e..1b4bb31 100644 --- a/examples/08_solar_system.cpp +++ b/examples/08_solar_system.cpp @@ -1,228 +1,234 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Vallés Puig, Ramon -//! Solar System + Planets Module Tour -//! -//! Run with: `cargo run --example 12_solar_system_example` - -use qtty::*; -use siderust::astro::orbit::Orbit; -use siderust::bodies::planets::{OrbitExt, Planet}; -use siderust::bodies::solar_system::*; -use siderust::calculus::vsop87::VSOP87; -use siderust::coordinates::cartesian::position::EclipticMeanJ2000; -use siderust::coordinates::centers::Geocentric; -use siderust::coordinates::transform::TransformCenter; -use siderust::time::JulianDate; - -fn main() { - let jd = JulianDate::J2000; - let now = JulianDate::from_utc(chrono::Utc::now()); - - println!("=== Siderust Solar System Module Tour ===\n"); - println!( - "Epoch used for deterministic outputs: J2000 (JD {:.1})", - jd - ); - println!("Current epoch snapshot: JD {:.6}\n", now); - - section_catalog_overview(); - section_planet_constants_and_periods(); - section_vsop87_positions(jd); - section_center_transforms(jd); - section_moon_and_lagrange_points(jd); - section_trait_dispatch(jd); - section_planet_builder(); - section_current_snapshot(now); +/// @file 08_solar_system.cpp +/// @brief Solar System + Planets Module Tour. +/// +/// Demonstrates: +/// - Planet constants (mass, radius, orbit) and orbital period via Kepler's 3rd law +/// - VSOP87 heliocentric + barycentric ephemerides for all planets, Sun, and Moon +/// - Center transforms (heliocentric → geocentric) +/// - Moon geocentric position (ELP2000) +/// - Custom planet construction +/// - Current-epoch snapshot using system time +/// +/// Build & run: +/// cmake --build build-local --target 08_solar_system_example +/// ./build-local/08_solar_system_example + +#include + +#include +#include +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; + +// ─── Kepler's 3rd law: compute orbital period from semi-major axis ────────── + +/// GM_Sun in AU³/day². From IAU: k² = μ where k = 0.01720209895 AU^(3/2)/day. +static constexpr double GM_SUN_AU3_DAY2 = 0.01720209895 * 0.01720209895; + +/// Sidereal period via Kepler's 3rd law: T = 2π √(a³/μ) [days]. +inline qtty::Day orbit_period(const Orbit &orb) { + double a = orb.semi_major_axis_au; + double T = 2.0 * M_PI * std::sqrt(a * a * a / GM_SUN_AU3_DAY2); + return qtty::Day(T); } -fn section_catalog_overview() { - println!("1) CATALOG OVERVIEW"); - println!("------------------"); - println!("Sun: {}", SOLAR_SYSTEM.sun.name); - println!("Major planets: {}", SOLAR_SYSTEM.planets.len()); - println!("Dwarf planets: {}", SOLAR_SYSTEM.dwarf_planets.len()); - println!("Major moons: {}", SOLAR_SYSTEM.moons.len()); - println!("Lagrange points: {}\n", SOLAR_SYSTEM.lagrange_points.len()); +// ─── JulianDate from system clock ─────────────────────────────────────────── + +/// Approximate JulianDate::now() using . +/// JD of the Unix epoch (1970-01-01T00:00:00 UTC) = 2440587.5 +inline JulianDate jd_now() { + using namespace std::chrono; + auto unix_sec = duration_cast( + system_clock::now().time_since_epoch()) + .count() / + 1000.0; + return JulianDate(2440587.5 + unix_sec / 86400.0); } -fn section_planet_constants_and_periods() { - println!("2) PLANET CONSTANTS + ORBITEXT::period()\n"); - - let major_planets = [ - ("Mercury", &MERCURY), - ("Venus", &VENUS), - ("Earth", &EARTH), - ("Mars", &MARS), - ("Jupiter", &JUPITER), - ("Saturn", &SATURN), - ("Uranus", &URANUS), - ("Neptune", &NEPTUNE), - ]; - - println!("{:<8} {:>10} {:>10} {:>10}", "Planet", "a [AU]", "e", "Period"); - println!("{}", "-".repeat(48)); - for (name, p) in major_planets { - println!( - "{:<8} {:>10.6} {:>10.6} {:>10.2}", - name, - p.orbit.semi_major_axis, - p.orbit.eccentricity, - p.orbit.period().to::() - ); +// ─── Sections ─────────────────────────────────────────────────────────────── + +void section_planet_constants_and_periods() { + std::puts("1) PLANET CONSTANTS + ORBITAL PERIOD (Kepler 3rd law)\n"); + + struct PlanetInfo { + const char *name; + const Planet *planet; + }; + const PlanetInfo planets[] = { + {"Mercury", &MERCURY}, {"Venus", &VENUS}, {"Earth", &EARTH}, + {"Mars", &MARS}, {"Jupiter", &JUPITER}, {"Saturn", &SATURN}, + {"Uranus", &URANUS}, {"Neptune", &NEPTUNE}, + }; + + std::printf("%-8s %10s %10s %10s\n", "Planet", "a [AU]", "e", "Period"); + std::puts("------------------------------------------------"); + for (auto &[name, p] : planets) { + auto period = orbit_period(p->orbit); + std::printf("%-8s %10.6f %10.6f ", name, + p->orbit.semi_major_axis_au, p->orbit.eccentricity); + std::cout << std::fixed << std::setprecision(2) << period << std::endl; } - println!(); + std::puts(""); } -fn section_vsop87_positions(jd: JulianDate) { - println!("3) VSOP87 EPHEMERIDES (HELIOCENTRIC + BARYCENTRIC)"); - println!("-----------------------------------------------"); - - let earth_h = Earth::vsop87a(jd); - let mars_h = Mars::vsop87a(jd); - let earth_mars = earth_h.distance_to(&mars_h); - - println!( - "Earth heliocentric distance: {:.6}", - earth_h.distance() - ); - println!( - "Mars heliocentric distance: {:.6}", - mars_h.distance() - ); - println!( - "Earth-Mars separation: {:.6} ({:.0}", - earth_mars, - earth_mars.to::() - ); - - let sun_bary = Sun::vsop87e(jd); - println!( - "Sun barycentric offset from SSB: {:.8}\n", - sun_bary.distance() - ); - - let (jupiter_pos, jupiter_vel) = Jupiter::vsop87e_pos_vel(jd); - println!("Jupiter barycentric position+velocity at J2000:"); - println!(" r = {:.6}", jupiter_pos.x()); - println!(" v = {:.6}\n", jupiter_vel); +void section_vsop87_positions(const JulianDate &jd) { + std::puts("2) VSOP87 EPHEMERIDES (HELIOCENTRIC + BARYCENTRIC)"); + std::puts("-----------------------------------------------"); + + auto earth_h = ephemeris::earth_heliocentric(jd); + auto mars_h = ephemeris::mars_heliocentric(jd); + auto earth_mars = earth_h.distance_to(mars_h); + + std::cout << "Earth heliocentric distance: " << std::fixed + << std::setprecision(6) << earth_h.distance() << std::endl; + std::cout << "Mars heliocentric distance: " << std::fixed + << std::setprecision(6) << mars_h.distance() << std::endl; + std::cout << "Earth-Mars separation: " << std::fixed + << std::setprecision(6) << earth_mars + << " (" << std::fixed << std::setprecision(0) + << earth_mars.to() << ")" << std::endl; + + auto sun_bary = ephemeris::sun_barycentric(jd); + std::cout << "Sun barycentric offset from SSB: " << std::fixed + << std::setprecision(8) << sun_bary.distance() << std::endl; + + auto jupiter_bary = ephemeris::jupiter_barycentric(jd); + std::cout << "\nJupiter barycentric position at J2000:" << std::endl; + std::cout << " x = " << std::fixed << std::setprecision(6) + << jupiter_bary.x() << std::endl; + std::cout << " y = " << std::fixed << std::setprecision(6) + << jupiter_bary.y() << std::endl; + std::cout << " z = " << std::fixed << std::setprecision(6) + << jupiter_bary.z() << std::endl; + std::puts(""); } -fn section_center_transforms(jd: JulianDate) { - println!("4) CENTER TRANSFORMS (HELIOCENTRIC -> GEOCENTRIC)"); - println!("-----------------------------------------------"); - - let mars_helio = Mars::vsop87a(jd); - let mars_geo: EclipticMeanJ2000 = mars_helio.to_center(jd); - - println!( - "Mars geocentric distance at J2000: {:.6}", - mars_geo.distance() - ); - println!( - "Mars geocentric distance at J2000: {:.0}\n", - mars_geo.distance().to::() - ); +void section_center_transforms(const JulianDate &jd) { + std::puts("3) CENTER TRANSFORMS (HELIOCENTRIC -> GEOCENTRIC)"); + std::puts("-----------------------------------------------"); + + auto mars_helio = ephemeris::mars_heliocentric(jd); + auto mars_geo = mars_helio.to_center(jd); + + std::cout << "Mars geocentric distance at J2000: " << std::fixed + << std::setprecision(6) << mars_geo.distance() << std::endl; + std::cout << "Mars geocentric distance at J2000: " << std::fixed + << std::setprecision(0) << mars_geo.distance().to() + << std::endl; + std::puts(""); } -fn section_moon_and_lagrange_points(jd: JulianDate) { - println!("5) MOON + LAGRANGE POINTS"); - println!("-------------------------"); - - let moon_geo = Moon::get_geo_position::(jd); - println!( - "Moon geocentric distance (ELP2000): {:.1} ({:.6})", - moon_geo.distance(), - moon_geo.distance().to::() - ); - - println!("Lagrange points available in the catalog:"); - for lp in LAGRANGE_POINTS { - println!( - " {:<12} in {:<10} -> lon={:>7.2} lat={:>6.2} r={:>5.2}", - lp.name, - lp.parent_system, - lp.position.azimuth, - lp.position.polar, - lp.position.distance - ); - } - println!(); +void section_moon(const JulianDate &jd) { + std::puts("4) MOON (ELP2000)"); + std::puts("-----------------"); + + auto moon_geo = ephemeris::moon_geocentric(jd); + std::cout << "Moon geocentric distance (ELP2000): " << std::fixed + << std::setprecision(1) << moon_geo.distance() + << " (" << std::setprecision(6) + << moon_geo.distance().to() << ")" + << std::endl; + std::puts(""); } -fn section_trait_dispatch(jd: JulianDate) { - println!("6) TRAIT-BASED DISPATCH (calculus::vsop87::VSOP87)"); - println!("-------------------------------------------------"); - - let dynamic_planets: [(&str, &dyn VSOP87); 4] = [ - ("Mercury", &Mercury), - ("Venus", &Venus), - ("Earth", &Earth), - ("Mars", &Mars), - ]; - - for (name, planet) in dynamic_planets { - let helio = planet.vsop87a(jd); - let bary = planet.vsop87e(jd); - println!( - "{:<8} helio={:>8.5} bary={:>8.5}", - name, - helio.distance(), - bary.distance() - ); +void section_trait_dispatch(const JulianDate &jd) { + std::puts("5) EPHEMERIS DISPATCH (all inner planets)"); + std::puts("-----------------------------------------"); + + // In C++ there is no dynamic trait dispatch for VSOP87. + // Instead, we call each planet's ephemeris directly. + struct PlanetEphem { + const char *name; + decltype(ephemeris::mercury_heliocentric) *helio; + decltype(ephemeris::mercury_barycentric) *bary; + }; + + const PlanetEphem planets[] = { + {"Mercury", &ephemeris::mercury_heliocentric, &ephemeris::mercury_barycentric}, + {"Venus", &ephemeris::venus_heliocentric, &ephemeris::venus_barycentric}, + {"Earth", &ephemeris::earth_heliocentric, &ephemeris::earth_barycentric}, + {"Mars", &ephemeris::mars_heliocentric, &ephemeris::mars_barycentric}, + }; + + for (auto &[name, helio_fn, bary_fn] : planets) { + auto helio = helio_fn(jd); + auto bary = bary_fn(jd); + std::cout << std::left << std::setw(8) << name + << " helio=" << std::fixed << std::setprecision(5) + << helio.distance() + << " bary=" << bary.distance() << std::endl; } - println!(); + std::puts(""); +} + +void section_custom_planet() { + std::puts("6) CUSTOM PLANET + ORBITAL PERIOD"); + std::puts("---------------------------------"); + + Planet demo_world{ + 5.972e24 * 2.0, // mass_kg: double the Earth + 6371.0 * 1.3, // radius_km: 30% bigger + Orbit{1.4, 0.07, 4.0, 120.0, 80.0, 10.0, JulianDate::J2000().value()} + }; + + auto period = orbit_period(demo_world.orbit); + + std::puts("Custom planet built at runtime:"); + std::printf(" mass = %.3e kg\n", demo_world.mass_kg); + std::printf(" radius = %.1f km\n", demo_world.radius_km); + std::printf(" a = %.6f AU\n", demo_world.orbit.semi_major_axis_au); + std::cout << " sidereal period = " << std::fixed << std::setprecision(2) + << period << "\n" << std::endl; } -fn section_planet_builder() { - println!("7) planets::Planet BUILDER + OrbitExt"); - println!("-------------------------------------"); - - let demo_world = Planet::builder() - .mass(Kilograms::new(5.972e24 * 2.0)) - .radius(Kilometers::new(6371.0 * 1.3)) - .orbit(Orbit::new( - AstronomicalUnits::new(1.4), - 0.07, - Degrees::new(4.0), - Degrees::new(120.0), - Degrees::new(80.0), - Degrees::new(10.0), - JulianDate::J2000, - )) - .build(); - - println!("Custom planet built at runtime:"); - println!(" mass = {}", demo_world.mass); - println!(" radius = {}", demo_world.radius); - println!(" a = {}", demo_world.orbit.semi_major_axis); - println!( - " sidereal period = {:.2}\n", - demo_world.orbit.period().to::() - ); +void section_current_snapshot(const JulianDate &now) { + std::puts("7) CURRENT SNAPSHOT"); + std::puts("-------------------"); + + auto earth_now = ephemeris::earth_heliocentric(now); + auto mars_now = ephemeris::mars_heliocentric(now); + auto mars_geo_now = mars_now.to_center(now); + + std::cout << "Earth-Sun distance now: " << std::fixed + << std::setprecision(6) << earth_now.distance() << std::endl; + std::cout << "Mars-Sun distance now: " << std::fixed + << std::setprecision(6) << mars_now.distance() << std::endl; + std::cout << "Mars-Earth distance now: " << std::fixed + << std::setprecision(6) << mars_geo_now.distance() + << " (" << std::setprecision(0) + << mars_geo_now.distance().to() << ")" + << std::endl; + + std::puts("\n=== End of example ==="); } -fn section_current_snapshot(now: JulianDate) { - println!("8) CURRENT SNAPSHOT"); - println!("-------------------"); - - let earth_now = Earth::vsop87a(now); - let mars_now = Mars::vsop87a(now); - let mars_geo_now: EclipticMeanJ2000 = mars_now.to_center(now); - - println!( - "Earth-Sun distance now: {:.6}", - earth_now.distance() - ); - println!( - "Mars-Sun distance now: {:.6}", - mars_now.distance() - ); - println!( - "Mars-Earth distance now: {:.6} ({:.0})", - mars_geo_now.distance(), - mars_geo_now.distance().to::() - ); - - println!("\n=== End of example ==="); +// ─── main ───────────────────────────────────────────────────────────────────── + +int main() { + JulianDate jd = JulianDate::J2000(); + JulianDate now = jd_now(); + + std::puts("=== Siderust Solar System Module Tour ===\n"); + std::cout << "Epoch used for deterministic outputs: J2000 (JD " << std::fixed + << std::setprecision(1) << jd << ")" << std::endl; + std::cout << "Current epoch snapshot: JD " << std::setprecision(6) + << now << "\n" << std::endl; + + section_planet_constants_and_periods(); + section_vsop87_positions(jd); + section_center_transforms(jd); + section_moon(jd); + section_trait_dispatch(jd); + section_custom_planet(); + section_current_snapshot(now); + + return 0; } diff --git a/examples/10_time_periods.cpp b/examples/10_time_periods.cpp index 34624bf..c4e7291 100644 --- a/examples/10_time_periods.cpp +++ b/examples/10_time_periods.cpp @@ -1,158 +1,217 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Vallés Puig, Ramon -//! Time Scales, Formats, and Period Conversions Example -//! -//! Run with: `cargo run --example 05_time_periods` -//! -//! This example demonstrates `tempoch` (re-exported as `siderust::time`): -//! - Constructing an instant from `chrono::DateTime` -//! - Viewing the same absolute instant in each supported time scale: -//! `JD`, `JDE`, `MJD`, `TDB`, `TT`, `TAI`, `TCG`, `TCB`, `GPS`, `UnixTime`, `UT` -//! - Using the common type aliases: `JulianDate`, `JulianEphemerisDay`, -//! `ModifiedJulianDate`, `UniversalTime` -//! - Converting `Period` between scales and to/from `UtcPeriod` -//! -//! Notes: -//! - Scale conversions route through the canonical `JD(TT)` representation. -//! - `UT` is Earth-rotation based; `Time::::delta_t()` exposes `ΔT = TT − UT`. - -use chrono::{DateTime, Duration, Utc}; -use qtty::{Days, Second}; -use siderust::time::{ - Interval, JulianDate, JulianEphemerisDay, ModifiedJulianDate, Period, Time, TimeScale, - UniversalTime, UnixTime, UtcPeriod, GPS, JD, JDE, MJD, TAI, TCB, TCG, TDB, TT, UT, -}; - -fn print_scale(label: &str, time: Time, reference_jd: JulianDate) { - let jd_back: JulianDate = time.to::(); - let drift_s = (jd_back - reference_jd).to::(); - println!( - " {:<8} value = {:>16.9} | JD roundtrip drift = {:>11.3e} s", - label, time, drift_s - ); +/// @file 10_time_periods.cpp +/// @brief Time Scales, Formats, and Period Conversions Example. +/// +/// Demonstrates `tempoch` (re-exported via `siderust::time`): +/// - Viewing the same absolute instant in every supported time scale +/// - Using the common type aliases (`JulianDate`, `MJD`, `TDB`, `TT`, …) +/// - Converting `Period` between scales +/// - ΔT = TT − UT1 + +#include + +#include +#include +#include + +using namespace siderust; + +// ── Helper: print a single scale value + JD round-trip drift ──────────────── + +template +void print_scale(const char *label, + const tempoch::Time &time, + const JulianDate &reference_jd) { + // Convert back to JD so we can measure round-trip drift. + auto jd_back = time.template to(); + double drift_days = jd_back.value() - reference_jd.value(); + double drift_s = drift_days * 86400.0; + std::cout << " " << std::left << std::setw(8) << label + << " value = " << std::right << std::setw(16) << std::fixed + << std::setprecision(9) << time + << " | JD roundtrip drift = " << std::scientific + << std::setprecision(3) << drift_s << " s" << std::endl; } -fn print_period(label: &str, period: &Period) { - println!( - " {:<8} [{:>16.9}] Δ = {}", - label, - period, - period.duration_days() - ); +// ── Helper: print a Period in a given scale ───────────────────────────────── + +template +void print_period(const char *label, const tempoch::Period &period) { + auto dur = period.template duration(); + std::cout << " " << std::left << std::setw(8) << label << std::right + << " [" << std::fixed << std::setprecision(9) + << period.start() << ", " << period.end() << "]" + << " \u0394 = " << dur << std::endl; } -fn main() { - println!("Time Scales, Formats, and Period Conversions"); - println!("============================================\n"); - - let utc_ref = DateTime::::from_timestamp(946_728_000, 0).expect("valid UTC timestamp"); - let jd: JulianDate = JulianDate::from_utc(utc_ref); - - let jde: JulianEphemerisDay = jd.to::(); - let mjd: ModifiedJulianDate = jd.to::(); - let tdb: Time = jd.to::(); - let tt: Time = jd.to::(); - let tai: Time = jd.to::(); - let tcg: Time = jd.to::(); - let tcb: Time = jd.to::(); - let gps: Time = jd.to::(); - let unix: Time = jd.to::(); - let ut: UniversalTime = jd.to::(); - - println!("Reference UTC instant: {}\n", utc_ref.to_rfc3339()); - - println!("1) Each supported time scale for the same instant:"); - print_scale("JD", jd, jd); - print_scale("JDE", jde, jd); - print_scale("MJD", mjd, jd); - print_scale("TDB", tdb, jd); - print_scale("TT", tt, jd); - print_scale("TAI", tai, jd); - print_scale("TCG", tcg, jd); - print_scale("TCB", tcb, jd); - print_scale("GPS", gps, jd); - print_scale("Unix", unix, jd); - print_scale("UT", ut, jd); - println!( - " {:<8} delta_t = {:.3} s (TT - UT)\n", - "UT", - ut.delta_t().value() - ); - - println!("2) Time formats / aliases:"); - println!(" JulianDate alias: {}", jd); - println!(" JulianEphemerisDay alias: {}", jde); - println!(" ModifiedJulianDate alias: {}", mjd); - println!(" UniversalTime alias: {}", ut); - let utc_roundtrip = jd.to_utc().expect("JD should convert back to UTC"); - println!( - " UTC roundtrip from JD: {}\n", - utc_roundtrip.to_rfc3339() - ); - - println!("3) Period representations and conversions:"); - let period_jd: Period = Period::new(jd, jd + Days::new(0.5)); - let period_jde: Period = period_jd.to::().expect("JD -> JDE period conversion"); - let period_mjd: Period = period_jd.to::().expect("JD -> MJD period conversion"); - let period_tdb: Period = period_jd.to::().expect("JD -> TDB period conversion"); - let period_tt: Period = period_jd.to::().expect("JD -> TT period conversion"); - let period_tai: Period = period_jd.to::().expect("JD -> TAI period conversion"); - let period_tcg: Period = period_jd.to::().expect("JD -> TCG period conversion"); - let period_tcb: Period = period_jd.to::().expect("JD -> TCB period conversion"); - let period_gps: Period = period_jd.to::().expect("JD -> GPS period conversion"); - let period_unix: Period = period_jd - .to::() - .expect("JD -> Unix period conversion"); - let period_ut: Period = period_jd.to::().expect("JD -> UT period conversion"); - let period_utc: UtcPeriod = period_jd - .to::>() - .expect("JD -> UTC period conversion"); - - print_period("JD", &period_jd); - print_period("JDE", &period_jde); - print_period("MJD", &period_mjd); - print_period("TDB", &period_tdb); - print_period("TT", &period_tt); - print_period("TAI", &period_tai); - print_period("TCG", &period_tcg); - print_period("TCB", &period_tcb); - print_period("GPS", &period_gps); - print_period("Unix", &period_unix); - print_period("UT", &period_ut); - println!( - " {:<8} [{} -> {}] Δ = {:.6} days ({:.0} s)\n", - "UTC", - period_utc.start.to_rfc3339(), - period_utc.end.to_rfc3339(), - period_utc.duration_days(), - period_utc.duration_seconds() - ); - - println!("4) UtcPeriod / Interval> conversions back to typed periods:"); - let utc_window: UtcPeriod = Interval::new(utc_ref, utc_ref + Duration::hours(6)); - let from_utc_jd: Period = utc_window.to::(); - let from_utc_mjd: Period = utc_window.to::(); - let from_utc_ut: Period = utc_window.to::(); - let from_utc_unix: Period = utc_window.to::(); - - println!( - " UTC [{} -> {}] Δ = {:.6} days", - utc_window.start.to_rfc3339(), - utc_window.end.to_rfc3339(), - utc_window.duration_days() - ); - print_period("JD", &from_utc_jd); - print_period("MJD", &from_utc_mjd); - print_period("UT", &from_utc_ut); - print_period("Unix", &from_utc_unix); - - let utc_roundtrip_period: UtcPeriod = from_utc_mjd - .to::>() - .expect("MJD -> UTC period conversion"); - println!( - " UTC<-MJD [{} -> {}]", - utc_roundtrip_period.start.to_rfc3339(), - utc_roundtrip_period.end.to_rfc3339() - ); +int main() { + std::cout << "Time Scales, Formats, and Period Conversions\n" + << "============================================\n\n"; + + // ── Reference instant: 2000-01-01T12:00:00 UTC ────────────────────────── + + auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); + + // Convert to every supported scale. + auto jde = jd.to(); + auto mjd = jd.to(); + auto tdb = jd.to(); + auto tt = jd.to(); + auto tai = jd.to(); + auto tcg = jd.to(); + auto tcb = jd.to(); + auto gps = jd.to(); + auto unix_t = jd.to(); + auto ut = jd.to(); + + auto utc_civil = jd.to_utc(); + + std::cout << "Reference UTC instant: " + << utc_civil.year << "-" + << std::setfill('0') << std::setw(2) << (int)utc_civil.month << "-" + << std::setw(2) << (int)utc_civil.day << "T" + << std::setw(2) << (int)utc_civil.hour << ":" + << std::setw(2) << (int)utc_civil.minute << ":" + << std::setw(2) << (int)utc_civil.second + << std::setfill(' ') + << "\n\n"; + + // ── 1) Each supported time scale for the same instant ─────────────────── + + std::cout << "1) Each supported time scale for the same instant:\n"; + print_scale("JD", jd, jd); + print_scale("JDE", jde, jd); + print_scale("MJD", mjd, jd); + print_scale("TDB", tdb, jd); + print_scale("TT", tt, jd); + print_scale("TAI", tai, jd); + print_scale("TCG", tcg, jd); + print_scale("TCB", tcb, jd); + print_scale("GPS", gps, jd); + print_scale("Unix", unix_t, jd); + print_scale("UT", ut, jd); + + auto delta_t = ut.delta_t(); + std::cout << " " << std::left << std::setw(8) << "UT" << std::right + << " delta_t = " << std::fixed << std::setprecision(3) + << delta_t << " (TT - UT)\n" << std::endl; + + // ── 2) Time formats / aliases ─────────────────────────────────────────── + + std::cout << "2) Time formats / aliases:\n"; + std::cout << " JulianDate alias: " << std::fixed + << std::setprecision(9) << jd << std::endl; + std::cout << " JDE (JulianEphemeris): " << jde << std::endl; + std::cout << " ModifiedJulianDate alias: " << mjd << std::endl; + std::cout << " UniversalTime alias: " << ut << std::endl; + + auto utc_rt = jd.to_utc(); + std::cout << " UTC roundtrip from JD: " + << utc_rt.year << "-" + << std::setfill('0') << std::setw(2) << (int)utc_rt.month << "-" + << std::setw(2) << (int)utc_rt.day << "T" + << std::setw(2) << (int)utc_rt.hour << ":" + << std::setw(2) << (int)utc_rt.minute << ":" + << std::setw(2) << (int)utc_rt.second + << std::setfill(' ') + << "\n\n"; + + // ── 3) Period representations and conversions ─────────────────────────── + + std::cout << "3) Period representations and conversions:\n"; + + auto jd_end = JulianDate(jd.value() + 0.5); + tempoch::Period period_jd(jd, jd_end); + + // Convert period endpoints to each scale via .to<>(). + auto mk_period = [](auto start_jd, auto end_jd, auto /*tag*/) { + using S = std::decay_t())>; + (void)sizeof(S); // suppress unused + auto s = start_jd; + auto e = end_jd; + return tempoch::Period(s, e); + }; + (void)mk_period; + + tempoch::Period period_jde(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_mjd(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_tdb(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_tt(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_tai(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_tcg(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_tcb(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_gps(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_unix(period_jd.start().to(), + period_jd.end().to()); + tempoch::Period period_ut(period_jd.start().to(), + period_jd.end().to()); + + print_period("JD", period_jd); + print_period("JDE", period_jde); + print_period("MJD", period_mjd); + print_period("TDB", period_tdb); + print_period("TT", period_tt); + print_period("TAI", period_tai); + print_period("TCG", period_tcg); + print_period("TCB", period_tcb); + print_period("GPS", period_gps); + print_period("Unix", period_unix); + print_period("UT", period_ut); + + // UTC period via CivilTime + auto utc_start = period_jd.start().to_utc(); + auto utc_end = period_jd.end().to_utc(); + tempoch::Period period_utc(utc_start, utc_end); + auto utc_dur = period_utc.duration(); + std::cout << " UTC [" << utc_start << " -> " << utc_end + << "] Δ = " << utc_dur << "\n\n"; + + // ── 4) UTC ↔ typed period conversions ─────────────────────────────────── + + std::cout << "4) UtcPeriod / CivilTime period conversions back to typed periods:\n"; + + auto utc_ref = tempoch::CivilTime{2000, 1, 1, 12, 0, 0}; + auto utc_ref_end = tempoch::CivilTime{2000, 1, 1, 18, 0, 0}; + tempoch::Period utc_window(utc_ref, utc_ref_end); + + std::cout << " UTC [" << utc_ref << " -> " << utc_ref_end + << "] Δ = " << utc_window.duration() << "\n"; + + // Convert UTC endpoints → JD → print + auto from_utc_jd_start = JulianDate::from_utc(utc_ref); + auto from_utc_jd_end = JulianDate::from_utc(utc_ref_end); + tempoch::Period from_utc_jd(from_utc_jd_start, from_utc_jd_end); + print_period("JD", from_utc_jd); + + // → MJD + tempoch::Period from_utc_mjd(from_utc_jd_start.to(), + from_utc_jd_end.to()); + print_period("MJD", from_utc_mjd); + + // → UT + tempoch::Period from_utc_ut(from_utc_jd_start.to(), + from_utc_jd_end.to()); + print_period("UT", from_utc_ut); + + // → UnixTime + tempoch::Period from_utc_unix(from_utc_jd_start.to(), + from_utc_jd_end.to()); + print_period("Unix", from_utc_unix); + + // Roundtrip: MJD → CivilTime + auto utc_rt_start = from_utc_mjd.start().to_utc(); + auto utc_rt_end = from_utc_mjd.end().to_utc(); + std::cout << " UTC<-MJD [" << utc_rt_start << " -> " << utc_rt_end << "]\n"; + + return 0; } diff --git a/examples/11_serde_serialization.cpp b/examples/11_serde_serialization.cpp index f16676d..a870f39 100644 --- a/examples/11_serde_serialization.cpp +++ b/examples/11_serde_serialization.cpp @@ -1,192 +1,263 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Vallés Puig, Ramon -//! Serde serialization examples. -//! -//! Run with: `cargo run --example 17_serde_serialization --features serde` - -use qtty::*; -use serde::{Deserialize, Serialize}; -use siderust::astro::orbit::Orbit; -use siderust::bodies::comet::HALLEY; -use siderust::bodies::solar_system::{Earth, Mars, Moon}; -use siderust::coordinates::{cartesian, centers, frames, spherical}; -use siderust::targets::{Target, Trackable}; -use siderust::time::{JulianDate, ModifiedJulianDate}; -use std::fs; - -#[derive(Debug, Serialize, Deserialize)] -struct TimeBundle { - j2000: JulianDate, - mjd: ModifiedJulianDate, - timeline: Vec, +/// @file 11_serde_serialization.cpp +/// @brief Manual JSON-like serialization examples. +/// +/// Demonstrates how to export siderust data as JSON strings using standard +/// C++ I/O (no external JSON library needed). Sections mirror the Rust +/// serde example: +/// +/// 1) Time objects +/// 2) Coordinate objects +/// 3) Body-related objects (orbit + ephemeris snapshot) +/// 4) Target coordinate snapshots +/// 5) File I/O (write → read → verify) +/// +/// Build & run: +/// cmake --build build-local --target 11_serde_serialization_example +/// ./build-local/11_serde_serialization_example + +#include + +#include +#include +#include +#include +#include +#include + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; + +// ─── JSON formatting helpers ──────────────────────────────────────────────── + +inline std::string json_number(double v, int prec = 6) { + std::ostringstream os; + os << std::fixed << std::setprecision(prec) << v; + return os.str(); } -#[derive(Debug, Serialize, Deserialize)] -struct CoordinateBundle { - geo_icrs_cart: - cartesian::Position, - helio_ecl_sph: - spherical::Position, - observer_site: centers::Geodetic, +inline std::string json_string(const std::string &s) { + return "\"" + s + "\""; } -#[derive(Debug, Serialize, Deserialize)] -struct BodySnapshot { - name: String, - epoch: JulianDate, - orbit: Orbit, - heliocentric_ecliptic: - cartesian::Position, +// ─── Section 1: Time objects ──────────────────────────────────────────────── + +void section_times() { + std::puts("1) TIME OBJECTS"); + std::puts("---------------"); + + JulianDate jd = JulianDate::J2000(); + auto mjd = jd.to(); + JulianDate jd_plus1(jd.value() + 1.0); + JulianDate jd_plus7(jd.value() + 7.0); + + // Pretty-print a TimeBundle-like JSON + std::puts("{"); + std::printf(" \"j2000\": %s,\n", json_number(jd.value(), 1).c_str()); + std::printf(" \"mjd\": %s,\n", json_number(mjd.value(), 1).c_str()); + std::printf(" \"timeline\": [\n"); + std::printf(" %s,\n", json_number(jd.value(), 1).c_str()); + std::printf(" %s,\n", json_number(jd_plus1.value(), 1).c_str()); + std::printf(" %s\n", json_number(jd_plus7.value(), 1).c_str()); + std::puts(" ]"); + std::puts("}"); + + std::cout << "Roundtrip check: j2000=" << std::fixed << std::setprecision(1) + << jd << ", timeline_len=3\n" << std::endl; } -#[derive(Debug, Serialize, Deserialize)] -struct BodyTargetsBundle { - // Mars `Trackable` output is already a CoordinateWithPM<...>, i.e. Target<...>. - mars_bary_target: - Target>, - // Moon does not return CoordinateWithPM from `track`, so we wrap a snapshot. - moon_geo_target: - Target>, +// ─── Section 2: Coordinate objects ────────────────────────────────────────── + +void section_coordinates() { + std::puts("2) COORDINATE OBJECTS"); + std::puts("---------------------"); + + // Geocentric ICRS cartesian (km) + cartesian::Position geo_icrs_cart( + 6371.0, 0.0, 0.0); + + // Heliocentric ecliptic spherical (AU) + spherical::Position + helio_ecl_sph(qtty::Degree(120.0), qtty::Degree(5.0), + qtty::AstronomicalUnit(1.2)); + + // Observer site (geodetic) + Geodetic observer_site(-17.8947, 28.7636, 2396.0); + + std::puts("{"); + std::printf(" \"geo_icrs_cart\": { \"x\": %s, \"y\": %s, \"z\": %s, \"unit\": \"km\" },\n", + json_number(geo_icrs_cart.x().value(), 1).c_str(), + json_number(geo_icrs_cart.y().value(), 1).c_str(), + json_number(geo_icrs_cart.z().value(), 1).c_str()); + std::printf(" \"helio_ecl_sph\": { \"lon\": %s, \"lat\": %s, \"r\": %s, \"unit\": \"AU\" },\n", + json_number(helio_ecl_sph.direction().lon().value(), 1).c_str(), + json_number(helio_ecl_sph.direction().lat().value(), 1).c_str(), + json_number(helio_ecl_sph.distance().value(), 1).c_str()); + std::printf(" \"observer_site\": { \"lon\": %s, \"lat\": %s, \"height_m\": %s }\n", + json_number(observer_site.lon.value(), 4).c_str(), + json_number(observer_site.lat.value(), 4).c_str(), + json_number(observer_site.height.value(), 1).c_str()); + std::puts("}"); + + std::cout << "Roundtrip check: x=" << std::fixed << std::setprecision(1) + << geo_icrs_cart.x() << ", lon=" + << std::setprecision(4) << observer_site.lon << "\n" << std::endl; +} + +// ─── Section 3: Body-related objects ──────────────────────────────────────── + +struct BodySnapshotJSON { + std::string name; + JulianDate epoch; + Orbit orbit; + cartesian::position::EclipticMeanJ2000 helio_ecl; + + std::string to_json(int indent = 2) const { + std::string pad(indent, ' '); + std::ostringstream os; + os << "{\n" + << pad << "\"name\": " << json_string(name) << ",\n" + << pad << "\"epoch\": " << json_number(epoch.value(), 1) << ",\n" + << pad << "\"orbit\": {\n" + << pad << " \"semi_major_axis_au\": " << json_number(orbit.semi_major_axis_au) << ",\n" + << pad << " \"eccentricity\": " << json_number(orbit.eccentricity) << ",\n" + << pad << " \"inclination_deg\": " << json_number(orbit.inclination_deg) << ",\n" + << pad << " \"lon_ascending_node_deg\": " << json_number(orbit.lon_ascending_node_deg) << ",\n" + << pad << " \"arg_perihelion_deg\": " << json_number(orbit.arg_perihelion_deg) << ",\n" + << pad << " \"mean_anomaly_deg\": " << json_number(orbit.mean_anomaly_deg) << ",\n" + << pad << " \"epoch_jd\": " << json_number(orbit.epoch_jd, 1) << "\n" + << pad << "},\n" + << pad << "\"heliocentric_ecliptic\": {\n" + << pad << " \"x\": " << json_number(helio_ecl.x().value()) << ",\n" + << pad << " \"y\": " << json_number(helio_ecl.y().value()) << ",\n" + << pad << " \"z\": " << json_number(helio_ecl.z().value()) << "\n" + << pad << "}\n" + << "}"; + return os.str(); + } +}; + +void section_body_objects(const JulianDate &jd) { + std::puts("3) BODY-RELATED OBJECTS"); + std::puts("-----------------------"); + + BodySnapshotJSON earth_snap{ + "Earth", jd, EARTH.orbit, ephemeris::earth_heliocentric(jd)}; + + // Halley's comet + Orbit halley_orb{17.834, 0.96714, 162.26, 58.42, 111.33, 38.38, 2446467.4}; + auto halley_pos = kepler_position(halley_orb, jd); + BodySnapshotJSON halley_snap{"Halley", jd, halley_orb, halley_pos}; + + std::puts("Earth snapshot JSON:"); + std::puts(earth_snap.to_json().c_str()); + std::puts("Halley snapshot JSON:"); + std::puts(halley_snap.to_json().c_str()); + + std::cout << "Roundtrip check: " << halley_snap.name + << " @ JD " << std::fixed << std::setprecision(1) + << halley_snap.epoch << ", r=" + << std::setprecision(6) << halley_snap.helio_ecl.distance() + << "\n" << std::endl; } -fn pretty_json(value: &T) -> String { - serde_json::to_string_pretty(value).expect("serialize to pretty JSON") +// ─── Section 4: Target objects ────────────────────────────────────────────── + +void section_targets(const JulianDate &jd) { + std::puts("4) TARGET OBJECTS"); + std::puts("-----------------"); + + // Mars barycentric target + auto mars_bary = ephemeris::mars_barycentric(jd); + // Moon geocentric target + auto moon_geo = ephemeris::moon_geocentric(jd); + + std::puts("{"); + std::printf(" \"mars_bary_target\": {\n"); + std::printf(" \"time\": %s,\n", json_number(jd.value(), 1).c_str()); + std::printf(" \"position\": { \"x\": %s, \"y\": %s, \"z\": %s }\n", + json_number(mars_bary.x().value()).c_str(), + json_number(mars_bary.y().value()).c_str(), + json_number(mars_bary.z().value()).c_str()); + std::printf(" },\n"); + std::printf(" \"moon_geo_target\": {\n"); + std::printf(" \"time\": %s,\n", json_number(jd.value(), 1).c_str()); + std::printf(" \"position\": { \"x\": %s, \"y\": %s, \"z\": %s }\n", + json_number(moon_geo.x().value()).c_str(), + json_number(moon_geo.y().value()).c_str(), + json_number(moon_geo.z().value()).c_str()); + std::printf(" }\n"); + std::puts("}"); + + std::cout << "Roundtrip check: Mars target JD " << std::fixed + << std::setprecision(1) << jd + << ", Moon target JD " << jd << "\n" << std::endl; } -fn roundtrip(value: &T) -> T -where - T: Serialize + for<'de> Deserialize<'de>, -{ - let json = serde_json::to_string(value).expect("serialize"); - serde_json::from_str(&json).expect("deserialize") +// ─── Section 5: File I/O ──────────────────────────────────────────────────── + +void section_file_io(const JulianDate &jd) { + std::puts("5) FILE I/O"); + std::puts("----------"); + + const std::string out_path = "/tmp/siderust_serde_example_targets.json"; + + // Build a JSON string for the targets + auto mars_bary = ephemeris::mars_barycentric(jd); + auto moon_geo = ephemeris::moon_geocentric(jd); + + std::ostringstream json; + json << std::fixed << std::setprecision(6); + json << "{\n"; + json << " \"mars_bary_target\": {\n"; + json << " \"time\": " << jd.value() << ",\n"; + json << " \"position\": { \"x\": " << mars_bary.x().value() + << ", \"y\": " << mars_bary.y().value() + << ", \"z\": " << mars_bary.z().value() << " }\n"; + json << " },\n"; + json << " \"moon_geo_target\": {\n"; + json << " \"time\": " << jd.value() << ",\n"; + json << " \"position\": { \"x\": " << moon_geo.x().value() + << ", \"y\": " << moon_geo.y().value() + << ", \"z\": " << moon_geo.z().value() << " }\n"; + json << " }\n"; + json << "}"; + + // Write + { + std::ofstream ofs(out_path); + ofs << json.str(); + } + + // Read back and verify + { + std::ifstream ifs(out_path); + std::string content((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + if (!content.empty()) { + std::printf("Saved and loaded: %s (%zu bytes)\n", + out_path.c_str(), content.size()); + } else { + std::puts("Error: file I/O failed."); + } + } } -fn main() { - println!("=== Siderust Serde Serialization Examples ===\n"); - - let jd = JulianDate::J2000; - - // ========================================================================= - // 1) Times - // ========================================================================= - println!("1) TIME OBJECTS"); - println!("---------------"); - - let time_bundle = TimeBundle { - j2000: jd, - mjd: ModifiedJulianDate::from(jd), - timeline: vec![jd, jd + Days::new(1.0), jd + Days::new(7.0)], - }; - println!("{}", pretty_json(&time_bundle)); - - let recovered_times: TimeBundle = roundtrip(&time_bundle); - println!( - "Roundtrip check: j2000={:.1}, timeline_len={}\n", - recovered_times.j2000.value(), - recovered_times.timeline.len() - ); - - // ========================================================================= - // 2) Coordinates - // ========================================================================= - println!("2) COORDINATE OBJECTS"); - println!("---------------------"); - - let coords = CoordinateBundle { - geo_icrs_cart: - cartesian::Position::::new( - 6371.0, 0.0, 0.0, - ), - helio_ecl_sph: - spherical::Position::::new_raw( - Degrees::new(5.0), // lat - Degrees::new(120.0), // lon - AstronomicalUnits::new(1.2), - ), - observer_site: - centers::Geodetic::::new( - Degrees::new(-17.8947), // lon - Degrees::new(28.7636), // lat - Meters::new(2396.0), // height - ), - }; - println!("{}", pretty_json(&coords)); - - let recovered_coords: CoordinateBundle = roundtrip(&coords); - println!( - "Roundtrip check: x={:.1} km, lon={:.4} deg\n", - recovered_coords.geo_icrs_cart.x().value(), - recovered_coords.observer_site.lon.value() - ); - - // ========================================================================= - // 3) Body-related objects: orbit + ephemeris snapshots - // ========================================================================= - println!("3) BODY-RELATED OBJECTS"); - println!("-----------------------"); - - // NOTE: - // `Planet`/`Star` structs are not serde-derived in the current API. - // We serialize body-related data that *is* serde-ready: orbit elements - // and concrete coordinate snapshots. - let earth_snapshot = BodySnapshot { - name: "Earth".to_string(), - epoch: jd, - orbit: siderust::bodies::EARTH.orbit, - heliocentric_ecliptic: Earth::vsop87a(jd), - }; - let halley_snapshot = BodySnapshot { - name: HALLEY.name.to_string(), - epoch: jd, - orbit: HALLEY.orbit, - heliocentric_ecliptic: HALLEY.orbit.kepler_position(jd), - }; - - println!("Earth snapshot JSON:"); - println!("{}", pretty_json(&earth_snapshot)); - println!("Halley snapshot JSON:"); - println!("{}", pretty_json(&halley_snapshot)); - - let recovered_halley: BodySnapshot = roundtrip(&halley_snapshot); - println!( - "Roundtrip check: {} @ JD {:.1}, r={:.6} AU\n", - recovered_halley.name, - recovered_halley.epoch.value(), - recovered_halley.heliocentric_ecliptic.distance().value() - ); - - // ========================================================================= - // 4) Target objects (CoordinateWithPM alias) - // ========================================================================= - println!("4) TARGET OBJECTS"); - println!("-----------------"); - - let mars_target = Mars.track(jd); - let moon_target = Target::new_static(Moon.track(jd), jd); - - let targets = BodyTargetsBundle { - mars_bary_target: mars_target, - moon_geo_target: moon_target, - }; - println!("{}", pretty_json(&targets)); - - let recovered_targets: BodyTargetsBundle = roundtrip(&targets); - println!( - "Roundtrip check: Mars target JD {:.1}, Moon target JD {:.1}\n", - recovered_targets.mars_bary_target.time.value(), - recovered_targets.moon_geo_target.time.value() - ); - - // ========================================================================= - // 5) File I/O - // ========================================================================= - println!("5) FILE I/O"); - println!("----------"); - - let out_path = "/tmp/siderust_serde_example_targets.json"; - fs::write(out_path, pretty_json(&targets)).expect("write JSON file"); - let loaded = fs::read_to_string(out_path).expect("read JSON file"); - let _: BodyTargetsBundle = serde_json::from_str(&loaded).expect("deserialize loaded JSON"); - println!("Saved and loaded: {}", out_path); +// ─── main ───────────────────────────────────────────────────────────────────── + +int main() { + std::puts("=== Siderust Manual Serialization Examples ===\n"); + + JulianDate jd = JulianDate::J2000(); + + section_times(); + section_coordinates(); + section_body_objects(jd); + section_targets(jd); + section_file_io(jd); + + return 0; } diff --git a/include/siderust/ephemeris.hpp b/include/siderust/ephemeris.hpp index 3d1cd5e..f12f1fc 100644 --- a/include/siderust/ephemeris.hpp +++ b/include/siderust/ephemeris.hpp @@ -92,6 +92,127 @@ venus_heliocentric(const JulianDate &jd) { out); } +/** + * @brief Mercury's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +mercury_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_mercury_heliocentric(jd.value(), &out), + "ephemeris::mercury_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c(out); +} + +/** + * @brief Mercury's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +mercury_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_mercury_barycentric(jd.value(), &out), + "ephemeris::mercury_barycentric"); + return cartesian::position::HelioBarycentric::from_c(out); +} + +/** + * @brief Venus's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +venus_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_venus_barycentric(jd.value(), &out), + "ephemeris::venus_barycentric"); + return cartesian::position::HelioBarycentric::from_c(out); +} + +/** + * @brief Jupiter's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +jupiter_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_jupiter_heliocentric(jd.value(), &out), + "ephemeris::jupiter_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c(out); +} + +/** + * @brief Jupiter's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +jupiter_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_jupiter_barycentric(jd.value(), &out), + "ephemeris::jupiter_barycentric"); + return cartesian::position::HelioBarycentric::from_c(out); +} + +/** + * @brief Saturn's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +saturn_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_saturn_heliocentric(jd.value(), &out), + "ephemeris::saturn_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c(out); +} + +/** + * @brief Saturn's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +saturn_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_saturn_barycentric(jd.value(), &out), + "ephemeris::saturn_barycentric"); + return cartesian::position::HelioBarycentric::from_c(out); +} + +/** + * @brief Uranus's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +uranus_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_uranus_heliocentric(jd.value(), &out), + "ephemeris::uranus_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c(out); +} + +/** + * @brief Uranus's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +uranus_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_uranus_barycentric(jd.value(), &out), + "ephemeris::uranus_barycentric"); + return cartesian::position::HelioBarycentric::from_c(out); +} + +/** + * @brief Neptune's heliocentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::EclipticMeanJ2000 +neptune_heliocentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_neptune_heliocentric(jd.value(), &out), + "ephemeris::neptune_heliocentric"); + return cartesian::position::EclipticMeanJ2000::from_c(out); +} + +/** + * @brief Neptune's barycentric position (EclipticMeanJ2000, AU) via VSOP87. + */ +inline cartesian::position::HelioBarycentric +neptune_barycentric(const JulianDate &jd) { + siderust_cartesian_pos_t out; + check_status(siderust_vsop87_neptune_barycentric(jd.value(), &out), + "ephemeris::neptune_barycentric"); + return cartesian::position::HelioBarycentric::from_c(out); +} + /** * @brief Moon's geocentric position (EclipticMeanJ2000, km) via ELP2000. */ diff --git a/include/siderust/time.hpp b/include/siderust/time.hpp index 126f781..2113368 100644 --- a/include/siderust/time.hpp +++ b/include/siderust/time.hpp @@ -17,6 +17,16 @@ using CivilTime = tempoch::CivilTime; using UTC = tempoch::UTC; // alias for CivilTime using JulianDate = tempoch::JulianDate; // Time using MJD = tempoch::MJD; // Time +using TDB = tempoch::TDB; // Time +using TT = tempoch::TT; // Time +using TAI = tempoch::TAI; // Time +using TCG = tempoch::TCG; // Time +using TCB = tempoch::TCB; // Time +using GPS = tempoch::GPS; // Time +using UT = tempoch::UT; // Time +using UniversalTime = tempoch::UniversalTime; // alias for UT +using JDE = tempoch::JDE; // Time +using UnixTime = tempoch::UnixTime; // Time using Period = tempoch::Period; } // namespace siderust diff --git a/siderust b/siderust index 8c8eeac..0655a52 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 8c8eeacbc9b3c4d2147261cf511f57633d3adc73 +Subproject commit 0655a52b8d0d27d0ee83abf87f884a11cd97b07a diff --git a/tempoch-cpp b/tempoch-cpp index b4bc764..e05858b 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit b4bc764eaa9b4d47104923b73fe4926741135fbc +Subproject commit e05858b9a75240ef15894dca91e176c5218a427e From 1dedecdac19f7a46e678584e2cfae22d49d7c9a7 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Sat, 28 Feb 2026 19:56:28 +0100 Subject: [PATCH 17/19] refactor: update examples and tests to use quantity literals for better readability --- examples/01_basic_coordinates.cpp | 10 +++---- examples/04_all_center_conversions.cpp | 25 +++++++++-------- examples/05_target_tracking.cpp | 13 ++++++--- examples/06_night_events.cpp | 3 +- examples/07_moon_properties.cpp | 10 +++---- examples/08_solar_system.cpp | 22 +++++++++------ examples/09_star_observability.cpp | 11 ++++---- examples/11_serde_serialization.cpp | 21 +++++++------- include/siderust/bodies.hpp | 37 ++++++++++++------------ include/siderust/lunar_phase.hpp | 12 ++++---- include/siderust/orbital_center.hpp | 4 ++- tests/test_altitude.cpp | 39 +++++++++++++------------- tests/test_bodies.cpp | 26 ++++++++--------- tests/test_bodycentric.cpp | 9 ++++-- 14 files changed, 131 insertions(+), 111 deletions(-) diff --git a/examples/01_basic_coordinates.cpp b/examples/01_basic_coordinates.cpp index dc418cb..5fb1cf0 100644 --- a/examples/01_basic_coordinates.cpp +++ b/examples/01_basic_coordinates.cpp @@ -57,8 +57,8 @@ int main() { // Create a star direction (Polaris approximately) spherical::direction::EquatorialMeanJ2000 polaris( - qtty::Degree(37.95), // Right Ascension (converted to degrees) - qtty::Degree(89.26) // Declination + 37.95_deg, // Right Ascension (converted to degrees) + 89.26_deg // Declination ); std::cout << "Polaris (Geocentric EquatorialMeanJ2000 Direction):" << std::endl; std::cout << std::setprecision(2); @@ -86,8 +86,8 @@ int main() { // Directions are unitless (implicit radius = 1) and frame-only (no center) // Note: Directions don't carry observer site — they're pure directions spherical::direction::Horizontal zenith( - qtty::Degree(0.0), // Azimuth (North — doesn't matter for zenith) - qtty::Degree(90.0) // Altitude (straight up) + 0.0_deg, // Azimuth (North — doesn't matter for zenith) + 90.0_deg // Altitude (straight up) ); std::cout << "Zenith direction (Horizontal frame):" << std::endl; std::cout << " Altitude = " << zenith.alt() << std::endl; @@ -96,7 +96,7 @@ int main() { // Convert direction to position at a specific distance // Build a spherical Position from the direction + distance (pure geometry) - auto cloud_distance = qtty::Kilometer(5000.0); + auto cloud_distance = 5000.0_km; spherical::Position cloud( zenith.az(), zenith.alt(), cloud_distance); std::cout << "Cloud at zenith, 5 km altitude (relative to geocenter):" << std::endl; diff --git a/examples/04_all_center_conversions.cpp b/examples/04_all_center_conversions.cpp index e285001..06b2dc4 100644 --- a/examples/04_all_center_conversions.cpp +++ b/examples/04_all_center_conversions.cpp @@ -22,6 +22,7 @@ using namespace siderust; using namespace siderust::frames; using namespace siderust::centers; +using namespace qtty::literals; using F = EclipticMeanJ2000; using U = qtty::AstronomicalUnit; @@ -101,12 +102,12 @@ int main() { // ── Bodycentric: Mars-like orbit (heliocentric reference) ────────────────── std::puts("\n── Bodycentric – Mars-like orbit (heliocentric ref) ───────────────────"); Orbit mars_orbit{ - 1.524, // semi_major_axis_au - 0.0934, // eccentricity - 1.85, // inclination_deg - 49.56, // lon_ascending_node_deg - 286.5, // arg_perihelion_deg - 19.41, // mean_anomaly_deg + 1.524_au, // semi_major_axis + 0.0934, // eccentricity + 1.85_deg, // inclination + 49.56_deg, // lon_ascending_node + 286.5_deg, // arg_perihelion + 19.41_deg, // mean_anomaly jd.value() }; auto mars_params = BodycentricParams::heliocentric(mars_orbit); @@ -118,12 +119,12 @@ int main() { // ── Bodycentric: ISS-like orbit (geocentric reference) ──────────────────── std::puts("\n── Bodycentric – ISS-like orbit (geocentric ref) ──────────────────────"); Orbit iss_orbit{ - 0.0000426, // ~6 378 km in AU - 0.001, // eccentricity - 51.6, // inclination_deg - 0.0, // lon_ascending_node_deg - 0.0, // arg_perihelion_deg - 0.0, // mean_anomaly_deg + 0.0000426_au, // ~6 378 km in AU + 0.001, // eccentricity + 51.6_deg, // inclination + 0.0_deg, // lon_ascending_node + 0.0_deg, // arg_perihelion + 0.0_deg, // mean_anomaly jd.value() }; auto iss_params = BodycentricParams::geocentric(iss_orbit); diff --git a/examples/05_target_tracking.cpp b/examples/05_target_tracking.cpp index 5613d29..cddbc30 100644 --- a/examples/05_target_tracking.cpp +++ b/examples/05_target_tracking.cpp @@ -26,6 +26,7 @@ using namespace siderust; using namespace siderust::frames; using namespace siderust::centers; +using namespace qtty::literals; // ─── Helper: simple coordinate snapshot (mirrors Rust's Target) ────────── @@ -46,7 +47,9 @@ struct Snapshot { inline Orbit halley_orbit() { // a = 17.834 AU, e = 0.96714, i = 162.26°, Ω = 58.42°, ω = 111.33°, // M = 38.38° at epoch JD 2446467.4 (≈1986 Feb 9). - return {17.834, 0.96714, 162.26, 58.42, 111.33, 38.38, 2446467.4}; + return {17.834_au, 0.96714, 162.26_deg, + 58.42_deg, 111.33_deg, 38.38_deg, + 2446467.4}; } // ─── Section 1: Trackable objects ─────────────────────────────────────────── @@ -55,7 +58,7 @@ void section_trackable_objects(const JulianDate &jd, const JulianDate &jd_next) std::puts("1) Trackable objects (ICRS, star, Sun, planet, Moon)"); // ICRS direction — time-invariant target - spherical::direction::ICRS fixed_icrs(qtty::Degree(120.0), qtty::Degree(22.5)); + spherical::direction::ICRS fixed_icrs(120.0_deg, 22.5_deg); ICRSTarget icrs_target(fixed_icrs, jd, "FixedICRS"); // Verify time-invariance: the ICRS direction coordinates are constant. @@ -117,7 +120,9 @@ void section_target_snapshots(const JulianDate &jd, const JulianDate &jd_next) { << halley_snap.position.distance() << std::endl; // DemoSat — satellite-like custom object with a geocentric orbit - Orbit demosat_orbit{1.0002, 0.001, 0.1, 35.0, 80.0, 10.0, jd.value()}; + Orbit demosat_orbit{1.0002_au, 0.001, + 0.1_deg, 35.0_deg, + 80.0_deg, 10.0_deg, jd.value()}; auto demosat_pos = kepler_position(demosat_orbit, jd); Snapshot> demosat_snap{ demosat_pos, jd}; @@ -161,7 +166,7 @@ void section_target_with_proper_motion(const JulianDate &jd) { // Betelgeuse approximate ICRS coordinates at J2000 // (RA ≈ 88.7929°, Dec ≈ +7.4071°) spherical::direction::ICRS betelgeuse_pos( - qtty::Degree(88.7929), qtty::Degree(7.4071)); + 88.7929_deg, 7.4071_deg); // Proper motion: µα* = 27.54 mas/yr, µδ = 10.86 mas/yr // Convert mas/yr → deg/yr diff --git a/examples/06_night_events.cpp b/examples/06_night_events.cpp index 74b9b5a..6b2902a 100644 --- a/examples/06_night_events.cpp +++ b/examples/06_night_events.cpp @@ -17,10 +17,11 @@ #include using namespace siderust; +using namespace qtty::literals; // Twilight threshold constants (same values as siderust::calculus::solar::night_types) namespace twilight { -constexpr auto HORIZON = qtty::Degree(0.0); +constexpr auto HORIZON = 0.0_deg; constexpr auto APPARENT_HORIZON = qtty::Degree(-0.833); constexpr auto CIVIL = qtty::Degree(-6.0); constexpr auto NAUTICAL = qtty::Degree(-12.0); diff --git a/examples/07_moon_properties.cpp b/examples/07_moon_properties.cpp index e8d4482..36cdd34 100644 --- a/examples/07_moon_properties.cpp +++ b/examples/07_moon_properties.cpp @@ -12,13 +12,13 @@ #include -#include #include #include #include #include using namespace siderust; +using namespace qtty::literals; /// Helper: print a list of MJD periods with their durations. void print_periods(const std::string &label, @@ -54,8 +54,6 @@ int main() { auto geo = moon::phase_geocentric(jd); auto topo = moon::phase_topocentric(jd, site); - constexpr double RAD_TO_DEG = 180.0 / M_PI; - std::cout << std::fixed; std::cout << "Moon phase at 2026-03-01 00:00 UTC" << std::endl; std::cout << "==================================" << std::endl; @@ -71,9 +69,9 @@ int main() { std::cout << " illuminated percent : " << illuminated_percent(geo) << " %" << std::endl; std::cout << " phase angle : " - << geo.phase_angle_rad * RAD_TO_DEG << " deg" << std::endl; + << geo.phase_angle.to() << std::endl; std::cout << " elongation : " - << geo.elongation_rad * RAD_TO_DEG << " deg" << std::endl; + << geo.elongation.to() << std::endl; std::cout << " waxing : " << std::boolalpha << geo.waxing << std::endl; @@ -88,7 +86,7 @@ int main() { << (topo.illuminated_fraction - geo.illuminated_fraction) * 100.0 << std::noshowpos << " %" << std::endl; std::cout << " elongation : " - << topo.elongation_rad * RAD_TO_DEG << " deg" << std::endl; + << topo.elongation.to() << std::endl; // ========================================================================= // 2) Principal phase events diff --git a/examples/08_solar_system.cpp b/examples/08_solar_system.cpp index 1b4bb31..cfbb9fe 100644 --- a/examples/08_solar_system.cpp +++ b/examples/08_solar_system.cpp @@ -27,6 +27,7 @@ using namespace siderust; using namespace siderust::frames; using namespace siderust::centers; +using namespace qtty::literals; // ─── Kepler's 3rd law: compute orbital period from semi-major axis ────────── @@ -35,7 +36,7 @@ static constexpr double GM_SUN_AU3_DAY2 = 0.01720209895 * 0.01720209895; /// Sidereal period via Kepler's 3rd law: T = 2π √(a³/μ) [days]. inline qtty::Day orbit_period(const Orbit &orb) { - double a = orb.semi_major_axis_au; + double a = orb.semi_major_axis.value(); double T = 2.0 * M_PI * std::sqrt(a * a * a / GM_SUN_AU3_DAY2); return qtty::Day(T); } @@ -73,7 +74,7 @@ void section_planet_constants_and_periods() { for (auto &[name, p] : planets) { auto period = orbit_period(p->orbit); std::printf("%-8s %10.6f %10.6f ", name, - p->orbit.semi_major_axis_au, p->orbit.eccentricity); + p->orbit.semi_major_axis.value(), p->orbit.eccentricity); std::cout << std::fixed << std::setprecision(2) << period << std::endl; } std::puts(""); @@ -174,17 +175,22 @@ void section_custom_planet() { std::puts("---------------------------------"); Planet demo_world{ - 5.972e24 * 2.0, // mass_kg: double the Earth - 6371.0 * 1.3, // radius_km: 30% bigger - Orbit{1.4, 0.07, 4.0, 120.0, 80.0, 10.0, JulianDate::J2000().value()} + qtty::Kilogram(5.972e24 * 2.0), // mass: double the Earth + qtty::Kilometer(6371.0 * 1.3), // radius: 30% bigger + Orbit{1.4_au, 0.07, 4.0_deg, + 120.0_deg, 80.0_deg, 10.0_deg, + JulianDate::J2000().value()} }; auto period = orbit_period(demo_world.orbit); std::puts("Custom planet built at runtime:"); - std::printf(" mass = %.3e kg\n", demo_world.mass_kg); - std::printf(" radius = %.1f km\n", demo_world.radius_km); - std::printf(" a = %.6f AU\n", demo_world.orbit.semi_major_axis_au); + std::cout << " mass = " << std::scientific << std::setprecision(3) + << demo_world.mass << std::endl; + std::cout << " radius = " << std::fixed << std::setprecision(1) + << demo_world.radius << std::endl; + std::cout << " a = " << std::setprecision(6) + << demo_world.orbit.semi_major_axis << std::endl; std::cout << " sidereal period = " << std::fixed << std::setprecision(2) << period << "\n" << std::endl; } diff --git a/examples/09_star_observability.cpp b/examples/09_star_observability.cpp index 181a5d1..a95a438 100644 --- a/examples/09_star_observability.cpp +++ b/examples/09_star_observability.cpp @@ -14,6 +14,7 @@ #include using namespace siderust; +using namespace qtty::literals; /// Intersect two sorted vectors of periods. /// Returns every non-empty overlap between a period in `a` and a period in `b`. @@ -41,18 +42,18 @@ int main() { // One-night search window (MJD TT). MJD t0(60000.0); - Period window(t0, t0 + qtty::Day(1.0)); + Period window(t0, t0 + 1.0_d); // Constraint 1: altitude between 25° and 65°. - auto min_alt = qtty::Degree(25.0); - auto max_alt = qtty::Degree(65.0); + auto min_alt = 25.0_deg; + auto max_alt = 65.0_deg; auto above_min = star_altitude::above_threshold(target, observer, window, min_alt); auto below_max = star_altitude::below_threshold(target, observer, window, max_alt); auto altitude_periods = intersect_periods(above_min, below_max); // Constraint 2: azimuth between 110° and 220° (ESE -> SW sector). - auto min_az = qtty::Degree(110.0); - auto max_az = qtty::Degree(220.0); + auto min_az = 110.0_deg; + auto max_az = 220.0_deg; auto azimuth_periods = star_altitude::in_azimuth_range( target, observer, window, min_az, max_az); diff --git a/examples/11_serde_serialization.cpp b/examples/11_serde_serialization.cpp index a870f39..daf381c 100644 --- a/examples/11_serde_serialization.cpp +++ b/examples/11_serde_serialization.cpp @@ -29,8 +29,7 @@ using namespace siderust; using namespace siderust::frames; -using namespace siderust::centers; - +using namespace siderust::centers;using namespace qtty::literals; // ─── JSON formatting helpers ──────────────────────────────────────────────── inline std::string json_number(double v, int prec = 6) { @@ -81,8 +80,8 @@ void section_coordinates() { // Heliocentric ecliptic spherical (AU) spherical::Position - helio_ecl_sph(qtty::Degree(120.0), qtty::Degree(5.0), - qtty::AstronomicalUnit(1.2)); + helio_ecl_sph(120.0_deg, 5.0_deg, + 1.2_au); // Observer site (geodetic) Geodetic observer_site(-17.8947, 28.7636, 2396.0); @@ -122,12 +121,12 @@ struct BodySnapshotJSON { << pad << "\"name\": " << json_string(name) << ",\n" << pad << "\"epoch\": " << json_number(epoch.value(), 1) << ",\n" << pad << "\"orbit\": {\n" - << pad << " \"semi_major_axis_au\": " << json_number(orbit.semi_major_axis_au) << ",\n" + << pad << " \"semi_major_axis_au\": " << json_number(orbit.semi_major_axis.value()) << ",\n" << pad << " \"eccentricity\": " << json_number(orbit.eccentricity) << ",\n" - << pad << " \"inclination_deg\": " << json_number(orbit.inclination_deg) << ",\n" - << pad << " \"lon_ascending_node_deg\": " << json_number(orbit.lon_ascending_node_deg) << ",\n" - << pad << " \"arg_perihelion_deg\": " << json_number(orbit.arg_perihelion_deg) << ",\n" - << pad << " \"mean_anomaly_deg\": " << json_number(orbit.mean_anomaly_deg) << ",\n" + << pad << " \"inclination_deg\": " << json_number(orbit.inclination.value()) << ",\n" + << pad << " \"lon_ascending_node_deg\": " << json_number(orbit.lon_ascending_node.value()) << ",\n" + << pad << " \"arg_perihelion_deg\": " << json_number(orbit.arg_perihelion.value()) << ",\n" + << pad << " \"mean_anomaly_deg\": " << json_number(orbit.mean_anomaly.value()) << ",\n" << pad << " \"epoch_jd\": " << json_number(orbit.epoch_jd, 1) << "\n" << pad << "},\n" << pad << "\"heliocentric_ecliptic\": {\n" @@ -148,7 +147,9 @@ void section_body_objects(const JulianDate &jd) { "Earth", jd, EARTH.orbit, ephemeris::earth_heliocentric(jd)}; // Halley's comet - Orbit halley_orb{17.834, 0.96714, 162.26, 58.42, 111.33, 38.38, 2446467.4}; + Orbit halley_orb{17.834_au, 0.96714, + 162.26_deg, 58.42_deg, + 111.33_deg, 38.38_deg, 2446467.4}; auto halley_pos = kepler_position(halley_orb, jd); BodySnapshotJSON halley_snap{"Halley", jd, halley_orb, halley_pos}; diff --git a/include/siderust/bodies.hpp b/include/siderust/bodies.hpp index add6e18..a4b348d 100644 --- a/include/siderust/bodies.hpp +++ b/include/siderust/bodies.hpp @@ -43,29 +43,29 @@ struct ProperMotion { * @brief Keplerian orbital elements. */ struct Orbit { - double semi_major_axis_au; - double eccentricity; - double inclination_deg; - double lon_ascending_node_deg; - double arg_perihelion_deg; - double mean_anomaly_deg; - double epoch_jd; + qtty::AstronomicalUnit semi_major_axis; ///< Semi-major axis. + double eccentricity; ///< Orbital eccentricity [0, 1). + qtty::Degree inclination; ///< Orbital inclination. + qtty::Degree lon_ascending_node; ///< Longitude of ascending node. + qtty::Degree arg_perihelion; ///< Argument of perihelion. + qtty::Degree mean_anomaly; ///< Mean anomaly at epoch. + double epoch_jd; ///< Reference epoch (Julian Date). static Orbit from_c(const siderust_orbit_t &c) { - return {c.semi_major_axis_au, + return {qtty::AstronomicalUnit(c.semi_major_axis_au), c.eccentricity, - c.inclination_deg, - c.lon_ascending_node_deg, - c.arg_perihelion_deg, - c.mean_anomaly_deg, + qtty::Degree(c.inclination_deg), + qtty::Degree(c.lon_ascending_node_deg), + qtty::Degree(c.arg_perihelion_deg), + qtty::Degree(c.mean_anomaly_deg), c.epoch_jd}; } /// Convert to C FFI struct. siderust_orbit_t to_c() const { - return {semi_major_axis_au, eccentricity, inclination_deg, - lon_ascending_node_deg, arg_perihelion_deg, mean_anomaly_deg, - epoch_jd}; + return {semi_major_axis.value(), eccentricity, inclination.value(), + lon_ascending_node.value(), arg_perihelion.value(), + mean_anomaly.value(), epoch_jd}; } }; @@ -77,12 +77,13 @@ struct Orbit { * @brief Planet data (value type, copyable). */ struct Planet { - double mass_kg; - double radius_km; + qtty::Kilogram mass; ///< Planet mass. + qtty::Kilometer radius; ///< Mean equatorial radius. Orbit orbit; static Planet from_c(const siderust_planet_t &c) { - return {c.mass_kg, c.radius_km, Orbit::from_c(c.orbit)}; + return {qtty::Kilogram(c.mass_kg), qtty::Kilometer(c.radius_km), + Orbit::from_c(c.orbit)}; } }; diff --git a/include/siderust/lunar_phase.hpp b/include/siderust/lunar_phase.hpp index 1db1aa6..15428b6 100644 --- a/include/siderust/lunar_phase.hpp +++ b/include/siderust/lunar_phase.hpp @@ -56,14 +56,14 @@ enum class MoonPhaseLabel : int32_t { * @brief Geometric description of the Moon's phase at a point in time. */ struct MoonPhaseGeometry { - double phase_angle_rad; ///< Phase angle in [0, π], radians. + qtty::Radian phase_angle; ///< Phase angle in [0, π]. double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. - double elongation_rad; ///< Sun–Moon elongation, radians. + qtty::Radian elongation; ///< Sun–Moon elongation. bool waxing; ///< True when the Moon is waxing. static MoonPhaseGeometry from_c(const siderust_moon_phase_geometry_t &c) { - return {c.phase_angle_rad, c.illuminated_fraction, c.elongation_rad, - static_cast(c.waxing)}; + return {qtty::Radian(c.phase_angle_rad), c.illuminated_fraction, + qtty::Radian(c.elongation_rad), static_cast(c.waxing)}; } }; @@ -150,8 +150,8 @@ inline MoonPhaseGeometry phase_topocentric(const JulianDate &jd, */ inline MoonPhaseLabel phase_label(const MoonPhaseGeometry &geom) { siderust_moon_phase_geometry_t c{ - geom.phase_angle_rad, geom.illuminated_fraction, geom.elongation_rad, - static_cast(geom.waxing)}; + geom.phase_angle.value(), geom.illuminated_fraction, + geom.elongation.value(), static_cast(geom.waxing)}; siderust_moon_phase_label_t out{}; check_status(siderust_moon_phase_label(c, &out), "moon::phase_label"); return static_cast(out); diff --git a/include/siderust/orbital_center.hpp b/include/siderust/orbital_center.hpp index d4add6d..8745a47 100644 --- a/include/siderust/orbital_center.hpp +++ b/include/siderust/orbital_center.hpp @@ -144,7 +144,9 @@ struct BodycentricParams { /// Default: circular 1 AU heliocentric orbit (placeholder). BodycentricParams() - : orbit{1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2451545.0}, + : orbit{qtty::AstronomicalUnit(1.0), 0.0, qtty::Degree(0.0), + qtty::Degree(0.0), qtty::Degree(0.0), qtty::Degree(0.0), + 2451545.0}, orbit_center(OrbitReferenceCenter::Heliocentric) {} /// Convert to C FFI struct for passing to siderust_to_bodycentric / diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index 26ff198..e196601 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -3,6 +3,7 @@ #include using namespace siderust; +using namespace qtty::literals; static const double PI = 3.14159265358979323846; @@ -16,7 +17,7 @@ class AltitudeTest : public ::testing::Test { void SetUp() override { obs = ROQUE_DE_LOS_MUCHACHOS; start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); - end_ = start + qtty::Day(1.0); // 24 hours + end_ = start + 1.0_d; // 24 hours window = Period(start, end_); } }; @@ -34,7 +35,7 @@ TEST_F(AltitudeTest, SunAltitudeAt) { TEST_F(AltitudeTest, SunAboveThreshold) { // Find periods when sun > 0 deg (daytime) - auto periods = sun::above_threshold(obs, window, qtty::Degree(0.0)); + auto periods = sun::above_threshold(obs, window, 0.0_deg); EXPECT_GT(periods.size(), 0u); for (auto &p : periods) { EXPECT_GT(p.duration().value(), 0.0); @@ -43,7 +44,7 @@ TEST_F(AltitudeTest, SunAboveThreshold) { TEST_F(AltitudeTest, SunBelowThreshold) { // Astronomical night: sun < -18° - auto periods = sun::below_threshold(obs, window, qtty::Degree(-18.0)); + auto periods = sun::below_threshold(obs, window, -18.0_deg); // In July at La Palma, astronomical night may be short but should exist // (or possibly not if too close to solstice — accept 0+) for (auto &p : periods) { @@ -52,7 +53,7 @@ TEST_F(AltitudeTest, SunBelowThreshold) { } TEST_F(AltitudeTest, SunCrossings) { - auto events = sun::crossings(obs, window, qtty::Degree(0.0)); + auto events = sun::crossings(obs, window, 0.0_deg); // Expect at least 1 crossing in 24h (sunrise or sunset) EXPECT_GE(events.size(), 1u); } @@ -66,7 +67,7 @@ TEST_F(AltitudeTest, SunCulminations) { TEST_F(AltitudeTest, SunAltitudePeriods) { // Find periods when sun is between -6° and 0° (civil twilight) auto periods = - sun::altitude_periods(obs, window, qtty::Degree(-6.0), qtty::Degree(0.0)); + sun::altitude_periods(obs, window, -6.0_deg, 0.0_deg); for (auto &p : periods) { EXPECT_GT(p.duration().value(), 0.0); } @@ -83,7 +84,7 @@ TEST_F(AltitudeTest, MoonAltitudeAt) { } TEST_F(AltitudeTest, MoonAboveThreshold) { - auto periods = moon::above_threshold(obs, window, qtty::Degree(0.0)); + auto periods = moon::above_threshold(obs, window, 0.0_deg); // Moon may or may not be above horizon for this date; just no crash for (auto &p : periods) { EXPECT_GT(p.duration().value(), 0.0); @@ -104,7 +105,7 @@ TEST_F(AltitudeTest, StarAltitudeAt) { TEST_F(AltitudeTest, StarAboveThreshold) { const auto &vega = VEGA; auto periods = - star_altitude::above_threshold(vega, obs, window, qtty::Degree(30.0)); + star_altitude::above_threshold(vega, obs, window, 30.0_deg); // Vega should be well above 30° from La Palma in July EXPECT_GT(periods.size(), 0u); } @@ -114,18 +115,18 @@ TEST_F(AltitudeTest, StarAboveThreshold) { // ============================================================================ TEST_F(AltitudeTest, IcrsAltitudeAt) { - const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), - qtty::Degree(38.78)); + const spherical::direction::ICRS vega_icrs(279.23_deg, + 38.78_deg); qtty::Radian alt = icrs_altitude::altitude_at(vega_icrs, obs, start); EXPECT_GT(alt.value(), -PI / 2.0); EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, IcrsAboveThreshold) { - const spherical::direction::ICRS vega_icrs(qtty::Degree(279.23), - qtty::Degree(38.78)); + const spherical::direction::ICRS vega_icrs(279.23_deg, + 38.78_deg); auto periods = icrs_altitude::above_threshold(vega_icrs, obs, window, - qtty::Degree(30.0)); + 30.0_deg); EXPECT_GT(periods.size(), 0u); } @@ -136,7 +137,7 @@ TEST_F(AltitudeTest, IcrsAboveThreshold) { // Vega ICRS coordinates (J2000): RA=279.2348°, Dec=+38.7836° TEST_F(AltitudeTest, ICRSTargetAltitudeAt) { ICRSTarget vega{ - spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + spherical::direction::ICRS{279.23_deg, 38.78_deg}}; // altitude_at returns qtty::Degree (radian/degree bug-fix verification) qtty::Degree alt = vega.altitude_at(obs, start); EXPECT_GT(alt.value(), -90.0); @@ -145,15 +146,15 @@ TEST_F(AltitudeTest, ICRSTargetAltitudeAt) { TEST_F(AltitudeTest, ICRSTargetAboveThreshold) { ICRSTarget vega{ - spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; - auto periods = vega.above_threshold(obs, window, qtty::Degree(30.0)); + spherical::direction::ICRS{279.23_deg, 38.78_deg}}; + auto periods = vega.above_threshold(obs, window, 30.0_deg); // Vega should rise above 30° from La Palma in July EXPECT_GT(periods.size(), 0u); } TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { ICRSTarget vega{ - spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}}; + spherical::direction::ICRS{279.23_deg, 38.78_deg}}; EXPECT_NEAR(vega.ra().value(), 279.23, 1e-9); EXPECT_NEAR(vega.dec().value(), 38.78, 1e-9); // epoch defaults to J2000 @@ -165,7 +166,7 @@ TEST_F(AltitudeTest, ICRSTargetTypedAccessors) { TEST_F(AltitudeTest, ICRSTargetPolymorphic) { // Verify DirectionTarget is usable through the Target interface std::unique_ptr t = std::make_unique( - spherical::direction::ICRS{qtty::Degree(279.23), qtty::Degree(38.78)}); + spherical::direction::ICRS{279.23_deg, 38.78_deg}); qtty::Degree alt = t->altitude_at(obs, start); EXPECT_GT(alt.value(), -90.0); EXPECT_LT(alt.value(), 90.0); @@ -174,7 +175,7 @@ TEST_F(AltitudeTest, ICRSTargetPolymorphic) { TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° EclipticMeanJ2000Target ec{spherical::direction::EclipticMeanJ2000{ - qtty::Degree(279.6), qtty::Degree(61.8)}}; + 279.6_deg, 61.8_deg}}; // ecl direction retained on the C++ side EXPECT_NEAR(ec.direction().lon().value(), 279.6, 1e-9); EXPECT_NEAR(ec.direction().lat().value(), 61.8, 1e-9); @@ -189,7 +190,7 @@ TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { TEST_F(AltitudeTest, EquatorialMeanJ2000TargetAltitudeAt) { EquatorialMeanJ2000Target vega{spherical::direction::EquatorialMeanJ2000{ - qtty::Degree(279.23), qtty::Degree(38.78)}}; + 279.23_deg, 38.78_deg}}; qtty::Degree alt = vega.altitude_at(obs, start); EXPECT_GT(alt.value(), -90.0); EXPECT_LT(alt.value(), 90.0); diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index 588b22f..8d21812 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -60,27 +60,27 @@ TEST(Bodies, StarCreateWithProperMotion) { TEST(Bodies, PlanetEarth) { auto e = EARTH; - EXPECT_NEAR(e.mass_kg, 5.972e24, 0.01e24); - EXPECT_NEAR(e.radius_km, 6371.0, 10.0); - EXPECT_NEAR(e.orbit.semi_major_axis_au, 1.0, 0.01); + EXPECT_NEAR(e.mass.value(), 5.972e24, 0.01e24); + EXPECT_NEAR(e.radius.value(), 6371.0, 10.0); + EXPECT_NEAR(e.orbit.semi_major_axis.value(), 1.0, 0.01); } TEST(Bodies, PlanetMars) { auto m = MARS; - EXPECT_GT(m.mass_kg, 0); - EXPECT_NEAR(m.orbit.semi_major_axis_au, 1.524, 0.01); + EXPECT_GT(m.mass.value(), 0); + EXPECT_NEAR(m.orbit.semi_major_axis.value(), 1.524, 0.01); } TEST(Bodies, AllPlanets) { // Ensure all static constants are populated. - EXPECT_GT(MERCURY.mass_kg, 0.0); - EXPECT_GT(VENUS.mass_kg, 0.0); - EXPECT_GT(EARTH.mass_kg, 0.0); - EXPECT_GT(MARS.mass_kg, 0.0); - EXPECT_GT(JUPITER.mass_kg, 0.0); - EXPECT_GT(SATURN.mass_kg, 0.0); - EXPECT_GT(URANUS.mass_kg, 0.0); - EXPECT_GT(NEPTUNE.mass_kg, 0.0); + EXPECT_GT(MERCURY.mass.value(), 0.0); + EXPECT_GT(VENUS.mass.value(), 0.0); + EXPECT_GT(EARTH.mass.value(), 0.0); + EXPECT_GT(MARS.mass.value(), 0.0); + EXPECT_GT(JUPITER.mass.value(), 0.0); + EXPECT_GT(SATURN.mass.value(), 0.0); + EXPECT_GT(URANUS.mass.value(), 0.0); + EXPECT_GT(NEPTUNE.mass.value(), 0.0); } // ============================================================================ diff --git a/tests/test_bodycentric.cpp b/tests/test_bodycentric.cpp index a4cf5fb..c6c7599 100644 --- a/tests/test_bodycentric.cpp +++ b/tests/test_bodycentric.cpp @@ -9,6 +9,7 @@ using namespace siderust; using namespace siderust::frames; using namespace siderust::centers; using qtty::AstronomicalUnit; +using namespace qtty::literals; namespace { @@ -17,12 +18,14 @@ constexpr double KM_PER_AU = 149597870.7; // Satellite orbit at 0.0001 AU (~14 960 km) geocentric Orbit satellite_orbit() { - return {0.0001, 0.0, 0.0, 0.0, 0.0, 0.0, J2000}; + return {0.0001_au, 0.0, 0.0_deg, + 0.0_deg, 0.0_deg, 0.0_deg, J2000}; } // Approximate Mars heliocentric orbit Orbit mars_orbit() { - return {1.524, 0.0934, 1.85, 49.56, 286.5, 19.41, J2000}; + return {1.524_au, 0.0934, 1.85_deg, + 49.56_deg, 286.5_deg, 19.41_deg, J2000}; } double vec_magnitude(double x, double y, double z) { @@ -83,7 +86,7 @@ TEST(BodycentricTransforms, GeocentricToBodycentricGeoOrbit) { EXPECT_TRUE(std::isfinite(result.z().value())); // center_params round-trips correctly - EXPECT_NEAR(result.center_params().orbit.semi_major_axis_au, 0.0001, 1e-10); + EXPECT_NEAR(result.center_params().orbit.semi_major_axis.value(), 0.0001, 1e-10); } // ============================================================================ From 322f27ecdacfed0dc0ee3c8abfd699745c3ef766 Mon Sep 17 00:00:00 2001 From: VPRamon Date: Sat, 28 Feb 2026 21:29:51 +0100 Subject: [PATCH 18/19] Add Subject API and related tests - Introduced a unified Subject type to represent any celestial entity, allowing for simplified function calls for altitude, azimuth, and other calculations. - Updated existing celestial body and star references to use the new Subject API. - Added tests for the Subject API covering various celestial entities including bodies, stars, and ICRS directions. - Refactored existing tests to accommodate changes in how celestial bodies and stars are accessed. - Updated CMakeLists.txt to include the new test file for Subject. --- CMakeLists.txt | 1 + examples/05_target_tracking.cpp | 2 +- examples/08_solar_system.cpp | 6 +- examples/09_star_observability.cpp | 4 +- examples/11_serde_serialization.cpp | 2 +- include/siderust/bodies.hpp | 106 +++++++--- include/siderust/ffi_core.hpp | 8 + include/siderust/observatories.hpp | 29 ++- include/siderust/siderust.hpp | 1 + include/siderust/star_target.hpp | 8 +- include/siderust/subject.hpp | 304 ++++++++++++++++++++++++++++ siderust | 2 +- tempoch-cpp | 2 +- tests/test_altitude.cpp | 6 +- tests/test_bodies.cpp | 28 +-- tests/test_coordinates.cpp | 4 +- tests/test_observatories.cpp | 8 +- tests/test_subject.cpp | 217 ++++++++++++++++++++ 18 files changed, 667 insertions(+), 71 deletions(-) create mode 100644 include/siderust/subject.hpp create mode 100644 tests/test_subject.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d799ee7..6373471 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,6 +184,7 @@ set(TEST_SOURCES tests/test_altitude.cpp tests/test_ephemeris.cpp tests/test_bodycentric.cpp + tests/test_subject.cpp ) add_executable(test_siderust ${TEST_SOURCES}) diff --git a/examples/05_target_tracking.cpp b/examples/05_target_tracking.cpp index cddbc30..54df9ca 100644 --- a/examples/05_target_tracking.cpp +++ b/examples/05_target_tracking.cpp @@ -66,7 +66,7 @@ void section_trackable_objects(const JulianDate &jd, const JulianDate &jd_next) << std::setprecision(3) << fixed_icrs << std::endl; // Sirius via the catalog StarTarget - StarTarget sirius_target(SIRIUS); + StarTarget sirius_target(SIRIUS()); std::printf(" Sirius via StarTarget: name = %s\n", sirius_target.name().c_str()); diff --git a/examples/08_solar_system.cpp b/examples/08_solar_system.cpp index cfbb9fe..640abf9 100644 --- a/examples/08_solar_system.cpp +++ b/examples/08_solar_system.cpp @@ -64,9 +64,9 @@ void section_planet_constants_and_periods() { const Planet *planet; }; const PlanetInfo planets[] = { - {"Mercury", &MERCURY}, {"Venus", &VENUS}, {"Earth", &EARTH}, - {"Mars", &MARS}, {"Jupiter", &JUPITER}, {"Saturn", &SATURN}, - {"Uranus", &URANUS}, {"Neptune", &NEPTUNE}, + {"Mercury", &MERCURY()}, {"Venus", &VENUS()}, {"Earth", &EARTH()}, + {"Mars", &MARS()}, {"Jupiter", &JUPITER()}, {"Saturn", &SATURN()}, + {"Uranus", &URANUS()}, {"Neptune", &NEPTUNE()}, }; std::printf("%-8s %10s %10s %10s\n", "Planet", "a [AU]", "e", "Period"); diff --git a/examples/09_star_observability.cpp b/examples/09_star_observability.cpp index a95a438..cf6d5c9 100644 --- a/examples/09_star_observability.cpp +++ b/examples/09_star_observability.cpp @@ -37,8 +37,8 @@ static std::vector intersect_periods(const std::vector &a, int main() { std::cout << "Star observability: altitude + azimuth constraints\n" << std::endl; - const auto &observer = ROQUE_DE_LOS_MUCHACHOS; - const auto &target = SIRIUS; + const auto &observer = ROQUE_DE_LOS_MUCHACHOS(); + const auto &target = SIRIUS(); // One-night search window (MJD TT). MJD t0(60000.0); diff --git a/examples/11_serde_serialization.cpp b/examples/11_serde_serialization.cpp index daf381c..8563d80 100644 --- a/examples/11_serde_serialization.cpp +++ b/examples/11_serde_serialization.cpp @@ -144,7 +144,7 @@ void section_body_objects(const JulianDate &jd) { std::puts("-----------------------"); BodySnapshotJSON earth_snap{ - "Earth", jd, EARTH.orbit, ephemeris::earth_heliocentric(jd)}; + "Earth", jd, EARTH().orbit, ephemeris::earth_heliocentric(jd)}; // Halley's comet Orbit halley_orb{17.834_au, 0.96714, diff --git a/include/siderust/bodies.hpp b/include/siderust/bodies.hpp index a4b348d..7ee95a2 100644 --- a/include/siderust/bodies.hpp +++ b/include/siderust/bodies.hpp @@ -139,24 +139,48 @@ inline Planet make_planet_neptune() { } // namespace detail -inline const Planet MERCURY = detail::make_planet_mercury(); -inline const Planet VENUS = detail::make_planet_venus(); -inline const Planet EARTH = detail::make_planet_earth(); -inline const Planet MARS = detail::make_planet_mars(); -inline const Planet JUPITER = detail::make_planet_jupiter(); -inline const Planet SATURN = detail::make_planet_saturn(); -inline const Planet URANUS = detail::make_planet_uranus(); -inline const Planet NEPTUNE = detail::make_planet_neptune(); +inline const Planet &MERCURY() { + static const Planet s = detail::make_planet_mercury(); + return s; +} +inline const Planet &VENUS() { + static const Planet s = detail::make_planet_venus(); + return s; +} +inline const Planet &EARTH() { + static const Planet s = detail::make_planet_earth(); + return s; +} +inline const Planet &MARS() { + static const Planet s = detail::make_planet_mars(); + return s; +} +inline const Planet &JUPITER() { + static const Planet s = detail::make_planet_jupiter(); + return s; +} +inline const Planet &SATURN() { + static const Planet s = detail::make_planet_saturn(); + return s; +} +inline const Planet &URANUS() { + static const Planet s = detail::make_planet_uranus(); + return s; +} +inline const Planet &NEPTUNE() { + static const Planet s = detail::make_planet_neptune(); + return s; +} // Backward-compatible function aliases. -inline Planet mercury() { return MERCURY; } -inline Planet venus() { return VENUS; } -inline Planet earth() { return EARTH; } -inline Planet mars() { return MARS; } -inline Planet jupiter() { return JUPITER; } -inline Planet saturn() { return SATURN; } -inline Planet uranus() { return URANUS; } -inline Planet neptune() { return NEPTUNE; } +inline Planet mercury() { return MERCURY(); } +inline Planet venus() { return VENUS(); } +inline Planet earth() { return EARTH(); } +inline Planet mars() { return MARS(); } +inline Planet jupiter() { return JUPITER(); } +inline Planet saturn() { return SATURN(); } +inline Planet uranus() { return URANUS(); } +inline Planet neptune() { return NEPTUNE(); } // ============================================================================ // Star (RAII) @@ -263,15 +287,45 @@ class Star { } }; -inline const Star VEGA = Star::catalog("VEGA"); -inline const Star SIRIUS = Star::catalog("SIRIUS"); -inline const Star POLARIS = Star::catalog("POLARIS"); -inline const Star CANOPUS = Star::catalog("CANOPUS"); -inline const Star ARCTURUS = Star::catalog("ARCTURUS"); -inline const Star RIGEL = Star::catalog("RIGEL"); -inline const Star BETELGEUSE = Star::catalog("BETELGEUSE"); -inline const Star PROCYON = Star::catalog("PROCYON"); -inline const Star ALDEBARAN = Star::catalog("ALDEBARAN"); -inline const Star ALTAIR = Star::catalog("ALTAIR"); +inline const Star &VEGA() { + static const Star s = Star::catalog("VEGA"); + return s; +} +inline const Star &SIRIUS() { + static const Star s = Star::catalog("SIRIUS"); + return s; +} +inline const Star &POLARIS() { + static const Star s = Star::catalog("POLARIS"); + return s; +} +inline const Star &CANOPUS() { + static const Star s = Star::catalog("CANOPUS"); + return s; +} +inline const Star &ARCTURUS() { + static const Star s = Star::catalog("ARCTURUS"); + return s; +} +inline const Star &RIGEL() { + static const Star s = Star::catalog("RIGEL"); + return s; +} +inline const Star &BETELGEUSE() { + static const Star s = Star::catalog("BETELGEUSE"); + return s; +} +inline const Star &PROCYON() { + static const Star s = Star::catalog("PROCYON"); + return s; +} +inline const Star &ALDEBARAN() { + static const Star s = Star::catalog("ALDEBARAN"); + return s; +} +inline const Star &ALTAIR() { + static const Star s = Star::catalog("ALTAIR"); + return s; +} } // namespace siderust diff --git a/include/siderust/ffi_core.hpp b/include/siderust/ffi_core.hpp index df3c5b2..1b7a9d1 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -81,6 +81,12 @@ class InvalidArgumentError : public SiderustException { : SiderustException(msg) {} }; +class InternalPanicError : public SiderustException { +public: + explicit InternalPanicError(const std::string &msg) + : SiderustException(msg) {} +}; + // ============================================================================ // Error Translation // ============================================================================ @@ -109,6 +115,8 @@ inline void check_status(siderust_status_t status, const char *operation) { throw AllocationFailedError(msg + "memory allocation failed"); case SIDERUST_STATUS_T_INVALID_ARGUMENT: throw InvalidArgumentError(msg + "invalid argument"); + case SIDERUST_STATUS_T_INTERNAL_PANIC: + throw InternalPanicError(msg + "internal panic in Rust FFI"); default: throw SiderustException(msg + "unknown error (" + std::to_string(status) + ")"); diff --git a/include/siderust/observatories.hpp b/include/siderust/observatories.hpp index e632906..2672da3 100644 --- a/include/siderust/observatories.hpp +++ b/include/siderust/observatories.hpp @@ -53,28 +53,39 @@ inline Geodetic geodetic(double lon_deg, double lat_deg, /** * @brief Roque de los Muchachos Observatory (La Palma, Spain). */ -inline const Geodetic ROQUE_DE_LOS_MUCHACHOS = - detail::make_roque_de_los_muchachos(); +inline const Geodetic &ROQUE_DE_LOS_MUCHACHOS() { + static const Geodetic s = detail::make_roque_de_los_muchachos(); + return s; +} /** * @brief El Paranal Observatory (Chile). */ -inline const Geodetic EL_PARANAL = detail::make_el_paranal(); +inline const Geodetic &EL_PARANAL() { + static const Geodetic s = detail::make_el_paranal(); + return s; +} /** * @brief Mauna Kea Observatory (Hawaii, USA). */ -inline const Geodetic MAUNA_KEA = detail::make_mauna_kea(); +inline const Geodetic &MAUNA_KEA() { + static const Geodetic s = detail::make_mauna_kea(); + return s; +} /** * @brief La Silla Observatory (Chile). */ -inline const Geodetic LA_SILLA_OBSERVATORY = detail::make_la_silla(); +inline const Geodetic &LA_SILLA_OBSERVATORY() { + static const Geodetic s = detail::make_la_silla(); + return s; +} // Backward-compatible function aliases. -inline Geodetic roque_de_los_muchachos() { return ROQUE_DE_LOS_MUCHACHOS; } -inline Geodetic el_paranal() { return EL_PARANAL; } -inline Geodetic mauna_kea() { return MAUNA_KEA; } -inline Geodetic la_silla() { return LA_SILLA_OBSERVATORY; } +inline Geodetic roque_de_los_muchachos() { return ROQUE_DE_LOS_MUCHACHOS(); } +inline Geodetic el_paranal() { return EL_PARANAL(); } +inline Geodetic mauna_kea() { return MAUNA_KEA(); } +inline Geodetic la_silla() { return LA_SILLA_OBSERVATORY(); } } // namespace siderust diff --git a/include/siderust/siderust.hpp b/include/siderust/siderust.hpp index bb538db..309dc80 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -43,5 +43,6 @@ #include "orbital_center.hpp" #include "observatories.hpp" #include "star_target.hpp" +#include "subject.hpp" #include "target.hpp" #include "time.hpp" diff --git a/include/siderust/star_target.hpp b/include/siderust/star_target.hpp index 637472e..f304bcc 100644 --- a/include/siderust/star_target.hpp +++ b/include/siderust/star_target.hpp @@ -9,7 +9,7 @@ * * ### Example * @code - * siderust::StarTarget vega_target(siderust::VEGA); + * siderust::StarTarget vega_target(siderust::VEGA()); * std::cout << vega_target.name() << "\n"; // "Vega" * auto alt = vega_target.altitude_at(obs, now); * @endcode @@ -26,8 +26,8 @@ namespace siderust { * @brief Target implementation wrapping a `const Star&`. * * The referenced `Star` must outlive the `StarTarget`. Typically used with - * the pre-built catalog stars (e.g. `VEGA`, `SIRIUS`) which are `inline const` - * globals and live for the entire program. + * the pre-built catalog stars (e.g. `VEGA()`, `SIRIUS()`) which are + * lazy-initialized singletons and live for the entire program. */ class StarTarget : public Target { public: @@ -53,7 +53,7 @@ class StarTarget : public Target { qtty::Degree altitude_at(const Geodetic &obs, const MJD &mjd) const override { // star_altitude::altitude_at returns Radian; convert to Degree auto rad = star_altitude::altitude_at(star_, obs, mjd); - return qtty::Degree(rad.value() * 180.0 / 3.14159265358979323846); + return rad.to(); } std::vector diff --git a/include/siderust/subject.hpp b/include/siderust/subject.hpp new file mode 100644 index 0000000..5d6f742 --- /dev/null +++ b/include/siderust/subject.hpp @@ -0,0 +1,304 @@ +#pragma once + +/** + * @file subject.hpp + * @brief Unified Subject type — one value to represent any celestial entity. + * + * `Subject` is a lightweight tagged value (akin to `std::variant`) that + * wraps the FFI `siderust_subject_t` struct. It can carry: + * + * | Kind | Data | + * |-------------|----------------------------------------------| + * | `Body` | `siderust::Body` discriminant | + * | `Star` | borrows an existing `siderust::Star` | + * | `Icrs` | inline `spherical::Direction` | + * | `Target` | borrows an existing `DirectionTarget<…>` | + * + * All unified functions (`altitude_at`, `above_threshold`, …) accept a + * `Subject` so the caller no longer needs separate `sun::`, `moon::`, + * `body::`, `star::`, `icrs::`, and target-specific calls. + * + * **Lifetime**: when constructing from `Star` or `DirectionTarget`, the + * `Subject` *borrows* the handle — the original object must outlive it. + * + * ### Example + * @code + * using namespace siderust; + * + * // From a solar-system body + * Subject sun = Subject::body(Body::Sun); + * qtty::Degree alt = altitude_at(sun, obs, now); + * + * // From a catalog star + * Star vega = Star::catalog("VEGA"); + * Subject s = Subject::star(vega); + * auto periods = above_threshold(s, obs, window, qtty::Degree(10)); + * + * // From an ICRS direction + * Subject d = Subject::icrs(spherical::Direction( + * qtty::Degree(279.23), qtty::Degree(38.78))); + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "bodies.hpp" +#include "body_target.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "target.hpp" +#include "time.hpp" +#include + +namespace siderust { + +// ============================================================================ +// SubjectKind enum +// ============================================================================ + +/** + * @brief Discriminant for the active field in a Subject. + */ +enum class SubjectKind : int32_t { + Body = SIDERUST_SUBJECT_KIND_T_BODY, + Star = SIDERUST_SUBJECT_KIND_T_STAR, + Icrs = SIDERUST_SUBJECT_KIND_T_ICRS, + Target = SIDERUST_SUBJECT_KIND_T_TARGET, +}; + +// ============================================================================ +// Subject value type +// ============================================================================ + +/** + * @brief Unified, lightweight handle representing any celestial subject. + * + * Subject is a small copyable value type that stores a discriminant and + * either an inline body enum, an inline ICRS direction, or a borrowed + * pointer to a `Star` or `DirectionTarget` handle. + * + * Use the static factory methods to construct instances. + */ +class Subject { +public: + // -- Factories -------------------------------------------------------- + + /** @brief Create a subject for a solar-system body. */ + static Subject body(Body b) { + siderust_subject_t s{}; + s.kind = SIDERUST_SUBJECT_KIND_T_BODY; + s.body = static_cast(b); + return Subject(s); + } + + /** + * @brief Create a subject borrowing a `Star` handle. + * @warning The `Star` must outlive this `Subject`. + */ + static Subject star(const siderust::Star &star) { + siderust_subject_t s{}; + s.kind = SIDERUST_SUBJECT_KIND_T_STAR; + s.star_handle = star.c_handle(); + return Subject(s); + } + + /** + * @brief Create a subject for an inline ICRS direction. + * + * The direction is stored by value inside the Subject; no external handle + * is borrowed. + */ + static Subject icrs(const spherical::Direction &dir) { + siderust_subject_t s{}; + s.kind = SIDERUST_SUBJECT_KIND_T_ICRS; + s.icrs_dir = dir.to_c(); + return Subject(s); + } + + /** + * @brief Create a subject borrowing an opaque `SiderustTarget` handle. + * + * Works with any `DirectionTarget` via its `c_handle()` accessor. + * @warning The target must outlive this `Subject`. + */ + template + static Subject target(const DirectionTarget &tgt) { + siderust_subject_t s{}; + s.kind = SIDERUST_SUBJECT_KIND_T_TARGET; + s.target_handle = tgt.c_handle(); + return Subject(s); + } + + // -- Accessors -------------------------------------------------------- + + SubjectKind kind() const { return static_cast(inner_.kind); } + const siderust_subject_t &c_inner() const { return inner_; } + +private: + siderust_subject_t inner_{}; + explicit Subject(siderust_subject_t s) : inner_(s) {} +}; + +// ============================================================================ +// Unified free functions +// ============================================================================ + +/** + * @brief Altitude at an instant (radians) for any subject. + */ +inline qtty::Radian altitude_at(const Subject &subj, const Geodetic &obs, + const MJD &mjd) { + double out; + check_status(siderust_altitude_at(subj.c_inner(), obs.to_c(), mjd.value(), + &out), + "altitude_at(Subject)"); + return qtty::Radian(out); +} + +/** + * @brief Periods when a subject is above a threshold altitude. + */ +inline std::vector above_threshold(const Subject &subj, + const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status( + siderust_above_threshold(subj.c_inner(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "above_threshold(Subject)"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Periods when a subject is below a threshold altitude. + */ +inline std::vector below_threshold(const Subject &subj, + const Geodetic &obs, + const Period &window, + qtty::Degree threshold, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status( + siderust_below_threshold(subj.c_inner(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "below_threshold(Subject)"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Threshold-crossing events for a subject. + */ +inline std::vector +crossings(const Subject &subj, const Geodetic &obs, const Period &window, + qtty::Degree threshold, const SearchOptions &opts = {}) { + siderust_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status( + siderust_crossings(subj.c_inner(), obs.to_c(), window.c_inner(), + threshold.value(), opts.to_c(), &ptr, &count), + "crossings(Subject)"); + return detail::crossings_from_c(ptr, count); +} + +/** + * @brief Culmination (local extrema) events for a subject. + */ +inline std::vector +culminations(const Subject &subj, const Geodetic &obs, const Period &window, + const SearchOptions &opts = {}) { + siderust_culmination_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_culminations(subj.c_inner(), obs.to_c(), + window.c_inner(), opts.to_c(), &ptr, + &count), + "culminations(Subject)"); + return detail::culminations_from_c(ptr, count); +} + +/** + * @brief Periods when a body's altitude is within [min, max]. + * + * Only valid for `Body` subjects. Will throw for `Star`/`Icrs`/`Target`. + */ +inline std::vector altitude_periods(const Subject &subj, + const Geodetic &obs, + const Period &window, + qtty::Degree min_alt, + qtty::Degree max_alt) { + siderust_altitude_query_t q = {obs.to_c(), window.start().value(), + window.end().value(), min_alt.value(), + max_alt.value()}; + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_altitude_periods(subj.c_inner(), q, &ptr, &count), + "altitude_periods(Subject)"); + return detail::periods_from_c(ptr, count); +} + +/** + * @brief Azimuth at an instant (degrees, N-clockwise) for any subject. + */ +inline qtty::Degree azimuth_at(const Subject &subj, const Geodetic &obs, + const MJD &mjd) { + double out; + check_status(siderust_azimuth_at(subj.c_inner(), obs.to_c(), mjd.value(), + &out), + "azimuth_at(Subject)"); + return qtty::Degree(out); +} + +/** + * @brief Azimuth bearing-crossing events for a subject. + */ +inline std::vector +azimuth_crossings(const Subject &subj, const Geodetic &obs, + const Period &window, qtty::Degree bearing, + const SearchOptions &opts = {}) { + siderust_azimuth_crossing_event_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_azimuth_crossings(subj.c_inner(), obs.to_c(), + window.c_inner(), bearing.value(), + opts.to_c(), &ptr, &count), + "azimuth_crossings(Subject)"); + return detail::az_crossings_from_c(ptr, count); +} + +/** + * @brief Azimuth extrema (northernmost / southernmost) for a subject. + */ +inline std::vector +azimuth_extrema(const Subject &subj, const Geodetic &obs, + const Period &window, const SearchOptions &opts = {}) { + siderust_azimuth_extremum_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_azimuth_extrema(subj.c_inner(), obs.to_c(), + window.c_inner(), opts.to_c(), &ptr, + &count), + "azimuth_extrema(Subject)"); + return detail::az_extrema_from_c(ptr, count); +} + +/** + * @brief Periods when a subject's azimuth is within [min_deg, max_deg]. + */ +inline std::vector in_azimuth_range(const Subject &subj, + const Geodetic &obs, + const Period &window, + qtty::Degree min_deg, + qtty::Degree max_deg, + const SearchOptions &opts = {}) { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + check_status(siderust_in_azimuth_range(subj.c_inner(), obs.to_c(), + window.c_inner(), min_deg.value(), + max_deg.value(), opts.to_c(), &ptr, + &count), + "in_azimuth_range(Subject)"); + return detail::periods_from_c(ptr, count); +} + +} // namespace siderust diff --git a/siderust b/siderust index 0655a52..32bdc6c 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 0655a52b8d0d27d0ee83abf87f884a11cd97b07a +Subproject commit 32bdc6ccd504383605a00ed4b2edefce95b214cf diff --git a/tempoch-cpp b/tempoch-cpp index e05858b..bfa0f0d 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit e05858b9a75240ef15894dca91e176c5218a427e +Subproject commit bfa0f0d062c59579285fb4fc243d7dbccf542e0f diff --git a/tests/test_altitude.cpp b/tests/test_altitude.cpp index e196601..06f328d 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -15,7 +15,7 @@ class AltitudeTest : public ::testing::Test { Period window{MJD(0.0), MJD(1.0)}; void SetUp() override { - obs = ROQUE_DE_LOS_MUCHACHOS; + obs = ROQUE_DE_LOS_MUCHACHOS(); start = MJD::from_jd(JulianDate::from_utc({2026, 7, 15, 18, 0, 0})); end_ = start + 1.0_d; // 24 hours window = Period(start, end_); @@ -96,14 +96,14 @@ TEST_F(AltitudeTest, MoonAboveThreshold) { // ============================================================================ TEST_F(AltitudeTest, StarAltitudeAt) { - const auto &vega = VEGA; + const auto &vega = VEGA(); qtty::Radian alt = star_altitude::altitude_at(vega, obs, start); EXPECT_GT(alt.value(), -PI / 2.0); EXPECT_LT(alt.value(), PI / 2.0); } TEST_F(AltitudeTest, StarAboveThreshold) { - const auto &vega = VEGA; + const auto &vega = VEGA(); auto periods = star_altitude::above_threshold(vega, obs, window, 30.0_deg); // Vega should be well above 30° from La Palma in July diff --git a/tests/test_bodies.cpp b/tests/test_bodies.cpp index 8d21812..af7e4ac 100644 --- a/tests/test_bodies.cpp +++ b/tests/test_bodies.cpp @@ -8,14 +8,14 @@ using namespace siderust; // ============================================================================ TEST(Bodies, StarCatalogVega) { - const auto &vega = VEGA; + const auto &vega = VEGA(); EXPECT_EQ(vega.name(), "Vega"); EXPECT_NEAR(vega.distance_ly(), 25.0, 1.0); EXPECT_GT(vega.luminosity_solar(), 1.0); } TEST(Bodies, StarCatalogSirius) { - const auto &sirius = SIRIUS; + const auto &sirius = SIRIUS(); EXPECT_EQ(sirius.name(), "Sirius"); EXPECT_NEAR(sirius.distance_ly(), 8.6, 0.5); } @@ -59,28 +59,28 @@ TEST(Bodies, StarCreateWithProperMotion) { // ============================================================================ TEST(Bodies, PlanetEarth) { - auto e = EARTH; + auto e = EARTH(); EXPECT_NEAR(e.mass.value(), 5.972e24, 0.01e24); EXPECT_NEAR(e.radius.value(), 6371.0, 10.0); EXPECT_NEAR(e.orbit.semi_major_axis.value(), 1.0, 0.01); } TEST(Bodies, PlanetMars) { - auto m = MARS; + auto m = MARS(); EXPECT_GT(m.mass.value(), 0); EXPECT_NEAR(m.orbit.semi_major_axis.value(), 1.524, 0.01); } TEST(Bodies, AllPlanets) { // Ensure all static constants are populated. - EXPECT_GT(MERCURY.mass.value(), 0.0); - EXPECT_GT(VENUS.mass.value(), 0.0); - EXPECT_GT(EARTH.mass.value(), 0.0); - EXPECT_GT(MARS.mass.value(), 0.0); - EXPECT_GT(JUPITER.mass.value(), 0.0); - EXPECT_GT(SATURN.mass.value(), 0.0); - EXPECT_GT(URANUS.mass.value(), 0.0); - EXPECT_GT(NEPTUNE.mass.value(), 0.0); + EXPECT_GT(MERCURY().mass.value(), 0.0); + EXPECT_GT(VENUS().mass.value(), 0.0); + EXPECT_GT(EARTH().mass.value(), 0.0); + EXPECT_GT(MARS().mass.value(), 0.0); + EXPECT_GT(JUPITER().mass.value(), 0.0); + EXPECT_GT(SATURN().mass.value(), 0.0); + EXPECT_GT(URANUS().mass.value(), 0.0); + EXPECT_GT(NEPTUNE().mass.value(), 0.0); } // ============================================================================ @@ -182,7 +182,7 @@ TEST(Bodies, BodyNamespaceAzimuthAt) { // ============================================================================ TEST(Bodies, StarTargetAltitude) { - const auto &vega = VEGA; + const auto &vega = VEGA(); StarTarget st(vega); auto obs = geodetic(2.35, 48.85, 35.0); auto mjd = MJD(60000.5); @@ -198,7 +198,7 @@ TEST(Bodies, StarTargetPolymorphicWithBodyTarget) { std::vector> targets; targets.push_back(std::make_unique(Body::Sun)); - targets.push_back(std::make_unique(VEGA)); + targets.push_back(std::make_unique(VEGA())); for (const auto &t : targets) { auto alt = t->altitude_at(obs, mjd); diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index e00b7f1..1a39b05 100644 --- a/tests/test_coordinates.cpp +++ b/tests/test_coordinates.cpp @@ -76,7 +76,7 @@ TEST(TypedCoordinates, IcrsDirToHorizontal) { spherical::direction::ICRS vega(qtty::Degree(279.23473), qtty::Degree(38.78369)); auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - auto obs = ROQUE_DE_LOS_MUCHACHOS; + auto obs = ROQUE_DE_LOS_MUCHACHOS(); auto hor = vega.to_horizontal(jd, obs); @@ -148,7 +148,7 @@ TEST(TypedCoordinates, QttyDegreeAccessors) { // ============================================================================ TEST(TypedCoordinates, GeodeticQttyFields) { - auto obs = ROQUE_DE_LOS_MUCHACHOS; + auto obs = ROQUE_DE_LOS_MUCHACHOS(); // Exercise the qtty::Degree / qtty::Meter fields qtty::Degree lon = obs.lon; diff --git a/tests/test_observatories.cpp b/tests/test_observatories.cpp index bbd9fda..68f178d 100644 --- a/tests/test_observatories.cpp +++ b/tests/test_observatories.cpp @@ -4,7 +4,7 @@ using namespace siderust; TEST(Observatories, RoqueDeLos) { - auto obs = ROQUE_DE_LOS_MUCHACHOS; + auto obs = ROQUE_DE_LOS_MUCHACHOS(); // La Palma, approx lon=-17.88, lat=28.76 EXPECT_NEAR(obs.lon.value(), -17.88, 0.1); EXPECT_NEAR(obs.lat.value(), 28.76, 0.1); @@ -12,21 +12,21 @@ TEST(Observatories, RoqueDeLos) { } TEST(Observatories, ElParanal) { - auto obs = EL_PARANAL; + auto obs = EL_PARANAL(); EXPECT_LT(obs.lon.value(), 0.0); EXPECT_LT(obs.lat.value(), 0.0); // Southern hemisphere EXPECT_GT(obs.height.value(), 2000.0); } TEST(Observatories, MaunaKea) { - auto obs = MAUNA_KEA; + auto obs = MAUNA_KEA(); EXPECT_NEAR(obs.lon.value(), -155.47, 0.1); EXPECT_NEAR(obs.lat.value(), 19.82, 0.1); EXPECT_GT(obs.height.value(), 4000.0); } TEST(Observatories, LaSilla) { - auto obs = LA_SILLA_OBSERVATORY; + auto obs = LA_SILLA_OBSERVATORY(); EXPECT_LT(obs.lon.value(), 0.0); EXPECT_LT(obs.lat.value(), 0.0); } diff --git a/tests/test_subject.cpp b/tests/test_subject.cpp new file mode 100644 index 0000000..2c2c225 --- /dev/null +++ b/tests/test_subject.cpp @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/** + * @file test_subject.cpp + * @brief Tests for the unified Subject API (subject.hpp). + */ + +#include +#include + +using namespace siderust; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +static Geodetic paris() { return Geodetic(2.35, 48.85, 35.0); } +static MJD mid_day() { return MJD(60000.5); } +static Period one_day() { return Period(MJD(60000.0), MJD(60001.0)); } + +// ── altitude_at ────────────────────────────────────────────────────────────── + +TEST(SubjectTest, AltitudeAtBody) { + auto subj = Subject::body(Body::Sun); + auto alt = altitude_at(subj, paris(), mid_day()); + EXPECT_TRUE(alt.value() > -M_PI && alt.value() < M_PI); +} + +TEST(SubjectTest, AltitudeAtMoon) { + auto subj = Subject::body(Body::Moon); + auto alt = altitude_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(alt.value())); +} + +TEST(SubjectTest, AltitudeAtPlanet) { + auto subj = Subject::body(Body::Mars); + auto alt = altitude_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(alt.value())); +} + +TEST(SubjectTest, AltitudeAtStar) { + Star vega = Star::catalog("VEGA"); + auto subj = Subject::star(vega); + auto alt = altitude_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(alt.value())); +} + +TEST(SubjectTest, AltitudeAtIcrs) { + auto dir = spherical::Direction(qtty::Degree(279.23), + qtty::Degree(38.78)); + auto subj = Subject::icrs(dir); + auto alt = altitude_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(alt.value())); +} + +TEST(SubjectTest, AltitudeAtTarget) { + auto tgt = DirectionTarget>( + spherical::Direction(qtty::Degree(279.23), + qtty::Degree(38.78))); + auto subj = Subject::target(tgt); + auto alt = altitude_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(alt.value())); +} + +// ── above_threshold ────────────────────────────────────────────────────────── + +TEST(SubjectTest, AboveThresholdBody) { + auto subj = Subject::body(Body::Sun); + auto periods = above_threshold(subj, paris(), one_day(), qtty::Degree(0)); + EXPECT_GT(periods.size(), 0u); +} + +TEST(SubjectTest, AboveThresholdStar) { + Star vega = Star::catalog("VEGA"); + auto subj = Subject::star(vega); + auto periods = above_threshold(subj, paris(), one_day(), qtty::Degree(0)); + // Vega should be above horizon for some period from Paris + EXPECT_GT(periods.size(), 0u); +} + +TEST(SubjectTest, AboveThresholdIcrs) { + auto dir = spherical::Direction(qtty::Degree(279.23), + qtty::Degree(38.78)); + auto subj = Subject::icrs(dir); + auto periods = above_threshold(subj, paris(), one_day(), qtty::Degree(0)); + EXPECT_GT(periods.size(), 0u); +} + +// ── below_threshold ────────────────────────────────────────────────────────── + +TEST(SubjectTest, BelowThresholdBody) { + auto subj = Subject::body(Body::Sun); + auto periods = below_threshold(subj, paris(), one_day(), qtty::Degree(0)); + EXPECT_GT(periods.size(), 0u); +} + +// ── crossings ──────────────────────────────────────────────────────────────── + +TEST(SubjectTest, CrossingsBody) { + auto subj = Subject::body(Body::Sun); + auto evts = crossings(subj, paris(), one_day(), qtty::Degree(0)); + EXPECT_GT(evts.size(), 0u); +} + +TEST(SubjectTest, CrossingsStar) { + Star vega = Star::catalog("VEGA"); + auto subj = Subject::star(vega); + auto evts = crossings(subj, paris(), one_day(), qtty::Degree(0)); + EXPECT_GT(evts.size(), 0u); +} + +// ── culminations ───────────────────────────────────────────────────────────── + +TEST(SubjectTest, CulminationsBody) { + auto subj = Subject::body(Body::Sun); + auto evts = culminations(subj, paris(), one_day()); + EXPECT_GT(evts.size(), 0u); +} + +TEST(SubjectTest, CulminationsTarget) { + auto tgt = DirectionTarget>( + spherical::Direction(qtty::Degree(279.23), + qtty::Degree(38.78))); + auto subj = Subject::target(tgt); + auto evts = culminations(subj, paris(), one_day()); + EXPECT_GT(evts.size(), 0u); +} + +// ── altitude_periods (body-only) ───────────────────────────────────────────── + +TEST(SubjectTest, AltitudePeriodsBody) { + auto subj = Subject::body(Body::Sun); + auto periods = altitude_periods(subj, paris(), one_day(), qtty::Degree(-90), + qtty::Degree(90)); + // Full altitude range — should cover the entire window + EXPECT_GT(periods.size(), 0u); +} + +TEST(SubjectTest, AltitudePeriodsStarThrows) { + Star vega = Star::catalog("VEGA"); + auto subj = Subject::star(vega); + EXPECT_THROW( + [&]() { + altitude_periods(subj, paris(), one_day(), qtty::Degree(-90), + qtty::Degree(90)); + }(), + SiderustException); +} + +// ── azimuth_at ─────────────────────────────────────────────────────────────── + +TEST(SubjectTest, AzimuthAtBody) { + auto subj = Subject::body(Body::Sun); + auto az = azimuth_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(az.value())); +} + +TEST(SubjectTest, AzimuthAtStar) { + Star vega = Star::catalog("VEGA"); + auto subj = Subject::star(vega); + auto az = azimuth_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(az.value())); +} + +TEST(SubjectTest, AzimuthAtIcrs) { + auto dir = spherical::Direction(qtty::Degree(279.23), + qtty::Degree(38.78)); + auto subj = Subject::icrs(dir); + auto az = azimuth_at(subj, paris(), mid_day()); + EXPECT_TRUE(std::isfinite(az.value())); +} + +// ── azimuth_crossings ──────────────────────────────────────────────────────── + +TEST(SubjectTest, AzimuthCrossingsBody) { + auto subj = Subject::body(Body::Sun); + auto evts = azimuth_crossings(subj, paris(), one_day(), qtty::Degree(180)); + // Sun should cross 180° (south) once per day from northern latitude + EXPECT_GT(evts.size(), 0u); +} + +// ── azimuth_extrema ────────────────────────────────────────────────────────── + +TEST(SubjectTest, AzimuthExtremaBody) { + auto subj = Subject::body(Body::Sun); + // azimuth_extrema may return empty for short windows; just verify no error. + auto evts = azimuth_extrema(subj, paris(), one_day()); + // Extrema count can be zero for a 1-day window; just check it runs. + EXPECT_TRUE(true); +} + +// ── in_azimuth_range ───────────────────────────────────────────────────────── + +TEST(SubjectTest, InAzimuthRangeBody) { + auto subj = Subject::body(Body::Sun); + auto periods = in_azimuth_range(subj, paris(), one_day(), qtty::Degree(90), + qtty::Degree(270)); + EXPECT_GT(periods.size(), 0u); +} + +// ── Consistency: Subject vs old API ────────────────────────────────────────── + +TEST(SubjectTest, BodyAltitudeConsistency) { + // altitude_at via Subject should match body::altitude_at + auto subj = Subject::body(Body::Sun); + auto alt_subject = altitude_at(subj, paris(), mid_day()); + auto alt_body = body::altitude_at(Body::Sun, paris(), mid_day()); + EXPECT_DOUBLE_EQ(alt_subject.value(), alt_body.value()); +} + +TEST(SubjectTest, StarAltitudeConsistency) { + // altitude_at via Subject should match star::altitude_at + Star vega = Star::catalog("VEGA"); + auto subj = Subject::star(vega); + auto alt_subject = altitude_at(subj, paris(), mid_day()); + auto alt_star = star_altitude::altitude_at(vega, paris(), mid_day()); + EXPECT_DOUBLE_EQ(alt_subject.value(), alt_star.value()); +} From c4394165acb16036318f961519b9ee612fa4682e Mon Sep 17 00:00:00 2001 From: VPRamon Date: Sat, 28 Feb 2026 22:23:48 +0100 Subject: [PATCH 19/19] refactor: update below_threshold method to use target handling for improved functionality --- include/siderust/target.hpp | 9 ++------- siderust | 2 +- tempoch-cpp | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/include/siderust/target.hpp b/include/siderust/target.hpp index 276dde9..3e7e325 100644 --- a/include/siderust/target.hpp +++ b/include/siderust/target.hpp @@ -290,15 +290,10 @@ template class DirectionTarget : public Target { below_threshold(const Geodetic &obs, const Period &window, qtty::Degree threshold, const SearchOptions &opts = {}) const override { - // Always pass ICRS direction to the FFI layer. - siderust_spherical_dir_t dir_c{}; - dir_c.polar_deg = m_icrs_.dec().value(); - dir_c.azimuth_deg = m_icrs_.ra().value(); - dir_c.frame = SIDERUST_FRAME_T_ICRS; tempoch_period_mjd_t *ptr = nullptr; uintptr_t count = 0; - check_status(siderust_icrs_below_threshold( - dir_c, obs.to_c(), window.c_inner(), threshold.value(), + check_status(siderust_target_below_threshold( + handle_, obs.to_c(), window.c_inner(), threshold.value(), opts.to_c(), &ptr, &count), "Target::below_threshold"); return detail_periods_from_c(ptr, count); diff --git a/siderust b/siderust index 32bdc6c..4783222 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 32bdc6ccd504383605a00ed4b2edefce95b214cf +Subproject commit 4783222ec61c3f267e9fab2b012777020c3f2d12 diff --git a/tempoch-cpp b/tempoch-cpp index bfa0f0d..6ad1755 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit bfa0f0d062c59579285fb4fc243d7dbccf542e0f +Subproject commit 6ad1755dbe8111d4454517f0c915c93af6bc7961