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/.gitignore b/.gitignore index 5ade5e6..0c80895 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,9 @@ build/ cmake-build-*/ out/ -build-make/ -build-verify/ -build-ci-config-check/ +build-* +coverage_html/ +coverage.xml # IDE files .vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 751413a..6373471 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() # --------------------------------------------------------------------------- @@ -135,52 +142,35 @@ 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(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() +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() -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() +# --------------------------------------------------------------------------- +# Examples +# --------------------------------------------------------------------------- -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() +# 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 @@ -193,6 +183,8 @@ set(TEST_SOURCES tests/test_bodies.cpp 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/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/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 563c5ff..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 | --- @@ -29,29 +32,27 @@ codebase without writing a single line of Rust. ```cpp #include -#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 at the observatory - 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 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()); + 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::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; } @@ -112,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 @@ -130,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/01_basic_coordinates.cpp b/examples/01_basic_coordinates.cpp new file mode 100644 index 0000000..5fb1cf0 --- /dev/null +++ b/examples/01_basic_coordinates.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// 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() { + 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() < 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() << 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() << std::endl; + std::cout << " Dec = " << sph_pos.dec() << std::endl; + std::cout << std::setprecision(3); + 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() << std::endl; + std::cout << " Y = " << cart_pos_back.y() << std::endl; + std::cout << " Z = " << cart_pos_back.z() << 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 new file mode 100644 index 0000000..55e0e3e --- /dev/null +++ b/examples/02_coordinate_transformations.cpp @@ -0,0 +1,210 @@ +// 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 + +using namespace siderust; +using namespace siderust::frames; +using namespace siderust::centers; +using AU = qtty::AstronomicalUnit; + +int main() { + 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 << ")\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_frames_conversions.cpp b/examples/03_all_frames_conversions.cpp new file mode 100644 index 0000000..6edf8af --- /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 << 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 << 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 new file mode 100644 index 0000000..06b2dc4 --- /dev/null +++ b/examples/04_all_center_conversions.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// @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 namespace qtty::literals; + +using F = EclipticMeanJ2000; +using U = qtty::AstronomicalUnit; + +// ─── Standard center shifts ────────────────────────────────────────────────── + +/// 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`) to body-centric coordinates and back. +/// +/// Round-trip: C → Bodycentric → Geocentric → C. +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 ───────────────────────────────────────────────────────────────────── + +int main() { + JulianDate jd(2460000.5); + std::cout << "Center conversion demo at JD(TT) = " << std::fixed + << std::setprecision(1) << jd << "\n" << std::endl; + + 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 ──────────────────────────────────────────────── + 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); + + // Heliocentric source + 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); + + // ── Bodycentric: Mars-like orbit (heliocentric reference) ────────────────── + std::puts("\n── Bodycentric – Mars-like orbit (heliocentric ref) ───────────────────"); + Orbit mars_orbit{ + 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); + + 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) ──────────────────── + std::puts("\n── Bodycentric – ISS-like orbit (geocentric ref) ──────────────────────"); + Orbit iss_orbit{ + 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); + + 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 new file mode 100644 index 0000000..54df9ca --- /dev/null +++ b/examples/05_target_tracking.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// @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; +using namespace qtty::literals; + +// ─── 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_au, 0.96714, 162.26_deg, + 58.42_deg, 111.33_deg, 38.38_deg, + 2446467.4}; +} + +// ─── 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(120.0_deg, 22.5_deg); + 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); + + // 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; +} + +// ─── 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_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}; + 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; +} + +// ─── 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)); +} + +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( + 88.7929_deg, 7.4071_deg); + + // 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; +} + +// ─── Section 4: Frame + center transforms ─────────────────────────────────── + +void section_target_transform(const JulianDate &jd) { + std::puts("4) Target conversion across frame + center"); + + // Mars heliocentric ecliptic → geocentric equatorial + auto mars_helio = ephemeris::mars_heliocentric(jd); + auto mars_geoeq = mars_helio.template transform(jd); + + 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; +} + +// ─── main ───────────────────────────────────────────────────────────────────── + +int main() { + JulianDate jd = JulianDate::J2000(); + JulianDate jd_next(jd.value() + 1.0); + + 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); + + return 0; +} diff --git a/examples/06_night_events.cpp b/examples/06_night_events.cpp new file mode 100644 index 0000000..6b2902a --- /dev/null +++ b/examples/06_night_events.cpp @@ -0,0 +1,118 @@ +// 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 + +using namespace siderust; +using namespace qtty::literals; + +// Twilight threshold constants (same values as siderust::calculus::solar::night_types) +namespace twilight { +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); +constexpr auto ASTRONOMICAL = qtty::Degree(-18.0); +} // namespace twilight + +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::fixed << std::setprecision(3) + << threshold << " -> " << 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 " << 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 << "): " << 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 << " - " << s << " -> " << e + << " (" << std::setprecision(1) << hours << ")" << 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_moon_properties.cpp b/examples/07_moon_properties.cpp new file mode 100644 index 0000000..36cdd34 --- /dev/null +++ b/examples/07_moon_properties.cpp @@ -0,0 +1,115 @@ +// 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 + +using namespace siderust; +using namespace qtty::literals; + +/// 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); + + 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.to() << std::endl; + std::cout << " elongation : " + << geo.elongation.to() << 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.to() << 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_solar_system.cpp b/examples/08_solar_system.cpp new file mode 100644 index 0000000..640abf9 --- /dev/null +++ b/examples/08_solar_system.cpp @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// @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; +using namespace qtty::literals; + +// ─── 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.value(); + double T = 2.0 * M_PI * std::sqrt(a * a * a / GM_SUN_AU3_DAY2); + return qtty::Day(T); +} + +// ─── 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); +} + +// ─── 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.value(), p->orbit.eccentricity); + std::cout << std::fixed << std::setprecision(2) << period << std::endl; + } + std::puts(""); +} + +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(""); +} + +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(""); +} + +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(""); +} + +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; + } + std::puts(""); +} + +void section_custom_planet() { + std::puts("6) CUSTOM PLANET + ORBITAL PERIOD"); + std::puts("---------------------------------"); + + Planet demo_world{ + 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::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; +} + +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 ==="); +} + +// ─── 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/09_star_observability.cpp b/examples/09_star_observability.cpp new file mode 100644 index 0000000..cf6d5c9 --- /dev/null +++ b/examples/09_star_observability.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// 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; + +/// 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; +} + +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 + 1.0_d); + + // Constraint 1: altitude between 25° and 65°. + 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 = 110.0_deg; + auto max_az = 220.0_deg; + 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() << " -> " << window.end() + << "\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() << " -> " + << observable[i].end() + << " (" << std::setprecision(4) << hours << ")" + << std::endl; + } + + std::cout << "\nTotal observable time in both ranges: " + << std::setprecision(4) << qtty::Hour(total_hours) << std::endl; + + return 0; +} diff --git a/examples/10_time_periods.cpp b/examples/10_time_periods.cpp new file mode 100644 index 0000000..c4e7291 --- /dev/null +++ b/examples/10_time_periods.cpp @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// @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; +} + +// ── 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; +} + +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 new file mode 100644 index 0000000..8563d80 --- /dev/null +++ b/examples/11_serde_serialization.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2026 Vallés Puig, Ramon + +/// @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;using namespace qtty::literals; +// ─── 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(); +} + +inline std::string json_string(const std::string &s) { + return "\"" + s + "\""; +} + +// ─── 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; +} + +// ─── 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(120.0_deg, 5.0_deg, + 1.2_au); + + // 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.value()) << ",\n" + << pad << " \"eccentricity\": " << json_number(orbit.eccentricity) << ",\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" + << 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_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}; + + 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; +} + +// ─── 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; +} + +// ─── 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."); + } + } +} + +// ─── 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/examples/README.md b/examples/README.md deleted file mode 100644 index ac3ef29..0000000 --- a/examples/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# C++ Examples - -Build from the repository root: - -```bash -cmake -S . -B build-make -cmake --build build-make -``` - -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 -``` - -## 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. diff --git a/examples/altitude_events_example.cpp b/examples/altitude_events_example.cpp deleted file mode 100644 index a3b1a92..0000000 --- a/examples/altitude_events_example.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @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 - */ - -#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"; -} - -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); -} - -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); - } -} - -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); - } -} - -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; - 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)); - } - 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"); - - // 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); - - return 0; -} diff --git a/examples/coordinate_systems_example.cpp b/examples/coordinate_systems_example.cpp deleted file mode 100644 index b7eeb42..0000000 --- a/examples/coordinate_systems_example.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @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 - */ - -#include - -#include - -using namespace siderust; -using namespace siderust::frames; -using namespace qtty::literals; - -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()); - - // Vega J2000 ICRS direction. - spherical::direction::ICRS vega_icrs(279.23473, 38.78369); - - 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()); - - 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()); - - return 0; -} diff --git a/examples/coordinates_examples.cpp b/examples/coordinates_examples.cpp deleted file mode 100644 index f32fa07..0000000 --- a/examples/coordinates_examples.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @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 - -using namespace siderust; -using namespace qtty::literals; - -static void geodetic_and_ecef_example() { - std::printf("1) Geodetic -> ECEF cartesian\n"); - - 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"); - - 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"); - - geodetic_and_ecef_example(); - spherical_direction_example(); - spherical_position_example(); - cartesian_and_units_example(); - ephemeris_typed_example(); - - std::printf("Done.\n"); - return 0; -} diff --git a/examples/demo.cpp b/examples/demo.cpp deleted file mode 100644 index 40c2038..0000000 --- a/examples/demo.cpp +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @file demo.cpp - * @example demo.cpp - * @brief Demonstrates the siderust C++ API. - * - * Usage: - * cd build && cmake .. && cmake --build . && ./demo - */ - -#include -#include -#include - -int main() { - using namespace siderust; - using namespace siderust::frames; - using namespace qtty::literals; - - std::printf("=== siderust-cpp demo ===\n\n"); - - // --- 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()); - - auto mjd = MJD::from_jd(jd); - std::printf("MJD: %.6f\n\n", mjd.value()); - - // --- 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 --- - 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 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()); - - 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); - } - - std::printf("\nDone.\n"); - return 0; -} diff --git a/examples/solar_system_bodies_example.cpp b/examples/solar_system_bodies_example.cpp deleted file mode 100644 index bf82f2f..0000000 --- a/examples/solar_system_bodies_example.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @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 - */ - -#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"); - - auto jd = JulianDate::from_utc({2026, 7, 15, 0, 0, 0}); - std::printf("Epoch JD: %.6f\n\n", jd.value()); - - 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); - - const qtty::Kilometer earth_x_km = earth_helio.x().to(); - std::printf("Earth heliocentric x component: %.2f km\n\n", earth_x_km.value()); - - 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); - - 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 new file mode 100644 index 0000000..7276159 --- /dev/null +++ b/include/siderust/azimuth.hpp @@ -0,0 +1,447 @@ +#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 +#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); +} + +/** + * @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 + +// ============================================================================ +// 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); +} + +/** + * @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 + +// ============================================================================ +// 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/bodies.hpp b/include/siderust/bodies.hpp index cbc3247..7ee95a2 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,30 @@ 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}; - } + 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 {qtty::AstronomicalUnit(c.semi_major_axis_au), + c.eccentricity, + 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.value(), eccentricity, inclination.value(), + lon_ascending_node.value(), arg_perihelion.value(), + mean_anomaly.value(), epoch_jd}; + } }; // ============================================================================ @@ -66,85 +77,110 @@ struct Orbit { * @brief Planet data (value type, copyable). */ struct Planet { - 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)}; - } + qtty::Kilogram mass; ///< Planet mass. + qtty::Kilometer radius; ///< Mean equatorial radius. + Orbit orbit; + + static Planet from_c(const siderust_planet_t &c) { + return {qtty::Kilogram(c.mass_kg), qtty::Kilometer(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 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) @@ -156,113 +192,140 @@ 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); - } - - // 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; - } - 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); + 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; } - - /** - * @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); + 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; } - - // -- 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 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/body_target.hpp b/include/siderust/body_target.hpp new file mode 100644 index 0000000..0bbb307 --- /dev/null +++ b/include/siderust/body_target.hpp @@ -0,0 +1,324 @@ +#pragma once + +/** + * @file body_target.hpp + * @brief Target implementation for solar-system bodies. + * + * `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 + * Meeus/Williams series (Pluto) for ephemeris. + * + * ### Example + * @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; + * targets.push_back(std::make_unique(Body::Sun)); + * targets.push_back(std::make_unique(Body::Jupiter)); + * for (const auto& t : targets) { + * std::cout << t->name() << ": " << t->altitude_at(obs, now) << "\n"; + * } + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "ffi_core.hpp" +#include "trackable.hpp" +#include + +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 — Target implementation 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 + * polymorphic dispatch. + */ +class BodyTarget : public Target { +public: + /** + * @brief Construct a BodyTarget for a given solar-system body. + * @param body The body to track. + */ + 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 + // ------------------------------------------------------------------ + + 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.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/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 79a069b..a97a347 100644 --- a/include/siderust/coordinates/cartesian.hpp +++ b/include/siderust/coordinates/cartesian.hpp @@ -9,9 +9,17 @@ #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; } } + namespace siderust { namespace cartesian { @@ -23,20 +31,56 @@ 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; + /** + * @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); + } }; /** @@ -49,42 +93,193 @@ 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"); +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; } - U comp_x; ///< X component. - U comp_y; ///< Y component. - U comp_z; ///< Z component. + 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)); + } - Position() - : comp_x(U(0)), comp_y(U(0)), comp_z(U(0)) {} + /** + * @brief Convert this cartesian position to a spherical Position. + */ + spherical::Position to_spherical() const; - Position(U x_, U y_, U z_) - : comp_x(x_), comp_y(y_), comp_z(z_) {} + 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)); + } - Position(double x_, double y_, double z_) - : comp_x(U(x_)), comp_y(U(y_)), comp_z(U(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; + } - U x() const { return comp_x; } - U y() const { return comp_y; } - U z() const { return comp_z; } + /// 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()}; + } - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_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); + } - /// 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()}; + /** + * @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); } + } - /// 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); + /** + * @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 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(); } }; +// ============================================================================ +// 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/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 86dc552..32c5e69 100644 --- a/include/siderust/coordinates/geodetic.hpp +++ b/include/siderust/coordinates/geodetic.hpp @@ -12,10 +12,11 @@ #include +#include + namespace siderust { namespace cartesian { -template -struct Position; +template struct Position; } /** @@ -26,38 +27,50 @@ 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. - - 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) {} - - /// 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()}; - } - - /// 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; + 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(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)) {} + + /// 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); + } + + /** + * @brief Convert geodetic (WGS84/ECEF) to cartesian position. + * + * @tparam U Output length unit (default: meter). + */ + template + 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/pos_conversions.hpp b/include/siderust/coordinates/pos_conversions.hpp new file mode 100644 index 0000000..ea16b34 --- /dev/null +++ b/include/siderust/coordinates/pos_conversions.hpp @@ -0,0 +1,66 @@ +#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)); +} + +// 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 cd94da9..5426516 100644 --- a/include/siderust/coordinates/spherical.hpp +++ b/include/siderust/coordinates/spherical.hpp @@ -13,7 +13,12 @@ #include +#include #include +#include + +// Forward-declare cartesian Position to avoid circular include. +namespace siderust { namespace cartesian { template struct Position; } } namespace siderust { namespace spherical { @@ -24,7 +29,9 @@ 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: @@ -32,138 +39,157 @@ namespace spherical { * - 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) {} - - /// 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() { - 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 {azimuth_.value(), polar_.value(), frame_id()}; - } - - static Direction from_c(const siderust_spherical_dir_t& c) { - return Direction(c.lon_deg, c.lat_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_.value(), polar_.value()); - } else { - siderust_spherical_dir_t out; - check_status( - siderust_spherical_dir_transform_frame( - azimuth_.value(), polar_.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( - azimuth_.value(), polar_.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); + } }; /** @@ -176,62 +202,159 @@ 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) {} - - 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_); - } - - /// @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 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(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_); } + + /// @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_; } + /** + * @brief Convert this spherical position to a cartesian 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) + 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); + } +}; - template , int> = 0> - qtty::Degree lon() const { return azimuth_; } +// ============================================================================ +// Stream operators +// ============================================================================ - template , int> = 0> - qtty::Degree lat() const { return polar_; } - /// @} +/** + * @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(); +} - static constexpr siderust_frame_t frame_id() { return frames::FrameTraits::ffi_id; } - static constexpr siderust_center_t center_id() { return centers::CenterTraits::ffi_id; } +/** + * @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(); +} - U distance() const { return dist_; } -}; +/** + * @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/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/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/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..f12f1fc 100644 --- a/include/siderust/ephemeris.hpp +++ b/include/siderust/ephemeris.hpp @@ -23,41 +23,205 @@ 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 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 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. + */ +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 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. */ -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 94d8a9f..1b7a9d1 100644 --- a/include/siderust/ffi_core.hpp +++ b/include/siderust/ffi_core.hpp @@ -9,6 +9,7 @@ */ #include +#include #include #include @@ -25,136 +26,186 @@ 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) {} +}; + +class InternalPanicError : public SiderustException { +public: + explicit InternalPanicError(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"); + case SIDERUST_STATUS_T_INTERNAL_PANIC: + throw InternalPanicError(msg + "internal panic in Rust FFI"); + 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); } +// ============================================================================ +// 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) // ============================================================================ 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, 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 new file mode 100644 index 0000000..15428b6 --- /dev/null +++ b/include/siderust/lunar_phase.hpp @@ -0,0 +1,355 @@ +#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 +#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 { + qtty::Radian phase_angle; ///< Phase angle in [0, π]. + double illuminated_fraction; ///< Illuminated disc fraction in [0, 1]. + 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 {qtty::Radian(c.phase_angle_rad), c.illuminated_fraction, + qtty::Radian(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.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); +} + +/** + * @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 + +// ============================================================================ +// 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; + } +} + +// ============================================================================ +// 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/observatories.hpp b/include/siderust/observatories.hpp index 5e9c345..2672da3 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,37 +42,50 @@ 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() { + 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/orbital_center.hpp b/include/siderust/orbital_center.hpp new file mode 100644 index 0000000..8745a47 --- /dev/null +++ b/include/siderust/orbital_center.hpp @@ -0,0 +1,176 @@ +#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{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 / + /// 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 5cf0df1..309dc80 100644 --- a/include/siderust/siderust.hpp +++ b/include/siderust/siderust.hpp @@ -13,25 +13,36 @@ * using namespace siderust::frames; * * // Typed coordinates with compile-time frame/center - * spherical::direction::ICRS vega_icrs(279.23473, 38.78369); // Direction - * 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 */ #include "altitude.hpp" +#include "azimuth.hpp" #include "bodies.hpp" +#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 "subject.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..f304bcc --- /dev/null +++ b/include/siderust/star_target.hpp @@ -0,0 +1,107 @@ +#pragma once + +/** + * @file star_target.hpp + * @brief Target implementation for Star catalog objects. + * + * `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 + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "bodies.hpp" +#include "trackable.hpp" + +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 + * lazy-initialized singletons and live for the entire program. + */ +class StarTarget : public Target { +public: + /** + * @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 + // ------------------------------------------------------------------ + + 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 rad.to(); + } + + 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/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/include/siderust/target.hpp b/include/siderust/target.hpp new file mode 100644 index 0000000..3e7e325 --- /dev/null +++ b/include/siderust/target.hpp @@ -0,0 +1,442 @@ +#pragma once + +/** + * @file target.hpp + * @brief Strongly-typed fixed-direction Target for any supported frame. + * + * `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 + * (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" +#include "azimuth.hpp" +#include "coordinates.hpp" +#include "ffi_core.hpp" +#include "time.hpp" +#include "trackable.hpp" +#include +#include +#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 Fixed celestial direction target — a `Target` for a specific sky + * position. + * + * @tparam C Spherical direction type (e.g. `spherical::direction::ICRS`). + * + * ### Example — ICRS target (Vega at J2000) + * @code + * 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{ + * spherical::direction::EclipticMeanJ2000{ 246.2_deg, 59.2_deg } }; + * auto alt = ec.altitude_at(obs, now); + * @endcode + */ +template class DirectionTarget : public Target { + + 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). + * @param label Optional human-readable name. If empty, a default + * "Frame(lon°, lat°)" string is generated from the direction. + */ + 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; + } 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; + } + + ~DirectionTarget() { + if (handle_) { + siderust_target_free(handle_); + handle_ = nullptr; + } + } + + /// Move constructor. + DirectionTarget(DirectionTarget &&other) noexcept + : 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; + } + + /// Move assignment. + DirectionTarget &operator=(DirectionTarget &&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_; + label_ = std::move(other.label_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + // Prevent copying (the handle has unique ownership). + 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 + // ------------------------------------------------------------------ + + /// 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 { + tempoch_period_mjd_t *ptr = nullptr; + uintptr_t count = 0; + 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); + } + + /// 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_; + std::string label_; + 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; + } +}; + +// ============================================================================ +// Convenience type aliases +// ============================================================================ + +/// Fixed direction in ICRS (most common use-case). +using ICRSTarget = DirectionTarget; + +/// Fixed direction in ICRF (treated identically to ICRS in Siderust). +using ICRFTarget = DirectionTarget; + +/// Fixed direction in mean equatorial coordinates of J2000.0 (FK5). +using EquatorialMeanJ2000Target = + DirectionTarget; + +/// Fixed direction in mean equatorial coordinates of date (precessed only). +using EquatorialMeanOfDateTarget = + DirectionTarget; + +/// Fixed direction in true equatorial coordinates of date (precessed + +/// nutated). +using EquatorialTrueOfDateTarget = + DirectionTarget; + +/// Fixed direction in mean ecliptic coordinates of J2000.0. +using EclipticMeanJ2000Target = + DirectionTarget; + +} // namespace siderust diff --git a/include/siderust/time.hpp b/include/siderust/time.hpp index 351af5a..2113368 100644 --- a/include/siderust/time.hpp +++ b/include/siderust/time.hpp @@ -13,9 +13,20 @@ 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 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/include/siderust/trackable.hpp b/include/siderust/trackable.hpp new file mode 100644 index 0000000..9def307 --- /dev/null +++ b/include/siderust/trackable.hpp @@ -0,0 +1,137 @@ +#pragma once + +/** + * @file trackable.hpp + * @brief Abstract base class for all celestial targets. + * + * `Target` is the unified concept for anything in the sky that can be + * pointed at from an observer location. Concrete implementations cover: + * + * - **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) + * - *(future)* **SatelliteTarget** — Earth-orbiting satellites (TLE/SGP4) + * + * 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; + * targets.push_back(std::move(sun)); + * targets.push_back(std::make_unique(VEGA)); + * for (const auto& t : targets) { + * std::cout << t->name() << ": " << t->altitude_at(obs, now) << "\n"; + * } + * @endcode + */ + +#include "altitude.hpp" +#include "azimuth.hpp" +#include "coordinates.hpp" +#include "time.hpp" +#include +#include +#include + +namespace siderust { + +/** + * @brief Abstract base for any celestial object that can be tracked from an + * observer location. + * + * 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 Target { +public: + 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 (degrees) at a given MJD instant. + */ + 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, 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/qtty-cpp b/qtty-cpp index 953ebe1..a5821e1 160000 --- a/qtty-cpp +++ b/qtty-cpp @@ -1 +1 @@ -Subproject commit 953ebe15bcd6f1b929d4516970e5127e2e1ad953 +Subproject commit a5821e14e6f5271998c5837b933abb9e281c8b77 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)" diff --git a/siderust b/siderust index 283032a..4783222 160000 --- a/siderust +++ b/siderust @@ -1 +1 @@ -Subproject commit 283032a541a8bbcc46622e6a06f807717438f998 +Subproject commit 4783222ec61c3f267e9fab2b012777020c3f2d12 diff --git a/tempoch-cpp b/tempoch-cpp index 39a5e85..6ad1755 160000 --- a/tempoch-cpp +++ b/tempoch-cpp @@ -1 +1 @@ -Subproject commit 39a5e8557e2382d47dcb3ebe01a3e3237f8a94e5 +Subproject commit 6ad1755dbe8111d4454517f0c915c93af6bc7961 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 8ad505a..06f328d 100644 --- a/tests/test_altitude.cpp +++ b/tests/test_altitude.cpp @@ -3,22 +3,23 @@ #include using namespace siderust; +using namespace qtty::literals; 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 + 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 + 1.0_d; // 24 hours + window = Period(start, end_); + } }; // ============================================================================ @@ -26,49 +27,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_days(), 0.0); - } + // Find periods when sun > 0 deg (daytime) + 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); + } } 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_days(), 0.0); - } + // Astronomical night: sun < -18° + 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) { + 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, 0.0_deg); + // 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_days(), 0.0); - } + // Find periods when sun is between -6° and 0° (civil twilight) + auto periods = + sun::altitude_periods(obs, window, -6.0_deg, 0.0_deg); + for (auto &p : periods) { + EXPECT_GT(p.duration().value(), 0.0); + } } // ============================================================================ @@ -76,17 +78,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_days(), 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); + } } // ============================================================================ @@ -94,17 +96,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, 30.0_deg); + // Vega should be well above 30° from La Palma in July + EXPECT_GT(periods.size(), 0u); } // ============================================================================ @@ -112,15 +115,83 @@ 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(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)); - 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(279.23_deg, + 38.78_deg); + auto periods = icrs_altitude::above_threshold(vega_icrs, obs, window, + 30.0_deg); + 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{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); + EXPECT_LT(alt.value(), 90.0); +} + +TEST_F(AltitudeTest, ICRSTargetAboveThreshold) { + ICRSTarget vega{ + 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{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 + 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 DirectionTarget is usable through the Target interface + std::unique_ptr t = std::make_unique( + 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); +} + +TEST_F(AltitudeTest, EclipticTargetAltitudeAt) { + // Vega in ecliptic J2000 coordinates (approx): lon≈279.6°, lat≈+61.8° + EclipticMeanJ2000Target ec{spherical::direction::EclipticMeanJ2000{ + 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); + // 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{ + 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 755e69b..af7e4ac 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,26 +59,149 @@ 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.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); + 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_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.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); +} + +// ============================================================================ +// BodyTarget — solar-system body via Target 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 — Target implementation for catalog stars +// ============================================================================ + +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())); + } } diff --git a/tests/test_bodycentric.cpp b/tests/test_bodycentric.cpp new file mode 100644 index 0000000..c6c7599 --- /dev/null +++ b/tests/test_bodycentric.cpp @@ -0,0 +1,259 @@ +// 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; +using namespace qtty::literals; + +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_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_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) { + 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.value(), 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); +} diff --git a/tests/test_coordinates.cpp b/tests/test_coordinates.cpp index 8cc6cc7..1a39b05 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(279.23473, 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(100.0, 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(100.0, 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(279.23473, 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(100.0, 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(100.0, 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(123.456, -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(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(); - 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,172 @@ 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); +} + +// ============================================================================ +// 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); } 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..68f178d 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_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()); +} diff --git a/tests/test_time.cpp b/tests/test_time.cpp index 395814c..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 + 365.25; - EXPECT_NEAR(jd2 - jd1, 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,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); - auto c = a.intersection(b); - EXPECT_NEAR(c.start_mjd(), 60201.0, 1e-10); - EXPECT_NEAR(c.end_mjd(), 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(60200.0, 60201.0); - Period b(60202.0, 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(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); }