diff --git a/CMakeLists.txt b/CMakeLists.txt index 515e8a83..319dc06a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,6 +145,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/flyby.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/core_astro/ic2par2ic.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/core_astro/ic2eq2ic.cpp" diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 4a05611a..84cbf9fc 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -24,5 +24,7 @@ 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) +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..f2f26b17 --- /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(); + [[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, + 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(); + [[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, + 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_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, + 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 eaa1cc10..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_test_speed(unsigned N, unsigned nseg, unsigned pop_size) +void perform_convergence_benchmark(unsigned N, unsigned nseg) { // // Engines @@ -42,77 +42,86 @@ 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 / 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); 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 (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 = 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}; - // 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(unsigned N, unsigned nseg, unsigned pop_size) { // // Engines @@ -122,90 +131,93 @@ 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 / 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); 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}}; + double count_n = 0; + double count_a = 0; + 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 = 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 = 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}; + + // 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() { - // 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"); + 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 new file mode 100644 index 00000000..e075a07f --- /dev/null +++ b/benchmark/leg_sims_flanagan_hf_benchmark.cpp @@ -0,0 +1,217 @@ +// 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" + +using std::chrono::duration_cast; +using std::chrono::high_resolution_clock; +using std::chrono::microseconds; + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +void perform_convergence_benchmark(unsigned N, unsigned nseg) +{ + // + // Engines + // + // NOLINTNEXTLINE(cert-msc32-c, cert-msc51-cpp) + std::mt19937 rng_engine(122012203u); + // + // Distributions + // + 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 / 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); + kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); + kep3::planet earth{udpla_earth}; + kep3::planet jupiter{udpla_jupiter}; + 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 (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 = 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); + 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]); + + // 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}; + + // 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 {}/{}\n", nseg, count_a, N, count_n, N); +} + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +void perform_speed_benchmark(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_random(0., 0.1); + std::uniform_real_distribution mass_random(1.0, 1.2); + 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); + kep3::udpla::vsop2013 udpla_jupiter("jupiter", 1e-2); + kep3::planet earth{udpla_earth}; + kep3::planet jupiter{udpla_jupiter}; + 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 (decltype(N) i = 0; i < N; ++i) { + auto rvs = earth.eph(ts_random(rng_engine)); + auto rvf = jupiter.eph(ts_random(rng_engine)); + 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; + + // Create HF legs + double mass = mass_random(rng_engine); + double max_thrust = 1; + double isp = 1; + 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); + prob_n.set_c_tol(1e-8); + + // We construct the same random population + pagmo::population pop{prob_a, pop_size}; + + // 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}; + + // 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 + 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("{} 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:\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 new file mode 100644 index 00000000..3f1357cd --- /dev/null +++ b/benchmark/leg_sims_flanagan_hf_udp_bench.hpp @@ -0,0 +1,197 @@ +// 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_HF_UDP_BENCH_H +#define kep3_TEST_LEG_SIMS_FLANAGAN_HF_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), + 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]; // 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 + 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 = 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 = 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 + { + if (m_analytical) { + return _gradient_analytical(x); + } else { + return _gradient_numerical(x); + } + } + + [[nodiscard]] std::vector _gradient_numerical(const std::vector &x) const + { + 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]; // 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 + m_leg.set_throttles(x.begin(), x.end() - 2); + + // We compute the 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) = 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}); + + xgradient(0, m_nseg * 3 + 1) = -1.; // fitness gradient - obj fun + xt::view(xgradient, xt::range(1u, 4u), xt::range(0, m_nseg * 3u + 1u)) + = 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_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_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_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_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)); + + 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] = 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}; + } + + [[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{}; + // m_leg needs to be mutable because the heyoka integrator needs to be modifiable + mutable kep3::leg::sims_flanagan_hf m_leg{}; +}; + +#endif \ No newline at end of file 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/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_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 c33e8a53..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", @@ -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": "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 diff --git a/include/kep3/leg/sf_checks.hpp b/include/kep3/leg/sf_checks.hpp new file mode 100644 index 00000000..b37de2c6 --- /dev/null +++ b/include/kep3/leg/sf_checks.hpp @@ -0,0 +1,35 @@ +// 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 +#include + +// These checks are used for the low- and high-fidelity legs (in sims_flanagan.cpp and sims_flanagan_hf.cpp) +namespace kep3::leg { + +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); +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); + + +} // namespace kep3::leg + +#endif diff --git a/include/kep3/leg/sims_flanagan.hpp b/include/kep3/leg/sims_flanagan.hpp index a42faf09..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,26 +103,26 @@ 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. - 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..fce03a75 100644 --- a/include/kep3/leg/sims_flanagan_hf.hpp +++ b/include/kep3/leg/sims_flanagan_hf.hpp @@ -11,16 +11,16 @@ #define kep3_LEG_SIMS_FLANAGAN_HF_H #include -#include -#include - +#include #include +#include #include #include #include #include +#include namespace kep3::leg { @@ -40,90 +40,130 @@ namespace kep3::leg class kep3_DLL_PUBLIC sims_flanagan_hf { public: - // Default Constructor. - 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); + // Default Constructor. + sims_flanagan_hf(); // = default; + // 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 = 1e-16); // 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_ts(double ts); + 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); + // 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); + void set(const std::array &rvms, const std::vector &throttles, const std::array &rvmf, + double time_of_flight); // 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]] 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 compute_mismatch_constraints() const; [[nodiscard]] std::vector compute_throttle_constraints() const; + [[nodiscard]] std::vector compute_constraints() const; + [[nodiscard]] std::vector set_and_compute_constraints(const std::vector &chromosome); - // Compute mismatch constraint gradients (w.r.t. rvm state and w.r.t. throttles, tof) + // 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]] + std::tuple>, std::vector>, + 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) + [[nodiscard]] std::tuple, std::array, std::vector> + 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) [[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 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_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_vars; + 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; } - // 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.}; + // Initial variational state + std::array m_vars{}; // 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,14 +174,18 @@ 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; + // Taylor-adaptive integrator + // m_tas needs to be mutable because the heyoka integrator needs to be modifiable + mutable heyoka::taylor_adaptive m_tas{}; + // 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{}; }; // Streaming operator for the class kep3::leg::sims_flanagan. diff --git a/include/kep3/ta/stark.hpp b/include/kep3/ta/stark.hpp index 4c986bab..b994353f 100644 --- a/include/kep3/ta/stark.hpp +++ b/include/kep3/ta/stark.hpp @@ -10,7 +10,6 @@ #ifndef kep3_TA_STARK_H #define kep3_TA_STARK_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/pykep/core.cpp b/pykep/core.cpp index fd64eb85..780bbf8f 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,210 @@ 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(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); + 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); + PYKEP3_EXPOSE_LEG_SF_HF_ATTRIBUTES(tol); + +#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("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, + 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..41688747 100644 --- a/pykep/docstrings.cpp +++ b/pykep/docstrings.cpp @@ -1847,4 +1847,231 @@ 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_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, .....]."; +}; +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_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."; +}; +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_tol_docstring() +{ + return "The tolerance of the Taylor adaptive integrator."; +}; +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() +)"; +}; + +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 a3163ccb..4adb0419 100644 --- a/pykep/docstrings.hpp +++ b/pykep/docstrings.hpp @@ -125,6 +125,34 @@ 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_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(); +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(); +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/__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 3d101d95..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 @@ -186,3 +186,160 @@ 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. 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. + + *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) + 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, + ) + + throttles_fwd = throttles[:, 0 : nseg_fwd * 3] + throttles_bck = throttles[:, nseg_fwd * 3 : nseg * 3] + + # 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): + 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", + ) + + if show_throttles: + 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", + ) + + 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): + 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", + ) + + if show_throttles: + 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 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/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/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..6ced0cc1 --- /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 +#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."); + } +} +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 _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); + _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); + _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); +} + +} // namespace kep3::leg \ No newline at end of file diff --git a/src/leg/sims_flanagan.cpp b/src/leg/sims_flanagan.cpp index f26ae5c1..604e4591 100644 --- a/src/leg/sims_flanagan.cpp +++ b/src/leg/sims_flanagan.cpp @@ -9,8 +9,6 @@ #include #include -#include -#include #include #include @@ -29,6 +27,7 @@ #include #include #include +#include #include #include @@ -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,16 +50,16 @@ 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); + 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(std::array, 2> rv) +void sims_flanagan::set_rvs(const std::array, 2> &rv) { m_rvs = rv; } @@ -123,15 +67,16 @@ 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) { - _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); 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."); @@ -142,7 +87,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; } @@ -152,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; @@ -178,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(rvs, ms, throttles, rvf, mf, tof, max_thrust, isp, mu, cut); + 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 358eb125..2cde273b 100644 --- a/src/leg/sims_flanagan_hf.cpp +++ b/src/leg/sims_flanagan_hf.cpp @@ -9,9 +9,10 @@ #include #include -#include -#include #include +#include +#include +#include #include #include @@ -21,129 +22,158 @@ #include #include +#include +#include #include #include #include #include #include -#include #include +#include #include #include +#include + +#include namespace kep3::leg { -using kep3::linalg::_dot; -using kep3::linalg::mat13; -using kep3::linalg::mat61; -using kep3::linalg::mat63; -using kep3::linalg::mat66; +// Constructors -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) +sims_flanagan_hf::sims_flanagan_hf() { - 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) + // We perform some sanity checks on the user provided inputs + 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); + 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; + + // ... 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) + 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) { - 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) -{ - _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 + 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); + 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; + + // ... 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); + // 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(); - } + 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); + 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; + + // ... 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); } // 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(std::array, 2> rv) +void sims_flanagan_hf::set_rvs(const 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) +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); 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) +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( @@ -154,57 +184,135 @@ 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) +void sims_flanagan_hf::set_rvf(const 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) { - _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) +{ + kep3::leg::_check_tol(tol); + m_tol = tol; +} +void sims_flanagan_hf::set_rvms(const std::array &rvms) +{ + m_rvms = rvms; +} +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(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) +{ + // 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; + 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(); }; + 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 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; + 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; + 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(); }; + 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 @@ -212,25 +320,31 @@ 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; + 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; + 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 +362,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,72 +378,76 @@ 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; +} +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; +} +const std::array &sims_flanagan_hf::get_rvmf() const +{ + return m_rvmf; +} // The core routines std::array sims_flanagan_hf::compute_mismatch_constraints() 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; + // General settings + const double prop_seg_duration = (m_tof / m_nseg); + // 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; + // 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 (auto i = 0u; i < m_nseg_fwd; ++i) { + // Assign current thrusts to Taylor adaptive integrator + 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) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE + } } + // Set fwd final state + std::vector rvm_fwd_final = m_tas.get_state(); + // 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; + // 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 (auto i = 0u; i < m_nseg_bck; ++i) { + // Assign current_thrusts to 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)); + // ... 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."); // LCOV_EXCL_LINE + } } - 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 {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_throttle_constraints() const @@ -338,251 +460,294 @@ 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]; +std::vector sims_flanagan_hf::compute_constraints() const +{ + 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; } -// 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::vector sims_flanagan_hf::set_and_compute_constraints(const 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[(7 + m_nseg * 3 + 7 + 1) - 1]; + // 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(const std::array &state, + const std::array &throttles) const +{ + + 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() * std::pow(r2, -3. / 2) * state[0] + thrusts[0] / state[6]; + dstatedt[4] = -get_mu() * std::pow(r2, -3. / 2) * state[1] + thrusts[1] / state[6]; + dstatedt[5] = -get_mu() * std::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>, std::vector>, + std::vector>> +sims_flanagan_hf::compute_all_gradients() const +{ + // Initialise + 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); + + // 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 (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); + // Assign current thrusts to Taylor adaptive integrator + 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) { + throw std::domain_error("stark_problem: failure to reach the final time requested during a propagation."); // LCOV_EXCL_LINE } - - 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); + // 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); } } - // 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]); + // 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 (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); + // Assign current thrusts to 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)); + // ... 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."); // 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[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); + } } - // 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]); + // Get ToF gradients + // Initialize initial state matrix + if (m_nseg_fwd > 0) { + x0_per_seg[0] = m_rvms; + } + 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 (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)]; } - 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)))); + 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); } - // 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)))); + + 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(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}); + // 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 (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}); + } else { + final_M = Mn_o[i]; + } + Mn_o[i + 1] = xt::linalg::dot(final_M, current_M); + } } - // 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)))); + // 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 (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}); + } else { + final_M = Mn_o[m_nseg_fwd + i]; + } + Mn_o[m_nseg_fwd + i + 1] = xt::linalg::dot(final_M, current_M); + } } - // 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) + + // Initial and final displacements + std::array grad_rvm = {0}; 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); + 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); } - // 3) We reverse the Isp, hence veff (a = 1/veff) - a = -a; + 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; + } - // 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); + // 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 (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); + } 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."); // LCOV_EXCL_LINE + } + xt::view(xgrad_final_throttle, xt::all(), xt::range(3 * i, 3 * (i + 1))) + = xt::linalg::dot(corresponding_M, current_U); } - // And finally return. - return std::make_pair(grad_rvm, std::move(grad)); + + // 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 (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 + [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."); // LCOV_EXCL_LINE + } + 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)}; } -// 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 +std::tuple, std::array, std::vector> +sims_flanagan_hf::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); + // 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(); - // We assemble the final results + std::array grad_rvm = {0}; + std::array grad_rvm_bck = {0}; 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); + 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, std::move(grad_final)}; } @@ -597,6 +762,80 @@ std::vector sims_flanagan_hf::compute_tc_grad() const return retval; } +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); + std::vector leg_time_grid; + // Initial time + double timestep = 0.0; + leg_time_grid.push_back(timestep); + + 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); + 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 (decltype(m_nseg_fwd) i = 0u; i < m_nseg_fwd; ++i) { + // Assign current thrusts to Taylor adaptive integrator + 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)), + 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."); // LCOV_EXCL_LINE + } + output_per_seg[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 (decltype(m_nseg) i = 0u; i < m_nseg_bck; ++i) { + // Assign current_thrusts to 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)); + + // 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."); // LCOV_EXCL_LINE + } + output_per_seg[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()); @@ -611,7 +850,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 9d112dd9..9885a34c 100644 --- a/src/ta/stark.cpp +++ b/src/ta/stark.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -53,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/CMakeLists.txt b/test/CMakeLists.txt index 8c3bfe78..14d3ed78 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -41,6 +41,7 @@ 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) ADD_kep3_TESTCASE(ta_cr3bp_test) -ADD_kep3_TESTCASE(flyby_test) \ No newline at end of file +ADD_kep3_TESTCASE(flyby_test) diff --git a/test/leg_sims_flanagan_hf_helpers.hpp b/test/leg_sims_flanagan_hf_helpers.hpp new file mode 100644 index 00000000..376db15e --- /dev/null +++ b/test/leg_sims_flanagan_hf_helpers.hpp @@ -0,0 +1,188 @@ +// 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 +#include + +struct sf_hf_test_object { + + // Default constructor + sf_hf_test_object() = default; + + explicit sf_hf_test_object(std::vector &throttles) : m_throttles(throttles) + { + for (double m_throttle : m_throttles) { + m_thrusts.push_back(m_throttle * m_max_thrust); + } + } + + explicit sf_hf_test_object(double cut) : m_cut(cut) {} + + 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); + } + } + + explicit sf_hf_test_object(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() + { + 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}; + *(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; + }; + + void set_cut(double cut) + { + m_cut = cut; + } + + [[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, + 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); + } + + [[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, + 1e-16); + 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(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)) + = 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}}}; + 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; + double m_tof = 1; + double m_max_thrust = 1; + double m_isp = 1; + double m_mu = 1; + 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}; +}; + +#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 new file mode 100644 index 00000000..0f490fb9 --- /dev/null +++ b/test/leg_sims_flanagan_hf_test.cpp @@ -0,0 +1,441 @@ +// 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 +#include + +#include "catch.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") +{ + { + // 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)); + 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); + 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); + 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); + 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); + + } +} + +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); + 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, 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); + 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()))); + } +} + +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); + } + } +} + +// Compare low-fidelity and high-fidelity methods with zero thrust (ought to be the same) +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]}; + + 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); +} + +// Compare high-fidelity method with manually calculated (direct heyoka interfacing) Taylor integration. +TEST_CASE("compute_mismatch_constraints_test3") +{ + + // 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_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") +{ + + // 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(); + 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, + 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(); + auto xt_tc_a_grad = xt::adapt(tc_a_grad, {nseg, 3u * nseg}); + + 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(); + unsigned 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("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(); + unsigned 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 + 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 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); } }