// This file is part of Bertini 2. // // powerseries_endgame.hpp is free software: you can redistribute it and/or // modify it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or //(at your option) any later version. // // powerseries_endgame.hpp is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with powerseries_endgame.hpp. If not, see // . // // Copyright(C) 2015 - 2021 by Bertini2 Development Team // // See for a copy of the license, // as well as COPYING. Bertini2 is provided with permitted // additional terms in the b2/licenses/ directory. // individual authors of this file include: // silviana amethyst, university of wisconsin eau claire // Tim Hodges, Colorado State University #pragma once #include "endgames/base_endgame.hpp" namespace bertini { namespace endgame { /** \class PowerSeriesEndgame \brief class used to finish tracking paths during Homotopy Continuation \ingroup endgame ## Explanation The bertini::PowerSeriesEndgame class enables us to finish tracking on possibly singular paths on an arbitrary square homotopy. The intended usage is to: 1. Create a system, tracker, and instantiate some settings. 2. Using the tracker created track to the engame boundary, by default this is t = 0.1. 3. Create a PowerSeriesEndgame, associating it to the tracker you wish to use. The tracker knows the system being solved. 4. For each path being tracked send the PowerSeriesEndgame the time value and other variable values that it should use to start the endgame. 5. The PowerSeriesEndgame, if successful, will store the homotopy solutions at t = 0. ## Example Usage Below we demonstrate a basic usage of the PowerSeriesEndgame class to find the singularity at t = 0. The pattern is as described above: create an instance of the class, feeding it the system to be used, and the endgame boundary time and other variable values at the endgame boundary. \code{.cpp} using namespace bertini::tracking; using RealT = tracking::TrackerTraits::BaseRealType; // Real types using ComplexT = tracking::TrackerTraits::BaseComplexType; Complex types // 1. Define the polynomial system that we wish to solve. System target_sys; Var x = Variable::Make("x"), t = Variable::Make("t"), y = Variable::Make("y"); VariableGroup vars{x,y}; target_sys.AddVariableGroup(vars); target_sys.AddFunction((pow(x-1,3)); target_sys.AddFunction((pow(y-1,2)); // 1b. Homogenize and patch the polynomial system to work over projective space. sys.Homogenize(); sys.AutoPatch(); // 2. Create a start system, for us we will use a total degree start system. auto TD_start_sys = bertini::start_system::TotalDegree(target_sys); // 2b. Creating homotopy between the start system and system we wish to solve. auto my_homotopy = (1-t)*target_sys + t*TD_start_sys*Rational::Rand(); //the random number is our gamma for a random path between t = 1 and t = 0. my_homotopy.AddPathVariable(t); //Sets up configuration settings for our particular system. auto precision_config = PrecisionConfig(my_homotopy); // 3. Creating a tracker. For us this is an AMPTracker. AMPTracker tracker(my_homotopy); //Tracker setup of settings. SteppingConfig stepping_preferences; stepping_preferences.initial_step_size = RealT(1)/RealT(5);// change a stepping preference NewtonConfig newton_preferences; tracker.Setup(TestedPredictor, RealFromString("1e-6"), RealFromString("1e5"), stepping_preferences, newton_preferences); tracker.PrecisionSetup(precision_config); //We start at t = 1, and will stop at t = 0.1 before starting the endgames. ComplexT t_start(1), t_endgame_boundary(0.1); //This will hold our solutions at t = 0.1 std::vector > my_homotopy_solutions_at_endgame_boundary; // result holds the value we track to at 0.1, and tracking success will report if we are unsucessful. Vec result; //4. Track all points to 0.1 for (unsigned ii = 0; ii < TD_start_sys.NumStartPoints(); ++ii) { DefaultPrecision(ambient_precision); my_homotopy.precision(ambient_precision); // making sure our precision is all set up auto start_point = TD_start_sys.StartPoint(ii); tracker.TrackPath(result,t_start,t_endgame_boundary,start_point); my_homotopy_solutions_at_endgame_boundary.push_back(result); } //Settings for the endgames. PowerSeriesConfig power_series_settings; power_series_settings.max_cycle_number = 4; // 5. Create a power series endgame, and use them to get the soutions at t = 0. EndgameSelector::PSEG my_pseg_endgame(tracker,power_series_settings,tolerances); std::vector > my_homotopy_solutions; std::vector > my_homotopy_divergent_paths; for(auto s : my_homotopy_solutions_at_endgame_boundary) { SuccessCode endgame_success = my_pseg_endgame.Run(t_endgame_boundary,s); if(endgame_success == SuccessCode::Success) { my_homotopy_solutions.push_back(my_homotopy.DehomogenizePoint(my_endgame.FinalApproximation())); } else { my_homotopy_divergent_paths.push_back(my_homotopy.DehomogenizePoint(my_endgame.FinalApproximation())); } } \endcode If this documentation is insufficient, please contact the authors with suggestions, or get involved! Pull requests welcomed. ## Testing Test suite driving this class: endgames_test. File: test/endgames/generic_pseg_test.hpp File: test/endgames/amp_powerseries_test.cpp File: test/endgames/fixed_double_powerseries_test.cpp FIle: test/endgames/fixed_multiple_powerseries_test.cpp */ template class PowerSeriesEndgame : public virtual EndgameBase, PrecT> { public: using BaseEGT = EndgameBase, PrecT>; using FinalEGT = PowerSeriesEndgame; using TrackerType = typename BaseEGT::TrackerType; using BaseComplexType = typename BaseEGT::BaseComplexType; using BaseRealType = typename BaseEGT::BaseRealType; using EmitterType = PowerSeriesEndgame; protected: using EndgameBase, PrecT>::NotifyObservers; using TupleOfTimes = typename BaseEGT::TupleOfTimes; using TupleOfSamps = typename BaseEGT::TupleOfSamps; using BCT = BaseComplexType; using BRT = BaseRealType; using Configs = typename AlgoTraits::NeededConfigs; using ConfigsAsTuple = typename Configs::ToTuple; /** \brief State variable representing a computed upper bound on the cycle number. */ mutable unsigned upper_bound_on_cycle_number_; /** \brief Holds the time values for different space values used in the Power series endgame. */ mutable TupleOfTimes times_; /** \brief Holds the space values used in the Power series endgame. */ mutable TupleOfSamps samples_; /** \brief Holds the derivatives at each space point. */ mutable TupleOfSamps derivatives_; /** \brief Random vector used in computing an upper bound on the cycle number. */ mutable Vec rand_vector_; template void AssertSizesTimeSpace() const { const auto num_sample_points = this->EndgameSettings().num_sample_points; assert(std::get>(samples_).size() == std::get>(times_).size() && "must have same number of samples in times and spaces"); assert(std::get>(samples_).size() >= num_sample_points && "must have sufficient number of samples"); } template void AssertSizesTimeSpaceDeriv() const { const auto num_sample_points = this->EndgameSettings().num_sample_points; assert(std::get>(samples_).size() == std::get>(times_).size() && "must have same number of samples in times and spaces"); assert(std::get>(samples_).size() == std::get>(derivatives_).size() && "must have same number of samples in derivatives and spaces"); assert(std::get>(samples_).size() >= num_sample_points && "must have sufficient number of samples"); } public: auto UpperBoundOnCycleNumber() const { return upper_bound_on_cycle_number_; } /** \brief Function that clears all samples and times from data members for the Power Series endgame */ template void ClearTimesAndSamples() { std::get>(times_).clear(); std::get>(samples_).clear(); } /** \brief Function to set the times used for the Power Series endgame. */ template void SetTimes(TimeCont const& times_to_set) { std::get>(times_) = times_to_set; } /** \brief Function to get the times used for the Power Series endgame. */ template const auto& GetTimes() const { return std::get>(times_); } const BCT& LatestTimeImpl() const { return GetTimes().back(); } /** \brief Function to set the space values used for the Power Series endgame. */ template void SetSamples(SampCont const& samples_to_set) { std::get>(samples_) = samples_to_set; } /** \brief Function to get the space values used for the Power Series endgame. */ template const auto& GetSamples() const { return std::get>(samples_); } /** \brief Function to set the times used for the Power Series endgame. */ template void SetRandVec(int size) { rand_vector_ = Vec::Random(size); } explicit PowerSeriesEndgame(TrackerType const& tr, const ConfigsAsTuple& settings) : BaseEGT(tr, settings), EndgamePrecPolicyBase(tr) {} template explicit PowerSeriesEndgame(TrackerType const& tr, const Ts&... ts) : PowerSeriesEndgame(tr, Configs::Unpermute(ts...)) {} /** \brief Computes an upper bound on the cycle number. Consult page 53 of \cite bertinibook. ## Input: None: all data needed are class data members. ## Output: upper_bound_on_cycle_number_: Used for an exhaustive search for the best cycle number for approimating the path to t = 0. ##Details: \tparam CT The complex number type. */ template unsigned ComputeBoundOnCycleNumber() { using RT = typename Eigen::NumTraits::Real; using std::abs; using std::log; const auto& samples = std::get>(samples_); AssertSizesTimeSpace(); auto num_samples = samples.size(); const Vec& sample0 = samples[num_samples - 3]; const Vec& sample1 = samples[num_samples - 2]; const Vec& sample2 = samples[num_samples - 1]; // most recent sample. oldest samples at // front of the container // should this only be if the system is homogenized? CT rand_sum1 = ((sample1 - sample0).transpose() * rand_vector_).sum(); CT rand_sum2 = ((sample2 - sample1).transpose() * rand_vector_).sum(); if (abs(rand_sum1) == 0 || abs(rand_sum2) == 0) // avoid division by 0 { upper_bound_on_cycle_number_ = 1; return upper_bound_on_cycle_number_; } RT estimate = log(static_cast(this->EndgameSettings().sample_factor)) / log(abs(rand_sum2 / rand_sum1)); if (estimate < 1) // would be nan if sample points are same as each other upper_bound_on_cycle_number_ = 1; else { using std::max; auto upper_bound = unsigned( round(estimate) * this->template Get().cycle_number_amplification); upper_bound_on_cycle_number_ = max(upper_bound, this->template Get().max_cycle_number); } return upper_bound_on_cycle_number_; } // end ComputeBoundOnCycleNumber /** \brief This function computes the cycle number using an exhaustive search up the upper bound computed by the above function BoundOnCyleNumber. ## Input: None: all data needed are class data members. ## Output: cycle_number_: Used to create a hermite interpolation to t = 0. ##Details: \tparam CT The complex number type. This is done by an exhaustive search from 1 to upper_bound_on_cycle_number. There is a conversion to the s-space from t-space in this function. As a by-product the derivatives at each of the samples is returned for further use. */ template unsigned ComputeCycleNumber(CT const& t0) { using RT = typename Eigen::NumTraits::Real; const auto& samples = std::get>(samples_); const auto& times = std::get>(times_); const auto& derivatives = std::get>(derivatives_); AssertSizesTimeSpaceDeriv(); const Vec& most_recent_sample = samples.back(); const CT& most_recent_time = times.back(); // Compute upper bound for cycle number. ComputeBoundOnCycleNumber(); unsigned num_pts; if (samples.size() > this->EndgameSettings().num_sample_points) num_pts = this->EndgameSettings().num_sample_points; else num_pts = this->EndgameSettings().num_sample_points - 1; auto min_found_difference = Eigen::NumTraits::highest(); TimeCont s_times(num_pts); SampCont s_derivatives(num_pts); auto offset = samples.size() - num_pts - 1; // -1 here to shift away from the back of the container for (unsigned int candidate = 1; candidate <= upper_bound_on_cycle_number_; ++candidate) { using std::pow; std::tie(s_times, s_derivatives) = TransformToSPlane(candidate, t0, num_pts, ContStart::Front); RT cand_power{1 / static_cast(candidate)}; RT curr_diff = (HermiteInterpolateAndSolve( pow((most_recent_time - t0) / (times[0] - t0), cand_power), // the target time num_pts, s_times, samples, s_derivatives, ContStart::Front) // the input data - most_recent_sample) .template lpNorm(); if (curr_diff < min_found_difference) { min_found_difference = curr_diff; this->cycle_number_ = candidate; } } // end cc loop over cycle number possibilities return this->cycle_number_; } // end ComputeCycleNumber /** \brief Compute a set of derivatives using internal data to the endgame. ## Input: None: all data needed are class data members. ## Output: None: Derivatives are members of this class. ##Details: \tparam CT The complex number type. */ template void ComputeAllDerivatives() { auto& samples = std::get>(samples_); auto& times = std::get>(times_); auto& derivatives = std::get>(derivatives_); assert((samples.size() == times.size()) && "must have same number of times and samples"); if (tracking::TrackerTraits::IsAdaptivePrec) // known at // compile time { auto max_precision = this->EnsureAtUniformPrecision(times, samples); this->GetSystem().precision(max_precision); } // Compute dx_dt for each sample. derivatives.clear(); derivatives.resize(samples.size()); for (unsigned ii = 0; ii < samples.size(); ++ii) { derivatives[ii] = -this->GetSystem() .Jacobian(samples[ii], times[ii]) .lu() .solve(this->GetSystem().TimeDerivative(samples[ii], times[ii])); } } /** \param c The cycle number you want to use This function transforms the times and derivatives into the S-plane, scaled by the cycle number. this function also transforms them into the interval [0 1] */ template std::tuple, SampCont> TransformToSPlane( int cycle_num, CT const& t0, unsigned num_pts, ContStart shift_from) { if (cycle_num == 0) throw std::runtime_error( "cannot transform to s plane with cycle number 0"); AssertSizesTimeSpaceDeriv(); using RT = typename Eigen::NumTraits::Real; const auto& times = std::get>(times_); const auto& derivatives = std::get>(derivatives_); RT c = static_cast(cycle_num); RT one_over_c = 1 / c; unsigned offset_t, offset_d; if (shift_from == ContStart::Back) { offset_t = times.size() - num_pts; offset_d = derivatives.size() - num_pts; } else offset_t = offset_d = 0; TimeCont s_times(num_pts); SampCont s_derivatives(num_pts); CT time_shift = times[offset_t] - t0; for (unsigned ii = 0; ii < num_pts; ++ii) { s_times[ii] = pow((times[ii + offset_t] - t0) / time_shift, one_over_c); s_derivatives[ii] = derivatives[ii + offset_d] * (c * pow(s_times[ii], cycle_num - 1)) * time_shift; } return std::make_tuple(s_times, s_derivatives); } /** \brief This function computes an approximation of the space value at the time time_t0. ## Input: result: Passed by reference this holds the value of the approximation we compute t0: This is the time value for which we wish to compute an approximation at. ## Output: SuccessCode: This reports back if we were successful in making an approximation. ##Details: \tparam CT The complex number type. This function handles computing an approximation at the origin. We compute the cycle number best for the approximation, and convert derivatives and times to the s-plane where s = t^(1/c). We use the converted times and derivatives along with the samples to do a Hermite interpolation. */ template SuccessCode ComputeApproximationOfXAtT0(Vec& result, const CT& t0) { const auto c = ComputeCycleNumber(t0); auto num_pts = this->EndgameSettings().num_sample_points; TimeCont s_times; SampCont s_derivatives; std::tie(s_times, s_derivatives) = TransformToSPlane(c, t0, num_pts, ContStart::Back); // the data was transformed to be on the interval [0 1] so we can hard-code // the time-to-solve as 0 here. Precision(result, Precision(s_derivatives.back())); result = HermiteInterpolateAndSolve(CT(0), num_pts, s_times, std::get>(samples_), s_derivatives, ContStart::Back); return SuccessCode::Success; } // end ComputeApproximationOfXAtT0 /** \brief The samples used in the power series endgame are collected by advancing time to t = 0, by multiplying the current time by the sample factor. ## Input: target_time: This is the time we are trying to approximate, default is t = 0. ## Output: SuccessCode: This reports back if we were successful in advancing time. ##Details: \tparam CT The complex number type. This function computes the next time value for the power series endgame. After computing this time value, it will track to it and compute the derivative at this time value for further appoximations to be made during the endgame. */ template SuccessCode AdvanceTime(const CT& target_time) { using RT = typename Eigen::NumTraits::Real; auto& samples = std::get>(samples_); auto& times = std::get>(times_); auto& derivatives = std::get>(derivatives_); AssertSizesTimeSpaceDeriv(); Vec next_sample; CT next_time = (times.back() + target_time) * static_cast(this->EndgameSettings() .sample_factor); // setting up next time value // using the midpoint formula, // sample_factor will give us some if (abs(next_time - target_time) < this->EndgameSettings() .min_track_time) // generalized for target_time not equal to 0. { NotifyObservers(MinTrackTimeReached(*this)); return SuccessCode::MinTrackTimeReached; } SuccessCode tracking_success = this->GetTracker().TrackPath( next_sample, times.back(), next_time, samples.back()); if (tracking_success != SuccessCode::Success) return tracking_success; NotifyObservers(InEGOperatingZone(*this)); this->EnsureAtPrecision(next_time, Precision(next_sample)); times.push_back(next_time); samples.push_back(next_sample); auto refine_success = this->RefineSample( samples.back(), next_sample, times.back(), this->FinalTolerance() * this->EndgameSettings().sample_point_refinement_factor, this->EndgameSettings().max_num_newton_iterations); if (refine_success != SuccessCode::Success) { NotifyObservers(RefiningFailed(*this)); return refine_success; } this->EnsureAtPrecision(times.back(), Precision(samples.back())); NotifyObservers(SampleRefined(*this)); // we keep one more samplepoint than needed around, for estimating the cycle // number if (times.size() > this->EndgameSettings().num_sample_points + 1) { times.pop_front(); samples.pop_front(); } return SuccessCode::Success; } /** \brief Primary function running the Power Series endgame. ## Input: start_time: This is the time value for which the endgame begins, by default this is t = 0.1 start_point: An approximate solution of the homotopy at t = start_time target_time: The time value that we are wishing to approximate to. This is default set to t = 0. ## Output: SuccessCode: This reports back if we were successful in advancing time. ##Details: \tparam CT The complex number type. Tracking forward with the number of sample points, this function will make approximations using Hermite interpolation. This process will continue until two consecutive approximations are withing final tolerance of each other. */ template SuccessCode RunImpl(const CT& start_time, const Vec& start_point, CT const& target_time) { if (start_point.size() != this->GetSystem().NumVariables()) { std::stringstream err_msg; err_msg << "number of variables in start point for PSEG, " << start_point.size() << ", must match the number of variables in the system, " << this->GetSystem().NumVariables(); throw std::runtime_error(err_msg.str()); } DefaultPrecision(Precision(start_point)); using RT = typename Eigen::NumTraits::Real; // Set up for the endgame. ClearTimesAndSamples(); // unpack some references for easy use auto& samples = std::get>(samples_); auto& times = std::get>(times_); auto& derivatives = std::get>(derivatives_); Vec& latest_approx = this->final_approximation_; Vec& prev_approx = this->previous_approximation_; // this is for estimating a ... norm? SetRandVec(start_point.size()); auto initial_sample_success = this->ComputeInitialSamples( start_time, target_time, start_point, times, samples); if (initial_sample_success != SuccessCode::Success) { NotifyObservers(EndgameFailure(*this)); return initial_sample_success; } this->template RefineAllSamples(samples, times); ComputeAllDerivatives(); auto extrapolation_code = ComputeApproximationOfXAtT0(prev_approx, target_time); latest_approx = prev_approx; if (extrapolation_code != SuccessCode::Success) return extrapolation_code; RT norm_of_dehom_of_latest_approx; RT norm_of_dehom_of_prev_approx; if (this->SecuritySettings().level <= 0) norm_of_dehom_of_prev_approx = this->GetSystem() .DehomogenizePoint(prev_approx) .template lpNorm(); NumErrorT& approx_error = this->approximate_error_; approx_error = 1; while (approx_error > this->FinalTolerance()) { auto advance_code = AdvanceTime(target_time); if (advance_code != SuccessCode::Success) { NotifyObservers(EndgameFailure(*this)); return advance_code; } // this code is what bertini1 does... it refines all samples, like, all // the time. this->template RefineAllSamples(samples, times); ComputeAllDerivatives(); extrapolation_code = ComputeApproximationOfXAtT0(latest_approx, target_time); if (extrapolation_code != SuccessCode::Success) { NotifyObservers(EndgameFailure(*this)); return extrapolation_code; } approx_error = static_cast( (latest_approx - prev_approx).template lpNorm()); NotifyObservers(ApproximatedRoot(*this)); if (this->SecuritySettings().level <= 0) { norm_of_dehom_of_latest_approx = this->GetSystem() .DehomogenizePoint(latest_approx) .template lpNorm(); if (norm_of_dehom_of_latest_approx > this->SecuritySettings().max_norm && norm_of_dehom_of_prev_approx > this->SecuritySettings().max_norm) { NotifyObservers(SecurityMaxNormReached(*this)); return SuccessCode::SecurityMaxNormReached; } norm_of_dehom_of_prev_approx = norm_of_dehom_of_latest_approx; } Precision(prev_approx, Precision(latest_approx)); prev_approx = latest_approx; } // end while NotifyObservers(Converged(*this)); return SuccessCode::Success; } // end PSEG virtual ~PowerSeriesEndgame() = default; }; // end powerseries class } // namespace endgame } // namespace bertini