Skip to content

Commit ada32c7

Browse files
author
Ilya Mandel
committed
Updated rotational velocity solver to use boost root finder
1 parent e721e90 commit ada32c7

File tree

5 files changed

+128
-80
lines changed

5 files changed

+128
-80
lines changed

src/BaseStar.cpp

Lines changed: 90 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,28 +3047,14 @@ double BaseStar::CalculateOStarRotationalVelocityAnalyticCDF_Static(const double
30473047

30483048
boost::math::inverse_gamma_distribution<> gammaComponent(alpha, beta); // (shape, scale) = (alpha, beta)
30493049
boost::math::normal_distribution<> normalComponent(mu, sigma);
3050+
3051+
// Compute CDF at zero rotational velocity -- the CDF should relative to this quantity
3052+
double CDFzero = (iGamma * boost::math::cdf(gammaComponent, 0.0)) + ((1.0 - iGamma) * boost::math::cdf(normalComponent, 0.0));
3053+
3054+
double CDFunnormalised = (iGamma * boost::math::cdf(gammaComponent, p_Ve)) + ((1.0 - iGamma) * boost::math::cdf(normalComponent, p_Ve));
3055+
3056+
return ((CDFunnormalised-CDFzero) / (1.0 - CDFzero));
30503057

3051-
return (iGamma * boost::math::cdf(gammaComponent, p_Ve)) + ((1.0 - iGamma) * boost::math::cdf(normalComponent, p_Ve));
3052-
}
3053-
3054-
3055-
/*
3056-
* Calculate the inverse of the analytic cumulative distribution function (CDF) for the
3057-
* equatorial rotational velocity of single O stars.
3058-
*
3059-
* (i.e. calculate the inverse of CalculateOStarRotationalVelocityAnalyticCDF_Static())
3060-
*
3061-
*
3062-
* double CalculateOStarRotationalVelocityAnalyticCDFInverse_Static(const double p_Ve, const void *p_Params)
3063-
*
3064-
* @param [IN] p_vE Rotational velocity (in km s^-1) - value of the kick vk which we want to find
3065-
* @param [IN] p_Params Pointer to RotationalVelocityParams structure containing y, the CDF draw U(0,1)
3066-
* @return Inverse CDF
3067-
* Should be zero when p_Ve = vk, the value of the kick to draw
3068-
*/
3069-
double BaseStar::CalculateOStarRotationalVelocityAnalyticCDFInverse_Static(double p_Ve, void* p_Params) {
3070-
RotationalVelocityParams* params = (RotationalVelocityParams*) p_Params;
3071-
return CalculateOStarRotationalVelocityAnalyticCDF_Static(p_Ve) - params->u;
30723058
}
30733059

30743060

@@ -3081,68 +3067,93 @@ double BaseStar::CalculateOStarRotationalVelocityAnalyticCDFInverse_Static(doubl
30813067
* Ramirez-Agudelo et al. 2013 https://arxiv.org/abs/1309.2929
30823068
*
30833069
*
3084-
* double CalculateOStarRotationalVelocity_Static(const double p_Xmin, const double p_Xmax)
3070+
* double CalculateOStarRotationalVelocity
30853071
*
3086-
* @param [IN] p_Xmin Minimum value for root
3087-
* @param [IN] p_Xmax Maximum value for root
30883072
* @return Rotational velocity in km s^-1
30893073
*/
3090-
double BaseStar::CalculateOStarRotationalVelocity_Static(const double p_Xmin, const double p_Xmax) {
3091-
3092-
double xMin = p_Xmin;
3093-
double xMax = p_Xmax;
3094-
3095-
double result = xMin;
3096-
3097-
double maximumInverse = CalculateOStarRotationalVelocityAnalyticCDF_Static(xMax);
3098-
double minimumInverse = CalculateOStarRotationalVelocityAnalyticCDF_Static(xMin);
3099-
3100-
double rand = RAND->Random();
3101-
3102-
while (utils::Compare(rand, maximumInverse) > 0) {
3103-
xMax *= 2.0;
3104-
maximumInverse = CalculateOStarRotationalVelocityAnalyticCDF_Static(xMax);
3105-
}
3106-
3107-
if (utils::Compare(rand, minimumInverse) >= 0) {
3108-
3109-
const gsl_root_fsolver_type *T;
3110-
gsl_root_fsolver *s;
3111-
gsl_function F;
3112-
3113-
RotationalVelocityParams params = {rand};
3114-
3115-
F.function = &CalculateOStarRotationalVelocityAnalyticCDFInverse_Static;
3116-
F.params = &params;
3117-
3118-
// gsl_root_fsolver_brent
3119-
// gsl_root_fsolver_bisection
3120-
T = gsl_root_fsolver_brent;
3121-
s = gsl_root_fsolver_alloc(T);
3122-
3123-
gsl_root_fsolver_set(s, &F, xMin, xMax);
3124-
3125-
int status = GSL_CONTINUE;
3126-
int iter = 0;
3127-
int maxIter = 100;
3128-
3129-
while (status == GSL_CONTINUE && iter < maxIter) {
3130-
iter++;
3131-
status = gsl_root_fsolver_iterate(s);
3132-
result = gsl_root_fsolver_root(s);
3133-
xMin = gsl_root_fsolver_x_lower(s);
3134-
xMax = gsl_root_fsolver_x_upper(s);
3135-
status = gsl_root_test_interval(xMin, xMax, 0, 0.001);
3074+
double BaseStar::CalculateOStarRotationalVelocity() {
3075+
3076+
double desiredCDF = RAND->Random(); // Random desired CDF
3077+
3078+
const boost::uintmax_t maxit = ADAPTIVE_RV_MAX_ITERATIONS; // Limit to maximum iterations.
3079+
boost::uintmax_t it = maxit; // Initially our chosen max iterations, but updated with actual.
3080+
3081+
// find root
3082+
// we use an iterative algorithm to find the root here:
3083+
// - if the root finder throws an exception, we stop and return a negative value for the root (indicating no root found)
3084+
// - if the root finder reaches the maximum number of (internal) iterations, we stop and return a negative value for the root (indicating no root found)
3085+
// - if the root finder returns a solution, we check that func(solution) = 0.0 +/ ROOT_ABS_TOLERANCE
3086+
// - if the solution is acceptable, we stop and return the solution
3087+
// - if the solution is not acceptable, we reduce the search step size and try again
3088+
// - if we reach the maximum number of search step reduction iterations, or the search step factor reduces to 1.0 (so search step size = 0.0),
3089+
// we stop and return a negative value for the root (indicating no root found)
3090+
3091+
double guess = 100.0; // guess at 100 km s^-1 (arbitrary initial guess)
3092+
3093+
double factorFrac = ADAPTIVE_RV_SEARCH_FACTOR_FRAC; // search step size factor fractional part
3094+
double factor = 1.0 + factorFrac; // factor to determine search step size (size = guess * factor)
3095+
3096+
std::pair<double, double> root(-1.0, -1.0); // initialise root - default return
3097+
std::size_t tries = 0; // number of tries
3098+
bool done = false; // finished (found root or exceed maximum tries)?
3099+
ERROR error = ERROR::NONE;
3100+
OStarRotationVelocityFunctor<double> func = OStarRotationVelocityFunctor<double>(desiredCDF);
3101+
while (!done) { // while no error and acceptable root found
3102+
3103+
bool isRising = true; //guess for direction of search; CDF increases monotonically
3104+
3105+
// run the root finder
3106+
// regardless of any exceptions or errors, display any problems as a warning, then
3107+
// check if the root returned is within tolerance - so even if the root finder
3108+
// bumped up against the maximum iterations, or couldn't bracket the root, use
3109+
// whatever value it ended with and check if it's good enough for us - not finding
3110+
// an acceptable root should be the exception rather than the rule, so this strategy
3111+
// shouldn't cause undue performance issues.
3112+
try {
3113+
error = ERROR::NONE;
3114+
root = boost::math::tools::bracket_and_solve_root(func, guess, factor, isRising, utils::BracketTolerance, it); // find root
3115+
// root finder returned without raising an exception
3116+
if (error != ERROR::NONE) { SHOW_WARN(error); } // root finder encountered an error
3117+
else if (it >= maxit) { SHOW_WARN(ERROR::TOO_MANY_RV_ITERATIONS); } // too many root finder iterations
3118+
}
3119+
catch(std::exception& e) { // catch generic boost root finding error
3120+
// root finder exception
3121+
// could be too many iterations, or unable to bracket root - it may not
3122+
// be a hard error - so no matter what the reason is that we are here,
3123+
// we'll just emit a warning and keep trying
3124+
if (it >= maxit) { SHOW_WARN(ERROR::TOO_MANY_RV_ITERATIONS); } // too many root finder iterations
3125+
else { SHOW_WARN(ERROR::ROOT_FINDER_FAILED, e.what()); } // some other problem - show it as a warning
31363126
}
31373127

3138-
// JR: should we issue a warning, or throw an error, if the root finder didn't actually find the roor here (i.e. we stopped because pf maxIter)?
3139-
// To be consistent, should we use the Boost root solver here?
3140-
// **Ilya** both questions above -- IM: yes to both, TBC
3141-
3142-
gsl_root_fsolver_free(s); // de-allocate memory for root solver
3128+
// we have a solution from the root finder - it may not be an acceptable solution
3129+
// so we check if it is within our preferred tolerance
3130+
if (fabs(func(root.first + (root.second - root.first) / 2.0)) <= ROOT_ABS_TOLERANCE) { // solution within tolerance?
3131+
done = true; // yes - we're done
3132+
}
3133+
else if (fabs(func(root.first)) <= ROOT_ABS_TOLERANCE) { // solution within tolerance at endpoint 1?
3134+
root.second=root.first;
3135+
done = true; // yes - we're done
3136+
}
3137+
else if (fabs(func(root.second)) <= ROOT_ABS_TOLERANCE) { // solution within tolerance at endpoint 2?
3138+
root.first=root.second;
3139+
done = true; // yes - we're done
3140+
}
3141+
else { // no - try again
3142+
// we don't have an acceptable solution - reduce search step size and try again
3143+
factorFrac /= 2.0; // reduce fractional part of factor
3144+
factor = 1.0 + factorFrac; // new search step size
3145+
tries++; // increment number of tries
3146+
if (tries > ADAPTIVE_RV_MAX_TRIES || fabs(factor - 1.0) <= ROOT_ABS_TOLERANCE) { // too many tries, or step size 0.0?
3147+
// we've tried as much as we can - fail here with -ve return value
3148+
root.first = -1.0; // yes - set error return
3149+
root.second = -1.0;
3150+
SHOW_WARN(ERROR::TOO_MANY_RV_TRIES); // show warning
3151+
done = true; // we're done
3152+
}
3153+
}
31433154
}
3144-
3145-
return result;
3155+
3156+
return root.first + (root.second - root.first) / 2.0; // Midway between brackets is our result, if necessary we could return the result as an interval here.
31463157
}
31473158

31483159

@@ -3184,7 +3195,8 @@ double BaseStar::CalculateRotationalVelocity(double p_MZAMS) {
31843195
// For lower mass stars, default back to Hurley et al. 2000 distribution for now
31853196

31863197
if (utils::Compare(p_MZAMS, 16.0) >= 0) {
3187-
vRot = CalculateOStarRotationalVelocity_Static(0.0, 800.0);
3198+
vRot = CalculateOStarRotationalVelocity();
3199+
vRot = max(vRot, 0.0); // Set to no rotation if no positive solution found; warning already raised
31883200
}
31893201
else if (utils::Compare(p_MZAMS, 2.0) >= 0) {
31903202
vRot = utils::InverseSampleFromTabulatedCDF(RAND->Random(), BStarRotationalVelocityCDFTable);

src/BaseStar.h

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,8 +627,7 @@ class BaseStar {
627627
static double CalculateOpacity_Static(const double p_HeliumAbundanceSurface);
628628

629629
static double CalculateOStarRotationalVelocityAnalyticCDF_Static(const double p_Ve);
630-
static double CalculateOStarRotationalVelocityAnalyticCDFInverse_Static(double p_Ve, void *p_Params);
631-
static double CalculateOStarRotationalVelocity_Static(const double p_Xmin, const double p_Xmax);
630+
double CalculateOStarRotationalVelocity();
632631

633632
double CalculatePerturbationB(const double p_Mass) const;
634633
double CalculatePerturbationC(double p_Mass) const;
@@ -743,6 +742,34 @@ class BaseStar {
743742

744743
void UpdateAttributesAndAgeOneTimestepPreamble(const double p_DeltaMass, const double p_DeltaMass0, const double p_DeltaTime);
745744

745+
746+
/*
747+
* Functor for CalculateOStarRotationalVelocity()
748+
*
749+
*
750+
* Constructor: initialise the class
751+
* template <class T> OStarRotationVelocityFunctor(double p_CDF, ERROR *p_Error)
752+
*
753+
* @param [IN] p_CDF Desired CDF value
754+
*
755+
* Function: calculate the CDF of the O star rotational velocity and compare to desired value
756+
* T OStarRotationVelocityFunctor(double const& p_Ve)
757+
*
758+
* @param [IN] p_Ve Rotational velocity, km s^-1
759+
* @return Difference between star's Roche Lobe radius and radius after mass loss
760+
*/
761+
template <class T>
762+
struct OStarRotationVelocityFunctor {
763+
OStarRotationVelocityFunctor(double p_CDF) {
764+
m_CDF = p_CDF;
765+
}
766+
T operator()(double const& p_Ve) {
767+
768+
return (CalculateOStarRotationalVelocityAnalyticCDF_Static(p_Ve) - m_CDF);
769+
}
770+
private:
771+
double m_CDF;
772+
};
746773
};
747774

748775
#endif // __BaseStar_h__

src/ErrorCatalog.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ enum class ERROR: int {
133133
TOO_MANY_RETRIES, // generic too many retries
134134
TOO_MANY_RLOF_ITERATIONS, // too many iterations in RLOF root finder
135135
TOO_MANY_RLOF_TRIES, // too many tries in RLOF root finder
136+
TOO_MANY_RV_ITERATIONS, // too many iterations in rotational velocity root finder
137+
TOO_MANY_RV_TRIES, // too many tries in rotational velocity root finder
136138
TOO_MANY_TIMESTEPS_IN_TIMESTEPS_FILE, // too many timesteps in timesteps file (exceeds maximum)
137139
UNABLE_TO_CREATE_DIRECTORY, // unable to create directory
138140
UNABLE_TO_REMOVE_DIRECTORY, // unable to remove directory
@@ -310,6 +312,8 @@ const COMPASUnorderedMap<ERROR, std::tuple<ERROR_SCOPE, std::string>> ERROR_CATA
310312
{ ERROR::TOO_MANY_RETRIES, { ERROR_SCOPE::ALWAYS, "Too many retries" }},
311313
{ ERROR::TOO_MANY_RLOF_ITERATIONS, { ERROR_SCOPE::ALWAYS, "Reached maximum number of iterations when fitting star inside Roche Lobe in RLOF" }},
312314
{ ERROR::TOO_MANY_RLOF_TRIES, { ERROR_SCOPE::ALWAYS, "Reached maximum number of tries when fitting star inside Roche Lobe in RLOF" }},
315+
{ ERROR::TOO_MANY_RV_ITERATIONS, { ERROR_SCOPE::ALWAYS, "Reached maximum number of tries when inverting the CDF of rotational velocitiesF" }},
316+
{ ERROR::TOO_MANY_RV_TRIES, { ERROR_SCOPE::ALWAYS, "Reached maximum number of tries when inverting the CDF of rotational velocities" }},
313317
{ ERROR::TOO_MANY_TIMESTEPS_IN_TIMESTEPS_FILE, { ERROR_SCOPE::ALWAYS, "Number of timesteps in timestpes file exceeds maximum timesteps" }},
314318
{ ERROR::UNABLE_TO_CREATE_DIRECTORY, { ERROR_SCOPE::ALWAYS, "Unable to create directory" }},
315319
{ ERROR::UNABLE_TO_REMOVE_DIRECTORY, { ERROR_SCOPE::ALWAYS, "Unable to remove directory" }},

src/changelog.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,7 @@
16721672
// 03.26.02 IM - October 26, 2025 - Enhancements
16731673
// - Added option -USSN-kicks-overwrite-mandel-muller ; if set to true, use user-defined USSN kicks (as a fixed value) in lieu of the Mandel & Muller kick prescription for USSNe
16741674
// - Replaced --scale-CHE-mass-loss-with-surface-helium-abundance with the more general --scale-mass-loss-with-surface-helium-abundance
1675+
// - Updated rotational velocity solver to use boost root finder
16751676
//
16761677
// Version string format is MM.mm.rr, where
16771678
//

src/constants.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ constexpr int ADAPTIVE_RLOF_MAX_TRIES = 30;
279279
constexpr int ADAPTIVE_RLOF_MAX_ITERATIONS = 50; // Maximum number of root finder iterations in BaseBinaryStar::MassLossToFitInsideRocheLobe()
280280
constexpr double ADAPTIVE_RLOF_SEARCH_FACTOR_FRAC = 1.0; // Search size factor (fractional part) in BaseBinaryStar::MassLossToFitInsideRocheLobe() (added to 1.0)
281281

282+
constexpr int ADAPTIVE_RV_MAX_TRIES = 30; // Maximum number of tries in BaseStar::CalculateOStarRotationalVelocity_Static()
283+
constexpr int ADAPTIVE_RV_MAX_ITERATIONS = 50; // Maximum number of root finder iterations in BaseStar::CalculateOStarRotationalVelocity_Static()
284+
constexpr double ADAPTIVE_RV_SEARCH_FACTOR_FRAC = 1.0; // Search size factor (fractional part) in BaseStar::CalculateOStarRotationalVelocity_Static() (added to 1.0)
285+
282286
constexpr int ADAPTIVE_MASS0_MAX_TRIES = 30; // Maximum number of tries in HG::Mass0ToMatchDesiredCoreMass()
283287
constexpr int ADAPTIVE_MASS0_MAX_ITERATIONS = 50; // Maximum number of iterations in HG::Mass0ToMatchDesiredCoreMass()
284288
constexpr double ADAPTIVE_MASS0_SEARCH_FACTOR_FRAC = 1.0; // Search size factor (fractional part) in HG::Mass0ToMatchDesiredCoreMass() (added to 1.0)

0 commit comments

Comments
 (0)