From 57b992ec5982cfb2d3047beda0fedd28833776ce Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Wed, 18 Sep 2024 17:44:32 +0200 Subject: [PATCH 01/22] [WIP] Initial commit to stark-like sims flanagan model implementation. Minor typo fixes. --- CMakeLists.txt | 2 + include/kep3/leg/sf_checks.hpp | 27 + include/kep3/leg/sims_flanagan.hpp | 4 +- include/kep3/leg/sims_flanagan_hf.hpp | 109 +-- include/kep3/ta/stark.hpp | 3 +- src/core_astro/propagate_lagrangian.cpp | 2 +- src/leg/sf_checks.cpp | 97 +++ src/leg/sims_flanagan.cpp | 66 +- src/leg/sims_flanagan_hf.cpp | 858 +++++++++++++----------- src/ta/stark.cpp | 1 - test/CMakeLists.txt | 1 + test/leg_sims_flanagan_hf_test.cpp | 338 ++++++++++ 12 files changed, 988 insertions(+), 520 deletions(-) create mode 100644 include/kep3/leg/sf_checks.hpp create mode 100644 src/leg/sf_checks.cpp create mode 100644 test/leg_sims_flanagan_hf_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ce8b679..59b55423 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,8 @@ set(kep3_SRC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/udpla/jpl_lp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/udpla/vsop2013.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/leg/sims_flanagan.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/leg/sims_flanagan_hf.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/leg/sf_checks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/core_astro/ic2par2ic.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/core_astro/ic2eq2ic.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/core_astro/eq2par2eq.cpp" diff --git a/include/kep3/leg/sf_checks.hpp b/include/kep3/leg/sf_checks.hpp new file mode 100644 index 00000000..96608cb3 --- /dev/null +++ b/include/kep3/leg/sf_checks.hpp @@ -0,0 +1,27 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef kep3_SF_CHECKS_H +#define kep3_SF_CHECKS_H + +#include + +void _check_tof(double tof); +void _check_throttles(const std::vector &throttles, unsigned nseg); +void _check_max_thrust(double max_thrust); +void _check_isp(double isp); +void _check_mu(double mu); +void _check_cut(double cut); +void _check_tol(double tol); +void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, + double cut, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck); +void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, + double cut, double tol, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck); + +#endif \ No newline at end of file diff --git a/include/kep3/leg/sims_flanagan.hpp b/include/kep3/leg/sims_flanagan.hpp index a42faf09..f8847b5d 100644 --- a/include/kep3/leg/sims_flanagan.hpp +++ b/include/kep3/leg/sims_flanagan.hpp @@ -120,10 +120,10 @@ class kep3_DLL_PUBLIC sims_flanagan } // Initial spacecraft state. - std::array, 2> m_rvs{{{1., 0., 0.}, {0, 1., 0.}}}; + std::array, 2> m_rvs{{{1., 0., 0.}, {0., 1., 0.}}}; double m_ms = 1.; // Sequence of throttles. - std::vector m_throttles{0., .0, 0., 0., 0., 0.}; + std::vector m_throttles{0., 0., 0., 0., 0., 0.}; // Final spacecraft state. std::array, 2> m_rvf{{{0., 1., 0.}, {-1., 0., 0.}}}; double m_mf = 1.; diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 84294683..1b471167 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -11,7 +11,7 @@ #define kep3_LEG_SIMS_FLANAGAN_HF_H #include -#include +// #include #include #include @@ -21,6 +21,7 @@ #include #include #include +#include namespace kep3::leg { @@ -41,14 +42,16 @@ class kep3_DLL_PUBLIC sims_flanagan_hf { public: // Default Constructor. - sims_flanagan_hf() = default; + sims_flanagan_hf(); // = default; // Constructors - sims_flanagan_hf( - const std::array, 2> &rvs, double ms, std::vector throttles, - const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, - double mu, double cut, double ts, - const std::optional &, heyoka::taylor_adaptive &>> - &tas); + // Backwards-compatible constructor with rv and m states separately + sims_flanagan_hf(const std::array, 2> &rvs, double ms, std::vector throttles, + const std::array, 2> &rvf, double mf, double tof, double max_thrust, + double isp, double mu, double cut = 0.5, double tol = 1e-16); + // Constructor with rvm states + sims_flanagan_hf(const std::array &rvms, std::vector throttles, + const std::array &rvmf, double tof, double max_thrust, double isp, double mu, + double cut, double tol); // Setters void set_tof(double tof); @@ -62,68 +65,81 @@ class kep3_DLL_PUBLIC sims_flanagan_hf void set_isp(double isp); void set_mu(double mu); void set_cut(double cut); - void set_ts(double ts); + void set_tol(double tol); + void set_tas(heyoka::taylor_adaptive tas); + void set_rvms(std::array rvms); + void set_rvmf(std::array rvmf); + void set_walking_rvm(std::array rvm); + // Backwards-compatible setting function with rv and m states separately void set(const std::array, 2> &rvs, double ms, const std::vector &throttles, const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, - double mu, double cut = 0.5); + double mu, double cut = 0.5, double tol = 1e-16); + // Setting function with rvm states + void set(const std::array &rvms, const std::vector &throttles, const std::array &rvmf, + double tof, double max_thrust, double isp, + double mu, double cut = 0.5, double tol = 1e-16); // Getters [[nodiscard]] double get_tof() const; - [[nodiscard]] const std::array, 2> &get_rvs() const; + [[nodiscard]] const std::array, 2> get_rvs() const; [[nodiscard]] double get_ms() const; [[nodiscard]] const std::vector &get_throttles() const; - [[nodiscard]] const std::array, 2> &get_rvf() const; + [[nodiscard]] const std::array, 2> get_rvf() const; [[nodiscard]] double get_mf() const; [[nodiscard]] double get_max_thrust() const; [[nodiscard]] double get_isp() const; [[nodiscard]] double get_mu() const; [[nodiscard]] double get_cut() const; - [[nodiscard]] double get_ts() const; + [[nodiscard]] double get_tol() const; [[nodiscard]] unsigned get_nseg() const; [[nodiscard]] unsigned get_nseg_fwd() const; [[nodiscard]] unsigned get_nseg_bck() const; + [[nodiscard]] heyoka::taylor_adaptive get_tas() const; + [[nodiscard]] std::array get_rvms() const; + [[nodiscard]] std::array get_rvmf() const; + [[nodiscard]] std::array get_walking_rvm() const; // Compute constraints - [[nodiscard]] std::array compute_mismatch_constraints() const; + [[nodiscard]] std::array compute_mismatch_constraints(); [[nodiscard]] std::vector compute_throttle_constraints() const; - // Compute mismatch constraint gradients (w.r.t. rvm state and w.r.t. throttles, tof) - [[nodiscard]] std::tuple, std::array, std::vector> - compute_mc_grad() const; + // // Compute mismatch constraint gradients (w.r.t. rvm state and w.r.t. throttles, tof) + // [[nodiscard]] std::tuple, std::array, std::vector> + // compute_mc_grad() const; - // Compute throttle constraint gradients - [[nodiscard]] std::vector compute_tc_grad() const; + // // Compute throttle constraint gradients + // [[nodiscard]] std::vector compute_tc_grad() const; private: friend class boost::serialization::access; template void serialize(Archive &ar, const unsigned int) { - ar &m_rvs; - ar &m_ms; - ar &m_throttles; - ar &m_tof; - ar &m_rvf; - ar &m_mf; - ar &m_max_thrust; - ar &m_isp; - ar &m_mu; - ar &m_cut; - ar &m_ts; - ar &m_nseg; - ar &m_nseg_fwd; - ar &m_nseg_bck; - ar &m_tas; + ar & m_rvms; + ar & m_throttles; + ar & m_thrusts; + ar & m_tof; + ar & m_rvmf; + ar & m_max_thrust; + ar & m_isp; + ar & m_mu; + ar & m_cut; + ar & m_tol; + ar & m_nseg; + ar & m_nseg_fwd; + ar & m_nseg_bck; + ar & m_tas; + // ar & m_walking_rvm; } - // Initial spacecraft state. - std::array, 2> m_rvs{{{1., 0., 0.}, {0, 1., 0.}}}; - double m_ms = 1.; + // Initial rvm state + std::array m_rvms{1., 0., 0., 0., 1., 0., 1.}; // Sequence of throttles. - std::vector m_throttles{0., .0, 0., 0., 0., 0.}; - // Final spacecraft state. - std::array, 2> m_rvf{{{0., 1., 0.}, {-1., 0., 0.}}}; - double m_mf = 1.; + std::vector m_throttles{0., 0., 0., 0., 0., 0.}; + // Sequence of thrusts. + std::vector m_thrusts{0., 0., 0., 0., 0., 0.}; + // Final rvm state + std::array m_rvmf{0., 1., 0., -1., 0., 0., 1.}; // Time of flight (defaults to 1/4 of the period) double m_tof = kep3::pi / 2; // Spacecraft propulsion system maximum thrust. @@ -134,16 +150,19 @@ class kep3_DLL_PUBLIC sims_flanagan_hf double m_mu{1.}; // The cut parameter double m_cut = 0.5; - // The reference epoch - double m_ts = 0.; - // The adaptive Taylor integrators - std::optional, heyoka::taylor_adaptive>> m_tas = std::nullopt; + // The tolerance + double m_tol = 1e-16; // Segment sizes unsigned m_nseg = 2u; unsigned m_nseg_fwd = 1u; unsigned m_nseg_bck = 1u; + // We introduce ta from cache + const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); + heyoka::taylor_adaptive m_tas = ta_cache; + }; + // Streaming operator for the class kep3::leg::sims_flanagan. kep3_DLL_PUBLIC std::ostream &operator<<(std::ostream &, const sims_flanagan_hf &); diff --git a/include/kep3/ta/stark.hpp b/include/kep3/ta/stark.hpp index 52a1a716..d76a235c 100644 --- a/include/kep3/ta/stark.hpp +++ b/include/kep3/ta/stark.hpp @@ -10,7 +10,6 @@ #ifndef kep3_TA_LT_KEPLER_H #define kep3_TA_LT_KEPLER_H -#include #include #include @@ -24,7 +23,7 @@ namespace kep3::ta kep3_DLL_PUBLIC std::vector> stark_dyn(); // These return const references to function level static variables of type heyoka::taylor_adaptive. -// NOTE: The object retruned are expected to be copied to then be modified. +// NOTE: The object returned are expected to be copied to then be modified. kep3_DLL_PUBLIC const heyoka::taylor_adaptive &get_ta_stark(double tol); kep3_DLL_PUBLIC const heyoka::taylor_adaptive &get_ta_stark_var(double tol); // variational (x,y,z,vx,vy,vz,ux,uy,uz) first order diff --git a/src/core_astro/propagate_lagrangian.cpp b/src/core_astro/propagate_lagrangian.cpp index fcb14ae9..902be8ef 100644 --- a/src/core_astro/propagate_lagrangian.cpp +++ b/src/core_astro/propagate_lagrangian.cpp @@ -263,7 +263,7 @@ propagate_lagrangian_u(const std::array, 2> &pos_vel0, con * such a high factor ..investigate?) */ std::pair, 2>, std::optional>> -propagate_keplerian(const std::array, 2> &pos_vel0, const double dt, const double mu, // NOLINT +propagate_keplerian(const std::array, 2> &pos_vel0, const double dt, const double mu, // NOLINT bool) { // 1 - Compute the orbital parameters at t0 diff --git a/src/leg/sf_checks.cpp b/src/leg/sf_checks.cpp new file mode 100644 index 00000000..ddaa20ee --- /dev/null +++ b/src/leg/sf_checks.cpp @@ -0,0 +1,97 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include + +void _check_tof(double tof) +{ + // SC: One should be able to give this as a negative number to run the system backwards, no? + if (tof < 0.) { + ; + // throw std::domain_error("The time of flight of a sims_flanagan leg needs to be larger or equal to zero."); + } +} +void _check_throttles(const std::vector &throttles, unsigned nseg) +{ + if ((throttles.size() % 3) != 0u) { + throw std::logic_error("The throttles of a sims_flanagan leg are detected to be not a multiple of 3 in size " + "[u0x, u0y, u0z, .....]."); + } + if (throttles.size() / 3 != static_cast(nseg)) + { + throw std::logic_error("The throttle count does not correspond to the number of segments provided."); + } + if (throttles.empty()) { + throw std::logic_error( + "The throttles of a sims_flanagan leg are detected to be empty! At least one segment is necessary."); + } +} +void _check_max_thrust(double max_thrust) +{ + if (max_thrust < 0.) { + throw std::domain_error( + "The maximum allowed thrust of a sims_flanagan leg is detected to be smaller than zero."); + } +} +void _check_isp(double isp) +{ + if (isp < 0.) { + throw std::domain_error("The specific impulse of a sims_flanagan leg is detected to be smaller than zero."); + } +} +void _check_mu(double mu) +{ + if (mu < 0.) { + throw std::domain_error( + "The gravitational parameter of a sims_flanagan leg is detected to be smaller than zero."); + } +} +void _check_cut(double cut) +{ + if (cut < 0. || cut > 1.) { + throw std::domain_error("The parameter cut of a sims_flanagan leg must be in [0, 1]."); + } +} +void _check_tol(double tol) +{ + if (tol <= 0. || tol > 1.) { + throw std::domain_error("The parameter tol of a high-fidelity sims-flanagan leg leg must be in <0, 1]."); + } +} +void _check_nseg(unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck) +{ + if (nseg_fwd + nseg_bck != nseg) + { + throw std::logic_error("The number of segments provided does not add up."); + } +} +void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, + double cut, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck) +{ + _check_throttles(throttles, nseg); + _check_tof(tof); + _check_max_thrust(max_thrust); + _check_isp(isp); + _check_mu(mu); + _check_cut(cut); + _check_nseg(nseg, nseg_fwd, nseg_bck); +} +void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, + double cut, double tol, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck) +{ + _check_throttles(throttles, nseg); + _check_tof(tof); + _check_max_thrust(max_thrust); + _check_isp(isp); + _check_mu(mu); + _check_cut(cut); + _check_tol(tol); + _check_nseg(nseg, nseg_fwd, nseg_bck); +} diff --git a/src/leg/sims_flanagan.cpp b/src/leg/sims_flanagan.cpp index f26ae5c1..c9e4d1bc 100644 --- a/src/leg/sims_flanagan.cpp +++ b/src/leg/sims_flanagan.cpp @@ -9,8 +9,6 @@ #include #include -#include -#include #include #include @@ -30,6 +28,7 @@ #include #include #include +#include #include namespace kep3::leg @@ -41,62 +40,7 @@ using kep3::linalg::mat61; using kep3::linalg::mat63; using kep3::linalg::mat66; -void _check_tof(double tof) -{ - if (tof < 0.) { - throw std::domain_error("The time of flight of a sims_flanagan leg needs to be larger or equal to zero."); - } -} -void _check_throttles(const std::vector &throttles) -{ - if ((throttles.size() % 3) != 0u) { - throw std::logic_error("The throttles of a sims_flanagan leg are detected to be not a multiple of 3 in size " - "[u0x, u0y, u0z, .....]."); - } - if (throttles.empty()) { - throw std::logic_error( - "The throttles of a sims_flanagan leg are detected to be empty! At least one segment is necessary."); - } -} -void _check_max_thrust(double max_thrust) -{ - if (max_thrust < 0.) { - throw std::domain_error( - "The maximum allowed thrust of a sims_flanagan leg is detected to be smaller than zero."); - } -} -void _check_isp(double isp) -{ - if (isp < 0.) { - throw std::domain_error("The specific impulse of a sims_flanagan leg is detected to be smaller than zero."); - } -} -void _check_mu(double mu) -{ - if (mu < 0.) { - throw std::domain_error( - "The gravitational parameter of a sims_flanagan leg is detected to be smaller than zero."); - } -} -void _check_cut(double cut) -{ - if (cut < 0. || cut > 1.) { - throw std::domain_error("The parameter cut of a sims_flanagan leg must be in [0, 1]."); - } -} -void _sanity_checks(const std::array, 2> &, double, const std::vector &throttles, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const std::array, 2> &, double, double tof, double max_thrust, double isp, - double mu, double cut) -{ - _check_throttles(throttles); - _check_tof(tof); - _check_max_thrust(max_thrust); - _check_isp(isp); - _check_mu(mu); - _check_cut(cut); -} - +// Constructors sims_flanagan::sims_flanagan(const std::array, 2> &rvs, double ms, std::vector throttles, // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) const std::array, 2> &rvf, double mf, double tof, double max_thrust, @@ -106,7 +50,7 @@ sims_flanagan::sims_flanagan(const std::array, 2> &rvs, do m_nseg(static_cast(m_throttles.size()) / 3u), m_nseg_fwd(static_cast(static_cast(m_nseg) * m_cut)), m_nseg_bck(m_nseg - m_nseg_fwd) { - _sanity_checks(rvs, ms, m_throttles, rvf, mf, tof, max_thrust, isp, mu, cut); + _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_nseg, m_nseg_fwd, m_nseg_bck); } // Setters @@ -125,7 +69,7 @@ void sims_flanagan::set_ms(double mass) } void sims_flanagan::set_throttles(std::vector throttles) { - _check_throttles(throttles); + _check_throttles(throttles, m_nseg); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); @@ -178,7 +122,7 @@ void sims_flanagan::set(const std::array, 2> &rvs, double const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, double mu, double cut) { - _sanity_checks(rvs, ms, throttles, rvf, mf, tof, max_thrust, isp, mu, cut); + _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_nseg, m_nseg_fwd, m_nseg_bck); m_rvs = rvs; m_ms = ms; m_throttles = throttles; diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 358eb125..0ef03559 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -9,9 +9,8 @@ #include #include -#include -#include #include +#include #include #include @@ -27,98 +26,84 @@ #include #include -#include #include +#include #include #include +#include + +#include namespace kep3::leg { -using kep3::linalg::_dot; +// using kep3::linalg::_dot; using kep3::linalg::mat13; using kep3::linalg::mat61; using kep3::linalg::mat63; using kep3::linalg::mat66; -void _check_tof(double tof) -{ - if (tof < 0.) { - throw std::domain_error( - "The time of flight of a high-fidelity sims-flanagan leg leg needs to be larger or equal to zero."); - } -} -void _check_throttles(const std::vector &throttles) -{ - if ((throttles.size() % 3) != 0u) { - throw std::logic_error( - "The throttles of a high-fidelity sims-flanagan leg leg are detected to be not a multiple of 3 in size " - "[u0x, u0y, u0z, .....]."); - } - if (throttles.empty()) { - throw std::logic_error("The throttles of a high-fidelity sims-flanagan leg leg are detected to be empty! At " - "least one segment is necessary."); - } -} -void _check_max_thrust(double max_thrust) -{ - if (max_thrust < 0.) { - throw std::domain_error( - "The maximum allowed thrust of a high-fidelity sims-flanagan leg leg is detected to be smaller than zero."); - } -} -void _check_isp(double isp) -{ - if (isp < 0.) { - throw std::domain_error( - "The specific impulse of a high-fidelity sims-flanagan leg leg is detected to be smaller than zero."); - } -} -void _check_mu(double mu) -{ - if (mu < 0.) { - throw std::domain_error("The gravitational parameter of a high-fidelity sims-flanagan leg leg is detected to " - "be smaller than zero."); - } -} -void _check_cut(double cut) +// Constructors + +sims_flanagan_hf::sims_flanagan_hf() { - if (cut < 0. || cut > 1.) { - throw std::domain_error("The parameter cut of a high-fidelity sims-flanagan leg leg must be in [0, 1]."); - } -} -void _sanity_checks(const std::array, 2> &, double, const std::vector &throttles, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const std::array, 2> &, double, double tof, double max_thrust, double isp, - double mu, double cut) + // We perform some sanity checks on the user provided inputs + _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + // We set mu and veff for the non variational + *m_tas.get_pars_data() = m_mu; + *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; + + // Convert throttles to current_thrusts. (SC: Change so that we don't need extra memory) + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); +} +sims_flanagan_hf::sims_flanagan_hf(const std::array, 2> &rvs, double ms, + std::vector throttles, + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + const std::array, 2> &rvf, double mf, double tof, + double max_thrust, double isp, double mu, double cut, double tol) + : m_throttles(std::move(throttles)), m_tof(tof), m_max_thrust(max_thrust), m_isp(isp), m_mu(mu), m_cut(cut), + m_tol(tol), m_nseg(static_cast(m_throttles.size()) / 3u), + m_nseg_fwd(static_cast(static_cast(m_nseg) * m_cut)), m_nseg_bck(m_nseg - m_nseg_fwd) { - _check_throttles(throttles); - _check_tof(tof); - _check_max_thrust(max_thrust); - _check_isp(isp); - _check_mu(mu); - _check_cut(cut); -} - -sims_flanagan_hf::sims_flanagan_hf( - const std::array, 2> &rvs, double ms, std::vector throttles, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, double mu, - double cut, double ts, - const std::optional &, heyoka::taylor_adaptive &>> &tas) - : m_rvs(rvs), m_ms(ms), m_throttles(std::move(throttles)), m_rvf(rvf), m_mf(mf), m_tof(tof), - m_max_thrust(max_thrust), m_isp(isp), m_mu(mu), m_cut(cut), m_ts(ts), - m_nseg(static_cast(m_throttles.size()) / 3u), + // We perform some sanity checks on the user provided inputs + _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + // We set mu and veff for the non variational + *m_tas.get_pars_data() = m_mu; + *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; + + // Convert throttles to current_thrusts. (SC: Change so that we don't need extra memory) + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); + // Fill in m_rvm from m_rvs and m_ms + std::copy(rvs[0].begin(), rvs[0].end(), m_rvms.begin()); + std::copy(rvs[1].begin(), rvs[1].end(), std::next(m_rvms.begin(), 3)); + set_ms(ms); + // Fill in m_rvm from m_rvf and m_mf + std::copy(rvf[0].begin(), rvf[0].end(), m_rvmf.begin()); + std::copy(rvf[1].begin(), rvf[1].end(), std::next(m_rvmf.begin(), 3)); + set_mf(mf); +} + +sims_flanagan_hf::sims_flanagan_hf(const std::array &rvms, std::vector throttles, + const std::array &rvmf, double tof, double max_thrust, double isp, + double mu, double cut, double tol) + : m_rvms(rvms), m_throttles(std::move(throttles)), m_rvmf(rvmf), m_tof(tof), m_max_thrust(max_thrust), m_isp(isp), + m_mu(mu), m_cut(cut), m_tol(tol), m_nseg(static_cast(m_throttles.size()) / 3u), m_nseg_fwd(static_cast(static_cast(m_nseg) * m_cut)), m_nseg_bck(m_nseg - m_nseg_fwd) { // We perform some sanity checks on the user provided inputs - _sanity_checks(rvs, ms, m_throttles, rvf, mf, tof, max_thrust, isp, mu, cut); - // If the user provides the taylor integrators we use those, else we provide default ones (Keplerian) - if (tas) { - m_tas.value() = tas.value(); - } else { - tas = _build_default_taylors(); - } + _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + // We set mu and veff for the non variational + *m_tas.get_pars_data() = m_mu; + *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; + + // Convert throttles to current_thrusts. (SC: Change so that we don't need extra memory) + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } // Setters @@ -129,15 +114,16 @@ void sims_flanagan_hf::set_tof(double tof) } void sims_flanagan_hf::set_rvs(std::array, 2> rv) { - m_rvs = rv; + std::copy(rv[0].begin(), rv[0].end(), m_rvms.begin()); + std::copy(rv[1].begin(), rv[1].end(), std::next(m_rvms.begin(), 3)); } void sims_flanagan_hf::set_ms(double mass) { - m_ms = mass; + m_rvms[6] = mass; } void sims_flanagan_hf::set_throttles(std::vector throttles) { - _check_throttles(throttles); + _check_throttles(throttles, m_nseg); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); @@ -157,11 +143,12 @@ void sims_flanagan_hf::set_throttles(std::vector::const_iterator it1, st } void sims_flanagan_hf::set_rvf(std::array, 2> rv) { - m_rvf = rv; + std::copy(rv[0].begin(), rv[0].end(), m_rvmf.begin()); + std::copy(rv[1].begin(), rv[1].end(), std::next(m_rvmf.begin(), 3)); } void sims_flanagan_hf::set_mf(double mass) { - m_mf = mass; + m_rvmf[6] = mass; } void sims_flanagan_hf::set_max_thrust(double max_thrust) { @@ -185,23 +172,59 @@ void sims_flanagan_hf::set_cut(double cut) m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; } +void sims_flanagan_hf::set_tol(double tol) +{ + _check_tol(tol); + m_tol = tol; +} +void sims_flanagan_hf::set_rvms(std::array rvms) +{ + m_rvms = rvms; +} +void sims_flanagan_hf::set_rvmf(std::array rvmf) +{ + m_rvmf = rvmf; +} + void sims_flanagan_hf::set(const std::array, 2> &rvs, double ms, const std::vector &throttles, // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) const std::array, 2> &rvf, double mf, double tof, double max_thrust, - double isp, double mu, double cut) + double isp, double mu, double cut, double tol) +{ + _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); + // Set initial state + set_rvs(rvs); + set_ms(ms); + // Set final state + set_rvf(rvf); + set_mf(mf); + m_throttles = throttles; + m_tof = tof; + m_max_thrust = max_thrust; + m_isp = isp; + m_mu = mu; + m_cut = cut; + m_tol = tol; + m_nseg = static_cast(m_throttles.size()) / 3u; + m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); + m_nseg_bck = m_nseg - m_nseg_fwd; +} + +void sims_flanagan_hf::set(const std::array &rvms, const std::vector &throttles, + const std::array &rvmf, double tof, double max_thrust, double isp, double mu, + double cut, double tol) { - _sanity_checks(rvs, ms, throttles, rvf, mf, tof, max_thrust, isp, mu, cut); - m_rvs = rvs; - m_ms = ms; + _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); + set_rvms(rvms); m_throttles = throttles; - m_rvf = rvf; - m_mf = mf; + set_rvmf(rvmf); m_tof = tof; m_max_thrust = max_thrust; m_isp = isp; m_mu = mu; m_cut = cut; + m_tol = tol; m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; @@ -212,25 +235,33 @@ double sims_flanagan_hf::get_tof() const { return m_tof; } -const std::array, 2> &sims_flanagan_hf::get_rvs() const +const std::array, 2> sims_flanagan_hf::get_rvs() const { - return m_rvs; + // SC: This could be done with xtensor? It isn't very efficient + std::array, 2> rvs{}; + std::copy(m_rvms.begin(), std::next(m_rvms.begin(), 3), rvs[0].begin()); + std::copy(std::next(m_rvms.begin(), 3), std::next(m_rvms.begin(), 6), rvs[1].begin()); + return rvs; } double sims_flanagan_hf::get_ms() const { - return m_ms; + return m_rvms[6]; } const std::vector &sims_flanagan_hf::get_throttles() const { return m_throttles; } -const std::array, 2> &sims_flanagan_hf::get_rvf() const +const std::array, 2> sims_flanagan_hf::get_rvf() const { - return m_rvf; + // SC: This could be done with xtensor? It isn't very efficient + std::array, 2> rvf{{{0., 0., 0.}, {0., 0., 0.}}}; + std::copy(m_rvmf.begin(), std::next(m_rvmf.begin(), 3), rvf[0].begin()); + std::copy(std::next(m_rvmf.begin(), 3), std::next(m_rvmf.begin(), 6), rvf[1].begin()); + return rvf; } double sims_flanagan_hf::get_mf() const { - return m_mf; + return m_rvmf[6]; } double sims_flanagan_hf::get_max_thrust() const { @@ -248,6 +279,10 @@ double sims_flanagan_hf::get_cut() const { return m_cut; } +double sims_flanagan_hf::get_tol() const +{ + return m_tol; +} unsigned sims_flanagan_hf::get_nseg() const { return m_nseg; @@ -260,343 +295,350 @@ unsigned sims_flanagan_hf::get_nseg_bck() const { return m_nseg_bck; } - -// The core routines -std::array sims_flanagan_hf::compute_mismatch_constraints() const +heyoka::taylor_adaptive sims_flanagan_hf::get_tas() const { - // We introduce some convenience variables - std::array dv{}; - const double veff = m_isp * kep3::G0; - const double dt = m_tof / static_cast(m_nseg); - const double c = m_max_thrust * dt; - // Forward pass - // Initial state - std::array, 2> rv_fwd(get_rvs()); - double mass_fwd = get_ms(); - // We propagate for a first dt/2 (only if there is at least one forward segment) - if (m_nseg_fwd > 0) { - rv_fwd = propagate_lagrangian(rv_fwd, dt / 2, m_mu, false).first; - } - // We now loop through the forward segments and 1) add a dv + 2) propagate for dt (except on the last segment, where - // we propagate for dt/2). - for (decltype(m_throttles.size()) i = 0u; i < m_nseg_fwd; ++i) { - // We compute the the dv - dv[0] = c / mass_fwd * m_throttles[3 * i]; - dv[1] = c / mass_fwd * m_throttles[3 * i + 1]; - dv[2] = c / mass_fwd * m_throttles[3 * i + 2]; - // Add it to the current spacecraft velocity - rv_fwd[1][0] += dv[0]; - rv_fwd[1][1] += dv[1]; - rv_fwd[1][2] += dv[2]; - // Update the mass accordingly - const double norm_dv = std::sqrt(dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]); - mass_fwd *= std::exp(-norm_dv / veff); - // Perform the propagation - const double prop_duration = (i == m_nseg_fwd - 1) ? dt / 2 : dt; - rv_fwd = propagate_lagrangian(rv_fwd, prop_duration, m_mu, false).first; - } - - // Backward pass - // Final state - std::array, 2> rv_bck(get_rvf()); - double mass_bck = get_mf(); - // We propagate for a first dt/2 (only if there is at least one backward segment) - if (m_nseg_bck > 0) { - rv_bck = propagate_lagrangian(rv_bck, -dt / 2, m_mu, false).first; - } - // We now loop through the backward segments and 1) add a dv + 2) propagate for -dt (except on the last segment, - // where we propagate for -dt/2). - for (decltype(m_throttles.size()) i = 0u; i < m_nseg_bck; ++i) { - // We compute the the dv - dv[0] = c / mass_bck * m_throttles[m_throttles.size() - 1 - 3 * i - 2]; - dv[1] = c / mass_bck * m_throttles[m_throttles.size() - 1 - 3 * i - 1]; - dv[2] = c / mass_bck * m_throttles[m_throttles.size() - 1 - 3 * i]; - // Subtract it (remember we are going backward) to the current spacecraft velocity - rv_bck[1][0] -= dv[0]; - rv_bck[1][1] -= dv[1]; - rv_bck[1][2] -= dv[2]; - // Update the mass accordingly (will increase as we go backward) - double norm_dv = std::sqrt(dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]); - mass_bck *= std::exp(norm_dv / veff); - // Perform the propagation - double prop_duration = (i == m_nseg_bck - 1) ? -dt / 2 : -dt; - rv_bck = propagate_lagrangian(rv_bck, prop_duration, m_mu, false).first; - } - - return {rv_fwd[0][0] - rv_bck[0][0], rv_fwd[0][1] - rv_bck[0][1], rv_fwd[0][2] - rv_bck[0][2], - rv_fwd[1][0] - rv_bck[1][0], rv_fwd[1][1] - rv_bck[1][1], rv_fwd[1][2] - rv_bck[1][2], - mass_fwd - mass_bck}; + return m_tas; } - -std::vector sims_flanagan_hf::compute_throttle_constraints() const +std::array sims_flanagan_hf::get_rvms() const { - std::vector retval(m_nseg); - for (decltype(m_throttles.size()) i = 0u; i < m_nseg; ++i) { - retval[i] = m_throttles[3 * i] * m_throttles[3 * i] + m_throttles[3 * i + 1] * m_throttles[3 * i + 1] - + m_throttles[3 * i + 2] * m_throttles[3 * i + 2] - 1.; - } - return retval; + return m_rvms; } - -mat61 _dyn(std::array, 2> rv, double mu) +std::array sims_flanagan_hf::get_rvmf() const { - mat61 retval; - auto R3 = std::pow(rv[0][0] * rv[0][0] + rv[0][1] * rv[0][1] + rv[0][2] * rv[0][2], 1.5); - retval(0, 0) = rv[1][0]; - retval(1, 0) = rv[1][1]; - retval(2, 0) = rv[1][2]; - retval(3, 0) = -mu / R3 * rv[0][0]; - retval(4, 0) = -mu / R3 * rv[0][1]; - retval(5, 0) = -mu / R3 * rv[0][2]; - return retval; + return m_rvmf; } -// Performs the state updates for nseg sarting from rvs, ms. Computes all gradient information -std::pair, std::vector> sims_flanagan_hf::gradients_multiple_impulses( - std::vector::const_iterator th1, std::vector::const_iterator th2, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const std::array, 2> &rvs, double ms, double c, double a, double dt) const +// The core routines +std::array sims_flanagan_hf::compute_mismatch_constraints() { - assert(std::distance(th1, th2) % 3 == 0u); - auto nseg = static_cast(std::distance(th1, th2) / 3u); + // General settings + const double prop_seg_duration = (m_tof / m_nseg); // * static_cast(i + 1); - // Corner case: nseg is zero - if (nseg == 0) { - std::array grad_rvm{}; // The mismatch constraints gradient w.r.t. extended state r,v,m - auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); - xgrad_rvm = xt::eye(7); - std::vector grad(7, 0.); // The mismatch constraints gradient w.r.t. throttles (0 in this case) and tof - return std::make_pair(grad_rvm, std::move(grad)); - } - // Allocate memory. - std::vector u(nseg); - std::vector> du(nseg, xt::zeros({3u, nseg * 3u + 2u})); - std::vector m(nseg + 1, 0.); - std::vector> dm(nseg + 1u, xt::zeros({1u, nseg * 3u + 2u})); - xt::xarray dtof = xt::zeros({1u, nseg * 3u + 2u}); - std::vector Dv(nseg); - std::vector> dDv(nseg, xt::zeros({3u, nseg * 3u + 2u})); - std::vector M(nseg + 1); // The STMs - std::vector Mc(nseg + 1); // Mc will contain [Mn@..@M0,Mn@..@M1, Mn] - std::vector f(nseg + 1, xt::zeros({6u, 1u})); - // Initialize values - m[0] = ms; - unsigned i_tmp = 0u; - for (auto it = th1; it != th2; it += 3) { - u[i_tmp](0, 0) = *it; - u[i_tmp](0, 1) = *(it + 1); - u[i_tmp](0, 2) = *(it + 2); - du[i_tmp](0, 3 * i_tmp) = 1.; - du[i_tmp](1, 3 * i_tmp + 1) = 1.; - du[i_tmp](2, 3 * i_tmp + 2) = 1.; - i_tmp++; - } - dm[0](0, nseg * 3u) = 1.; - dtof(0, nseg * 3u + 1) = 1.; - // 1 - We compute the mass schedule and related gradients - for (decltype(nseg) i = 0; i < nseg; ++i) { - Dv[i] = c / m[i] * u[i]; - double un = std::sqrt(u[i](0, 0) * u[i](0, 0) + u[i](0, 1) * u[i](0, 1) + u[i](0, 2) * u[i](0, 2)); - double Dvn = c / m[i] * un; - dDv[i] = c / m[i] * du[i] - c / m[i] / m[i] * xt::linalg::dot(xt::transpose(u[i]), dm[i]) - + m_max_thrust / m[i] * xt::linalg::dot(xt::transpose(u[i]), dtof) / nseg; - auto dDvn = c / m[i] / un * xt::linalg::dot(u[i], du[i]) - c / m[i] / m[i] * un * dm[i] - + m_max_thrust / m[i] * un * dtof / nseg; - m[i + 1] = m[i] * std::exp(-Dvn * a); - dm[i + 1] = -m[i + 1] * a * dDvn + std::exp(-Dvn * a) * dm[i]; - } - // 2 - We compute the various STMs - std::array, 2> rv_it(rvs); - std::optional> M_it; - for (decltype(nseg) i = 0; i < nseg + 1; ++i) { - auto dur = dt; - if (i == 0 || i == nseg) { - dur = dt / 2; + // Forward pass + // Initial state + // Set the Taylor Integration initial conditions + m_tas.set_time(0.); + std::copy(m_rvms.begin(), m_rvms.end(), m_tas.get_state_data()); + + // Loop through segments in forward pass of Sims-Flanagan transcription + for (unsigned int i = 0u; i < m_nseg_fwd; ++i) { + // Assign current thrusts to Taylor adaptive integrator + if (static_cast((i + 1) * 3) <= m_thrusts.size()) { + std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), + std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), + std::next(m_tas.get_pars_data(), 2)); + } else { + throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); } - - std::tie(rv_it, M_it) = kep3::propagate_lagrangian(rv_it, dur, m_mu, true); - // Now we have the STM in M_it, but its a vector, we must operate on an xtensor object instead. - assert(M_it); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - M[i] = xt::adapt(*M_it, {6, 6}); - f[i] = _dyn(rv_it, m_mu); - // And add the impulse if needed - if (i < nseg) { - rv_it[1][0] += Dv[i](0, 0); - rv_it[1][1] += Dv[i](0, 1); - rv_it[1][2] += Dv[i](0, 2); + // ... and integrate + auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until((i + 1) * prop_seg_duration); // Added - for bck propagation + if (status != heyoka::taylor_outcome::time_limit) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } } - // 3 - We now need to apply the chain rule to assemble the gradients we want (i.e. not w.r.t DV but w.r.t. u etc...) - mat63 Iv = xt::zeros({6u, 3u}); // This is the gradient of x (rv) w.r.t. v - Iv(3, 0) = 1.; - Iv(4, 1) = 1.; - Iv(5, 2) = 1.; - Mc[nseg] = M[nseg]; // Mc will contain [Mn@..@M0,Mn@..@M1, Mn] - for (decltype(nseg) i = 1; i < nseg + 1; ++i) { - Mc[nseg - i] = _dot(Mc[nseg - i + 1], M[nseg - i]); - } - // grad_tof./ - // First the d/dtof term - example: (0.5 * f3 + M3 @ f2 + M3 @ M2 @ f1 + 0.5 * M3 @ M2 @ M1 @ f0) / N + // Set fwd final state + std::vector rvm_fwd_final = m_tas.get_state(); - mat61 grad_tof = 0.5 * f[nseg]; - for (decltype(nseg) i = 0; i + 1 < nseg; ++i) { // i+1 < nseg avoids overflow - grad_tof += _dot(Mc[i + 2], f[i + 1]); - } - - grad_tof += 0.5 * _dot(Mc[1], f[0]); - grad_tof /= nseg; - // Then we add the d/Dvi * dDvi/dtof - example: M3 @ Iv @ dDv2 + M3 @ M2 @ Iv @ dDv1 + M3 @ M2 @ M1 @ Iv @ dDv0 - for (decltype(nseg) i = 0; i < nseg; ++i) { - grad_tof += xt::linalg::dot(_dot(Mc[i + 1], Iv), - xt::eval(xt::view(dDv[i], xt::all(), xt::range(nseg * 3 + 1, nseg * 3 + 2)))); - } - // grad_u - xt::xarray grad_u = xt::zeros({6u, nseg * 3u}); - for (decltype(nseg) i = 0u; i < nseg; ++i) { - grad_u += xt::linalg::dot(_dot(Mc[i + 1], Iv), xt::eval(xt::view(dDv[i], xt::all(), xt::range(0, nseg * 3)))); - } - // grad_ms - xt::xarray grad_ms = xt::zeros({6u, 1u}); - for (decltype(nseg) i = 0u; i < nseg; ++i) { - grad_ms += xt::linalg::dot(_dot(Mc[i + 1], Iv), - xt::eval(xt::view(dDv[i], xt::all(), xt::range(nseg * 3, nseg * 3 + 1)))); - } - // grad_xs - mat66 grad_xs = Mc[0]; - - // Allocate the return values - std::array grad_rvm{}; // The mismatch constraints gradient w.r.t. extended state r,v,m - std::vector grad((nseg * 3lu + 1) * 7, - 0.); // The mismatch constraints gradient w.r.t. throttles and tof - // Copying in the computed derivatives - // a) xgrad (the xtensor gradient w.r.t. throttles and tof) - auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); - auto xgrad = xt::adapt(grad, {7u, nseg * 3 + 1u}); - xt::view(xgrad, xt::range(0u, 6u), xt::range(0u, nseg * 3u)) = grad_u; - xt::view(xgrad, xt::range(0u, 6u), xt::range(nseg * 3, nseg * 3 + 1)) = grad_tof; - xt::view(xgrad, xt::range(6u, 7u), xt::all()) = xt::view(dm[nseg], xt::all(), xt::range(0u, nseg * 3 + 1)); - // At this point since the variable order is u,m,tof we have put dmf/dms in rather than dms/dtof. So we fix this. - xgrad(6u, nseg * 3) = dm[nseg](0, nseg * 3 + 1); - // b) xgrad_rvm (the xtensor gradient w.r.t. the initial conditions) - xt::view(xgrad_rvm, xt::range(0, 6), xt::range(0, 6)) = grad_xs; - xt::view(xgrad_rvm, xt::range(0, 6), xt::range(6, 7)) = grad_ms; - xgrad_rvm(6, 6) = dm[nseg](0, nseg * 3); - return std::make_pair(grad_rvm, std::move(grad)); -} - -std::pair, std::vector> -sims_flanagan_hf::gradients_fwd(std::vector::const_iterator th1, std::vector::const_iterator th2, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const std::array, 2> &rvs, double ms, double c, double a, - double dt) const -{ - return gradients_multiple_impulses(th1, th2, rvs, ms, c, a, dt); -} - -std::pair, std::vector> -sims_flanagan_hf::gradients_bck(std::vector::const_iterator th1, std::vector::const_iterator th2, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const std::array, 2> &rvf_orig, double mf, double c, double a, - double dt) const -{ - // 1) we invert the starting velocity. - auto rvf = rvf_orig; - rvf[1][0] = -rvf[1][0]; - rvf[1][1] = -rvf[1][1]; - rvf[1][2] = -rvf[1][2]; - - // 2) we reverse the throttles ([1,2,3,4,5,6] -> [4,5,6,1,2,3]) - auto size = static_cast(std::distance(th1, th2)); - // Create a new vector to store the reversed values three by three. - // Here we allocate a vector. Might be not necessary using the C++ range library? - std::vector reversed_throttles(size); - // Iterate in reverse order with a step of three - for (decltype(size) i = 0u, j = size - 1; i < size; i += 3, j -= 3) { - // Copy three elements at a time in reverse order - reversed_throttles[j - 2] = *(th1 + i); - reversed_throttles[j - 1] = *(th1 + i + 1); - reversed_throttles[j] = *(th1 + i + 2); - } - - // 3) We reverse the Isp, hence veff (a = 1/veff) - a = -a; - - // 4) We then compute gradients as if this was a forward leg - auto [grad_rvm, grad] - = gradients_multiple_impulses(reversed_throttles.begin(), reversed_throttles.end(), rvf, mf, c, a, dt); - // 5) We have computed dxf/dxs and dxf/dus, but the initial and final velocites (and us) had their sign - // inverted! We thus need to account for that and change sign once again of the relevant entries. - // We also must account for changes in the mass equation (now -a) - auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); - xt::view(xgrad_rvm, xt::range(3, 6), xt::all()) *= -1; // dvf/dall - xt::view(xgrad_rvm, xt::all(), xt::range(0, 3)) *= -1; // dmc/drs - xt::view(xgrad_rvm, xt::all(), xt::range(6, 7)) *= -1; // dmc/dmf - - auto xgrad = xt::adapt(grad, {7u, size + 1u}); - xt::view(xgrad, xt::range(3, 6), xt::all()) *= -1; // dvf/dall - xt::view(xgrad, xt::all(), xt::range(0, size)) *= -1; // dmc/dus - - // 6) Note that the throttles in xgrad are ordered in reverse. Before returning we must restore the forward order - xt::view(xgrad, xt::all(), xt::range(0, size)) = xt::flip(xt::view(xgrad, xt::all(), xt::range(0, size)), 1); - for (decltype(size) i = 0u; i < size / 3; ++i) { - xt::view(xgrad, xt::all(), xt::range(3 * i, 3 * i + 3)) - = xt::flip(xt::view(xgrad, xt::all(), xt::range(3 * i, 3 * i + 3)), 1); + // Backward pass + // Final state + // Set the Taylor Integration final conditions + m_tas.set_time(m_tof); + std::copy(m_rvmf.begin(), m_rvmf.end(), m_tas.get_state_data()); + + // Loop through segments in backward pass of Sims-Flanagan transcription + for (unsigned int i = 0u; i < m_nseg_bck; ++i) { + // Assign current_thrusts to Taylor adaptive integrator + if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { + // Copy thrust into Taylor-adaptive integrator + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg -(i + 1)) * 3)), + std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), + std::next(m_tas.get_pars_data(), 2)); + } else { + throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); + } + // ... and integrate + auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until(m_tof - (i + 1) * prop_seg_duration); // Added - for bck propagation + if (status != heyoka::taylor_outcome::time_limit) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + } } - // And finally return. - return std::make_pair(grad_rvm, std::move(grad)); -} - -// Computes the gradient of the mismatch constraints w.r.t. xs, xf and [throttles, tof] -std::tuple, std::array, std::vector> sims_flanagan::compute_mc_grad() const -{ - // Preliminaries - const auto dt = m_tof / static_cast(m_nseg); // dt - const auto c = m_max_thrust * dt; // T*tof/nseg - const auto a = 1. / m_isp / kep3::G0; // 1/veff - - // We compute for the forward half-leg: dxf/dxs and dxf/dxu (the gradients w.r.t. initial state ant throttles ) - auto [grad_rvm, grad_fwd] - = gradients_fwd(m_throttles.begin(), m_throttles.begin() + static_cast(3 * m_nseg_fwd), get_rvs(), - get_ms(), c, a, dt); - // We compute for the backward half-leg: dxf/dxs and dxf/dxu (the gradients w.r.t. final state and throttles ) - auto [grad_rvm_bck, grad_bck] = gradients_bck(m_throttles.begin() + static_cast(3 * m_nseg_fwd), - m_throttles.end(), get_rvf(), get_mf(), c, a, dt); - - // We assemble the final results - std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); - auto xgrad_final = xt::adapt(grad_final, {7u, static_cast(m_nseg) * 3u + 1u}); - auto xgrad_fwd = xt::adapt(grad_fwd, {7u, static_cast(m_nseg_fwd) * 3u + 1u}); - auto xgrad_bck = xt::adapt(grad_bck, {7u, static_cast(m_nseg - m_nseg_fwd) * 3u + 1u}); - - // Copy the gradient w.r.t. the forward throttles as is - xt::view(xgrad_final, xt::all(), xt::range(0, m_nseg_fwd * 3)) - = xt::view(xgrad_fwd, xt::all(), xt::range(0, m_nseg_fwd * 3)); - // Copy the gradient w.r.t. the backward throttles as is - xt::view(xgrad_final, xt::all(), xt::range(m_nseg_fwd * 3, m_nseg * 3)) - = xt::view(xgrad_bck, xt::all(), xt::range(0, (m_nseg - m_nseg_fwd) * 3)); - - // Copy the gradient w.r.t. tof as fwd-bck - xt::view(xgrad_final, xt::all(), xt::range(m_nseg * 3, m_nseg * 3 + 1)) - = xt::view(xgrad_fwd, xt::all(), xt::range(m_nseg_fwd * 3, m_nseg_fwd * 3 + 1)) / m_nseg * m_nseg_fwd - - xt::view(xgrad_bck, xt::all(), xt::range((m_nseg - m_nseg_fwd) * 3, (m_nseg - m_nseg_fwd) * 3 + 1)) / m_nseg - * (m_nseg - m_nseg_fwd); - return {grad_rvm, grad_rvm_bck, std::move(grad_final)}; + return {rvm_fwd_final[0] - m_tas.get_state()[0], rvm_fwd_final[1] - m_tas.get_state()[1], + rvm_fwd_final[2] - m_tas.get_state()[2], rvm_fwd_final[3] - m_tas.get_state()[3], + rvm_fwd_final[4] - m_tas.get_state()[4], rvm_fwd_final[5] - m_tas.get_state()[5], + rvm_fwd_final[6] - m_tas.get_state()[6]}; } -std::vector sims_flanagan_hf::compute_tc_grad() const +std::vector sims_flanagan_hf::compute_throttle_constraints() const { - std::vector retval(static_cast(m_nseg) * m_nseg * 3u, 0); + std::vector retval(m_nseg); for (decltype(m_throttles.size()) i = 0u; i < m_nseg; ++i) { - retval[i * m_nseg * 3 + 3 * i] = 2 * m_throttles[3 * i]; - retval[i * m_nseg * 3 + 3 * i + 1] = 2 * m_throttles[3 * i + 1]; - retval[i * m_nseg * 3 + 3 * i + 2] = 2 * m_throttles[3 * i + 2]; + retval[i] = m_throttles[3 * i] * m_throttles[3 * i] + m_throttles[3 * i + 1] * m_throttles[3 * i + 1] + + m_throttles[3 * i + 2] * m_throttles[3 * i + 2] - 1.; } return retval; } +// mat61 _dyn(std::array, 2> rv, double mu) +// { +// mat61 retval; +// auto R3 = std::pow(rv[0][0] * rv[0][0] + rv[0][1] * rv[0][1] + rv[0][2] * rv[0][2], 1.5); +// retval(0, 0) = rv[1][0]; +// retval(1, 0) = rv[1][1]; +// retval(2, 0) = rv[1][2]; +// retval(3, 0) = -mu / R3 * rv[0][0]; +// retval(4, 0) = -mu / R3 * rv[0][1]; +// retval(5, 0) = -mu / R3 * rv[0][2]; +// return retval; +// } + +// // Performs the state updates for nseg sarting from rvs, ms. Computes all gradient information +// std::pair, std::vector> sims_flanagan_hf::gradients_multiple_impulses( +// std::vector::const_iterator th1, std::vector::const_iterator th2, +// // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +// const std::array, 2> &rvs, double ms, double c, double a, double dt) const +// { +// assert(std::distance(th1, th2) % 3 == 0u); +// auto nseg = static_cast(std::distance(th1, th2) / 3u); + +// // Corner case: nseg is zero +// if (nseg == 0) { +// std::array grad_rvm{}; // The mismatch constraints gradient w.r.t. extended state r,v,m +// auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); +// xgrad_rvm = xt::eye(7); +// std::vector grad(7, 0.); // The mismatch constraints gradient w.r.t. throttles (0 in this case) and +// tof return std::make_pair(grad_rvm, std::move(grad)); +// } +// // Allocate memory. +// std::vector u(nseg); +// std::vector> du(nseg, xt::zeros({3u, nseg * 3u + 2u})); +// std::vector m(nseg + 1, 0.); +// std::vector> dm(nseg + 1u, xt::zeros({1u, nseg * 3u + 2u})); +// xt::xarray dtof = xt::zeros({1u, nseg * 3u + 2u}); +// std::vector Dv(nseg); +// std::vector> dDv(nseg, xt::zeros({3u, nseg * 3u + 2u})); +// std::vector M(nseg + 1); // The STMs +// std::vector Mc(nseg + 1); // Mc will contain [Mn@..@M0,Mn@..@M1, Mn] +// std::vector f(nseg + 1, xt::zeros({6u, 1u})); +// // Initialize values +// m[0] = ms; +// unsigned i_tmp = 0u; +// for (auto it = th1; it != th2; it += 3) { +// u[i_tmp](0, 0) = *it; +// u[i_tmp](0, 1) = *(it + 1); +// u[i_tmp](0, 2) = *(it + 2); +// du[i_tmp](0, 3 * i_tmp) = 1.; +// du[i_tmp](1, 3 * i_tmp + 1) = 1.; +// du[i_tmp](2, 3 * i_tmp + 2) = 1.; +// i_tmp++; +// } +// dm[0](0, nseg * 3u) = 1.; +// dtof(0, nseg * 3u + 1) = 1.; +// // 1 - We compute the mass schedule and related gradients +// for (decltype(nseg) i = 0; i < nseg; ++i) { +// Dv[i] = c / m[i] * u[i]; +// double un = std::sqrt(u[i](0, 0) * u[i](0, 0) + u[i](0, 1) * u[i](0, 1) + u[i](0, 2) * u[i](0, 2)); +// double Dvn = c / m[i] * un; +// dDv[i] = c / m[i] * du[i] - c / m[i] / m[i] * xt::linalg::dot(xt::transpose(u[i]), dm[i]) +// + m_max_thrust / m[i] * xt::linalg::dot(xt::transpose(u[i]), dtof) / nseg; +// auto dDvn = c / m[i] / un * xt::linalg::dot(u[i], du[i]) - c / m[i] / m[i] * un * dm[i] +// + m_max_thrust / m[i] * un * dtof / nseg; +// m[i + 1] = m[i] * std::exp(-Dvn * a); +// dm[i + 1] = -m[i + 1] * a * dDvn + std::exp(-Dvn * a) * dm[i]; +// } +// // 2 - We compute the various STMs +// std::array, 2> rv_it(rvs); +// std::optional> M_it; +// for (decltype(nseg) i = 0; i < nseg + 1; ++i) { +// auto dur = dt; +// if (i == 0 || i == nseg) { +// dur = dt / 2; +// } + +// std::tie(rv_it, M_it) = kep3::propagate_lagrangian(rv_it, dur, m_mu, true); +// // Now we have the STM in M_it, but its a vector, we must operate on an xtensor object instead. +// assert(M_it); +// // NOLINTNEXTLINE(bugprone-unchecked-optional-access) +// M[i] = xt::adapt(*M_it, {6, 6}); +// f[i] = _dyn(rv_it, m_mu); +// // And add the impulse if needed +// if (i < nseg) { +// rv_it[1][0] += Dv[i](0, 0); +// rv_it[1][1] += Dv[i](0, 1); +// rv_it[1][2] += Dv[i](0, 2); +// } +// } + +// // 3 - We now need to apply the chain rule to assemble the gradients we want (i.e. not w.r.t DV but w.r.t. u +// etc...) mat63 Iv = xt::zeros({6u, 3u}); // This is the gradient of x (rv) w.r.t. v Iv(3, 0) = 1.; Iv(4, +// 1) = 1.; Iv(5, 2) = 1.; Mc[nseg] = M[nseg]; // Mc will contain [Mn@..@M0,Mn@..@M1, Mn] for (decltype(nseg) i = 1; +// i < nseg + 1; ++i) { +// Mc[nseg - i] = _dot(Mc[nseg - i + 1], M[nseg - i]); +// } +// // grad_tof./ +// // First the d/dtof term - example: (0.5 * f3 + M3 @ f2 + M3 @ M2 @ f1 + 0.5 * M3 @ M2 @ M1 @ f0) / N + +// mat61 grad_tof = 0.5 * f[nseg]; +// for (decltype(nseg) i = 0; i + 1 < nseg; ++i) { // i+1 < nseg avoids overflow +// grad_tof += _dot(Mc[i + 2], f[i + 1]); +// } + +// grad_tof += 0.5 * _dot(Mc[1], f[0]); +// grad_tof /= nseg; +// // Then we add the d/Dvi * dDvi/dtof - example: M3 @ Iv @ dDv2 + M3 @ M2 @ Iv @ dDv1 + M3 @ M2 @ M1 @ Iv @ dDv0 +// for (decltype(nseg) i = 0; i < nseg; ++i) { +// grad_tof += xt::linalg::dot(_dot(Mc[i + 1], Iv), +// xt::eval(xt::view(dDv[i], xt::all(), xt::range(nseg * 3 + 1, nseg * 3 + 2)))); +// } +// // grad_u +// xt::xarray grad_u = xt::zeros({6u, nseg * 3u}); +// for (decltype(nseg) i = 0u; i < nseg; ++i) { +// grad_u += xt::linalg::dot(_dot(Mc[i + 1], Iv), xt::eval(xt::view(dDv[i], xt::all(), xt::range(0, nseg * +// 3)))); +// } +// // grad_ms +// xt::xarray grad_ms = xt::zeros({6u, 1u}); +// for (decltype(nseg) i = 0u; i < nseg; ++i) { +// grad_ms += xt::linalg::dot(_dot(Mc[i + 1], Iv), +// xt::eval(xt::view(dDv[i], xt::all(), xt::range(nseg * 3, nseg * 3 + 1)))); +// } +// // grad_xs +// mat66 grad_xs = Mc[0]; + +// // Allocate the return values +// std::array grad_rvm{}; // The mismatch constraints gradient w.r.t. extended state r,v,m +// std::vector grad((nseg * 3lu + 1) * 7, +// 0.); // The mismatch constraints gradient w.r.t. throttles and tof +// // Copying in the computed derivatives +// // a) xgrad (the xtensor gradient w.r.t. throttles and tof) +// auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); +// auto xgrad = xt::adapt(grad, {7u, nseg * 3 + 1u}); +// xt::view(xgrad, xt::range(0u, 6u), xt::range(0u, nseg * 3u)) = grad_u; +// xt::view(xgrad, xt::range(0u, 6u), xt::range(nseg * 3, nseg * 3 + 1)) = grad_tof; +// xt::view(xgrad, xt::range(6u, 7u), xt::all()) = xt::view(dm[nseg], xt::all(), xt::range(0u, nseg * 3 + 1)); +// // At this point since the variable order is u,m,tof we have put dmf/dms in rather than dms/dtof. So we fix this. +// xgrad(6u, nseg * 3) = dm[nseg](0, nseg * 3 + 1); +// // b) xgrad_rvm (the xtensor gradient w.r.t. the initial conditions) +// xt::view(xgrad_rvm, xt::range(0, 6), xt::range(0, 6)) = grad_xs; +// xt::view(xgrad_rvm, xt::range(0, 6), xt::range(6, 7)) = grad_ms; +// xgrad_rvm(6, 6) = dm[nseg](0, nseg * 3); +// return std::make_pair(grad_rvm, std::move(grad)); +// } + +// std::pair, std::vector> +// sims_flanagan_hf::gradients_fwd(std::vector::const_iterator th1, std::vector::const_iterator th2, +// // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +// const std::array, 2> &rvs, double ms, double c, double a, +// double dt) const +// { +// return gradients_multiple_impulses(th1, th2, rvs, ms, c, a, dt); +// } + +// std::pair, std::vector> +// sims_flanagan_hf::gradients_bck(std::vector::const_iterator th1, std::vector::const_iterator th2, +// // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +// const std::array, 2> &rvf_orig, double mf, double c, double a, +// double dt) const +// { +// // 1) we invert the starting velocity. +// auto rvf = rvf_orig; +// rvf[1][0] = -rvf[1][0]; +// rvf[1][1] = -rvf[1][1]; +// rvf[1][2] = -rvf[1][2]; + +// // 2) we reverse the throttles ([1,2,3,4,5,6] -> [4,5,6,1,2,3]) +// auto size = static_cast(std::distance(th1, th2)); +// // Create a new vector to store the reversed values three by three. +// // Here we allocate a vector. Might be not necessary using the C++ range library? +// std::vector reversed_throttles(size); +// // Iterate in reverse order with a step of three +// for (decltype(size) i = 0u, j = size - 1; i < size; i += 3, j -= 3) { +// // Copy three elements at a time in reverse order +// reversed_throttles[j - 2] = *(th1 + i); +// reversed_throttles[j - 1] = *(th1 + i + 1); +// reversed_throttles[j] = *(th1 + i + 2); +// } + +// // 3) We reverse the Isp, hence veff (a = 1/veff) +// a = -a; + +// // 4) We then compute gradients as if this was a forward leg +// auto [grad_rvm, grad] +// = gradients_multiple_impulses(reversed_throttles.begin(), reversed_throttles.end(), rvf, mf, c, a, dt); +// // 5) We have computed dxf/dxs and dxf/dus, but the initial and final velocites (and us) had their sign +// // inverted! We thus need to account for that and change sign once again of the relevant entries. +// // We also must account for changes in the mass equation (now -a) +// auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); +// xt::view(xgrad_rvm, xt::range(3, 6), xt::all()) *= -1; // dvf/dall +// xt::view(xgrad_rvm, xt::all(), xt::range(0, 3)) *= -1; // dmc/drs +// xt::view(xgrad_rvm, xt::all(), xt::range(6, 7)) *= -1; // dmc/dmf + +// auto xgrad = xt::adapt(grad, {7u, size + 1u}); +// xt::view(xgrad, xt::range(3, 6), xt::all()) *= -1; // dvf/dall +// xt::view(xgrad, xt::all(), xt::range(0, size)) *= -1; // dmc/dus + +// // 6) Note that the throttles in xgrad are ordered in reverse. Before returning we must restore the forward order +// xt::view(xgrad, xt::all(), xt::range(0, size)) = xt::flip(xt::view(xgrad, xt::all(), xt::range(0, size)), 1); +// for (decltype(size) i = 0u; i < size / 3; ++i) { +// xt::view(xgrad, xt::all(), xt::range(3 * i, 3 * i + 3)) +// = xt::flip(xt::view(xgrad, xt::all(), xt::range(3 * i, 3 * i + 3)), 1); +// } +// // And finally return. +// return std::make_pair(grad_rvm, std::move(grad)); +// } + +// // Computes the gradient of the mismatch constraints w.r.t. xs, xf and [throttles, tof] +// std::tuple, std::array, std::vector> sims_flanagan::compute_mc_grad() +// const +// { +// // Preliminaries +// const auto dt = m_tof / static_cast(m_nseg); // dt +// const auto c = m_max_thrust * dt; // T*tof/nseg +// const auto a = 1. / m_isp / kep3::G0; // 1/veff + +// // We compute for the forward half-leg: dxf/dxs and dxf/dxu (the gradients w.r.t. initial state ant throttles ) +// auto [grad_rvm, grad_fwd] +// = gradients_fwd(m_throttles.begin(), m_throttles.begin() + static_cast(3 * m_nseg_fwd), get_rvs(), +// get_ms(), c, a, dt); +// // We compute for the backward half-leg: dxf/dxs and dxf/dxu (the gradients w.r.t. final state and throttles ) +// auto [grad_rvm_bck, grad_bck] = gradients_bck(m_throttles.begin() + static_cast(3 * m_nseg_fwd), +// m_throttles.end(), get_rvf(), get_mf(), c, a, dt); + +// // We assemble the final results +// std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); +// auto xgrad_final = xt::adapt(grad_final, {7u, static_cast(m_nseg) * 3u + 1u}); +// auto xgrad_fwd = xt::adapt(grad_fwd, {7u, static_cast(m_nseg_fwd) * 3u + 1u}); +// auto xgrad_bck = xt::adapt(grad_bck, {7u, static_cast(m_nseg - m_nseg_fwd) * 3u + 1u}); + +// // Copy the gradient w.r.t. the forward throttles as is +// xt::view(xgrad_final, xt::all(), xt::range(0, m_nseg_fwd * 3)) +// = xt::view(xgrad_fwd, xt::all(), xt::range(0, m_nseg_fwd * 3)); + +// // Copy the gradient w.r.t. the backward throttles as is +// xt::view(xgrad_final, xt::all(), xt::range(m_nseg_fwd * 3, m_nseg * 3)) +// = xt::view(xgrad_bck, xt::all(), xt::range(0, (m_nseg - m_nseg_fwd) * 3)); + +// // Copy the gradient w.r.t. tof as fwd-bck +// xt::view(xgrad_final, xt::all(), xt::range(m_nseg * 3, m_nseg * 3 + 1)) +// = xt::view(xgrad_fwd, xt::all(), xt::range(m_nseg_fwd * 3, m_nseg_fwd * 3 + 1)) / m_nseg * m_nseg_fwd +// - xt::view(xgrad_bck, xt::all(), xt::range((m_nseg - m_nseg_fwd) * 3, (m_nseg - m_nseg_fwd) * 3 + 1)) / +// m_nseg +// * (m_nseg - m_nseg_fwd); +// return {grad_rvm, grad_rvm_bck, std::move(grad_final)}; +// } + +// std::vector sims_flanagan_hf::compute_tc_grad() const +// { +// std::vector retval(static_cast(m_nseg) * m_nseg * 3u, 0); +// for (decltype(m_throttles.size()) i = 0u; i < m_nseg; ++i) { +// retval[i * m_nseg * 3 + 3 * i] = 2 * m_throttles[3 * i]; +// retval[i * m_nseg * 3 + 3 * i + 1] = 2 * m_throttles[3 * i + 1]; +// retval[i * m_nseg * 3 + 3 * i + 2] = 2 * m_throttles[3 * i + 2]; +// } +// return retval; +// } + std::ostream &operator<<(std::ostream &s, const sims_flanagan_hf &sf) { s << fmt::format("Number of segments: {}\n", sf.get_nseg()); @@ -611,7 +653,7 @@ std::ostream &operator<<(std::ostream &s, const sims_flanagan_hf &sf) s << fmt::format("State at departure: {}\n", sf.get_rvs()); s << fmt::format("State at arrival: {}\n", sf.get_rvf()); s << fmt::format("Throttles values: {}\n\n", sf.get_throttles()); - s << fmt::format("Mismatch constraints: {}\n", sf.compute_mismatch_constraints()); + // s << fmt::format("Mismatch constraints: {}\n", sf.compute_mismatch_constraints()); s << fmt::format("Throttle constraints: {}\n\n", sf.compute_throttle_constraints()); return s; } diff --git a/src/ta/stark.cpp b/src/ta/stark.cpp index c337baa8..cc8aa51f 100644 --- a/src/ta/stark.cpp +++ b/src/ta/stark.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9f610060..4e44dc1c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -41,4 +41,5 @@ ADD_kep3_TESTCASE(propagate_keplerian_test) ADD_kep3_TESTCASE(lambert_problem_test) ADD_kep3_TESTCASE(stark_problem_test) ADD_kep3_TESTCASE(leg_sims_flanagan_test) +ADD_kep3_TESTCASE(leg_sims_flanagan_hf_test) ADD_kep3_TESTCASE(ta_stark_test) \ No newline at end of file diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp new file mode 100644 index 00000000..cac519e9 --- /dev/null +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -0,0 +1,338 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +// #include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "catch.hpp" +// #include "leg_sims_flanagan_udp.hpp" +// #include "test_helpers.hpp" + +TEST_CASE("constructor") +{ + { + // The default constructor constructs a valid leg with no mismatches. + kep3::leg::sims_flanagan_hf sf{}; + auto mc = sf.compute_mismatch_constraints(); + REQUIRE(*std::max_element(mc.begin(), mc.end()) < 1e-13); + auto tc = sf.compute_throttle_constraints(); + REQUIRE(*std::max_element(tc.begin(), tc.end()) < 0.); + } + { + // The constructor fails when data are malformed + std::array, 2> rvs{{{1, 0, 0}, {0, 1, 0}}}; + std::array, 2> rvf{{{0, 1, 0}, {-1, 0, 0}}}; + double ms = 1.; + double mf = 1.; + REQUIRE_NOTHROW( + kep3::leg::sims_flanagan_hf(rvs, ms, {0., 0., 0., 0., 0., 0.}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5)); + REQUIRE_THROWS_AS( + kep3::leg::sims_flanagan_hf(rvs, ms, {0., 0., 0., 0., 0.}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), + std::logic_error); + // REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, -0.42, 1., 1., 1., 0.5), + // std::domain_error); // SC: negative ToF valid? + REQUIRE_THROWS_AS( + kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, -0.3, 1., 1., 0.5), + std::domain_error); + REQUIRE_THROWS_AS( + kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, 1., -2., 1., 0.5), + std::domain_error); + REQUIRE_THROWS_AS( + kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, 1., 1., -0.32, 0.5), + std::domain_error); + REQUIRE_THROWS_AS( + kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, 1., 1., 1., 32), + std::domain_error); + REQUIRE_THROWS_AS( + kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, 1., 1., 1., -0.1), + std::domain_error); + REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), + std::logic_error); + } +} + +TEST_CASE("getters_and_setters") +{ + { + kep3::leg::sims_flanagan_hf sf{}; + std::array, 2> rvf{{{1, 1, 1}, {1, 1, 1}}}; + double mass = 123.; + sf.set_rvf(rvf); + REQUIRE(sf.get_rvf() == rvf); + sf.set_ms(mass); + REQUIRE(sf.get_ms() == mass); + sf.set_rvs(rvf); + REQUIRE(sf.get_rvs() == rvf); + sf.set_mf(mass); + REQUIRE(sf.get_mf() == mass); + std::vector throttles{1., 2., 3., 1., 2., 3.}; + std::vector throttles2{1.1, 2.1, 3.1, 1.1, 2.1, 3.1}; + sf.set_throttles(throttles); + REQUIRE(sf.get_throttles() == throttles); + sf.set_throttles(throttles2.begin(), throttles2.end()); + REQUIRE(sf.get_throttles() == throttles2); + REQUIRE_THROWS_AS(sf.set_throttles(throttles2.begin(), throttles2.end() - 1), std::logic_error); + sf.set_cut(0.333); + REQUIRE(sf.get_cut() == 0.333); + sf.set_max_thrust(0.333); + REQUIRE(sf.get_max_thrust() == 0.333); + sf.set_isp(0.333); + REQUIRE(sf.get_isp() == 0.333); + sf.set_mu(0.333); + REQUIRE(sf.get_mu() == 0.333); + sf.set_tof(0.333); + REQUIRE(sf.get_tof() == 0.333); + } + { + kep3::leg::sims_flanagan_hf sf{}; + std::array, 2> rvf{{{1, 1, 1}, {1, 1, 1}}}; + std::vector throttles{1., 2., 3., 1., 2., 3.}; + + sf.set(rvf, 12, throttles, rvf, 12, 4, 4, 4, 4, 0.333); + REQUIRE(sf.get_rvs() == rvf); + REQUIRE(sf.get_ms() == 12); + REQUIRE(sf.get_rvf() == rvf); + REQUIRE(sf.get_mf() == 12); + REQUIRE(sf.get_throttles() == throttles); + REQUIRE(sf.get_cut() == 0.333); + REQUIRE(sf.get_max_thrust() == 4); + REQUIRE(sf.get_isp() == 4); + REQUIRE(sf.get_mu() == 4); + REQUIRE(sf.get_tof() == 4); + } +} + +TEST_CASE("compute_throttle_constraints_test") +{ + std::array, 2> rvs{{{1, 0, 0}, {0, 1, 0}}}; + std::array, 2> rvf{{{0, 1, 0}, {-1, 0, 0}}}; + kep3::leg::sims_flanagan_hf sf(rvs, 1., {0, 1, 0, 1, 1, 1, 0, 1, 1}, rvf, 1, 1, 1, 1, 1, 1); + auto tc = sf.compute_throttle_constraints(); + REQUIRE(tc[0] == 0.); + REQUIRE(tc[1] == 2.); + REQUIRE(tc[2] == 1.); +} + +std::array normalize_con(std::array con) +{ + con[0] /= kep3::AU; + con[1] /= kep3::AU; + con[2] /= kep3::AU; + con[3] /= kep3::EARTH_VELOCITY; + con[4] /= kep3::EARTH_VELOCITY; + con[5] /= kep3::EARTH_VELOCITY; + con[6] /= 1000; + return con; +} + +TEST_CASE("compute_mismatch_constraints_test") +{ + // We test that an engineered ballistic arc always returns no mismatch for all cuts. + // We use (for no reason) the ephs of the Earth and Jupiter + kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); + kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); + kep3::planet earth{udpla_earth}; + kep3::planet jupiter{udpla_jupiter}; + // And some epochs / tofs. + double dt_days = 1000.; + double dt = dt_days * kep3::DAY2SEC; + double t0 = 1233.3; + // double mass = 1000; + auto rv0 = earth.eph(t0); + auto rv1 = jupiter.eph(t0 + dt_days); + // We create a ballistic arc matching the two. + kep3::lambert_problem lp{rv0[0], rv1[0], dt, kep3::MU_SUN}; + rv0[1][0] = lp.get_v0()[0][0]; + rv0[1][1] = lp.get_v0()[0][1]; + rv0[1][2] = lp.get_v0()[0][2]; + rv1[1][0] = lp.get_v1()[0][0]; + rv1[1][1] = lp.get_v1()[0][1]; + rv1[1][2] = lp.get_v1()[0][2]; + // We test for 1 to 33 segments and cuts in [0,0.1,0.2, ..., 1] + std::vector cut_values{0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1}; + + for (unsigned long N = 1u; N < 34; ++N) { + for (auto cut : cut_values) { + std::vector throttles(N * 3, 0.); + kep3::leg::sims_flanagan_hf sf(rv0, 1., throttles, rv1, 1., dt, 1., 1., kep3::MU_SUN, cut); + auto mc = sf.compute_mismatch_constraints(); + mc = normalize_con(mc); + REQUIRE(*std::max_element(mc.begin(), mc.end()) < 1e-8); + } + } + + // { + // // Here we reuse the ballitic arc as a ground truth for an optimization. + // // We check that, when feasible, the optimal mass solution is indeed ballistic. + // pagmo::problem prob{sf_test_udp{rv0, mass, rv1, 0.05, 2000, 10u}}; + // prob.set_c_tol(1e-8); + // bool found = false; + // unsigned trial = 0u; + // pagmo::nlopt uda{"slsqp"}; + // uda.set_xtol_abs(1e-10); + // uda.set_xtol_rel(1e-10); + // uda.set_ftol_abs(0); + // uda.set_maxeval(1000); + // pagmo::algorithm algo{uda}; + // while ((!found) && (trial < 20u)) { + // pagmo::population pop{prob, 1u}; + // algo.set_verbosity(10u); + // pop = algo.evolve(pop); + // auto champ = pop.champion_f(); + // found = prob.feasibility_f(champ); + // if (found) { + // fmt::print("{}\n", champ); + // found = *std::min_element(champ.begin() + 7, champ.end()) < -0.99999; + // } + // trial++; + // } + // REQUIRE_FALSE(!found); // If this does not pass, then the optimization above never found a ballistic arc ... + // // theres a problem somewhere. + // } + // { + // // Here we create an ALMOST ballistic arc as a ground truth for an optimization. + // // We check that, when feasible, the optimal mass solution is indeed ballistic. + // auto rv1_modified = rv1; + // rv1_modified[1][0] += 1000; // Adding 1km/s along x + // pagmo::problem prob{sf_test_udp{rv0, mass, rv1_modified, 0.05, 2000, 10u}}; + // prob.set_c_tol(1e-8); + // bool found = false; + // unsigned trial = 0u; + // pagmo::nlopt uda{"slsqp"}; + // uda.set_xtol_abs(1e-10); + // uda.set_xtol_rel(1e-10); + // uda.set_ftol_abs(0); + // uda.set_maxeval(1000); + // pagmo::algorithm algo{uda}; + // while ((!found) && (trial < 20u)) { + // pagmo::population pop{prob, 1u}; + // algo.set_verbosity(10u); + // pop = algo.evolve(pop); + // auto champ = pop.champion_f(); + // found = prob.feasibility_f(champ); + // if (found) { + // fmt::print("{}\n", champ); + // } + // trial++; + // } + // // If this does not pass, then the optimization above never converged to a feasible solution. + // REQUIRE_FALSE(!found); + // } +} + +// SC: Not supposed to work because different dynamics are at play +TEST_CASE("mismatch_constraints_test2") +{ + // // We test the correctness of the compute_mismatch_constraints computations against a ground truth (computed with + // a different program) + std::array, 2> rvs{ + {{1 * kep3::AU, 0.1 * kep3::AU, -0.1 * kep3::AU}, + {0.2 * kep3::EARTH_VELOCITY, 1 * kep3::EARTH_VELOCITY, -0.2 * kep3::EARTH_VELOCITY}}}; + + std::array, 2> rvf{ + {{1.2 * kep3::AU, -0.1 * kep3::AU, 0.1 * kep3::AU}, + {-0.2 * kep3::EARTH_VELOCITY, 1.023 * kep3::EARTH_VELOCITY, -0.44 * kep3::EARTH_VELOCITY}}}; + + double ms = 1500.; + double mf = 1300.; + std::vector throttles + = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24}; + std::vector inverted_throttles = throttles; + // Invert direction of backwards leg thrust + auto invert_direction = [](double throttle) { return throttle * -1; }; + std::transform(throttles.begin(), throttles.end(), inverted_throttles.begin(), invert_direction); + kep3::leg::sims_flanagan_hf sf(rvs, ms, throttles, rvf, mf, 324.0 * kep3::DAY2SEC, 0.12, 100, kep3::MU_SUN, 0.6, + 1e-13); + auto retval = sf.compute_mismatch_constraints(); + + // Change initial and final state to have inverted velocities + // std::transform(rvs[1].begin(), rvs[1].end(), rvs[1].begin(), invert_direction); + // std::transform(rvf[1].begin(), rvf[1].end(), rvf[1].begin(), invert_direction); + kep3::leg::sims_flanagan_hf sf2(rvf, mf, throttles, rvs, ms, -324.0 * kep3::DAY2SEC, 0.12, 100, kep3::MU_SUN, 0.4, + 1e-13); + auto retval2 = sf2.compute_mismatch_constraints(); + + REQUIRE(std::abs((retval2[0] - retval[0]) / retval2[0]) < 1e-13); + REQUIRE(std::abs((retval2[1] - retval[1]) / retval2[1]) < 1e-13); + REQUIRE(std::abs((retval2[2] - retval[2]) / retval2[2]) < 1e-13); + REQUIRE(std::abs((retval2[3] - retval[3]) / retval2[3]) < 1e-13); + REQUIRE(std::abs((retval2[4] - retval[4]) / retval2[4]) < 1e-13); + REQUIRE(std::abs((retval2[5] - retval[5]) / retval2[5]) < 1e-13); +} + +// TEST_CASE("grad_test") +// { +// // Here we test the analytical gradient against an equivalent numerical one. We do so through the udp +// "sf_test_udp" std::array, 2> rvs{ +// {{1 * kep3::AU, 0.1 * kep3::AU, -0.1 * kep3::AU}, +// {0.2 * kep3::EARTH_VELOCITY, 1 * kep3::EARTH_VELOCITY, -0.2 * kep3::EARTH_VELOCITY}}}; +// // +// std::array, 2> rvf{ +// {{1.2 * kep3::AU, -0.1 * kep3::AU, 0.1 * kep3::AU}, +// {-0.2 * kep3::EARTH_VELOCITY, 1.023 * kep3::EARTH_VELOCITY, -0.44 * kep3::EARTH_VELOCITY}}}; +// // +// double ms = 1500.; + +// sf_test_udp udp{rvs, ms, rvf, 0.12, 100, 5}; +// std::vector x +// = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 324., 1300.}; +// auto grad = udp.gradient_numerical(x); +// auto grad_a = udp.gradient(x); +// auto xgrad = xt::adapt(grad, {1u + 7u + 5u, 17u}); +// auto xgrad_a = xt::adapt(grad_a, {1u + 7u + 5u, 17u}); +// REQUIRE(xt::linalg::norm(xgrad - xgrad_a) < 1e-8); // With the high fidelity gradient this is still the best we +// can achieve +// } + +// TEST_CASE("serialization_test") +// { +// // Instantiate a generic lambert problem +// std::array, 2> rvs{{{-1, -1, -1}, {-1, -1, -1}}}; +// std::array, 2> rvf{{{0.1, 1.1, 0.1}, {-1.1, 0.1, 0.1}}}; +// kep3::leg::sims_flanagan_hf sf1{rvs, 12., {1, 2, 3, 4, 5, 6}, rvf, 10, 2.3, 2.3, 2.3, 1.1, 0.2}; + +// // Store the string representation. +// std::stringstream ss; +// auto before = boost::lexical_cast(sf1); +// // Now serialize +// { +// boost::archive::binary_oarchive oarchive(ss); +// oarchive << sf1; +// } +// // Deserialize +// // Create a new lambert problem object +// kep3::leg::sims_flanagan_hf sf2{}; +// { +// boost::archive::binary_iarchive iarchive(ss); +// iarchive >> sf2; +// } +// auto after = boost::lexical_cast(sf2); +// // Compare the string represetation +// REQUIRE(before == after); +// } From bda08ce803ae671cb2730bfb248fb709a60bc0d0 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Tue, 1 Oct 2024 11:53:37 +0200 Subject: [PATCH 02/22] Implement mismatch and throttle constrain functions as well as mismatch and throttle constraint gradients with accompanying tests. Minor fixes. --- include/kep3/leg/sims_flanagan_hf.hpp | 40 ++- src/leg/sf_checks.cpp | 2 +- src/leg/sims_flanagan_hf.cpp | 488 ++++++++++++-------------- src/ta/stark.cpp | 2 +- test/leg_sims_flanagan_hf_helpers.hpp | 290 +++++++++++++++ test/leg_sims_flanagan_hf_test.cpp | 304 ++++++++-------- 6 files changed, 695 insertions(+), 431 deletions(-) create mode 100644 test/leg_sims_flanagan_hf_helpers.hpp diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 1b471167..3a72ea38 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -11,10 +11,9 @@ #define kep3_LEG_SIMS_FLANAGAN_HF_H #include -// #include -#include - +#include #include +#include #include @@ -41,9 +40,9 @@ namespace kep3::leg class kep3_DLL_PUBLIC sims_flanagan_hf { public: + // Constructors // Default Constructor. sims_flanagan_hf(); // = default; - // Constructors // Backwards-compatible constructor with rv and m states separately sims_flanagan_hf(const std::array, 2> &rvs, double ms, std::vector throttles, const std::array, 2> &rvf, double mf, double tof, double max_thrust, @@ -75,9 +74,10 @@ class kep3_DLL_PUBLIC sims_flanagan_hf const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, double mu, double cut = 0.5, double tol = 1e-16); // Setting function with rvm states - void set(const std::array &rvms, const std::vector &throttles, const std::array &rvmf, - double tof, double max_thrust, double isp, - double mu, double cut = 0.5, double tol = 1e-16); + void set(const std::array &rvms, const std::vector &throttles, const std::array &rvmf, + double tof, double max_thrust, double isp, double mu, double cut = 0.5, double tol = 1e-16); + void set(const std::array &rvms, const std::vector &throttles, const std::array &rvmf, + double time_of_flight); // Getters [[nodiscard]] double get_tof() const; @@ -95,20 +95,27 @@ class kep3_DLL_PUBLIC sims_flanagan_hf [[nodiscard]] unsigned get_nseg_fwd() const; [[nodiscard]] unsigned get_nseg_bck() const; [[nodiscard]] heyoka::taylor_adaptive get_tas() const; + [[nodiscard]] heyoka::taylor_adaptive get_tas_var() const; [[nodiscard]] std::array get_rvms() const; [[nodiscard]] std::array get_rvmf() const; [[nodiscard]] std::array get_walking_rvm() const; // Compute constraints + [[nodiscard]] std::array get_state_derivative(std::array state, + std::array throttles); [[nodiscard]] std::array compute_mismatch_constraints(); [[nodiscard]] std::vector compute_throttle_constraints() const; + [[nodiscard]] std::vector compute_constraints(); + + [[nodiscard]] std::vector set_and_compute_constraints(std::vector chromosome); - // // Compute mismatch constraint gradients (w.r.t. rvm state and w.r.t. throttles, tof) - // [[nodiscard]] std::tuple, std::array, std::vector> - // compute_mc_grad() const; + // Compute mismatch constraint gradients (w.r.t. rvm state and w.r.t. throttles) + [[nodiscard]] std::tuple, 5u>, std::array, 5u>, + std::array, 5u>> + compute_mc_grad(); - // // Compute throttle constraint gradients - // [[nodiscard]] std::vector compute_tc_grad() const; + // Compute throttle constraint gradients + [[nodiscard]] std::vector compute_tc_grad() const; private: friend class boost::serialization::access; @@ -116,6 +123,7 @@ class kep3_DLL_PUBLIC sims_flanagan_hf void serialize(Archive &ar, const unsigned int) { ar & m_rvms; + ar & m_vars; ar & m_throttles; ar & m_thrusts; ar & m_tof; @@ -129,11 +137,12 @@ class kep3_DLL_PUBLIC sims_flanagan_hf ar & m_nseg_fwd; ar & m_nseg_bck; ar & m_tas; - // ar & m_walking_rvm; } // Initial rvm state std::array m_rvms{1., 0., 0., 0., 1., 0., 1.}; + // Initial variational state + std::array m_vars{}; // Sequence of throttles. std::vector m_throttles{0., 0., 0., 0., 0., 0.}; // Sequence of thrusts. @@ -159,10 +168,11 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // We introduce ta from cache const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); heyoka::taylor_adaptive m_tas = ta_cache; - + // Introduce variational ta from cache + const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); + heyoka::taylor_adaptive m_tas_var = ta_var_cache; }; - // Streaming operator for the class kep3::leg::sims_flanagan. kep3_DLL_PUBLIC std::ostream &operator<<(std::ostream &, const sims_flanagan_hf &); diff --git a/src/leg/sf_checks.cpp b/src/leg/sf_checks.cpp index ddaa20ee..e407c1f1 100644 --- a/src/leg/sf_checks.cpp +++ b/src/leg/sf_checks.cpp @@ -15,7 +15,7 @@ void _check_tof(double tof) // SC: One should be able to give this as a negative number to run the system backwards, no? if (tof < 0.) { ; - // throw std::domain_error("The time of flight of a sims_flanagan leg needs to be larger or equal to zero."); + throw std::domain_error("The time of flight of a sims_flanagan leg needs to be larger or equal to zero."); } } void _check_throttles(const std::vector &throttles, unsigned nseg) diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 0ef03559..9911cfbe 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,8 @@ #include #include +#include +#include #include #include #include @@ -37,7 +40,6 @@ namespace kep3::leg { -// using kep3::linalg::_dot; using kep3::linalg::mat13; using kep3::linalg::mat61; using kep3::linalg::mat63; @@ -53,11 +55,18 @@ sims_flanagan_hf::sims_flanagan_hf() *m_tas.get_pars_data() = m_mu; *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; - // Convert throttles to current_thrusts. (SC: Change so that we don't need extra memory) + // ... and variational version of the integrator + *(m_tas_var.get_pars_data()) = m_mu; + *(m_tas_var.get_pars_data() + 1) = m_isp * kep3::G0; + // We copy the initial conditions for the variational equations + std::copy(m_tas_var.get_state().begin() + 7, m_tas_var.get_state().end(), m_vars.begin()); + + // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } + sims_flanagan_hf::sims_flanagan_hf(const std::array, 2> &rvs, double ms, std::vector throttles, // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) @@ -73,7 +82,13 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array, 2> &r *m_tas.get_pars_data() = m_mu; *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; - // Convert throttles to current_thrusts. (SC: Change so that we don't need extra memory) + // ... and variational version of the integrator + *(m_tas_var.get_pars_data()) = m_mu; + *(m_tas_var.get_pars_data() + 1) = m_isp * kep3::G0; + // We copy the initial conditions for the variational equations + std::copy(m_tas_var.get_state().begin() + 7, m_tas_var.get_state().end(), m_vars.begin()); + + // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); @@ -100,7 +115,13 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array &rvms, std::vecto *m_tas.get_pars_data() = m_mu; *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; - // Convert throttles to current_thrusts. (SC: Change so that we don't need extra memory) + // ... and variational version of the integrator + *(m_tas_var.get_pars_data()) = m_mu; + *(m_tas_var.get_pars_data() + 1) = m_isp * kep3::G0; + // We copy the initial conditions for the variational equations + std::copy(m_tas_var.get_state().begin() + 7, m_tas_var.get_state().end(), m_vars.begin()); + + // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); @@ -209,6 +230,11 @@ void sims_flanagan_hf::set(const std::array, 2> &rvs, doub m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; + + // Convert throttles to current_thrusts. + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } void sims_flanagan_hf::set(const std::array &rvms, const std::vector &throttles, @@ -228,6 +254,29 @@ void sims_flanagan_hf::set(const std::array &rvms, const std::vector< m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; + + // Convert throttles to current_thrusts. + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); +} + +void sims_flanagan_hf::set(const std::array &rvms, const std::vector &throttles, + const std::array &rvmf, double time_of_flight) +{ + set_rvms(rvms); + m_throttles = throttles; + set_rvmf(rvmf); + m_tof = time_of_flight; + m_nseg = static_cast(m_throttles.size()) / 3u; + m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); + m_nseg_bck = m_nseg - m_nseg_fwd; + _sanity_checks(throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + + // Convert throttles to current_thrusts. + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } // Getters @@ -237,7 +286,6 @@ double sims_flanagan_hf::get_tof() const } const std::array, 2> sims_flanagan_hf::get_rvs() const { - // SC: This could be done with xtensor? It isn't very efficient std::array, 2> rvs{}; std::copy(m_rvms.begin(), std::next(m_rvms.begin(), 3), rvs[0].begin()); std::copy(std::next(m_rvms.begin(), 3), std::next(m_rvms.begin(), 6), rvs[1].begin()); @@ -253,7 +301,6 @@ const std::vector &sims_flanagan_hf::get_throttles() const } const std::array, 2> sims_flanagan_hf::get_rvf() const { - // SC: This could be done with xtensor? It isn't very efficient std::array, 2> rvf{{{0., 0., 0.}, {0., 0., 0.}}}; std::copy(m_rvmf.begin(), std::next(m_rvmf.begin(), 3), rvf[0].begin()); std::copy(std::next(m_rvmf.begin(), 3), std::next(m_rvmf.begin(), 6), rvf[1].begin()); @@ -299,6 +346,10 @@ heyoka::taylor_adaptive sims_flanagan_hf::get_tas() const { return m_tas; } +heyoka::taylor_adaptive sims_flanagan_hf::get_tas_var() const +{ + return m_tas_var; +} std::array sims_flanagan_hf::get_rvms() const { return m_rvms; @@ -312,7 +363,7 @@ std::array sims_flanagan_hf::get_rvmf() const std::array sims_flanagan_hf::compute_mismatch_constraints() { // General settings - const double prop_seg_duration = (m_tof / m_nseg); // * static_cast(i + 1); + const double prop_seg_duration = (m_tof / m_nseg); // Forward pass // Initial state @@ -331,7 +382,7 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); } // ... and integrate - auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until((i + 1) * prop_seg_duration); // Added - for bck propagation + auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until((i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } @@ -351,14 +402,14 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() // Assign current_thrusts to Taylor adaptive integrator if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { // Copy thrust into Taylor-adaptive integrator - std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg -(i + 1)) * 3)), + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), std::next(m_tas.get_pars_data(), 2)); } else { throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); } // ... and integrate - auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until(m_tof - (i + 1) * prop_seg_duration); // Added - for bck propagation + auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until(m_tof - (i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } @@ -380,264 +431,165 @@ std::vector sims_flanagan_hf::compute_throttle_constraints() const return retval; } -// mat61 _dyn(std::array, 2> rv, double mu) -// { -// mat61 retval; -// auto R3 = std::pow(rv[0][0] * rv[0][0] + rv[0][1] * rv[0][1] + rv[0][2] * rv[0][2], 1.5); -// retval(0, 0) = rv[1][0]; -// retval(1, 0) = rv[1][1]; -// retval(2, 0) = rv[1][2]; -// retval(3, 0) = -mu / R3 * rv[0][0]; -// retval(4, 0) = -mu / R3 * rv[0][1]; -// retval(5, 0) = -mu / R3 * rv[0][2]; -// return retval; -// } - -// // Performs the state updates for nseg sarting from rvs, ms. Computes all gradient information -// std::pair, std::vector> sims_flanagan_hf::gradients_multiple_impulses( -// std::vector::const_iterator th1, std::vector::const_iterator th2, -// // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -// const std::array, 2> &rvs, double ms, double c, double a, double dt) const -// { -// assert(std::distance(th1, th2) % 3 == 0u); -// auto nseg = static_cast(std::distance(th1, th2) / 3u); - -// // Corner case: nseg is zero -// if (nseg == 0) { -// std::array grad_rvm{}; // The mismatch constraints gradient w.r.t. extended state r,v,m -// auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); -// xgrad_rvm = xt::eye(7); -// std::vector grad(7, 0.); // The mismatch constraints gradient w.r.t. throttles (0 in this case) and -// tof return std::make_pair(grad_rvm, std::move(grad)); -// } -// // Allocate memory. -// std::vector u(nseg); -// std::vector> du(nseg, xt::zeros({3u, nseg * 3u + 2u})); -// std::vector m(nseg + 1, 0.); -// std::vector> dm(nseg + 1u, xt::zeros({1u, nseg * 3u + 2u})); -// xt::xarray dtof = xt::zeros({1u, nseg * 3u + 2u}); -// std::vector Dv(nseg); -// std::vector> dDv(nseg, xt::zeros({3u, nseg * 3u + 2u})); -// std::vector M(nseg + 1); // The STMs -// std::vector Mc(nseg + 1); // Mc will contain [Mn@..@M0,Mn@..@M1, Mn] -// std::vector f(nseg + 1, xt::zeros({6u, 1u})); -// // Initialize values -// m[0] = ms; -// unsigned i_tmp = 0u; -// for (auto it = th1; it != th2; it += 3) { -// u[i_tmp](0, 0) = *it; -// u[i_tmp](0, 1) = *(it + 1); -// u[i_tmp](0, 2) = *(it + 2); -// du[i_tmp](0, 3 * i_tmp) = 1.; -// du[i_tmp](1, 3 * i_tmp + 1) = 1.; -// du[i_tmp](2, 3 * i_tmp + 2) = 1.; -// i_tmp++; -// } -// dm[0](0, nseg * 3u) = 1.; -// dtof(0, nseg * 3u + 1) = 1.; -// // 1 - We compute the mass schedule and related gradients -// for (decltype(nseg) i = 0; i < nseg; ++i) { -// Dv[i] = c / m[i] * u[i]; -// double un = std::sqrt(u[i](0, 0) * u[i](0, 0) + u[i](0, 1) * u[i](0, 1) + u[i](0, 2) * u[i](0, 2)); -// double Dvn = c / m[i] * un; -// dDv[i] = c / m[i] * du[i] - c / m[i] / m[i] * xt::linalg::dot(xt::transpose(u[i]), dm[i]) -// + m_max_thrust / m[i] * xt::linalg::dot(xt::transpose(u[i]), dtof) / nseg; -// auto dDvn = c / m[i] / un * xt::linalg::dot(u[i], du[i]) - c / m[i] / m[i] * un * dm[i] -// + m_max_thrust / m[i] * un * dtof / nseg; -// m[i + 1] = m[i] * std::exp(-Dvn * a); -// dm[i + 1] = -m[i + 1] * a * dDvn + std::exp(-Dvn * a) * dm[i]; -// } -// // 2 - We compute the various STMs -// std::array, 2> rv_it(rvs); -// std::optional> M_it; -// for (decltype(nseg) i = 0; i < nseg + 1; ++i) { -// auto dur = dt; -// if (i == 0 || i == nseg) { -// dur = dt / 2; -// } - -// std::tie(rv_it, M_it) = kep3::propagate_lagrangian(rv_it, dur, m_mu, true); -// // Now we have the STM in M_it, but its a vector, we must operate on an xtensor object instead. -// assert(M_it); -// // NOLINTNEXTLINE(bugprone-unchecked-optional-access) -// M[i] = xt::adapt(*M_it, {6, 6}); -// f[i] = _dyn(rv_it, m_mu); -// // And add the impulse if needed -// if (i < nseg) { -// rv_it[1][0] += Dv[i](0, 0); -// rv_it[1][1] += Dv[i](0, 1); -// rv_it[1][2] += Dv[i](0, 2); -// } -// } - -// // 3 - We now need to apply the chain rule to assemble the gradients we want (i.e. not w.r.t DV but w.r.t. u -// etc...) mat63 Iv = xt::zeros({6u, 3u}); // This is the gradient of x (rv) w.r.t. v Iv(3, 0) = 1.; Iv(4, -// 1) = 1.; Iv(5, 2) = 1.; Mc[nseg] = M[nseg]; // Mc will contain [Mn@..@M0,Mn@..@M1, Mn] for (decltype(nseg) i = 1; -// i < nseg + 1; ++i) { -// Mc[nseg - i] = _dot(Mc[nseg - i + 1], M[nseg - i]); -// } -// // grad_tof./ -// // First the d/dtof term - example: (0.5 * f3 + M3 @ f2 + M3 @ M2 @ f1 + 0.5 * M3 @ M2 @ M1 @ f0) / N - -// mat61 grad_tof = 0.5 * f[nseg]; -// for (decltype(nseg) i = 0; i + 1 < nseg; ++i) { // i+1 < nseg avoids overflow -// grad_tof += _dot(Mc[i + 2], f[i + 1]); -// } - -// grad_tof += 0.5 * _dot(Mc[1], f[0]); -// grad_tof /= nseg; -// // Then we add the d/Dvi * dDvi/dtof - example: M3 @ Iv @ dDv2 + M3 @ M2 @ Iv @ dDv1 + M3 @ M2 @ M1 @ Iv @ dDv0 -// for (decltype(nseg) i = 0; i < nseg; ++i) { -// grad_tof += xt::linalg::dot(_dot(Mc[i + 1], Iv), -// xt::eval(xt::view(dDv[i], xt::all(), xt::range(nseg * 3 + 1, nseg * 3 + 2)))); -// } -// // grad_u -// xt::xarray grad_u = xt::zeros({6u, nseg * 3u}); -// for (decltype(nseg) i = 0u; i < nseg; ++i) { -// grad_u += xt::linalg::dot(_dot(Mc[i + 1], Iv), xt::eval(xt::view(dDv[i], xt::all(), xt::range(0, nseg * -// 3)))); -// } -// // grad_ms -// xt::xarray grad_ms = xt::zeros({6u, 1u}); -// for (decltype(nseg) i = 0u; i < nseg; ++i) { -// grad_ms += xt::linalg::dot(_dot(Mc[i + 1], Iv), -// xt::eval(xt::view(dDv[i], xt::all(), xt::range(nseg * 3, nseg * 3 + 1)))); -// } -// // grad_xs -// mat66 grad_xs = Mc[0]; - -// // Allocate the return values -// std::array grad_rvm{}; // The mismatch constraints gradient w.r.t. extended state r,v,m -// std::vector grad((nseg * 3lu + 1) * 7, -// 0.); // The mismatch constraints gradient w.r.t. throttles and tof -// // Copying in the computed derivatives -// // a) xgrad (the xtensor gradient w.r.t. throttles and tof) -// auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); -// auto xgrad = xt::adapt(grad, {7u, nseg * 3 + 1u}); -// xt::view(xgrad, xt::range(0u, 6u), xt::range(0u, nseg * 3u)) = grad_u; -// xt::view(xgrad, xt::range(0u, 6u), xt::range(nseg * 3, nseg * 3 + 1)) = grad_tof; -// xt::view(xgrad, xt::range(6u, 7u), xt::all()) = xt::view(dm[nseg], xt::all(), xt::range(0u, nseg * 3 + 1)); -// // At this point since the variable order is u,m,tof we have put dmf/dms in rather than dms/dtof. So we fix this. -// xgrad(6u, nseg * 3) = dm[nseg](0, nseg * 3 + 1); -// // b) xgrad_rvm (the xtensor gradient w.r.t. the initial conditions) -// xt::view(xgrad_rvm, xt::range(0, 6), xt::range(0, 6)) = grad_xs; -// xt::view(xgrad_rvm, xt::range(0, 6), xt::range(6, 7)) = grad_ms; -// xgrad_rvm(6, 6) = dm[nseg](0, nseg * 3); -// return std::make_pair(grad_rvm, std::move(grad)); -// } - -// std::pair, std::vector> -// sims_flanagan_hf::gradients_fwd(std::vector::const_iterator th1, std::vector::const_iterator th2, -// // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -// const std::array, 2> &rvs, double ms, double c, double a, -// double dt) const -// { -// return gradients_multiple_impulses(th1, th2, rvs, ms, c, a, dt); -// } - -// std::pair, std::vector> -// sims_flanagan_hf::gradients_bck(std::vector::const_iterator th1, std::vector::const_iterator th2, -// // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -// const std::array, 2> &rvf_orig, double mf, double c, double a, -// double dt) const -// { -// // 1) we invert the starting velocity. -// auto rvf = rvf_orig; -// rvf[1][0] = -rvf[1][0]; -// rvf[1][1] = -rvf[1][1]; -// rvf[1][2] = -rvf[1][2]; - -// // 2) we reverse the throttles ([1,2,3,4,5,6] -> [4,5,6,1,2,3]) -// auto size = static_cast(std::distance(th1, th2)); -// // Create a new vector to store the reversed values three by three. -// // Here we allocate a vector. Might be not necessary using the C++ range library? -// std::vector reversed_throttles(size); -// // Iterate in reverse order with a step of three -// for (decltype(size) i = 0u, j = size - 1; i < size; i += 3, j -= 3) { -// // Copy three elements at a time in reverse order -// reversed_throttles[j - 2] = *(th1 + i); -// reversed_throttles[j - 1] = *(th1 + i + 1); -// reversed_throttles[j] = *(th1 + i + 2); -// } - -// // 3) We reverse the Isp, hence veff (a = 1/veff) -// a = -a; - -// // 4) We then compute gradients as if this was a forward leg -// auto [grad_rvm, grad] -// = gradients_multiple_impulses(reversed_throttles.begin(), reversed_throttles.end(), rvf, mf, c, a, dt); -// // 5) We have computed dxf/dxs and dxf/dus, but the initial and final velocites (and us) had their sign -// // inverted! We thus need to account for that and change sign once again of the relevant entries. -// // We also must account for changes in the mass equation (now -a) -// auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); -// xt::view(xgrad_rvm, xt::range(3, 6), xt::all()) *= -1; // dvf/dall -// xt::view(xgrad_rvm, xt::all(), xt::range(0, 3)) *= -1; // dmc/drs -// xt::view(xgrad_rvm, xt::all(), xt::range(6, 7)) *= -1; // dmc/dmf - -// auto xgrad = xt::adapt(grad, {7u, size + 1u}); -// xt::view(xgrad, xt::range(3, 6), xt::all()) *= -1; // dvf/dall -// xt::view(xgrad, xt::all(), xt::range(0, size)) *= -1; // dmc/dus - -// // 6) Note that the throttles in xgrad are ordered in reverse. Before returning we must restore the forward order -// xt::view(xgrad, xt::all(), xt::range(0, size)) = xt::flip(xt::view(xgrad, xt::all(), xt::range(0, size)), 1); -// for (decltype(size) i = 0u; i < size / 3; ++i) { -// xt::view(xgrad, xt::all(), xt::range(3 * i, 3 * i + 3)) -// = xt::flip(xt::view(xgrad, xt::all(), xt::range(3 * i, 3 * i + 3)), 1); -// } -// // And finally return. -// return std::make_pair(grad_rvm, std::move(grad)); -// } - -// // Computes the gradient of the mismatch constraints w.r.t. xs, xf and [throttles, tof] -// std::tuple, std::array, std::vector> sims_flanagan::compute_mc_grad() -// const -// { -// // Preliminaries -// const auto dt = m_tof / static_cast(m_nseg); // dt -// const auto c = m_max_thrust * dt; // T*tof/nseg -// const auto a = 1. / m_isp / kep3::G0; // 1/veff - -// // We compute for the forward half-leg: dxf/dxs and dxf/dxu (the gradients w.r.t. initial state ant throttles ) -// auto [grad_rvm, grad_fwd] -// = gradients_fwd(m_throttles.begin(), m_throttles.begin() + static_cast(3 * m_nseg_fwd), get_rvs(), -// get_ms(), c, a, dt); -// // We compute for the backward half-leg: dxf/dxs and dxf/dxu (the gradients w.r.t. final state and throttles ) -// auto [grad_rvm_bck, grad_bck] = gradients_bck(m_throttles.begin() + static_cast(3 * m_nseg_fwd), -// m_throttles.end(), get_rvf(), get_mf(), c, a, dt); - -// // We assemble the final results -// std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); -// auto xgrad_final = xt::adapt(grad_final, {7u, static_cast(m_nseg) * 3u + 1u}); -// auto xgrad_fwd = xt::adapt(grad_fwd, {7u, static_cast(m_nseg_fwd) * 3u + 1u}); -// auto xgrad_bck = xt::adapt(grad_bck, {7u, static_cast(m_nseg - m_nseg_fwd) * 3u + 1u}); - -// // Copy the gradient w.r.t. the forward throttles as is -// xt::view(xgrad_final, xt::all(), xt::range(0, m_nseg_fwd * 3)) -// = xt::view(xgrad_fwd, xt::all(), xt::range(0, m_nseg_fwd * 3)); - -// // Copy the gradient w.r.t. the backward throttles as is -// xt::view(xgrad_final, xt::all(), xt::range(m_nseg_fwd * 3, m_nseg * 3)) -// = xt::view(xgrad_bck, xt::all(), xt::range(0, (m_nseg - m_nseg_fwd) * 3)); - -// // Copy the gradient w.r.t. tof as fwd-bck -// xt::view(xgrad_final, xt::all(), xt::range(m_nseg * 3, m_nseg * 3 + 1)) -// = xt::view(xgrad_fwd, xt::all(), xt::range(m_nseg_fwd * 3, m_nseg_fwd * 3 + 1)) / m_nseg * m_nseg_fwd -// - xt::view(xgrad_bck, xt::all(), xt::range((m_nseg - m_nseg_fwd) * 3, (m_nseg - m_nseg_fwd) * 3 + 1)) / -// m_nseg -// * (m_nseg - m_nseg_fwd); -// return {grad_rvm, grad_rvm_bck, std::move(grad_final)}; -// } - -// std::vector sims_flanagan_hf::compute_tc_grad() const -// { -// std::vector retval(static_cast(m_nseg) * m_nseg * 3u, 0); -// for (decltype(m_throttles.size()) i = 0u; i < m_nseg; ++i) { -// retval[i * m_nseg * 3 + 3 * i] = 2 * m_throttles[3 * i]; -// retval[i * m_nseg * 3 + 3 * i + 1] = 2 * m_throttles[3 * i + 1]; -// retval[i * m_nseg * 3 + 3 * i + 2] = 2 * m_throttles[3 * i + 2]; -// } -// return retval; -// } +std::vector sims_flanagan_hf::compute_constraints() +{ + std::vector retval(7 + m_nseg, 0.); + // Fitness + // Equality Constraints + auto eq_con = compute_mismatch_constraints(); + retval[0] = eq_con[0]; + retval[1] = eq_con[1]; + retval[2] = eq_con[2]; + retval[3] = eq_con[3]; + retval[4] = eq_con[4]; + retval[5] = eq_con[5]; + retval[6] = eq_con[6]; + // Inequality Constraints + auto ineq_con = compute_throttle_constraints(); + std::copy(ineq_con.begin(), ineq_con.end(), retval.begin() + 7); + return retval; +} + +std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector chromosome) +{ + std::array rvms; + std::copy(chromosome.begin(), chromosome.begin() + 7, rvms.begin()); + std::vector throttles(m_nseg * 3); + std::copy(chromosome.begin() + 7, chromosome.begin() + 7 + m_nseg * 3, throttles.begin()); + std::array rvmf; + std::copy(chromosome.begin() + 7 + m_nseg * 3, chromosome.begin() + 7 + m_nseg * 3 + 7, rvmf.begin()); + double time_of_flight = chromosome[29]; + // Set relevant quantities before evaluating constraints + set(rvms, throttles, rvmf, time_of_flight); + // Evaluate and return constraints + return compute_constraints(); +} + +// Return specific two-body 'stark' dynamics state derivative +std::array sims_flanagan_hf::get_state_derivative(std::array state, + std::array throttles) +{ + + std::array thrusts; + // Convert throttles to current_thrusts. + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + std::transform(throttles.begin(), throttles.end(), thrusts.begin(), throttle_to_thrust); + + std::array dstatedt; + // The square of the radius + std::array state_squared = {std::pow(state[0], 2.), std::pow(state[1], 2.), std::pow(state[2], 2.)}; + const auto r2 = std::accumulate(state_squared.begin(), state_squared.end(), 0.0); + double veff = get_isp() * kep3::G0; + + // The throttle magnitude + std::array thrusts_squared + = {std::pow(thrusts[0], 2.), std::pow(thrusts[1], 2.), std::pow(thrusts[2], 2.)}; + const auto u_norm = std::sqrt(std::accumulate(thrusts_squared.begin(), thrusts_squared.end(), 0.0)); + + // The Equations of Motion + dstatedt[0] = state[3]; + dstatedt[1] = state[4]; + dstatedt[2] = state[5]; + dstatedt[3] = -get_mu() * pow(r2, -3. / 2) * state[0] + thrusts[0] / state[6]; + dstatedt[4] = -get_mu() * pow(r2, -3. / 2) * state[1] + thrusts[1] / state[6]; + dstatedt[5] = -get_mu() * pow(r2, -3. / 2) * state[2] + thrusts[2] / state[6]; + dstatedt[6] = (u_norm != 0) ? -u_norm / veff : 0; // Conditional for if thrust is zero or not + + return dstatedt; +} + +std::tuple, 5u>, std::array, 5u>, + std::array, 5u>> +sims_flanagan_hf::compute_mc_grad() +{ + // Initialise + std::array, 5u> xf_per_seg = {{{0}}}; + std::array, 5u> dxdx_per_seg = {{{0}}}; + std::array, 5u> dxdu_per_seg = {{{0}}}; + + // General settings + const double prop_seg_duration = (m_tof / m_nseg); + + // Forward loop + // Set the Taylor Integration initial conditions + m_tas_var.set_time(0.); + std::copy(m_rvms.begin(), m_rvms.end(), m_tas_var.get_state_data()); + + for (unsigned int i = 0u; i < m_nseg_fwd; ++i) { + + // Initialise var conditions + std::copy(m_vars.begin(), m_vars.end(), m_tas_var.get_state_data() + 7); + // Assign current thrusts to Taylor adaptive integrator + if (static_cast((i + 1) * 3) <= m_thrusts.size()) { + std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), + std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), + std::next(m_tas_var.get_pars_data(), 2)); + } else { + throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); + } + // ... and integrate + auto [status, min_h, max_h, nsteps, _1, _2] = m_tas_var.propagate_until((i + 1) * prop_seg_duration); + if (status != heyoka::taylor_outcome::time_limit) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + } + // Save the variational state variables to respective arrays + std::copy(m_tas_var.get_state().begin(), m_tas_var.get_state().begin() + 7, xf_per_seg[i].begin()); + for (auto j = 0; j < 7; ++j) { + std::copy(std::next(m_tas_var.get_state().begin(), 7 + 10l * j), + std::next(m_tas_var.get_state().begin(), 7 + 10l * j + 7), + std::next(dxdx_per_seg[i].begin(), 7 * j)); + std::copy(m_tas_var.get_state().begin() + 14 + 10l * j, m_tas_var.get_state().begin() + 14 + 10l * j + 3, + dxdu_per_seg[i].begin() + 3l * j); + } + } + + // Backward loop + // Set the Taylor Integration initial conditions + m_tas_var.set_time(m_tof); + std::copy(m_rvmf.begin(), m_rvmf.end(), m_tas_var.get_state_data()); + + for (unsigned int i = 0u; i < m_nseg_bck; ++i) { + + // Initialise var conditions + std::copy(m_vars.begin(), m_vars.end(), m_tas_var.get_state_data() + 7); + // Assign current thrusts to Taylor adaptive integrator + if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { + // Copy thrust into Taylor-adaptive integrator + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), + std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), + std::next(m_tas_var.get_pars_data(), 2)); + } else { + throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); + } + // ... and integrate + auto [status, min_h, max_h, nsteps, _1, _2] = m_tas_var.propagate_until(m_tof - (i + 1) * prop_seg_duration); + if (status != heyoka::taylor_outcome::time_limit) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + } + // Save the variational state variables to respective arrays + std::copy(m_tas_var.get_state().begin(), m_tas_var.get_state().begin() + 7, + xf_per_seg[m_nseg - (i + 1)].begin()); + for (auto j = 0; j < 7; ++j) { + std::copy(m_tas_var.get_state().begin() + 7 + 10l * j, m_tas_var.get_state().begin() + 7 + 10l * j + 7, + dxdx_per_seg[m_nseg - (i + 1)].begin() + 7 * j); + std::copy(m_tas_var.get_state().begin() + 14 + 10l * j, m_tas_var.get_state().begin() + 14 + 10l * j + 3, + dxdu_per_seg[m_nseg - (i + 1)].begin() + 3l * j); + } + } + + return std::make_tuple(xf_per_seg, dxdx_per_seg, dxdu_per_seg); +} + +std::vector sims_flanagan_hf::compute_tc_grad() const +{ + std::vector retval(static_cast(m_nseg) * m_nseg * 3u, 0); + for (decltype(m_throttles.size()) i = 0u; i < m_nseg; ++i) { + retval[i * m_nseg * 3 + 3 * i] = 2 * m_throttles[3 * i]; + retval[i * m_nseg * 3 + 3 * i + 1] = 2 * m_throttles[3 * i + 1]; + retval[i * m_nseg * 3 + 3 * i + 2] = 2 * m_throttles[3 * i + 2]; + } + return retval; +} std::ostream &operator<<(std::ostream &s, const sims_flanagan_hf &sf) { diff --git a/src/ta/stark.cpp b/src/ta/stark.cpp index cc8aa51f..a0d23df4 100644 --- a/src/ta/stark.cpp +++ b/src/ta/stark.cpp @@ -52,7 +52,7 @@ std::vector> stark_dyn() // The square of the radius const auto r2 = sum({pow(x, 2.), pow(y, 2.), pow(z, 2.)}); - // The throttle magnitude + // The thrust magnitude const auto u_norm = sqrt(sum({pow(ux, 2.), pow(uy, 2.), pow(uz, 2.)})); // The Equations of Motion diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp new file mode 100644 index 00000000..f3c1ddf3 --- /dev/null +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -0,0 +1,290 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the term_ms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef kep3_TEST_LEG_SIMS_FLANAGAN_HF_HELPERS_H +#define kep3_TEST_LEG_SIMS_FLANAGAN_HF_HELPERS_H + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +struct sf_hf_test_object { + + // Default constructor + sf_hf_test_object() = default; + + sf_hf_test_object(std::vector throttles) : m_throttles(throttles) + { + for (unsigned int i(0); i < m_throttles.size(); ++i) { + m_thrusts.push_back(m_throttles[i] * m_max_thrust); + } + } + + sf_hf_test_object(double cut) : m_cut(cut) {} + + sf_hf_test_object(std::vector throttles, double cut) : m_cut(cut), m_throttles(throttles) + { + for (unsigned int i(0); i < m_throttles.size(); ++i) { + m_thrusts.push_back(m_throttles[i] * m_max_thrust); + } + } + + // Retrieve mismatch constraints from manual heyoka Taylor adaptive integrator + [[nodiscard]] std::array compute_manual_mc() + { + for (unsigned int i(0); i < m_throttles.size(); ++i) { + m_thrusts.push_back(m_throttles[i] * m_max_thrust); + } + + m_new_ta = heyoka::taylor_adaptive{kep3::ta::stark_dyn(), m_rvms, heyoka::kw::tol = m_tol}; + *(m_new_ta.get_pars_data()) = m_mu; + *(m_new_ta.get_pars_data() + 1) = m_isp * kep3::G0; + + // Fwd leg + std::copy(m_thrusts.begin(), std::next(m_thrusts.begin(), 3), m_new_ta.get_pars_data() + 2); + // Set the Taylor Integration initial conditions + m_new_ta.set_time(0.); + std::copy(m_rvms.begin(), m_rvms.end(), m_new_ta.get_state_data()); + // ... and integrate + auto out = m_new_ta.propagate_until(m_tof / 2); + std::copy(m_new_ta.get_state().begin(), m_new_ta.get_state().end(), m_fwd_final_state.begin()); + + // Bck leg + std::copy(std::next(m_thrusts.begin(), 3), std::next(m_thrusts.begin(), 6), m_new_ta.get_pars_data() + 2); + // Set the Taylor Integration initial conditions + m_new_ta.set_time(m_tof); + std::copy(m_rvmf.begin(), m_rvmf.end(), m_new_ta.get_state_data()); + // ... and integrate + auto out2 = m_new_ta.propagate_until(m_tof / 2); + std::copy(m_new_ta.get_state().begin(), m_new_ta.get_state().end(), m_bck_final_state.begin()); + + for (unsigned int i(0); i < m_mc_manual.size(); ++i) { + m_mc_manual[i] = m_fwd_final_state[i] - m_bck_final_state[i]; + } + return m_mc_manual; + }; + + [[nodiscard]] void set_cut(double cut) + { + m_cut = cut; + } + + std::vector compute_numerical_gradient() + { + // Create SF leg. + kep3::leg::sims_flanagan_hf sf_num(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, + m_cut, 1e-16); + // Create chromosome + std::vector rvms_vec = std::vector(m_rvms.begin(), m_rvms.end()); + std::vector rvmf_vec = std::vector(m_rvmf.begin(), m_rvmf.end()); + std::vector chromosome; + chromosome.insert(chromosome.end(), rvms_vec.begin(), rvms_vec.end()); + chromosome.insert(chromosome.end(), m_throttles.begin(), m_throttles.end()); + chromosome.insert(chromosome.end(), rvmf_vec.begin(), rvmf_vec.end()); + chromosome.push_back(m_tof); + + // Calculate numerical gradient + return pagmo::estimate_gradient_h( + [&sf_num](const std::vector &x) { return sf_num.set_and_compute_constraints(x); }, chromosome); + } + + std::tuple, xt::xarray, xt::xarray, xt::xarray, xt::xarray, + xt::xarray, xt::xarray, xt::xarray> + process_mc_numerical_gradient(std::vector num_grad) + { + m_num_grad = num_grad; + + std::array, 7> num_dmc_dxs = {{{0}}}; + std::array, 7> num_dmc_dxf = {{{0}}}; + std::array, 7> num_dmc_du = {{{0}}}; + std::array num_dmc_dtof = {{0}}; + + // Loop over first 7 constraints (the states) and fill in the respective matrices + for (unsigned int i(0); i < num_dmc_dxs.size(); ++i) { + // dmc_dxs + std::copy(std::next(m_num_grad.begin(), 30 * i), std::next(m_num_grad.begin(), 7 + 30 * i), + std::next(num_dmc_dxs.begin(), i)->begin()); + // dmc_du + std::copy(std::next(m_num_grad.begin(), 7 + 30 * i), std::next(m_num_grad.begin(), 22 + 30 * i), + std::next(num_dmc_du.begin(), i)->begin()); + // dmc_dxf + std::copy(std::next(m_num_grad.begin(), 22 + 30 * i), std::next(m_num_grad.begin(), 29 + 30 * i), + std::next(num_dmc_dxf.begin(), i)->begin()); + // dmc_dtof + num_dmc_dtof[i] = m_num_grad[29 + 30 * i]; + } + + xt::xarray xt_num_dmc_dxs = xt::adapt(reinterpret_cast(num_dmc_dxs.data()), {7, 7}); + xt::xarray xt_num_dmc_dxf = xt::adapt(reinterpret_cast(num_dmc_dxf.data()), {7, 7}) + * -1; // Multiple by -1 because mass correlation is -1. + xt::xarray xt_num_dmc_du = xt::adapt(reinterpret_cast(num_dmc_du.data()), {7, 15}); + xt::xarray xt_num_dmc_dtof = xt::adapt(reinterpret_cast(num_dmc_dtof.data()), {7, 1}); + auto xt_num_dmc_du0 = xt::view(xt_num_dmc_du, xt::all(), xt::range(0, 3)); + auto xt_num_dmc_du1 = xt::view(xt_num_dmc_du, xt::all(), xt::range(3, 6)); + auto xt_num_dmc_du2 = xt::view(xt_num_dmc_du, xt::all(), xt::range(6, 9)); + auto xt_num_dmc_du3 = xt::view(xt_num_dmc_du, xt::all(), xt::range(9, 12)) + * -1; // Multiple by -1 because mass correlation is -1. + auto xt_num_dmc_du4 = xt::view(xt_num_dmc_du, xt::all(), xt::range(12, 15)) + * -1; // Multiple by -1 because mass correlation is -1. + + return std::make_tuple(xt_num_dmc_dxs, xt_num_dmc_dxf, xt_num_dmc_du0, xt_num_dmc_du1, xt_num_dmc_du2, + xt_num_dmc_du3, xt_num_dmc_du4, xt_num_dmc_dtof); + } + + std::tuple, xt::xarray, xt::xarray, xt::xarray, xt::xarray, + xt::xarray, xt::xarray, xt::xarray> + compute_analytical_gradient() + { + unsigned int nseg = static_cast(m_throttles.size()) / 3; + auto nseg_fwd = nseg * 0.6; + auto nseg_bck = nseg - nseg_fwd; + // Initialise + std::array, 5u> xf_per_seg; + std::array, 5u> x0_per_seg; + std::array, 5u> dxdx_per_seg; + std::array, 5u> dxdu_per_seg; + std::array, 5u> dxdtof_per_seg; + kep3::leg::sims_flanagan_hf sf_a(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, m_cut, + 1e-16); + std::tie(xf_per_seg, dxdx_per_seg, dxdu_per_seg) = sf_a.compute_mc_grad(); + // Initialize initial state matrix + x0_per_seg[0] = sf_a.get_rvms(); + for (unsigned int i(0); i < nseg_fwd - 1; ++i) { + x0_per_seg[i + 1] = xf_per_seg[i]; + } + x0_per_seg[nseg - 1] = sf_a.get_rvmf(); + for (unsigned int i(0); i < nseg_bck - 1; ++i) { + x0_per_seg[(nseg - 1) - (i + 1)] = xf_per_seg[(nseg - 1) - i]; + } + + for (unsigned int i(0); i < dxdtof_per_seg.size(); ++i) { + std::array current_throttles + = {m_throttles[i * 3], m_throttles[i * 3 + 1], m_throttles[i * 3 + 2]}; + dxdtof_per_seg[i] = sf_a.get_state_derivative(x0_per_seg[i], current_throttles); + } + + xt::xarray xt_dxdx_per_seg = xt::zeros({5, 49}); + for (size_t col = 0; col < 49; ++col) { // Iterate over columns + for (size_t row = 0; row < 5; ++row) { // Iterate over rows + xt_dxdx_per_seg(row, col) = dxdx_per_seg[row][col]; + } + } + + // Create matrices from final states to previous states + // Fwd leg + auto M0 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 0, xt::all()), {7, 7}); + auto M1 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 1, xt::all()), {7, 7}); + auto M2 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 2, xt::all()), {7, 7}); + // Bck leg + auto M3 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 3, xt::all()), {7, 7}); + auto M4 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 4, xt::all()), {7, 7}); + + // Create matrices from final states to throttles + xt::xarray xt_dxdu_per_seg = xt::adapt(reinterpret_cast(dxdu_per_seg.data()), {5, 21}); + // Fwd leg + auto U0 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 0, xt::all()), {7, 3}); + auto U1 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 1, xt::all()), {7, 3}); + auto U2 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 2, xt::all()), {7, 3}); + // Bck leg + auto U3 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 3, xt::all()), {7, 3}); + auto U4 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 4, xt::all()), {7, 3}); + + // Create matrices from final states to throttles + xt::xarray xt_dxdtof_per_seg = xt::adapt(reinterpret_cast(dxdtof_per_seg.data()), {5, 7}); + // Fwd leg + auto f0 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 0, xt::all()), {7, 1}); + auto f1 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 1, xt::all()), {7, 1}); + auto f2 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 2, xt::all()), {7, 1}); + // Bck leg + auto f5 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 3, xt::all()), + {7, 1}); // f5 is because we don't care about f3 and f4 and the mc + auto f6 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 4, xt::all()), {7, 1}); + + // Initial and final displacements + auto xt_a_dmc_dxs = xt::linalg::dot(xt::linalg::dot(M2, M1), M0); + auto xt_a_dmc_dxf = xt::linalg::dot(M3, M4); + // Throttle derivatives + auto xt_a_dmc_du0 = xt::linalg::dot(xt::linalg::dot(M2, M1), U0); + auto xt_a_dmc_du1 = xt::linalg::dot(M2, U1); + auto xt_a_dmc_du2 = U2; + auto xt_a_dmc_du3 = U3; + auto xt_a_dmc_du4 = xt::linalg::dot(M3, U4); + // ToF derivatives + auto xt_a_dmc_dtof0 = xt::linalg::dot(xt::linalg::dot(xt::linalg::dot(M2, M1), M0), f0); + auto xt_a_dmc_dtof1 = xt::linalg::dot(xt::linalg::dot(M2, M1), f1); + auto xt_a_dmc_dtof2 = xt::linalg::dot(M2, f2); + auto xt_a_dmc_dtof3 = xt::linalg::dot(M3, f5); + auto xt_a_dmc_dtof4 = xt::linalg::dot(xt::linalg::dot(M3, M4), f6); + auto xt_a_dmc_dtof + = (xt_a_dmc_dtof0 + xt_a_dmc_dtof1 + xt_a_dmc_dtof2 + xt_a_dmc_dtof3 + xt_a_dmc_dtof4) / nseg; + + return std::make_tuple(xt_a_dmc_dxs, xt_a_dmc_dxf, xt_a_dmc_du0, xt_a_dmc_du1, xt_a_dmc_du2, xt_a_dmc_du3, + xt_a_dmc_du4, xt_a_dmc_dtof); + } + + std::vector m_num_grad; + + heyoka::taylor_adaptive m_new_ta; + std::array m_fwd_final_state; + std::array m_bck_final_state; + std::array m_mc_manual; + + std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; + + std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; + + double m_ms = 1; + double m_mf = m_ms * 13 / 15; + double m_isp = 1; + double m_max_thrust = 1; + double m_cut = 0.5; + double m_mu = 1; + + double m_tof = 1; + + std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + std::vector m_thrusts; + + double m_tol = 1e-16; + const std::vector m_rvms + = {m_rvs[0][0], m_rvs[0][1], m_rvs[0][2], m_rvs[1][0], m_rvs[1][1], m_rvs[1][2], m_ms}; + const std::vector m_rvmf + = {m_rvf[0][0], m_rvf[0][1], m_rvf[0][2], m_rvf[1][0], m_rvf[1][1], m_rvf[1][2], m_mf}; +}; + +#endif \ No newline at end of file diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index cac519e9..f86b9825 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -9,9 +9,9 @@ #include #include -// #include #include +#include #include #include @@ -28,11 +28,23 @@ #include #include #include +#include #include #include "catch.hpp" -// #include "leg_sims_flanagan_udp.hpp" -// #include "test_helpers.hpp" +#include "leg_sims_flanagan_hf_helpers.hpp" +#include "test_helpers.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include TEST_CASE("constructor") { @@ -55,8 +67,8 @@ TEST_CASE("constructor") REQUIRE_THROWS_AS( kep3::leg::sims_flanagan_hf(rvs, ms, {0., 0., 0., 0., 0.}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), std::logic_error); - // REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, -0.42, 1., 1., 1., 0.5), - // std::domain_error); // SC: negative ToF valid? + REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, -0.42, 1., 1., 1., 0.5), + std::domain_error); REQUIRE_THROWS_AS( kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, -0.3, 1., 1., 0.5), std::domain_error); @@ -186,153 +198,153 @@ TEST_CASE("compute_mismatch_constraints_test") REQUIRE(*std::max_element(mc.begin(), mc.end()) < 1e-8); } } +} + +TEST_CASE("compute_mismatch_constraints_test2") +{ + + // Initialise unique test quantities + double cut = 0.6; + auto sf_helper_object = sf_hf_test_object(cut); + + kep3::leg::sims_flanagan_hf sf(sf_helper_object.m_rvs, sf_helper_object.m_ms, sf_helper_object.m_throttles, + sf_helper_object.m_rvf, sf_helper_object.m_mf, sf_helper_object.m_tof, + sf_helper_object.m_max_thrust, sf_helper_object.m_isp, sf_helper_object.m_mu, + sf_helper_object.m_cut, 1e-16); + kep3::leg::sims_flanagan sf_lf(sf_helper_object.m_rvs, sf_helper_object.m_ms, sf_helper_object.m_throttles, + sf_helper_object.m_rvf, sf_helper_object.m_mf, sf_helper_object.m_tof, + sf_helper_object.m_max_thrust, sf_helper_object.m_isp, sf_helper_object.m_mu, + sf_helper_object.m_cut); + + auto retval = sf.compute_mismatch_constraints(); + auto retval_lf = sf_lf.compute_mismatch_constraints(); + + std::array r1 = {retval[0], retval[1], retval[2]}; + std::array r2 = {retval_lf[0], retval_lf[1], retval_lf[2]}; + std::array v1 = {retval[3], retval[4], retval[5]}; + std::array v2 = {retval_lf[3], retval_lf[4], retval_lf[5]}; - // { - // // Here we reuse the ballitic arc as a ground truth for an optimization. - // // We check that, when feasible, the optimal mass solution is indeed ballistic. - // pagmo::problem prob{sf_test_udp{rv0, mass, rv1, 0.05, 2000, 10u}}; - // prob.set_c_tol(1e-8); - // bool found = false; - // unsigned trial = 0u; - // pagmo::nlopt uda{"slsqp"}; - // uda.set_xtol_abs(1e-10); - // uda.set_xtol_rel(1e-10); - // uda.set_ftol_abs(0); - // uda.set_maxeval(1000); - // pagmo::algorithm algo{uda}; - // while ((!found) && (trial < 20u)) { - // pagmo::population pop{prob, 1u}; - // algo.set_verbosity(10u); - // pop = algo.evolve(pop); - // auto champ = pop.champion_f(); - // found = prob.feasibility_f(champ); - // if (found) { - // fmt::print("{}\n", champ); - // found = *std::min_element(champ.begin() + 7, champ.end()) < -0.99999; - // } - // trial++; - // } - // REQUIRE_FALSE(!found); // If this does not pass, then the optimization above never found a ballistic arc ... - // // theres a problem somewhere. - // } - // { - // // Here we create an ALMOST ballistic arc as a ground truth for an optimization. - // // We check that, when feasible, the optimal mass solution is indeed ballistic. - // auto rv1_modified = rv1; - // rv1_modified[1][0] += 1000; // Adding 1km/s along x - // pagmo::problem prob{sf_test_udp{rv0, mass, rv1_modified, 0.05, 2000, 10u}}; - // prob.set_c_tol(1e-8); - // bool found = false; - // unsigned trial = 0u; - // pagmo::nlopt uda{"slsqp"}; - // uda.set_xtol_abs(1e-10); - // uda.set_xtol_rel(1e-10); - // uda.set_ftol_abs(0); - // uda.set_maxeval(1000); - // pagmo::algorithm algo{uda}; - // while ((!found) && (trial < 20u)) { - // pagmo::population pop{prob, 1u}; - // algo.set_verbosity(10u); - // pop = algo.evolve(pop); - // auto champ = pop.champion_f(); - // found = prob.feasibility_f(champ); - // if (found) { - // fmt::print("{}\n", champ); - // } - // trial++; - // } - // // If this does not pass, then the optimization above never converged to a feasible solution. - // REQUIRE_FALSE(!found); - // } + REQUIRE(kep3_tests::floating_point_error_vector(r1, r2) < 1e-14); + REQUIRE(kep3_tests::floating_point_error_vector(v1, v2) < 1e-14); + REQUIRE(std::abs((retval[6] - retval_lf[6]) / retval[6]) < 1e-14); } -// SC: Not supposed to work because different dynamics are at play -TEST_CASE("mismatch_constraints_test2") +TEST_CASE("compute_mismatch_constraints_test3") { - // // We test the correctness of the compute_mismatch_constraints computations against a ground truth (computed with - // a different program) - std::array, 2> rvs{ - {{1 * kep3::AU, 0.1 * kep3::AU, -0.1 * kep3::AU}, - {0.2 * kep3::EARTH_VELOCITY, 1 * kep3::EARTH_VELOCITY, -0.2 * kep3::EARTH_VELOCITY}}}; - - std::array, 2> rvf{ - {{1.2 * kep3::AU, -0.1 * kep3::AU, 0.1 * kep3::AU}, - {-0.2 * kep3::EARTH_VELOCITY, 1.023 * kep3::EARTH_VELOCITY, -0.44 * kep3::EARTH_VELOCITY}}}; - - double ms = 1500.; - double mf = 1300.; + + // Initialise unique test quantities + double cut = 0.5; + std::vector throttles = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15}; + auto sf_test_object = sf_hf_test_object(throttles, cut); + std::array mc_manual = sf_test_object.compute_manual_mc(); + + // Calculate equivalent with hf leg. + kep3::leg::sims_flanagan_hf sf(sf_test_object.m_rvs, sf_test_object.m_ms, sf_test_object.m_throttles, + sf_test_object.m_rvf, sf_test_object.m_mf, sf_test_object.m_tof, + sf_test_object.m_max_thrust, sf_test_object.m_isp, sf_test_object.m_mu, + sf_test_object.m_cut, 1e-16); + auto mc_sf_hf = sf.compute_mismatch_constraints(); + + std::array r1 = {mc_sf_hf[0], mc_sf_hf[1], mc_sf_hf[2]}; + std::array r2 = {mc_manual[0], mc_manual[1], mc_manual[2]}; + REQUIRE(kep3_tests::floating_point_error_vector(r1, r2) < 1e-16); + std::array v1 = {mc_sf_hf[3], mc_sf_hf[4], mc_sf_hf[5]}; + std::array v2 = {mc_manual[3], mc_manual[4], mc_manual[5]}; + REQUIRE(kep3_tests::floating_point_error_vector(v1, v2) < 1e-16); + REQUIRE(std::abs((mc_sf_hf[6] - mc_manual[6]) / mc_sf_hf[6]) < 1e-16); +} + +TEST_CASE("compute_mc_grad_test") +{ + // Initialise unique test quantities std::vector throttles = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24}; - std::vector inverted_throttles = throttles; - // Invert direction of backwards leg thrust - auto invert_direction = [](double throttle) { return throttle * -1; }; - std::transform(throttles.begin(), throttles.end(), inverted_throttles.begin(), invert_direction); - kep3::leg::sims_flanagan_hf sf(rvs, ms, throttles, rvf, mf, 324.0 * kep3::DAY2SEC, 0.12, 100, kep3::MU_SUN, 0.6, - 1e-13); - auto retval = sf.compute_mismatch_constraints(); + double cut = 0.6; + auto sf_test_object = sf_hf_test_object(throttles, cut); + + // Numerical gradient + std::vector num_grad = sf_test_object.compute_numerical_gradient(); + + xt::xarray xt_num_dmc_dxs, xt_num_dmc_dxf, xt_num_dmc_du0, xt_num_dmc_du1, xt_num_dmc_du2, xt_num_dmc_du3, + xt_num_dmc_du4, xt_num_dmc_dtof; + std::tie(xt_num_dmc_dxs, xt_num_dmc_dxf, xt_num_dmc_du0, xt_num_dmc_du1, xt_num_dmc_du2, xt_num_dmc_du3, + xt_num_dmc_du4, xt_num_dmc_dtof) + = sf_test_object.process_mc_numerical_gradient(num_grad); - // Change initial and final state to have inverted velocities - // std::transform(rvs[1].begin(), rvs[1].end(), rvs[1].begin(), invert_direction); - // std::transform(rvf[1].begin(), rvf[1].end(), rvf[1].begin(), invert_direction); - kep3::leg::sims_flanagan_hf sf2(rvf, mf, throttles, rvs, ms, -324.0 * kep3::DAY2SEC, 0.12, 100, kep3::MU_SUN, 0.4, - 1e-13); - auto retval2 = sf2.compute_mismatch_constraints(); - - REQUIRE(std::abs((retval2[0] - retval[0]) / retval2[0]) < 1e-13); - REQUIRE(std::abs((retval2[1] - retval[1]) / retval2[1]) < 1e-13); - REQUIRE(std::abs((retval2[2] - retval[2]) / retval2[2]) < 1e-13); - REQUIRE(std::abs((retval2[3] - retval[3]) / retval2[3]) < 1e-13); - REQUIRE(std::abs((retval2[4] - retval[4]) / retval2[4]) < 1e-13); - REQUIRE(std::abs((retval2[5] - retval[5]) / retval2[5]) < 1e-13); + // Analytical gradient + xt::xarray xt_a_dmc_dxs, xt_a_dmc_dxf, xt_a_dmc_du0, xt_a_dmc_du1, xt_a_dmc_du2, xt_a_dmc_du3, xt_a_dmc_du4, + xt_a_dmc_dtof; + std::tie(xt_a_dmc_dxs, xt_a_dmc_dxf, xt_a_dmc_du0, xt_a_dmc_du1, xt_a_dmc_du2, xt_a_dmc_du3, xt_a_dmc_du4, + xt_a_dmc_dtof) + = sf_test_object.compute_analytical_gradient(); + + // Calculate analytical gradient + + REQUIRE(xt::linalg::norm(xt_num_dmc_dxs - xt_a_dmc_dxs) < 1e-9); // SC: The difference is like 4.56e-8 + REQUIRE(xt::linalg::norm(xt_num_dmc_dxf - xt_a_dmc_dxf) < 1e-9); + REQUIRE(xt::linalg::norm(xt_num_dmc_du0 - xt_a_dmc_du0) < 1e-9); + REQUIRE(xt::linalg::norm(xt_num_dmc_du1 - xt_a_dmc_du1) < 1e-9); + REQUIRE(xt::linalg::norm(xt_num_dmc_du2 - xt_a_dmc_du2) < 1e-9); + REQUIRE(xt::linalg::norm(xt_num_dmc_du3 - xt_a_dmc_du3) < 1e-9); + REQUIRE(xt::linalg::norm(xt_num_dmc_du4 - xt_a_dmc_du4) < 1e-9); + REQUIRE(xt::linalg::norm(xt_num_dmc_dtof - xt_a_dmc_dtof) < 1e-9); +} + +TEST_CASE("compute_tc_grad_test") +{ + + // Initialise unique test quantities + std::vector throttles + = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24}; + unsigned int nseg = static_cast(throttles.size()) / 3; + double cut = 0.6; + // Initialise helper quantities + auto sf_test_object = sf_hf_test_object(throttles, cut); + + // Numerical gradient + std::vector num_grad = sf_test_object.compute_numerical_gradient(); + std::vector tc_num_grad(nseg * 15); + for (unsigned int i(0); i < nseg; ++i) { + // dtc_du + std::copy(std::next(num_grad.begin(), 7 + 30 * (i + 7)), std::next(num_grad.begin(), 22 + 30 * (i + 7)), + std::next(tc_num_grad.begin(), i * 15)); + } + xt::xarray xt_tc_num_grad = xt::adapt(reinterpret_cast(tc_num_grad.data()), {5, 15}); + + // Calculate throttle constraint gradients + kep3::leg::sims_flanagan_hf sf(sf_test_object.m_rvs, sf_test_object.m_ms, sf_test_object.m_throttles, + sf_test_object.m_rvf, sf_test_object.m_mf, sf_test_object.m_tof, + sf_test_object.m_max_thrust, sf_test_object.m_isp, sf_test_object.m_mu, + sf_test_object.m_cut, 1e-16); + std::vector tc_a_grad = sf.compute_tc_grad(); + xt::xarray xt_tc_a_grad = xt::adapt(reinterpret_cast(tc_a_grad.data()), {5, 15}); + + REQUIRE(xt::linalg::norm(xt_tc_num_grad - xt_tc_a_grad) < 1e-13); // SC: 1e-14 fails } -// TEST_CASE("grad_test") -// { -// // Here we test the analytical gradient against an equivalent numerical one. We do so through the udp -// "sf_test_udp" std::array, 2> rvs{ -// {{1 * kep3::AU, 0.1 * kep3::AU, -0.1 * kep3::AU}, -// {0.2 * kep3::EARTH_VELOCITY, 1 * kep3::EARTH_VELOCITY, -0.2 * kep3::EARTH_VELOCITY}}}; -// // -// std::array, 2> rvf{ -// {{1.2 * kep3::AU, -0.1 * kep3::AU, 0.1 * kep3::AU}, -// {-0.2 * kep3::EARTH_VELOCITY, 1.023 * kep3::EARTH_VELOCITY, -0.44 * kep3::EARTH_VELOCITY}}}; -// // -// double ms = 1500.; - -// sf_test_udp udp{rvs, ms, rvf, 0.12, 100, 5}; -// std::vector x -// = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 324., 1300.}; -// auto grad = udp.gradient_numerical(x); -// auto grad_a = udp.gradient(x); -// auto xgrad = xt::adapt(grad, {1u + 7u + 5u, 17u}); -// auto xgrad_a = xt::adapt(grad_a, {1u + 7u + 5u, 17u}); -// REQUIRE(xt::linalg::norm(xgrad - xgrad_a) < 1e-8); // With the high fidelity gradient this is still the best we -// can achieve -// } - -// TEST_CASE("serialization_test") -// { -// // Instantiate a generic lambert problem -// std::array, 2> rvs{{{-1, -1, -1}, {-1, -1, -1}}}; -// std::array, 2> rvf{{{0.1, 1.1, 0.1}, {-1.1, 0.1, 0.1}}}; -// kep3::leg::sims_flanagan_hf sf1{rvs, 12., {1, 2, 3, 4, 5, 6}, rvf, 10, 2.3, 2.3, 2.3, 1.1, 0.2}; - -// // Store the string representation. -// std::stringstream ss; -// auto before = boost::lexical_cast(sf1); -// // Now serialize -// { -// boost::archive::binary_oarchive oarchive(ss); -// oarchive << sf1; -// } -// // Deserialize -// // Create a new lambert problem object -// kep3::leg::sims_flanagan_hf sf2{}; -// { -// boost::archive::binary_iarchive iarchive(ss); -// iarchive >> sf2; -// } -// auto after = boost::lexical_cast(sf2); -// // Compare the string represetation -// REQUIRE(before == after); -// } +TEST_CASE("serialization_test") +{ + // Instantiate a generic lambert problem + std::array, 2> rvs{{{-1, -1, -1}, {-1, -1, -1}}}; + std::array, 2> rvf{{{0.1, 1.1, 0.1}, {-1.1, 0.1, 0.1}}}; + kep3::leg::sims_flanagan_hf sf1{rvs, 12., {1, 2, 3, 4, 5, 6}, rvf, 10, 2.3, 2.3, 2.3, 1.1, 0.2}; + + // Store the string representation. + std::stringstream ss; + auto before = boost::lexical_cast(sf1); + // Now serialize + { + boost::archive::binary_oarchive oarchive(ss); + oarchive << sf1; + } + // Deserialize + // Create a new lambert problem object + kep3::leg::sims_flanagan_hf sf_a{}; + { + boost::archive::binary_iarchive iarchive(ss); + iarchive >> sf_a; + } + auto after = boost::lexical_cast(sf_a); + // Compare the string represetation + REQUIRE(before == after); +} \ No newline at end of file From 07df133188ce760e0b712326a0ea64d46a08206b Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Mon, 7 Oct 2024 09:46:22 +0200 Subject: [PATCH 03/22] Generalized hf leg to all possible segment counts. Improved tests to include various segment counts and cut ratios. Small formatting fixes. --- include/kep3/leg/sims_flanagan_hf.hpp | 21 +++- src/leg/sims_flanagan_hf.cpp | 173 +++++++++++++++++++++++--- test/leg_sims_flanagan_hf_helpers.hpp | 158 ++++------------------- test/leg_sims_flanagan_hf_test.cpp | 72 +++++------ 4 files changed, 229 insertions(+), 195 deletions(-) diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 3a72ea38..4024adfe 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -102,17 +102,28 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Compute constraints [[nodiscard]] std::array get_state_derivative(std::array state, - std::array throttles); + std::array throttles) const; [[nodiscard]] std::array compute_mismatch_constraints(); [[nodiscard]] std::vector compute_throttle_constraints() const; [[nodiscard]] std::vector compute_constraints(); [[nodiscard]] std::vector set_and_compute_constraints(std::vector chromosome); - // Compute mismatch constraint gradients (w.r.t. rvm state and w.r.t. throttles) - [[nodiscard]] std::tuple, 5u>, std::array, 5u>, - std::array, 5u>> - compute_mc_grad(); + // Compute all gradients w.r.t. all legs + [[nodiscard]] + std::tuple>, std::vector>, + std::vector>> compute_all_gradients(); + + // Process all gradients to retrieve relevant gradients (w.r.t. initial and final rvm state as well as w.r.t. + // throttles and tof) + [[nodiscard]] std::tuple, std::array, std::vector> + get_relevant_gradients(std::vector> &dxdx_per_seg, + std::vector> &dxdu_per_seg, + std::vector> &dxdtof_per_seg) const; + + // Compute mismatch constraint gradients (w.r.t. initial and final rvm state as well as w.r.t. throttles and + // tof) + [[nodiscard]] std::tuple, std::array, std::vector> compute_mc_grad(); // Compute throttle constraint gradients [[nodiscard]] std::vector compute_tc_grad() const; diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 9911cfbe..3d28d078 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -40,11 +40,6 @@ namespace kep3::leg { -using kep3::linalg::mat13; -using kep3::linalg::mat61; -using kep3::linalg::mat63; -using kep3::linalg::mat66; - // Constructors sims_flanagan_hf::sims_flanagan_hf() @@ -444,7 +439,7 @@ std::vector sims_flanagan_hf::compute_constraints() retval[4] = eq_con[4]; retval[5] = eq_con[5]; retval[6] = eq_con[6]; - // Inequality Constraints + // Inequality Constraints auto ineq_con = compute_throttle_constraints(); std::copy(ineq_con.begin(), ineq_con.end(), retval.begin() + 7); return retval; @@ -458,7 +453,7 @@ std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector rvmf; std::copy(chromosome.begin() + 7 + m_nseg * 3, chromosome.begin() + 7 + m_nseg * 3 + 7, rvmf.begin()); - double time_of_flight = chromosome[29]; + double time_of_flight = chromosome[(7 + m_nseg * 3 + 7 + 1) - 1]; // Set relevant quantities before evaluating constraints set(rvms, throttles, rvmf, time_of_flight); // Evaluate and return constraints @@ -467,7 +462,7 @@ std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector sims_flanagan_hf::get_state_derivative(std::array state, - std::array throttles) + std::array throttles) const { std::array thrusts; @@ -498,14 +493,17 @@ std::array sims_flanagan_hf::get_state_derivative(std::array, 5u>, std::array, 5u>, - std::array, 5u>> -sims_flanagan_hf::compute_mc_grad() +std::tuple>, std::vector>, + std::vector>> +sims_flanagan_hf::compute_all_gradients() { // Initialise - std::array, 5u> xf_per_seg = {{{0}}}; - std::array, 5u> dxdx_per_seg = {{{0}}}; - std::array, 5u> dxdu_per_seg = {{{0}}}; + std::vector> xf_per_seg(m_nseg, {0}); + std::vector> dxdx_per_seg(m_nseg, {0}); + std::vector> dxdu_per_seg(m_nseg, {0}); + // For ToF gradient + std::vector> x0_per_seg(m_nseg, {0}); + std::vector> dxdtof_per_seg(m_nseg, {0}); // General settings const double prop_seg_duration = (m_tof / m_nseg); @@ -577,7 +575,152 @@ sims_flanagan_hf::compute_mc_grad() } } - return std::make_tuple(xf_per_seg, dxdx_per_seg, dxdu_per_seg); + // Get ToF gradients + // Initialize initial state matrix + if (m_nseg_fwd > 0) { + x0_per_seg[0] = m_rvms; + } + for (unsigned int i(1); i < m_nseg_fwd; ++i) { + x0_per_seg[i] = xf_per_seg[i - 1]; + } + if (m_nseg_bck > 0) { + x0_per_seg[m_nseg - 1] = m_rvmf; + } + for (unsigned int i(1); i < m_nseg_bck; ++i) { + x0_per_seg[(m_nseg - 1) - i] = xf_per_seg[(m_nseg - 1) - (i - 1)]; + } + + for (unsigned int i(0); i < dxdtof_per_seg.size(); ++i) { + std::array current_throttles = {m_throttles[i * 3], m_throttles[i * 3 + 1], m_throttles[i * 3 + 2]}; + dxdtof_per_seg[i] = get_state_derivative(x0_per_seg[i], current_throttles); + } + + return std::make_tuple(dxdx_per_seg, dxdu_per_seg, dxdtof_per_seg); +} + +std::tuple, std::array, std::vector> +sims_flanagan_hf::get_relevant_gradients(std::vector> &dxdx_per_seg, + std::vector> &dxdu_per_seg, + std::vector> &dxdtof_per_seg) const +{ + + auto xt_dxdx_per_seg = xt::adapt(reinterpret_cast(dxdx_per_seg.data()), {m_nseg, 49u}); + // Mn_o will contain [Mnf-1, Mnf-1@Mnf-2, Mnf-2@Mnf-3, Mnf-1@M0, Mnf, Mnf@Mnf+1, Mnf@Mnf+2, Mnf@Mn] + std::vector> Mn_o(m_nseg, xt::zeros({7u, 7u})); + // Fwd leg + xt::xarray final_M; + xt::xarray current_M; + if (m_nseg_fwd > 0) { + Mn_o[0] = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd - 1, xt::all()), {7, 7}); + for (unsigned int i(0); i < m_nseg_fwd - 1; ++i) { + current_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd - 1 - (i + 1), xt::all()), {7, 7}); + if (i == 0) { + final_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd - 1, xt::all()), {7, 7}); + } else { + final_M = Mn_o[i]; + } + Mn_o[i + 1] = xt::linalg::dot(final_M, current_M); + } + } + // Bck leg + if (m_nseg_bck > 0) { + Mn_o[m_nseg_fwd] = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd, xt::all()), {7, 7}); + for (unsigned int i(0); i < m_nseg_bck - 1; ++i) { + current_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd + (i + 1), xt::all()), {7, 7}); + if (i == 0) { + final_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd, xt::all()), {7, 7}); + } else { + final_M = Mn_o[m_nseg_fwd + i]; + } + Mn_o[m_nseg_fwd + i + 1] = xt::linalg::dot(final_M, current_M); + } + } + + // Initial and final displacements + std::array grad_rvm = {0}; + auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); + if (m_nseg_fwd > 0) { + xt::view(xgrad_rvm, xt::all(), xt::all()) = xt::view(Mn_o[m_nseg_fwd - 1], xt::all(), xt::all()); + } else { + xt::view(xgrad_rvm, xt::all(), xt::all()) = xt::eye(7); + } + + std::array grad_rvm_bck = {0}; + auto xgrad_rvm_bck = xt::adapt(grad_rvm_bck, {7u, 7u}); + if (m_nseg_bck > 0) { + xt::view(xgrad_rvm_bck, xt::all(), xt::all()) + = xt::view(Mn_o[m_nseg - 1], xt::all(), xt::all()) * -1; // Multiple by -1 because mass correlation is -1. + } else { + xt::view(xgrad_rvm_bck, xt::all(), xt::all()) = xt::eye(7) * -1; + } + + // Throttle derivatives + xt::xarray xt_dxdu_per_seg = xt::adapt(reinterpret_cast(dxdu_per_seg.data()), {m_nseg, 21u}); + std::vector grad_final_throttle(static_cast(7) * (m_nseg * 3u), 0.); + auto xgrad_final_throttle = xt::adapt(grad_final_throttle, {7u, static_cast(m_nseg) * 3u}); + xt::xarray corresponding_M; + xt::xarray current_U; + for (unsigned int i(0); i < m_nseg; ++i) { + current_U = xt::reshape_view(xt::view(xt_dxdu_per_seg, i, xt::all()), {7, 3}); + if (i == m_nseg_fwd - 1) { + corresponding_M = xt::eye(7); + } else if (i == m_nseg_fwd) { + corresponding_M = xt::eye(7) * -1; // Multiple by -1 because mass correlation is -1. + } else if (i <= m_nseg_fwd - 2 && m_nseg_fwd >= 2) { + corresponding_M = Mn_o[m_nseg_fwd - 2 - i]; + } else if (i > m_nseg_fwd) { + corresponding_M = Mn_o[i - 1] * -1; // Multiple by -1 because mass correlation is -1. + } else { + throw std::runtime_error("During calculation of the throttle derivatives, the index doesn't correspond to " + "any leg and therefore cannot find the corresponding gradients."); + } + xt::view(xgrad_final_throttle, xt::all(), xt::range(3 * i, 3 * (i + 1))) + = xt::linalg::dot(corresponding_M, current_U); + } + + // ToF derivatives + xt::xarray xt_dxdtof_per_seg = xt::adapt(reinterpret_cast(dxdtof_per_seg.data()), {m_nseg, 7u}); + std::vector grad_final_tof(static_cast(7), 0.); + auto xgrad_final_tof = xt::adapt(grad_final_tof, {7u, 1u}); + for (unsigned int i(0); i < m_nseg; ++i) { + xt::xarray current_F = xt::reshape_view(xt::view(xt_dxdtof_per_seg, i, xt::all()), {7, 1}); + if ((i <= m_nseg_fwd - 1) && m_nseg_fwd > 0) { + corresponding_M = Mn_o + [m_nseg_fwd - 1 + - i]; // +1 w.r.t. throttle derivatives because dx/dtof is defined at begin of leg rather than end + } else if ((static_cast(i) > static_cast(m_nseg_fwd) - 1) && m_nseg_bck > 0) { + corresponding_M = Mn_o[i]; // Idem + } else { + throw std::runtime_error("During calculation of the tof derivatives, the index doesn't correspond to " + "any leg and therefore cannot find the corresponding gradients."); + } + xgrad_final_tof += xt::linalg::dot(corresponding_M, current_F); + } + xgrad_final_tof /= m_nseg; + + // Combine throttle and tof matrices + std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); + auto xgrad_final = xt::adapt(grad_final, {7u, static_cast(m_nseg) * 3u + 1u}); + xt::view(xgrad_final, xt::all(), xt::range(0, m_nseg * 3)) = xt::view(xgrad_final_throttle, xt::all(), xt::all()); + xt::view(xgrad_final, xt::all(), m_nseg * 3) = xt::view(xgrad_final_tof, xt::all(), 0); + + return {std::move(grad_rvm), std::move(grad_rvm_bck), std::move(grad_final)}; +} + +std::tuple, std::array, std::vector> sims_flanagan_hf::compute_mc_grad() +{ + // Initialise + std::vector> dxdx_per_seg; + std::vector> dxdu_per_seg; + std::vector> dxdtof_per_seg; + std::tie(dxdx_per_seg, dxdu_per_seg, dxdtof_per_seg) = compute_all_gradients(); + + std::array grad_rvm = {0}; + std::array grad_rvm_bck = {0}; + std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); + std::tie(grad_rvm, grad_rvm_bck, grad_final) = get_relevant_gradients(dxdx_per_seg, dxdu_per_seg, dxdtof_per_seg); + + return {grad_rvm, grad_rvm_bck, grad_final}; } std::vector sims_flanagan_hf::compute_tc_grad() const diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp index f3c1ddf3..1ed92e9a 100644 --- a/test/leg_sims_flanagan_hf_helpers.hpp +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -123,163 +123,51 @@ struct sf_hf_test_object { [&sf_num](const std::vector &x) { return sf_num.set_and_compute_constraints(x); }, chromosome); } - std::tuple, xt::xarray, xt::xarray, xt::xarray, xt::xarray, - xt::xarray, xt::xarray, xt::xarray> - process_mc_numerical_gradient(std::vector num_grad) + std::vector compute_analytical_gradient() { - m_num_grad = num_grad; - - std::array, 7> num_dmc_dxs = {{{0}}}; - std::array, 7> num_dmc_dxf = {{{0}}}; - std::array, 7> num_dmc_du = {{{0}}}; - std::array num_dmc_dtof = {{0}}; - - // Loop over first 7 constraints (the states) and fill in the respective matrices - for (unsigned int i(0); i < num_dmc_dxs.size(); ++i) { - // dmc_dxs - std::copy(std::next(m_num_grad.begin(), 30 * i), std::next(m_num_grad.begin(), 7 + 30 * i), - std::next(num_dmc_dxs.begin(), i)->begin()); - // dmc_du - std::copy(std::next(m_num_grad.begin(), 7 + 30 * i), std::next(m_num_grad.begin(), 22 + 30 * i), - std::next(num_dmc_du.begin(), i)->begin()); - // dmc_dxf - std::copy(std::next(m_num_grad.begin(), 22 + 30 * i), std::next(m_num_grad.begin(), 29 + 30 * i), - std::next(num_dmc_dxf.begin(), i)->begin()); - // dmc_dtof - num_dmc_dtof[i] = m_num_grad[29 + 30 * i]; - } - - xt::xarray xt_num_dmc_dxs = xt::adapt(reinterpret_cast(num_dmc_dxs.data()), {7, 7}); - xt::xarray xt_num_dmc_dxf = xt::adapt(reinterpret_cast(num_dmc_dxf.data()), {7, 7}) - * -1; // Multiple by -1 because mass correlation is -1. - xt::xarray xt_num_dmc_du = xt::adapt(reinterpret_cast(num_dmc_du.data()), {7, 15}); - xt::xarray xt_num_dmc_dtof = xt::adapt(reinterpret_cast(num_dmc_dtof.data()), {7, 1}); - auto xt_num_dmc_du0 = xt::view(xt_num_dmc_du, xt::all(), xt::range(0, 3)); - auto xt_num_dmc_du1 = xt::view(xt_num_dmc_du, xt::all(), xt::range(3, 6)); - auto xt_num_dmc_du2 = xt::view(xt_num_dmc_du, xt::all(), xt::range(6, 9)); - auto xt_num_dmc_du3 = xt::view(xt_num_dmc_du, xt::all(), xt::range(9, 12)) - * -1; // Multiple by -1 because mass correlation is -1. - auto xt_num_dmc_du4 = xt::view(xt_num_dmc_du, xt::all(), xt::range(12, 15)) - * -1; // Multiple by -1 because mass correlation is -1. - - return std::make_tuple(xt_num_dmc_dxs, xt_num_dmc_dxf, xt_num_dmc_du0, xt_num_dmc_du1, xt_num_dmc_du2, - xt_num_dmc_du3, xt_num_dmc_du4, xt_num_dmc_dtof); - } - - std::tuple, xt::xarray, xt::xarray, xt::xarray, xt::xarray, - xt::xarray, xt::xarray, xt::xarray> - compute_analytical_gradient() - { - unsigned int nseg = static_cast(m_throttles.size()) / 3; - auto nseg_fwd = nseg * 0.6; - auto nseg_bck = nseg - nseg_fwd; // Initialise - std::array, 5u> xf_per_seg; - std::array, 5u> x0_per_seg; - std::array, 5u> dxdx_per_seg; - std::array, 5u> dxdu_per_seg; - std::array, 5u> dxdtof_per_seg; kep3::leg::sims_flanagan_hf sf_a(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, m_cut, 1e-16); - std::tie(xf_per_seg, dxdx_per_seg, dxdu_per_seg) = sf_a.compute_mc_grad(); - // Initialize initial state matrix - x0_per_seg[0] = sf_a.get_rvms(); - for (unsigned int i(0); i < nseg_fwd - 1; ++i) { - x0_per_seg[i + 1] = xf_per_seg[i]; - } - x0_per_seg[nseg - 1] = sf_a.get_rvmf(); - for (unsigned int i(0); i < nseg_bck - 1; ++i) { - x0_per_seg[(nseg - 1) - (i + 1)] = xf_per_seg[(nseg - 1) - i]; - } - - for (unsigned int i(0); i < dxdtof_per_seg.size(); ++i) { - std::array current_throttles - = {m_throttles[i * 3], m_throttles[i * 3 + 1], m_throttles[i * 3 + 2]}; - dxdtof_per_seg[i] = sf_a.get_state_derivative(x0_per_seg[i], current_throttles); - } - - xt::xarray xt_dxdx_per_seg = xt::zeros({5, 49}); - for (size_t col = 0; col < 49; ++col) { // Iterate over columns - for (size_t row = 0; row < 5; ++row) { // Iterate over rows - xt_dxdx_per_seg(row, col) = dxdx_per_seg[row][col]; - } - } - - // Create matrices from final states to previous states - // Fwd leg - auto M0 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 0, xt::all()), {7, 7}); - auto M1 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 1, xt::all()), {7, 7}); - auto M2 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 2, xt::all()), {7, 7}); - // Bck leg - auto M3 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 3, xt::all()), {7, 7}); - auto M4 = xt::reshape_view(xt::view(xt_dxdx_per_seg, 4, xt::all()), {7, 7}); - - // Create matrices from final states to throttles - xt::xarray xt_dxdu_per_seg = xt::adapt(reinterpret_cast(dxdu_per_seg.data()), {5, 21}); - // Fwd leg - auto U0 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 0, xt::all()), {7, 3}); - auto U1 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 1, xt::all()), {7, 3}); - auto U2 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 2, xt::all()), {7, 3}); - // Bck leg - auto U3 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 3, xt::all()), {7, 3}); - auto U4 = xt::reshape_view(xt::view(xt_dxdu_per_seg, 4, xt::all()), {7, 3}); - - // Create matrices from final states to throttles - xt::xarray xt_dxdtof_per_seg = xt::adapt(reinterpret_cast(dxdtof_per_seg.data()), {5, 7}); - // Fwd leg - auto f0 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 0, xt::all()), {7, 1}); - auto f1 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 1, xt::all()), {7, 1}); - auto f2 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 2, xt::all()), {7, 1}); - // Bck leg - auto f5 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 3, xt::all()), - {7, 1}); // f5 is because we don't care about f3 and f4 and the mc - auto f6 = xt::reshape_view(xt::view(xt_dxdtof_per_seg, 4, xt::all()), {7, 1}); - - // Initial and final displacements - auto xt_a_dmc_dxs = xt::linalg::dot(xt::linalg::dot(M2, M1), M0); - auto xt_a_dmc_dxf = xt::linalg::dot(M3, M4); - // Throttle derivatives - auto xt_a_dmc_du0 = xt::linalg::dot(xt::linalg::dot(M2, M1), U0); - auto xt_a_dmc_du1 = xt::linalg::dot(M2, U1); - auto xt_a_dmc_du2 = U2; - auto xt_a_dmc_du3 = U3; - auto xt_a_dmc_du4 = xt::linalg::dot(M3, U4); - // ToF derivatives - auto xt_a_dmc_dtof0 = xt::linalg::dot(xt::linalg::dot(xt::linalg::dot(M2, M1), M0), f0); - auto xt_a_dmc_dtof1 = xt::linalg::dot(xt::linalg::dot(M2, M1), f1); - auto xt_a_dmc_dtof2 = xt::linalg::dot(M2, f2); - auto xt_a_dmc_dtof3 = xt::linalg::dot(M3, f5); - auto xt_a_dmc_dtof4 = xt::linalg::dot(xt::linalg::dot(M3, M4), f6); - auto xt_a_dmc_dtof - = (xt_a_dmc_dtof0 + xt_a_dmc_dtof1 + xt_a_dmc_dtof2 + xt_a_dmc_dtof3 + xt_a_dmc_dtof4) / nseg; - - return std::make_tuple(xt_a_dmc_dxs, xt_a_dmc_dxf, xt_a_dmc_du0, xt_a_dmc_du1, xt_a_dmc_du2, xt_a_dmc_du3, - xt_a_dmc_du4, xt_a_dmc_dtof); + std::array grad_rvm = {0}; + std::array grad_rvm_bck = {0}; + unsigned int nseg = static_cast(m_throttles.size()) / 3; + std::vector grad_final(static_cast(7) * (nseg * 3u + 1u), 0.); + std::tie(grad_rvm, grad_rvm_bck, grad_final) = sf_a.compute_mc_grad(); + auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); + auto xgrad_rvm_bck = xt::adapt(grad_rvm_bck, {7u, 7u}); + auto xgrad_final = xt::adapt(grad_final, {7u, nseg * 3u + 1u}); + + // Cast gradients into a single vector + std::vector gradient(7u * (7u + static_cast(nseg) * 3u + 1u + 7u), 0); + auto xgradient = xt::adapt(gradient, {7u, 7u + static_cast(nseg) * 3u + 1u + 7u}); + xt::view(xgradient, xt::all(), xt::range(0u, 7u)) = xt::view(xgrad_rvm, xt::all(), xt::all()); // dmc_dxs + xt::view(xgradient, xt::all(), xt::range(7u, 7u + nseg * 3u)) + = xt::view(xgrad_final, xt::all(), xt::range(0, nseg * 3u)); // throttles + xt::view(xgradient, xt::all(), xt::range(7u + nseg * 3u, 7u + nseg * 3u + 7u)) + = xt::view(xgrad_rvm_bck, xt::all(), xt::all()); // dmc_dxf + xt::view(xgradient, xt::all(), xt::range(7u + nseg * 3u + 7u, 7u + nseg * 3u + 7u + 1u)) + = xt::view(xgrad_final, xt::all(), xt::range(nseg * 3u, nseg * 3u + 1)); // tof + + return gradient; } + // Member attributes std::vector m_num_grad; - heyoka::taylor_adaptive m_new_ta; std::array m_fwd_final_state; std::array m_bck_final_state; std::array m_mc_manual; - std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; - std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; - double m_ms = 1; double m_mf = m_ms * 13 / 15; double m_isp = 1; double m_max_thrust = 1; double m_cut = 0.5; double m_mu = 1; - double m_tof = 1; - std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; std::vector m_thrusts; - double m_tol = 1e-16; const std::vector m_rvms = {m_rvs[0][0], m_rvs[0][1], m_rvs[0][2], m_rvs[1][0], m_rvs[1][1], m_rvs[1][2], m_ms}; diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index f86b9825..82dd14ae 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -200,6 +200,7 @@ TEST_CASE("compute_mismatch_constraints_test") } } +// Compare low-fidelity and high-fidelity methods with zero thrust (ought to be the same) TEST_CASE("compute_mismatch_constraints_test2") { @@ -229,6 +230,7 @@ TEST_CASE("compute_mismatch_constraints_test2") REQUIRE(std::abs((retval[6] - retval_lf[6]) / retval[6]) < 1e-14); } +// Compare high-fidelity method with manually calculated (direct heyoka interfacing) Taylor integration. TEST_CASE("compute_mismatch_constraints_test3") { @@ -257,37 +259,32 @@ TEST_CASE("compute_mismatch_constraints_test3") TEST_CASE("compute_mc_grad_test") { // Initialise unique test quantities - std::vector throttles - = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24}; - double cut = 0.6; - auto sf_test_object = sf_hf_test_object(throttles, cut); - - // Numerical gradient - std::vector num_grad = sf_test_object.compute_numerical_gradient(); - - xt::xarray xt_num_dmc_dxs, xt_num_dmc_dxf, xt_num_dmc_du0, xt_num_dmc_du1, xt_num_dmc_du2, xt_num_dmc_du3, - xt_num_dmc_du4, xt_num_dmc_dtof; - std::tie(xt_num_dmc_dxs, xt_num_dmc_dxf, xt_num_dmc_du0, xt_num_dmc_du1, xt_num_dmc_du2, xt_num_dmc_du3, - xt_num_dmc_du4, xt_num_dmc_dtof) - = sf_test_object.process_mc_numerical_gradient(num_grad); - - // Analytical gradient - xt::xarray xt_a_dmc_dxs, xt_a_dmc_dxf, xt_a_dmc_du0, xt_a_dmc_du1, xt_a_dmc_du2, xt_a_dmc_du3, xt_a_dmc_du4, - xt_a_dmc_dtof; - std::tie(xt_a_dmc_dxs, xt_a_dmc_dxf, xt_a_dmc_du0, xt_a_dmc_du1, xt_a_dmc_du2, xt_a_dmc_du3, xt_a_dmc_du4, - xt_a_dmc_dtof) - = sf_test_object.compute_analytical_gradient(); - - // Calculate analytical gradient - - REQUIRE(xt::linalg::norm(xt_num_dmc_dxs - xt_a_dmc_dxs) < 1e-9); // SC: The difference is like 4.56e-8 - REQUIRE(xt::linalg::norm(xt_num_dmc_dxf - xt_a_dmc_dxf) < 1e-9); - REQUIRE(xt::linalg::norm(xt_num_dmc_du0 - xt_a_dmc_du0) < 1e-9); - REQUIRE(xt::linalg::norm(xt_num_dmc_du1 - xt_a_dmc_du1) < 1e-9); - REQUIRE(xt::linalg::norm(xt_num_dmc_du2 - xt_a_dmc_du2) < 1e-9); - REQUIRE(xt::linalg::norm(xt_num_dmc_du3 - xt_a_dmc_du3) < 1e-9); - REQUIRE(xt::linalg::norm(xt_num_dmc_du4 - xt_a_dmc_du4) < 1e-9); - REQUIRE(xt::linalg::norm(xt_num_dmc_dtof - xt_a_dmc_dtof) < 1e-9); + std::vector throttles_full + = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, + 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34}; + std::array nseg_array = {1, 2, 5, 10}; + std::array cut_array = {0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0}; + std::vector throttles; + for (unsigned long nseg : nseg_array) { + for (double cut : cut_array) { + throttles + = std::vector(throttles_full.begin(), throttles_full.begin() + static_cast(nseg) * 3); + auto sf_test_object = sf_hf_test_object(throttles, cut); + + // Numerical gradient + std::vector num_grad = sf_test_object.compute_numerical_gradient(); + auto xt_num_gradients = xt::adapt(num_grad, {7u + nseg, 7u + 3u * nseg + 1u + 7u}); + auto xt_num_mc_gradients = xt::view(xt_num_gradients, xt::range(0, 7), xt::all()); + + // Analytical gradient + std::vector a_gradients = sf_test_object.compute_analytical_gradient(); + auto xt_a_gradients = xt::adapt(a_gradients, {7u, 7u + 3u * static_cast(nseg) + 1u + 7u}); + + REQUIRE(xt::linalg::norm(xt_num_mc_gradients - xt_a_gradients) + < 1e-8); // With the high fidelity gradient this is still the best we can achieve. The difference is + // like 4.56e-8 + } + } } TEST_CASE("compute_tc_grad_test") @@ -303,13 +300,8 @@ TEST_CASE("compute_tc_grad_test") // Numerical gradient std::vector num_grad = sf_test_object.compute_numerical_gradient(); - std::vector tc_num_grad(nseg * 15); - for (unsigned int i(0); i < nseg; ++i) { - // dtc_du - std::copy(std::next(num_grad.begin(), 7 + 30 * (i + 7)), std::next(num_grad.begin(), 22 + 30 * (i + 7)), - std::next(tc_num_grad.begin(), i * 15)); - } - xt::xarray xt_tc_num_grad = xt::adapt(reinterpret_cast(tc_num_grad.data()), {5, 15}); + auto xt_num_gradients = xt::adapt(num_grad, {7u + nseg, 30u}); + auto xt_num_tc_gradients = xt::view(xt_num_gradients, xt::range(7, 12), xt::range(7, 22)); // Calculate throttle constraint gradients kep3::leg::sims_flanagan_hf sf(sf_test_object.m_rvs, sf_test_object.m_ms, sf_test_object.m_throttles, @@ -317,9 +309,9 @@ TEST_CASE("compute_tc_grad_test") sf_test_object.m_max_thrust, sf_test_object.m_isp, sf_test_object.m_mu, sf_test_object.m_cut, 1e-16); std::vector tc_a_grad = sf.compute_tc_grad(); - xt::xarray xt_tc_a_grad = xt::adapt(reinterpret_cast(tc_a_grad.data()), {5, 15}); + auto xt_tc_a_grad = xt::adapt(tc_a_grad, {nseg, 3u * nseg}); - REQUIRE(xt::linalg::norm(xt_tc_num_grad - xt_tc_a_grad) < 1e-13); // SC: 1e-14 fails + REQUIRE(xt::linalg::norm(xt_num_tc_gradients - xt_tc_a_grad) < 1e-13); // 1e-14 fails } TEST_CASE("serialization_test") From d13fab78d58f8be07c2511ad04106197eddee9a5 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Mon, 7 Oct 2024 10:42:16 +0200 Subject: [PATCH 04/22] Updated taylor adaptive integrator attributes in sf_hf leg to be mutable for identical interface to sf leg. --- include/kep3/leg/sims_flanagan_hf.hpp | 10 +++++----- src/leg/sims_flanagan_hf.cpp | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 4024adfe..39b74cda 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -103,7 +103,7 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Compute constraints [[nodiscard]] std::array get_state_derivative(std::array state, std::array throttles) const; - [[nodiscard]] std::array compute_mismatch_constraints(); + [[nodiscard]] std::array compute_mismatch_constraints() const; [[nodiscard]] std::vector compute_throttle_constraints() const; [[nodiscard]] std::vector compute_constraints(); @@ -112,7 +112,7 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Compute all gradients w.r.t. all legs [[nodiscard]] std::tuple>, std::vector>, - std::vector>> compute_all_gradients(); + std::vector>> compute_all_gradients() const; // Process all gradients to retrieve relevant gradients (w.r.t. initial and final rvm state as well as w.r.t. // throttles and tof) @@ -123,7 +123,7 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Compute mismatch constraint gradients (w.r.t. initial and final rvm state as well as w.r.t. throttles and // tof) - [[nodiscard]] std::tuple, std::array, std::vector> compute_mc_grad(); + [[nodiscard]] std::tuple, std::array, std::vector> compute_mc_grad() const; // Compute throttle constraint gradients [[nodiscard]] std::vector compute_tc_grad() const; @@ -178,10 +178,10 @@ class kep3_DLL_PUBLIC sims_flanagan_hf unsigned m_nseg_bck = 1u; // We introduce ta from cache const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); - heyoka::taylor_adaptive m_tas = ta_cache; + mutable heyoka::taylor_adaptive m_tas = ta_cache; // Introduce variational ta from cache const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); - heyoka::taylor_adaptive m_tas_var = ta_var_cache; + mutable heyoka::taylor_adaptive m_tas_var = ta_var_cache; }; // Streaming operator for the class kep3::leg::sims_flanagan. diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 3d28d078..8add67c8 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -355,7 +355,7 @@ std::array sims_flanagan_hf::get_rvmf() const } // The core routines -std::array sims_flanagan_hf::compute_mismatch_constraints() +std::array sims_flanagan_hf::compute_mismatch_constraints() const { // General settings const double prop_seg_duration = (m_tof / m_nseg); @@ -426,7 +426,7 @@ std::vector sims_flanagan_hf::compute_throttle_constraints() const return retval; } -std::vector sims_flanagan_hf::compute_constraints() +std::vector sims_flanagan_hf::compute_constraints() { std::vector retval(7 + m_nseg, 0.); // Fitness @@ -445,7 +445,7 @@ std::vector sims_flanagan_hf::compute_constraints() return retval; } -std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector chromosome) +std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector chromosome) { std::array rvms; std::copy(chromosome.begin(), chromosome.begin() + 7, rvms.begin()); @@ -495,7 +495,7 @@ std::array sims_flanagan_hf::get_state_derivative(std::array>, std::vector>, std::vector>> -sims_flanagan_hf::compute_all_gradients() +sims_flanagan_hf::compute_all_gradients() const { // Initialise std::vector> xf_per_seg(m_nseg, {0}); @@ -707,7 +707,7 @@ sims_flanagan_hf::get_relevant_gradients(std::vector> &d return {std::move(grad_rvm), std::move(grad_rvm_bck), std::move(grad_final)}; } -std::tuple, std::array, std::vector> sims_flanagan_hf::compute_mc_grad() +std::tuple, std::array, std::vector> sims_flanagan_hf::compute_mc_grad() const { // Initialise std::vector> dxdx_per_seg; From 36ad9e89a6de379475b3d91e3a0db8ce5ae86bd3 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 11 Oct 2024 09:47:19 +0200 Subject: [PATCH 05/22] [WIP] Sims-Flanagan HF documentation and minor changes. --- doc/leg.rst | 3 + doc/notebooks/sims_flanagan_leg.ipynb | 6 +- include/kep3/leg/sims_flanagan_hf.hpp | 3 +- pykep/core.cpp | 325 ++++++++++++++++++-------- pykep/docstrings.cpp | 193 +++++++++++++++ pykep/docstrings.hpp | 24 ++ pykep/leg/__init__.py | 4 + pykep/plot/_sf_leg.py | 182 +++++++++++++++ pykep/plot/test.ipynb | 146 ++++++++++++ pykep/plot/test.py | 10 + src/leg/sims_flanagan.cpp | 3 +- src/leg/sims_flanagan_hf.cpp | 11 +- test/leg_sims_flanagan_hf_helpers.hpp | 2 +- 13 files changed, 805 insertions(+), 107 deletions(-) create mode 100644 pykep/plot/test.py diff --git a/doc/leg.rst b/doc/leg.rst index 599b9ab9..1c5b969d 100644 --- a/doc/leg.rst +++ b/doc/leg.rst @@ -7,3 +7,6 @@ Interplanetary transfer legs .. autoclass:: sims_flanagan :members: + +.. autoclass:: sims_flanagan_hf + :members: diff --git a/doc/notebooks/sims_flanagan_leg.ipynb b/doc/notebooks/sims_flanagan_leg.ipynb index c33e8a53..56033739 100644 --- a/doc/notebooks/sims_flanagan_leg.ipynb +++ b/doc/notebooks/sims_flanagan_leg.ipynb @@ -84,12 +84,12 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { - "image/png": "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NBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUVERESChoKLiIiIBA0FFxEREQkaCi4iIiISNBRcREREJGgouIiIiEjQUHARERGRoKHgIiIiIkFDwUX+v906IAEAAAAQ9P91OwJdIQBsiAsAsCEuAMCGuAAAG+ICAGyICwCwIS4AwIa4AAAb4gIAbIgLALAhLgDAhrgAABviAgBsiAsAsCEuAMCGuAAAG+ICAGyICwCwIS4AwIa4AAAb4gIAbIgLALAhLgDAhrgAABviAgBsiAsAsCEuAMCGuAAAG+ICAGyICwCwIS4AwEZaO9N8ToRr1QAAAABJRU5ErkJggg==", + "image/png": "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", "text/plain": [ "
" ] @@ -138,7 +138,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "undefined.undefined.undefined" } }, "nbformat": 4, diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 39b74cda..b030b204 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -65,9 +65,10 @@ class kep3_DLL_PUBLIC sims_flanagan_hf void set_mu(double mu); void set_cut(double cut); void set_tol(double tol); - void set_tas(heyoka::taylor_adaptive tas); void set_rvms(std::array rvms); void set_rvmf(std::array rvmf); + void set_tas(heyoka::taylor_adaptive tas); + void set_tas_var(heyoka::taylor_adaptive tas_var); void set_walking_rvm(std::array rvm); // Backwards-compatible setting function with rv and m states separately void set(const std::array, 2> &rvs, double ms, const std::vector &throttles, diff --git a/pykep/core.cpp b/pykep/core.cpp index fd64eb85..47ed3ecb 100644 --- a/pykep/core.cpp +++ b/pykep/core.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -43,7 +44,7 @@ namespace py = pybind11; namespace pk = pykep; -PYBIND11_MODULE(core, m) //NOLINT +PYBIND11_MODULE(core, m) // NOLINT { py::options options; options.disable_function_signatures(); @@ -461,104 +462,27 @@ PYBIND11_MODULE(core, m) //NOLINT // Exposing the sims_flanagan leg py::class_ sims_flanagan(m, "_sims_flanagan", pykep::leg_sf_docstring().c_str()); - sims_flanagan - .def(py::init, 2> &, double, std::vector, - const std::array, 2> &, double, double, double, double, double, double>(), - py::arg("rvs") = std::array, 2>{{{1., 0, 0.}, {0., 1., 0.}}}, py::arg("ms") = 1., - py::arg("throttles") = std::vector{0, 0, 0, 0, 0, 0}, - py::arg("rvf") = std::array, 2>{{{0., 1., 0.}, {-1., 0., 0.}}}, py::arg("mf") = 1., - py::arg("tof") = kep3::pi / 2, py::arg("max_thrust") = 1., py::arg("isp") = 1., py::arg("mu") = 1, - py::arg("cut") = 0.5) - // repr(). - .def("__repr__", &pykep::ostream_repr) - // Copy and deepcopy. - .def("__copy__", &pykep::generic_copy_wrapper) - .def("__deepcopy__", &pykep::generic_deepcopy_wrapper) - // Pickle support. - .def(py::pickle(&pykep::pickle_getstate_wrapper, - &pykep::pickle_setstate_wrapper)) - // The rest - .def_property( - "throttles", &kep3::leg::sims_flanagan::get_throttles, - [](kep3::leg::sims_flanagan &sf, const std::vector &throttles) { - return sf.set_throttles(throttles); - }, - pykep::leg_sf_throttles_docstring().c_str()) - .def("compute_mismatch_constraints", &kep3::leg::sims_flanagan::compute_mismatch_constraints, - pykep::leg_sf_mc_docstring().c_str()) - .def("compute_throttle_constraints", &kep3::leg::sims_flanagan::compute_throttle_constraints, - pykep::leg_sf_tc_docstring().c_str()) - .def( - "compute_mc_grad", - [](const kep3::leg::sims_flanagan &leg) { - auto tc_cpp = leg.compute_mc_grad(); - // Lets transfer ownership to python of the three - const std::array &rs_addr = std::get<0>(tc_cpp); - const std::array &rf_addr = std::get<1>(tc_cpp); - const std::vector &th_addr = std::get<2>(tc_cpp); - - // We create three separate capsules for the py::array_t to manage ownership change. - auto vec_ptr_rs = std::make_unique>(rs_addr); - py::capsule vec_caps_rs(vec_ptr_rs.get(), [](void *ptr) { - const std::unique_ptr> vptr(static_cast *>(ptr)); - }); - auto vec_ptr_rf = std::make_unique>(rf_addr); - py::capsule vec_caps_rf(vec_ptr_rf.get(), [](void *ptr) { - const std::unique_ptr> vptr(static_cast *>(ptr)); - }); - auto vec_ptr_th = std::make_unique>(th_addr); - py::capsule vec_caps_th(vec_ptr_th.get(), [](void *ptr) { - const std::unique_ptr> vptr(static_cast *>(ptr)); - }); - // NOTE: at this point, the capsules have been created successfully (including - // the registration of the destructor). We can thus release ownership from vec_ptr_xx, - // as now the capsules are responsible for destroying its contents. - auto *ptr_rs = vec_ptr_rs.release(); - auto *ptr_rf = vec_ptr_rf.release(); - auto *ptr_th = vec_ptr_th.release(); - auto rs_python = py::array_t( - py::array::ShapeContainer{static_cast(7), static_cast(7)}, // shape - ptr_rs->data(), std::move(vec_caps_rs)); - auto rf_python = py::array_t( - py::array::ShapeContainer{static_cast(7), static_cast(7)}, // shape - ptr_rf->data(), std::move(vec_caps_rf)); - auto th_python = py::array_t( - py::array::ShapeContainer{static_cast(7), - static_cast(leg.get_nseg() * 3 + 1u)}, // shape - ptr_th->data(), std::move(vec_caps_th)); - return py::make_tuple(rs_python, rf_python, th_python); - }, - pykep::leg_sf_mc_grad_docstring().c_str()) - .def( - "compute_tc_grad", - [](const kep3::leg::sims_flanagan &leg) { - const std::vector tc_cpp = leg.compute_tc_grad(); - // Lets transfer ownership to python - const std::vector &tc_cpp_addr = tc_cpp; - // We create a capsule for the py::array_t to manage ownership change. - auto vec_ptr = std::make_unique>(tc_cpp_addr); - py::capsule vec_caps(vec_ptr.get(), [](void *ptr) { - const std::unique_ptr> vptr(static_cast *>(ptr)); - }); - // NOTE: at this point, the capsule has been created successfully (including - // the registration of the destructor). We can thus release ownership from vec_ptr, - // as now the capsule is responsible for destroying its contents. If the capsule constructor - // throws, the destructor function is not registered/invoked, and the destructor - // of vec_ptr will take care of cleaning up. - auto *ptr = vec_ptr.release(); - - auto tc_python = py::array_t( - py::array::ShapeContainer{static_cast(leg.get_nseg()), - static_cast(leg.get_nseg() * 3)}, // shape - ptr->data(), std::move(vec_caps)); - return tc_python; - }, - pykep::leg_sf_tc_grad_docstring().c_str()) - .def_property_readonly("nseg", &kep3::leg::sims_flanagan::get_nseg, pykep::leg_sf_nseg_docstring().c_str()) - .def_property_readonly("nseg_fwd", &kep3::leg::sims_flanagan::get_nseg_fwd, - pykep::leg_sf_nseg_fwd_docstring().c_str()) - .def_property_readonly("nseg_bck", &kep3::leg::sims_flanagan::get_nseg_bck, - pykep::leg_sf_nseg_bck_docstring().c_str()); + sims_flanagan.def( + py::init, 2> &, double, std::vector, + const std::array, 2> &, double, double, double, double, double, double>(), + py::arg("rvs") = std::array, 2>{{{1., 0, 0.}, {0., 1., 0.}}}, py::arg("ms") = 1., + py::arg("throttles") = std::vector{0, 0, 0, 0, 0, 0}, + py::arg("rvf") = std::array, 2>{{{0., 1., 0.}, {-1., 0., 0.}}}, py::arg("mf") = 1., + py::arg("tof") = kep3::pi / 2, py::arg("max_thrust") = 1., py::arg("isp") = 1., py::arg("mu") = 1, + py::arg("cut") = 0.5); + // repr(). + sims_flanagan.def("__repr__", &pykep::ostream_repr); + // Copy and deepcopy. + sims_flanagan.def("__copy__", &pykep::generic_copy_wrapper); + sims_flanagan.def("__deepcopy__", &pykep::generic_deepcopy_wrapper); + // Pickle support. + sims_flanagan.def(py::pickle(&pykep::pickle_getstate_wrapper, + &pykep::pickle_setstate_wrapper)); + // The rest + sims_flanagan.def_property( + "throttles", &kep3::leg::sims_flanagan::get_throttles, + [](kep3::leg::sims_flanagan &sf, const std::vector &throttles) { return sf.set_throttles(throttles); }, + pykep::leg_sf_throttles_docstring().c_str()); #define PYKEP3_EXPOSE_LEG_SF_ATTRIBUTES(name) \ sims_flanagan.def_property(#name, &kep3::leg::sims_flanagan::get_##name, &kep3::leg::sims_flanagan::set_##name, \ @@ -572,4 +496,205 @@ PYBIND11_MODULE(core, m) //NOLINT PYKEP3_EXPOSE_LEG_SF_ATTRIBUTES(isp); PYKEP3_EXPOSE_LEG_SF_ATTRIBUTES(mu); PYKEP3_EXPOSE_LEG_SF_ATTRIBUTES(cut); -} + +#undef PYKEP3_EXPOSE_LEG_SF_ATTRIBUTES + + sims_flanagan.def("compute_mismatch_constraints", &kep3::leg::sims_flanagan::compute_mismatch_constraints, + pykep::leg_sf_mc_docstring().c_str()); + sims_flanagan.def("compute_throttle_constraints", &kep3::leg::sims_flanagan::compute_throttle_constraints, + pykep::leg_sf_tc_docstring().c_str()); + sims_flanagan.def( + "compute_mc_grad", + [](const kep3::leg::sims_flanagan &leg) { + auto tc_cpp = leg.compute_mc_grad(); + // Lets transfer ownership to python of the three + const std::array &rs_addr = std::get<0>(tc_cpp); + const std::array &rf_addr = std::get<1>(tc_cpp); + const std::vector &th_addr = std::get<2>(tc_cpp); + + // We create three separate capsules for the py::array_t to manage ownership change. + auto vec_ptr_rs = std::make_unique>(rs_addr); + py::capsule vec_caps_rs(vec_ptr_rs.get(), [](void *ptr) { + const std::unique_ptr> vptr(static_cast *>(ptr)); + }); + auto vec_ptr_rf = std::make_unique>(rf_addr); + py::capsule vec_caps_rf(vec_ptr_rf.get(), [](void *ptr) { + const std::unique_ptr> vptr(static_cast *>(ptr)); + }); + auto vec_ptr_th = std::make_unique>(th_addr); + py::capsule vec_caps_th(vec_ptr_th.get(), [](void *ptr) { + const std::unique_ptr> vptr(static_cast *>(ptr)); + }); + // NOTE: at this point, the capsules have been created successfully (including + // the registration of the destructor). We can thus release ownership from vec_ptr_xx, + // as now the capsules are responsible for destroying its contents. + auto *ptr_rs = vec_ptr_rs.release(); + auto *ptr_rf = vec_ptr_rf.release(); + auto *ptr_th = vec_ptr_th.release(); + auto rs_python = py::array_t( + py::array::ShapeContainer{static_cast(7), static_cast(7)}, // shape + ptr_rs->data(), std::move(vec_caps_rs)); + auto rf_python = py::array_t( + py::array::ShapeContainer{static_cast(7), static_cast(7)}, // shape + ptr_rf->data(), std::move(vec_caps_rf)); + auto th_python = py::array_t( + py::array::ShapeContainer{static_cast(7), + static_cast(leg.get_nseg() * 3 + 1u)}, // shape + ptr_th->data(), std::move(vec_caps_th)); + return py::make_tuple(rs_python, rf_python, th_python); + }, + pykep::leg_sf_mc_grad_docstring().c_str()); + sims_flanagan.def( + "compute_tc_grad", + [](const kep3::leg::sims_flanagan &leg) { + const std::vector tc_cpp = leg.compute_tc_grad(); + // Lets transfer ownership to python + const std::vector &tc_cpp_addr = tc_cpp; + // We create a capsule for the py::array_t to manage ownership change. + auto vec_ptr = std::make_unique>(tc_cpp_addr); + py::capsule vec_caps(vec_ptr.get(), [](void *ptr) { + const std::unique_ptr> vptr(static_cast *>(ptr)); + }); + // NOTE: at this point, the capsule has been created successfully (including + // the registration of the destructor). We can thus release ownership from vec_ptr, + // as now the capsule is responsible for destroying its contents. If the capsule constructor + // throws, the destructor function is not registered/invoked, and the destructor + // of vec_ptr will take care of cleaning up. + auto *ptr = vec_ptr.release(); + + auto tc_python + = py::array_t(py::array::ShapeContainer{static_cast(leg.get_nseg()), + static_cast(leg.get_nseg() * 3)}, // shape + ptr->data(), std::move(vec_caps)); + return tc_python; + }, + pykep::leg_sf_tc_grad_docstring().c_str()); + sims_flanagan.def_property_readonly("nseg", &kep3::leg::sims_flanagan::get_nseg, + pykep::leg_sf_nseg_docstring().c_str()); + sims_flanagan.def_property_readonly("nseg_fwd", &kep3::leg::sims_flanagan::get_nseg_fwd, + pykep::leg_sf_nseg_fwd_docstring().c_str()); + sims_flanagan.def_property_readonly("nseg_bck", &kep3::leg::sims_flanagan::get_nseg_bck, + pykep::leg_sf_nseg_bck_docstring().c_str()); + + // Exposing the sims_flanagan_hf leg + py::class_ sims_flanagan_hf(m, "_sims_flanagan_hf", + pykep::leg_sf_hf_docstring().c_str()); + sims_flanagan_hf.def( + py::init, 2> &, double, std::vector, + const std::array, 2> &, double, double, double, double, double, double>(), + py::arg("rvs") = std::array, 2>{{{1., 0, 0.}, {0., 1., 0.}}}, py::arg("ms") = 1., + py::arg("throttles") = std::vector{0, 0, 0, 0, 0, 0}, + py::arg("rvf") = std::array, 2>{{{0., 1., 0.}, {-1., 0., 0.}}}, py::arg("mf") = 1., + py::arg("tof") = kep3::pi / 2, py::arg("max_thrust") = 1., py::arg("isp") = 1., py::arg("mu") = 1, + py::arg("cut") = 0.5); + // repr(). + sims_flanagan_hf.def("__repr__", &pykep::ostream_repr); + // Copy and deepcopy. + sims_flanagan_hf.def("__copy__", &pykep::generic_copy_wrapper); + sims_flanagan_hf.def("__deepcopy__", &pykep::generic_deepcopy_wrapper); + // Pickle support. + sims_flanagan_hf.def(py::pickle(&pykep::pickle_getstate_wrapper, + &pykep::pickle_setstate_wrapper)); + // The rest + sims_flanagan_hf.def_property( + "throttles", &kep3::leg::sims_flanagan_hf::get_throttles, + [](kep3::leg::sims_flanagan_hf &sf, const std::vector &throttles) { + return sf.set_throttles(throttles); + }, + pykep::leg_sf_hf_throttles_docstring().c_str()); + +#define PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(name) \ + sims_flanagan_hf.def_property(#name, &kep3::leg::sims_flanagan_hf::get_##name, \ + &kep3::leg::sims_flanagan_hf::set_##name, \ + pykep::leg_sf_hf_##name##_docstring().c_str()); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(rvs); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(ms); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(rvf); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(mf); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tof); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(max_thrust); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(isp); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas_var); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(mu); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(cut); + +#undef PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES + + sims_flanagan_hf.def("compute_mismatch_constraints", &kep3::leg::sims_flanagan_hf::compute_mismatch_constraints, + pykep::leg_sf_hf_mc_docstring().c_str()); + sims_flanagan_hf.def("compute_throttle_constraints", &kep3::leg::sims_flanagan_hf::compute_throttle_constraints, + pykep::leg_sf_hf_tc_docstring().c_str()); + sims_flanagan_hf.def( + "compute_mc_grad", + [](const kep3::leg::sims_flanagan_hf &leg) { + auto tc_cpp = leg.compute_mc_grad(); + // Lets transfer ownership to python of the three + std::array &rs_addr = std::get<0>(tc_cpp); + std::array &rf_addr = std::get<1>(tc_cpp); + std::vector &th_addr = std::get<2>(tc_cpp); + + // We create three separate capsules for the py::array_t to manage ownership change. + auto vec_ptr_rs = std::make_unique>(rs_addr); + py::capsule vec_caps_rs(vec_ptr_rs.get(), [](void *ptr) { + std::unique_ptr> vptr(static_cast *>(ptr)); + }); + auto vec_ptr_rf = std::make_unique>(rf_addr); + py::capsule vec_caps_rf(vec_ptr_rf.get(), [](void *ptr) { + std::unique_ptr> vptr(static_cast *>(ptr)); + }); + auto vec_ptr_th = std::make_unique>(th_addr); + py::capsule vec_caps_th(vec_ptr_th.get(), [](void *ptr) { + std::unique_ptr> vptr(static_cast *>(ptr)); + }); + // NOTE: at this point, the capsules have been created successfully (including + // the registration of the destructor). We can thus release ownership from vec_ptr_xx, + // as now the capsules are responsible for destroying its contents. + auto *ptr_rs = vec_ptr_rs.release(); + auto *ptr_rf = vec_ptr_rf.release(); + auto *ptr_th = vec_ptr_th.release(); + auto rs_python = py::array_t( + py::array::ShapeContainer{static_cast(7), static_cast(7)}, // shape + ptr_rs->data(), std::move(vec_caps_rs)); + auto rf_python = py::array_t( + py::array::ShapeContainer{static_cast(7), static_cast(7)}, // shape + ptr_rf->data(), std::move(vec_caps_rf)); + auto th_python = py::array_t( + py::array::ShapeContainer{static_cast(7), + static_cast(leg.get_nseg() * 3 + 1u)}, // shape + ptr_th->data(), std::move(vec_caps_th)); + return py::make_tuple(rs_python, rf_python, th_python); + }, + pykep::leg_sf_hf_mc_grad_docstring().c_str()); + sims_flanagan_hf.def( + "compute_tc_grad", + [](const kep3::leg::sims_flanagan_hf &leg) { + std::vector tc_cpp = leg.compute_tc_grad(); + // Lets transfer ownership to python + std::vector &tc_cpp_addr = tc_cpp; + // We create a capsule for the py::array_t to manage ownership change. + auto vec_ptr = std::make_unique>(tc_cpp_addr); + py::capsule vec_caps(vec_ptr.get(), [](void *ptr) { + std::unique_ptr> vptr(static_cast *>(ptr)); + }); + // NOTE: at this point, the capsule has been created successfully (including + // the registration of the destructor). We can thus release ownership from vec_ptr, + // as now the capsule is responsible for destroying its contents. If the capsule constructor + // throws, the destructor function is not registered/invoked, and the destructor + // of vec_ptr will take care of cleaning up. + auto *ptr = vec_ptr.release(); + + auto tc_python + = py::array_t(py::array::ShapeContainer{static_cast(leg.get_nseg()), + static_cast(leg.get_nseg() * 3)}, // shape + ptr->data(), std::move(vec_caps)); + return tc_python; + }, + pykep::leg_sf_hf_tc_grad_docstring().c_str()); + sims_flanagan_hf.def_property_readonly("nseg", &kep3::leg::sims_flanagan_hf::get_nseg, + pykep::leg_sf_hf_nseg_docstring().c_str()); + sims_flanagan_hf.def_property_readonly("nseg_fwd", &kep3::leg::sims_flanagan_hf::get_nseg_fwd, + pykep::leg_sf_hf_nseg_fwd_docstring().c_str()); + sims_flanagan_hf.def_property_readonly("nseg_bck", &kep3::leg::sims_flanagan_hf::get_nseg_bck, + pykep::leg_sf_hf_nseg_bck_docstring().c_str()); +} \ No newline at end of file diff --git a/pykep/docstrings.cpp b/pykep/docstrings.cpp index 06b1c59c..a055e453 100644 --- a/pykep/docstrings.cpp +++ b/pykep/docstrings.cpp @@ -1847,4 +1847,197 @@ relative velocity (normalized), :math:`\hat{\mathbf b}_1 \times \mathbf v_{pla}` )"; }; +std::string leg_sf_hf_docstring() +{ + return R"(__init__(rvs = [[1,0,0], [0,1,0]], ms = 1., throttles = [0,0,0,0,0,0], rvf = [[0,1,0], [-1,0,0]], mf = 1., tof = pi/2, max_thrust = 1., isp = 1., mu=1., cut = 0.5, tol=1e-16) + + This class represents an interplanetary low-thrust transfer between a starting and a final point in the augmented state-space :math:`[\mathbf r, \mathbf v, m]`. + The low-thrust transfer is described by a sequence of two-body segments with a continuous and constant thrust defined per segment: + + Lantoine, Gregory & Russell, Ryan. (2009). The Stark Model: an exact, closed-form approach to low-thrust trajectory optimization. + + The low-thrust transfer will be feasible is the state mismatch equality constraints and the throttle mismatch inequality constraints are satisfied. + + Args: + *rvs* (2D array-like): Cartesian components of the initial position vector and velocity [[xs, ys, zs], [vxs, vys, vzs]]. Defaults to [[1,0,0], [0,1,0]]. + + *ms* (:class:`float`): initial mass. Defaults to 1. + + *throttles* (1D array-like): the Cartesan components of the throttle history [ux1, uy1, uz1, ux2, uy2, uz2, .....]. Defaults to a ballistic, two segments profile [0,0,0,0,0,0]. + + *rvf* (2D array-like): Cartesian components of the final position vector and velocity [[xf, yf, zf], [vxf, vyf, vzf]]. Defaults to [[0,1,0], [-1,0,0]]. + + *mf* (:class:`float`): final mass. Defaults to 1. + + *tof* (:class:`float`): time of flight. Defaults to :math:`\frac{\pi}{2}`. + + *max_thrust* (:class:`float`): maximum level for the spacecraft thrust. Defaults to 1. + + *isp* (:class:`float`): specific impulse of the propulasion system. Defaults to 1. + + *mu* (:class:`float`): gravitational parameter. Defaults to 1. + + *cut* (:class:`float`): the leg cut, in [0,1]. It determines the number of forward and backward segments. Defaults to 0.5. + + *tol* (:class:`float`): the leg tolerance, in [0,1]. It determines the tolerance allowed by the heyoka Taylor integrator. Defaults to 1e-16. + + .. note:: + + Units need to be consistent. + + Examples: + >>> import pykep as pk + >>> import numpy as np + >>> sf_hf = pk.leg.sims_flanagan_hf() +)"; +} +std::string leg_sf_hf_rvs_docstring() +{ + return "The initial position vector and velocity: [[xs, ys, zs], [vxs, vys, vzs]]."; +}; +std::string leg_sf_hf_ms_docstring() +{ + return "Initial mass."; +}; +std::string leg_sf_hf_throttles_docstring() +{ + return "The Cartesan components of the throttle history [ux1, uy1, uz1, ux2, uy2, uz2, .....]."; +}; +std::string leg_sf_hf_rvf_docstring() +{ + return "The final position vector and velocity: [[xs, ys, zs], [vxs, vys, vzs]]."; +}; +std::string leg_sf_hf_mf_docstring() +{ + return "Final mass."; +}; +std::string leg_sf_hf_tof_docstring() +{ + return "Time of flight."; +}; +std::string leg_sf_hf_max_thrust_docstring() +{ + return "Maximum spacecraft thruet."; +}; +std::string leg_sf_hf_isp_docstring() +{ + return "Specific impulse of the propulasion system"; +}; +std::string leg_sf_hf_mu_docstring() +{ + return "Central body gravitational parameter."; +}; +std::string leg_sf_hf_cut_docstring() +{ + return "The leg cut: it determines the number of forward and backward segments."; +}; +std::string leg_sf_hf_nseg_docstring() +{ + return "The total number of segments"; +}; +std::string leg_sf_hf_nseg_bck_docstring() +{ + return "The total number of backward segments"; +}; +std::string leg_sf_hf_nseg_fwd_docstring() +{ + return "The total number of forward segments"; +}; +std::string leg_sf_hf_tas_docstring() +{ + return "The Taylor integrator"; +}; +std::string leg_sf_hf_tas_var_docstring() +{ + return "The Taylor integrator with variational variables"; +}; +std::string leg_sf_hf_mc_docstring() +{ + return R"(compute_mismatch_constraints() + + In the Sims-Flanagan trajectory leg model, a forward propagation is performed from the starting state as well as a backward from the final state. + The state values thus computed need to match in some middle control point. This is typically imposed as 7 independent constraints called mismatch-constraints + computed by this method. + + Returns: + :class:`list` [:class:`float`]: The seven mismatch constraints in the same units used to construct the leg. + + Examples: + >>> import pykep as pk + >>> import numpy as np + >>> sf_hf = pk.leg.sims_flanagan_hf() + >>> sf_hf.compute_mismatch_constraints() +)"; +}; +std::string leg_sf_hf_tc_docstring() +{ + return R"(compute_throttle_constraints() + + In the Sims-Flanagan trajectory leg model implemented in pykep, we introduce the concept of throttles. Each throttle is defined by three numbers + :math:`[u_x, u_y, u_z] \in [0,1]` indicating that a certain component of the thrust vector has reached a fraction of its maximum allowed value. + As a consequence, along the segment along which the throttle is applied, the constraint :math:`u_x ^2 + u_y ^2 + u_z^2 = 1`, called a throttle constraint, + has to be met. + + Returns: + :class:`list` [:class:`float`]: The throttle constraints. + + Examples: + >>> import pykep as pk + >>> import numpy as np + >>> sf_hf = pk.leg.sims_flanagan_hf() + >> sf_hf.throttles = [0.8]*3 + >>> sf_hf.compute_throttle_constraints() +)"; +}; +std::string leg_sf_hf_mc_grad_docstring() +{ + return R"(compute_mc_grad() + +Computes the gradients of the mismatch constraints. Indicating the initial augmented state with :math:`\mathbf x_s = [\mathbf r_s, \mathbf v_s, m_s]`, the +final augmented state with :math:`\mathbf x_f = [\mathbf r_f, \mathbf v_f, m_f]`, the total time of flight with :math:`T` and the introducing the augmented throttle vector +:math:`\mathbf u = [u_{x0}, u_{y0}, u_{z0}, u_{x1}, u_{y1}, u_{z1} ..., T]` (note the time of flight at the end), this method computes the following gradients: + +.. math:: + \frac{\partial \mathbf {mc}}{\partial \mathbf x_s} + +.. math:: + \frac{\partial \mathbf {mc}}{\partial \mathbf x_f} + +.. math:: + \frac{\partial \mathbf {mc}}{\partial \mathbf u} + +Returns: + :class:`tuple` [:class:`numpy.ndarray`, :class:`numpy.ndarray`, :class:`numpy.ndarray`]: The three gradients. sizes will be (7,7), (7,7) and (7,nseg*3) + +Examples: + >>> import pykep as pk + >>> import numpy as np + >>> sf_hf = pk.leg.sims_flanagan_hf() + >> sf_hf.throttles = [0.8]*3 + >>> sf_hf.compute_mc_grad() +)"; +}; + +std::string leg_sf_hf_tc_grad_docstring() +{ + return R"(compute_tc_grad() + +Computes the gradients of the throttles constraints. Indicating the total time of flight with :math:`T` and introducing the augmented throttle vector +:math:`\mathbf u = [u_{x0}, u_{y0}, u_{z0}, u_{x1}, u_{y1}, u_{z1} ..., T]` (note the time of flight at the end), this method computes the following gradient: + +.. math:: + \frac{\partial \mathbf {tc}}{\partial \mathbf u} + +Returns: + :class:`tuple` [:class:`numpy.ndarray`]: The gradient. Size will be (nseg,nseg*3). + +Examples: + >>> import pykep as pk + >>> import numpy as np + >>> sf_hf = pk.leg.sims_flanagan_hf() + >> sf_hf.throttles = [0.8]*3 + >>> sf_hf.compute_tc_grad() +)"; +}; + } // namespace pykep \ No newline at end of file diff --git a/pykep/docstrings.hpp b/pykep/docstrings.hpp index a3163ccb..4ffcc40c 100644 --- a/pykep/docstrings.hpp +++ b/pykep/docstrings.hpp @@ -125,6 +125,30 @@ std::string leg_sf_nseg_docstring(); std::string leg_sf_nseg_fwd_docstring(); std::string leg_sf_nseg_bck_docstring(); +// LEG +// Sims Flanagan High-Fidelity +std::string leg_sf_hf_docstring(); +std::string leg_sf_hf_rvs_docstring(); +std::string leg_sf_hf_ms_docstring(); +std::string leg_sf_hf_throttles_docstring(); +std::string leg_sf_hf_rvf_docstring(); +std::string leg_sf_hf_mf_docstring(); +std::string leg_sf_hf_tof_docstring(); +std::string leg_sf_hf_max_thrust_docstring(); +std::string leg_sf_hf_isp_docstring(); +std::string leg_sf_hf_mu_docstring(); +std::string leg_sf_hf_cut_docstring(); +std::string leg_sf_hf_mc_docstring(); +std::string leg_sf_hf_tc_docstring(); +std::string leg_sf_hf_mc_grad_docstring(); +std::string leg_sf_hf_tc_grad_docstring(); +std::string leg_sf_hf_nseg_docstring(); +std::string leg_sf_hf_nseg_fwd_docstring(); +std::string leg_sf_hf_nseg_bck_docstring(); +std::string leg_sf_hf_tas_docstring(); +std::string leg_sf_hf_tas_var_docstring(); + + } // namespace pykep #endif \ No newline at end of file diff --git a/pykep/leg/__init__.py b/pykep/leg/__init__.py index fd82894b..8963bcf2 100644 --- a/pykep/leg/__init__.py +++ b/pykep/leg/__init__.py @@ -17,5 +17,9 @@ sims_flanagan.__name__ = "sims_flanagan" sims_flanagan.__module__ = "leg" +sims_flanagan_hf = _core._sims_flanagan_hf +sims_flanagan_hf.__name__ = "sims_flanagan_hf" +sims_flanagan_hf.__module__ = "leg" + # Removing core from the list of imported symbols. del _core diff --git a/pykep/plot/_sf_leg.py b/pykep/plot/_sf_leg.py index 3d101d95..f11bb0f1 100644 --- a/pykep/plot/_sf_leg.py +++ b/pykep/plot/_sf_leg.py @@ -186,3 +186,185 @@ def add_sf_leg( ) return ax + +def add_sf_hf_leg( + ax, + sf: _pk.leg.sims_flanagan_hf, + units=_pk.AU, + N=10, + show_gridpoints=False, + show_throttles=False, + length=0.1, + arrow_length_ratio=0.05, + **kwargs +): + """ + Add a trajectory leg of Sims-Flanagan problem to a 3D matplotlib Axes. + + Args: + *ax* (:class:`mpl_toolkits.mplot3d.axes3d.Axes3D`): The 3D Axes object to which the trajectory leg will be added. + + *sf* (:class:`~pykep.leg.sims_flanagan`): The Sims-Flanagan object containing relevant information. + + *units* (:class:`float`, optional): The unit conversion factor for plotting. Default is pk.AU. + + *N* (:class:`int`, optional): The number of points to generate along each segment of the trajectory. Default is 10. + + *show_gridpoints* (:class:`bool`, optional): If True, gridpoints of the trajectory are shown. Default is False. + + *show_throttles* (:class:`bool`, optional): If True, thrust vectors at midpoints are shown. Default is False. + + *length* (:class:`float`, optional): The length of the thrust vectors when show_throttles is True. Default is 0.1. + + *arrow_length_ratio* (:class:`float`, optional): The ratio of arrow length to the total length when show_throttles is True. Default is 0.05. + + *\*\*kwargs*: Additional keyword arguments to pass to the Axes3D.plot function. + + Notes: + - This function visualizes a Sims-Flanagan trajectory leg on the provided 3D Axes object. + - Midpoints, gridpoints, and thrust vectors can be optionally shown based on the provided parameters. + + Returns: + :class:`mpl_toolkits.mplot3d.axes3d.Axes3D`: The modified Axes object with the Sims-Flanagan leg added. + """ + # We extract the number of segments from the leg. + nseg = int(len(sf.throttles) / 3) + dt = sf.tof / nseg + c = sf.max_thrust * dt + nseg_fwd = int(nseg * sf.cut) + nseg_bck = nseg - nseg_fwd + + # We start the forward pass of the Sims-Flanagan model------------------------------------------------------------------------ + pos_fwd = [] + pos_m_fwd = [] + throttles_fwd = [] + rv = _deepcopy(sf.rvs) + mass_fwd = sf.ms + # Append to plotting data + pos_fwd.append(rv[0]) + + stark_integrator = _pk.ta.get_stark + for i in range(nseg_fwd): + # compute the dv + throttles = sf.throttles[3 * i : 3 * i + 3] + throttles_fwd.append(throttles) + dv = _np.linalg.norm(throttles) * c / mass_fwd + # plot it in a color that is proportional to the strength from royalblue to indianred + color = ( + 0.25 + (1. - 0.25) * min(1.0, _np.linalg.norm(throttles)), + 0.41 + (0. - 0.41) * min(1.0, _np.linalg.norm(throttles)), + 0.88 + (0. - 0.88) * min(1.0, _np.linalg.norm(throttles)), + ) + _pk.plot.add_ballistic_arc( + ax, rv, dt / 2, sf.mu, units=units, N=N, c=color, **kwargs + ) + # propagate for dt/2 + rv = list(_pk.propagate_lagrangian(rv, tof=dt / 2, mu=sf.mu, stm=False)) + # register the position as a mid-point state + pos_m_fwd.append(rv[0]) + # add dv to the state (now dimensional) + rv[1] = [a + b * c / mass_fwd for a, b in zip(rv[1], throttles)] + # update the mass (increases) + mass_fwd *= _np.exp(-dv / sf.isp / _pk.G0) + # 2 - propagate for dt/2 + _pk.plot.add_ballistic_arc( + ax, rv, dt / 2, sf.mu, units=units, N=N, c=color, **kwargs + ) + rv = _pk.propagate_lagrangian(rv, tof=dt / 2, mu=sf.mu, stm=False) + pos_fwd.append(rv[0]) + pos_fwd = _np.array(pos_fwd) + pos_m_fwd = _np.array(pos_m_fwd) + throttles_fwd = _np.array(throttles_fwd) + + # We plot optionally gridpoints, the low-thrust or the mid points + if show_gridpoints: + ax.plot( + pos_fwd[:, 0] / units, pos_fwd[:, 1] / units, pos_fwd[:, 2] / units, "k." + ) + if show_midpoints: + ax.plot( + pos_m_fwd[:, 0] / units, + pos_m_fwd[:, 1] / units, + pos_m_fwd[:, 2] / units, + "kx", + ) + if show_throttles: + ax.quiver( + pos_m_fwd[:, 0] / units, + pos_m_fwd[:, 1] / units, + pos_m_fwd[:, 2] / units, + throttles_fwd[:, 0], + throttles_fwd[:, 1], + throttles_fwd[:, 2], + length=length, + color="indianred", + arrow_length_ratio=arrow_length_ratio, + ) + + # We start the backward pass of the Sims-Flanagan model------------------------------------------------------------------------ + pos_bck = [] + pos_bck_m = [] + throttles_bck = [] + rv = _deepcopy(sf.rvf) + mass_bck = sf.mf + # Append to plotting data + pos_bck.append(rv[0]) + + for i in range(nseg_bck): + # compute the dv (first non dimensional) + throttles = sf.throttles[nseg*3-3-3*i:nseg*3-3*i] + throttles_bck.append(throttles) + dv = _np.linalg.norm(throttles) * c / mass_bck + # plot it in a color that is proportional to the strength + color = ( + 0.25 + (0.80 - 0.25) * min(1.0, _np.linalg.norm(throttles)), + 0.41 + (0.36 - 0.41) * min(1.0, _np.linalg.norm(throttles)), + 0.88 + (0.36 - 0.88) * min(1.0, _np.linalg.norm(throttles)), + ) + _pk.plot.add_ballistic_arc( + ax, rv, -dt / 2, sf.mu, units=units, N=N, c=color, **kwargs + ) + # propagate for dt/2 + rv = list(_pk.propagate_lagrangian(rv, tof=-dt / 2, mu=sf.mu, stm=False)) + # register the position as a mid-point state + pos_bck_m.append(rv[0]) + # add it to the state (now dimensional) + rv[1] = [a - b * c / mass_bck for a, b in zip(rv[1], throttles)] + # update the mass + mass_bck *= _np.exp(dv / sf.isp / _pk.G0) + # 2 - propagate for dt/2 + _pk.plot.add_ballistic_arc( + ax, rv, -dt / 2, sf.mu, units=units, N=N, c=color, **kwargs + ) + rv = _pk.propagate_lagrangian(rv, tof=-dt / 2, mu=sf.mu, stm=False) + pos_bck.append(rv[0]) + pos_bck = _np.array(pos_bck) + pos_bck_m = _np.array(pos_bck_m) + throttles_bck = _np.array(throttles_bck) + + # We plot optionally gridpoints, the low-thrust or the mid points + if show_gridpoints: + ax.plot( + pos_bck[:, 0] / units, pos_bck[:, 1] / units, pos_bck[:, 2] / units, "k." + ) + if show_midpoints: + ax.plot( + pos_bck_m[:, 0] / units, + pos_bck_m[:, 1] / units, + pos_bck_m[:, 2] / units, + "kx", + ) + if show_throttles: + ax.quiver( + pos_bck_m[:, 0] / units, + pos_bck_m[:, 1] / units, + pos_bck_m[:, 2] / units, + throttles_bck[:, 0], + throttles_bck[:, 1], + throttles_bck[:, 2], + length=length, + color="indianred", + arrow_length_ratio=arrow_length_ratio, + ) + + return ax \ No newline at end of file diff --git a/pykep/plot/test.ipynb b/pykep/plot/test.ipynb index e69de29b..d5000544 100644 --- a/pykep/plot/test.ipynb +++ b/pykep/plot/test.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "import pykep as pk\n", + "import numpy as np\n", + "import heyoka as hy\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "TaylorIntegrator = hy.taylor_adaptive" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Unable to convert function return value to a Python type! The signature was\n\t(tol: float = 1e-16) -> heyoka::v29::taylor_adaptive", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;31mTypeError\u001b[0m: Unregistered type : heyoka::v29::taylor_adaptive", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[23], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m ta \u001b[38;5;241m=\u001b[39m \u001b[43mpk\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mta\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_stark\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtol\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1e-16\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(ta)\n\u001b[1;32m 3\u001b[0m ta\u001b[38;5;241m.\u001b[39mtime \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0.\u001b[39m\n", + "\u001b[0;31mTypeError\u001b[0m: Unable to convert function return value to a Python type! The signature was\n\t(tol: float = 1e-16) -> heyoka::v29::taylor_adaptive" + ] + } + ], + "source": [ + "ta : TaylorIntegrator = pk.ta.get_stark(tol = 1e-16)\n", + "# print(ta)\n", + "# ta.time = 0.\n", + "# ta.state[:] = [1.,0.,0.,0.,1.,0.,1.]\n", + "# mu = 1.\n", + "# veff = 1.\n", + "# thrust = [0., 0., 0.]\n", + "# tof = 1.\n", + "# ta.pars[:] = [mu, veff] + thrust\n", + "# # print(ta.propagate_until(tof))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.5403023058681397, 0.8414709848078965, 0.0, -0.8414709848078965, 0.5403023058681397, 0.0, 1.0]\n" + ] + } + ], + "source": [ + "\n", + "import pykep as pk\n", + "import numpy as np\n", + "# mu = pk.MU_SUN\n", + "mu = 1\n", + "# veff = 3000. * pk.G0\n", + "veff = 1\n", + "tol = 1e-14\n", + "sp = pk.stark_problem(mu, veff, tol)\n", + "print(sp.propagate(rvm_state = [1., 0., 0., 0., 1., 0., 1], thrust = [0., 0., 0], tof=1.00))\n", + "# [0.5089647068650076, 0.8607873878989034, 0.0, -0.8607873878989032, 0.5089647068650074, 0.0, 1.0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "\n", + "# Making the axis\n", + "ax = pk.plot.make_3Daxis(figsize = (5,5))\n", + "\n", + "# Adding the Sun and the Earth for added coolness and reference.\n", + "pk.plot.add_sun(ax, s=40)\n", + "pk.plot.add_planet_orbit(ax, earth, c = 'black', alpha=0.3)\n", + "\n", + "# And plotting the leg (ballistic)\n", + "pk.plot.add_sf_leg(ax, sf, units=1., show_throttles=True, length=0.1, arrow_length_ratio=0.2)\n", + "\n", + "# And plotting the leg (random thrust)\n", + "sf.throttles = 1 - 2*np.random.random((nseg*3, ))\n", + "pk.plot.add_sf_leg(ax, sf, units=1., show_throttles=True, length=0.1, arrow_length_ratio=0.2)\n", + "\n", + "ax.view_init(90,270)\n", + "ax.set_xlim3d(-0.2,1)\n", + "ax.set_ylim3d(-0.2,1)\n", + "ax.axis('off')\n", + "\n", + "ax.set_title(\"A ballistic and a random Sims-Flanagan leg\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stark_integrator = pk.ta.get_stark\n", + "stark_integrator.propagate_grid(2)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "kep3_devel", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pykep/plot/test.py b/pykep/plot/test.py new file mode 100644 index 00000000..bd7511b5 --- /dev/null +++ b/pykep/plot/test.py @@ -0,0 +1,10 @@ +import pykep as pk +import numpy as np + +# Earth +udpla = pk.udpla.jpl_lp(body="EARTH") +earth = pk.planet(udpla) + +sf = pk.leg.sims_flanagan() +nseg = 20 +sf.throttles=[0,0,0] * nseg \ No newline at end of file diff --git a/src/leg/sims_flanagan.cpp b/src/leg/sims_flanagan.cpp index c9e4d1bc..3c55e3cd 100644 --- a/src/leg/sims_flanagan.cpp +++ b/src/leg/sims_flanagan.cpp @@ -69,7 +69,8 @@ void sims_flanagan::set_ms(double mass) } void sims_flanagan::set_throttles(std::vector throttles) { - _check_throttles(throttles, m_nseg); + auto nseg = static_cast(throttles.size()) / 3u; + _check_throttles(throttles, nseg); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 8add67c8..6eb8dbb6 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -139,7 +139,8 @@ void sims_flanagan_hf::set_ms(double mass) } void sims_flanagan_hf::set_throttles(std::vector throttles) { - _check_throttles(throttles, m_nseg); + auto nseg = static_cast(throttles.size()) / 3u; + _check_throttles(throttles, nseg); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); @@ -201,6 +202,14 @@ void sims_flanagan_hf::set_rvmf(std::array rvmf) { m_rvmf = rvmf; } +void sims_flanagan_hf::set_tas(heyoka::taylor_adaptive tas) +{ + m_tas = tas; +} +void sims_flanagan_hf::set_tas_var(heyoka::taylor_adaptive tas_var) +{ + m_tas_var = tas_var; +} void sims_flanagan_hf::set(const std::array, 2> &rvs, double ms, const std::vector &throttles, diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp index 1ed92e9a..55701946 100644 --- a/test/leg_sims_flanagan_hf_helpers.hpp +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -99,7 +99,7 @@ struct sf_hf_test_object { return m_mc_manual; }; - [[nodiscard]] void set_cut(double cut) + void set_cut(double cut) { m_cut = cut; } From e63651258579cc4a1b3cb15856145de2c4943553 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 11 Oct 2024 17:29:10 +0200 Subject: [PATCH 06/22] Added python tests for sims flanagan low and high fidelity. Small bug fix with setting throttles. Resolved minor compiler complaints. --- pykep/core.cpp | 3 + pykep/docstrings.cpp | 12 ++ pykep/docstrings.hpp | 3 + pykep/plot/test.py | 10 -- pykep/test.py | 204 ++++++++++++++++++++++++++ src/leg/sims_flanagan_hf.cpp | 5 + test/leg_sims_flanagan_hf_helpers.hpp | 35 ++--- 7 files changed, 245 insertions(+), 27 deletions(-) delete mode 100644 pykep/plot/test.py diff --git a/pykep/core.cpp b/pykep/core.cpp index 47ed3ecb..0e903f01 100644 --- a/pykep/core.cpp +++ b/pykep/core.cpp @@ -608,8 +608,10 @@ PYBIND11_MODULE(core, m) // NOLINT &kep3::leg::sims_flanagan_hf::set_##name, \ pykep::leg_sf_hf_##name##_docstring().c_str()); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(rvs); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(rvms); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(ms); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(rvf); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(rvmf); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(mf); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tof); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(max_thrust); @@ -618,6 +620,7 @@ PYBIND11_MODULE(core, m) // NOLINT PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas_var); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(mu); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(cut); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tol); #undef PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES diff --git a/pykep/docstrings.cpp b/pykep/docstrings.cpp index a055e453..ce052275 100644 --- a/pykep/docstrings.cpp +++ b/pykep/docstrings.cpp @@ -1899,6 +1899,10 @@ std::string leg_sf_hf_ms_docstring() { return "Initial mass."; }; +std::string leg_sf_hf_rvms_docstring() +{ + return "The initial position vector, velocity, and mass: [xs, ys, zs, vxs, vys, vzs, ms]."; +}; std::string leg_sf_hf_throttles_docstring() { return "The Cartesan components of the throttle history [ux1, uy1, uz1, ux2, uy2, uz2, .....]."; @@ -1911,6 +1915,10 @@ std::string leg_sf_hf_mf_docstring() { return "Final mass."; }; +std::string leg_sf_hf_rvmf_docstring() +{ + return "The final position vector, velocity, and mass: [xf, yf, zf, vxf, vyf, vzf, mf]."; +}; std::string leg_sf_hf_tof_docstring() { return "Time of flight."; @@ -1931,6 +1939,10 @@ std::string leg_sf_hf_cut_docstring() { return "The leg cut: it determines the number of forward and backward segments."; }; +std::string leg_sf_hf_tol_docstring() +{ + return "The tolerance of the Taylor adaptive integrator."; +}; std::string leg_sf_hf_nseg_docstring() { return "The total number of segments"; diff --git a/pykep/docstrings.hpp b/pykep/docstrings.hpp index 4ffcc40c..df197d40 100644 --- a/pykep/docstrings.hpp +++ b/pykep/docstrings.hpp @@ -130,14 +130,17 @@ std::string leg_sf_nseg_bck_docstring(); std::string leg_sf_hf_docstring(); std::string leg_sf_hf_rvs_docstring(); std::string leg_sf_hf_ms_docstring(); +std::string leg_sf_hf_rvms_docstring(); std::string leg_sf_hf_throttles_docstring(); std::string leg_sf_hf_rvf_docstring(); std::string leg_sf_hf_mf_docstring(); +std::string leg_sf_hf_rvmf_docstring(); std::string leg_sf_hf_tof_docstring(); std::string leg_sf_hf_max_thrust_docstring(); std::string leg_sf_hf_isp_docstring(); std::string leg_sf_hf_mu_docstring(); std::string leg_sf_hf_cut_docstring(); +std::string leg_sf_hf_tol_docstring(); std::string leg_sf_hf_mc_docstring(); std::string leg_sf_hf_tc_docstring(); std::string leg_sf_hf_mc_grad_docstring(); diff --git a/pykep/plot/test.py b/pykep/plot/test.py deleted file mode 100644 index bd7511b5..00000000 --- a/pykep/plot/test.py +++ /dev/null @@ -1,10 +0,0 @@ -import pykep as pk -import numpy as np - -# Earth -udpla = pk.udpla.jpl_lp(body="EARTH") -earth = pk.planet(udpla) - -sf = pk.leg.sims_flanagan() -nseg = 20 -sf.throttles=[0,0,0] * nseg \ No newline at end of file diff --git a/pykep/test.py b/pykep/test.py index 88605aae..c943c1cd 100644 --- a/pykep/test.py +++ b/pykep/test.py @@ -292,6 +292,205 @@ def test_stark(self): self.assertTrue(np.allclose(rvm[:3], r_gt, atol=1e-13)) self.assertTrue(np.allclose(rvm[3:6], v_gt, atol=1e-13)) +def compute_numerical_gradient(sf_leg, sf_leg_type = 'lf'): + import numpy as np + import pykep as pk + import pygmo as pg + + state_length = np.array(sf_leg.rvs).flatten().size + 1 + throttle_length = np.array(sf_leg.throttles).size + chromosome = np.zeros((state_length * 2 + throttle_length + 1)) + chromosome[0:state_length] = np.append(np.array(sf_leg.rvs).flatten(), sf_leg.ms) + chromosome[state_length:state_length+throttle_length] = np.array(sf_leg.throttles) + chromosome[state_length+throttle_length:state_length*2+throttle_length] = np.append(np.array(sf_leg.rvf).flatten(), sf_leg.mf) + chromosome[-1] = sf_leg.tof + + def set_and_compute_constraints(chromosome, sf_leg_type = 'lf'): + + if sf_leg_type == 'hf' or sf_leg_type == 'high-fidelity': + sf_leg_constraint = pk.leg.sims_flanagan_hf() + else: + sf_leg_constraint = pk.leg.sims_flanagan() + sf_leg_constraint.cut = 0.5 + sf_leg_constraint.max_thrust = 1 + sf_leg_constraint.mu = 1 + sf_leg_constraint.isp = 1 + sf_leg_constraint.rvs = [chromosome[0:3],chromosome[3:6]] + sf_leg_constraint.ms = chromosome[6] + sf_leg_constraint.throttles = chromosome[state_length:state_length+throttle_length] + sf_leg_constraint.rvf = [chromosome[state_length+throttle_length:state_length+throttle_length+3],chromosome[state_length+throttle_length+3:state_length+throttle_length+6]] + sf_leg_constraint.mf = chromosome[2*state_length+throttle_length-1] + sf_leg_constraint.tof = chromosome[2*state_length+throttle_length] + eq_con = sf_leg_constraint.compute_mismatch_constraints() + ineq_con = sf_leg_constraint.compute_throttle_constraints() + return np.concatenate((eq_con, ineq_con)) + + return pg.estimate_gradient_h(callable = lambda x : set_and_compute_constraints(x, sf_leg_type), x=chromosome) + +class sims_flanagan_test(_ut.TestCase): + + + def test_sims_flanagan(self): + import numpy as np + + udpla_e = pk.udpla.vsop2013("earth_moon", 1e-2) + udpla_j = pk.udpla.vsop2013("jupiter", 1e-2) + earth = pk.planet(udpla_e) + jupiter = pk.planet(udpla_j) + dt_days = 1000 + dt = dt_days * pk.DAY2SEC + t0 = 1233.3 + rv0 = earth.eph(t0) + rv1 = jupiter.eph(t0 + dt_days) + lp = pk.lambert_problem(rv0[0], rv1[0], dt, pk.MU_SUN) + rv0[1] = lp.v0[0] + rv1[1] = lp.v1[0] + + cut_values = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + mc_list = [] + for i in range(1, 34): + for cut in cut_values: + throttles = [0.0] * i * 3 + sf_hf_leg = pk.leg.sims_flanagan(rv0, 1.0, throttles, rv1, 1.0, dt, 1.0, 1.0, pk.MU_SUN, cut) + mc = sf_hf_leg.compute_mismatch_constraints() + mc[0] /= pk.AU + mc[1] /= pk.AU + mc[2] /= pk.AU + mc[3] /= pk.EARTH_VELOCITY + mc[4] /= pk.EARTH_VELOCITY + mc[5] /= pk.EARTH_VELOCITY + mc[6] /= 1000 + mc_list.append(mc) + + self.assertTrue(np.array([np.max(i) < 1e-8 for i in mc_list]).all()) + + def test_mc_grad(self): + import numpy as np + + sf_leg = pk.leg.sims_flanagan() + sf_leg.cut = 0.5 + sf_leg.throttles = np.array([0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, + 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34]) + sf_leg.rvs = np.array([[1, 0.1, -0.1], [0.2, 1.0, -0.2]]) + sf_leg.ms = 1 + sf_leg.rvf = np.array([[1.2, -0.1, 0.1], [-0.2, 1.023, -0.44]]) + sf_leg.mf = 13 / 15 + sf_leg.max_thrust = 1 + sf_leg.mu = 1 + sf_leg.isp = 1 + sf_leg.tof = 1 + #sf_leg.tol = 1e-16 + state_length = np.array(sf_leg.rvs).flatten().size + 1 + throttle_length = np.array(sf_leg.throttles).size + + num_grad = compute_numerical_gradient(sf_leg, sf_leg_type = 'lf') + num_grad = num_grad.reshape((17, 45), order='C') + grad_rvm, grad_rvm_bck, grad_final = sf_leg.compute_mc_grad() + a_tc_grad = sf_leg.compute_tc_grad() + a_grad = np.zeros((state_length+throttle_length // 3, 2 * state_length + throttle_length + 1)) + a_grad[0:state_length, 0:state_length] = grad_rvm + a_grad[0:state_length, state_length:state_length + throttle_length] = grad_final[:,0:throttle_length] + a_grad[0:state_length, state_length+throttle_length:state_length*2+throttle_length] = grad_rvm_bck + a_grad[0:state_length, state_length*2+throttle_length] = grad_final[:, throttle_length:throttle_length + 1].reshape(7,) + a_grad[state_length:, state_length:state_length+throttle_length] = a_tc_grad + self.assertTrue(np.allclose(num_grad, a_grad, atol=1e-8)) + +class sims_flanagan_hf_test(_ut.TestCase): + def test_comparison_sf_and_sf_hf(self): + import pykep as pk + import numpy as np + + sf_leg = pk.leg.sims_flanagan() + sf_leg.cut = 0.5 + sf_leg.throttles = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + sf_leg.rvs = np.array([[1, 0.1, -0.1], [0.2, 1.0, -0.2]]) + sf_leg.ms = 1 + sf_leg.rvf = np.array([[1.2, -0.1, 0.1], [-0.2, 1.023, -0.44]]) + sf_leg.mf = 13 / 15 + sf_leg.max_thrust = 1 + sf_leg.mu = 1 + sf_leg.isp = 1 + sf_leg.tof = 1 + rvm_mc_sf = sf_leg.compute_mismatch_constraints() + + sf_hf_leg = pk.leg.sims_flanagan_hf() + sf_hf_leg.cut = 0.5 + sf_hf_leg.throttles = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + sf_hf_leg.rvms = np.array([1, 0.1, -0.1, 0.2, 1.0, -0.2, 1]) + sf_hf_leg.rvmf = np.array([1.2, -0.1, 0.1, -0.2, 1.023, -0.44, 13 / 15]) + sf_hf_leg.max_thrust = 1 + sf_hf_leg.mu = 1 + sf_hf_leg.isp = 1 + sf_hf_leg.tof = 1 + rvm_mc_sf_hf = sf_hf_leg.compute_mismatch_constraints() + self.assertTrue(np.allclose(rvm_mc_sf, rvm_mc_sf_hf, atol=1e-13)) + + def test_sims_flanagan_hf(self): + import numpy as np + + udpla_e = pk.udpla.vsop2013("earth_moon", 1e-2) + udpla_j = pk.udpla.vsop2013("jupiter", 1e-2) + earth = pk.planet(udpla_e) + jupiter = pk.planet(udpla_j) + dt_days = 1000 + dt = dt_days * pk.DAY2SEC + t0 = 1233.3 + rv0 = earth.eph(t0) + rv1 = jupiter.eph(t0 + dt_days) + lp = pk.lambert_problem(rv0[0], rv1[0], dt, pk.MU_SUN) + rv0[1] = lp.v0[0] + rv1[1] = lp.v1[0] + + + cut_values = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + mc_list = [] + for i in range(1, 34): + for cut in cut_values: + throttles = [0.0] * i * 3 + sf_hf_leg = pk.leg.sims_flanagan_hf(rv0, 1.0, throttles, rv1, 1.0, dt, 1.0, 1.0, pk.MU_SUN, cut) + mc = sf_hf_leg.compute_mismatch_constraints() + mc[0] /= pk.AU + mc[1] /= pk.AU + mc[2] /= pk.AU + mc[3] /= pk.EARTH_VELOCITY + mc[4] /= pk.EARTH_VELOCITY + mc[5] /= pk.EARTH_VELOCITY + mc[6] /= 1000 + mc_list.append(mc) + + self.assertTrue(np.array([np.max(i) < 1e-8 for i in mc_list]).all()) + + def test_mc_grad_hf(self): + import numpy as np + + sf_leg = pk.leg.sims_flanagan_hf() + sf_leg.cut = 0.5 + sf_leg.throttles = np.array([0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, + 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34]) + sf_leg.rvs = np.array([[1, 0.1, -0.1], [0.2, 1.0, -0.2]]) + sf_leg.ms = 1 + sf_leg.rvf = np.array([[1.2, -0.1, 0.1], [-0.2, 1.023, -0.44]]) + sf_leg.mf = 13 / 15 + sf_leg.max_thrust = 1 + sf_leg.mu = 1 + sf_leg.isp = 1 + sf_leg.tof = 1 + state_length = np.array(sf_leg.rvs).flatten().size + 1 + throttle_length = np.array(sf_leg.throttles).size + + num_grad = compute_numerical_gradient(sf_leg, sf_leg_type = 'hf') + num_grad = num_grad.reshape((17, 45), order='C') + grad_rvm, grad_rvm_bck, grad_final = sf_leg.compute_mc_grad() + a_tc_grad = sf_leg.compute_tc_grad() + a_grad = np.zeros((state_length+throttle_length // 3, 2 * state_length + throttle_length + 1)) + a_grad[0:state_length, 0:state_length] = grad_rvm + a_grad[0:state_length, state_length:state_length + throttle_length] = grad_final[:,0:throttle_length] + a_grad[0:state_length, state_length+throttle_length:state_length*2+throttle_length] = grad_rvm_bck + a_grad[0:state_length, state_length*2+throttle_length] = grad_final[:, throttle_length:throttle_length + 1].reshape(7,) + a_grad[state_length:, state_length:state_length+throttle_length] = a_tc_grad + self.assertTrue(np.allclose(num_grad, a_grad, atol=1e-8)) + + def run_test_suite(): tl = _ut.TestLoader() @@ -311,6 +510,11 @@ def run_test_suite(): suite.addTest(epoch_test("test_epoch_operators")) suite.addTest(propagate_test("test_lagrangian")) suite.addTest(propagate_test("test_stark")) + suite.addTest(sims_flanagan_test("test_sims_flanagan")) + suite.addTest(sims_flanagan_test("test_mc_grad")) + suite.addTest(sims_flanagan_hf_test("test_comparison_sf_and_sf_hf")) + suite.addTest(sims_flanagan_hf_test("test_sims_flanagan_hf")) + suite.addTest(sims_flanagan_hf_test("test_mc_grad_hf")) suite.addTest(py_udplas_test("test_tle")) suite.addTest(py_udplas_test("test_spice")) suite.addTest(trajopt_mga_tests("test_construction")) diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 6eb8dbb6..b6db487f 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -145,6 +145,11 @@ void sims_flanagan_hf::set_throttles(std::vector throttles) m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; + + // Convert throttles to current_thrusts. + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } void sims_flanagan_hf::set_throttles(std::vector::const_iterator it1, std::vector::const_iterator it2) { diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp index 55701946..b040c2aa 100644 --- a/test/leg_sims_flanagan_hf_helpers.hpp +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -10,6 +10,7 @@ #ifndef kep3_TEST_LEG_SIMS_FLANAGAN_HF_HELPERS_H #define kep3_TEST_LEG_SIMS_FLANAGAN_HF_HELPERS_H +#include #include #include @@ -48,27 +49,27 @@ struct sf_hf_test_object { // Default constructor sf_hf_test_object() = default; - sf_hf_test_object(std::vector throttles) : m_throttles(throttles) + explicit sf_hf_test_object(std::vector &throttles) : m_throttles(throttles) { - for (unsigned int i(0); i < m_throttles.size(); ++i) { - m_thrusts.push_back(m_throttles[i] * m_max_thrust); + for (double m_throttle : m_throttles) { + m_thrusts.push_back(m_throttle * m_max_thrust); } } - sf_hf_test_object(double cut) : m_cut(cut) {} + explicit sf_hf_test_object(double cut) : m_cut(cut) {} - sf_hf_test_object(std::vector throttles, double cut) : m_cut(cut), m_throttles(throttles) + sf_hf_test_object(std::vector &throttles, double cut) : m_cut(cut), m_throttles(throttles) { - for (unsigned int i(0); i < m_throttles.size(); ++i) { - m_thrusts.push_back(m_throttles[i] * m_max_thrust); + for (double m_throttle : m_throttles) { + m_thrusts.push_back(m_throttle * m_max_thrust); } } // Retrieve mismatch constraints from manual heyoka Taylor adaptive integrator [[nodiscard]] std::array compute_manual_mc() { - for (unsigned int i(0); i < m_throttles.size(); ++i) { - m_thrusts.push_back(m_throttles[i] * m_max_thrust); + for (double m_throttle : m_throttles) { + m_thrusts.push_back(m_throttle * m_max_thrust); } m_new_ta = heyoka::taylor_adaptive{kep3::ta::stark_dyn(), m_rvms, heyoka::kw::tol = m_tol}; @@ -104,7 +105,7 @@ struct sf_hf_test_object { m_cut = cut; } - std::vector compute_numerical_gradient() + [[nodiscard]] std::vector compute_numerical_gradient() { // Create SF leg. kep3::leg::sims_flanagan_hf sf_num(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, @@ -123,7 +124,7 @@ struct sf_hf_test_object { [&sf_num](const std::vector &x) { return sf_num.set_and_compute_constraints(x); }, chromosome); } - std::vector compute_analytical_gradient() + [[nodiscard]] std::vector compute_analytical_gradient() const { // Initialise kep3::leg::sims_flanagan_hf sf_a(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, m_cut, @@ -138,7 +139,7 @@ struct sf_hf_test_object { auto xgrad_final = xt::adapt(grad_final, {7u, nseg * 3u + 1u}); // Cast gradients into a single vector - std::vector gradient(7u * (7u + static_cast(nseg) * 3u + 1u + 7u), 0); + std::vector gradient(static_cast(7u * (7u + static_cast(nseg) * 3u + 1u + 7u)), 0); auto xgradient = xt::adapt(gradient, {7u, 7u + static_cast(nseg) * 3u + 1u + 7u}); xt::view(xgradient, xt::all(), xt::range(0u, 7u)) = xt::view(xgrad_rvm, xt::all(), xt::all()); // dmc_dxs xt::view(xgradient, xt::all(), xt::range(7u, 7u + nseg * 3u)) @@ -154,9 +155,9 @@ struct sf_hf_test_object { // Member attributes std::vector m_num_grad; heyoka::taylor_adaptive m_new_ta; - std::array m_fwd_final_state; - std::array m_bck_final_state; - std::array m_mc_manual; + std::array m_fwd_final_state{}; + std::array m_bck_final_state{}; + std::array m_mc_manual{}; std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; double m_ms = 1; @@ -169,9 +170,9 @@ struct sf_hf_test_object { std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; std::vector m_thrusts; double m_tol = 1e-16; - const std::vector m_rvms + std::vector m_rvms = {m_rvs[0][0], m_rvs[0][1], m_rvs[0][2], m_rvs[1][0], m_rvs[1][1], m_rvs[1][2], m_ms}; - const std::vector m_rvmf + std::vector m_rvmf = {m_rvf[0][0], m_rvf[0][1], m_rvf[0][2], m_rvf[1][0], m_rvf[1][1], m_rvf[1][2], m_mf}; }; From 3b4897ca647c51b3f9df94f5292629e15c9edaee Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Thu, 17 Oct 2024 17:12:49 +0200 Subject: [PATCH 07/22] Added state history function to be used for plotting. Also added python bindings but output is WIP. --- include/kep3/leg/sims_flanagan_hf.hpp | 6 +- pykep/core.cpp | 2 + pykep/docstrings.cpp | 22 +++++++ pykep/docstrings.hpp | 1 + pykep/plot/test.py | 10 --- src/leg/sims_flanagan_hf.cpp | 91 ++++++++++++++++++++++++++- test/leg_sims_flanagan_hf_test.cpp | 27 ++++++++ 7 files changed, 145 insertions(+), 14 deletions(-) delete mode 100644 pykep/plot/test.py diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index b030b204..e4c92646 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -124,11 +124,15 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Compute mismatch constraint gradients (w.r.t. initial and final rvm state as well as w.r.t. throttles and // tof) - [[nodiscard]] std::tuple, std::array, std::vector> compute_mc_grad() const; + [[nodiscard]] std::tuple, std::array, std::vector> + compute_mc_grad() const; // Compute throttle constraint gradients [[nodiscard]] std::vector compute_tc_grad() const; + // Retrieve the state history of the sims flanagan leg + [[nodiscard]] std::vector> get_state_history(const unsigned int grid_points_per_segment) const; + private: friend class boost::serialization::access; template diff --git a/pykep/core.cpp b/pykep/core.cpp index 47ed3ecb..70eefeb4 100644 --- a/pykep/core.cpp +++ b/pykep/core.cpp @@ -691,6 +691,8 @@ PYBIND11_MODULE(core, m) // NOLINT return tc_python; }, pykep::leg_sf_hf_tc_grad_docstring().c_str()); + sims_flanagan_hf.def("get_state_history", &kep3::leg::sims_flanagan_hf::get_state_history, + pykep::leg_sf_hf_get_state_history_docstring().c_str()); sims_flanagan_hf.def_property_readonly("nseg", &kep3::leg::sims_flanagan_hf::get_nseg, pykep::leg_sf_hf_nseg_docstring().c_str()); sims_flanagan_hf.def_property_readonly("nseg_fwd", &kep3::leg::sims_flanagan_hf::get_nseg_fwd, diff --git a/pykep/docstrings.cpp b/pykep/docstrings.cpp index a055e453..e4f9b1e2 100644 --- a/pykep/docstrings.cpp +++ b/pykep/docstrings.cpp @@ -2040,4 +2040,26 @@ Computes the gradients of the throttles constraints. Indicating the total time o )"; }; +std::string leg_sf_hf_get_state_history_docstring() +{ + return R"(get_state_history() + +Retrieves the state history of the Sims-Flanagan leg at specified times defined +by the grid_points_per_segment argument. This defines how many points are +saved per segment: if grid_points_per_segment=4, then each segment will include its initial and final +state as well as two temporally equidistant points. + + +Returns: + :class:`tuple` [:class:`numpy.ndarray`]: The state history. Size will be (nseg,grid_points_per_segment*7). + +Examples: + >>> import pykep as pk + >>> import numpy as np + >>> sf_hf = pk.leg.sims_flanagan_hf() + >>> grid_points_per_segment = 10 + >>> sf_hf.get_state_history(grid_points_per_segment) +)"; +}; + } // namespace pykep \ No newline at end of file diff --git a/pykep/docstrings.hpp b/pykep/docstrings.hpp index 4ffcc40c..8d937763 100644 --- a/pykep/docstrings.hpp +++ b/pykep/docstrings.hpp @@ -142,6 +142,7 @@ std::string leg_sf_hf_mc_docstring(); std::string leg_sf_hf_tc_docstring(); std::string leg_sf_hf_mc_grad_docstring(); std::string leg_sf_hf_tc_grad_docstring(); +std::string leg_sf_hf_get_state_history_docstring(); std::string leg_sf_hf_nseg_docstring(); std::string leg_sf_hf_nseg_fwd_docstring(); std::string leg_sf_hf_nseg_bck_docstring(); diff --git a/pykep/plot/test.py b/pykep/plot/test.py deleted file mode 100644 index bd7511b5..00000000 --- a/pykep/plot/test.py +++ /dev/null @@ -1,10 +0,0 @@ -import pykep as pk -import numpy as np - -# Earth -udpla = pk.udpla.jpl_lp(body="EARTH") -earth = pk.planet(udpla) - -sf = pk.leg.sims_flanagan() -nseg = 20 -sf.throttles=[0,0,0] * nseg \ No newline at end of file diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 6eb8dbb6..1912dc80 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -435,7 +436,7 @@ std::vector sims_flanagan_hf::compute_throttle_constraints() const return retval; } -std::vector sims_flanagan_hf::compute_constraints() +std::vector sims_flanagan_hf::compute_constraints() { std::vector retval(7 + m_nseg, 0.); // Fitness @@ -454,7 +455,7 @@ std::vector sims_flanagan_hf::compute_constraints() return retval; } -std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector chromosome) +std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector chromosome) { std::array rvms; std::copy(chromosome.begin(), chromosome.begin() + 7, rvms.begin()); @@ -716,7 +717,8 @@ sims_flanagan_hf::get_relevant_gradients(std::vector> &d return {std::move(grad_rvm), std::move(grad_rvm_bck), std::move(grad_final)}; } -std::tuple, std::array, std::vector> sims_flanagan_hf::compute_mc_grad() const +std::tuple, std::array, std::vector> +sims_flanagan_hf::compute_mc_grad() const { // Initialise std::vector> dxdx_per_seg; @@ -743,6 +745,89 @@ std::vector sims_flanagan_hf::compute_tc_grad() const return retval; } +std::vector> sims_flanagan_hf::get_state_history(unsigned int grid_points_per_segment) const +{ + // Get time grid + const double prop_seg_duration = (m_tof / m_nseg); + std::vector leg_time_grid; + // Initial time + double timestep = 0.0; + leg_time_grid.push_back(timestep); + + for (uint _(0); _ < grid_points_per_segment * m_nseg - 2; ++_) { + timestep += prop_seg_duration / (grid_points_per_segment - 1); + leg_time_grid.push_back(timestep); + } + // leg_time_grid.push_back(m_tof); + std::vector current_leg_time_grid(grid_points_per_segment); + + // Forward pass + // Initial state + // Set the Taylor Integration initial conditions + m_tas.set_time(0.); + std::copy(m_rvms.begin(), m_rvms.end(), m_tas.get_state_data()); + std::vector> output_per_seg(m_nseg); + + // Loop through segments in forward pass of Sims-Flanagan transcription + for (unsigned int i = 0u; i < m_nseg_fwd; ++i) { + // Assign current thrusts to Taylor adaptive integrator + if (static_cast((i + 1) * 3) <= m_thrusts.size()) { + std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), + std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), + std::next(m_tas.get_pars_data(), 2)); + } else { + throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); + } + + // Current leg time grid + std::copy(std::next(leg_time_grid.begin(), i * (grid_points_per_segment - 1)), + std::next(leg_time_grid.begin(), (i + 1) * (grid_points_per_segment - 1) + 1), + current_leg_time_grid.begin()); + m_tas.set_time(current_leg_time_grid.at(0)); + // ... and integrate + auto [status, min_h, max_h, nsteps, _1, output_states] = m_tas.propagate_grid(current_leg_time_grid); + if (status != heyoka::taylor_outcome::time_limit) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + } + output_per_seg.insert(output_per_seg.begin() + i, output_states); + } + + // Backward pass + // Final state + // Set the Taylor Integration final conditions + m_tas.set_time(m_tof); + std::copy(m_rvmf.begin(), m_rvmf.end(), m_tas.get_state_data()); + std::vector back_time_grid(grid_points_per_segment); + + // Loop through segments in backward pass of Sims-Flanagan transcription + for (unsigned int i = 0u; i < m_nseg_bck; ++i) { + // Assign current_thrusts to Taylor adaptive integrator + if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { + // Copy thrust into Taylor-adaptive integrator + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), + std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), + std::next(m_tas.get_pars_data(), 2)); + } else { + throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); + } + + // Current leg time grid + std::reverse_copy(leg_time_grid.begin() + (m_nseg - (i + 1)) * (grid_points_per_segment - 1), + leg_time_grid.begin() + (m_nseg - i) * (grid_points_per_segment - 1) + 1, + back_time_grid.begin()); + m_tas.set_time(back_time_grid.at(0)); + + // ... and integrate + auto [status, min_h, max_h, nsteps, _1, output_states] = m_tas.propagate_grid(back_time_grid); + if (status != heyoka::taylor_outcome::time_limit) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + } + output_per_seg.insert(output_per_seg.begin() + m_nseg - 1 - i, output_states); + } + + return output_per_seg; +} + std::ostream &operator<<(std::ostream &s, const sims_flanagan_hf &sf) { s << fmt::format("Number of segments: {}\n", sf.get_nseg()); diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index 82dd14ae..93ec521e 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -314,6 +314,33 @@ TEST_CASE("compute_tc_grad_test") REQUIRE(xt::linalg::norm(xt_num_tc_gradients - xt_tc_a_grad) < 1e-13); // 1e-14 fails } +TEST_CASE("compute_state_history") +{ + // Get state history + kep3::leg::sims_flanagan_hf sf{}; + auto mc = sf.compute_mismatch_constraints(); + uint grid_points_per_segment = 4; + auto state_history = sf.get_state_history(grid_points_per_segment); + + // Get fwd final state + std::vector fwd_seg_sh = state_history.at(sf.get_nseg_fwd() - 1); + std::array final_fwd_state; + std::copy(fwd_seg_sh.begin() + (grid_points_per_segment - 1) * 7, fwd_seg_sh.begin() + grid_points_per_segment * 7, + final_fwd_state.begin()); + + // Get bck final state + std::vector bck_seg_sh = state_history.at(sf.get_nseg_fwd()); + std::array final_bck_state; + std::copy(bck_seg_sh.begin() + (grid_points_per_segment - 1) * 7, bck_seg_sh.begin() + grid_points_per_segment * 7, + final_bck_state.begin()); + + // Get mismatch and calculate Linfty norm + std::transform(final_fwd_state.begin(), final_fwd_state.end(), final_bck_state.begin(), final_fwd_state.begin(), + std::minus()); + std::array manual_mismatch = final_fwd_state; // final_fwd_state is overridden with the subtracted values + REQUIRE(kep3_tests::L_infinity_norm(manual_mismatch, mc) < 1e-15); +} + TEST_CASE("serialization_test") { // Instantiate a generic lambert problem From 93f0b4b72d56fa2b670b5a43010d68e785f0d2fb Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Thu, 17 Oct 2024 17:57:23 +0200 Subject: [PATCH 08/22] Bug fix for output of get_state_history() function. --- src/leg/sims_flanagan_hf.cpp | 4 +-- test/leg_sims_flanagan_hf_test.cpp | 39 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 5bfaff84..32358b44 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -794,7 +794,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in if (status != heyoka::taylor_outcome::time_limit) { throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } - output_per_seg.insert(output_per_seg.begin() + i, output_states); + output_per_seg[i] = output_states; } // Backward pass @@ -827,7 +827,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in if (status != heyoka::taylor_outcome::time_limit) { throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } - output_per_seg.insert(output_per_seg.begin() + m_nseg - 1 - i, output_states); + output_per_seg[m_nseg - 1 - i] = output_states; } return output_per_seg; diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index 93ec521e..a3eca0a3 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -341,6 +341,45 @@ TEST_CASE("compute_state_history") REQUIRE(kep3_tests::L_infinity_norm(manual_mismatch, mc) < 1e-15); } +TEST_CASE("compute_state_history_2") +{ + // Initialise unique test quantities + std::vector throttles + = {0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, + 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34}; + double cut = 0.6; + // Initialise helper quantities + auto sf_test_object = sf_hf_test_object(throttles, cut); + + kep3::leg::sims_flanagan_hf sf(sf_test_object.m_rvs, sf_test_object.m_ms, sf_test_object.m_throttles, + sf_test_object.m_rvf, sf_test_object.m_mf, sf_test_object.m_tof, + sf_test_object.m_max_thrust, sf_test_object.m_isp, sf_test_object.m_mu, + sf_test_object.m_cut, 1e-16); + + // Get state history + auto mc = sf.compute_mismatch_constraints(); + uint grid_points_per_segment = 4; + auto state_history = sf.get_state_history(grid_points_per_segment); + + // Get fwd final state + std::vector fwd_seg_sh = state_history.at(sf.get_nseg_fwd() - 1); + std::array final_fwd_state; + std::copy(fwd_seg_sh.begin() + (grid_points_per_segment - 1) * 7, fwd_seg_sh.begin() + grid_points_per_segment * 7, + final_fwd_state.begin()); + + // Get bck final state + std::vector bck_seg_sh = state_history.at(sf.get_nseg_fwd()); + std::array final_bck_state; + std::copy(bck_seg_sh.begin() + (grid_points_per_segment - 1) * 7, bck_seg_sh.begin() + grid_points_per_segment * 7, + final_bck_state.begin()); + + // Get mismatch and calculate Linfty norm + std::transform(final_fwd_state.begin(), final_fwd_state.end(), final_bck_state.begin(), final_fwd_state.begin(), + std::minus()); + std::array manual_mismatch = final_fwd_state; // final_fwd_state is overridden with the subtracted values + REQUIRE(kep3_tests::L_infinity_norm(manual_mismatch, mc) < 1e-15); +} + TEST_CASE("serialization_test") { // Instantiate a generic lambert problem From b60962c2f43555cfc67b5da296167f2a65b7247f Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 18 Oct 2024 11:05:16 +0200 Subject: [PATCH 09/22] Added plotting function for the high fidelity SF leg. --- pykep/plot/__init__.py | 1 + pykep/plot/_sf_leg.py | 235 ++++++++++++++++++----------------------- 2 files changed, 106 insertions(+), 130 deletions(-) diff --git a/pykep/plot/__init__.py b/pykep/plot/__init__.py index a68bee12..002549bb 100644 --- a/pykep/plot/__init__.py +++ b/pykep/plot/__init__.py @@ -8,6 +8,7 @@ from ._lambert import add_lambert from ._ballistic import add_ballistic_arc from ._sf_leg import add_sf_leg +from ._sf_leg import add_sf_hf_leg def make_3Daxis(**kwargs): """Constructs and returns a 3D axis. All kwargs are forwarded to the call to `figure()` in matplotlib. diff --git a/pykep/plot/_sf_leg.py b/pykep/plot/_sf_leg.py index f11bb0f1..b0b6192e 100644 --- a/pykep/plot/_sf_leg.py +++ b/pykep/plot/_sf_leg.py @@ -69,9 +69,9 @@ def add_sf_leg( dv = _np.linalg.norm(throttles) * c / mass_fwd # plot it in a color that is proportional to the strength from royalblue to indianred color = ( - 0.25 + (1. - 0.25) * min(1.0, _np.linalg.norm(throttles)), - 0.41 + (0. - 0.41) * min(1.0, _np.linalg.norm(throttles)), - 0.88 + (0. - 0.88) * min(1.0, _np.linalg.norm(throttles)), + 0.25 + (1.0 - 0.25) * min(1.0, _np.linalg.norm(throttles)), + 0.41 + (0.0 - 0.41) * min(1.0, _np.linalg.norm(throttles)), + 0.88 + (0.0 - 0.88) * min(1.0, _np.linalg.norm(throttles)), ) _pk.plot.add_ballistic_arc( ax, rv, dt / 2, sf.mu, units=units, N=N, c=color, **kwargs @@ -130,7 +130,7 @@ def add_sf_leg( for i in range(nseg_bck): # compute the dv (first non dimensional) - throttles = sf.throttles[nseg*3-3-3*i:nseg*3-3*i] + throttles = sf.throttles[nseg * 3 - 3 - 3 * i : nseg * 3 - 3 * i] throttles_bck.append(throttles) dv = _np.linalg.norm(throttles) * c / mass_bck # plot it in a color that is proportional to the strength @@ -187,6 +187,7 @@ def add_sf_leg( return ax + def add_sf_hf_leg( ax, sf: _pk.leg.sims_flanagan_hf, @@ -208,7 +209,7 @@ def add_sf_hf_leg( *units* (:class:`float`, optional): The unit conversion factor for plotting. Default is pk.AU. - *N* (:class:`int`, optional): The number of points to generate along each segment of the trajectory. Default is 10. + *N* (:class:`int`, optional): The number of points to generate along each segment of the trajectory. Default is 10. This translates to the grid_points_per_segment argument for retrieving the state history. *show_gridpoints* (:class:`bool`, optional): If True, gridpoints of the trajectory are shown. Default is False. @@ -229,142 +230,116 @@ def add_sf_hf_leg( """ # We extract the number of segments from the leg. nseg = int(len(sf.throttles) / 3) - dt = sf.tof / nseg - c = sf.max_thrust * dt nseg_fwd = int(nseg * sf.cut) nseg_bck = nseg - nseg_fwd + state_history_raw = sf.get_state_history(N) + throttles = _np.repeat( + _np.array(sf.throttles).reshape((1, len(sf.throttles))), + N, + axis=0, + ) - # We start the forward pass of the Sims-Flanagan model------------------------------------------------------------------------ - pos_fwd = [] - pos_m_fwd = [] - throttles_fwd = [] - rv = _deepcopy(sf.rvs) - mass_fwd = sf.ms - # Append to plotting data - pos_fwd.append(rv[0]) + throttles_fwd = throttles[:, 0 : nseg_fwd * 3] + throttles_bck = throttles[:, nseg_fwd * 3 : nseg * 3] - stark_integrator = _pk.ta.get_stark + # We start the forward pass of the Sims-Flanagan model------------------------------------------------------------------------ + state_history_fwd = _np.zeros((nseg_fwd * N, 7)) + it = 0 for i in range(nseg_fwd): - # compute the dv - throttles = sf.throttles[3 * i : 3 * i + 3] - throttles_fwd.append(throttles) - dv = _np.linalg.norm(throttles) * c / mass_fwd - # plot it in a color that is proportional to the strength from royalblue to indianred - color = ( - 0.25 + (1. - 0.25) * min(1.0, _np.linalg.norm(throttles)), - 0.41 + (0. - 0.41) * min(1.0, _np.linalg.norm(throttles)), - 0.88 + (0. - 0.88) * min(1.0, _np.linalg.norm(throttles)), - ) - _pk.plot.add_ballistic_arc( - ax, rv, dt / 2, sf.mu, units=units, N=N, c=color, **kwargs - ) - # propagate for dt/2 - rv = list(_pk.propagate_lagrangian(rv, tof=dt / 2, mu=sf.mu, stm=False)) - # register the position as a mid-point state - pos_m_fwd.append(rv[0]) - # add dv to the state (now dimensional) - rv[1] = [a + b * c / mass_fwd for a, b in zip(rv[1], throttles)] - # update the mass (increases) - mass_fwd *= _np.exp(-dv / sf.isp / _pk.G0) - # 2 - propagate for dt/2 - _pk.plot.add_ballistic_arc( - ax, rv, dt / 2, sf.mu, units=units, N=N, c=color, **kwargs - ) - rv = _pk.propagate_lagrangian(rv, tof=dt / 2, mu=sf.mu, stm=False) - pos_fwd.append(rv[0]) - pos_fwd = _np.array(pos_fwd) - pos_m_fwd = _np.array(pos_m_fwd) - throttles_fwd = _np.array(throttles_fwd) + for j in range(N): + state_history_fwd[it, :] = state_history_raw[i][7 * j : 7 * (j + 1)] + it += 1 + + ax.plot( + state_history_fwd[:, 0] / units, + state_history_fwd[:, 1] / units, + state_history_fwd[:, 2] / units, + c="k", + ) - # We plot optionally gridpoints, the low-thrust or the mid points - if show_gridpoints: - ax.plot( - pos_fwd[:, 0] / units, pos_fwd[:, 1] / units, pos_fwd[:, 2] / units, "k." - ) - if show_midpoints: - ax.plot( - pos_m_fwd[:, 0] / units, - pos_m_fwd[:, 1] / units, - pos_m_fwd[:, 2] / units, - "kx", - ) if show_throttles: - ax.quiver( - pos_m_fwd[:, 0] / units, - pos_m_fwd[:, 1] / units, - pos_m_fwd[:, 2] / units, - throttles_fwd[:, 0], - throttles_fwd[:, 1], - throttles_fwd[:, 2], - length=length, - color="indianred", - arrow_length_ratio=arrow_length_ratio, - ) + for i in range(nseg_fwd): + current_states = state_history_fwd[ + i * N : (i + 1) * N, 0:3 + ] + current_throttles = throttles_fwd[:, i * 3 : (i + 1) * 3] + current_quiver_tips = current_states / units + current_throttles * length + ax.quiver( + current_states[:, 0] / units, + current_states[:, 1] / units, + current_states[:, 2] / units, + current_throttles[:, 0], + current_throttles[:, 1], + current_throttles[:, 2], + length=length, + color="indianred", + arrow_length_ratio=arrow_length_ratio, + ) + ax.plot( + current_quiver_tips[:, 0], + current_quiver_tips[:, 1], + current_quiver_tips[:, 2], + color="indianred", + ) - # We start the backward pass of the Sims-Flanagan model------------------------------------------------------------------------ - pos_bck = [] - pos_bck_m = [] - throttles_bck = [] - rv = _deepcopy(sf.rvf) - mass_bck = sf.mf - # Append to plotting data - pos_bck.append(rv[0]) + if show_gridpoints: + ax.scatter( + state_history_fwd[:, 0] / units, + state_history_fwd[:, 1] / units, + state_history_fwd[:, 2] / units, + c="indianred", + s=5, + ) + # We start the forward pass of the Sims-Flanagan model------------------------------------------------------------------------ + state_history_bck = _np.zeros((nseg_bck * N, 7)) + it = 0 for i in range(nseg_bck): - # compute the dv (first non dimensional) - throttles = sf.throttles[nseg*3-3-3*i:nseg*3-3*i] - throttles_bck.append(throttles) - dv = _np.linalg.norm(throttles) * c / mass_bck - # plot it in a color that is proportional to the strength - color = ( - 0.25 + (0.80 - 0.25) * min(1.0, _np.linalg.norm(throttles)), - 0.41 + (0.36 - 0.41) * min(1.0, _np.linalg.norm(throttles)), - 0.88 + (0.36 - 0.88) * min(1.0, _np.linalg.norm(throttles)), - ) - _pk.plot.add_ballistic_arc( - ax, rv, -dt / 2, sf.mu, units=units, N=N, c=color, **kwargs - ) - # propagate for dt/2 - rv = list(_pk.propagate_lagrangian(rv, tof=-dt / 2, mu=sf.mu, stm=False)) - # register the position as a mid-point state - pos_bck_m.append(rv[0]) - # add it to the state (now dimensional) - rv[1] = [a - b * c / mass_bck for a, b in zip(rv[1], throttles)] - # update the mass - mass_bck *= _np.exp(dv / sf.isp / _pk.G0) - # 2 - propagate for dt/2 - _pk.plot.add_ballistic_arc( - ax, rv, -dt / 2, sf.mu, units=units, N=N, c=color, **kwargs - ) - rv = _pk.propagate_lagrangian(rv, tof=-dt / 2, mu=sf.mu, stm=False) - pos_bck.append(rv[0]) - pos_bck = _np.array(pos_bck) - pos_bck_m = _np.array(pos_bck_m) - throttles_bck = _np.array(throttles_bck) + for j in range(N): + state_history_bck[it, :] = state_history_raw[nseg - i - 1][ + 7 * j : 7 * (j + 1) + ] + it += 1 + + ax.plot( + state_history_bck[:, 0] / units, + state_history_bck[:, 1] / units, + state_history_bck[:, 2] / units, + c="k", + ) - # We plot optionally gridpoints, the low-thrust or the mid points - if show_gridpoints: - ax.plot( - pos_bck[:, 0] / units, pos_bck[:, 1] / units, pos_bck[:, 2] / units, "k." - ) - if show_midpoints: - ax.plot( - pos_bck_m[:, 0] / units, - pos_bck_m[:, 1] / units, - pos_bck_m[:, 2] / units, - "kx", - ) if show_throttles: - ax.quiver( - pos_bck_m[:, 0] / units, - pos_bck_m[:, 1] / units, - pos_bck_m[:, 2] / units, - throttles_bck[:, 0], - throttles_bck[:, 1], - throttles_bck[:, 2], - length=length, - color="indianred", - arrow_length_ratio=arrow_length_ratio, + for i in range(nseg_bck): + current_states = state_history_bck[ + i * N : (i + 1) * N, 0:3 + ] + current_throttles = throttles_bck[:, i * 3 : (i + 1) * 3] + current_quiver_tips = current_states / units + current_throttles * length + ax.quiver( + current_states[:, 0] / units, + current_states[:, 1] / units, + current_states[:, 2] / units, + current_throttles[:, 0], + current_throttles[:, 1], + current_throttles[:, 2], + length=length, + color="indianred", + arrow_length_ratio=arrow_length_ratio, + ) + ax.plot( + current_quiver_tips[:, 0], + current_quiver_tips[:, 1], + current_quiver_tips[:, 2], + color="indianred", + ) + + if show_gridpoints: + ax.scatter( + state_history_bck[:, 0] / units, + state_history_bck[:, 1] / units, + state_history_bck[:, 2] / units, + c="indianred", + s=5, ) - return ax \ No newline at end of file + return ax From e21e0bfb6df85424feca04f33be5adc000ec45aa Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 18 Oct 2024 13:53:31 +0200 Subject: [PATCH 10/22] Added SF HF notebook and corresponding documentation link. Small typo fix. --- doc/notebooks/sims_flanagan_hf_leg.ipynb | 147 +++++++++++++++++++++++ doc/notebooks/sims_flanagan_leg.ipynb | 4 +- doc/plot.rst | 2 + doc/tut_basic.rst | 1 + 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 doc/notebooks/sims_flanagan_hf_leg.ipynb diff --git a/doc/notebooks/sims_flanagan_hf_leg.ipynb b/doc/notebooks/sims_flanagan_hf_leg.ipynb new file mode 100644 index 00000000..5af8193a --- /dev/null +++ b/doc/notebooks/sims_flanagan_hf_leg.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The Sims-Flanagan high-fidelity trajectory leg\n", + "\n", + "The Sims-Flanagan trajectory leg {cite:p}`sims` is implemented in `pykep` in the class {class}`pykep.leg.sims_flanagan_hf`. The leg can be used to describe efficiently a low-thrust leg with low-fidelity as it assumes Keplerian dynamics\n", + "and approximates the continuous thrust via a sequence of continuous, constant thrust arcs. The leg is defined by a starting position $\\mathbf x_s = [\\mathbf r_s, \\mathbf v_s, m_s]$, an arrival position $\\mathbf x_f = [\\mathbf r_f, \\mathbf v_f, m_f]$ and a time of flight $T$.\n", + "\n", + "A sequence of throttles $\\mathbf u = [u_{x0}, u_{y0}, u_{z0}, u_{x1}, u_{y1}, u_{z1}, u_{x2}, u_{y2}, u_{z2}, ... ]$ define the direction and magnitude of the continuous thrust vector along each segment (i.e. trajectory parts of equal temporal length $\\frac Tn$).\n", + "\n", + "In this tutorial we show the basic API to interface with the class {class}`pykep.leg.sims_flanagan_hf` efficiently.\n", + "\n", + "We start with some imports:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pykep as pk\n", + "import numpy as np\n", + "import time\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from mpl_toolkits import mplot3d\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then define the spacecraft propulsion system and the initial and final state. In this case they are not related to any orbital mechanics and are chosen arbitrarily for the purpose of clearlyshowing the API." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Problem data\n", + "mu = pk.MU_SUN\n", + "max_thrust = 0.12\n", + "isp = 3000\n", + "\n", + "# Initial state\n", + "ms = 1500.0\n", + "rs = np.array([1, 0.1, -0.1]) * pk.AU\n", + "vs = np.array([0.2, 1, -0.2]) * pk.EARTH_VELOCITY\n", + "\n", + "# Final state\n", + "mf = 1300.0\n", + "rf = np.array([-1.2, -0.1, 0.1]) * pk.AU\n", + "vf = np.array([0.2, -1.023, 0.44]) * pk.EARTH_VELOCITY\n", + "\n", + "# Throttles and tof\n", + "nseg = 10\n", + "cut = 0.6\n", + "throttles = np.random.uniform(-1, 1, size=(nseg * 3))\n", + "tof = 324.0 * pk.DAY2SEC\n", + "# We are now ready to instantiate a leg\n", + "sf = pk.leg.sims_flanagan_hf(\n", + " rvs=[rs, vs],\n", + " ms=ms,\n", + " throttles=throttles,\n", + " rvf=[rf, vf],\n", + " mf=mf,\n", + " tof=tof,\n", + " max_thrust=max_thrust,\n", + " isp=isp,\n", + " mu=mu,\n", + " cut=cut,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we instantiate the leg:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Making the axis\n", + "ax = pk.plot.make_3Daxis(figsize=(7, 7))\n", + "\n", + "# Adding the Sun Earth and the boundary states\n", + "udpla = pk.udpla.jpl_lp(body=\"EARTH\")\n", + "earth = pk.planet(udpla)\n", + "pk.plot.add_sun(ax, s=40)\n", + "pk.plot.add_planet_orbit(ax, earth)\n", + "ax.scatter(rs[0] / pk.AU, rs[1] / pk.AU, rs[2] / pk.AU, c=\"k\", s=20)\n", + "ax.scatter(rf[0] / pk.AU, rf[1] / pk.AU, rf[2] / pk.AU, c=\"k\", s=20)\n", + "\n", + "\n", + "# Plotting the trajctory leg\n", + "ax = pk.plot.add_sf_hf_leg(\n", + " ax, sf, units=pk.AU, N=5, show_gridpoints=True, show_throttles=True, length=0.1, arrow_length_ratio=0.5\n", + ")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gtoc12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/doc/notebooks/sims_flanagan_leg.ipynb b/doc/notebooks/sims_flanagan_leg.ipynb index 56033739..bd22a2b9 100644 --- a/doc/notebooks/sims_flanagan_leg.ipynb +++ b/doc/notebooks/sims_flanagan_leg.ipynb @@ -7,7 +7,7 @@ "# The Sims-Flanagan trajectory leg\n", "\n", "The Sims-Flanagan trajectory leg {cite:p}`sims` is implemented in `pykep` in the class {class}`pykep.leg.sims_flanagan`. The leg can be used to describe efficiently a low-thrust leg with low-fidelity as it assumes Keplerian dynamics\n", - "and approximates the continuous thrust via a sequence of impulsive, equally-spaced manouvres. The leg is defined by a starting position $\\mathbf x_s = [\\mathbf r_s, \\mathbf v_s, m_s]$, an arrival position $\\mathbf x_f = [\\mathbf r_f, \\mathbf v_f, m_f]$ and a time of flight $T$.\n", + "and approximates the continuous thrust via a sequence of impulsive, equally-spaced manoeuvres. The leg is defined by a starting position $\\mathbf x_s = [\\mathbf r_s, \\mathbf v_s, m_s]$, an arrival position $\\mathbf x_f = [\\mathbf r_f, \\mathbf v_f, m_f]$ and a time of flight $T$.\n", "\n", "A sequence of throttles $\\mathbf u = [u_{x0}, u_{y0}, u_{z0}, u_{x1}, u_{y1}, u_{z1}, u_{x2}, u_{y2}, u_{z2}, ... ]$ define an equal number of impulsive $\\Delta \\mathbf V$ applied in the middle of the leg's $n$ segments (i.e. trajectory parts of equal temporal length $\\frac Tn$).\n", "\n", @@ -138,7 +138,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "undefined.undefined.undefined" + "version": "3.11.10" } }, "nbformat": 4, diff --git a/doc/plot.rst b/doc/plot.rst index 97a395aa..1bd39bc4 100644 --- a/doc/plot.rst +++ b/doc/plot.rst @@ -30,6 +30,8 @@ Plotting trajectories .. autofunction:: add_sf_leg +.. autofunction:: add_sf_hf_leg + diff --git a/doc/tut_basic.rst b/doc/tut_basic.rst index aab95529..58f31267 100644 --- a/doc/tut_basic.rst +++ b/doc/tut_basic.rst @@ -34,5 +34,6 @@ astrodynamical notation and computations. notebooks/interface_to_spice notebooks/propagate_lagrangian notebooks/sims_flanagan_leg + notebooks/sims_flanagan_hf_leg notebooks/plotting From aec3a75b93977d2c0d13fd13d240bf91d876f50e Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 18 Oct 2024 13:54:20 +0200 Subject: [PATCH 11/22] [WIP] Benchmark equivalent for SF HF leg. --- benchmark/CMakeLists.txt | 1 + benchmark/leg_sims_flanagan_hf_benchmark.cpp | 211 +++++++++++++++++++ benchmark/leg_sims_flanagan_hf_udp_bench.hpp | 166 +++++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 benchmark/leg_sims_flanagan_hf_benchmark.cpp create mode 100644 benchmark/leg_sims_flanagan_hf_udp_bench.hpp diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 4a05611a..a61ee4d6 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -24,5 +24,6 @@ ADD_kep3_BENCHMARK(propagate_lagrangian_benchmark) ADD_kep3_BENCHMARK(lambert_problem_benchmark) ADD_kep3_BENCHMARK(stm_benchmark) ADD_kep3_BENCHMARK(leg_sims_flanagan_benchmark) +ADD_kep3_BENCHMARK(leg_sims_flanagan_hf_benchmark) diff --git a/benchmark/leg_sims_flanagan_hf_benchmark.cpp b/benchmark/leg_sims_flanagan_hf_benchmark.cpp new file mode 100644 index 00000000..35d74d14 --- /dev/null +++ b/benchmark/leg_sims_flanagan_hf_benchmark.cpp @@ -0,0 +1,211 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "leg_sims_flanagan_hf_udp_bench.hpp" + +using std::chrono::duration_cast; +using std::chrono::high_resolution_clock; +using std::chrono::microseconds; + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) +{ + // + // Engines + // + // NOLINTNEXTLINE(cert-msc32-c, cert-msc51-cpp) + std::mt19937 rng_engine(122012203u); + // + // Distributions + // + std::uniform_real_distribution dv_pert_d(0., 1000.); + std::uniform_real_distribution mass_d(500, 1500); + std::uniform_real_distribution tof_d(1000, 1500); + std::uniform_real_distribution ts_d(1100, 1300); + + // We construct the solver + pagmo::nlopt uda{"slsqp"}; + uda.set_xtol_abs(1e-8); + uda.set_xtol_rel(1e-8); + uda.set_ftol_abs(1e-8); + uda.set_maxeval(1000); + pagmo::algorithm algo{uda}; + algo.set_verbosity(0u); + + // The initial positions + kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); + kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); + kep3::planet earth{udpla_earth}; + kep3::planet jupiter{udpla_jupiter}; + double count_a = 0; + double count_n = 0; + std::cout << "\n"; + for (auto i = 0u; i < N; ++i) { + // And some epochs / tofs. + const double tof_days = tof_d(rng_engine); + const double tof = tof_days * kep3::DAY2SEC; + const double ts = ts_d(rng_engine); + const double mass = mass_d(rng_engine); + auto rvs = earth.eph(ts); + auto rvf = jupiter.eph(ts + tof_days); + // We create a ballistic arc matching the two. + const kep3::lambert_problem lp{rvs[0], rvf[0], tof, kep3::MU_SUN}; + rvs[1][0] = lp.get_v0()[0][0]; + rvs[1][1] = lp.get_v0()[0][1]; + rvs[1][2] = lp.get_v0()[0][2]; + rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); + rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); + rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); + + // We construct two problems (analytical gradient and numerical gradient) + pagmo::problem prob_a{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, true}}; + pagmo::problem prob_n{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, false}}; + prob_a.set_c_tol(1e-8); + prob_n.set_c_tol(1e-8); + + // We construct the random chromosmes + const pagmo::population pop{prob_a, pop_size}; + + // First we time the analytical gradients + auto start = high_resolution_clock::now(); + for (decltype(pop_size) j = 0u; j < pop_size; ++j) { + prob_a.gradient(pop.get_x()[j]); + } + auto stop = high_resolution_clock::now(); + auto duration = duration_cast(stop - start); + count_a+=static_cast(duration.count()) / 1e6; + + // then the numerical ones + start = high_resolution_clock::now(); + for (decltype(pop_size) j = 0u; j < pop_size; ++j) { + prob_n.gradient(pop.get_x()[j]); + } + stop = high_resolution_clock::now(); + duration = duration_cast(stop - start); + count_n+=static_cast(duration.count()) / 1e6; + } + fmt::print("{} nseg - timing: analytical {} - numerical {}", nseg, count_a, count_n); +} + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +void perform_test_convergence(unsigned N, unsigned nseg) +{ + // + // Engines + // + // NOLINTNEXTLINE(cert-msc32-c, cert-msc51-cpp) + std::mt19937 rng_engine(122012203u); + // + // Distributions + // + std::uniform_real_distribution dv_pert_d(0., 1000.); + std::uniform_real_distribution mass_d(500, 1500); + std::uniform_real_distribution tof_d(1000, 1500); + std::uniform_real_distribution ts_d(1100, 1300); + + // We construct the solver + pagmo::nlopt uda{"slsqp"}; + uda.set_xtol_abs(0); + uda.set_xtol_rel(0); + uda.set_ftol_abs(0); + uda.set_maxeval(1000); + pagmo::algorithm algo{uda}; + algo.set_verbosity(0u); + + // The initial positions + kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); + kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); + kep3::planet earth{udpla_earth}; + kep3::planet jupiter{udpla_jupiter}; + unsigned count_a = 0; + unsigned count_n = 0; + std::cout << "\n"; + for (auto i = 0u; i < N; ++i) { + // And some epochs / tofs. + const double tof_days = tof_d(rng_engine); + const double tof = tof_days * kep3::DAY2SEC; + double ts = ts_d(rng_engine); + const double mass = mass_d(rng_engine); + auto rvs = earth.eph(ts); + auto rvf = jupiter.eph(ts + tof_days); + // We create a ballistic arc matching the two. + const kep3::lambert_problem lp{rvs[0], rvf[0], tof, kep3::MU_SUN}; + rvs[1][0] = lp.get_v0()[0][0]; + rvs[1][1] = lp.get_v0()[0][1]; + rvs[1][2] = lp.get_v0()[0][2]; + rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); + rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); + rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); + + // We construct two problems (analytical gradient and numerical gradient) + pagmo::problem prob_a{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, true}}; + pagmo::problem prob_n{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, false}}; + prob_a.set_c_tol(1e-8); + prob_n.set_c_tol(1e-8); + + // We construct the same random population + pagmo::population pop_a{prob_a, 1u}; + pagmo::population pop_n{prob_n}; + pop_n.push_back(pop_a.get_x()[0]); + + // We solve first a + pop_a = algo.evolve(pop_a); + if (prob_a.feasibility_f(pop_a.get_f()[0])) { + count_a++; + std::cout << "." << std::flush; + } else { + std::cout << "x" << std::flush; + } + // then n + pop_n = algo.evolve(pop_n); + if (prob_n.feasibility_f(pop_n.get_f()[0])) { + count_n++; + std::cout << "." << std::flush; + } else { + std::cout << "x" << std::flush; + } + } + fmt::print("\n{} nseg - success rates: analytical {}/{} - numerical {}/{}", nseg, count_a, N, count_n, N); +} + +int main() +{ + // performing tests + fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); + perform_test_convergence(200, 5); + perform_test_convergence(200, 10); + perform_test_convergence(200, 15); + + fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:"); + perform_test_speed(100, 5, 10); + perform_test_speed(100, 10, 10); + perform_test_speed(100, 15, 10); + perform_test_speed(100, 20, 10); + perform_test_speed(100, 70, 10); + fmt::print("\n"); + +} \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp new file mode 100644 index 00000000..72975069 --- /dev/null +++ b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp @@ -0,0 +1,166 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef kep3_TEST_LEG_SIMS_FLANAGAN_UDP_BENCH_H +#define kep3_TEST_LEG_SIMS_FLANAGAN_UDP_BENCH_H + +#include +#include + +#include +#include +#include + +#include + +#include +#include + +struct sf_hf_bench_udp { + sf_hf_bench_udp() = default; + sf_hf_bench_udp(std::array, 2> rvs, double ms, std::array, 2> rvf, + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + double max_thrust, double isp, unsigned nseg, bool analytical) + : m_rvs(rvs), m_rvf(rvf), m_ms(ms), m_max_thrust(max_thrust), m_isp(isp), m_nseg(nseg), m_analytical(analytical) + { + } + + [[nodiscard]] std::vector fitness(const std::vector &x) const + { + // x = [throttles, tof (in days), mf (in kg)] + // We set the leg (avoiding the allocation for the throttles is possible but requires mutable data members.) + double tof = x[m_nseg * 3] * kep3::DAY2SEC; // in s + double mf = x[m_nseg * 3 + 1]; // in kg + kep3::leg::sims_flanagan_hf leg(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, mf, tof, m_max_thrust, + m_isp, kep3::MU_SUN); + + // We set the throttles + leg.set_throttles(x.begin(), x.end() - 2); + + std::vector retval(1 + 7 + m_nseg, 0.); + // Fitness + retval[0] = -mf; + // Equality Constraints + auto eq_con = leg.compute_mismatch_constraints(); + retval[1] = eq_con[0] / kep3::AU; + retval[2] = eq_con[1] / kep3::AU; + retval[3] = eq_con[2] / kep3::AU; + retval[4] = eq_con[3] / kep3::EARTH_VELOCITY; + retval[5] = eq_con[4] / kep3::EARTH_VELOCITY; + retval[6] = eq_con[5] / kep3::EARTH_VELOCITY; + retval[7] = eq_con[6] / 1e8; // + // Inequality Constraints + auto ineq_con = leg.compute_throttle_constraints(); + std::copy(ineq_con.begin(), ineq_con.end(), retval.begin() + 8); + return retval; + } + + [[nodiscard]] std::vector gradient(const std::vector &x) const + { + if (m_analytical) { + return _gradient_analytical(x); + } else { + return _gradient_numerical(x); + } + } + + [[nodiscard]] std::vector _gradient_numerical(const std::vector &x) const + { + return pagmo::estimate_gradient([this](const std::vector &x) { return this->fitness(x); }, x); + } + + [[nodiscard]] std::vector _gradient_analytical(const std::vector &x) const + { + // x = [throttles, tof (in days), mf (in kg)] + // We set the leg (avoiding the allocation for the throttles is possible but requires mutable data members.) + double tof = x[m_nseg * 3] * kep3::DAY2SEC; // in s + double mf = x[m_nseg * 3 + 1]; // in kg + kep3::leg::sims_flanagan_hf leg(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, mf, tof, m_max_thrust, + m_isp, kep3::MU_SUN); + // We set the throttles + leg.set_throttles(x.begin(), x.end() - 2); + + // We compute the gradients + auto grad_mc_all = (leg.compute_mc_grad()); + auto grad_tc = leg.compute_tc_grad(); + auto grad_mc_xf = std::get<1>(grad_mc_all); + auto grad_mc = std::move(std::get<2>(grad_mc_all)); + + // We allocate the final gradient containing all + std::vector gradient((1u + 7u + m_nseg) * (m_nseg * 3u + 2u), 0); + // Create the various xtensor objects adapting the std containers + auto xgradient + = xt::adapt(gradient, {1u + 7u + static_cast(m_nseg), static_cast(m_nseg) * 3u + 2u}); + auto xgrad_mc = xt::adapt(grad_mc, {7u, static_cast(m_nseg) * 3u + 1u}); + auto xgrad_mc_xf = xt::adapt(grad_mc_xf, {7u, 7u}); + auto xgrad_tc = xt::adapt(grad_tc, {static_cast(m_nseg), static_cast(m_nseg) * 3u}); + + // Row 1 - fitness gradient + xgradient(0, m_nseg * 3 + 1) = -1.; // fitness gradient - obj fun + // [1:4,:-1] - fitness gradient - position mismatch + xt::view(xgradient, xt::range(1u, 4u), xt::range(0, m_nseg * 3u + 1u)) + = xt::view(xgrad_mc, xt::range(0u, 3u), xt::all()) / kep3::AU; // throttles, tof + // [4:7,:-1] - fitness gradient - velocity mismatch + xt::view(xgradient, xt::range(4u, 7u), xt::range(0, m_nseg * 3u + 1u)) + = xt::view(xgrad_mc, xt::range(3u, 6u), xt::all()) / kep3::EARTH_VELOCITY; // throttles, tof + // [7:8,:-1] - fitness gradient - mass mismatch + xt::view(xgradient, xt::range(7u, 8u), xt::range(0, static_cast(m_nseg) * 3u + 1)) + = xt::view(xgrad_mc, xt::range(6u, 7u), xt::all()) / 1e8; // throttles, tof + // [8:,:-2] - fitness gradient - throttle constraints + xt::view(xgradient, xt::range(8u, 8u + static_cast(m_nseg)), + xt::range(0, static_cast(m_nseg) * 3u)) + = xgrad_tc; + + // [1:4,-1] - fitness gradient, position mismatch w.r.t. mf + xt::view(xgradient, xt::range(1u, 4u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) + = xt::view(xgrad_mc_xf, xt::range(0u, 3u), xt::range(6u, 7u)) / kep3::AU; // mf + // [4:7,-1] - fitness gradient - velocity mismatch w.r.t. mf + xt::view(xgradient, xt::range(4u, 7u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) + = xt::view(xgrad_mc_xf, xt::range(3u, 6u), xt::range(6u, 7u)) / kep3::EARTH_VELOCITY; // mf + // [7:8,-1] - fitness gradient - mass mismatch w.r.t. mf + xt::view(xgradient, xt::range(7u, 8u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) + = xt::view(xgrad_mc_xf, xt::range(6u, 7u), xt::range(6u, 7u)) / 1e8; // mf + + // Units for the tof + xt::view(xgradient, xt::all(), xt::range(m_nseg * 3u, m_nseg * 3u + 1u)) *= kep3::DAY2SEC; + return gradient; + } + + [[nodiscard]] std::pair, std::vector> get_bounds() const + { + // x = [throttles, tof (in days), mf (in kg)] + std::vector lb(m_nseg * 3 + 2, -1.); + std::vector ub(m_nseg * 3 + 2, +1.); + lb[m_nseg * 3] = 1.; // days + ub[m_nseg * 3] = 2500.; // days + lb[m_nseg * 3 + 1] = m_ms / 2.; // kg + ub[m_nseg * 3 + 1] = m_ms; // kg + return {lb, ub}; + } + + [[nodiscard]] static std::vector::size_type get_nec() + { + return 7u; + } + + [[nodiscard]] std::vector::size_type get_nic() const + { + return m_nseg; + } + + std::array, 2> m_rvs{}; + std::array, 2> m_rvf{}; + double m_ms{}; + double m_max_thrust{}; + double m_isp{}; + std::size_t m_nseg{}; + bool m_analytical{}; +}; + +#endif \ No newline at end of file From 77c1b547f3a0c29d3c492d9cd8e6e93359b3782a Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 18 Oct 2024 15:08:27 +0200 Subject: [PATCH 12/22] Solved bug with benchmark run for SF HF leg. --- src/leg/sims_flanagan_hf.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 32358b44..35a529c2 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -163,6 +163,11 @@ void sims_flanagan_hf::set_throttles(std::vector::const_iterator it1, st m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; + + // Convert throttles to current_thrusts. + auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; + m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles + std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } void sims_flanagan_hf::set_rvf(std::array, 2> rv) { @@ -736,7 +741,7 @@ sims_flanagan_hf::compute_mc_grad() const std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); std::tie(grad_rvm, grad_rvm_bck, grad_final) = get_relevant_gradients(dxdx_per_seg, dxdu_per_seg, dxdtof_per_seg); - return {grad_rvm, grad_rvm_bck, grad_final}; + return {grad_rvm, grad_rvm_bck, std::move(grad_final)}; } std::vector sims_flanagan_hf::compute_tc_grad() const From f7bf1deec0b80ae64d17f760c8a67f20537f0358 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 25 Oct 2024 15:40:28 +0200 Subject: [PATCH 13/22] Solved various performance issues. Numerous small improvements. --- benchmark/leg_sims_flanagan_benchmark.cpp | 20 +-- benchmark/leg_sims_flanagan_hf_benchmark.cpp | 165 ++++++++++++++----- benchmark/leg_sims_flanagan_hf_udp_bench.hpp | 150 ++++++++++------- include/kep3/leg/sims_flanagan_hf.hpp | 10 +- src/leg/sims_flanagan_hf.cpp | 27 ++- test/leg_sims_flanagan_hf_helpers.hpp | 17 +- 6 files changed, 268 insertions(+), 121 deletions(-) diff --git a/benchmark/leg_sims_flanagan_benchmark.cpp b/benchmark/leg_sims_flanagan_benchmark.cpp index eaa1cc10..e49dc394 100644 --- a/benchmark/leg_sims_flanagan_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_benchmark.cpp @@ -87,7 +87,7 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); - // We construct the random chromosmes + // We construct the random chromosmes const pagmo::population pop{prob_a, pop_size}; // First we time the analytical gradients @@ -97,7 +97,7 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) } auto stop = high_resolution_clock::now(); auto duration = duration_cast(stop - start); - count_a+=static_cast(duration.count()) / 1e6; + count_a += static_cast(duration.count()) / 1e6; // then the numerical ones start = high_resolution_clock::now(); @@ -106,7 +106,7 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) } stop = high_resolution_clock::now(); duration = duration_cast(stop - start); - count_n+=static_cast(duration.count()) / 1e6; + count_n += static_cast(duration.count()) / 1e6; } fmt::print("{} nseg - timing: analytical {} - numerical {}", nseg, count_a, count_n); } @@ -194,18 +194,18 @@ void perform_test_convergence(unsigned N, unsigned nseg) int main() { - // performing tests - fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); - perform_test_convergence(200, 5); - perform_test_convergence(200, 10); - perform_test_convergence(200, 15); - fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:"); perform_test_speed(100, 5, 10); perform_test_speed(100, 10, 10); perform_test_speed(100, 15, 10); perform_test_speed(100, 20, 10); perform_test_speed(100, 70, 10); - fmt::print("\n"); + // performing tests + fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); + perform_test_convergence(200, 5); + perform_test_convergence(200, 10); + perform_test_convergence(200, 15); + + fmt::print("\n"); } \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_hf_benchmark.cpp b/benchmark/leg_sims_flanagan_hf_benchmark.cpp index 35d74d14..13ff6529 100644 --- a/benchmark/leg_sims_flanagan_hf_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_hf_benchmark.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,60 @@ using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::microseconds; +void perform_single_nogradient_speed_test() +{ + std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; + std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; + double m_ms = 1; + double m_mf = m_ms * 13 / 15; + double m_isp = 1; + double m_max_thrust = 1; + double m_cut = 0.5; + double m_mu = 1; + double m_tof = 1; + std::vector m_throttles = {0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1}; + + auto start_con = high_resolution_clock::now(); + auto sf_leg + = kep3::leg::sims_flanagan(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, m_cut); + auto stop_con = high_resolution_clock::now(); + auto duration_con = duration_cast(stop_con - start_con); + fmt::print("\nLow-fidelity leg construction: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(duration_con.count()) / 1e6); + auto start = high_resolution_clock::now(); + auto mc = sf_leg.compute_mismatch_constraints(); + auto stop = high_resolution_clock::now(); + auto duration = duration_cast(stop - start); + fmt::print("\nLow-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(duration.count()) / 1e6); + auto two_start = high_resolution_clock::now(); + auto two_mc = sf_leg.compute_mc_grad(); + auto two_stop = high_resolution_clock::now(); + auto two_duration = duration_cast(two_stop - two_start); + fmt::print("\nLow-fidelity leg mc_grad: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(two_duration.count()) / 1e6); + + auto start_hf_con = high_resolution_clock::now(); + auto sf_hf_leg = kep3::leg::sims_flanagan_hf(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, + m_mu, m_cut, 1e-16); + auto stop_hf_con = high_resolution_clock::now(); + auto duration_hf_con = duration_cast(stop_hf_con - start_hf_con); + fmt::print("\nHigh-fidelity leg construction: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(duration_hf_con.count()) / 1e6); + auto hf_start = high_resolution_clock::now(); + auto hf_mc = sf_hf_leg.compute_mismatch_constraints(); + auto hf_stop = high_resolution_clock::now(); + auto hf_duration = duration_cast(hf_stop - hf_start); + fmt::print("\nHigh-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(hf_duration.count()) / 1e6); + auto hf_two_start = high_resolution_clock::now(); + auto hf_two_mc = sf_hf_leg.compute_mc_grad(); + auto hf_two_stop = high_resolution_clock::now(); + auto hf_two_duration = duration_cast(hf_two_stop - hf_two_start); + fmt::print("\nHigh-fidelity leg mc_grad: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(hf_two_duration.count()) / 1e6); +} + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) { @@ -42,9 +97,9 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) // // Distributions // - std::uniform_real_distribution dv_pert_d(0., 1000.); - std::uniform_real_distribution mass_d(500, 1500); - std::uniform_real_distribution tof_d(1000, 1500); + std::uniform_real_distribution dv_pert_d(0., 0.1); + std::uniform_real_distribution mass_d(0.9, 1.1); + std::uniform_real_distribution tof_d(0.9, 1.1); std::uniform_real_distribution ts_d(1100, 1300); // We construct the solver @@ -54,7 +109,7 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) uda.set_ftol_abs(1e-8); uda.set_maxeval(1000); pagmo::algorithm algo{uda}; - algo.set_verbosity(0u); + // algo.set_verbosity(5u); // The initial positions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); @@ -64,30 +119,43 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) double count_a = 0; double count_n = 0; std::cout << "\n"; + auto rvs = earth.eph(1000); + auto rvf = jupiter.eph(1000); + double mass = 1; + auto bench_udp_a = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, true}; + auto bench_udp_n = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, false}; + for (auto i = 0u; i < N; ++i) { // And some epochs / tofs. - const double tof_days = tof_d(rng_engine); - const double tof = tof_days * kep3::DAY2SEC; - const double ts = ts_d(rng_engine); - const double mass = mass_d(rng_engine); - auto rvs = earth.eph(ts); - auto rvf = jupiter.eph(ts + tof_days); + double ts = ts_d(rng_engine); + rvs = earth.eph(ts); + rvf = jupiter.eph(ts); + mass = mass_d(rng_engine); // We create a ballistic arc matching the two. - const kep3::lambert_problem lp{rvs[0], rvf[0], tof, kep3::MU_SUN}; - rvs[1][0] = lp.get_v0()[0][0]; - rvs[1][1] = lp.get_v0()[0][1]; - rvs[1][2] = lp.get_v0()[0][2]; + rvs[0][0] /= kep3::AU; + rvs[0][1] /= kep3::AU; + rvs[0][2] /= kep3::AU; + rvf[0][0] /= kep3::AU; + rvf[0][1] /= kep3::AU; + rvf[0][2] /= kep3::AU; + const double tof_days = tof_d(rng_engine); + const kep3::lambert_problem lp{rvs[0], rvf[0], tof_days, 1.0}; + rvs[1][0] = lp.get_v0()[0][0] + dv_pert_d(rng_engine); + rvs[1][1] = lp.get_v0()[0][1] + dv_pert_d(rng_engine); + rvs[1][2] = lp.get_v0()[0][2] + dv_pert_d(rng_engine); rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); // We construct two problems (analytical gradient and numerical gradient) - pagmo::problem prob_a{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, true}}; - pagmo::problem prob_n{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, false}}; + bench_udp_a.set_leg(rvs, mass, rvf, 1, 1, nseg, true); + bench_udp_n.set_leg(rvs, mass, rvf, 1, 1, nseg, false); + pagmo::problem prob_a{bench_udp_a}; + pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); - // We construct the random chromosmes + // We construct the random chromosmes const pagmo::population pop{prob_a, pop_size}; // First we time the analytical gradients @@ -97,7 +165,7 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) } auto stop = high_resolution_clock::now(); auto duration = duration_cast(stop - start); - count_a+=static_cast(duration.count()) / 1e6; + count_a += static_cast(duration.count()) / 1e6; // then the numerical ones start = high_resolution_clock::now(); @@ -106,7 +174,7 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) } stop = high_resolution_clock::now(); duration = duration_cast(stop - start); - count_n+=static_cast(duration.count()) / 1e6; + count_n += static_cast(duration.count()) / 1e6; } fmt::print("{} nseg - timing: analytical {} - numerical {}", nseg, count_a, count_n); } @@ -122,9 +190,9 @@ void perform_test_convergence(unsigned N, unsigned nseg) // // Distributions // - std::uniform_real_distribution dv_pert_d(0., 1000.); - std::uniform_real_distribution mass_d(500, 1500); - std::uniform_real_distribution tof_d(1000, 1500); + std::uniform_real_distribution dv_pert_d(0., 0.1); + std::uniform_real_distribution mass_d(0.9, 1.1); + std::uniform_real_distribution tof_d(0.9, 1.1); std::uniform_real_distribution ts_d(1100, 1300); // We construct the solver @@ -134,7 +202,7 @@ void perform_test_convergence(unsigned N, unsigned nseg) uda.set_ftol_abs(0); uda.set_maxeval(1000); pagmo::algorithm algo{uda}; - algo.set_verbosity(0u); + // algo.set_verbosity(5u); // The initial positions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); @@ -144,26 +212,40 @@ void perform_test_convergence(unsigned N, unsigned nseg) unsigned count_a = 0; unsigned count_n = 0; std::cout << "\n"; + // Create sf_hf_leg outside loop to save time + auto rvs = earth.eph(1000); + auto rvf = jupiter.eph(1000); + double mass = 1; + auto bench_udp_a = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, true}; + auto bench_udp_n = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, false}; + for (auto i = 0u; i < N; ++i) { // And some epochs / tofs. - const double tof_days = tof_d(rng_engine); - const double tof = tof_days * kep3::DAY2SEC; double ts = ts_d(rng_engine); - const double mass = mass_d(rng_engine); - auto rvs = earth.eph(ts); - auto rvf = jupiter.eph(ts + tof_days); + rvs = earth.eph(ts); + rvf = jupiter.eph(ts); + mass = mass_d(rng_engine); // We create a ballistic arc matching the two. - const kep3::lambert_problem lp{rvs[0], rvf[0], tof, kep3::MU_SUN}; - rvs[1][0] = lp.get_v0()[0][0]; - rvs[1][1] = lp.get_v0()[0][1]; - rvs[1][2] = lp.get_v0()[0][2]; + rvs[0][0] /= kep3::AU; + rvs[0][1] /= kep3::AU; + rvs[0][2] /= kep3::AU; + rvf[0][0] /= kep3::AU; + rvf[0][1] /= kep3::AU; + rvf[0][2] /= kep3::AU; + const double tof_days = tof_d(rng_engine); + const kep3::lambert_problem lp{rvs[0], rvf[0], tof_days, 1.0}; + rvs[1][0] = lp.get_v0()[0][0] + dv_pert_d(rng_engine); + rvs[1][1] = lp.get_v0()[0][1] + dv_pert_d(rng_engine); + rvs[1][2] = lp.get_v0()[0][2] + dv_pert_d(rng_engine); rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); // We construct two problems (analytical gradient and numerical gradient) - pagmo::problem prob_a{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, true}}; - pagmo::problem prob_n{sf_hf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, false}}; + bench_udp_a.set_leg(rvs, mass, rvf, 1, 1, nseg, true); + bench_udp_n.set_leg(rvs, mass, rvf, 1, 1, nseg, false); + pagmo::problem prob_a{bench_udp_a}; + pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); @@ -194,11 +276,8 @@ void perform_test_convergence(unsigned N, unsigned nseg) int main() { - // performing tests - fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); - perform_test_convergence(200, 5); - perform_test_convergence(200, 10); - perform_test_convergence(200, 15); + fmt::print("\nComputes the speed of a single compute_mismatch_constraints() run for a lf and hf leg."); + perform_single_nogradient_speed_test(); fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:"); perform_test_speed(100, 5, 10); @@ -206,6 +285,12 @@ int main() perform_test_speed(100, 15, 10); perform_test_speed(100, 20, 10); perform_test_speed(100, 70, 10); - fmt::print("\n"); + // performing tests + fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); + perform_test_convergence(200, 5); + perform_test_convergence(200, 10); + perform_test_convergence(200, 15); + + fmt::print("\n"); } \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp index 72975069..27a8f7c1 100644 --- a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp +++ b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp @@ -25,110 +25,139 @@ struct sf_hf_bench_udp { sf_hf_bench_udp() = default; sf_hf_bench_udp(std::array, 2> rvs, double ms, std::array, 2> rvf, - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - double max_thrust, double isp, unsigned nseg, bool analytical) - : m_rvs(rvs), m_rvf(rvf), m_ms(ms), m_max_thrust(max_thrust), m_isp(isp), m_nseg(nseg), m_analytical(analytical) + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + double max_thrust, double isp, unsigned nseg, bool analytical) + : m_rvs(rvs), m_rvf(rvf), m_ms(ms), m_max_thrust(max_thrust), m_isp(isp), m_nseg(nseg), + m_analytical(analytical), + m_leg(kep3::leg::sims_flanagan_hf(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, 0.0, 0.0, + m_max_thrust, m_isp, 1.0, 0.5, 1e-16)) { } + [[nodiscard]] void create_leg(std::array, 2> rvs, double ms, + std::array, 2> rvf, + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + double max_thrust, double isp, unsigned nseg, bool analytical) + { + m_rvs = rvs; + m_rvf = rvf; + m_ms = ms; + m_max_thrust = max_thrust; + m_isp = isp; + m_nseg = nseg; + m_analytical = analytical; + m_leg = kep3::leg::sims_flanagan_hf(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, 0.0, 0.0, + m_max_thrust, m_isp, 1.0, 0.5, 1e-16); + } + + [[nodiscard]] void set_leg(std::array, 2> rvs, double ms, + std::array, 2> rvf, + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + double max_thrust, double isp, unsigned nseg, bool analytical) + { + m_rvs = rvs; + m_rvf = rvf; + m_ms = ms; + m_max_thrust = max_thrust; + m_isp = isp; + m_nseg = nseg; + m_analytical = analytical; + m_leg.set(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, 0.0, 0.0, m_max_thrust, m_isp, 1.0, 0.5, + 1e-16); + } [[nodiscard]] std::vector fitness(const std::vector &x) const { // x = [throttles, tof (in days), mf (in kg)] // We set the leg (avoiding the allocation for the throttles is possible but requires mutable data members.) - double tof = x[m_nseg * 3] * kep3::DAY2SEC; // in s - double mf = x[m_nseg * 3 + 1]; // in kg - kep3::leg::sims_flanagan_hf leg(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, mf, tof, m_max_thrust, - m_isp, kep3::MU_SUN); + double tof = x[m_nseg * 3]; // in s + double mf = x[m_nseg * 3 + 1]; // in kg + m_leg.set_tof(tof); + m_leg.set_mf(mf); // We set the throttles - leg.set_throttles(x.begin(), x.end() - 2); + m_leg.set_throttles(x.begin(), x.end() - 2); std::vector retval(1 + 7 + m_nseg, 0.); // Fitness retval[0] = -mf; // Equality Constraints - auto eq_con = leg.compute_mismatch_constraints(); - retval[1] = eq_con[0] / kep3::AU; - retval[2] = eq_con[1] / kep3::AU; - retval[3] = eq_con[2] / kep3::AU; - retval[4] = eq_con[3] / kep3::EARTH_VELOCITY; - retval[5] = eq_con[4] / kep3::EARTH_VELOCITY; - retval[6] = eq_con[5] / kep3::EARTH_VELOCITY; - retval[7] = eq_con[6] / 1e8; // + auto eq_con = m_leg.compute_mismatch_constraints(); + retval[1] = eq_con[0]; + retval[2] = eq_con[1]; + retval[3] = eq_con[2]; + retval[4] = eq_con[3]; + retval[5] = eq_con[4]; + retval[6] = eq_con[5]; + retval[7] = eq_con[6]; // Inequality Constraints - auto ineq_con = leg.compute_throttle_constraints(); + auto ineq_con = m_leg.compute_throttle_constraints(); std::copy(ineq_con.begin(), ineq_con.end(), retval.begin() + 8); return retval; } - [[nodiscard]] std::vector gradient(const std::vector &x) const - { + [[nodiscard]] std::vector gradient(const std::vector &x) const + { if (m_analytical) { return _gradient_analytical(x); } else { return _gradient_numerical(x); } - } + } [[nodiscard]] std::vector _gradient_numerical(const std::vector &x) const { - return pagmo::estimate_gradient([this](const std::vector &x) { return this->fitness(x); }, x); + auto num_grad = pagmo::estimate_gradient([this](const std::vector &x) { return this->fitness(x); }, x); + return num_grad; } [[nodiscard]] std::vector _gradient_analytical(const std::vector &x) const { // x = [throttles, tof (in days), mf (in kg)] // We set the leg (avoiding the allocation for the throttles is possible but requires mutable data members.) - double tof = x[m_nseg * 3] * kep3::DAY2SEC; // in s - double mf = x[m_nseg * 3 + 1]; // in kg - kep3::leg::sims_flanagan_hf leg(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, mf, tof, m_max_thrust, - m_isp, kep3::MU_SUN); + double tof = x[m_nseg * 3]; // in s + double mf = x[m_nseg * 3 + 1]; // in kg + m_leg.set_tof(tof); + m_leg.set_mf(mf); // We set the throttles - leg.set_throttles(x.begin(), x.end() - 2); + m_leg.set_throttles(x.begin(), x.end() - 2); // We compute the gradients - auto grad_mc_all = (leg.compute_mc_grad()); - auto grad_tc = leg.compute_tc_grad(); - auto grad_mc_xf = std::get<1>(grad_mc_all); - auto grad_mc = std::move(std::get<2>(grad_mc_all)); - - // We allocate the final gradient containing all + std::array grad_rvm = {0}; + std::array grad_rvm_bck = {0}; + std::vector grad_final(static_cast(7) * (m_nseg * 3u + 1u), 0.); + std::tie(grad_rvm, grad_rvm_bck, grad_final) = m_leg.compute_mc_grad(); + auto xgrad_rvm = xt::adapt(grad_rvm, {7u, 7u}); + auto xgrad_rvm_bck = xt::adapt(grad_rvm_bck, {7u, 7u}); + auto xgrad_final = xt::adapt(grad_final, {7u, static_cast(m_nseg) * 3u + 1u}); + + std::vector grad_tc = m_leg.compute_tc_grad(); + auto xt_grad_tc = xt::adapt(grad_tc, {m_nseg, 3u * m_nseg}); + + // Initialise gradient std::vector gradient((1u + 7u + m_nseg) * (m_nseg * 3u + 2u), 0); // Create the various xtensor objects adapting the std containers auto xgradient = xt::adapt(gradient, {1u + 7u + static_cast(m_nseg), static_cast(m_nseg) * 3u + 2u}); - auto xgrad_mc = xt::adapt(grad_mc, {7u, static_cast(m_nseg) * 3u + 1u}); - auto xgrad_mc_xf = xt::adapt(grad_mc_xf, {7u, 7u}); - auto xgrad_tc = xt::adapt(grad_tc, {static_cast(m_nseg), static_cast(m_nseg) * 3u}); - // Row 1 - fitness gradient xgradient(0, m_nseg * 3 + 1) = -1.; // fitness gradient - obj fun - // [1:4,:-1] - fitness gradient - position mismatch xt::view(xgradient, xt::range(1u, 4u), xt::range(0, m_nseg * 3u + 1u)) - = xt::view(xgrad_mc, xt::range(0u, 3u), xt::all()) / kep3::AU; // throttles, tof - // [4:7,:-1] - fitness gradient - velocity mismatch + = xt::view(xgrad_final, xt::range(0u, 3u), xt::all()); // dmc/du xt::view(xgradient, xt::range(4u, 7u), xt::range(0, m_nseg * 3u + 1u)) - = xt::view(xgrad_mc, xt::range(3u, 6u), xt::all()) / kep3::EARTH_VELOCITY; // throttles, tof - // [7:8,:-1] - fitness gradient - mass mismatch - xt::view(xgradient, xt::range(7u, 8u), xt::range(0, static_cast(m_nseg) * 3u + 1)) - = xt::view(xgrad_mc, xt::range(6u, 7u), xt::all()) / 1e8; // throttles, tof - // [8:,:-2] - fitness gradient - throttle constraints - xt::view(xgradient, xt::range(8u, 8u + static_cast(m_nseg)), - xt::range(0, static_cast(m_nseg) * 3u)) - = xgrad_tc; - - // [1:4,-1] - fitness gradient, position mismatch w.r.t. mf + = xt::view(xgrad_final, xt::range(3u, 6u), xt::all()); // dmc/du + xt::view(xgradient, xt::range(7u, 8u), xt::range(0, m_nseg * 3u + 1u)) + = xt::view(xgrad_final, xt::range(6u, 7u), xt::all()); // dmc/du + xt::view(xgradient, xt::range(1u, 4u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) - = xt::view(xgrad_mc_xf, xt::range(0u, 3u), xt::range(6u, 7u)) / kep3::AU; // mf - // [4:7,-1] - fitness gradient - velocity mismatch w.r.t. mf + = xt::view(xgrad_rvm_bck, xt::range(0u, 3u), xt::range(6u, 7u)); // dmc/dm_f xt::view(xgradient, xt::range(4u, 7u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) - = xt::view(xgrad_mc_xf, xt::range(3u, 6u), xt::range(6u, 7u)) / kep3::EARTH_VELOCITY; // mf - // [7:8,-1] - fitness gradient - mass mismatch w.r.t. mf + = xt::view(xgrad_rvm_bck, xt::range(3u, 6u), xt::range(6u, 7u)); // dmc/dm_f xt::view(xgradient, xt::range(7u, 8u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) - = xt::view(xgrad_mc_xf, xt::range(6u, 7u), xt::range(6u, 7u)) / 1e8; // mf + = xt::view(xgrad_rvm_bck, xt::range(6u, 7u), xt::range(6u, 7u)); // dmc/dm_f + xt::view(xgradient, xt::range(8u, 8u + m_nseg), xt::range(0, m_nseg * 3u)) + = xt::view(xt_grad_tc, xt::all(), xt::all()); // throttle constraints + + xt::view(xgradient, xt::all(), xt::range(m_nseg * 3u, m_nseg * 3u + 1u)); - // Units for the tof - xt::view(xgradient, xt::all(), xt::range(m_nseg * 3u, m_nseg * 3u + 1u)) *= kep3::DAY2SEC; return gradient; } @@ -137,10 +166,10 @@ struct sf_hf_bench_udp { // x = [throttles, tof (in days), mf (in kg)] std::vector lb(m_nseg * 3 + 2, -1.); std::vector ub(m_nseg * 3 + 2, +1.); - lb[m_nseg * 3] = 1.; // days - ub[m_nseg * 3] = 2500.; // days - lb[m_nseg * 3 + 1] = m_ms / 2.; // kg - ub[m_nseg * 3 + 1] = m_ms; // kg + lb[m_nseg * 3] = 0.5; // days + ub[m_nseg * 3] = 1.5; // days + lb[m_nseg * 3 + 1] = 0.5; // kg + ub[m_nseg * 3 + 1] = 1; // kg return {lb, ub}; } @@ -161,6 +190,7 @@ struct sf_hf_bench_udp { double m_isp{}; std::size_t m_nseg{}; bool m_analytical{}; + mutable kep3::leg::sims_flanagan_hf m_leg{}; }; #endif \ No newline at end of file diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index e4c92646..9b317f0d 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -182,11 +182,13 @@ class kep3_DLL_PUBLIC sims_flanagan_hf unsigned m_nseg_fwd = 1u; unsigned m_nseg_bck = 1u; // We introduce ta from cache - const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); - mutable heyoka::taylor_adaptive m_tas = ta_cache; + // const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); + // mutable heyoka::taylor_adaptive m_tas = ta_cache; + mutable heyoka::taylor_adaptive m_tas{}; // Introduce variational ta from cache - const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); - mutable heyoka::taylor_adaptive m_tas_var = ta_var_cache; + // const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); + // mutable heyoka::taylor_adaptive m_tas_var = ta_var_cache; + mutable heyoka::taylor_adaptive m_tas_var{}; }; // Streaming operator for the class kep3::leg::sims_flanagan. diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 35a529c2..c2ef73c2 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -47,6 +47,13 @@ sims_flanagan_hf::sims_flanagan_hf() { // We perform some sanity checks on the user provided inputs _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + + // Initialize m_tas and m_tas_var + const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); + m_tas = ta_cache; + const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); + m_tas_var = ta_var_cache; + // We set mu and veff for the non variational *m_tas.get_pars_data() = m_mu; *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; @@ -74,6 +81,13 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array, 2> &r { // We perform some sanity checks on the user provided inputs _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + + // Initialize m_tas and m_tas_var + const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); + m_tas = ta_cache; + const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); + m_tas_var = ta_var_cache; + // We set mu and veff for the non variational *m_tas.get_pars_data() = m_mu; *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; @@ -107,6 +121,13 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array &rvms, std::vecto { // We perform some sanity checks on the user provided inputs _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + + // Initialize m_tas and m_tas_var + const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); + m_tas = ta_cache; + const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); + m_tas_var = ta_var_cache; + // We set mu and veff for the non variational *m_tas.get_pars_data() = m_mu; *(m_tas.get_pars_data() + 1) = m_isp * kep3::G0; @@ -505,9 +526,9 @@ std::array sims_flanagan_hf::get_state_derivative(std::array, 2> rvs, double ms, std::vector throttles, + std::array, 2> rvf, double mf, double tof, double max_thrust, + double isp, double mu, double cut, double tol) + : m_rvs(rvs), m_ms(ms), m_throttles(throttles), m_rvf(rvf), m_mf(mf), m_tof(tof), m_max_thrust(max_thrust), + m_isp(isp), m_mu(mu), m_cut(cut), m_tol(tol) + { + for (double m_throttle : m_throttles) { + m_thrusts.push_back(m_throttle * m_max_thrust); + } + } + // Retrieve mismatch constraints from manual heyoka Taylor adaptive integrator [[nodiscard]] std::array compute_manual_mc() { @@ -170,10 +181,8 @@ struct sf_hf_test_object { std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; std::vector m_thrusts; double m_tol = 1e-16; - std::vector m_rvms - = {m_rvs[0][0], m_rvs[0][1], m_rvs[0][2], m_rvs[1][0], m_rvs[1][1], m_rvs[1][2], m_ms}; - std::vector m_rvmf - = {m_rvf[0][0], m_rvf[0][1], m_rvf[0][2], m_rvf[1][0], m_rvf[1][1], m_rvf[1][2], m_mf}; + std::vector m_rvms = {m_rvs[0][0], m_rvs[0][1], m_rvs[0][2], m_rvs[1][0], m_rvs[1][1], m_rvs[1][2], m_ms}; + std::vector m_rvmf = {m_rvf[0][0], m_rvf[0][1], m_rvf[0][2], m_rvf[1][0], m_rvf[1][1], m_rvf[1][2], m_mf}; }; #endif \ No newline at end of file From c490ca855f9a74431879107c525b22a2c73a0604 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Mon, 28 Oct 2024 15:27:41 +0100 Subject: [PATCH 14/22] Small improvements; changing return types from value to const ref. --- benchmark/leg_sims_flanagan_hf_udp_bench.hpp | 1 + include/kep3/leg/sf_checks.hpp | 2 + include/kep3/leg/sims_flanagan.hpp | 35 +++++++------- include/kep3/leg/sims_flanagan_hf.hpp | 51 +++++++++----------- src/leg/sims_flanagan.cpp | 11 +++-- src/leg/sims_flanagan_hf.cpp | 51 +++++++++++--------- 6 files changed, 77 insertions(+), 74 deletions(-) diff --git a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp index 27a8f7c1..b20a829a 100644 --- a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp +++ b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp @@ -190,6 +190,7 @@ struct sf_hf_bench_udp { double m_isp{}; std::size_t m_nseg{}; bool m_analytical{}; + // m_leg needs to be mutable because the heyoka integrator needs to be modifiable mutable kep3::leg::sims_flanagan_hf m_leg{}; }; diff --git a/include/kep3/leg/sf_checks.hpp b/include/kep3/leg/sf_checks.hpp index 96608cb3..46628b50 100644 --- a/include/kep3/leg/sf_checks.hpp +++ b/include/kep3/leg/sf_checks.hpp @@ -12,6 +12,8 @@ #include +// These checks are used for the low- and high-fidelity legs (in sims_flanagan.cpp and sims_flanagan_hf.cpp) + void _check_tof(double tof); void _check_throttles(const std::vector &throttles, unsigned nseg); void _check_max_thrust(double max_thrust); diff --git a/include/kep3/leg/sims_flanagan.hpp b/include/kep3/leg/sims_flanagan.hpp index f8847b5d..934d8c69 100644 --- a/include/kep3/leg/sims_flanagan.hpp +++ b/include/kep3/leg/sims_flanagan.hpp @@ -42,11 +42,11 @@ class kep3_DLL_PUBLIC sims_flanagan // Setters void set_tof(double tof); - void set_rvs(std::array, 2> rv); + void set_rvs(const std::array, 2> &rv); void set_ms(double mass); - void set_throttles(std::vector throttles); - void set_throttles(std::vector::const_iterator it1, std::vector::const_iterator it2); - void set_rvf(std::array, 2> rv); + void set_throttles(const std::vector &throttles); + void set_throttles(const std::vector::const_iterator &it1, const std::vector::const_iterator &it2); + void set_rvf(const std::array, 2> &rv); void set_mf(double mass); void set_max_thrust(double max_thrust); void set_isp(double isp); @@ -71,7 +71,6 @@ class kep3_DLL_PUBLIC sims_flanagan [[nodiscard]] unsigned get_nseg_fwd() const; [[nodiscard]] unsigned get_nseg_bck() const; - // Compute constraints [[nodiscard]] std::array compute_mismatch_constraints() const; [[nodiscard]] std::vector compute_throttle_constraints() const; @@ -104,19 +103,19 @@ class kep3_DLL_PUBLIC sims_flanagan template void serialize(Archive &ar, const unsigned int) { - ar &m_rvs; - ar &m_ms; - ar &m_throttles; - ar &m_tof; - ar &m_rvf; - ar &m_mf; - ar &m_max_thrust; - ar &m_isp; - ar &m_mu; - ar &m_cut; - ar &m_nseg; - ar &m_nseg_fwd; - ar &m_nseg_bck; + ar & m_rvs; + ar & m_ms; + ar & m_throttles; + ar & m_tof; + ar & m_rvf; + ar & m_mf; + ar & m_max_thrust; + ar & m_isp; + ar & m_mu; + ar & m_cut; + ar & m_nseg; + ar & m_nseg_fwd; + ar & m_nseg_bck; } // Initial spacecraft state. diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 9b317f0d..25fd69b3 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -54,22 +54,21 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Setters void set_tof(double tof); - void set_rvs(std::array, 2> rv); + void set_rvs(const std::array, 2> &rv); void set_ms(double mass); - void set_throttles(std::vector throttles); - void set_throttles(std::vector::const_iterator it1, std::vector::const_iterator it2); - void set_rvf(std::array, 2> rv); + void set_throttles(const std::vector &throttles); + void set_throttles(const std::vector::const_iterator &it1, const std::vector::const_iterator &it2); + void set_rvf(const std::array, 2> &rv); void set_mf(double mass); void set_max_thrust(double max_thrust); void set_isp(double isp); void set_mu(double mu); void set_cut(double cut); void set_tol(double tol); - void set_rvms(std::array rvms); - void set_rvmf(std::array rvmf); - void set_tas(heyoka::taylor_adaptive tas); - void set_tas_var(heyoka::taylor_adaptive tas_var); - void set_walking_rvm(std::array rvm); + void set_rvms(const std::array &rvms); + void set_rvmf(const std::array &rvmf); + void set_tas(const heyoka::taylor_adaptive &tas); + void set_tas_var(const heyoka::taylor_adaptive &tas_var); // Backwards-compatible setting function with rv and m states separately void set(const std::array, 2> &rvs, double ms, const std::vector &throttles, const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, @@ -95,20 +94,20 @@ class kep3_DLL_PUBLIC sims_flanagan_hf [[nodiscard]] unsigned get_nseg() const; [[nodiscard]] unsigned get_nseg_fwd() const; [[nodiscard]] unsigned get_nseg_bck() const; - [[nodiscard]] heyoka::taylor_adaptive get_tas() const; - [[nodiscard]] heyoka::taylor_adaptive get_tas_var() const; - [[nodiscard]] std::array get_rvms() const; - [[nodiscard]] std::array get_rvmf() const; - [[nodiscard]] std::array get_walking_rvm() const; + [[nodiscard]] const heyoka::taylor_adaptive &get_tas() const; + [[nodiscard]] const heyoka::taylor_adaptive &get_tas_var() const; + [[nodiscard]] const std::array &get_rvms() const; + [[nodiscard]] const std::array &get_rvmf() const; // Compute constraints - [[nodiscard]] std::array get_state_derivative(std::array state, - std::array throttles) const; [[nodiscard]] std::array compute_mismatch_constraints() const; [[nodiscard]] std::vector compute_throttle_constraints() const; - [[nodiscard]] std::vector compute_constraints(); + [[nodiscard]] std::vector compute_constraints() const; + [[nodiscard]] std::vector set_and_compute_constraints(const std::vector &chromosome); - [[nodiscard]] std::vector set_and_compute_constraints(std::vector chromosome); + // Get state derivative + [[nodiscard]] std::array get_state_derivative(const std::array &state, + const std::array &throttles) const; // Compute all gradients w.r.t. all legs [[nodiscard]] @@ -118,9 +117,9 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Process all gradients to retrieve relevant gradients (w.r.t. initial and final rvm state as well as w.r.t. // throttles and tof) [[nodiscard]] std::tuple, std::array, std::vector> - get_relevant_gradients(std::vector> &dxdx_per_seg, - std::vector> &dxdu_per_seg, - std::vector> &dxdtof_per_seg) const; + get_relevant_gradients(const std::vector> &dxdx_per_seg, + const std::vector> &dxdu_per_seg, + const std::vector> &dxdtof_per_seg) const; // Compute mismatch constraint gradients (w.r.t. initial and final rvm state as well as w.r.t. throttles and // tof) @@ -181,13 +180,11 @@ class kep3_DLL_PUBLIC sims_flanagan_hf unsigned m_nseg = 2u; unsigned m_nseg_fwd = 1u; unsigned m_nseg_bck = 1u; - // We introduce ta from cache - // const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); - // mutable heyoka::taylor_adaptive m_tas = ta_cache; + // Taylor-adaptive integrator + // m_tas needs to be mutable because the heyoka integrator needs to be modifiable mutable heyoka::taylor_adaptive m_tas{}; - // Introduce variational ta from cache - // const heyoka::taylor_adaptive ta_var_cache = kep3::ta::get_ta_stark_var(m_tol); - // mutable heyoka::taylor_adaptive m_tas_var = ta_var_cache; + // Variational Taylor-adaptive integrator + // m_tas_var needs to be mutable because the heyoka integrator needs to be modifiable mutable heyoka::taylor_adaptive m_tas_var{}; }; diff --git a/src/leg/sims_flanagan.cpp b/src/leg/sims_flanagan.cpp index 3c55e3cd..8dfebe93 100644 --- a/src/leg/sims_flanagan.cpp +++ b/src/leg/sims_flanagan.cpp @@ -27,8 +27,8 @@ #include #include #include -#include #include +#include #include namespace kep3::leg @@ -59,7 +59,7 @@ void sims_flanagan::set_tof(double tof) _check_tof(tof); m_tof = tof; } -void sims_flanagan::set_rvs(std::array, 2> rv) +void sims_flanagan::set_rvs(const std::array, 2> &rv) { m_rvs = rv; } @@ -67,7 +67,7 @@ void sims_flanagan::set_ms(double mass) { m_ms = mass; } -void sims_flanagan::set_throttles(std::vector throttles) +void sims_flanagan::set_throttles(const std::vector &throttles) { auto nseg = static_cast(throttles.size()) / 3u; _check_throttles(throttles, nseg); @@ -76,7 +76,8 @@ void sims_flanagan::set_throttles(std::vector throttles) m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; } -void sims_flanagan::set_throttles(std::vector::const_iterator it1, std::vector::const_iterator it2) +void sims_flanagan::set_throttles(const std::vector::const_iterator &it1, + const std::vector::const_iterator &it2) { if (((std::distance(it1, it2) % 3) != 0) || std::distance(it1, it2) <= 0) { throw std::logic_error("The throttles of a sims_flanagan leg are being set with invalid iterators."); @@ -87,7 +88,7 @@ void sims_flanagan::set_throttles(std::vector::const_iterator it1, std:: m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; } -void sims_flanagan::set_rvf(std::array, 2> rv) +void sims_flanagan::set_rvf(const std::array, 2> &rv) { m_rvf = rv; } diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index c2ef73c2..e5880102 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -150,7 +150,7 @@ void sims_flanagan_hf::set_tof(double tof) _check_tof(tof); m_tof = tof; } -void sims_flanagan_hf::set_rvs(std::array, 2> rv) +void sims_flanagan_hf::set_rvs(const std::array, 2> &rv) { std::copy(rv[0].begin(), rv[0].end(), m_rvms.begin()); std::copy(rv[1].begin(), rv[1].end(), std::next(m_rvms.begin(), 3)); @@ -159,7 +159,7 @@ void sims_flanagan_hf::set_ms(double mass) { m_rvms[6] = mass; } -void sims_flanagan_hf::set_throttles(std::vector throttles) +void sims_flanagan_hf::set_throttles(const std::vector &throttles) { auto nseg = static_cast(throttles.size()) / 3u; _check_throttles(throttles, nseg); @@ -173,7 +173,8 @@ void sims_flanagan_hf::set_throttles(std::vector throttles) m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } -void sims_flanagan_hf::set_throttles(std::vector::const_iterator it1, std::vector::const_iterator it2) +void sims_flanagan_hf::set_throttles(const std::vector::const_iterator &it1, + const std::vector::const_iterator &it2) { if (((std::distance(it1, it2) % 3) != 0) || std::distance(it1, it2) <= 0) { throw std::logic_error( @@ -190,7 +191,7 @@ void sims_flanagan_hf::set_throttles(std::vector::const_iterator it1, st m_thrusts.resize(m_throttles.size()); // Ensure that std::vector m_thrusts is same size as m_throttles std::transform(m_throttles.begin(), m_throttles.end(), m_thrusts.begin(), throttle_to_thrust); } -void sims_flanagan_hf::set_rvf(std::array, 2> rv) +void sims_flanagan_hf::set_rvf(const std::array, 2> &rv) { std::copy(rv[0].begin(), rv[0].end(), m_rvmf.begin()); std::copy(rv[1].begin(), rv[1].end(), std::next(m_rvmf.begin(), 3)); @@ -226,19 +227,19 @@ void sims_flanagan_hf::set_tol(double tol) _check_tol(tol); m_tol = tol; } -void sims_flanagan_hf::set_rvms(std::array rvms) +void sims_flanagan_hf::set_rvms(const std::array &rvms) { m_rvms = rvms; } -void sims_flanagan_hf::set_rvmf(std::array rvmf) +void sims_flanagan_hf::set_rvmf(const std::array &rvmf) { m_rvmf = rvmf; } -void sims_flanagan_hf::set_tas(heyoka::taylor_adaptive tas) +void sims_flanagan_hf::set_tas(const heyoka::taylor_adaptive &tas) { m_tas = tas; } -void sims_flanagan_hf::set_tas_var(heyoka::taylor_adaptive tas_var) +void sims_flanagan_hf::set_tas_var(const heyoka::taylor_adaptive &tas_var) { m_tas_var = tas_var; } @@ -378,19 +379,19 @@ unsigned sims_flanagan_hf::get_nseg_bck() const { return m_nseg_bck; } -heyoka::taylor_adaptive sims_flanagan_hf::get_tas() const +const heyoka::taylor_adaptive &sims_flanagan_hf::get_tas() const { return m_tas; } -heyoka::taylor_adaptive sims_flanagan_hf::get_tas_var() const +const heyoka::taylor_adaptive &sims_flanagan_hf::get_tas_var() const { return m_tas_var; } -std::array sims_flanagan_hf::get_rvms() const +const std::array &sims_flanagan_hf::get_rvms() const { return m_rvms; } -std::array sims_flanagan_hf::get_rvmf() const +const std::array &sims_flanagan_hf::get_rvmf() const { return m_rvmf; } @@ -467,7 +468,7 @@ std::vector sims_flanagan_hf::compute_throttle_constraints() const return retval; } -std::vector sims_flanagan_hf::compute_constraints() +std::vector sims_flanagan_hf::compute_constraints() const { std::vector retval(7 + m_nseg, 0.); // Fitness @@ -486,7 +487,7 @@ std::vector sims_flanagan_hf::compute_constraints() return retval; } -std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector chromosome) +std::vector sims_flanagan_hf::set_and_compute_constraints(const std::vector &chromosome) { std::array rvms; std::copy(chromosome.begin(), chromosome.begin() + 7, rvms.begin()); @@ -502,8 +503,8 @@ std::vector sims_flanagan_hf::set_and_compute_constraints(std::vector sims_flanagan_hf::get_state_derivative(std::array state, - std::array throttles) const +std::array sims_flanagan_hf::get_state_derivative(const std::array &state, + const std::array &throttles) const { std::array thrusts; @@ -640,12 +641,12 @@ sims_flanagan_hf::compute_all_gradients() const } std::tuple, std::array, std::vector> -sims_flanagan_hf::get_relevant_gradients(std::vector> &dxdx_per_seg, - std::vector> &dxdu_per_seg, - std::vector> &dxdtof_per_seg) const +sims_flanagan_hf::get_relevant_gradients(const std::vector> &dxdx_per_seg, + const std::vector> &dxdu_per_seg, + const std::vector> &dxdtof_per_seg) const { - auto xt_dxdx_per_seg = xt::adapt(reinterpret_cast(dxdx_per_seg.data()), {m_nseg, 49u}); + auto xt_dxdx_per_seg = xt::adapt(reinterpret_cast(dxdx_per_seg.data()), {m_nseg, 49u}); // Mn_o will contain [Mnf-1, Mnf-1@Mnf-2, Mnf-2@Mnf-3, Mnf-1@M0, Mnf, Mnf@Mnf+1, Mnf@Mnf+2, Mnf@Mn] std::vector> Mn_o(m_nseg, xt::zeros({7u, 7u})); // Fwd leg @@ -696,7 +697,8 @@ sims_flanagan_hf::get_relevant_gradients(std::vector> &d } // Throttle derivatives - xt::xarray xt_dxdu_per_seg = xt::adapt(reinterpret_cast(dxdu_per_seg.data()), {m_nseg, 21u}); + xt::xarray xt_dxdu_per_seg + = xt::adapt(reinterpret_cast(dxdu_per_seg.data()), {m_nseg, 21u}); std::vector grad_final_throttle(static_cast(7) * (m_nseg * 3u), 0.); auto xgrad_final_throttle = xt::adapt(grad_final_throttle, {7u, static_cast(m_nseg) * 3u}); xt::xarray corresponding_M; @@ -720,7 +722,8 @@ sims_flanagan_hf::get_relevant_gradients(std::vector> &d } // ToF derivatives - xt::xarray xt_dxdtof_per_seg = xt::adapt(reinterpret_cast(dxdtof_per_seg.data()), {m_nseg, 7u}); + xt::xarray xt_dxdtof_per_seg + = xt::adapt(reinterpret_cast(dxdtof_per_seg.data()), {m_nseg, 7u}); std::vector grad_final_tof(static_cast(7), 0.); auto xgrad_final_tof = xt::adapt(grad_final_tof, {7u, 1u}); for (unsigned int i(0); i < m_nseg; ++i) { @@ -820,7 +823,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in if (status != heyoka::taylor_outcome::time_limit) { throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } - output_per_seg[i] = output_states; + output_per_seg[i] = output_states; } // Backward pass @@ -853,7 +856,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in if (status != heyoka::taylor_outcome::time_limit) { throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); } - output_per_seg[m_nseg - 1 - i] = output_states; + output_per_seg[m_nseg - 1 - i] = output_states; } return output_per_seg; From d7b7f89ac6e0327e9b9c55f49b4ac3c30798e87b Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 1 Nov 2024 15:12:00 +0100 Subject: [PATCH 15/22] Small bug fix with sanity check placement. [WIP] benchmarks. --- benchmark/CMakeLists.txt | 1 + benchmark/leg_sf_benchmark_simple.cpp | 190 +++++++++++ benchmark/leg_sims_flanagan_benchmark.cpp | 271 ++++++++-------- benchmark/leg_sims_flanagan_hf_benchmark.cpp | 323 ++++++++----------- benchmark/leg_sims_flanagan_hf_udp_bench.hpp | 8 +- benchmark/leg_sims_flanagan_udp_bench.hpp | 51 +-- src/leg/sims_flanagan_hf.cpp | 4 +- 7 files changed, 502 insertions(+), 346 deletions(-) create mode 100644 benchmark/leg_sf_benchmark_simple.cpp diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index a61ee4d6..84cbf9fc 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -25,5 +25,6 @@ ADD_kep3_BENCHMARK(lambert_problem_benchmark) ADD_kep3_BENCHMARK(stm_benchmark) ADD_kep3_BENCHMARK(leg_sims_flanagan_benchmark) ADD_kep3_BENCHMARK(leg_sims_flanagan_hf_benchmark) +ADD_kep3_BENCHMARK(leg_sf_benchmark_simple) diff --git a/benchmark/leg_sf_benchmark_simple.cpp b/benchmark/leg_sf_benchmark_simple.cpp new file mode 100644 index 00000000..944cb68f --- /dev/null +++ b/benchmark/leg_sf_benchmark_simple.cpp @@ -0,0 +1,190 @@ +// Copyright 2023, 2024 Dario Izzo (dario.izzo@gmail.com), Francesco Biscani +// (bluescarni@gmail.com) +// +// This file is part of the kep3 library. +// +// This Source Code Form is subject to the terms of the Mozilla +// Public License v. 2.0. If a copy of the MPL was not distributed +// with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "leg_sims_flanagan_hf_udp_bench.hpp" +#include "leg_sims_flanagan_udp_bench.hpp" + +using std::chrono::duration_cast; +using std::chrono::high_resolution_clock; +using std::chrono::microseconds; + +void perform_single_nogradient_speed_test() +{ + std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; + std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; + double m_ms = 1; + double m_mf = m_ms * 13 / 15; + double m_isp = 1; + double m_max_thrust = 1; + double m_cut = 0.5; + double m_mu = 1; + double m_tof = 1; + std::vector m_throttles = {0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1}; + + auto start_con = high_resolution_clock::now(); + auto sf_leg + = kep3::leg::sims_flanagan(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, m_cut); + auto stop_con = high_resolution_clock::now(); + auto duration_con = duration_cast(stop_con - start_con); + fmt::print("\nLow-fidelity leg construction: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(duration_con.count()) / 1e6); + + auto start = high_resolution_clock::now(); + auto mc = sf_leg.compute_mismatch_constraints(); + auto stop = high_resolution_clock::now(); + auto duration = duration_cast(stop - start); + fmt::print("\nLow-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(duration.count()) / 1e6); + + auto two_start = high_resolution_clock::now(); + auto two_mc = sf_leg.compute_mc_grad(); + auto two_stop = high_resolution_clock::now(); + auto two_duration = duration_cast(two_stop - two_start); + fmt::print("\nLow-fidelity leg mc_grad: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(two_duration.count()) / 1e6); + + auto start_hf_con = high_resolution_clock::now(); + auto sf_hf_leg = kep3::leg::sims_flanagan_hf(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, + m_mu, m_cut, 1e-16); + auto stop_hf_con = high_resolution_clock::now(); + auto duration_hf_con = duration_cast(stop_hf_con - start_hf_con); + fmt::print("\nHigh-fidelity leg construction: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(duration_hf_con.count()) / 1e6); + + auto hf_start = high_resolution_clock::now(); + auto hf_mc = sf_hf_leg.compute_mismatch_constraints(); + auto hf_stop = high_resolution_clock::now(); + auto hf_duration = duration_cast(hf_stop - hf_start); + fmt::print("\nHigh-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(hf_duration.count()) / 1e6); + + auto hf_two_start = high_resolution_clock::now(); + auto hf_two_mc = sf_hf_leg.compute_mc_grad(); + auto hf_two_stop = high_resolution_clock::now(); + auto hf_two_duration = duration_cast(hf_two_stop - hf_two_start); + fmt::print("\nHigh-fidelity leg mc_grad: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(hf_two_duration.count()) / 1e6); + + fmt::print("\n\nBelow are the numerical and analytical gradient method calls from the UDPs.\n"); + + // Create chromosome + auto chromosome = m_throttles; + chromosome.push_back(m_tof); + chromosome.push_back(m_mf); + + // Create analytical hf benchmark + auto bench_hf_udp_a = sf_hf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), true}; + + auto agrad_start = high_resolution_clock::now(); + auto agrad = bench_hf_udp_a.gradient(chromosome); + auto agrad_stop = high_resolution_clock::now(); + auto agrad_duration = duration_cast(agrad_stop - agrad_start); + fmt::print("\nHigh-fidelity leg analytical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(agrad_duration.count()) / 1e6); + + // Create analytical benchmark + auto bench_udp_a = sf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), true}; + + auto lf_agrad_start = high_resolution_clock::now(); + auto lf_agrad = bench_udp_a.gradient(chromosome); + auto lf_agrad_stop = high_resolution_clock::now(); + auto lf_agrad_duration = duration_cast(lf_agrad_stop - lf_agrad_start); + fmt::print("\nLow-fidelity leg analytical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(lf_agrad_duration.count()) / 1e6); + + // Create numerical hf benchmark + auto bench_hf_udp_n = sf_hf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), false}; + + auto ngrad_start = high_resolution_clock::now(); + auto ngrad = bench_hf_udp_n.gradient(chromosome); + auto ngrad_stop = high_resolution_clock::now(); + auto ngrad_duration = duration_cast(ngrad_stop - ngrad_start); + fmt::print("\nHigh-fidelity leg numerical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(ngrad_duration.count()) / 1e6); + + // Create numerical benchmark + auto bench_udp_n = sf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), false}; + + auto lf_ngrad_start = high_resolution_clock::now(); + auto lf_ngrad = bench_udp_a.gradient(chromosome); + auto lf_ngrad_stop = high_resolution_clock::now(); + auto lf_ngrad_duration = duration_cast(lf_ngrad_stop - lf_ngrad_start); + fmt::print("\nLow-fidelity leg numerical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(lf_ngrad_duration.count()) / 1e6); + + fmt::print("\n\nBelow are the numerical and analytical gradient method calls from the pagmo::problems.\n"); + + // Create analytical hf benchmark + auto bench_hf_udp_a2 = sf_hf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), true}; + pagmo::problem hf_prob_a{bench_hf_udp_a2}; + + auto agrad_start2 = high_resolution_clock::now(); + auto agrad2 = hf_prob_a.gradient(chromosome); + auto agrad_stop2 = high_resolution_clock::now(); + auto agrad_duration2 = duration_cast(agrad_stop2 - agrad_start2); + fmt::print("\nPagmo problem High-fidelity leg analytical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(agrad_duration2.count()) / 1e6); + + // Create analytical benchmark + auto bench_udp_a2 = sf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), true}; + pagmo::problem prob_a{bench_udp_a2}; + + auto lf_agrad_start2 = high_resolution_clock::now(); + auto lf_agrad2 = prob_a.gradient(chromosome); + auto lf_agrad_stop2 = high_resolution_clock::now(); + auto lf_agrad_duration2 = duration_cast(lf_agrad_stop2 - lf_agrad_start2); + fmt::print("\nPagmo problem Low-fidelity leg analytical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(lf_agrad_duration2.count()) / 1e6); + + // Create numerical hf benchmark + auto bench_hf_udp_n2 = sf_hf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), false}; + pagmo::problem hf_prob_n{bench_hf_udp_n2}; + + auto ngrad_start2 = high_resolution_clock::now(); + auto ngrad2 = hf_prob_n.gradient(chromosome); + auto ngrad_stop2 = high_resolution_clock::now(); + auto ngrad_duration2 = duration_cast(ngrad_stop2 - ngrad_start2); + fmt::print("\nPagmo problem High-fidelity leg numerical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(ngrad_duration2.count()) / 1e6); + + // Create numerical benchmark + auto bench_udp_n2 = sf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), false}; + pagmo::problem prob_n{bench_udp_n2}; + + auto lf_ngrad_start2 = high_resolution_clock::now(); + auto lf_ngrad2 = prob_n.gradient(chromosome); + auto lf_ngrad_stop2 = high_resolution_clock::now(); + auto lf_ngrad_duration2 = duration_cast(lf_ngrad_stop2 - lf_ngrad_start2); + fmt::print("\nPagmo problem Low-fidelity leg numerical gradient: {} nseg - timing: {}", m_throttles.size() / 3, + static_cast(lf_ngrad_duration2.count()) / 1e6); +} + +int main() +{ + perform_single_nogradient_speed_test(); +} \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_benchmark.cpp b/benchmark/leg_sims_flanagan_benchmark.cpp index e49dc394..64f79f4e 100644 --- a/benchmark/leg_sims_flanagan_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_benchmark.cpp @@ -32,7 +32,7 @@ using std::chrono::high_resolution_clock; using std::chrono::microseconds; // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) +void perform_convergence_benchmark(uint N, uint nseg) { // // Engines @@ -42,77 +42,88 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) // // Distributions // - std::uniform_real_distribution dv_pert_d(0., 1000.); - std::uniform_real_distribution mass_d(500, 1500); - std::uniform_real_distribution tof_d(1000, 1500); - std::uniform_real_distribution ts_d(1100, 1300); - - // We construct the solver - pagmo::nlopt uda{"slsqp"}; - uda.set_xtol_abs(1e-8); - uda.set_xtol_rel(1e-8); - uda.set_ftol_abs(1e-8); - uda.set_maxeval(1000); - pagmo::algorithm algo{uda}; - algo.set_verbosity(0u); - - // The initial positions + std::uniform_real_distribution dv_pert_random(0., 0.1); + std::uniform_real_distribution mass_random(1.0, 1.2); + std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi); + std::uniform_real_distribution ts_random(1100, 1300); + + // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - double count_a = 0; - double count_n = 0; - std::cout << "\n"; - for (auto i = 0u; i < N; ++i) { - // And some epochs / tofs. - const double tof_days = tof_d(rng_engine); - const double tof = tof_days * kep3::DAY2SEC; - const double ts = ts_d(rng_engine); - const double mass = mass_d(rng_engine); - auto rvs = earth.eph(ts); - auto rvf = jupiter.eph(ts + tof_days); - // We create a ballistic arc matching the two. - const kep3::lambert_problem lp{rvs[0], rvf[0], tof, kep3::MU_SUN}; - rvs[1][0] = lp.get_v0()[0][0]; - rvs[1][1] = lp.get_v0()[0][1]; - rvs[1][2] = lp.get_v0()[0][2]; - rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); - rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); - rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); - - // We construct two problems (analytical gradient and numerical gradient) - pagmo::problem prob_a{sf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, true}}; - pagmo::problem prob_n{sf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, false}}; + // auto rvs = earth.eph(1000); + // auto rvf = jupiter.eph(1000); + int count_n = 0; + int count_a = 0; + for (uint i(0); i < N; ++i) { + auto rvs = earth.eph(ts_random(rng_engine)); + auto rvf = jupiter.eph(ts_random(rng_engine)); + // double tof_ic = kep3::pi / 2; + double tof_ic = tof_random(rng_engine); + double mu = 1; + rvs[0][0] /= kep3::AU; + rvs[0][1] /= kep3::AU; + rvs[0][2] /= kep3::AU; + rvf[0][0] /= kep3::AU; + rvf[0][1] /= kep3::AU; + rvf[0][2] /= kep3::AU; + const kep3::lambert_problem lp{rvs[0], rvf[0], tof_ic, mu}; + + // Create HF legs + std::array, 2> rvs_udp_ic = {{{lp.get_r0()[0], lp.get_r0()[1], lp.get_r0()[2]}, + {lp.get_v0()[0][0], lp.get_v0()[0][1], lp.get_v0()[0][2]}}}; + std::array, 2> rvf_udp_ic + = {{{lp.get_r1()[0], lp.get_r1()[1], lp.get_r1()[2]}, + {lp.get_v1()[0][0] + dv_pert_random(rng_engine), lp.get_v1()[0][1] + dv_pert_random(rng_engine), + lp.get_v1()[0][2] + dv_pert_random(rng_engine)}}}; + // double mass = 1; + double mass = mass_random(rng_engine); + double max_thrust = 1; + double isp = 1; + auto bench_udp_a = sf_bench_udp{rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, true}; + auto bench_udp_n = sf_bench_udp{rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, false}; + pagmo::problem prob_a{bench_udp_a}; + pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); - // We construct the random chromosmes - const pagmo::population pop{prob_a, pop_size}; + // We construct the same random population + pagmo::population pop_a{prob_a, 1u}; + pagmo::population pop_n{prob_n}; + pop_n.push_back(pop_a.get_x()[0]); - // First we time the analytical gradients - auto start = high_resolution_clock::now(); - for (decltype(pop_size) j = 0u; j < pop_size; ++j) { - prob_a.gradient(pop.get_x()[j]); - } - auto stop = high_resolution_clock::now(); - auto duration = duration_cast(stop - start); - count_a += static_cast(duration.count()) / 1e6; + // We construct the solver + pagmo::nlopt uda{"slsqp"}; + uda.set_xtol_abs(0); + uda.set_xtol_rel(0); + uda.set_ftol_abs(0); + uda.set_maxeval(1000); + pagmo::algorithm algo{uda}; + // algo.set_verbosity(5u); - // then the numerical ones - start = high_resolution_clock::now(); - for (decltype(pop_size) j = 0u; j < pop_size; ++j) { - prob_n.gradient(pop.get_x()[j]); + // We solve first a + pop_a = algo.evolve(pop_a); + if (prob_a.feasibility_f(pop_a.get_f()[0])) { + count_a++; + std::cout << "." << std::flush; + } else { + std::cout << "x" << std::flush; + } + // then n + pop_n = algo.evolve(pop_n); + if (prob_n.feasibility_f(pop_n.get_f()[0])) { + count_n++; + std::cout << "." << std::flush; + } else { + std::cout << "x" << std::flush; } - stop = high_resolution_clock::now(); - duration = duration_cast(stop - start); - count_n += static_cast(duration.count()) / 1e6; } - fmt::print("{} nseg - timing: analytical {} - numerical {}", nseg, count_a, count_n); + fmt::print("\n{} nseg - success rates: analytical {}/{} - numerical {}/{}\n", nseg, count_a, N, count_n, N); } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_test_convergence(unsigned N, unsigned nseg) +void perform_speed_benchmark(uint N, uint nseg, uint pop_size) { // // Engines @@ -122,90 +133,98 @@ void perform_test_convergence(unsigned N, unsigned nseg) // // Distributions // - std::uniform_real_distribution dv_pert_d(0., 1000.); - std::uniform_real_distribution mass_d(500, 1500); - std::uniform_real_distribution tof_d(1000, 1500); - std::uniform_real_distribution ts_d(1100, 1300); - - // We construct the solver - pagmo::nlopt uda{"slsqp"}; - uda.set_xtol_abs(0); - uda.set_xtol_rel(0); - uda.set_ftol_abs(0); - uda.set_maxeval(1000); - pagmo::algorithm algo{uda}; - algo.set_verbosity(0u); - - // The initial positions + std::uniform_real_distribution dv_pert_random(0., 0.1); + std::uniform_real_distribution mass_random(1.0, 1.2); + std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi / 3); + std::uniform_real_distribution ts_random(1100, 1300); + + // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - unsigned count_a = 0; - unsigned count_n = 0; - std::cout << "\n"; - for (auto i = 0u; i < N; ++i) { - // And some epochs / tofs. - const double tof_days = tof_d(rng_engine); - const double tof = tof_days * kep3::DAY2SEC; - double ts = ts_d(rng_engine); - const double mass = mass_d(rng_engine); - auto rvs = earth.eph(ts); - auto rvf = jupiter.eph(ts + tof_days); - // We create a ballistic arc matching the two. - const kep3::lambert_problem lp{rvs[0], rvf[0], tof, kep3::MU_SUN}; - rvs[1][0] = lp.get_v0()[0][0]; - rvs[1][1] = lp.get_v0()[0][1]; - rvs[1][2] = lp.get_v0()[0][2]; - rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); - rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); - rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); - - // We construct two problems (analytical gradient and numerical gradient) - pagmo::problem prob_a{sf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, true}}; - pagmo::problem prob_n{sf_bench_udp{rvs, mass, rvf, 0.05, 2000, nseg, false}}; + // auto rvs = earth.eph(1000); + // auto rvf = jupiter.eph(1000); + double count_n = 0; + double count_a = 0; + for (uint i(0); i < N; ++i) { + auto rvs = earth.eph(ts_random(rng_engine)); + auto rvf = jupiter.eph(ts_random(rng_engine)); + // double tof_ic = kep3::pi / 2; + double tof_ic = tof_random(rng_engine); + double mu = 1; + rvs[0][0] /= kep3::AU; + rvs[0][1] /= kep3::AU; + rvs[0][2] /= kep3::AU; + rvf[0][0] /= kep3::AU; + rvf[0][1] /= kep3::AU; + rvf[0][2] /= kep3::AU; + const kep3::lambert_problem lp{rvs[0], rvf[0], tof_ic, mu}; + + // Create HF legs + std::array, 2> rvs_udp_ic = {{{lp.get_r0()[0], lp.get_r0()[1], lp.get_r0()[2]}, + {lp.get_v0()[0][0], lp.get_v0()[0][1], lp.get_v0()[0][2]}}}; + std::array, 2> rvf_udp_ic + = {{{lp.get_r1()[0], lp.get_r1()[1], lp.get_r1()[2]}, + {lp.get_v1()[0][0] + dv_pert_random(rng_engine), lp.get_v1()[0][1] + dv_pert_random(rng_engine), + lp.get_v1()[0][2] + dv_pert_random(rng_engine)}}}; + // double mass = 1; + double mass = mass_random(rng_engine); + double max_thrust = 1; + double isp = 1; + auto bench_udp_a = sf_bench_udp{rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, true}; + auto bench_udp_n = sf_bench_udp{rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, false}; + pagmo::problem prob_a{bench_udp_a}; + pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); // We construct the same random population - pagmo::population pop_a{prob_a, 1u}; - pagmo::population pop_n{prob_n}; - pop_n.push_back(pop_a.get_x()[0]); + pagmo::population pop{prob_a, pop_size}; - // We solve first a - pop_a = algo.evolve(pop_a); - if (prob_a.feasibility_f(pop_a.get_f()[0])) { - count_a++; - std::cout << "." << std::flush; - } else { - std::cout << "x" << std::flush; + // We construct the solver + pagmo::nlopt uda{"slsqp"}; + uda.set_xtol_abs(0); + uda.set_xtol_rel(0); + uda.set_ftol_abs(0); + uda.set_maxeval(1000); + pagmo::algorithm algo{uda}; + // algo.set_verbosity(5u); + + // First we time the analytical gradients + auto start = high_resolution_clock::now(); + for (decltype(pop_size) j = 0u; j < pop_size; ++j) { + prob_a.gradient(pop.get_x()[j]); } - // then n - pop_n = algo.evolve(pop_n); - if (prob_n.feasibility_f(pop_n.get_f()[0])) { - count_n++; - std::cout << "." << std::flush; - } else { - std::cout << "x" << std::flush; + auto stop = high_resolution_clock::now(); + auto duration = duration_cast(stop - start); + count_a += static_cast(duration.count()) / 1e6; + + // then the numerical ones + auto start2 = high_resolution_clock::now(); + for (decltype(pop_size) j = 0u; j < pop_size; ++j) { + prob_n.gradient(pop.get_x()[j]); } + auto stop2 = high_resolution_clock::now(); + auto duration2 = duration_cast(stop2 - start2); + count_n += static_cast(duration2.count()) / 1e6; } - fmt::print("\n{} nseg - success rates: analytical {}/{} - numerical {}/{}", nseg, count_a, N, count_n, N); + fmt::print("{} nseg - timing: analytical {} - numerical {}\n", nseg, count_a, count_n); } int main() { - fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:"); - perform_test_speed(100, 5, 10); - perform_test_speed(100, 10, 10); - perform_test_speed(100, 15, 10); - perform_test_speed(100, 20, 10); - perform_test_speed(100, 70, 10); - - // performing tests - fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); - perform_test_convergence(200, 5); - perform_test_convergence(200, 10); - perform_test_convergence(200, 15); + fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:\n"); + perform_speed_benchmark(100, 5, 10); + // perform_speed_benchmark(100, 10, 10); + // perform_speed_benchmark(100, 20, 10); + // perform_speed_benchmark(100, 40, 10); + + // // performing tests + // fmt::print("\nSolves the same optimization problems with and without analytical gradients:\n"); + // perform_convergence_benchmark(100, 5); + // perform_convergence_benchmark(100, 10); + // perform_convergence_benchmark(100, 15); fmt::print("\n"); } \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_hf_benchmark.cpp b/benchmark/leg_sims_flanagan_hf_benchmark.cpp index 13ff6529..a67f63d1 100644 --- a/benchmark/leg_sims_flanagan_hf_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_hf_benchmark.cpp @@ -32,62 +32,8 @@ using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::microseconds; -void perform_single_nogradient_speed_test() -{ - std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; - std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; - double m_ms = 1; - double m_mf = m_ms * 13 / 15; - double m_isp = 1; - double m_max_thrust = 1; - double m_cut = 0.5; - double m_mu = 1; - double m_tof = 1; - std::vector m_throttles = {0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1}; - - auto start_con = high_resolution_clock::now(); - auto sf_leg - = kep3::leg::sims_flanagan(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, m_mu, m_cut); - auto stop_con = high_resolution_clock::now(); - auto duration_con = duration_cast(stop_con - start_con); - fmt::print("\nLow-fidelity leg construction: {} nseg - timing: {}", m_throttles.size() / 3, - static_cast(duration_con.count()) / 1e6); - auto start = high_resolution_clock::now(); - auto mc = sf_leg.compute_mismatch_constraints(); - auto stop = high_resolution_clock::now(); - auto duration = duration_cast(stop - start); - fmt::print("\nLow-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, - static_cast(duration.count()) / 1e6); - auto two_start = high_resolution_clock::now(); - auto two_mc = sf_leg.compute_mc_grad(); - auto two_stop = high_resolution_clock::now(); - auto two_duration = duration_cast(two_stop - two_start); - fmt::print("\nLow-fidelity leg mc_grad: {} nseg - timing: {}", m_throttles.size() / 3, - static_cast(two_duration.count()) / 1e6); - - auto start_hf_con = high_resolution_clock::now(); - auto sf_hf_leg = kep3::leg::sims_flanagan_hf(m_rvs, m_ms, m_throttles, m_rvf, m_mf, m_tof, m_max_thrust, m_isp, - m_mu, m_cut, 1e-16); - auto stop_hf_con = high_resolution_clock::now(); - auto duration_hf_con = duration_cast(stop_hf_con - start_hf_con); - fmt::print("\nHigh-fidelity leg construction: {} nseg - timing: {}", m_throttles.size() / 3, - static_cast(duration_hf_con.count()) / 1e6); - auto hf_start = high_resolution_clock::now(); - auto hf_mc = sf_hf_leg.compute_mismatch_constraints(); - auto hf_stop = high_resolution_clock::now(); - auto hf_duration = duration_cast(hf_stop - hf_start); - fmt::print("\nHigh-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, - static_cast(hf_duration.count()) / 1e6); - auto hf_two_start = high_resolution_clock::now(); - auto hf_two_mc = sf_hf_leg.compute_mc_grad(); - auto hf_two_stop = high_resolution_clock::now(); - auto hf_two_duration = duration_cast(hf_two_stop - hf_two_start); - fmt::print("\nHigh-fidelity leg mc_grad: {} nseg - timing: {}", m_throttles.size() / 3, - static_cast(hf_two_duration.count()) / 1e6); -} - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) +void perform_convergence_benchmark(uint N, uint nseg) { // // Engines @@ -97,90 +43,90 @@ void perform_test_speed(unsigned N, unsigned nseg, unsigned pop_size) // // Distributions // - std::uniform_real_distribution dv_pert_d(0., 0.1); - std::uniform_real_distribution mass_d(0.9, 1.1); - std::uniform_real_distribution tof_d(0.9, 1.1); - std::uniform_real_distribution ts_d(1100, 1300); + std::uniform_real_distribution dv_pert_random(0., 0.1); + std::uniform_real_distribution mass_random(1.0, 1.2); + std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi / 3); + std::uniform_real_distribution ts_random(1100, 1300); - // We construct the solver - pagmo::nlopt uda{"slsqp"}; - uda.set_xtol_abs(1e-8); - uda.set_xtol_rel(1e-8); - uda.set_ftol_abs(1e-8); - uda.set_maxeval(1000); - pagmo::algorithm algo{uda}; - // algo.set_verbosity(5u); - - // The initial positions + // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - double count_a = 0; - double count_n = 0; - std::cout << "\n"; - auto rvs = earth.eph(1000); - auto rvf = jupiter.eph(1000); - double mass = 1; - auto bench_udp_a = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, true}; - auto bench_udp_n = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, false}; - - for (auto i = 0u; i < N; ++i) { - // And some epochs / tofs. - double ts = ts_d(rng_engine); - rvs = earth.eph(ts); - rvf = jupiter.eph(ts); - mass = mass_d(rng_engine); - // We create a ballistic arc matching the two. + // auto rvs = earth.eph(1000); + // auto rvf = jupiter.eph(1000); + int count_n = 0; + int count_a = 0; + auto bench_udp_a = sf_hf_bench_udp(); + auto bench_udp_n = sf_hf_bench_udp(); + for (uint i(0); i < N; ++i) { + auto rvs = earth.eph(ts_random(rng_engine)); + auto rvf = jupiter.eph(ts_random(rng_engine)); + // double tof_ic = kep3::pi / 2; + double tof_ic = tof_random(rng_engine); + double mu = 1; rvs[0][0] /= kep3::AU; rvs[0][1] /= kep3::AU; rvs[0][2] /= kep3::AU; rvf[0][0] /= kep3::AU; rvf[0][1] /= kep3::AU; rvf[0][2] /= kep3::AU; - const double tof_days = tof_d(rng_engine); - const kep3::lambert_problem lp{rvs[0], rvf[0], tof_days, 1.0}; - rvs[1][0] = lp.get_v0()[0][0] + dv_pert_d(rng_engine); - rvs[1][1] = lp.get_v0()[0][1] + dv_pert_d(rng_engine); - rvs[1][2] = lp.get_v0()[0][2] + dv_pert_d(rng_engine); - rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); - rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); - rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); - - // We construct two problems (analytical gradient and numerical gradient) - bench_udp_a.set_leg(rvs, mass, rvf, 1, 1, nseg, true); - bench_udp_n.set_leg(rvs, mass, rvf, 1, 1, nseg, false); + const kep3::lambert_problem lp{rvs[0], rvf[0], tof_ic, mu}; + + // Create HF legs + std::array, 2> rvs_udp_ic = {{{lp.get_r0()[0], lp.get_r0()[1], lp.get_r0()[2]}, + {lp.get_v0()[0][0], lp.get_v0()[0][1], lp.get_v0()[0][2]}}}; + std::array, 2> rvf_udp_ic + = {{{lp.get_r1()[0], lp.get_r1()[1], lp.get_r1()[2]}, + {lp.get_v1()[0][0] + dv_pert_random(rng_engine), lp.get_v1()[0][1] + dv_pert_random(rng_engine), + lp.get_v1()[0][2] + dv_pert_random(rng_engine)}}}; + // double mass = 1; + double mass = mass_random(rng_engine); + double max_thrust = 1; + double isp = 1; + bench_udp_a.set_leg(rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, true); + bench_udp_n.set_leg(rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, false); pagmo::problem prob_a{bench_udp_a}; pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); - // We construct the random chromosmes - const pagmo::population pop{prob_a, pop_size}; + // We construct the same random population + pagmo::population pop_a{prob_a, 1u}; + pagmo::population pop_n{prob_n}; + pop_n.push_back(pop_a.get_x()[0]); - // First we time the analytical gradients - auto start = high_resolution_clock::now(); - for (decltype(pop_size) j = 0u; j < pop_size; ++j) { - prob_a.gradient(pop.get_x()[j]); - } - auto stop = high_resolution_clock::now(); - auto duration = duration_cast(stop - start); - count_a += static_cast(duration.count()) / 1e6; + // We construct the solver + pagmo::nlopt uda{"slsqp"}; + uda.set_xtol_abs(0); + uda.set_xtol_rel(0); + uda.set_ftol_abs(0); + uda.set_maxeval(1000); + pagmo::algorithm algo{uda}; + // algo.set_verbosity(5u); - // then the numerical ones - start = high_resolution_clock::now(); - for (decltype(pop_size) j = 0u; j < pop_size; ++j) { - prob_n.gradient(pop.get_x()[j]); + // We solve first a + pop_a = algo.evolve(pop_a); + if (prob_a.feasibility_f(pop_a.get_f()[0])) { + count_a++; + std::cout << "." << std::flush; + } else { + std::cout << "x" << std::flush; + } + // then n + pop_n = algo.evolve(pop_n); + if (prob_n.feasibility_f(pop_n.get_f()[0])) { + count_n++; + std::cout << "." << std::flush; + } else { + std::cout << "x" << std::flush; } - stop = high_resolution_clock::now(); - duration = duration_cast(stop - start); - count_n += static_cast(duration.count()) / 1e6; } - fmt::print("{} nseg - timing: analytical {} - numerical {}", nseg, count_a, count_n); + fmt::print("\n{} nseg - success rates: analytical {}/{} - numerical {}/{}\n", nseg, count_a, N, count_n, N); } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_test_convergence(unsigned N, unsigned nseg) +void perform_speed_benchmark(uint N, uint nseg, uint pop_size) { // // Engines @@ -190,107 +136,100 @@ void perform_test_convergence(unsigned N, unsigned nseg) // // Distributions // - std::uniform_real_distribution dv_pert_d(0., 0.1); - std::uniform_real_distribution mass_d(0.9, 1.1); - std::uniform_real_distribution tof_d(0.9, 1.1); - std::uniform_real_distribution ts_d(1100, 1300); - - // We construct the solver - pagmo::nlopt uda{"slsqp"}; - uda.set_xtol_abs(0); - uda.set_xtol_rel(0); - uda.set_ftol_abs(0); - uda.set_maxeval(1000); - pagmo::algorithm algo{uda}; - // algo.set_verbosity(5u); + std::uniform_real_distribution dv_pert_random(0., 0.1); + std::uniform_real_distribution mass_random(1.0, 1.2); + std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi); + std::uniform_real_distribution ts_random(1100, 1300); - // The initial positions + // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - unsigned count_a = 0; - unsigned count_n = 0; - std::cout << "\n"; - // Create sf_hf_leg outside loop to save time - auto rvs = earth.eph(1000); - auto rvf = jupiter.eph(1000); - double mass = 1; - auto bench_udp_a = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, true}; - auto bench_udp_n = sf_hf_bench_udp{rvs, mass, rvf, 1, 1, nseg, false}; - - for (auto i = 0u; i < N; ++i) { - // And some epochs / tofs. - double ts = ts_d(rng_engine); - rvs = earth.eph(ts); - rvf = jupiter.eph(ts); - mass = mass_d(rng_engine); - // We create a ballistic arc matching the two. + // auto rvs = earth.eph(1000); + // auto rvf = jupiter.eph(1000); + double count_n = 0; + double count_a = 0; + auto bench_udp_a = sf_hf_bench_udp(); + auto bench_udp_n = sf_hf_bench_udp(); + for (uint i(0); i < N; ++i) { + auto rvs = earth.eph(ts_random(rng_engine)); + auto rvf = jupiter.eph(ts_random(rng_engine)); + // double tof_ic = kep3::pi / 2; + double tof_ic = tof_random(rng_engine); + double mu = 1; rvs[0][0] /= kep3::AU; rvs[0][1] /= kep3::AU; rvs[0][2] /= kep3::AU; rvf[0][0] /= kep3::AU; rvf[0][1] /= kep3::AU; rvf[0][2] /= kep3::AU; - const double tof_days = tof_d(rng_engine); - const kep3::lambert_problem lp{rvs[0], rvf[0], tof_days, 1.0}; - rvs[1][0] = lp.get_v0()[0][0] + dv_pert_d(rng_engine); - rvs[1][1] = lp.get_v0()[0][1] + dv_pert_d(rng_engine); - rvs[1][2] = lp.get_v0()[0][2] + dv_pert_d(rng_engine); - rvf[1][0] = lp.get_v1()[0][0] + dv_pert_d(rng_engine); - rvf[1][1] = lp.get_v1()[0][1] + dv_pert_d(rng_engine); - rvf[1][2] = lp.get_v1()[0][2] + dv_pert_d(rng_engine); - - // We construct two problems (analytical gradient and numerical gradient) - bench_udp_a.set_leg(rvs, mass, rvf, 1, 1, nseg, true); - bench_udp_n.set_leg(rvs, mass, rvf, 1, 1, nseg, false); + const kep3::lambert_problem lp{rvs[0], rvf[0], tof_ic, mu}; + + // Create HF legs + std::array, 2> rvs_udp_ic = {{{lp.get_r0()[0], lp.get_r0()[1], lp.get_r0()[2]}, + {lp.get_v0()[0][0], lp.get_v0()[0][1], lp.get_v0()[0][2]}}}; + std::array, 2> rvf_udp_ic + = {{{lp.get_r1()[0], lp.get_r1()[1], lp.get_r1()[2]}, + {lp.get_v1()[0][0] + dv_pert_random(rng_engine), lp.get_v1()[0][1] + dv_pert_random(rng_engine), + lp.get_v1()[0][2] + dv_pert_random(rng_engine)}}}; + // double mass = 1; + double mass = mass_random(rng_engine); + double max_thrust = 1; + double isp = 1; + bench_udp_a.set_leg(rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, true); + bench_udp_n.set_leg(rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, false); pagmo::problem prob_a{bench_udp_a}; pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); prob_n.set_c_tol(1e-8); // We construct the same random population - pagmo::population pop_a{prob_a, 1u}; - pagmo::population pop_n{prob_n}; - pop_n.push_back(pop_a.get_x()[0]); + pagmo::population pop{prob_a, pop_size}; - // We solve first a - pop_a = algo.evolve(pop_a); - if (prob_a.feasibility_f(pop_a.get_f()[0])) { - count_a++; - std::cout << "." << std::flush; - } else { - std::cout << "x" << std::flush; + // We construct the solver + pagmo::nlopt uda{"slsqp"}; + uda.set_xtol_abs(0); + uda.set_xtol_rel(0); + uda.set_ftol_abs(0); + uda.set_maxeval(1000); + pagmo::algorithm algo{uda}; + // algo.set_verbosity(5u); + + // First we time the analytical gradients + auto start = high_resolution_clock::now(); + for (decltype(pop_size) j = 0u; j < pop_size; ++j) { + prob_a.gradient(pop.get_x()[j]); } - // then n - pop_n = algo.evolve(pop_n); - if (prob_n.feasibility_f(pop_n.get_f()[0])) { - count_n++; - std::cout << "." << std::flush; - } else { - std::cout << "x" << std::flush; + auto stop = high_resolution_clock::now(); + auto duration = duration_cast(stop - start); + count_a += static_cast(duration.count()) / 1e6; + + // then the numerical ones + auto start2 = high_resolution_clock::now(); + for (decltype(pop_size) j = 0u; j < pop_size; ++j) { + prob_n.gradient(pop.get_x()[j]); } + auto stop2 = high_resolution_clock::now(); + auto duration2 = duration_cast(stop2 - start2); + count_n += static_cast(duration2.count()) / 1e6; } - fmt::print("\n{} nseg - success rates: analytical {}/{} - numerical {}/{}", nseg, count_a, N, count_n, N); + fmt::print("{} nseg - timing: analytical {} - numerical {}\n", nseg, count_a, count_n); } int main() { - fmt::print("\nComputes the speed of a single compute_mismatch_constraints() run for a lf and hf leg."); - perform_single_nogradient_speed_test(); - - fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:"); - perform_test_speed(100, 5, 10); - perform_test_speed(100, 10, 10); - perform_test_speed(100, 15, 10); - perform_test_speed(100, 20, 10); - perform_test_speed(100, 70, 10); - - // performing tests - fmt::print("\nSolves the same optimization problems with and without analytical gradients:"); - perform_test_convergence(200, 5); - perform_test_convergence(200, 10); - perform_test_convergence(200, 15); + fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:\n"); + perform_speed_benchmark(100, 5, 10); + // perform_speed_benchmark(100, 10, 10); + // perform_speed_benchmark(100, 20, 10); + // perform_speed_benchmark(100, 40, 10); + + // // performing tests + // fmt::print("\nSolves the same optimization problems with and without analytical gradients:\n"); + // perform_convergence_benchmark(100, 5); + // perform_convergence_benchmark(100, 10); + // perform_convergence_benchmark(100, 15); fmt::print("\n"); } \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp index b20a829a..3f1357cd 100644 --- a/benchmark/leg_sims_flanagan_hf_udp_bench.hpp +++ b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp @@ -7,8 +7,8 @@ // Public License v. 2.0. If a copy of the MPL was not distributed // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -#ifndef kep3_TEST_LEG_SIMS_FLANAGAN_UDP_BENCH_H -#define kep3_TEST_LEG_SIMS_FLANAGAN_UDP_BENCH_H +#ifndef kep3_TEST_LEG_SIMS_FLANAGAN_HF_UDP_BENCH_H +#define kep3_TEST_LEG_SIMS_FLANAGAN_HF_UDP_BENCH_H #include #include @@ -166,8 +166,8 @@ struct sf_hf_bench_udp { // x = [throttles, tof (in days), mf (in kg)] std::vector lb(m_nseg * 3 + 2, -1.); std::vector ub(m_nseg * 3 + 2, +1.); - lb[m_nseg * 3] = 0.5; // days - ub[m_nseg * 3] = 1.5; // days + lb[m_nseg * 3] = kep3::pi / 12; // days + ub[m_nseg * 3] = 2 * kep3::pi; // days lb[m_nseg * 3 + 1] = 0.5; // kg ub[m_nseg * 3 + 1] = 1; // kg return {lb, ub}; diff --git a/benchmark/leg_sims_flanagan_udp_bench.hpp b/benchmark/leg_sims_flanagan_udp_bench.hpp index b6a9f7ca..68b2ec3c 100644 --- a/benchmark/leg_sims_flanagan_udp_bench.hpp +++ b/benchmark/leg_sims_flanagan_udp_bench.hpp @@ -35,10 +35,10 @@ struct sf_bench_udp { { // x = [throttles, tof (in days), mf (in kg)] // We set the leg (avoiding the allocation for the throttles is possible but requires mutable data members.) - double tof = x[m_nseg * 3] * kep3::DAY2SEC; // in s + double tof = x[m_nseg * 3];// * kep3::DAY2SEC; // in s double mf = x[m_nseg * 3 + 1]; // in kg kep3::leg::sims_flanagan leg(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, mf, tof, m_max_thrust, - m_isp, kep3::MU_SUN); + m_isp, 1); // We set the throttles leg.set_throttles(x.begin(), x.end() - 2); @@ -48,13 +48,13 @@ struct sf_bench_udp { retval[0] = -mf; // Equality Constraints auto eq_con = leg.compute_mismatch_constraints(); - retval[1] = eq_con[0] / kep3::AU; - retval[2] = eq_con[1] / kep3::AU; - retval[3] = eq_con[2] / kep3::AU; - retval[4] = eq_con[3] / kep3::EARTH_VELOCITY; - retval[5] = eq_con[4] / kep3::EARTH_VELOCITY; - retval[6] = eq_con[5] / kep3::EARTH_VELOCITY; - retval[7] = eq_con[6] / 1e8; // + retval[1] = eq_con[0]; // / kep3::AU; + retval[2] = eq_con[1]; // / kep3::AU; + retval[3] = eq_con[2]; // / kep3::AU; + retval[4] = eq_con[3]; // / kep3::EARTH_VELOCITY; + retval[5] = eq_con[4]; // / kep3::EARTH_VELOCITY; + retval[6] = eq_con[5]; // / kep3::EARTH_VELOCITY; + retval[7] = eq_con[6]; // / 1e8; // // Inequality Constraints auto ineq_con = leg.compute_throttle_constraints(); std::copy(ineq_con.begin(), ineq_con.end(), retval.begin() + 8); @@ -79,10 +79,10 @@ struct sf_bench_udp { { // x = [throttles, tof (in days), mf (in kg)] // We set the leg (avoiding the allocation for the throttles is possible but requires mutable data members.) - double tof = x[m_nseg * 3] * kep3::DAY2SEC; // in s + double tof = x[m_nseg * 3]; // * kep3::DAY2SEC; // in s double mf = x[m_nseg * 3 + 1]; // in kg kep3::leg::sims_flanagan leg(m_rvs, m_ms, std::vector(m_nseg * 3, 0.), m_rvf, mf, tof, m_max_thrust, - m_isp, kep3::MU_SUN); + m_isp, 1); // We set the throttles leg.set_throttles(x.begin(), x.end() - 2); @@ -105,13 +105,13 @@ struct sf_bench_udp { xgradient(0, m_nseg * 3 + 1) = -1.; // fitness gradient - obj fun // [1:4,:-1] - fitness gradient - position mismatch xt::view(xgradient, xt::range(1u, 4u), xt::range(0, m_nseg * 3u + 1u)) - = xt::view(xgrad_mc, xt::range(0u, 3u), xt::all()) / kep3::AU; // throttles, tof + = xt::view(xgrad_mc, xt::range(0u, 3u), xt::all()); // / kep3::AU; // throttles, tof // [4:7,:-1] - fitness gradient - velocity mismatch xt::view(xgradient, xt::range(4u, 7u), xt::range(0, m_nseg * 3u + 1u)) - = xt::view(xgrad_mc, xt::range(3u, 6u), xt::all()) / kep3::EARTH_VELOCITY; // throttles, tof + = xt::view(xgrad_mc, xt::range(3u, 6u), xt::all()); // / kep3::EARTH_VELOCITY; // throttles, tof // [7:8,:-1] - fitness gradient - mass mismatch xt::view(xgradient, xt::range(7u, 8u), xt::range(0, static_cast(m_nseg) * 3u + 1)) - = xt::view(xgrad_mc, xt::range(6u, 7u), xt::all()) / 1e8; // throttles, tof + = xt::view(xgrad_mc, xt::range(6u, 7u), xt::all()); // / 1e8; // throttles, tof // [8:,:-2] - fitness gradient - throttle constraints xt::view(xgradient, xt::range(8u, 8u + static_cast(m_nseg)), xt::range(0, static_cast(m_nseg) * 3u)) @@ -119,28 +119,35 @@ struct sf_bench_udp { // [1:4,-1] - fitness gradient, position mismatch w.r.t. mf xt::view(xgradient, xt::range(1u, 4u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) - = xt::view(xgrad_mc_xf, xt::range(0u, 3u), xt::range(6u, 7u)) / kep3::AU; // mf + = xt::view(xgrad_mc_xf, xt::range(0u, 3u), xt::range(6u, 7u)); // / kep3::AU; // mf // [4:7,-1] - fitness gradient - velocity mismatch w.r.t. mf xt::view(xgradient, xt::range(4u, 7u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) - = xt::view(xgrad_mc_xf, xt::range(3u, 6u), xt::range(6u, 7u)) / kep3::EARTH_VELOCITY; // mf + = xt::view(xgrad_mc_xf, xt::range(3u, 6u), xt::range(6u, 7u)); // / kep3::EARTH_VELOCITY; // mf // [7:8,-1] - fitness gradient - mass mismatch w.r.t. mf xt::view(xgradient, xt::range(7u, 8u), xt::range(m_nseg * 3u + 1u, m_nseg * 3u + 2u)) - = xt::view(xgrad_mc_xf, xt::range(6u, 7u), xt::range(6u, 7u)) / 1e8; // mf + = xt::view(xgrad_mc_xf, xt::range(6u, 7u), xt::range(6u, 7u)); // / 1e8; // mf // Units for the tof - xt::view(xgradient, xt::all(), xt::range(m_nseg * 3u, m_nseg * 3u + 1u)) *= kep3::DAY2SEC; + xt::view(xgradient, xt::all(), xt::range(m_nseg * 3u, m_nseg * 3u + 1u)); // *= kep3::DAY2SEC; return gradient; } [[nodiscard]] std::pair, std::vector> get_bounds() const { // x = [throttles, tof (in days), mf (in kg)] + // std::vector lb(m_nseg * 3 + 2, -1.); + // std::vector ub(m_nseg * 3 + 2, +1.); + // lb[m_nseg * 3] = 1.; // days + // ub[m_nseg * 3] = 2500.; // days + // lb[m_nseg * 3 + 1] = m_ms / 2.; // kg + // ub[m_nseg * 3 + 1] = m_ms; // kg + // return {lb, ub}; std::vector lb(m_nseg * 3 + 2, -1.); std::vector ub(m_nseg * 3 + 2, +1.); - lb[m_nseg * 3] = 1.; // days - ub[m_nseg * 3] = 2500.; // days - lb[m_nseg * 3 + 1] = m_ms / 2.; // kg - ub[m_nseg * 3 + 1] = m_ms; // kg + lb[m_nseg * 3] = kep3::pi / 12; // days + ub[m_nseg * 3] = 2 * kep3::pi; // days + lb[m_nseg * 3 + 1] = 0.5; // kg + ub[m_nseg * 3 + 1] = 1; // kg return {lb, ub}; } diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index e5880102..506dfd9e 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -250,7 +250,6 @@ void sims_flanagan_hf::set(const std::array, 2> &rvs, doub const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, double mu, double cut, double tol) { - _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Set initial state set_rvs(rvs); set_ms(ms); @@ -267,6 +266,7 @@ void sims_flanagan_hf::set(const std::array, 2> &rvs, doub m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; + _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; @@ -278,7 +278,6 @@ void sims_flanagan_hf::set(const std::array &rvms, const std::vector< const std::array &rvmf, double tof, double max_thrust, double isp, double mu, double cut, double tol) { - _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); set_rvms(rvms); m_throttles = throttles; set_rvmf(rvmf); @@ -291,6 +290,7 @@ void sims_flanagan_hf::set(const std::array &rvms, const std::vector< m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; + _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; From 1a6cf37cfe5272cb8dea7f0451ed5e0daa0400cf Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Mon, 4 Nov 2024 09:48:07 +0100 Subject: [PATCH 16/22] Updated values for benchmark to prevent singularities. --- benchmark/leg_sims_flanagan_benchmark.cpp | 22 ++++++++++---------- benchmark/leg_sims_flanagan_hf_benchmark.cpp | 22 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/benchmark/leg_sims_flanagan_benchmark.cpp b/benchmark/leg_sims_flanagan_benchmark.cpp index 64f79f4e..e9b13650 100644 --- a/benchmark/leg_sims_flanagan_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_benchmark.cpp @@ -44,8 +44,8 @@ void perform_convergence_benchmark(uint N, uint nseg) // std::uniform_real_distribution dv_pert_random(0., 0.1); std::uniform_real_distribution mass_random(1.0, 1.2); - std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi); - std::uniform_real_distribution ts_random(1100, 1300); + std::uniform_real_distribution tof_random(kep3::pi / 12, 2 * kep3::pi); + std::uniform_real_distribution ts_random(2170, 2200); // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); @@ -135,8 +135,8 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) // std::uniform_real_distribution dv_pert_random(0., 0.1); std::uniform_real_distribution mass_random(1.0, 1.2); - std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi / 3); - std::uniform_real_distribution ts_random(1100, 1300); + std::uniform_real_distribution tof_random(kep3::pi / 12, 2 * kep3::pi); + std::uniform_real_distribution ts_random(2170, 2200); // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); @@ -216,15 +216,15 @@ int main() { fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:\n"); perform_speed_benchmark(100, 5, 10); - // perform_speed_benchmark(100, 10, 10); - // perform_speed_benchmark(100, 20, 10); - // perform_speed_benchmark(100, 40, 10); + perform_speed_benchmark(100, 10, 10); + perform_speed_benchmark(100, 20, 10); + perform_speed_benchmark(100, 40, 10); // // performing tests - // fmt::print("\nSolves the same optimization problems with and without analytical gradients:\n"); - // perform_convergence_benchmark(100, 5); - // perform_convergence_benchmark(100, 10); - // perform_convergence_benchmark(100, 15); + fmt::print("\nSolves the same optimization problems with and without analytical gradients:\n"); + perform_convergence_benchmark(100, 5); + perform_convergence_benchmark(100, 10); + perform_convergence_benchmark(100, 15); fmt::print("\n"); } \ No newline at end of file diff --git a/benchmark/leg_sims_flanagan_hf_benchmark.cpp b/benchmark/leg_sims_flanagan_hf_benchmark.cpp index a67f63d1..652a5615 100644 --- a/benchmark/leg_sims_flanagan_hf_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_hf_benchmark.cpp @@ -45,8 +45,8 @@ void perform_convergence_benchmark(uint N, uint nseg) // std::uniform_real_distribution dv_pert_random(0., 0.1); std::uniform_real_distribution mass_random(1.0, 1.2); - std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi / 3); - std::uniform_real_distribution ts_random(1100, 1300); + std::uniform_real_distribution tof_random(kep3::pi / 12, 2 * kep3::pi); + std::uniform_real_distribution ts_random(2170, 2200); // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); @@ -138,8 +138,8 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) // std::uniform_real_distribution dv_pert_random(0., 0.1); std::uniform_real_distribution mass_random(1.0, 1.2); - std::uniform_real_distribution tof_random(kep3::pi / 3, 2 * kep3::pi); - std::uniform_real_distribution ts_random(1100, 1300); + std::uniform_real_distribution tof_random(kep3::pi / 12, 2 * kep3::pi); + std::uniform_real_distribution ts_random(2170, 2200); // Create test leg for initial conditions kep3::udpla::vsop2013 udpla_earth("earth_moon", 1e-2); @@ -221,15 +221,15 @@ int main() { fmt::print("\nComputes the same analytical and numerical gradients and tests for speed:\n"); perform_speed_benchmark(100, 5, 10); - // perform_speed_benchmark(100, 10, 10); - // perform_speed_benchmark(100, 20, 10); - // perform_speed_benchmark(100, 40, 10); + perform_speed_benchmark(100, 10, 10); + perform_speed_benchmark(100, 20, 10); + perform_speed_benchmark(100, 40, 10); // // performing tests - // fmt::print("\nSolves the same optimization problems with and without analytical gradients:\n"); - // perform_convergence_benchmark(100, 5); - // perform_convergence_benchmark(100, 10); - // perform_convergence_benchmark(100, 15); + fmt::print("\nSolves the same optimization problems with and without analytical gradients:\n"); + perform_convergence_benchmark(100, 5); + perform_convergence_benchmark(100, 10); + perform_convergence_benchmark(100, 15); fmt::print("\n"); } \ No newline at end of file From 2f5c249d669c23020a4b67091a0362f5e15895d3 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Thu, 7 Nov 2024 16:47:07 +0100 Subject: [PATCH 17/22] First fixes to Windows and macOS build compilation issues. --- include/kep3/leg/sims_flanagan_hf.hpp | 4 ++-- src/leg/sims_flanagan_hf.cpp | 32 +++++++++++++-------------- test/leg_sims_flanagan_hf_helpers.hpp | 12 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 25fd69b3..862f6d7f 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -130,12 +130,12 @@ class kep3_DLL_PUBLIC sims_flanagan_hf [[nodiscard]] std::vector compute_tc_grad() const; // Retrieve the state history of the sims flanagan leg - [[nodiscard]] std::vector> get_state_history(const unsigned int grid_points_per_segment) const; + [[nodiscard]] std::vector> get_state_history(const unsigned grid_points_per_segment) const; private: friend class boost::serialization::access; template - void serialize(Archive &ar, const unsigned int) + void serialize(Archive &ar, const unsigned) { ar & m_rvms; ar & m_vars; diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 506dfd9e..bc9a34da 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -409,7 +409,7 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() const std::copy(m_rvms.begin(), m_rvms.end(), m_tas.get_state_data()); // Loop through segments in forward pass of Sims-Flanagan transcription - for (unsigned int i = 0u; i < m_nseg_fwd; ++i) { + for (auto i = 0u; i < m_nseg_fwd; ++i) { // Assign current thrusts to Taylor adaptive integrator if (static_cast((i + 1) * 3) <= m_thrusts.size()) { std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), @@ -435,7 +435,7 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() const std::copy(m_rvmf.begin(), m_rvmf.end(), m_tas.get_state_data()); // Loop through segments in backward pass of Sims-Flanagan transcription - for (unsigned int i = 0u; i < m_nseg_bck; ++i) { + for (auto i = 0u; i < m_nseg_bck; ++i) { // Assign current_thrusts to Taylor adaptive integrator if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { // Copy thrust into Taylor-adaptive integrator @@ -555,7 +555,7 @@ sims_flanagan_hf::compute_all_gradients() const m_tas_var.set_time(0.); std::copy(m_rvms.begin(), m_rvms.end(), m_tas_var.get_state_data()); - for (unsigned int i = 0u; i < m_nseg_fwd; ++i) { + for (auto i = 0u; i < m_nseg_fwd; ++i) { // Initialise var conditions std::copy(m_vars.begin(), m_vars.end(), m_tas_var.get_state_data() + 7); @@ -588,7 +588,7 @@ sims_flanagan_hf::compute_all_gradients() const m_tas_var.set_time(m_tof); std::copy(m_rvmf.begin(), m_rvmf.end(), m_tas_var.get_state_data()); - for (unsigned int i = 0u; i < m_nseg_bck; ++i) { + for (auto i = 0u; i < m_nseg_bck; ++i) { // Initialise var conditions std::copy(m_vars.begin(), m_vars.end(), m_tas_var.get_state_data() + 7); @@ -622,17 +622,17 @@ sims_flanagan_hf::compute_all_gradients() const if (m_nseg_fwd > 0) { x0_per_seg[0] = m_rvms; } - for (unsigned int i(1); i < m_nseg_fwd; ++i) { + for (auto i = 1; i < m_nseg_fwd; ++i) { x0_per_seg[i] = xf_per_seg[i - 1]; } if (m_nseg_bck > 0) { x0_per_seg[m_nseg - 1] = m_rvmf; } - for (unsigned int i(1); i < m_nseg_bck; ++i) { + for (auto i = 1; i < m_nseg_bck; ++i) { x0_per_seg[(m_nseg - 1) - i] = xf_per_seg[(m_nseg - 1) - (i - 1)]; } - for (unsigned int i(0); i < dxdtof_per_seg.size(); ++i) { + for (auto i = 0; i < dxdtof_per_seg.size(); ++i) { std::array current_throttles = {m_throttles[i * 3], m_throttles[i * 3 + 1], m_throttles[i * 3 + 2]}; dxdtof_per_seg[i] = get_state_derivative(x0_per_seg[i], current_throttles); } @@ -654,7 +654,7 @@ sims_flanagan_hf::get_relevant_gradients(const std::vector current_M; if (m_nseg_fwd > 0) { Mn_o[0] = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd - 1, xt::all()), {7, 7}); - for (unsigned int i(0); i < m_nseg_fwd - 1; ++i) { + for (decltype(m_nseg_fwd) i = 0; i < m_nseg_fwd - 1; ++i) { current_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd - 1 - (i + 1), xt::all()), {7, 7}); if (i == 0) { final_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd - 1, xt::all()), {7, 7}); @@ -667,7 +667,7 @@ sims_flanagan_hf::get_relevant_gradients(const std::vector 0) { Mn_o[m_nseg_fwd] = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd, xt::all()), {7, 7}); - for (unsigned int i(0); i < m_nseg_bck - 1; ++i) { + for (decltype(m_nseg_fwd) i(0); i < m_nseg_bck - 1; ++i) { current_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd + (i + 1), xt::all()), {7, 7}); if (i == 0) { final_M = xt::reshape_view(xt::view(xt_dxdx_per_seg, m_nseg_fwd, xt::all()), {7, 7}); @@ -703,7 +703,7 @@ sims_flanagan_hf::get_relevant_gradients(const std::vector(m_nseg) * 3u}); xt::xarray corresponding_M; xt::xarray current_U; - for (unsigned int i(0); i < m_nseg; ++i) { + for (decltype(m_nseg_fwd) i(0); i < m_nseg; ++i) { current_U = xt::reshape_view(xt::view(xt_dxdu_per_seg, i, xt::all()), {7, 3}); if (i == m_nseg_fwd - 1) { corresponding_M = xt::eye(7); @@ -726,7 +726,7 @@ sims_flanagan_hf::get_relevant_gradients(const std::vector(dxdtof_per_seg.data()), {m_nseg, 7u}); std::vector grad_final_tof(static_cast(7), 0.); auto xgrad_final_tof = xt::adapt(grad_final_tof, {7u, 1u}); - for (unsigned int i(0); i < m_nseg; ++i) { + for (decltype(m_nseg_fwd) i(0); i < m_nseg; ++i) { xt::xarray current_F = xt::reshape_view(xt::view(xt_dxdtof_per_seg, i, xt::all()), {7, 1}); if ((i <= m_nseg_fwd - 1) && m_nseg_fwd > 0) { corresponding_M = Mn_o @@ -779,7 +779,7 @@ std::vector sims_flanagan_hf::compute_tc_grad() const return retval; } -std::vector> sims_flanagan_hf::get_state_history(unsigned int grid_points_per_segment) const +std::vector> sims_flanagan_hf::get_state_history(unsigned grid_points_per_segment) const { // Get time grid const double prop_seg_duration = (m_tof / m_nseg); @@ -788,8 +788,8 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in double timestep = 0.0; leg_time_grid.push_back(timestep); - for (uint _(0); _ < grid_points_per_segment * m_nseg - 2; ++_) { - timestep += prop_seg_duration / (grid_points_per_segment - 1); + for (decltype(m_nseg) i = 0; i < grid_points_per_segment * m_nseg - 2; ++i) { + timestep += (prop_seg_duration / (grid_points_per_segment - 1)); leg_time_grid.push_back(timestep); } // leg_time_grid.push_back(m_tof); @@ -803,7 +803,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in std::vector> output_per_seg(m_nseg); // Loop through segments in forward pass of Sims-Flanagan transcription - for (unsigned int i = 0u; i < m_nseg_fwd; ++i) { + for (decltype(m_nseg_fwd) i = 0u; i < m_nseg_fwd; ++i) { // Assign current thrusts to Taylor adaptive integrator if (static_cast((i + 1) * 3) <= m_thrusts.size()) { std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), @@ -834,7 +834,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned in std::vector back_time_grid(grid_points_per_segment); // Loop through segments in backward pass of Sims-Flanagan transcription - for (unsigned int i = 0u; i < m_nseg_bck; ++i) { + for (decltype(m_nseg) i = 0u; i < m_nseg_bck; ++i) { // Assign current_thrusts to Taylor adaptive integrator if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { // Copy thrust into Taylor-adaptive integrator diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp index 1b9dc5e9..7cbf7494 100644 --- a/test/leg_sims_flanagan_hf_helpers.hpp +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -169,17 +169,17 @@ struct sf_hf_test_object { std::array m_fwd_final_state{}; std::array m_bck_final_state{}; std::array m_mc_manual{}; + std::vector m_thrusts; std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; - std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; double m_ms = 1; + std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; double m_mf = m_ms * 13 / 15; - double m_isp = 1; + double m_tof = 1; double m_max_thrust = 1; - double m_cut = 0.5; + double m_isp = 1; double m_mu = 1; - double m_tof = 1; - std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - std::vector m_thrusts; + double m_cut = 0.5; double m_tol = 1e-16; std::vector m_rvms = {m_rvs[0][0], m_rvs[0][1], m_rvs[0][2], m_rvs[1][0], m_rvs[1][1], m_rvs[1][2], m_ms}; std::vector m_rvmf = {m_rvf[0][0], m_rvf[0][1], m_rvf[0][2], m_rvf[1][0], m_rvf[1][1], m_rvf[1][2], m_mf}; From 34f54127fa8e8d2b327fab3df4ec30afb43f23c5 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 8 Nov 2024 09:00:19 +0100 Subject: [PATCH 18/22] Some more fixes to Windows and macOS build issues. --- benchmark/leg_sims_flanagan_benchmark.cpp | 15 +++------- benchmark/leg_sims_flanagan_hf_benchmark.cpp | 30 ++++---------------- src/leg/sims_flanagan_hf.cpp | 6 ++-- test/leg_sims_flanagan_hf_helpers.hpp | 4 +-- test/leg_sims_flanagan_hf_test.cpp | 4 +-- 5 files changed, 17 insertions(+), 42 deletions(-) diff --git a/benchmark/leg_sims_flanagan_benchmark.cpp b/benchmark/leg_sims_flanagan_benchmark.cpp index e9b13650..db32dd8a 100644 --- a/benchmark/leg_sims_flanagan_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_benchmark.cpp @@ -32,7 +32,7 @@ using std::chrono::high_resolution_clock; using std::chrono::microseconds; // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_convergence_benchmark(uint N, uint nseg) +void perform_convergence_benchmark(unsigned N, unsigned nseg) { // // Engines @@ -56,10 +56,9 @@ void perform_convergence_benchmark(uint N, uint nseg) // auto rvf = jupiter.eph(1000); int count_n = 0; int count_a = 0; - for (uint i(0); i < N; ++i) { + for (decltype(N) i = 0; i < N; ++i) { auto rvs = earth.eph(ts_random(rng_engine)); auto rvf = jupiter.eph(ts_random(rng_engine)); - // double tof_ic = kep3::pi / 2; double tof_ic = tof_random(rng_engine); double mu = 1; rvs[0][0] /= kep3::AU; @@ -100,7 +99,6 @@ void perform_convergence_benchmark(uint N, uint nseg) uda.set_ftol_abs(0); uda.set_maxeval(1000); pagmo::algorithm algo{uda}; - // algo.set_verbosity(5u); // We solve first a pop_a = algo.evolve(pop_a); @@ -123,7 +121,7 @@ void perform_convergence_benchmark(uint N, uint nseg) } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_speed_benchmark(uint N, uint nseg, uint pop_size) +void perform_speed_benchmark(unsigned N, unsigned nseg, unsigned pop_size) { // // Engines @@ -143,14 +141,11 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - // auto rvs = earth.eph(1000); - // auto rvf = jupiter.eph(1000); double count_n = 0; double count_a = 0; - for (uint i(0); i < N; ++i) { + for (decltype(N) i = 0; i < N; ++i) { auto rvs = earth.eph(ts_random(rng_engine)); auto rvf = jupiter.eph(ts_random(rng_engine)); - // double tof_ic = kep3::pi / 2; double tof_ic = tof_random(rng_engine); double mu = 1; rvs[0][0] /= kep3::AU; @@ -168,7 +163,6 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) = {{{lp.get_r1()[0], lp.get_r1()[1], lp.get_r1()[2]}, {lp.get_v1()[0][0] + dv_pert_random(rng_engine), lp.get_v1()[0][1] + dv_pert_random(rng_engine), lp.get_v1()[0][2] + dv_pert_random(rng_engine)}}}; - // double mass = 1; double mass = mass_random(rng_engine); double max_thrust = 1; double isp = 1; @@ -189,7 +183,6 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) uda.set_ftol_abs(0); uda.set_maxeval(1000); pagmo::algorithm algo{uda}; - // algo.set_verbosity(5u); // First we time the analytical gradients auto start = high_resolution_clock::now(); diff --git a/benchmark/leg_sims_flanagan_hf_benchmark.cpp b/benchmark/leg_sims_flanagan_hf_benchmark.cpp index 652a5615..e075a07f 100644 --- a/benchmark/leg_sims_flanagan_hf_benchmark.cpp +++ b/benchmark/leg_sims_flanagan_hf_benchmark.cpp @@ -33,7 +33,7 @@ using std::chrono::high_resolution_clock; using std::chrono::microseconds; // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_convergence_benchmark(uint N, uint nseg) +void perform_convergence_benchmark(unsigned N, unsigned nseg) { // // Engines @@ -53,16 +53,13 @@ void perform_convergence_benchmark(uint N, uint nseg) kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - // auto rvs = earth.eph(1000); - // auto rvf = jupiter.eph(1000); int count_n = 0; int count_a = 0; auto bench_udp_a = sf_hf_bench_udp(); auto bench_udp_n = sf_hf_bench_udp(); - for (uint i(0); i < N; ++i) { + for (decltype(N) i = 0; i < N; ++i) { auto rvs = earth.eph(ts_random(rng_engine)); auto rvf = jupiter.eph(ts_random(rng_engine)); - // double tof_ic = kep3::pi / 2; double tof_ic = tof_random(rng_engine); double mu = 1; rvs[0][0] /= kep3::AU; @@ -103,7 +100,6 @@ void perform_convergence_benchmark(uint N, uint nseg) uda.set_ftol_abs(0); uda.set_maxeval(1000); pagmo::algorithm algo{uda}; - // algo.set_verbosity(5u); // We solve first a pop_a = algo.evolve(pop_a); @@ -126,7 +122,7 @@ void perform_convergence_benchmark(uint N, uint nseg) } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -void perform_speed_benchmark(uint N, uint nseg, uint pop_size) +void perform_speed_benchmark(unsigned N, unsigned nseg, unsigned pop_size) { // // Engines @@ -146,39 +142,26 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); kep3::planet earth{udpla_earth}; kep3::planet jupiter{udpla_jupiter}; - // auto rvs = earth.eph(1000); - // auto rvf = jupiter.eph(1000); double count_n = 0; double count_a = 0; auto bench_udp_a = sf_hf_bench_udp(); auto bench_udp_n = sf_hf_bench_udp(); - for (uint i(0); i < N; ++i) { + for (decltype(N) i = 0; i < N; ++i) { auto rvs = earth.eph(ts_random(rng_engine)); auto rvf = jupiter.eph(ts_random(rng_engine)); - // double tof_ic = kep3::pi / 2; - double tof_ic = tof_random(rng_engine); - double mu = 1; rvs[0][0] /= kep3::AU; rvs[0][1] /= kep3::AU; rvs[0][2] /= kep3::AU; rvf[0][0] /= kep3::AU; rvf[0][1] /= kep3::AU; rvf[0][2] /= kep3::AU; - const kep3::lambert_problem lp{rvs[0], rvf[0], tof_ic, mu}; // Create HF legs - std::array, 2> rvs_udp_ic = {{{lp.get_r0()[0], lp.get_r0()[1], lp.get_r0()[2]}, - {lp.get_v0()[0][0], lp.get_v0()[0][1], lp.get_v0()[0][2]}}}; - std::array, 2> rvf_udp_ic - = {{{lp.get_r1()[0], lp.get_r1()[1], lp.get_r1()[2]}, - {lp.get_v1()[0][0] + dv_pert_random(rng_engine), lp.get_v1()[0][1] + dv_pert_random(rng_engine), - lp.get_v1()[0][2] + dv_pert_random(rng_engine)}}}; - // double mass = 1; double mass = mass_random(rng_engine); double max_thrust = 1; double isp = 1; - bench_udp_a.set_leg(rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, true); - bench_udp_n.set_leg(rvs_udp_ic, mass, rvf_udp_ic, max_thrust, isp, nseg, false); + bench_udp_a.set_leg(rvs, mass, rvf, max_thrust, isp, nseg, true); + bench_udp_n.set_leg(rvs, mass, rvf, max_thrust, isp, nseg, false); pagmo::problem prob_a{bench_udp_a}; pagmo::problem prob_n{bench_udp_n}; prob_a.set_c_tol(1e-8); @@ -194,7 +177,6 @@ void perform_speed_benchmark(uint N, uint nseg, uint pop_size) uda.set_ftol_abs(0); uda.set_maxeval(1000); pagmo::algorithm algo{uda}; - // algo.set_verbosity(5u); // First we time the analytical gradients auto start = high_resolution_clock::now(); diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index bc9a34da..ed49154b 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -622,17 +622,17 @@ sims_flanagan_hf::compute_all_gradients() const if (m_nseg_fwd > 0) { x0_per_seg[0] = m_rvms; } - for (auto i = 1; i < m_nseg_fwd; ++i) { + for (decltype(m_nseg_fwd) i = 1; i < m_nseg_fwd; ++i) { x0_per_seg[i] = xf_per_seg[i - 1]; } if (m_nseg_bck > 0) { x0_per_seg[m_nseg - 1] = m_rvmf; } - for (auto i = 1; i < m_nseg_bck; ++i) { + for (decltype(m_nseg_bck) i = 1; i < m_nseg_bck; ++i) { x0_per_seg[(m_nseg - 1) - i] = xf_per_seg[(m_nseg - 1) - (i - 1)]; } - for (auto i = 0; i < dxdtof_per_seg.size(); ++i) { + for (decltype(dxdtof_per_seg.size()) i = 0; i < dxdtof_per_seg.size(); ++i) { std::array current_throttles = {m_throttles[i * 3], m_throttles[i * 3 + 1], m_throttles[i * 3 + 2]}; dxdtof_per_seg[i] = get_state_derivative(x0_per_seg[i], current_throttles); } diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp index 7cbf7494..376db15e 100644 --- a/test/leg_sims_flanagan_hf_helpers.hpp +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -58,7 +58,7 @@ struct sf_hf_test_object { explicit sf_hf_test_object(double cut) : m_cut(cut) {} - sf_hf_test_object(std::vector &throttles, double cut) : m_cut(cut), m_throttles(throttles) + sf_hf_test_object(std::vector &throttles, double cut) : m_throttles(throttles), m_cut(cut) { for (double m_throttle : m_throttles) { m_thrusts.push_back(m_throttle * m_max_thrust); @@ -169,9 +169,9 @@ struct sf_hf_test_object { std::array m_fwd_final_state{}; std::array m_bck_final_state{}; std::array m_mc_manual{}; - std::vector m_thrusts; std::array, 2> m_rvs{{{1, 0.1, -0.1}, {0.2, 1, -0.2}}}; double m_ms = 1; + std::vector m_thrusts; std::vector m_throttles = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; std::array, 2> m_rvf{{{1.2, -0.1, 0.1}, {-0.2, 1.023, -0.44}}}; double m_mf = m_ms * 13 / 15; diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index a3eca0a3..5c06c9d4 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -319,7 +319,7 @@ TEST_CASE("compute_state_history") // Get state history kep3::leg::sims_flanagan_hf sf{}; auto mc = sf.compute_mismatch_constraints(); - uint grid_points_per_segment = 4; + unsigned grid_points_per_segment = 4; auto state_history = sf.get_state_history(grid_points_per_segment); // Get fwd final state @@ -358,7 +358,7 @@ TEST_CASE("compute_state_history_2") // Get state history auto mc = sf.compute_mismatch_constraints(); - uint grid_points_per_segment = 4; + unsigned grid_points_per_segment = 4; auto state_history = sf.get_state_history(grid_points_per_segment); // Get fwd final state From 73eb855693efe0a32d39bdd9253151d0bc77a211 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 8 Nov 2024 13:37:02 +0100 Subject: [PATCH 19/22] Improved code coverage of failing files. Other minor fixes. Fixed typo in benchmark. --- benchmark/leg_sf_benchmark_simple.cpp | 6 +++--- include/kep3/leg/sims_flanagan_hf.hpp | 6 +++--- pykep/core.cpp | 4 ++-- src/leg/sf_checks.cpp | 10 +++------- src/leg/sims_flanagan_hf.cpp | 16 ++++++++-------- test/leg_sims_flanagan_hf_test.cpp | 27 ++++++++++++++++++++++++++- test/leg_sims_flanagan_test.cpp | 7 ++++--- 7 files changed, 49 insertions(+), 27 deletions(-) diff --git a/benchmark/leg_sf_benchmark_simple.cpp b/benchmark/leg_sf_benchmark_simple.cpp index 944cb68f..f2f26b17 100644 --- a/benchmark/leg_sf_benchmark_simple.cpp +++ b/benchmark/leg_sf_benchmark_simple.cpp @@ -55,7 +55,7 @@ void perform_single_nogradient_speed_test() static_cast(duration_con.count()) / 1e6); auto start = high_resolution_clock::now(); - auto mc = sf_leg.compute_mismatch_constraints(); + [[maybe_unused]] auto mc = sf_leg.compute_mismatch_constraints(); auto stop = high_resolution_clock::now(); auto duration = duration_cast(stop - start); fmt::print("\nLow-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, @@ -77,7 +77,7 @@ void perform_single_nogradient_speed_test() static_cast(duration_hf_con.count()) / 1e6); auto hf_start = high_resolution_clock::now(); - auto hf_mc = sf_hf_leg.compute_mismatch_constraints(); + [[maybe_unused]] auto hf_mc = sf_hf_leg.compute_mismatch_constraints(); auto hf_stop = high_resolution_clock::now(); auto hf_duration = duration_cast(hf_stop - hf_start); fmt::print("\nHigh-fidelity leg mc: {} nseg - timing: {}", m_throttles.size() / 3, @@ -131,7 +131,7 @@ void perform_single_nogradient_speed_test() auto bench_udp_n = sf_bench_udp{m_rvs, m_ms, m_rvf, 1, 1, static_cast(m_throttles.size() / 3), false}; auto lf_ngrad_start = high_resolution_clock::now(); - auto lf_ngrad = bench_udp_a.gradient(chromosome); + auto lf_ngrad = bench_udp_n.gradient(chromosome); auto lf_ngrad_stop = high_resolution_clock::now(); auto lf_ngrad_duration = duration_cast(lf_ngrad_stop - lf_ngrad_start); fmt::print("\nLow-fidelity leg numerical gradient: {} nseg - timing: {}", m_throttles.size() / 3, diff --git a/include/kep3/leg/sims_flanagan_hf.hpp b/include/kep3/leg/sims_flanagan_hf.hpp index 862f6d7f..fce03a75 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -50,7 +50,7 @@ class kep3_DLL_PUBLIC sims_flanagan_hf // Constructor with rvm states sims_flanagan_hf(const std::array &rvms, std::vector throttles, const std::array &rvmf, double tof, double max_thrust, double isp, double mu, - double cut, double tol); + double cut, double tol = 1e-16); // Setters void set_tof(double tof); @@ -67,8 +67,8 @@ class kep3_DLL_PUBLIC sims_flanagan_hf void set_tol(double tol); void set_rvms(const std::array &rvms); void set_rvmf(const std::array &rvmf); - void set_tas(const heyoka::taylor_adaptive &tas); - void set_tas_var(const heyoka::taylor_adaptive &tas_var); + // void set_tas(const heyoka::taylor_adaptive &tas); + // void set_tas_var(const heyoka::taylor_adaptive &tas_var); // Backwards-compatible setting function with rv and m states separately void set(const std::array, 2> &rvs, double ms, const std::vector &throttles, const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, diff --git a/pykep/core.cpp b/pykep/core.cpp index 7a353bdd..780bbf8f 100644 --- a/pykep/core.cpp +++ b/pykep/core.cpp @@ -616,8 +616,8 @@ PYBIND11_MODULE(core, m) // NOLINT PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tof); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(max_thrust); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(isp); - PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas); - PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas_var); + // PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas); + // PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tas_var); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(mu); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(cut); PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tol); diff --git a/src/leg/sf_checks.cpp b/src/leg/sf_checks.cpp index e407c1f1..b048a9af 100644 --- a/src/leg/sf_checks.cpp +++ b/src/leg/sf_checks.cpp @@ -18,16 +18,12 @@ void _check_tof(double tof) throw std::domain_error("The time of flight of a sims_flanagan leg needs to be larger or equal to zero."); } } -void _check_throttles(const std::vector &throttles, unsigned nseg) +void _check_throttles(const std::vector &throttles) { if ((throttles.size() % 3) != 0u) { throw std::logic_error("The throttles of a sims_flanagan leg are detected to be not a multiple of 3 in size " "[u0x, u0y, u0z, .....]."); } - if (throttles.size() / 3 != static_cast(nseg)) - { - throw std::logic_error("The throttle count does not correspond to the number of segments provided."); - } if (throttles.empty()) { throw std::logic_error( "The throttles of a sims_flanagan leg are detected to be empty! At least one segment is necessary."); @@ -75,7 +71,7 @@ void _check_nseg(unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck) void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, double cut, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck) { - _check_throttles(throttles, nseg); + _check_throttles(throttles); _check_tof(tof); _check_max_thrust(max_thrust); _check_isp(isp); @@ -86,7 +82,7 @@ void _sanity_checks(const std::vector &throttles, double tof, double max void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, double cut, double tol, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck) { - _check_throttles(throttles, nseg); + _check_throttles(throttles); _check_tof(tof); _check_max_thrust(max_thrust); _check_isp(isp); diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index ed49154b..c16994a2 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -235,14 +235,14 @@ void sims_flanagan_hf::set_rvmf(const std::array &rvmf) { m_rvmf = rvmf; } -void sims_flanagan_hf::set_tas(const heyoka::taylor_adaptive &tas) -{ - m_tas = tas; -} -void sims_flanagan_hf::set_tas_var(const heyoka::taylor_adaptive &tas_var) -{ - m_tas_var = tas_var; -} +// void sims_flanagan_hf::set_tas(const heyoka::taylor_adaptive &tas) +// { +// m_tas = tas; +// } +// void sims_flanagan_hf::set_tas_var(const heyoka::taylor_adaptive &tas_var) +// { +// m_tas_var = tas_var; +// } void sims_flanagan_hf::set(const std::array, 2> &rvs, double ms, const std::vector &throttles, diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index 5c06c9d4..1f3d7548 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -64,6 +64,10 @@ TEST_CASE("constructor") double mf = 1.; REQUIRE_NOTHROW( kep3::leg::sims_flanagan_hf(rvs, ms, {0., 0., 0., 0., 0., 0.}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5)); + const std::array rvms{1, 0, 0, 0, 1, 0, 1}; + const std::array rvmf{0, 1, 0, -1, 0, 0, 1}; + REQUIRE_NOTHROW( + kep3::leg::sims_flanagan_hf(rvms, {0., 0., 0., 0., 0., 0.}, rvmf, kep3::pi / 2, 1., 1., 1., 0.5)); REQUIRE_THROWS_AS( kep3::leg::sims_flanagan_hf(rvs, ms, {0., 0., 0., 0., 0.}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), std::logic_error); @@ -86,6 +90,8 @@ TEST_CASE("constructor") std::domain_error); REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), std::logic_error); + REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5, -1e-2), + std::logic_error); } } @@ -120,23 +126,42 @@ TEST_CASE("getters_and_setters") REQUIRE(sf.get_mu() == 0.333); sf.set_tof(0.333); REQUIRE(sf.get_tof() == 0.333); + sf.set_tol(1e-4); + REQUIRE(sf.get_tol() == 1e-4); } { kep3::leg::sims_flanagan_hf sf{}; std::array, 2> rvf{{{1, 1, 1}, {1, 1, 1}}}; std::vector throttles{1., 2., 3., 1., 2., 3.}; - sf.set(rvf, 12, throttles, rvf, 12, 4, 4, 4, 4, 0.333); + sf.set(rvf, 12, throttles, rvf, 12, 4, 4, 4, 4, 0.333, 2e-5); REQUIRE(sf.get_rvs() == rvf); REQUIRE(sf.get_ms() == 12); REQUIRE(sf.get_rvf() == rvf); REQUIRE(sf.get_mf() == 12); REQUIRE(sf.get_throttles() == throttles); + REQUIRE(sf.get_max_thrust() == 4); + REQUIRE(sf.get_isp() == 4); + REQUIRE(sf.get_mu() == 4); + REQUIRE(sf.get_tof() == 4); REQUIRE(sf.get_cut() == 0.333); + REQUIRE(sf.get_tol() == 2e-5); + } + { + kep3::leg::sims_flanagan_hf sf{}; + std::array rvms{1, 1, 1, 1, 1, 1, 1}; + std::vector throttles{1., 2., 3., 1., 2., 3.}; + + sf.set(rvms, throttles, rvms, 4, 4, 4, 4, 0.333, 2e-5); + REQUIRE(sf.get_rvms() == rvms); + REQUIRE(sf.get_rvmf() == rvms); + REQUIRE(sf.get_throttles() == throttles); REQUIRE(sf.get_max_thrust() == 4); REQUIRE(sf.get_isp() == 4); REQUIRE(sf.get_mu() == 4); REQUIRE(sf.get_tof() == 4); + REQUIRE(sf.get_cut() == 0.333); + REQUIRE(sf.get_tol() == 2e-5); } } diff --git a/test/leg_sims_flanagan_test.cpp b/test/leg_sims_flanagan_test.cpp index 298fd869..4e24d20e 100644 --- a/test/leg_sims_flanagan_test.cpp +++ b/test/leg_sims_flanagan_test.cpp @@ -9,11 +9,11 @@ #include #include -#include #include #include #include +#include #include #include @@ -31,7 +31,6 @@ #include "catch.hpp" #include "leg_sims_flanagan_udp.hpp" -#include "test_helpers.hpp" TEST_CASE("constructor") { @@ -72,6 +71,8 @@ TEST_CASE("constructor") std::domain_error); REQUIRE_THROWS_AS(kep3::leg::sims_flanagan(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), std::logic_error); + REQUIRE_THROWS_AS(kep3::leg::sims_flanagan(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), + std::logic_error); } } @@ -118,11 +119,11 @@ TEST_CASE("getters_and_setters") REQUIRE(sf.get_rvf() == rvf); REQUIRE(sf.get_mf() == 12); REQUIRE(sf.get_throttles() == throttles); - REQUIRE(sf.get_cut() == 0.333); REQUIRE(sf.get_max_thrust() == 4); REQUIRE(sf.get_isp() == 4); REQUIRE(sf.get_mu() == 4); REQUIRE(sf.get_tof() == 4); + REQUIRE(sf.get_cut() == 0.333); } } From f39fd813fe434a9b71907772996d5382485f7fcf Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 8 Nov 2024 13:44:15 +0100 Subject: [PATCH 20/22] Minor bug fix. --- include/kep3/leg/sf_checks.hpp | 2 +- src/leg/sims_flanagan.cpp | 3 +-- src/leg/sims_flanagan_hf.cpp | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/include/kep3/leg/sf_checks.hpp b/include/kep3/leg/sf_checks.hpp index 46628b50..53ba6620 100644 --- a/include/kep3/leg/sf_checks.hpp +++ b/include/kep3/leg/sf_checks.hpp @@ -15,7 +15,7 @@ // These checks are used for the low- and high-fidelity legs (in sims_flanagan.cpp and sims_flanagan_hf.cpp) void _check_tof(double tof); -void _check_throttles(const std::vector &throttles, unsigned nseg); +void _check_throttles(const std::vector &throttles); void _check_max_thrust(double max_thrust); void _check_isp(double isp); void _check_mu(double mu); diff --git a/src/leg/sims_flanagan.cpp b/src/leg/sims_flanagan.cpp index 8dfebe93..ab3f1f44 100644 --- a/src/leg/sims_flanagan.cpp +++ b/src/leg/sims_flanagan.cpp @@ -69,8 +69,7 @@ void sims_flanagan::set_ms(double mass) } void sims_flanagan::set_throttles(const std::vector &throttles) { - auto nseg = static_cast(throttles.size()) / 3u; - _check_throttles(throttles, nseg); + _check_throttles(throttles); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index c16994a2..89ff84fd 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -161,8 +161,7 @@ void sims_flanagan_hf::set_ms(double mass) } void sims_flanagan_hf::set_throttles(const std::vector &throttles) { - auto nseg = static_cast(throttles.size()) / 3u; - _check_throttles(throttles, nseg); + _check_throttles(throttles); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); From f3f7219f61ea919873c007dc31900dd0c52715e5 Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Fri, 8 Nov 2024 15:06:48 +0100 Subject: [PATCH 21/22] Minor improvements to code coverage. --- include/kep3/leg/sf_checks.hpp | 26 +++++---- src/leg/sf_checks.cpp | 6 ++- src/leg/sims_flanagan.cpp | 16 +++--- src/leg/sims_flanagan_hf.cpp | 87 +++++++++++------------------- test/leg_sims_flanagan_hf_test.cpp | 10 +++- 5 files changed, 68 insertions(+), 77 deletions(-) diff --git a/include/kep3/leg/sf_checks.hpp b/include/kep3/leg/sf_checks.hpp index 53ba6620..b37de2c6 100644 --- a/include/kep3/leg/sf_checks.hpp +++ b/include/kep3/leg/sf_checks.hpp @@ -11,19 +11,25 @@ #define kep3_SF_CHECKS_H #include +#include // These checks are used for the low- and high-fidelity legs (in sims_flanagan.cpp and sims_flanagan_hf.cpp) +namespace kep3::leg { -void _check_tof(double tof); -void _check_throttles(const std::vector &throttles); -void _check_max_thrust(double max_thrust); -void _check_isp(double isp); -void _check_mu(double mu); -void _check_cut(double cut); -void _check_tol(double tol); -void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, +kep3_DLL_PUBLIC void _check_tof(double tof); +kep3_DLL_PUBLIC void _check_throttles(const std::vector &throttles); +kep3_DLL_PUBLIC void _check_max_thrust(double max_thrust); +kep3_DLL_PUBLIC void _check_isp(double isp); +kep3_DLL_PUBLIC void _check_mu(double mu); +kep3_DLL_PUBLIC void _check_cut(double cut); +kep3_DLL_PUBLIC void _check_tol(double tol); +kep3_DLL_PUBLIC void _check_nseg(unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck); +kep3_DLL_PUBLIC void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, double cut, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck); -void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, +kep3_DLL_PUBLIC void _sanity_checks(const std::vector &throttles, double tof, double max_thrust, double isp, double mu, double cut, double tol, unsigned nseg, unsigned nseg_fwd, unsigned nseg_bck); -#endif \ No newline at end of file + +} // namespace kep3::leg + +#endif diff --git a/src/leg/sf_checks.cpp b/src/leg/sf_checks.cpp index b048a9af..6ced0cc1 100644 --- a/src/leg/sf_checks.cpp +++ b/src/leg/sf_checks.cpp @@ -9,12 +9,14 @@ #include #include +#include + +namespace kep3::leg { void _check_tof(double tof) { // SC: One should be able to give this as a negative number to run the system backwards, no? if (tof < 0.) { - ; throw std::domain_error("The time of flight of a sims_flanagan leg needs to be larger or equal to zero."); } } @@ -91,3 +93,5 @@ void _sanity_checks(const std::vector &throttles, double tof, double max _check_tol(tol); _check_nseg(nseg, nseg_fwd, nseg_bck); } + +} // namespace kep3::leg \ No newline at end of file diff --git a/src/leg/sims_flanagan.cpp b/src/leg/sims_flanagan.cpp index ab3f1f44..604e4591 100644 --- a/src/leg/sims_flanagan.cpp +++ b/src/leg/sims_flanagan.cpp @@ -50,13 +50,13 @@ sims_flanagan::sims_flanagan(const std::array, 2> &rvs, do m_nseg(static_cast(m_throttles.size()) / 3u), m_nseg_fwd(static_cast(static_cast(m_nseg) * m_cut)), m_nseg_bck(m_nseg - m_nseg_fwd) { - _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_nseg, m_nseg_fwd, m_nseg_bck); } // Setters void sims_flanagan::set_tof(double tof) { - _check_tof(tof); + kep3::leg::_check_tof(tof); m_tof = tof; } void sims_flanagan::set_rvs(const std::array, 2> &rv) @@ -69,7 +69,7 @@ void sims_flanagan::set_ms(double mass) } void sims_flanagan::set_throttles(const std::vector &throttles) { - _check_throttles(throttles); + kep3::leg::_check_throttles(throttles); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); @@ -97,22 +97,22 @@ void sims_flanagan::set_mf(double mass) } void sims_flanagan::set_max_thrust(double max_thrust) { - _check_max_thrust(max_thrust); + kep3::leg::_check_max_thrust(max_thrust); m_max_thrust = max_thrust; } void sims_flanagan::set_isp(double isp) { - _check_isp(isp); + kep3::leg::_check_isp(isp); m_isp = isp; } void sims_flanagan::set_mu(double mu) { - _check_mu(mu); + kep3::leg::_check_mu(mu); m_mu = mu; } void sims_flanagan::set_cut(double cut) { - _check_cut(cut); + kep3::leg::_check_cut(cut); m_cut = cut; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; @@ -123,7 +123,7 @@ void sims_flanagan::set(const std::array, 2> &rvs, double const std::array, 2> &rvf, double mf, double tof, double max_thrust, double isp, double mu, double cut) { - _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_nseg, m_nseg_fwd, m_nseg_bck); m_rvs = rvs; m_ms = ms; m_throttles = throttles; diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index 89ff84fd..bd334dfb 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -46,7 +46,7 @@ namespace kep3::leg sims_flanagan_hf::sims_flanagan_hf() { // We perform some sanity checks on the user provided inputs - _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Initialize m_tas and m_tas_var const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); @@ -80,7 +80,7 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array, 2> &r m_nseg_fwd(static_cast(static_cast(m_nseg) * m_cut)), m_nseg_bck(m_nseg - m_nseg_fwd) { // We perform some sanity checks on the user provided inputs - _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Initialize m_tas and m_tas_var const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); @@ -120,7 +120,7 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array &rvms, std::vecto m_nseg_fwd(static_cast(static_cast(m_nseg) * m_cut)), m_nseg_bck(m_nseg - m_nseg_fwd) { // We perform some sanity checks on the user provided inputs - _sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(m_throttles, m_tof, m_max_thrust, m_isp, m_mu, m_cut, m_tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Initialize m_tas and m_tas_var const heyoka::taylor_adaptive ta_cache = kep3::ta::get_ta_stark(m_tol); @@ -147,7 +147,7 @@ sims_flanagan_hf::sims_flanagan_hf(const std::array &rvms, std::vecto // Setters void sims_flanagan_hf::set_tof(double tof) { - _check_tof(tof); + kep3::leg::_check_tof(tof); m_tof = tof; } void sims_flanagan_hf::set_rvs(const std::array, 2> &rv) @@ -161,7 +161,7 @@ void sims_flanagan_hf::set_ms(double mass) } void sims_flanagan_hf::set_throttles(const std::vector &throttles) { - _check_throttles(throttles); + kep3::leg::_check_throttles(throttles); m_throttles = std::move(throttles); m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); @@ -201,29 +201,29 @@ void sims_flanagan_hf::set_mf(double mass) } void sims_flanagan_hf::set_max_thrust(double max_thrust) { - _check_max_thrust(max_thrust); + kep3::leg::_check_max_thrust(max_thrust); m_max_thrust = max_thrust; } void sims_flanagan_hf::set_isp(double isp) { - _check_isp(isp); + kep3::leg::_check_isp(isp); m_isp = isp; } void sims_flanagan_hf::set_mu(double mu) { - _check_mu(mu); + kep3::leg::_check_mu(mu); m_mu = mu; } void sims_flanagan_hf::set_cut(double cut) { - _check_cut(cut); + kep3::leg::_check_cut(cut); m_cut = cut; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; } void sims_flanagan_hf::set_tol(double tol) { - _check_tol(tol); + kep3::leg::_check_tol(tol); m_tol = tol; } void sims_flanagan_hf::set_rvms(const std::array &rvms) @@ -265,7 +265,7 @@ void sims_flanagan_hf::set(const std::array, 2> &rvs, doub m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; - _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; @@ -289,7 +289,7 @@ void sims_flanagan_hf::set(const std::array &rvms, const std::vector< m_nseg = static_cast(m_throttles.size()) / 3u; m_nseg_fwd = static_cast(static_cast(m_nseg) * m_cut); m_nseg_bck = m_nseg - m_nseg_fwd; - _sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); + kep3::leg::_sanity_checks(throttles, tof, max_thrust, isp, mu, cut, tol, m_nseg, m_nseg_fwd, m_nseg_bck); // Convert throttles to current_thrusts. auto throttle_to_thrust = [this](double throttle) { return throttle * get_max_thrust(); }; @@ -410,13 +410,9 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() const // Loop through segments in forward pass of Sims-Flanagan transcription for (auto i = 0u; i < m_nseg_fwd; ++i) { // Assign current thrusts to Taylor adaptive integrator - if (static_cast((i + 1) * 3) <= m_thrusts.size()) { - std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), - std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), - std::next(m_tas.get_pars_data(), 2)); - } else { - throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); - } + std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), + std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), + std::next(m_tas.get_pars_data(), 2)); // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until((i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { @@ -436,14 +432,9 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() const // Loop through segments in backward pass of Sims-Flanagan transcription for (auto i = 0u; i < m_nseg_bck; ++i) { // Assign current_thrusts to Taylor adaptive integrator - if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { - // Copy thrust into Taylor-adaptive integrator - std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), - std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), - std::next(m_tas.get_pars_data(), 2)); - } else { - throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); - } + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), + std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), + std::next(m_tas.get_pars_data(), 2)); // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until(m_tof - (i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { @@ -559,13 +550,9 @@ sims_flanagan_hf::compute_all_gradients() const // Initialise var conditions std::copy(m_vars.begin(), m_vars.end(), m_tas_var.get_state_data() + 7); // Assign current thrusts to Taylor adaptive integrator - if (static_cast((i + 1) * 3) <= m_thrusts.size()) { - std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), - std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), - std::next(m_tas_var.get_pars_data(), 2)); - } else { - throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); - } + std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), + std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), + std::next(m_tas_var.get_pars_data(), 2)); // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas_var.propagate_until((i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { @@ -592,14 +579,9 @@ sims_flanagan_hf::compute_all_gradients() const // Initialise var conditions std::copy(m_vars.begin(), m_vars.end(), m_tas_var.get_state_data() + 7); // Assign current thrusts to Taylor adaptive integrator - if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { - // Copy thrust into Taylor-adaptive integrator - std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), - std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), - std::next(m_tas_var.get_pars_data(), 2)); - } else { - throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); - } + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), + std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), + std::next(m_tas_var.get_pars_data(), 2)); // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas_var.propagate_until(m_tof - (i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { @@ -804,13 +786,9 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned gr // Loop through segments in forward pass of Sims-Flanagan transcription for (decltype(m_nseg_fwd) i = 0u; i < m_nseg_fwd; ++i) { // Assign current thrusts to Taylor adaptive integrator - if (static_cast((i + 1) * 3) <= m_thrusts.size()) { - std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), - std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), - std::next(m_tas.get_pars_data(), 2)); - } else { - throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); - } + std::copy(std::next(m_thrusts.begin(), static_cast(i * 3)), + std::next(m_thrusts.begin(), static_cast(3 * (i + 1))), + std::next(m_tas.get_pars_data(), 2)); // Current leg time grid std::copy(std::next(leg_time_grid.begin(), i * (grid_points_per_segment - 1)), @@ -835,14 +813,9 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned gr // Loop through segments in backward pass of Sims-Flanagan transcription for (decltype(m_nseg) i = 0u; i < m_nseg_bck; ++i) { // Assign current_thrusts to Taylor adaptive integrator - if (static_cast((m_nseg - i) * 3) <= m_thrusts.size()) { - // Copy thrust into Taylor-adaptive integrator - std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), - std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), - std::next(m_tas.get_pars_data(), 2)); - } else { - throw std::runtime_error("The retrieved thrust index is larger than the size of the m_thrusts vector."); - } + std::copy(std::next(m_thrusts.begin(), static_cast((m_nseg - (i + 1)) * 3)), + std::next(m_thrusts.begin(), static_cast((m_nseg - i) * 3)), + std::next(m_tas.get_pars_data(), 2)); // Current leg time grid std::reverse_copy(leg_time_grid.begin() + (m_nseg - (i + 1)) * (grid_points_per_segment - 1), diff --git a/test/leg_sims_flanagan_hf_test.cpp b/test/leg_sims_flanagan_hf_test.cpp index 1f3d7548..0f490fb9 100644 --- a/test/leg_sims_flanagan_hf_test.cpp +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -90,8 +91,13 @@ TEST_CASE("constructor") std::domain_error); REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5), std::logic_error); - REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5, -1e-2), + REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5, -1e-2), + std::domain_error); + REQUIRE_THROWS_AS(kep3::leg::sims_flanagan_hf(rvs, ms, {0, 0, 0, 0, 0, 0}, rvf, mf, kep3::pi / 2, 1., 1., 1., 0.5, 1.2), + std::domain_error); + REQUIRE_THROWS_AS(kep3::leg::_check_nseg(2, 1, 2), std::logic_error); + } } @@ -162,6 +168,8 @@ TEST_CASE("getters_and_setters") REQUIRE(sf.get_tof() == 4); REQUIRE(sf.get_cut() == 0.333); REQUIRE(sf.get_tol() == 2e-5); + REQUIRE(typeid(sf.get_tas()) == typeid(kep3::ta::get_ta_stark(sf.get_tol()))); + REQUIRE(typeid(sf.get_tas_var()) == typeid(kep3::ta::get_ta_stark_var(sf.get_tol()))); } } From 4870cdda2db16e868a205bbef229128e69b3ce8a Mon Sep 17 00:00:00 2001 From: Sean Cowan Date: Thu, 14 Nov 2024 11:28:39 +0100 Subject: [PATCH 22/22] Added hints to ignore lines for codecov. --- src/leg/sims_flanagan_hf.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/leg/sims_flanagan_hf.cpp b/src/leg/sims_flanagan_hf.cpp index bd334dfb..2cde273b 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -378,6 +378,7 @@ unsigned sims_flanagan_hf::get_nseg_bck() const { return m_nseg_bck; } +// LCOV_EXCL_START const heyoka::taylor_adaptive &sims_flanagan_hf::get_tas() const { return m_tas; @@ -386,6 +387,7 @@ const heyoka::taylor_adaptive &sims_flanagan_hf::get_tas_var() const { return m_tas_var; } +// LCOV_EXCL_END const std::array &sims_flanagan_hf::get_rvms() const { return m_rvms; @@ -416,7 +418,7 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() const // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until((i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { - throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } } @@ -438,7 +440,7 @@ std::array sims_flanagan_hf::compute_mismatch_constraints() const // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas.propagate_until(m_tof - (i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { - throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } } @@ -556,7 +558,7 @@ sims_flanagan_hf::compute_all_gradients() const // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas_var.propagate_until((i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { - throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } // Save the variational state variables to respective arrays std::copy(m_tas_var.get_state().begin(), m_tas_var.get_state().begin() + 7, xf_per_seg[i].begin()); @@ -585,7 +587,7 @@ sims_flanagan_hf::compute_all_gradients() const // ... and integrate auto [status, min_h, max_h, nsteps, _1, _2] = m_tas_var.propagate_until(m_tof - (i + 1) * prop_seg_duration); if (status != heyoka::taylor_outcome::time_limit) { - throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } // Save the variational state variables to respective arrays std::copy(m_tas_var.get_state().begin(), m_tas_var.get_state().begin() + 7, @@ -696,7 +698,7 @@ sims_flanagan_hf::get_relevant_gradients(const std::vector> sims_flanagan_hf::get_state_history(unsigned gr // ... and integrate auto [status, min_h, max_h, nsteps, _1, output_states] = m_tas.propagate_grid(current_leg_time_grid); if (status != heyoka::taylor_outcome::time_limit) { - throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } output_per_seg[i] = output_states; } @@ -826,7 +828,7 @@ std::vector> sims_flanagan_hf::get_state_history(unsigned gr // ... and integrate auto [status, min_h, max_h, nsteps, _1, output_states] = m_tas.propagate_grid(back_time_grid); if (status != heyoka::taylor_outcome::time_limit) { - throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } output_per_seg[m_nseg - 1 - i] = output_states; }