// This library is part of PLINK 2.0, copyright (C) 2005-2025 Shaun Purcell,
// Christopher Chang.
//
// This library is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by the
// Free Software Foundation; either version 3 of the License, or (at your
// option) any later version.
//
// This library 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 Lesser General Public License
// for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library.  If not, see <http://www.gnu.org/licenses/>.

#include "plink2_stats.h"

#ifdef __cplusplus
namespace plink2 {
#endif

// Thread-unsafe portions of plink_stats.c have been replaced, mostly by code
// derived from boost/math/special_functions/gamma.hpp and
// boost/math/special_functions/detail/igamma_inverse.hpp in Boost 1.60 and
// 1.84 (Maddock et al.).  The derived portions are subject to the following
// license:
//
// *****
// Boost Software License - Version 1.0 - August 17th, 2003
//
// Permission is hereby granted, free of charge, to any person or organization
// obtaining a copy of the software and accompanying documentation covered by
// this license (the "Software") to use, reproduce, display, distribute,
// execute, and transmit the Software, and to prepare derivative works of the
// Software, and to permit third-parties to whom the Software is furnished to
// do so, all subject to the following:
//
// The copyright notices in the Software and this entire statement, including
// the above license grant, this restriction and the following disclaimer,
// must be included in all copies of the Software, in whole or in part, and
// all derivative works of the Software, unless such copies or derivative
// works are solely in the form of machine-executable object code generated by
// a source language processor.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// *****

// ***** thread-safe ChisqToP *****
// port of Boost 1.60 implementation, float precision

static const double kLogMinValue = -708.0;
static const double kLogMaxValue = 709.0;

static const double kLentzFpmin = 1.0e-30;

static const double kFactorialRecips[34] = {
  1.0,
  1.0,
  0.5,
  0.16666666666666666,
  0.041666666666666664,
  0.008333333333333333,
  0.001388888888888889,
  0.0001984126984126984,
  2.48015873015873e-05,
  2.7557319223985893e-06,
  2.755731922398589e-07,
  2.505210838544172e-08,
  2.08767569878681e-09,
  1.6059043836821613e-10,
  1.1470745597729725e-11,
  7.647163731819816e-13,
  4.779477332387385e-14,
  2.8114572543455206e-15,
  1.5619206968586225e-16,
  8.22063524662433e-18,
  4.110317623312165e-19,
  1.9572941063391263e-20,
  8.896791392450574e-22,
  3.8681701706306835e-23,
  1.6117375710961184e-24,
  6.446950284384474e-26,
  2.4795962632247972e-27,
  9.183689863795546e-29,
  3.279889237069838e-30,
  1.1309962886447718e-31,
  3.7699876288159054e-33,
  1.2161250415535181e-34,
  3.800390754854744e-36,
  1.151633562077195e-37
};

// this may move to a more central location
static const double kSmallRecips[30] = {
  0.0,  // could make this nan, though that's annoying for C++03
  1.0 / 1,
  1.0 / 2,
  1.0 / 3,
  1.0 / 4,
  1.0 / 5,
  1.0 / 6,
  1.0 / 7,
  1.0 / 8,
  1.0 / 9,
  1.0 / 10,
  1.0 / 11,
  1.0 / 12,
  1.0 / 13,
  1.0 / 14,
  1.0 / 15,
  1.0 / 16,
  1.0 / 17,
  1.0 / 18,
  1.0 / 19,
  1.0 / 20,
  1.0 / 21,
  1.0 / 22,
  1.0 / 23,
  1.0 / 24,
  1.0 / 25,
  1.0 / 26,
  1.0 / 27,
  1.0 / 28,
  1.0 / 29
};

double finite_gamma_q(uint32_t aa, double xx, double* p_derivative) {
  // a is a positive integer < 30; max(0.6, a-1) < x < kLogMaxValue
  // (e^{-x})(1 + x + x^2/2 + x^3/3! + x^4/4! + ... + x^{a-1}/(a-1)!)
  const double ee = exp(-xx);
  if (ee == 0.0) {
    return 0;
  }
  double sum = ee;
  double term = sum;
  for (uint32_t nn = 1; nn != aa; ++nn) {
    term *= xx * kSmallRecips[nn];
    sum += term;
  }
  if (p_derivative) {
    *p_derivative = ee * pow(xx, u31tod(aa)) * kFactorialRecips[aa - 1];
  }
  return sum;
}

static const double kSqrtPi = 1.7724538509055159;

double lower_gamma_series(double aa, double zz, double init_value) {
  // evaluate init_value + 1 + (z/(a+1)) + (z^2 / ((a+1)(a+2))) +
  //   (z^3 / ((a+1)(a+2)(a+3))) + ...
  // z shouldn't be larger than (a+170), otherwise overflow is possible
  double result = 1;
  double total = init_value + 1;
  do {
    aa += 1.0;
    result *= zz / aa;
    total += result;
  } while (fabs(result) > (kBigEpsilon * kBigEpsilon));
  return total;
}

// ~6 digits of precision is appropriate for p-value computations
static const double kContinuedFractionEpsilon = 3.0e-7;

double upper_gamma_fraction(double a1, double z1) {
  // No overflow issues, this tends to be close to 1.

  // evaluate a_1 / (b_1 + (a_2 / (b_2 + (a_3 / (b_3 + ...)))))
  // see Boost continued_fraction_a(), upper_incomplete_gamma_fract
  double cur_b = z1 - a1 + 3;

  double hh = cur_b;
  const double a0 = a1 - 1.0;
  if (fabs(hh) < kLentzFpmin) {
    hh = kLentzFpmin;
  }
  double cc = hh;
  double dd = 0.0;
  for (double kk = 2.0; kk <= 100.0; kk += 1.0) {
    const double cur_a = kk * (a1 - kk);
    cur_b += 2.0;
    dd = cur_b + cur_a * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = cur_b + cur_a / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    const double delta = cc * dd;
    hh *= delta;
    if (fabs(delta - 1.0) < kContinuedFractionEpsilon) {
      break;
    }
  }
  // const double cont_frac = a0 / hh;
  // return 1 / (z1 - a1 + 1 + cont_frac);
  return hh / (hh * (z1 - a1 + 1) + a0);
}

// from Numerical Recipes in Fortran 77: The Art of Scientific Computing, via
// Wikipedia
// maximal error of 1.2e-7
double erfc_fast(double zz) {
  const double tt = 1.0 / (1.0 + 0.5 * zz);
  const double tau = tt * exp(((((((((0.17087277 * tt - 0.82215223) * tt + 1.48851587) * tt - 1.13520398) * tt + 0.27886807) * tt - 0.18628806) * tt + 0.09678418) * tt + 0.37409196) * tt + 1.00002368) * tt - 1.26551223 - zz * zz);
  return tau;
}

static const double kSmallHalfRecips[30] = {
  1.0 / 0.5,
  1.0 / 1.5,
  1.0 / 2.5,
  1.0 / 3.5,
  1.0 / 4.5,
  1.0 / 5.5,
  1.0 / 6.5,
  1.0 / 7.5,
  1.0 / 8.5,
  1.0 / 9.5,
  1.0 / 10.5,
  1.0 / 11.5,
  1.0 / 12.5,
  1.0 / 13.5,
  1.0 / 14.5,
  1.0 / 15.5,
  1.0 / 16.5,
  1.0 / 17.5,
  1.0 / 18.5,
  1.0 / 19.5,
  1.0 / 20.5,
  1.0 / 21.5,
  1.0 / 22.5,
  1.0 / 23.5,
  1.0 / 24.5,
  1.0 / 25.5,
  1.0 / 26.5,
  1.0 / 27.5,
  1.0 / 28.5,
  1.0 / 29.5
};

// This can underflow.  Use finite_half_gamma_q2_ln when that's a problem.
double finite_half_gamma_q2(uint32_t a_minus_half, double xx, double* p_derivative) {
  // a is in {0.5, 1.5, ..., 29.5}; max(0.2, a-1) < x < kLogMaxValue
  const double sqrt_x = sqrt(xx);
  double ee = erfc_fast(sqrt_x);
  if ((ee != 0) && a_minus_half) {
    double term = sqrt_x * exp(-xx) * (2.0 / kSqrtPi);
    double sum = term;
    for (uint32_t n_minus_half = 1; n_minus_half != a_minus_half; ++n_minus_half) {
      term *= xx * kSmallHalfRecips[n_minus_half];
      sum += term;
    }
    ee += sum;
    if (p_derivative) {
      *p_derivative = 0;
    }
  } else if (p_derivative) {
    *p_derivative = sqrt_x * exp(-xx) * (1.0 / kSqrtPi);
  }
  return ee;
}

static const double kLanczosFloatSumNumer[6] = {8706.3495925490091, 8523.650341121874, 3338.0292194764235, 653.64249942940087, 63.999518449381870, 2.5066282746310063};
static const double kLanczosFloatSumDenom[6] = {0, 24, 50, 35, 10, 1};
static const double kLanczosFloatSumExpgNumer[6] = {32.812445410297834, 32.123889414443320, 12.580347294552161, 2.4634444783532414, 0.2412010548258800, 0.0094469677045392};

// this depends on the polynomial coefficients above
static const double kLanczosFloatG = 5.581;

double lanczos_sum_f(double zz) {
  double s1;
  double s2;
  if (zz <= 1) {
    s1 = kLanczosFloatSumNumer[5];
    s2 = kLanczosFloatSumDenom[5];
    for (int32_t ii = 4; ii >= 0; --ii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosFloatSumNumer[S_CAST(uint32_t, ii)];
      s2 += kLanczosFloatSumDenom[S_CAST(uint32_t, ii)];
    }
  } else {
    zz = 1 / zz;
    s1 = kLanczosFloatSumNumer[0];
    s2 = kLanczosFloatSumDenom[0];
    for (uint32_t uii = 1; uii != 6; ++uii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosFloatSumNumer[uii];
      s2 += kLanczosFloatSumDenom[uii];
    }
  }
  return s1 / s2;
}

double lanczos_sum_f_expg_scaled_imp(double zz, double* s2_ptr) {
  double s1;
  double s2;
  if (zz <= 1) {
    s1 = kLanczosFloatSumExpgNumer[5];
    s2 = kLanczosFloatSumDenom[5];
    for (int32_t ii = 4; ii >= 0; --ii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosFloatSumExpgNumer[S_CAST(uint32_t, ii)];
      s2 += kLanczosFloatSumDenom[S_CAST(uint32_t, ii)];
    }
  } else {
    zz = 1 / zz;
    s1 = kLanczosFloatSumExpgNumer[0];
    s2 = kLanczosFloatSumDenom[0];
    for (uint32_t uii = 1; uii != 6; ++uii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosFloatSumExpgNumer[uii];
      s2 += kLanczosFloatSumDenom[uii];
    }
  }
  *s2_ptr = s2;
  return s1;
}

double lanczos_sum_f_expg_scaled(double zz) {
  double s2;
  double s1 = lanczos_sum_f_expg_scaled_imp(zz, &s2);
  return s1 / s2;
}

double lanczos_sum_f_expg_scaled_recip(double zz) {
  double s2;
  double s1 = lanczos_sum_f_expg_scaled_imp(zz, &s2);
  return s2 / s1;
}

// We want better-than-float precision for our log-factorial function.
static const double kLanczosDoubleSumDenom[13] = {0, 39916800, 120543840, 150917976, 105258076, 45995730, 13339535, 2637558, 357423, 32670, 1925, 66, 1};
static const double kLanczosDoubleSumExpgNumer[13] = {56906521.913471564, 103794043.11634455, 86363131.288138591, 43338889.324676138, 14605578.087685068, 3481712.1549806459, 601859.61716810988, 75999.293040145426, 6955.9996025153761, 449.94455690631681, 19.519927882476175, 0.50984166556566762, 0.0060618423462489065};

// this depends on the polynomial coefficients above
static const double kLanczosDoubleG = 6.0246800407767296;

double lanczos_sum_d_expg_scaled_imp(double zz, double* s2_ptr) {
  double s1;
  double s2;
  // Only called by Lfact() for now, where zz is guaranteed to be > 1.
  /*
  if (zz <= 1) {
    s1 = kLanczosDoubleSumExpgNumer[12];
    s2 = kLanczosDoubleSumDenom[12];
    for (int32_t ii = 11; ii >= 0; --ii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosDoubleSumExpgNumer[S_CAST(uint32_t, ii)];
      s2 += kLanczosDoubleSumDenom[S_CAST(uint32_t, ii)];
    }
  } else {
  */
  zz = 1 / zz;
  s1 = kLanczosDoubleSumExpgNumer[0];
  s2 = kLanczosDoubleSumDenom[0];
  for (uint32_t uii = 1; uii != 13; ++uii) {
    s1 *= zz;
    s2 *= zz;
    s1 += kLanczosDoubleSumExpgNumer[uii];
    s2 += kLanczosDoubleSumDenom[uii];
  }
  // }
  *s2_ptr = s2;
  return s1;
}

double lanczos_sum_d_expg_scaled(double zz) {
  double s2;
  double s1 = lanczos_sum_d_expg_scaled_imp(zz, &s2);
  return s1 / s2;
}


double log1pmx(double xx) {
  // log(1+x) - x
  // assumes abs(xx) < 0.95
  const double aa = fabs(xx);
  if (aa < (kBigEpsilon / kSqrt2)) { // 2^{-21.5}
    return -xx * xx * 0.5;
  }
  double kk = 1.0;  // skip first term of usual log(1+x) series
  const double m_mult = -xx;
  double m_prod = xx;
  double total = 0.0;
  double rr;
  do {
    m_prod *= m_mult;
    kk += 1.0;
    rr = m_prod / kk;
    total += rr;
    // todo: tune these epsilons, but let's wait until we know all of the
    // callers of these functions
  } while (fabs(rr) > (kBigEpsilon * kBigEpsilon));
  return total;
}

// compute (z^a)(e^{-z})/tgamma(a)
double regularized_gamma_prefix(double aa, double zz) {
  // assumes a == 0.5 if a < 1.  assumes z > 0.
  // we are fine with float-level precision, so lanczos_n=6,
  // kLanczosFloatG=5.581
  if (aa < 1) {
    return sqrt(zz) * exp(-zz) * (1.0 / kSqrtPi);
  }
  const double agh = aa + kLanczosFloatG - 0.5;
  const double agh_recip = 1.0 / agh;
  const double dd = ((zz - aa) - (kLanczosFloatG - 0.5)) * agh_recip;
  double prefix;
  if ((fabs(dd * dd * aa) <= 100) && (aa > 150)) {
    // abs(dd) < sqrt(2/3) < 0.95
    prefix = aa * log1pmx(dd) + zz * (0.5 - kLanczosFloatG) * agh_recip;
    prefix = exp(prefix);
  } else {
    const double alz = aa * log(zz * agh_recip);
    const double amz = aa - zz;
    const double cur_minv = MINV(alz, amz);
    if ((cur_minv <= kLogMinValue) || (MAXV(alz, amz) >= kLogMaxValue)) {
      const double amza = amz / aa;
      double sq;
      if ((cur_minv > 2 * kLogMinValue) && (MAXV(alz, amz) < 2 * kLogMaxValue)) {
        // need to structure this to avoid overflow
        sq = pow(zz * agh_recip, aa * 0.5) * exp(amz * 0.5);
        prefix = sq * sq;
      } else if ((cur_minv > 4 * kLogMinValue) && (MAXV(alz, amz) < 4 * kLogMaxValue) && (zz > aa)) {
        sq = pow(zz * agh_recip, aa * 0.25) * exp(amz * 0.25);
        prefix = sq * sq;
        prefix *= prefix;
      } else if ((amza > kLogMinValue) && (amza < kLogMaxValue)) {
        prefix = pow((zz * exp(amza)) * agh_recip, aa);
      } else {
        prefix = exp(alz + amz);
      }
    } else {
      prefix = pow(zz * agh_recip, aa) * exp(amz);
    }
  }
  prefix *= sqrt(agh * kRecipE) * lanczos_sum_f_expg_scaled_recip(aa);
  return prefix;
}

static const double kTemmeC0[7] = {-0.333333333, 0.0833333333, -0.0148148148, 0.00115740741, 0.000352733686, -0.000178755144, 0.391926318e-4};
static const double kTemmeC1[5] = {-0.00185185185, -0.00347222222, 0.00264550265, -0.000990226337, 0.000205761317};
static const double kTemmeC2[3] = {0.00413359788, -0.00268132716, 0.000771604938};

double igamma_temme_large(double aa, double xx) {
  // 24-bit precision is fine
  const double sigma = (xx - aa) / aa;
  // abs(sigma) < 0.4
  const double phi = -log1pmx(sigma);
  const double sqrt_a = sqrt(aa);
  const double sqrt_phi = sqrt(phi);
  const double yy = aa * phi;
  double zz = kSqrt2 * sqrt_phi;
  if (xx < aa) {
    zz = -zz;
  }
  double workspace[3];
  workspace[0] = (((((kTemmeC0[6] * zz + kTemmeC0[5]) * zz + kTemmeC0[4]) * zz + kTemmeC0[3]) * zz + kTemmeC0[2]) * zz + kTemmeC0[1]) * zz + kTemmeC0[0];
  workspace[1] = (((kTemmeC1[4] * zz + kTemmeC1[3]) * zz + kTemmeC1[2]) * zz + kTemmeC1[1]) * zz + kTemmeC1[0];
  workspace[2] = (kTemmeC2[2] * zz + kTemmeC2[1]) * zz + kTemmeC2[0];
  const double a_recip = 1 / aa;
  double result = (workspace[2] * a_recip + workspace[1]) * a_recip + workspace[0];
  result *= exp(-yy) / ((kSqrt2 * kSqrtPi) * sqrt_a);
  if (xx < aa) {
    result = -result;
  }
  result += erfc_fast(sqrt_a * sqrt_phi) * 0.5;
  return result;
}

double gamma_incomplete_imp2(uint32_t df, double xx, uint32_t invert, double* p_derivative) {
  // normalized = true
  assert(df);
  assert(xx >= 0.0);
  const double aa = u31tod(df) * 0.5;
  const uint32_t is_small_a = (df < 60) && (aa <= xx + 1) && (xx < kLogMaxValue);
  uint32_t is_int = 0;
  uint32_t is_half_int = 0;
  if (is_small_a) {
    is_half_int = df % 2;
    is_int = !is_half_int;
  }
  uint32_t eval_method;
  if (is_int && (xx > 0.6)) {
    invert = !invert;
    eval_method = 0;
  } else if (is_half_int && (xx > 0.2)) {
    invert = !invert;
    eval_method = 1;
  } else if (xx < kSmallEpsilon) {
    // avoid computing log(0)
    // don't need more precision here, 6 digits is enough
    // invert always == 1
    assert(!p_derivative);
    return 1.0;
  } else if (xx < 1.1) {
    // (simplified from original Boost code)
    eval_method = 2;
  } else {
    const double x_minus_a = xx - aa;
    uint32_t use_temme = 0;
    if (aa > 20) {
      // sigma = abs((x - a) / a);
      // igamma_temme_large() assumes abs(sigma) < 0.95
      if (aa > 200) {
        // abs(sigma) < sqrt(20 / a) < 0.316...
        use_temme = (20 * aa > x_minus_a * x_minus_a);
      } else {
        // abs(sigma) < 0.4
        const double sigma_times_a = fabs(x_minus_a);
        use_temme = (sigma_times_a < 0.4 * aa);
      }
    }
    if (use_temme) {
      eval_method = 5;
    } else {
      // x - (1 / (3 * x)) < a
      // x * x - (1/3) < a * x
      // x * x - a * x < 1/3
      // x * (x - a) < 1/3
      if (xx * x_minus_a < (1.0 / 3.0)) {
        eval_method = 2;
      } else {
        eval_method = 4;
        invert = !invert;
      }
    }
  }
  double result;
  switch(eval_method) {
  case 0:
    result = finite_gamma_q(df / 2, xx, p_derivative);
    break;
  case 1:
    // previously used erfc, but that was up to ~3x as slow as dcdflib (e.g.
    // ChisqToP(2.706, 1) case).
    result = finite_half_gamma_q2(df / 2, xx, p_derivative);
    if (p_derivative && (*p_derivative == 0)) {
      *p_derivative = regularized_gamma_prefix(aa, xx);
    }
    break;
  case 2:
    result = regularized_gamma_prefix(aa, xx);
    if (p_derivative) {
      *p_derivative = result;
    }
    if (result != 0) {
      // uint32_t optimized_invert = 0;
      double init_value = 0;
      if (invert) {
        init_value = -aa / result;
        // optimized_invert = 1;
      }
      result *= lower_gamma_series(aa, xx, init_value) / aa;
      // if (optimized_invert) {
      if (invert) {
        invert = 0;
        result = -result;
      }
    }
    break;
    // case 3:
    // only when df=1 and 0.449 < x < 1.1, so no overflow/underflow issues.
    // ...wait a sec, we never reach here at all since eval_method is always
    // 1 there.
  case 4:
    result = regularized_gamma_prefix(aa, xx);
    if (p_derivative) {
      *p_derivative = result;
    }
    if (result != 0) {
      result *= upper_gamma_fraction(aa, xx);
    }
    break;
  case 5:
    result = igamma_temme_large(aa, xx);
    if (xx >= aa) {
      invert = !invert;
    }
    if (p_derivative) {
      *p_derivative = regularized_gamma_prefix(aa, xx);
    }
  }
  if (result > 1) {
    result = 1;
  }
  if (invert) {
    result = 1 - result;
  }
  if (p_derivative) {
    if ((xx < 1) && (DBL_MAX * xx < (*p_derivative))) {
      *p_derivative = DBL_MAX / 2;  // overflow; do we really need this?
    } else {
      *p_derivative /= xx;
    }
  }
  return result;
}

double ChisqToP(double chisq, uint32_t df) {
  // todo: figure out when we were depending on this to return -9, and decide
  // how to handle those situations now
  return gamma_incomplete_imp2(df, chisq * 0.5, 1, nullptr);
}

// ***** end thread-safe ChisqToP *****


// ***** ChisqToLnP *****

double finite_gamma_q_ln(uint32_t aa, double xx) {
  // a is a positive integer < 30; max(0.6, a-1) < x < 1e10
  // (e^{-x})(1 + x + x^2/2 + x^3/3! + x^4/4! + ... + x^{a-1}/(a-1)!)
  //
  // logarithm:
  // log(1 + x + ... + x^{a-1}/(a-1)!) - x
  // no overflow or underflow danger for main term thanks to bounds
  double sum = 1.0;
  double term = 1.0;
  for (uint32_t nn = 1; nn != aa; ++nn) {
    // division is slow enough that the lookup table speeds up this function by
    // >3x
    term *= xx * kSmallRecips[nn];
    sum += term;
  }
  return log(sum) - xx;
}

double erfc_fast2(double zz, double* tau_ln_plus_z2_ptr) {
  const double tt = 1.0 / (1.0 + 0.5 * zz);
  *tau_ln_plus_z2_ptr = ((((((((0.17087277 * tt - 0.82215223) * tt + 1.48851587) * tt - 1.13520398) * tt + 0.27886807) * tt - 0.18628806) * tt + 0.09678418) * tt + 0.37409196) * tt + 1.00002368) * tt - 1.26551223;
  return tt;
}

double finite_half_gamma_q2_ln(uint32_t a_minus_half, double xx) {
  // a is in {0.5, 1.5, ..., 29.5}; max(0.2, a-1) < x < 1e10
  const double sqrt_x = sqrt(xx);
  double tau_ln_plus_x;
  double tt = erfc_fast2(sqrt_x, &tau_ln_plus_x);
  if (!a_minus_half) {
    return log(tt) + tau_ln_plus_x - xx;
  }
  // pre-multiply by e^x to avoid underflow
  double term = sqrt_x * (2.0 / kSqrtPi);
  double sum = term;
  for (uint32_t n_minus_half = 1; n_minus_half != a_minus_half; ++n_minus_half) {
    term *= xx * kSmallHalfRecips[n_minus_half];
    sum += term;
  }
  // tau_ln + xx guaranteed to be small
  double ee = tt * exp(tau_ln_plus_x) + sum;
  return log(ee) - xx;
}

static const double kLnSqrtPi = 0.5723649429247001;

// compute -log((z^a)(e^{-z})/tgamma(a))
double regularized_gamma_prefix_ln(double aa, double zz) {
  // assumes a == 0.5 if a < 1.  assumes z > 0.
  // we are fine with float-level precision, so lanczos_n=6,
  // kLanczosFloatG=5.581
  if (aa < 1) {
    return -zz + 0.5 * log(zz) - kLnSqrtPi;
  }
  const double agh = aa + kLanczosFloatG - 0.5;
  const double agh_recip = 1.0 / agh;
  const double dd = ((zz - aa) - (kLanczosFloatG - 0.5)) * agh_recip;
  double prefix_ln;
  if ((fabs(dd * dd * aa) <= 100) && (aa > 150)) {
    // abs(dd) < sqrt(2/3) < 0.95
    prefix_ln = aa * log1pmx(dd) + zz * (0.5 - kLanczosFloatG) * agh_recip;
  } else {
    prefix_ln = (aa - zz) + aa * log(zz * agh_recip);
  }
  const double scaled_recip = lanczos_sum_f_expg_scaled_recip(aa);
  return prefix_ln + 0.5 * log(agh * kRecipE * scaled_recip * scaled_recip);
}

// does not guarantee return value <= 0 for now; caller must do that.
double gamma_incomplete_imp2_ln(uint32_t df, double xx) {
  // normalized = true, invert = false
  assert(df);
  assert(xx >= 0.0);
  const double aa = u31tod(df) * 0.5;
  const uint32_t is_small_a = (df < 60) && (aa <= xx + 1) && (xx < 1e10);
  uint32_t is_int = 0;
  uint32_t is_half_int = 0;
  if (is_small_a) {
    is_half_int = df % 2;
    is_int = !is_half_int;
  }
  uint32_t eval_method;
  if (is_int && (xx > 0.6)) {
    eval_method = 0;
  } else if (is_half_int && (xx > 0.2)) {
    eval_method = 1;
  } else if (xx < kSmallEpsilon) {
    // avoid computing log(0)
    // don't need more precision here, 6 digits is enough
    return 0.0;
  } else if (xx < 1.1) {
    eval_method = 2;
  } else {
    const double x_minus_a = xx - aa;
    uint32_t use_temme = 0;
    if (aa > 20) {
      // sigma = abs((x - a) / a);
      // igamma_temme_large() assumes abs(sigma) < 0.95
      if (aa > 200) {
        // abs(sigma) < sqrt(20 / a) < 0.316...
        use_temme = (20 * aa > x_minus_a * x_minus_a);
      } else {
        // abs(sigma) < 0.4
        const double sigma_times_a = fabs(x_minus_a);
        use_temme = (sigma_times_a < 0.4 * aa);
      }
    }
    if (use_temme) {
      eval_method = 5;
    } else {
      // x - (1 / (3 * x)) < a
      // x * x - (1/3) < a * x
      // x * x - a * x < 1/3
      // x * (x - a) < 1/3
      if (xx * x_minus_a < (1.0 / 3.0)) {
        eval_method = 2;
      } else {
        eval_method = 4;
      }
    }
  }
  switch(eval_method) {
  case 0:
    return finite_gamma_q_ln(df / 2, xx);
  case 1:
    return finite_half_gamma_q2_ln(df / 2, xx);
  case 2:
    {
      const double result_ln = regularized_gamma_prefix_ln(aa, xx);
      if (result_ln < kLogMinValue + 22) {
        // init_value overflows.  Not a big deal, this just ends up getting
        // inverted to pval=1.
        // (+22 since aa could theoretically be as large as 2^31.  Todo: find
        // the smallest result_ln value that could result in a nonzero value
        // being returned.)
        return 0.0;
      }
      const double init_value = -aa * exp(-result_ln);
      const double multiplier = -lower_gamma_series(aa, xx, init_value) / aa;
      return result_ln + log(multiplier);
    }
  case 4:
    {
      const double result1_ln = regularized_gamma_prefix_ln(aa, xx);
      const double result2_ln = log(upper_gamma_fraction(aa, xx));
      return result1_ln + result2_ln;
    }
  case 5:
  default:  // silence compiler warning
    {
      // aa large, fabs(xx - aa) relatively small, so no overflow/underflow
      // issues.
      double result = igamma_temme_large(aa, xx);
      if (xx < aa) {
        result = 1.0 - result;
      }
      return log(result);
    }
  }
}

double ChisqToLnP(double chisq, uint32_t df) {
  return MINV(gamma_incomplete_imp2_ln(df, chisq * 0.5), 0.0);
}
// ***** end ChisqToLnP *****


// ***** thread-safe PToChisq *****
// port of Boost 1.60 implementation

double find_inverse_gamma2(uint32_t df, double pp, double qq, uint32_t* has_10_digits_ptr) {
  // currently assumes *has_10_digits_ptr initialized to zero
  if (df == 2) {
    return -log(qq);
  }
  if (df == 1) {
    // g == tgamma(0.5) == sqrt(pi)
    const double bb = qq * kSqrtPi;
    if (bb >= 0.45) {
      // b * q > 1e-8, q > 1e-5 guaranteed
      // u = pow(p * g * a, 1/a)
      //   = pow(p * g * 0.5, 2)
      //   = p * p * g * g * 0.25
      //   = p * p * pi * 0.25
      const double uu = pp * pp * (0.25 * kPi);
      return (uu / (1 - (uu * (1.0 / 1.5))));
    } else {
      const double yy = -log(bb);
      if (bb > 0.1) {
        const double uu = yy - 0.5 * log(yy);
        if (bb > 0.15) {
          return (yy - 0.5 * log(uu) - log(1 + 0.5 / uu));
        }
        return (yy - 0.5 * log(uu) - log(((uu + 5) * uu + 3.75) / ((uu + 4.5) * uu + 2)));
      } else {
        const double c1 = -0.5 * log(yy);
        const double c1_2 = c1 * c1;
        const double c1_3 = c1_2 * c1;
        const double c1_4 = c1_2 * c1_2;
        // a_2 = 0.25
        // a_3 = 0.125

        const double c2 = -0.5 * (1 + c1);
        const double c3 = 0.25 * c1_2 + 0.75 * c1 + 0.875;
        const double c4 = c1_3 * (-1.0 / 6.0) - 0.875 * c1_2 - 1.875 * c1 - (26.75 / 12.0);
        const double c5 = 0.125 * c1_4 + (5.75 / 6.0) * c1_3 + 3.625 * c1_2 + 7.75 * c1 + (83.0625 / 12.0);

        const double y_recip = 1.0 / yy;
        const double y_recip_2 = y_recip * y_recip;
        const double y_recip_3 = y_recip_2 * y_recip;
        const double y_recip_4 = y_recip_2 * y_recip_2;
        if (bb < 1e-28) {
          *has_10_digits_ptr = 1;
        }
        // er, I'd think this should just use Horner's instead?
        return (yy + c1 + c2 * y_recip + c3 * y_recip_2 + c4 * y_recip_3 + c5 * y_recip_4);
      }
    }
  }
  // not implemented yet
  assert(0);
  exit(1);
  return 0;
}

double gamma_p_inv_imp2(uint32_t df, double qq) {
  assert(df);
  assert(qq > 0.0);
  if (qq >= 1.0 - kSmallEpsilon) {
    return 0;
  }
  double pp = 1.0 - qq;
  uint32_t has_10_digits = 0;
  double guess = find_inverse_gamma2(df, pp, qq, &has_10_digits);
  if (has_10_digits) {
    return guess;
  }
  double min_guess = kSmallEpsilon;
  double max_guess = DBL_MAX;
  if (guess < kSmallEpsilon) {
    guess = kSmallEpsilon;
  }
  // halley iteration, digits == 24, lower == kSmallEpsilon
  // see second_order_root_finder in boost/math/tools/roots.hpp
  const uint32_t invert = (pp > 0.9);
  if (invert) {
    pp = qq;
  }
  const double a_minus_1 = 0.5 * S_CAST(double, S_CAST(int32_t, df) - 2);
  const double factor = 1.1920928955078125e-07;  // 2^{-23}
  double result = guess;
  double delta = 10000000;
  double delta1 = delta;
  uint32_t out_of_bounds_sentry = 0;
  do {
    double delta2 = delta1;
    delta1 = delta;

    // see gamma_p_inverse_func in
    // boost/math/special_functions/detail/igamma_inverse.hpp
    double f1;
    const double ff = gamma_incomplete_imp2(df, result, invert, &f1);
    const double div = (a_minus_1 - result) / result;
    double f2 = f1;
    if ((fabs(div) > 1) && (DBL_MAX / fabs(div) < f2)) {
      // overflow
      f2 = -DBL_MAX / 2;
    } else {
      f2 *= div;
    }
    if (invert) {
      f1 = -f1;
      f2 = -f2;
    }
    const double f0 = ff - pp;
    if (f0 == 0) {
      break;
    }
    assert(f1 != 0);  // shouldn't be possible, function is monotonic
    delta = f0 / f1;
    if (f2 != 0) {
      // delta = Stepper::step(result, f0, f1, f2);
      const double denom = 2 * f0;
      const double numer = 2 * f1 - f0 * (f2 / f1);
      if ((fabs(numer) >= 1) || (fabs(denom) < fabs(numer) * DBL_MAX)) {
        const double halley_step = denom / numer;
        if (halley_step / delta < 0) {
          if (fabs(delta) > 2 * fabs(guess)) {
            delta = ((delta < 0)? -1 : 1) * 2 * fabs(guess);
          }
        } else {
          delta = halley_step;
        }
      }
    }
    double convergence = fabs(delta / delta2);
    if ((convergence > 0.8) && (convergence < 2)) {
      delta = (delta > 0)? (0.5 * (result - min_guess)) : (0.5 * (result - max_guess));
      if (fabs(delta) > result) {
        delta = ((delta > 0)? 1 : -1) * result;
      }
      // delta2 = delta * 3;
    }
    guess = result;
    result -= delta;
    // do we actually need this?
    if (result < min_guess) {
      double diff = ((fabs(min_guess) < 1) && (fabs(result) > 1) && ((DBL_MAX / fabs(result)) < fabs(min_guess)))? 1000 : (result / min_guess);
      if (fabs(diff) < 1) {
        diff = 1 / diff;
      }
      if ((!out_of_bounds_sentry) && (diff > 0) && (diff < 3)) {
        delta = 0.99 * (guess - min_guess);
        result = guess - delta;
        out_of_bounds_sentry = 1;
      } else {
        delta = (guess - min_guess) * 0.5;
        result = guess - delta;
        if ((result == min_guess) || (result == max_guess)) {
          break;
        }
      }
    } else if (result > max_guess) {
      double diff = ((fabs(max_guess) < 1) && (fabs(result) > 1) && ((DBL_MAX / fabs(result)) < fabs(max_guess)))? 1000 : (result / max_guess);
      if (fabs(diff) < 1) {
        diff = 1 / diff;
      }
      if ((!out_of_bounds_sentry) && (diff > 0) && (diff < 3)) {
        delta = 0.99 * (guess - max_guess);
        result = guess - delta;
        out_of_bounds_sentry = 1;
      } else {
        delta = (guess - max_guess) * 0.5;
        result = guess - delta;
        if ((result == min_guess) || (result == max_guess)) {
          break;
        }
      }
    }
    if (delta > 0) {
      max_guess = guess;
    } else {
      min_guess = guess;
    }
  } while (fabs(result * factor) < fabs(delta));
  return result;
}

double PToChisq(double pval, uint32_t df) {
  // only need this to handle df=1, 2, 4 for now
  return gamma_p_inv_imp2(df, pval) * 2;
}

// ***** end thread-safe PToChisq *****

double LnPToChisq(double ln_pval) {
  if (ln_pval > -65.04474754675797) {
    return gamma_p_inv_imp2(1, exp(ln_pval)) * 2;
  }
  // (bb < 1e-28) case in find_inverse_gamma2()
  const double yy = kLnSqrtPi - ln_pval;
  const double c1 = -0.5 * log(yy);
  const double c1_2 = c1 * c1;
  const double c1_3 = c1_2 * c1;
  const double c1_4 = c1_2 * c1_2;
  // a_2 = 0.25
  // a_3 = 0.125

  const double c2 = -0.5 * (1 + c1);
  const double c3 = 0.25 * c1_2 + 0.75 * c1 + 0.875;
  const double c4 = c1_3 * (-1.0 / 6.0) - 0.875 * c1_2 - 1.875 * c1 - (26.75 / 12.0);
  const double c5 = 0.125 * c1_4 + (5.75 / 6.0) * c1_3 + 3.625 * c1_2 + 7.75 * c1 + (83.0625 / 12.0);

  const double y_recip = 1.0 / yy;
  const double y_recip_2 = y_recip * y_recip;
  const double y_recip_3 = y_recip_2 * y_recip;
  const double y_recip_4 = y_recip_2 * y_recip_2;
  // er, I'd think this should just use Horner's instead?
  return 2 * (yy + c1 + c2 * y_recip + c3 * y_recip_2 + c4 * y_recip_3 + c5 * y_recip_4);
}


// ***** thread-safe TstatToP *****
// ...time to get rid of this

// see Numerical Recipes, section 6.4
// no overflow or convergence issues
double betacf_slow(double aa, double bb, double xx) {
  double qab = aa + bb;
  double qap = aa + 1.0;
  double qam = aa - 1.0;
  double cc = 1.0;
  double dd = 1.0 - qab * xx / qap;
  if (fabs(dd) < kLentzFpmin) {
    dd = kLentzFpmin;
  }
  dd = 1.0 / dd;
  double hh = dd;
  // evaluate 1 / (1 + d_1 / (1 + d_2 / (1 + d_3 / (...))))
  // might need more iterations for very large df?
  for (double mm = 1.0; mm <= 100.0; mm += 1.0) {
    double m2 = 2 * mm;

    // d_{2m}
    double tmp_aa = mm * (bb - mm) * xx / ((qam + m2) * (aa + m2));

    dd = 1.0 + tmp_aa * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = 1.0 + tmp_aa / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    hh *= dd * cc;

    // d_{2m+1}
    tmp_aa = -(aa + mm) * (qab + mm) * xx / ((aa + m2) * (qap + m2));

    dd = 1.0 + tmp_aa * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = 1.0 + tmp_aa / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    double del = dd * cc;
    hh *= del;
    if (fabs(del - 1.0) < kContinuedFractionEpsilon) {
      return hh;
    }
  }
  // don't detect failure for now
  return hh;
}

// Returns log(1 - x^y).
// Assumes x in (0, 1), y positive.
double neg_powm1_imp_ln(double xx, double yy) {
  const double ll = yy * log(xx);
  if (ll > -1.0) {
    // For tiny |l|, we may lose all precision when exp(l) evaluates to 1.
    return log(-expm1(ll));
  }
  // For larger |l|, it's the log step that may throw away precision.
  if (ll < kLogMinValue) {
    return 0.0;
  }
  return log1p(-exp(ll));
}

double binomial_ccdf_ln(uint32_t nn, uint32_t kk, double xx, double yy, uint32_t inv) {
  // x^n + (n choose 1)x^{n-1}y + ... + (n choose (n-k-1))x^{k+1}y^{n-k-1}
  // This is currently just designed to work with ibeta_imp2_ln().  So we
  // assume x and y positive, y = 1 - x, (n-k) < 40, (n-k) <= (k+1)(y/x).

  // Thanks to the (n-k) <= (k+1)(y/x) condition, it's always reasonable to
  // start from the rightmost term normalized to 1, and sum leftward; then we
  // multiply by the rightmost term at the end.
  const double n_d = u31tod(nn);
  const double k_plus1_d = u31tod(kk + 1);
  const double x_div_y = xx / yy;
  double multiplier = 1.0;
  double cur_term = 1.0;
  // need to shift to avoid overflow for n > 1 billion, (n-k) close to 40
  double shifted_inv_binom_coeff = 1.0 / kExactTestBias;
  double i_d = n_d - k_plus1_d;
  for (uint32_t uii = nn - kk - 1; uii; --uii) {
    //   (n choose (i - 1)) / (n choose i)
    //   i!(n-i)! / (i-1)!(n-i+1)!
    // = i / (n-i+1)
    if (cur_term < kRecip2m53) {
      // Only need to finish computing shifted_inv_binom_coeff: multiply by
      // i!, divide by n!/(n-i)!.
      // Since we support n up to 2^31, n!/(n-i)! may overflow for i>33.
      for (; uii > 33; --uii) {
        shifted_inv_binom_coeff *= i_d;
        i_d -= 1.0;
        shifted_inv_binom_coeff /= n_d - i_d;
      }
      double denom = kFactorialRecips[uii];
      double n_minus_i = n_d - i_d;
      for (; uii; --uii) {
        n_minus_i += 1.0;
        denom *= n_minus_i;
      }
      shifted_inv_binom_coeff /= denom;
      break;
    }
    double cur_binom_ratio = i_d;
    i_d -= 1.0;
    cur_binom_ratio /= n_d - i_d;
    shifted_inv_binom_coeff *= cur_binom_ratio;
    cur_term *= cur_binom_ratio * x_div_y;
    multiplier += cur_term;
  }
  // 1.0 / kExactTestBias = 2^83
  // tried taking log(multiplier / shifted_inv_binom_coeff), but that was
  // slower on my Mac?
  const double result_ln = log(multiplier) + k_plus1_d * log(xx) + (n_d - k_plus1_d) * log(yy) - log(shifted_inv_binom_coeff) + 83 * kLn2;
  if (!inv) {
    return result_ln;
  }
  // The nightmare scenario of tiny result_ln shouldn't happen thanks to the
  // (n-k) <= (k+1)(y/x) condition.
  return log1p(-exp(result_ln));
}

double ibeta_series_ln(double aa, double bb, double xx, uint32_t inv) {
  // BPSER in DiDonato and Morris.
  // ibeta(a, b, x) ~= gamma(a+b) / (gamma(a) * gamma(b))
  //                 * x^a / a
  //                 * (1 + a\sum_j=1^N [(1-b)(2-b)...(j-b)x^j / (j!(a+j))])
  // Currently always called with x <= 0.7, and either b*x <= 0.7 or b<40, so
  // guaranteed to converge at reasonable speed.

  // normalized always true

  const double cc = aa + bb;

  const double agh = aa + kLanczosFloatG - 0.5;
  const double bgh = bb + kLanczosFloatG - 0.5;
  const double cgh = cc + kLanczosFloatG - 0.5;
  double numer_a;
  double denom_a = lanczos_sum_f_expg_scaled_imp(aa, &numer_a);
  double numer_b;
  double denom_b = lanczos_sum_f_expg_scaled_imp(bb, &numer_b);
  double denom_c;
  double numer_c = lanczos_sum_f_expg_scaled_imp(cc, &denom_c);
  double result = (numer_a * numer_b * numer_c) / (denom_a * denom_b * denom_c);
  double l1 = log(cgh / bgh) * (bb - 0.5);
  double l2 = log(xx * cgh / agh) * aa;
  double result_ln = log(result * result * agh) * 0.5 + l1 + l2 - 0.5;

  double series_sum = 0.0;
  double term = 1.0;
  double apn = aa;
  double poch = 1.0 - bb;
  double nn = 1.0;
  while (1) {
    double rr = term / apn;
    series_sum += rr;
    if (fabs(rr) <= fabs(series_sum * kEpsilon)) {
      result_ln += log(series_sum);
      if (!inv) {
        return result_ln;
      }
      // assume for now that we wouldn't be inverting into the tiny result_ln
      // case.
      return log1p(-exp(result_ln));
    }
    apn += 1.0;
    term *= poch * xx / nn;
    nn += 1.0;
    poch += 1.0;
  }
}

double ln_sum(double aa, double bb) {
  if (aa > bb) {
    const double tmp = aa;
    aa = bb;
    bb = tmp;
  }
  const double diff = bb - aa;
  if ((aa == -DBL_MAX) || (diff >= kLn2 * 53)) {
    return bb;
  }
  return bb + log(1 + exp(-diff));
}

double tgamma_delta_ratio(double zz, double delta) {
  // gamma(z) / gamma(z + delta)
  // We only call this with delta = 0.5 or 1, so no overflow issues.
  // zz >= 15 for now.
  if (delta == 1.0) {
    // Trivial case.
    return 1.0 / zz;
  }
  assert(delta == 0.5);
  // Can skip z < epsilon and z + delta == z branches for now.
  double zgh = zz + kLanczosFloatG - 0.5;
  // Also skip fabs(delta) >= 10 branch for now.
  double result = exp((0.5 - zz) * log1p(delta / zgh));
  result *= lanczos_sum_f(zz) / lanczos_sum_f(zz + delta);
  // exploit forced delta == 0.5
  result *= sqrt(kE / (zgh + delta));
  return result;
}

double beta_small_b_large_a_series_ln(double aa, double bb, double xx, double yy, double s0_ln, uint32_t inv) {
  // BGRAT in DiDonato and Morris.

  // normalized always true, mult always 1
  // a >= 15, b == 0.5 or 1, and ibeta_imp was patched to ensure x > 0.7.
  double bm1 = bb - 1.0;
  double tt = aa + bm1 * 0.5;
  double lx;
  if (yy < 0.35) {
    lx = log1p(-yy);
  } else {
    lx = log(xx);
  }
  double uu = -tt * lx;
  double hh_ln = regularized_gamma_prefix_ln(bb, uu);
  double prefix_ln = hh_ln - log(tgamma_delta_ratio(aa, bb)) - bb * log(tt);
  // validated up to this point

  double pp[15]; // ~8-15 digit accuracy
  pp[0] = 1.0;

  double jj;
  {
    double dxx;
    if (bb == 0.5) {
      // bugfix (17 Jun 2019): original expression could underflow
      // jj = finite_half_gamma_q2(0, uu, nullptr);
      dxx = finite_half_gamma_q2_ln(0, uu);
    } else {
      assert(bb == 1.0);
      dxx = -uu;
    }
    // Underflow is harmless here, jj becomes nonzero for the other terms in
    // the series in that case.
    jj = exp(dxx - hh_ln);
  }
  double sum = jj; // patch in s0 and prefix at the end
  uint32_t tnp1 = 1;
  double lx2 = lx * 0.5;
  lx2 *= lx2;
  double lxp = 1.0;
  double t4_recip = 0.25 / (tt * tt);
  double b2n = bb;
  for (uint32_t nn = 1; nn < 15; ++nn) {
    tnp1 += 2;
    double new_pn = 0.0;
    uint32_t tmp1 = 3;
    const double nn_d = u31tod(nn);
    for (uint32_t mm = 1; mm < nn; ++mm) {
      double mbn = u31tod(mm) * bb - nn_d;
      new_pn += mbn * pp[nn - mm] * kFactorialRecips[tmp1];
      tmp1 += 2;
    }
    new_pn /= nn_d;
    new_pn += bm1 * kFactorialRecips[tnp1];
    pp[nn] = new_pn;

    jj = (b2n * (b2n + 1) * jj + (uu + b2n + 1) * lxp) * t4_recip;
    lxp *= lx2;
    b2n += 2;

    double rr = new_pn * jj;
    sum += rr;
    if (rr > 1) {
      if (fabs(rr) < fabs(kEpsilon * sum)) {
        break;
      }
    } else {
      if (fabs(rr * (1.0 / kEpsilon)) < fabs(sum)) {
        break;
      }
    }
  }
  double result_ln = ln_sum(s0_ln, prefix_ln + log(sum));
  if (!inv) {
    return result_ln;
  }
  return log1p(-exp(result_ln));
}

double ibeta_power_terms_ln(double aa, double bb, double xx, double yy) {
  // returns log((x^a)(y^b) / Beta(a,b))
  //
  // normalized always true
  // prefix always 1
  double cc = aa + bb;

  const double agh = aa + kLanczosFloatG - 0.5;
  const double bgh = bb + kLanczosFloatG - 0.5;
  const double cgh = cc + kLanczosFloatG - 0.5;
  double numer_a;
  double denom_a = lanczos_sum_f_expg_scaled_imp(aa, &numer_a);
  double numer_b;
  double denom_b = lanczos_sum_f_expg_scaled_imp(bb, &numer_b);
  double denom_c;
  double numer_c = lanczos_sum_f_expg_scaled_imp(cc, &denom_c);
  double result = (numer_a * numer_b * numer_c) / (denom_a * denom_b * denom_c);
  result *= sqrt(agh * bgh * kRecipE / cgh);
  double result_ln = log(result);
  double l1 = (xx * bb - yy * agh) / agh;
  double l2 = (yy * aa - xx * bgh) / bgh;
  // Since we're returning log(result) rather than the original result (thus,
  // no intermediate overflow/underflow problems), and we only need 6 digits of
  // precision, we shouldn't need any of the numerous cases in the Boost code.
  return result_ln + aa * log1p(l1) + bb * log1p(l2);
}

double ibeta_a_step_ln(double aa, double bb, double xx, double yy, uint32_t kk) {
  double prefix_ln = ibeta_power_terms_ln(aa, bb, xx, yy) - log(aa);
  double sum = 1.0;
  double term = 1.0;
  const double a_plus_b = aa + bb;
  const double k_minus_1pt5 = u31tod(kk) - 1.5;
  for (double i_d = 0.0; i_d < k_minus_1pt5; ) {
    term *= (a_plus_b + i_d) * xx;
    i_d += 1.0;
    term /= aa + i_d;
    sum += term;
  }
  return prefix_ln + log(sum);
}

double ibeta_fraction2_ln(double aa, double bb, double xx, double yy, uint32_t inv) {
  // normalized always true

  // todo: original DiDonato and Morris paper notes that "x must also be a
  // sufficient distance from p when a > 100"; check if we have a problem there

  double result_ln = ibeta_power_terms_ln(aa, bb, xx, yy);

  // see Boost continued_fraction_b()
  const double ay_minus_bx_plus1 = aa * yy - bb * xx + 1.0;
  double cc = (aa * ay_minus_bx_plus1) / (aa + 1.0);
  if (fabs(cc) < kLentzFpmin) {
    cc = kLentzFpmin;
  }
  const double a_plus_b = aa + bb;
  const double x2 = xx * xx;
  result_ln -= log(cc);
  double dd = 0.0;
  double mm = 0.0;
  while (1) {
    double cur_a = (aa + mm) * (a_plus_b + mm);
    mm += 1.0;
    cur_a *= mm * (bb - mm) * x2;
    double denom = aa + 2 * mm - 1.0;
    cur_a /= denom * denom;
    double cur_b = mm;
    cur_b += (mm * (bb - mm) * xx) / (aa + 2 * mm - 1.0);
    cur_b += ((aa + mm) * (ay_minus_bx_plus1 + mm * (2.0 - xx))) / (aa + 2 * mm + 1.0);
    dd = cur_b + cur_a * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = cur_b + cur_a / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    const double delta = cc * dd;
    result_ln -= log(delta);
    if (fabs(delta - 1.0) <= kContinuedFractionEpsilon) {
      return inv? log1p(-exp(result_ln)) : result_ln;
    }
  }
}

// the '2' in imp2 refers to df1 and df2 arguments instead of a and b
double ibeta_imp2_ln(uint32_t df1, uint32_t df2, double xx, uint32_t inv) {
  // In addition to Boost beta.hpp and its dependencies, see DiDonato and
  // Morris's original paper at
  // https://apps.dtic.mil/dtic/tr/fulltext/u2/a210118.pdf .

  // normalized always true
  //
  // assume a >= 0, b >= 0, a+b > 0, x in [0, 1], a and b are multiples of 0.5
  // for now
  // in practice, x always <= 0.5 right now
  if (df1 == 0) {
    return inv? -DBL_MAX : 0.0;
  }
  if (df2 == 0) {
    return inv? 0.0 : -DBL_MAX;
  }
  if (xx == 0.0) {
    return inv? 0.0 : -DBL_MAX;
  }
  if (xx == 1.0) {
    return inv? -DBL_MAX : 0.0;
  }
  double yy = 1.0 - xx;
  if ((df1 == 1) && (df2 == 1)) {
    return log(asin(sqrt(inv? yy : xx)) * (2.0 / kPi));
  }
  if (df1 == 2) {
    df1 = df2;
    df2 = 2;

    const double tmp = xx;
    xx = yy;
    yy = tmp;

    inv = !inv;
  }
  double aa = u31tod(df1) * 0.5;
  if (df2 == 2) {
    if (df1 == 2) {
      return log(inv? yy : xx);
    }
    double ln_pp;
    if (yy < 0.5) {
      //       pp = inv? -expm1(aa * log1p(-yy)) : exp(aa * log1p(-yy))
      // -> ln_pp = inv? log(-exp(aa * log1p(-yy)) + 1.0) : (aa * log1p(-yy))
      //          = inv? log1p(-exp(aa * log1p(-yy))) : (aa * log1p(-yy))
      const double inner_term = aa * log1p(-yy);
      ln_pp = inv? log1p(-exp(inner_term)) : inner_term;
    } else {
      //       pp = inv? -powm1(xx, aa) : pow(xx, aa);
      // -> ln_pp = inv? log(-powm1(xx, aa)) : (aa * log(xx))
      ln_pp = inv? neg_powm1_imp_ln(xx, aa) : (aa * log(xx));
    }
    return ln_pp;
  }

  double bb = u31tod(df2) * 0.5;
  if ((df1 == 1) || (df2 == 1)) {
    if (xx > 0.5) {
      const uint32_t tmp1 = df1;
      df1 = df2;
      df2 = tmp1;

      double tmp2 = aa;
      aa = bb;
      bb = tmp2;

      tmp2 = xx;
      xx = yy;
      yy = tmp2;

      inv = !inv;
    }
    // can ignore max(a, b) <= 1 branch
    if ((df2 == 1) || ((xx < 0.1) && (bb * xx <= 0.49))) {
      return ibeta_series_ln(aa, bb, xx, inv);
    }
    // a/b swapped, x/y swapped, inv swapped
    if (xx >= 0.3) {
      return ibeta_series_ln(bb, aa, yy, !inv);
    }
    if (bb >= 15) {
      return beta_small_b_large_a_series_ln(bb, aa, yy, xx, -DBL_MAX, !inv);
    }
    double fract_ln = ibeta_a_step_ln(bb, aa, yy, xx, 20);
    return beta_small_b_large_a_series_ln(bb + 20.0, aa, yy, xx, fract_ln, !inv);
  }
  double lambda;
  if (aa < bb) {
    lambda = aa - (aa + bb) * xx;
  } else {
    lambda = (aa + bb) * yy - bb;
  }
  if (lambda < 0.0) {
    const uint32_t tmp1 = df1;
    df1 = df2;
    df2 = tmp1;

    double tmp2 = aa;
    aa = bb;
    bb = tmp2;

    tmp2 = xx;
    xx = yy;
    yy = tmp2;

    inv = !inv;
  }

  // a > 1, b > 1 guaranteed if we get here.
  if (df2 >= 80) {
    return ibeta_fraction2_ln(aa, bb, xx, yy, inv);
  }
  if ((!(df1 % 2)) && (!(df2 % 2)) && (yy != 1.0)) {
    const uint32_t a_int = df1 / 2;
    const uint32_t b_int = df2 / 2;
    const uint32_t kk = a_int - 1;
    const uint32_t nn = b_int + kk;
    return binomial_ccdf_ln(nn, kk, xx, yy, inv);
  }
  // Changed from b*x <= 0.7 since BGRAT has problems with small x, while BPSER
  // can handle this larger b*x case since b<40 is guaranteed.
  if (xx <= 0.7) {
    return ibeta_series_ln(aa, bb, xx, inv);
  }
  const uint32_t nn = (df2 - 1) / 2;
  const double bbar = bb - u31tod(nn);
  double fract_ln = ibeta_a_step_ln(bbar, aa, yy, xx, nn);
  if (df1 > 30) {
    return beta_small_b_large_a_series_ln(aa, bbar, xx, yy, fract_ln, inv);
  }
  fract_ln = ln_sum(fract_ln, ibeta_a_step_ln(aa, bbar, xx, yy, 20));
  return beta_small_b_large_a_series_ln(aa + 20.0, bbar, xx, yy, fract_ln, inv);
}

double TstatToP2(double tt, double df, double cached_gamma_mult) {
  // assumes cached_mult == exp(lgamma(df * 0.5 + 0.5) - lgamma(df * 0.5) -
  //   lgamma(0.5))
  //         invert_thresh = (df + 2) / (df + 5)
  double tt_sq = tt * tt;
  double denom_recip = 1.0 / (df + tt_sq);
  double xx = df * denom_recip;
  double yy = tt_sq * denom_recip;
  if ((xx < 0.0) || (yy < 0.0)) {
    return -9;
  }
  uint32_t do_invert = (xx * (df + 5.0)) >= (df + 2.0);
  if ((xx == 0.0) || (yy == 0.0)) {
    return u31tod(do_invert);
  }
  double aa = df * 0.5;
  double bt = cached_gamma_mult * pow(xx, aa) * sqrt(yy);
  if (!do_invert) {
    return bt * betacf_slow(aa, 0.5, xx) / aa;
  }
  return 1.0 - bt * 2 * betacf_slow(0.5, aa, yy);
}
// ***** end thread-safe TstatToP calculation *****

// ***** begin TstatToLnP *****

double TstatToLnP(double tt, uint32_t df) {
  const double df_d = u31tod(df);
  const double t2 = tt * tt;
  if (df_d > 2 * t2) {
    const double zz = t2 / (df_d + t2);
    return ibeta_imp2_ln(1, df, zz, 1);
  }
  const double zz = df_d / (df_d + t2);
  return ibeta_imp2_ln(df, 1, zz, 0);
}
// ***** end TstatToLnP *****

// ***** FstatToLnP *****

double FstatToLnP(double ff, uint32_t df1, uint32_t df2) {
  // See cdf() for complemented2_type in boost/math/distributions/fisher_f.hpp.
  const double df1_d = u31tod(df1);
  const double df2_d = u31tod(df2);
  const double v1x = df1_d * ff;
  if (v1x > df2_d) {
    return ibeta_imp2_ln(df2, df1, df2_d / (df2_d + v1x), 0);
  }
  return ibeta_imp2_ln(df1, df2, v1x / (df2_d + v1x), 1);
}

// ***** end FstatToLnP *****


// Inverse normal distribution
// (todo: check if boost implementation is better)

// Lower tail quantile for standard normal distribution function.
//
// This function returns an approximation of the inverse cumulative
// standard normal distribution function.  I.e., given P, it returns
// an approximation to the X satisfying P = Pr{Z <= X} where Z is a
// random variable from the standard normal distribution.
//
// The algorithm uses a minimax approximation by rational functions
// and the result has a relative error whose absolute value is less
// than 1.15e-9.
//
// Author:      Peter J. Acklam
// Time-stamp:  2002-06-09 18:45:44 +0200
// E-mail:      jacklam@math.uio.no
// WWW URL:     http://www.math.uio.no/~jacklam
//
// C implementation adapted from Peter's Perl version

// Coefficients in rational approximations.

static const double kIvnA[] =
  {
    -3.969683028665376e+01,
    2.209460984245205e+02,
    -2.759285104469687e+02,
    1.383577518672690e+02,
    -3.066479806614716e+01,
     2.506628277459239e+00
  };

static const double kIvnB[] =
  {
    -5.447609879822406e+01,
    1.615858368580409e+02,
    -1.556989798598866e+02,
    6.680131188771972e+01,
    -1.328068155288572e+01
  };

static const double kIvnC[] =
  {
    -7.784894002430293e-03,
    -3.223964580411365e-01,
    -2.400758277161838e+00,
    -2.549732539343734e+00,
    4.374664141464968e+00,
     2.938163982698783e+00
  };

static const double kIvnD[] =
  {
    7.784695709041462e-03,
    3.224671290700398e-01,
    2.445134137142996e+00,
    3.754408661907416e+00
  };

static const double kIvnLow = 0.02425;
static const double kIvnHigh = 0.97575;

double QuantileToZscore(double pp) {
  // assumes 0 < pp < 1
  double q, r;

  if (pp < kIvnLow) {
    // Rational approximation for lower region
    q = sqrt(-2*log(pp));
    return (((((kIvnC[0]*q+kIvnC[1])*q+kIvnC[2])*q+kIvnC[3])*q+kIvnC[4])*q+kIvnC[5]) /
      ((((kIvnD[0]*q+kIvnD[1])*q+kIvnD[2])*q+kIvnD[3])*q+1);
  }
  if (pp > kIvnHigh) {
    // Rational approximation for upper region
    q  = sqrt(-2*log(1-pp));
    return -(((((kIvnC[0]*q+kIvnC[1])*q+kIvnC[2])*q+kIvnC[3])*q+kIvnC[4])*q+kIvnC[5]) /
      ((((kIvnD[0]*q+kIvnD[1])*q+kIvnD[2])*q+kIvnD[3])*q+1);
  }
  // Rational approximation for central region
  q = pp - 0.5;
  r = q*q;
  return (((((kIvnA[0]*r+kIvnA[1])*r+kIvnA[2])*r+kIvnA[3])*r+kIvnA[4])*r+kIvnA[5])*q /
    (((((kIvnB[0]*r+kIvnB[1])*r+kIvnB[2])*r+kIvnB[3])*r+kIvnB[4])*r+1);
}

static const double kLogFactorials[99] = {
  0,
  0,
  0.69314718055994529,
  1.791759469228055,
  3.1780538303479458,
  4.7874917427820458,
  6.5792512120101012,
  8.5251613610654129,
  10.604602902745251,
  12.801827480081471,
  15.104412573075518,
  17.502307845873887,
  19.987214495661888,
  22.552163853123428,
  25.19122118273868,
  27.89927138384089,
  30.671860106080675,
  33.505073450136891,
  36.395445208033053,
  39.339884187199495,
  42.335616460753485,
  45.380138898476915,
  48.471181351835227,
  51.606675567764377,
  54.784729398112312,
  58.003605222980525,
  61.261701761002001,
  64.557538627006323,
  67.88974313718154,
  71.257038967168015,
  74.658236348830172,
  78.092223553315307,
  81.557959456115029,
  85.05446701758153,
  88.580827542197667,
  92.136175603687093,
  95.719694542143202,
  99.330612454787428,
  102.9681986145138,
  106.63176026064346,
  110.32063971475741,
  114.03421178146171,
  117.77188139974507,
  121.53308151543862,
  125.31727114935688,
  129.12393363912722,
  132.95257503561629,
  136.80272263732635,
  140.67392364823425,
  144.56574394634487,
  148.47776695177302,
  152.40959258449737,
  156.3608363030788,
  160.3311282166309,
  164.3201122631952,
  168.32744544842768,
  172.35279713916282,
  176.39584840699732,
  180.45629141754378,
  184.53382886144945,
  188.6281734236716,
  192.73904728784493,
  196.86618167289001,
  201.00931639928149,
  205.16819948264117,
  209.34258675253682,
  213.53224149456327,
  217.73693411395425,
  221.95644181913033,
  226.19054832372763,
  230.43904356577696,
  234.70172344281826,
  238.97838956183432,
  243.26884900298268,
  247.57291409618685,
  251.89040220972319,
  256.22113555000954,
  260.56494097186322,
  264.92164979855278,
  269.29109765101975,
  273.67312428569375,
  278.06757344036617,
  282.47429268763045,
  286.89313329542699,
  291.32395009427029,
  295.7666013507606,
  300.22094864701415,
  304.68685676566872,
  309.1641935801469,
  313.65282994987905,
  318.1526396202093,
  322.66349912672621,
  327.18528770377526,
  331.71788719692847,
  336.26118197919845,
  340.81505887079902,
  345.37940706226686,
  349.95411804077025,
  354.53908551944079
};

// port of Boost 1.84 implementation, double-precision
double Lfact(double xx) {
  if (xx < 99.0) {
    const uint32_t uii = S_CAST(int32_t, xx);
    return kLogFactorials[uii];
  }
  const double zz = xx + 1;
  double result = (log(zz + kLanczosDoubleG - 0.5) - 1) * (zz - 0.5);
  result += log(lanczos_sum_d_expg_scaled(zz));
  return result;
}

// obs_hets + obs_hom1 + obs_hom2 assumed to be <2^31.
double HweLnP(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, uint32_t midp) {
  // This function implements an exact SNP test of Hardy-Weinberg
  // Equilibrium as described in Wigginton, JE, Cutler, DJ, and
  // Abecasis, GR (2005) A Note on Exact Tests of Hardy-Weinberg
  // Equilibrium. American Journal of Human Genetics. 76: 887 - 893.
  //
  // The original version was written by Jan Wigginton.
  //
  // This version was written by Christopher Chang.  It contains the following
  // improvements over the original SNPHWE():
  // - Proper handling of >64k genotypes.  Previously, there was a potential
  //   integer overflow.
  // - Detection and efficient handling of floating point overflow and
  //   underflow.  E.g. instead of summing a tail all the way down, the loop
  //   stops once the latest increment underflows the partial sum's 53-bit
  //   precision; this results in a large speedup when max heterozygote count
  //   >1k.
  // - No malloc() call.  It's only necessary to keep track of a few partial
  //   sums.
  // - Support for the mid-p variant of this test.  See Graffelman J, Moreno V
  //   (2013) The mid p-value in exact tests for Hardy-Weinberg equilibrium.
  // - Log-p-value return (added Jan 2024).  A p-value of 1e-400 may be worth
  //   distinguishing from 1e-40000 in a biobank-scale dataset.
  //
  // Note that the HweThreshLn() function is a lot more efficient for testing
  // against a p-value inclusion threshold.  HweLnP() should only be used if
  // you need the actual p-value.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  const int64_t rare_ct = 2LL * obs_homr + obs_hets;
  if (rare_ct < 2) {
    if (midp) {
      return -kLn2;
    }
    return 0;
  }
  // MAF: rare_ct / allele_ct
  // modal #hets:
  //   sample_ct * 2 * MAF * (1 - MAF)
  // = rare_ct * (1 - MAF)
  const int64_t sample_ct = obs_hom1 + obs_hom2 + obs_hets;
  const double rare_ctd = rare_ct;
  const double sample_ctd = sample_ct;
  const double allele_ctd = sample_ctd * 2;
  const double maf = rare_ctd / allele_ctd;
  const double modal_nhet = rare_ctd * (1 - maf);
  double curr_hets = obs_hets;
  double curr_homr = obs_homr;
  double curr_homc = obs_homc;
  const double c_minus_r = curr_homc - curr_homr;
  int32_t tie_ct = 1;
  if (curr_hets > modal_nhet) {
    const double het_delta = curr_hets - modal_nhet;
    if ((!midp) && (het_delta < 2.0)) {
      // Fast path for p=1.
      if (curr_hets * (curr_hets - 1) <= (4 * (1 + kSmallEpsilon)) * (curr_homr + 1) * (curr_homc + 1)) {
        return 0;
      }
    }
    // Tried using old algorithm on rare_ct < 64, but this didn't make a
    // noticeable difference to --hardy execution time on 1000 Genomes data, so
    // I'd rather not bloat this function further.
    // (might still be worth it due to midp use case?  revisit after chrX is
    // working.)

    // If we're close enough to the center, we may be best off computing 1 -
    // <sum of center probabilities>.  But this approach is vulnerable to
    // catastrophic cancellation.  Doubles have 53 bits of precision, while
    // kSmallEpsilon (which governs equality checks) is configured to 2^{-44},
    // so it makes sense to insist that the final subtraction does not cost us
    // more than ~9 bits.
    // If the starting cell has probability > 2^{-10}, that is close enough to
    // true; and I think this catches most of the cases where we actively want
    // to use the subtract-center strategy?
    //
    // From e.g. the Wigginton paper, P(N_{AB}=n_{AB} | N, n_A) is
    //
    //      2^{n_{AB}} N! n_A! n_B!
    //   -----------------------------
    //   n_{AA}! n_{AB}! n_{BB}! (2N)!
    const double common_lnprob_component = Lfact(sample_ctd) + Lfact(rare_ctd) + Lfact(allele_ctd - rare_ctd) - Lfact(allele_ctd);
    const double starting_lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
    const double starting_lnprob = common_lnprob_component + starting_lnprob_other_component;
    double lastp = 1;
    // Either obs_hets >= 2, or obs_homr == obs_hets == obs_homc == 1.  In the
    // latter case, starting_lnprob will be well over -10 * kLn2, so the other
    // branch can assume obs_hets >= 2.
    if (starting_lnprob > -10 * kLn2) {
      // This condition stops triggering once we have more than ~3 million
      // samples, but I expect it to be a significant timesaver before that
      // point.
      double centerp = 0;
      // rescale this at the end.
      while (curr_hets > 1.5) {
        curr_homr += 1;
        curr_homc += 1;
        lastp *= (curr_hets * (curr_hets - 1)) / (4 * curr_homr * curr_homc);
        if (lastp < 1 + kSmallEpsilon) {
          tie_ct += (lastp > 1 - kSmallEpsilon);
          break;
        }
        curr_hets -= 2;
        centerp += lastp;
      }
      if (midp) {
        centerp += S_CAST(double, tie_ct) * 0.5;
      }
      const double starting_prob = exp(starting_lnprob);
      centerp *= starting_prob;
      return log(1 - centerp);
    }
    double tailp = 1;
    while (curr_homr > 0.5) {
      curr_hets += 2;
      lastp *= (4 * curr_homr * curr_homc) / (curr_hets * (curr_hets - 1));
      curr_homr -= 1;
      curr_homc -= 1;
      const double preaddp = tailp;
      tailp += lastp;
      if (tailp <= preaddp) {
        break;
      }
    }
    // Now we want to jump near the other tail, without evaluating that many
    // terms in between.
    //
    // Each full log-likelihood evaluation requires 3 Lfact() calls.  Lfact()
    // tends to be ~2-3 times as expensive as log in my testing, and I've seen
    // log be anywhere from ~3 to ~12 times as expensive as division, so we
    // want to limit ourselves to 1-2 full evaluations most of the time.
    //
    // The current heuristic starts by reflecting (obs_homr + curr_homr) * 0.5
    // across the mode, performing a full log-likelihood check at the nearest
    // valid point.  Hopefully we find that we're in (starting_lnprob - 62 *
    // kLn2, starting_lnprob], so we're at or near a cell that actually
    // contributes to the tail-sum.  (This window is chosen to be wide enough
    // to guarantee that at least one point falls inside when sample_ct <
    // 2^31.)
    //
    // If not, we jump again, using Newton's method.
    // If curr_homr is too low (i.e. current log-likelihood is too high), when
    // we increase curr_homr by 1, the likelihood gets multiplied by
    //   curr_hets * (curr_hets-1) / (4 * (curr_homr+1) * (curr_homc+1))
    // i.e. we're adding the logarithm of this value to the log-likelihood.
    // If curr_homr is too high, when we decrease curr_homr by 1, the
    // likelihood gets multiplied by
    //   4 * curr_homr * curr_homc / ((curr_hets+2) * (curr_hets+1))
    // We use the log of the first expression as the Newton's method f'(x) when
    // we're jumping to higher curr_homr, and the negative-log of the second
    // expression when we're jumping to lower curr_homr.
    // f''(x) is always negative, so we can aim for starting_lnprob instead of
    // the middle of the interval.

    // curr_hets moves twice as fast as curr_homr.  So if we add
    //   0.5 * (curr_hets + S_CAST(double, obs_hets)) - modal_nhet
    // to 0.5 * (curr_homr + S_CAST(double, obs_homr)), that reflects curr_homr
    // across the mode.
    const double max_homr = S_CAST(double, rare_ct / 2);
    {
      const double delta = 0.5 * (curr_hets + S_CAST(double, obs_hets)) - modal_nhet;
      curr_homr = 0.5 * (curr_homr + S_CAST(double, obs_homr)) + delta;
      // Round up (to guarantee we've actually moved to the other side of the
      // mode) and clamp.
      curr_homr = S_CAST(double, S_CAST(int32_t, curr_homr + 1));
      if (curr_homr > max_homr) {
        curr_homr = max_homr;
      }
    }
    // starting_lnprob_other_component is guaranteed to be positive here.
    // Lfact() should be accurate enough for us to use a smaller-than-usual
    // epsilon for identifying ties here; this isn't enough to cancel out the
    // effective increase in epsilon from working in log-space, but it helps.
    const double max_diff = starting_lnprob_other_component * (kSmallEpsilon / 8);
    while (1) {
      curr_hets = rare_ctd - curr_homr * 2;
      curr_homc = curr_homr + c_minus_r;
      const double lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
      const double lnprob_diff = lnprob_other_component - starting_lnprob_other_component;
      if (lnprob_diff > max_diff) {
        if (curr_homr >= max_homr) {
          // All terms on this tail are larger than the starting term.  Exit.
          // (This is possible when obs_hom1 == obs_hom2 == 0.)
          if (midp) {
            tailp -= 0.5;
          }
          return starting_lnprob + log(tailp);
        }
        const double ll_deriv = log(curr_hets * (curr_hets - 1) / (4 * (curr_homr + 1) * (curr_homc + 1)));
        // Round absolute value up, to guarantee that we make progress.
        // (lnprob_diff is positive and ll_deriv is negative.)
        // This may overshoot.  But the function is guaranteed to terminate
        // because we never overshoot (and we do always make progress on each
        // step) once we're on the other side.
        curr_homr += 1 - S_CAST(int64_t, (1 - kSmallEpsilon) * lnprob_diff / ll_deriv);
        if (curr_homr > max_homr) {
          curr_homr = max_homr;
        }
      } else if (lnprob_diff > -62 * kLn2) {
        lastp = exp(lnprob_diff);
        break;
      } else {
        const double ll_deriv = log((curr_hets + 2) * (curr_hets + 1) / (4 * curr_homr * curr_homc));
        // Round down, to guarantee we don't overshoot.
        // We're guaranteed to make progress, since lnprob_diff >= 62 * log(2)
        // and sample_ct < 2^31.
        curr_homr -= S_CAST(int64_t, lnprob_diff / ll_deriv);
        if (curr_homr < 0) {
          curr_homr = 0;
        }
      }
    }
    // Sum toward center, until lastp >= 1 or we're about to double-count the
    // starting cell (possible when midp true).
    double lastp_tail = lastp;
    if (lastp < 1 - kSmallEpsilon) {
      const double starting_homr_p1 = obs_homr + 1;
      double curr_homr_center = curr_homr;
      double curr_homc_center = curr_homc;
      double curr_hets_center = curr_hets;
      while (curr_homr_center > starting_homr_p1) {
        tailp += lastp;
        curr_hets_center += 2;
        lastp *= (4 * curr_homr_center * curr_homc_center) / (curr_hets_center * (curr_hets_center - 1));
        if (lastp >= 1 - kSmallEpsilon) {
          break;
        }
        curr_homr_center -= 1;
        curr_homc_center -= 1;
      }
    }
    if ((lastp >= 1 - kSmallEpsilon) && (lastp < 1 + kSmallEpsilon)) {
      tailp += lastp;
      ++tie_ct;
    }
    // Sum away from center, until sums stop changing.
    while (1) {
      curr_homr += 1;
      curr_homc += 1;
      lastp_tail *= (curr_hets * (curr_hets - 1)) / (4 * curr_homr * curr_homc);
      const double preaddp = tailp;
      tailp += lastp_tail;
      if (tailp <= preaddp) {
        break;
      }
      curr_hets -= 2;
    }
    if (midp) {
      tailp -= S_CAST(double, tie_ct) * 0.5;
    }
    return starting_lnprob + log(tailp);
  }
  // Same as above, just with directions flipped.
  const double het_delta = modal_nhet - curr_hets;
  if ((!midp) && (het_delta < 2.0)) {
    // Fast path for p=1.
    if ((4 * (1 - kSmallEpsilon)) * curr_homr * curr_homc <= (curr_hets + 2) * (curr_hets + 1)) {
      return 0;
    }
  }
  const double common_lnprob_component = Lfact(sample_ctd) + Lfact(rare_ctd) + Lfact(allele_ctd - rare_ctd) - Lfact(allele_ctd);
  const double starting_lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
  const double starting_lnprob = common_lnprob_component + starting_lnprob_other_component;
  double lastp = 1;
  if (starting_lnprob > -10 * kLn2) {
    double centerp = 0;
    // rescale this at the end.
    while (curr_homr > 0.5) {
      curr_hets += 2;
      lastp *= 4 * curr_homr * curr_homc / (curr_hets * (curr_hets - 1));
      curr_homr -= 1;
      curr_homc -= 1;
      if (lastp < 1 + kSmallEpsilon) {
        tie_ct += (lastp > 1 - kSmallEpsilon);
        break;
      }
      centerp += lastp;
    }
    if (midp) {
      centerp += S_CAST(double, tie_ct) * 0.5;
    }
    const double starting_prob = exp(starting_lnprob);
    centerp *= starting_prob;
    return log(1 - centerp);
  }
  double tailp = 1;
  while (curr_hets > 1.5) {
    curr_homr += 1;
    curr_homc += 1;
    lastp *= curr_hets * (curr_hets - 1) / (4 * curr_homr * curr_homc);
    curr_hets -= 2;
    const double preaddp = tailp;
    tailp += lastp;
    if (tailp <= preaddp) {
      break;
    }
  }
  // Jump to other tail.
  {
    const double delta = modal_nhet - 0.5 * (curr_hets + S_CAST(double, obs_hets));
    curr_homr = 0.5 * (curr_homr + S_CAST(double, obs_homr)) - delta;
    // Round down (to guarantee we've actually moved to the other side of the
    // mode) and clamp.
    curr_homr = S_CAST(double, S_CAST(int32_t, curr_homr));
    if (curr_homr < 0) {
      curr_homr = 0;
    }
  }
  const double max_diff = starting_lnprob_other_component * (kSmallEpsilon / 8);
  const double max_homr = S_CAST(double, rare_ct / 2);
  while (1) {
    curr_hets = rare_ctd - curr_homr * 2;
    curr_homc = curr_homr + c_minus_r;
    const double lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
    const double lnprob_diff = lnprob_other_component - starting_lnprob_other_component;
    if (lnprob_diff > max_diff) {
      if (curr_homr <= 0) {
        // All terms on this tail are larger than the starting term.  Exit.
        if (midp) {
          tailp -= 0.5;
        }
        return starting_lnprob + log(tailp);
      }
      const double ll_deriv = log(4 * curr_homr * curr_homc / ((curr_hets + 2) * (curr_hets + 1)));
      curr_homr += S_CAST(int64_t, (1 - kSmallEpsilon) * lnprob_diff / ll_deriv) - 1;
      if (curr_homr < 0) {
        curr_homr = 0;
      }
    } else if (lnprob_diff > -62 * kLn2) {
      lastp = exp(lnprob_diff);
      break;
    } else {
      const double ll_deriv = log(4 * (curr_homr + 1) * (curr_homc + 1) / (curr_hets * (curr_hets - 1)));
      curr_homr += S_CAST(int64_t, lnprob_diff / ll_deriv);
      if (curr_homr > max_homr) {
        curr_homr = max_homr;
      }
    }
  }
  // Sum toward center, until lastp >= 1 or we're about to double-count the
  // starting cell.
  double lastp_tail = lastp;
  if (lastp < 1 - kSmallEpsilon) {
    const double starting_homr_m1 = obs_homr - 1;
    double curr_homr_center = curr_homr;
    double curr_homc_center = curr_homc;
    double curr_hets_center = curr_hets;
    while (curr_homr_center < starting_homr_m1) {
      tailp += lastp;
      curr_homr_center += 1;
      curr_homc_center += 1;
      lastp *= curr_hets_center * (curr_hets_center - 1) / (4 * curr_homr_center * curr_homc_center);
      if (lastp >= 1 - kSmallEpsilon) {
        break;
      }
      curr_hets_center -= 2;
    }
  }
  if ((lastp >= 1 - kSmallEpsilon) && (lastp < 1 + kSmallEpsilon)) {
    tailp += lastp;
    ++tie_ct;
  }
  // Sum away from center, until sums stop changing.
  while (1) {
    curr_hets += 2;
    lastp_tail *= 4 * curr_homr * curr_homc / (curr_hets * (curr_hets - 1));
    const double preaddp = tailp;
    tailp += lastp_tail;
    if (tailp <= preaddp) {
      break;
    }
    curr_homr -= 1;
    curr_homc -= 1;
  }
  if (midp) {
    tailp -= S_CAST(double, tie_ct) * 0.5;
  }
  return starting_lnprob + log(tailp);
}

uint32_t HweThresh(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, double thresh) {
  // Threshold-test-only version of HweP() which is usually able to exit
  // from the calculation earlier.  Returns 0 if these counts are close enough
  // to Hardy-Weinberg equilibrium, 1 otherwise.
  //
  // Suppose, for definiteness, that the number of observed hets is no less
  // than expectation.  (Same ideas apply for the other case.)  We proceed as
  // follows:
  // - Sum the *relative* likelihoods of more likely smaller het counts.
  // - Determine the minimum tail mass to pass the threshold.
  // - The majority of the time, the tail boundary elements are enough to pass
  //   the threshold; we never need to sum the remainder of the tails.
  // - And in the case of disequilibrium, we will often be able to immediately
  //   determine that the tail sum cannot possibly pass the threshold, just by
  //   looking at the tail boundary elements and using a geometric series to
  //   upper-bound the tail sums.
  // - Only when neither of these conditions hold do we start traveling down
  //   the tails.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  int64_t rare_copies = 2LL * obs_homr + obs_hets;
  int64_t genotypes2 = (obs_hets + obs_homc + obs_homr) * 2LL;
  double curr_hets_t2 = obs_hets;  // tail 2
  double curr_homr_t2 = obs_homr;
  double curr_homc_t2 = obs_homc;

  // Subtract epsilon from initial probability mass, so that we can compare to
  // 1 when determining tail vs. center membership without floating point error
  // biting us in the ass
  double tailp1 = (1 - kSmallEpsilon) * kExactTestBias;
  double centerp = 0;
  double lastp2 = tailp1;
  double tailp2 = 0;
  double tail1_ceil;
  double tail2_ceil;
  double lastp1;
  double curr_hets_t1;
  double curr_homr_t1;
  double curr_homc_t1;

  // Initially, if center sum reaches this, the test can immediately fail.
  // Once center is summed, this is recalculated, and when tail sum has reached
  // this, we've passed.
  double exit_thresh;
  double exit_threshx;
  double ratio;
  double preaddp;
  if (!genotypes2) {
    return 0;
  }

  // Convert thresh into reverse odds ratio.
  thresh = (1 - thresh) / thresh;

  // Expected het count:
  //   2 * rarefreq * (1 - rarefreq) * genotypes
  // = 2 * (rare_copies / (2 * genotypes)) * (1 - rarefreq) * genotypes
  // = rare_copies * (1 - (rare_copies / (2 * genotypes)))
  // = (rare_copies * (2 * genotypes - rare_copies)) / (2 * genotypes)
  //
  // The computational identity is
  //   P(nhets == n) := P(nhets == n+2) * (n+2) * (n+1) /
  //                    (4 * homr(n) * homc(n))
  // where homr() and homc() are the number of homozygous rares/commons needed
  // to maintain the same allele frequencies.
  // This probability is always decreasing when proceeding away from the
  // expected het count.

  if (obs_hets * genotypes2 > rare_copies * (genotypes2 - rare_copies)) {
    // tail 1 = upper
    if (obs_hets < 2) {
      return 0;
    }

    // An initial upper bound on the tail sum is useful, since it lets us
    // report test failure before summing the entire center.  We use the
    // trivial bound of 1 + floor(rare_copies / 2): that's the total number
    // of possible het counts, and the relative probability for each count must
    // be <= 1 if it's in the tail.
    exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);

    // het_probs[curr_hets] = 1
    // het_probs[curr_hets - 2] = het_probs[curr_hets] * curr_hets * (curr_hets - 1) / (4 * (curr_homr + 1) * (curr_homc + 1))
    do {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      curr_hets_t2 -= 2;
      if (lastp2 < kExactTestBias) {
        tailp2 = lastp2;
        break;
      }
      centerp += lastp2;
      if (centerp > exit_thresh) {
        return 1;
      }
    } while (curr_hets_t2 > 1.5);
    exit_thresh = centerp / thresh;
    if (tailp1 + tailp2 >= exit_thresh) {
      return 0;
    }
    // c + cr + cr^2 + ... = c/(1-r), which is an upper bound for the tail sum
    ratio = (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * (curr_homr_t2 + 1) * (curr_homc_t2 + 1));
    tail2_ceil = tailp2 / (1 - ratio);
    curr_hets_t1 = obs_hets + 2;
    curr_homr_t1 = obs_homr;
    curr_homc_t1 = obs_homc;
    // ratio for the other tail
    lastp1 = (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
    tail1_ceil = tailp1 / (1 - lastp1);
    if (tail1_ceil + tail2_ceil < exit_thresh) {
      return 1;
    }
    lastp1 *= tailp1;
    tailp1 += lastp1;

    if (obs_homr > 1) {
      // het_probs[curr_hets + 2] = het_probs[curr_hets] * 4 * curr_homr * curr_homc / ((curr_hets + 2) * (curr_hets + 1))
      exit_threshx = exit_thresh - tailp2;
      do {
        curr_hets_t1 += 2;
        curr_homr_t1 -= 1;
        curr_homc_t1 -= 1;
        lastp1 *= (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
        preaddp = tailp1;
        tailp1 += lastp1;
        if (tailp1 > exit_threshx) {
          return 0;
        }
        if (tailp1 <= preaddp) {
          break;
        }
      } while (curr_homr_t1 > 1.5);
    }
    if (tailp1 + tail2_ceil < exit_thresh) {
      return 1;
    }
    exit_threshx = exit_thresh - tailp1;
    while (curr_hets_t2 > 1) {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      preaddp = tailp2;
      tailp2 += lastp2;
      if (tailp2 >= exit_threshx) {
        return 0;
      }
      if (tailp2 <= preaddp) {
        return 1;
      }
      curr_hets_t2 -= 2;
    }
    return 1;
  }
  // tail 1 = lower
  if (!obs_homr) {
    return 0;
  }
  exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);
  do {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    if (lastp2 < kExactTestBias) {
      tailp2 = lastp2;
      break;
    }
    centerp += lastp2;
    if (centerp > exit_thresh) {
      return 1;
    }
  } while (curr_homr_t2 > 0.5);
  exit_thresh = centerp / thresh;
  if (tailp1 + tailp2 >= exit_thresh) {
    return 0;
  }
  ratio = (4 * curr_homr_t2 * curr_homc_t2) / ((curr_hets_t2 + 2) * (curr_hets_t2 + 1));
  tail2_ceil = tailp2 / (1 - ratio);
  curr_hets_t1 = obs_hets;
  curr_homr_t1 = obs_homr + 1;
  curr_homc_t1 = obs_homc + 1;
  lastp1 = (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
  tail1_ceil = tailp1 / (1 - lastp1);
  lastp1 *= tailp1;
  tailp1 += lastp1;

  if (tail1_ceil + tail2_ceil < exit_thresh) {
    return 1;
  }
  if (obs_hets >= 4) {
    exit_threshx = exit_thresh - tailp2;
    do {
      curr_hets_t1 -= 2;
      curr_homr_t1 += 1;
      curr_homc_t1 += 1;
      lastp1 *= (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
      preaddp = tailp1;
      tailp1 += lastp1;
      if (tailp1 > exit_threshx) {
        return 0;
      }
      if (tailp1 <= preaddp) {
        break;
      }
    } while (curr_hets_t1 > 3.5);
  }
  if (tailp1 + tail2_ceil < exit_thresh) {
    return 1;
  }
  exit_threshx = exit_thresh - tailp1;
  while (curr_homr_t2 > 0.5) {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    preaddp = tailp2;
    tailp2 += lastp2;
    if (tailp2 >= exit_threshx) {
      return 0;
    }
    if (tailp2 <= preaddp) {
      return 1;
    }
  }
  return 1;
}

uint32_t HweThreshMidp(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, double thresh) {
  // Mid-p version of HweThresh().  (There are enough fiddly differences that I
  // think it's better for this to be a separate function.)  Assumes threshold
  // is smaller than 0.5.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  int64_t rare_copies = 2LL * obs_homr + obs_hets;
  int64_t genotypes2 = (obs_hets + obs_homc + obs_homr) * 2LL;
  double curr_hets_t2 = obs_hets;  // tail 2
  double curr_homr_t2 = obs_homr;
  double curr_homc_t2 = obs_homc;
  double tailp1 = (1 - kSmallEpsilon) * kExactTestBias * 0.5;
  double centerp = tailp1;
  double lastp2 = (1 - kSmallEpsilon) * kExactTestBias;
  double tailp2 = 0;
  double tail1_ceil;
  double tail2_ceil;
  double lastp1;
  double curr_hets_t1;
  double curr_homr_t1;
  double curr_homc_t1;
  double exit_thresh;
  double exit_threshx;
  double ratio;
  double preaddp;
  if (!genotypes2) {
    return 0;
  }
  thresh = (1 - thresh) / thresh;
  if (obs_hets * genotypes2 > rare_copies * (genotypes2 - rare_copies)) {
    if (obs_hets < 2) {
      return 0;
    }
    exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);
    do {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      curr_hets_t2 -= 2;
      if (lastp2 < kExactTestBias) {
        if (lastp2 > (1 - 2 * kSmallEpsilon) * kExactTestBias) {
          // tie with original contingency table, apply mid-p correction here
          // too
          tailp2 = tailp1;
          centerp += tailp1;
        } else {
          tailp2 = lastp2;
        }
        break;
      }
      centerp += lastp2;
      if (centerp > exit_thresh) {
        return 1;
      }
    } while (curr_hets_t2 > 1.5);
    exit_thresh = centerp / thresh;
    if (tailp1 + tailp2 >= exit_thresh) {
      return 0;
    }
    ratio = (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * (curr_homr_t2 + 1) * (curr_homc_t2 + 1));
    // this needs to work in both the tie and no-tie cases
    tail2_ceil = tailp2 + lastp2 * ratio / (1 - ratio);
    curr_hets_t1 = obs_hets + 2;
    curr_homr_t1 = obs_homr;
    curr_homc_t1 = obs_homc;
    lastp1 = (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
    // always a tie here
    tail1_ceil = tailp1 * 2 / (1 - lastp1) - tailp1;
    if (tail1_ceil + tail2_ceil < exit_thresh) {
      return 1;
    }
    lastp1 *= tailp1 * 2;
    tailp1 += lastp1;

    if (obs_homr > 1) {
      exit_threshx = exit_thresh - tailp2;
      do {
        curr_hets_t1 += 2;
        curr_homr_t1 -= 1;
        curr_homc_t1 -= 1;
        lastp1 *= (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
        preaddp = tailp1;
        tailp1 += lastp1;
        if (tailp1 > exit_threshx) {
          return 0;
        }
        if (tailp1 <= preaddp) {
          break;
        }
      } while (curr_homr_t1 > 1.5);
    }
    if (tailp1 + tail2_ceil < exit_thresh) {
      return 1;
    }
    exit_threshx = exit_thresh - tailp1;
    while (curr_hets_t2 > 1) {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      preaddp = tailp2;
      tailp2 += lastp2;
      if (tailp2 >= exit_threshx) {
        return 0;
      }
      if (tailp2 <= preaddp) {
        return 1;
      }
      curr_hets_t2 -= 2;
    }
    return 1;
  }
  if (!obs_homr) {
    return 0;
  }
  exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);
  do {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    if (lastp2 < kExactTestBias) {
      if (lastp2 > (1 - 2 * kSmallEpsilon) * kExactTestBias) {
        tailp2 = tailp1;
        centerp += tailp1;
      } else {
        tailp2 = lastp2;
      }
      break;
    }
    centerp += lastp2;
    if (centerp > exit_thresh) {
      return 1;
    }
  } while (curr_homr_t2 > 0.5);
  exit_thresh = centerp / thresh;
  if (tailp1 + tailp2 >= exit_thresh) {
    return 0;
  }
  ratio = (4 * curr_homr_t2 * curr_homc_t2) / ((curr_hets_t2 + 2) * (curr_hets_t2 + 1));
  tail2_ceil = tailp2 + lastp2 * ratio / (1 - ratio);
  curr_hets_t1 = obs_hets;
  curr_homr_t1 = obs_homr + 1;
  curr_homc_t1 = obs_homc + 1;
  lastp1 = (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
  tail1_ceil = 2 * tailp1 / (1 - lastp1) - tailp1;
  lastp1 *= 2 * tailp1;
  tailp1 += lastp1;

  if (tail1_ceil + tail2_ceil < exit_thresh) {
    return 1;
  }
  if (obs_hets >= 4) {
    exit_threshx = exit_thresh - tailp2;
    do {
      curr_hets_t1 -= 2;
      curr_homr_t1 += 1;
      curr_homc_t1 += 1;
      lastp1 *= (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
      preaddp = tailp1;
      tailp1 += lastp1;
      if (tailp1 > exit_threshx) {
        return 0;
      }
      if (tailp1 <= preaddp) {
        break;
      }
    } while (curr_hets_t1 > 3.5);
  }
  if (tailp1 + tail2_ceil < exit_thresh) {
    return 1;
  }
  exit_threshx = exit_thresh - tailp1;
  while (curr_homr_t2 > 0.5) {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    preaddp = tailp2;
    tailp2 += lastp2;
    if (tailp2 >= exit_threshx) {
      return 0;
    }
    if (tailp2 <= preaddp) {
      return 1;
    }
  }
  return 1;
}

uint32_t HweThreshLnMain(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, uint32_t midp, double ln_thresh) {
  assert(ln_thresh < -708);
  // Threshold-test-only version of HweLnP() which is usually able to exit
  // from the calculation earlier.  Returns 0 if these counts are close enough
  // to Hardy-Weinberg equilibrium, 1 otherwise.
  //
  // Assumes ln_thresh < -708.
  //
  // Caller is responsible for including a tolerance in ln_thresh when
  // appropriate; the only tolerance applied by this function is in the context
  // of other-tail tie detection.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  const int64_t rare_ct = 2LL * obs_homr + obs_hets;
  // Change this to "rare_ct < 2" if ln_thresh restriction is being loosened
  // (to e.g. compare results against HweThresh()).
  // TODO: also look at |obs_hets - modal_nhet|, bsru32(sample_ct)
  if (rare_ct < 64) {
    return 0;
  }
  const int64_t sample_ct = obs_hom1 + obs_hom2 + obs_hets;
  const double rare_ctd = rare_ct;
  const double sample_ctd = sample_ct;
  const double allele_ctd = sample_ctd * 2;

  // 1. Compute log-likelihood of test cell.  This may be high enough on its
  //    own to immediately return 0.
  //    If likelihood is lower than threshold / <total # of cells>, we can
  //    immediately return 1.
  // 2. Determine tailsum we must hit to return 0.
  // 3. The rest follows HweLnP(), except with an extra geometric-series-based
  //    early-exit attempt near the end.
  const double common_lnprob_component = Lfact(sample_ctd) + Lfact(rare_ctd) + Lfact(allele_ctd - rare_ctd) - Lfact(allele_ctd);
  double curr_hets = obs_hets;
  double curr_homr = obs_homr;
  double curr_homc = obs_homc;
  const double starting_lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
  const double starting_lnprob = common_lnprob_component + starting_lnprob_other_component;
  const double midp_d = u31tod(midp);
  if (ln_thresh <= starting_lnprob - midp_d * kLn2) {
    return 0;
  }
  const double max_homr = S_CAST(double, rare_ct / 2);
  if (ln_thresh > starting_lnprob + log(max_homr + 1 - midp_d * 0.5)) {
    return 1;
  }

  const double maf = rare_ctd / allele_ctd;
  const double modal_nhet = rare_ctd * (1 - maf);
  const double c_minus_r = curr_homc - curr_homr;
  // This should be in (0.5, 2^30].
  const double tail_thresh = exp(ln_thresh - starting_lnprob);
  double tailp = 1 - midp_d * 0.5;
  double lastp = 1;
  if (curr_hets > modal_nhet) {
    // No center-sum (or p=1) code path, since it doesn't make sense to choose
    // a HWE threshold that makes these relevant; we should have already
    // returned 0.
    // (So we can't assume obs_hets >= 2.)
    curr_hets += 2;
    while (curr_homr > 0.5) {
      lastp *= (4 * curr_homr * curr_homc) / (curr_hets * (curr_hets - 1));
      const double preaddp = tailp;
      tailp += lastp;
      if (tailp <= preaddp) {
        break;
      }
      if (tailp >= tail_thresh) {
        return 0;
      }
      curr_hets += 2;
      curr_homr -= 1;
      curr_homc -= 1;
    }
    {
      const double delta = 0.5 * (curr_hets + S_CAST(double, obs_hets)) - modal_nhet;
      curr_homr = 0.5 * (curr_homr + S_CAST(double, obs_homr)) + delta;
      curr_homr = S_CAST(double, S_CAST(int32_t, curr_homr + 0.5));
      if (curr_homr > max_homr) {
        curr_homr = max_homr;
      }
    }
    const double max_diff = starting_lnprob_other_component * (kSmallEpsilon / 8);
    while (1) {
      curr_hets = rare_ctd - curr_homr * 2;
      curr_homc = curr_homr + c_minus_r;
      const double lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
      const double lnprob_diff = lnprob_other_component - starting_lnprob_other_component;
      if (lnprob_diff > max_diff) {
        if (curr_homr >= max_homr) {
          return 1;
        }
        const double ll_deriv = log(curr_hets * (curr_hets - 1) / (4 * (curr_homr + 1) * (curr_homc + 1)));
        curr_homr += 1 - S_CAST(int64_t, (1 - kSmallEpsilon) * lnprob_diff / ll_deriv);
        if (curr_homr > max_homr) {
          curr_homr = max_homr;
        }
      } else if (lnprob_diff > -62 * kLn2) {
        lastp = exp(lnprob_diff);
        break;
      } else {
        const double ll_deriv = log((curr_hets + 2) * (curr_hets + 1) / (4 * curr_homr * curr_homc));
        curr_homr -= S_CAST(int64_t, lnprob_diff / ll_deriv);
        if (curr_homr < 0) {
          curr_homr = 0;
        }
      }
    }
    double lastp_tail = lastp;
    if (lastp < 1 - kSmallEpsilon) {
      double curr_homr_center = curr_homr;
      double curr_homc_center = curr_homc;
      double curr_hets_center = curr_hets;
      while (1) {
        tailp += lastp;
        if (tailp >= tail_thresh) {
          return 0;
        }
        curr_hets_center += 2;
        lastp *= (4 * curr_homr_center * curr_homc_center) / (curr_hets_center * (curr_hets_center - 1));
        if (lastp >= 1 - kSmallEpsilon) {
          break;
        }
        curr_homr_center -= 1;
        curr_homc_center -= 1;
      }
    }
    if (lastp < 1 + kSmallEpsilon) {
      tailp += lastp * (1 - midp_d * 0.5);
      if (tailp >= tail_thresh) {
        return 0;
      }
    }
    // We're down to one tail that can be tightly bounded by a geometric
    // series.  (ratio is always decreasing)
    // c + cr + cr^2 + ... = c/(1-r)
    curr_homr += 1;
    curr_homc += 1;
    const double cur_ratio = (curr_hets * (curr_hets - 1)) / (4 * curr_homr * curr_homc);
    lastp_tail *= cur_ratio;
    const double remaining_ceil = lastp_tail / (1 - cur_ratio);
    if (tailp + remaining_ceil < tail_thresh) {
      return 1;
    }
    while (1) {
      const double preaddp = tailp;
      tailp += lastp_tail;
      if (tailp <= preaddp) {
        return 1;
      }
      if (tailp >= tail_thresh) {
        return 0;
      }
      curr_hets -= 2;
      curr_homr += 1;
      curr_homc += 1;
      lastp_tail *= (curr_hets * (curr_hets - 1)) / (4 * curr_homr * curr_homc);
    }
    // unreachable
  }
  while (curr_hets > 1.5) {
    curr_homr += 1;
    curr_homc += 1;
    lastp *= curr_hets * (curr_hets - 1) / (4 * curr_homr * curr_homc);
    curr_hets -= 2;
    const double preaddp = tailp;
    tailp += lastp;
    if (tailp <= preaddp) {
      break;
    }
    if (tailp >= tail_thresh) {
      return 0;
    }
  }
  {
    const double delta = modal_nhet - 0.5 * (curr_hets + S_CAST(double, obs_hets));
    curr_homr = 0.5 * (curr_homr + S_CAST(double, obs_homr)) - delta;
    curr_homr = S_CAST(double, S_CAST(int32_t, curr_homr + 0.5));
    if (curr_homr < 0) {
      curr_homr = 0;
    }
  }
  const double max_diff = starting_lnprob_other_component * (kSmallEpsilon / 8);
  while (1) {
    curr_hets = rare_ctd - curr_homr * 2;
    curr_homc = curr_homr + c_minus_r;
    const double lnprob_other_component = curr_hets * kLn2 - Lfact(curr_hets) - Lfact(curr_homr) - Lfact(curr_homc);
    const double lnprob_diff = lnprob_other_component - starting_lnprob_other_component;
    if (lnprob_diff > max_diff) {
      if (curr_homr <= 0) {
        return 1;
      }
      const double ll_deriv = log(4 * curr_homr * curr_homc / ((curr_hets + 2) * (curr_hets + 1)));
      curr_homr += S_CAST(int64_t, (1 - kSmallEpsilon) * lnprob_diff / ll_deriv) - 1;
      if (curr_homr < 0) {
        curr_homr = 0;
      }
    } else if (lnprob_diff > -62 * kLn2) {
      lastp = exp(lnprob_diff);
      break;
    } else {
      const double ll_deriv = log(4 * (curr_homr + 1) * (curr_homc + 1) / (curr_hets * (curr_hets - 1)));
      curr_homr += S_CAST(int64_t, lnprob_diff / ll_deriv);
      if (curr_homr > max_homr) {
        curr_homr = max_homr;
      }
    }
  }
  double lastp_tail = lastp;
  if (lastp < 1 - kSmallEpsilon) {
    double curr_homr_center = curr_homr;
    double curr_homc_center = curr_homc;
    double curr_hets_center = curr_hets;
    while (1) {
      tailp += lastp;
      if (tailp >= tail_thresh) {
        return 0;
      }
      curr_homr_center += 1;
      curr_homc_center += 1;
      lastp *= curr_hets_center * (curr_hets_center - 1) / (4 * curr_homr_center * curr_homc_center);
      if (lastp >= 1 - kSmallEpsilon) {
        break;
      }
      curr_hets_center -= 2;
    }
  }
  if (lastp < 1 + kSmallEpsilon) {
    tailp += lastp * (1 - midp_d * 0.5);
    if (tailp >= tail_thresh) {
      return 0;
    }
  }
  curr_hets += 2;
  const double cur_ratio = 4 * curr_homr * curr_homc / (curr_hets * (curr_hets - 1));
  lastp_tail *= cur_ratio;
  const double remaining_ceil = lastp_tail / (1 - cur_ratio);
  if (tailp + remaining_ceil < tail_thresh) {
    return 1;
  }
  while (1) {
    const double preaddp = tailp;
    tailp += lastp_tail;
    if (tailp <= preaddp) {
      return 1;
    }
    if (tailp >= tail_thresh) {
      return 0;
    }
    curr_homr -= 1;
    curr_homc -= 1;
    curr_hets += 2;
    lastp_tail *= 4 * curr_homr * curr_homc / (curr_hets * (curr_hets - 1));
  }
}

// 2^{-40} for now, since 2^{-44} was too small on real data
static const double kExactTestEpsilon2 = 0.0000000000009094947017729282379150390625;

double FisherExact2x2P(uint32_t m11, uint32_t m12, uint32_t m21, uint32_t m22, uint32_t midp) {
  // Basic 2x2 Fisher exact test p-value calculation.
  double tprob = (1 - kExactTestEpsilon2) * kExactTestBias;
  double cur_prob = tprob;
  double cprob = 0;
  int32_t tie_ct = 1;
  uint32_t uii;
  double cur11;
  double cur12;
  double cur21;
  double cur22;
  double preaddp;
  // Ensure we are left of the distribution center, m11 <= m22, and m12 <= m21.
  if (m12 > m21) {
    uii = m12;
    m12 = m21;
    m21 = uii;
  }
  if (m11 > m22) {
    uii = m11;
    m11 = m22;
    m22 = uii;
  }
  if ((S_CAST(uint64_t, m11) * m22) > (S_CAST(uint64_t, m12) * m21)) {
    uii = m11;
    m11 = m12;
    m12 = uii;
    uii = m21;
    m21 = m22;
    m22 = uii;
  }
  cur11 = m11;
  cur12 = m12;
  cur21 = m21;
  cur22 = m22;
  while (cur12 > 0.5) {
    cur11 += 1;
    cur22 += 1;
    cur_prob *= (cur12 * cur21) / (cur11 * cur22);
    cur12 -= 1;
    cur21 -= 1;
    if (cur_prob > DBL_MAX) {
      return 0;
    }
    if (cur_prob < kExactTestBias) {
      if (cur_prob > (1 - 2 * kExactTestEpsilon2) * kExactTestBias) {
        tie_ct++;
      }
      tprob += cur_prob;
      break;
    }
    cprob += cur_prob;
  }
  if ((cprob == 0) && (!midp)) {
    return 1;
  }
  while (cur12 > 0.5) {
    cur11 += 1;
    cur22 += 1;
    cur_prob *= (cur12 * cur21) / (cur11 * cur22);
    cur12 -= 1;
    cur21 -= 1;
    preaddp = tprob;
    tprob += cur_prob;
    if (tprob <= preaddp) {
      break;
    }
  }
  if (m11) {
    cur11 = m11;
    cur12 = m12;
    cur21 = m21;
    cur22 = m22;
    cur_prob = (1 - kExactTestEpsilon2) * kExactTestBias;
    do {
      cur12 += 1;
      cur21 += 1;
      cur_prob *= (cur11 * cur22) / (cur12 * cur21);
      cur11 -= 1;
      cur22 -= 1;
      preaddp = tprob;
      tprob += cur_prob;
      if (tprob <= preaddp) {
        if (!midp) {
          return preaddp / (cprob + preaddp);
        }
        return (preaddp - ((1 - kExactTestEpsilon2) * kExactTestBias * 0.5) * tie_ct) / (cprob + preaddp);
      }
    } while (cur11 > 0.5);
  }
  if (!midp) {
    return tprob / (cprob + tprob);
  }
  return (tprob - ((1 - kExactTestEpsilon2) * kExactTestBias * 0.5) * tie_ct) / (cprob + tprob);
}

void HweLnFirstRow(double hetab, double homa, double homb, double* tailp_ptr, double* starting_lnprob_ptr, uint32_t* tie_ct_ptr, double* orig_base_lnprobl_ptr, double* orig_base_lnprobr_ptr, double* orig_saved_lhets_ptr, double* orig_saved_lhoma_ptr, double* orig_saved_lhomb_ptr, double* orig_saved_rhets_ptr, double* orig_saved_rhoma_ptr, double* orig_saved_rhomb_ptr) {
  const double sample_ctd = hetab + homa + homb;
  const double a_ctd = hetab + 2 * homa;
  const double b_ctd = hetab + 2 * homb;
  const double allele_ctd = sample_ctd * 2;
  const double common_lnprob_component = Lfact(sample_ctd) + Lfact(a_ctd) + Lfact(b_ctd) - Lfact(allele_ctd);
  const double starting_lnprob_other_component = hetab * kLn2 - Lfact(hetab) - Lfact(homa) - Lfact(homb);
  const double starting_lnprob = common_lnprob_component + starting_lnprob_other_component;
  *starting_lnprob_ptr = starting_lnprob;
  double lastp = 1;
  uint32_t tie_ct = 1;
  double tmp_hets = hetab;
  if (hetab * hetab > 4 * homa * homb) {
    // Incrementing hetab decreases likelihood from this point on.
    *orig_base_lnprobr_ptr = 0;
    *orig_saved_rhets_ptr = hetab;
    *orig_saved_rhoma_ptr = homa;
    *orig_saved_rhomb_ptr = homb;
    if (starting_lnprob > -10 * kLn2) {
      // Sum center instead of tail.
      double centerp = 0;
      double tmp_homa = homa;
      double tmp_homb = homb;
      while (tmp_hets > 1.5) {
        tmp_homa += 1;
        tmp_homb += 1;
        lastp *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_homa * tmp_homb);
        tmp_hets -= 2;
        if (lastp < 1 + kSmallEpsilon) {
          tie_ct += (lastp > 1 - kSmallEpsilon);
          break;
        }
        centerp += lastp;
      }
      *tie_ct_ptr = tie_ct;
      *orig_saved_lhets_ptr = tmp_hets;
      *orig_saved_lhoma_ptr = tmp_homa;
      *orig_saved_lhomb_ptr = tmp_homb;
      *orig_base_lnprobl_ptr = log(lastp);
      *tailp_ptr = exp(-starting_lnprob) - centerp;
      return;
    }
    double* orig_saved_lhomr_ptr = orig_saved_lhoma_ptr;
    double* orig_saved_lhomc_ptr = orig_saved_lhomb_ptr;
    double rare_ctd = a_ctd;
    double c_minus_r = (b_ctd - a_ctd) * 0.5;
    double tmp_homr = homa;
    double tmp_homc = homb;
    if (c_minus_r < 0.0) {
      orig_saved_lhomr_ptr = orig_saved_lhomb_ptr;
      orig_saved_lhomc_ptr = orig_saved_lhoma_ptr;
      rare_ctd = b_ctd;
      c_minus_r = -c_minus_r;
      tmp_homr = homb;
      tmp_homc = homa;
    }
    const double orig_homr = tmp_homr;
    double tailp = 1;
    while (tmp_homr > 0.5) {
      tmp_hets += 2;
      lastp *= (4 * tmp_homr * tmp_homc) / (tmp_hets * (tmp_hets - 1));
      tmp_homr -= 1;
      tmp_homc -= 1;
      const double preaddp = tailp;
      tailp += lastp;
      if (tailp <= preaddp) {
        break;
      }
    }
    const double maf = rare_ctd / allele_ctd;
    const double modal_nhet = rare_ctd * (1 - maf);
    const double max_homr = S_CAST(double, S_CAST(int32_t, rare_ctd * 0.5));
    {
      const double delta = 0.5 * (tmp_hets + hetab) - modal_nhet;
      // Round and clamp.
      tmp_homr = 0.5 * (tmp_homr + orig_homr) + delta;
      tmp_homr = S_CAST(double, S_CAST(int32_t, tmp_homr + 0.5));
      if (tmp_homr > max_homr) {
        tmp_homr = max_homr;
      }
    }
    const double max_diff = starting_lnprob_other_component * (kSmallEpsilon / 8);
    while (1) {
      tmp_hets = rare_ctd - tmp_homr * 2;
      tmp_homc = tmp_homr + c_minus_r;
      const double lnprob_other_component = tmp_hets * kLn2 - Lfact(tmp_hets) - Lfact(tmp_homr) - Lfact(tmp_homc);
      const double lnprob_diff = lnprob_other_component - starting_lnprob_other_component;
      if (lnprob_diff > max_diff) {
        if (tmp_homr >= max_homr) {
          // All terms on this tail are larger than the starting term.  Exit.
          assert(tmp_homr == max_homr);
          *tie_ct_ptr = 1;
          *orig_saved_lhets_ptr = tmp_hets;
          *orig_saved_lhomr_ptr = tmp_homr;
          *orig_saved_lhomc_ptr = tmp_homc;
          // exp(lnprob_diff) can be too large to fit in a double, so we make
          // this return value a log-probability.
          *orig_base_lnprobl_ptr = lnprob_diff;
          *tailp_ptr = tailp;
          return;
        }
        const double ll_deriv = log(tmp_hets * (tmp_hets - 1) / (4 * (tmp_homr + 1) * (tmp_homc + 1)));
        // Round absolute value up, to guarantee that we make progress.
        // (lnprob_diff is positive and ll_deriv is negative.)
        // This may overshoot.  But the function is guaranteed to terminate
        // because we never overshoot (and we do always make progress on each
        // step) once we're on the other side.
        tmp_homr += 1 - S_CAST(int64_t, (1 - kSmallEpsilon) * lnprob_diff / ll_deriv);
        if (tmp_homr > max_homr) {
          tmp_homr = max_homr;
        }
      } else if (lnprob_diff > -62 * kLn2) {
        lastp = exp(lnprob_diff);
        break;
      } else {
        const double ll_deriv = log((tmp_hets + 2) * (tmp_hets + 1) / (4 * tmp_homr * tmp_homc));
        // Round down, to guarantee we don't overshoot.
        // We're guaranteed to make progress, since lnprob_diff >= 62 * log(2)
        // and sample_ct < 2^31.
        tmp_homr -= S_CAST(int64_t, lnprob_diff / ll_deriv);
        if (tmp_homr < 0) {
          tmp_homr = 0;
        }
      }
    }
    // Sum toward center, until lastp >= 1...
    // ...or we have returned to the starting cell; this might be possible due
    // to floating-point error.  Unlike HweLnP(), we didn't early-exit when the
    // starting cell was a mode.
    double lastp_tail = lastp;
    {
      double tmp_homr_center = tmp_homr;
      double tmp_homc_center = tmp_homc;
      double tmp_hets_center = tmp_hets;
      while (tmp_homr_center > orig_homr) {
        if (lastp >= 1 - kSmallEpsilon) {
          if (lastp < 1 + kSmallEpsilon) {
            tailp += lastp;
            ++tie_ct;
          }
          break;
        }
        tailp += lastp;
        tmp_hets_center += 2;
        lastp *= (4 * tmp_homr_center * tmp_homc_center) / (tmp_hets_center * (tmp_hets_center - 1));
        tmp_homr_center -= 1;
        tmp_homc_center -= 1;
      }
      *tie_ct_ptr = tie_ct;
      *orig_saved_lhets_ptr = tmp_hets_center;
      *orig_saved_lhomr_ptr = tmp_homr_center;
      *orig_saved_lhomc_ptr = tmp_homc_center;
      *orig_base_lnprobl_ptr = log(lastp);
    }
    // Sum away from center, until sums stop changing.
    while (1) {
      tmp_homr += 1;
      tmp_homc += 1;
      lastp_tail *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_homr * tmp_homc);
      const double preaddp = tailp;
      tailp += lastp_tail;
      if (tailp <= preaddp) {
        break;
      }
      tmp_hets -= 2;
    }
    *tailp_ptr = tailp;
    return;
  }
  // Decrementing hetab decreases likelihood from this point on.
  *orig_base_lnprobl_ptr = 0;
  *orig_saved_lhets_ptr = hetab;
  *orig_saved_lhoma_ptr = homa;
  *orig_saved_lhomb_ptr = homb;
  if (starting_lnprob > -10 * kLn2) {
    const double tmp_hets_stop = MINV(a_ctd, b_ctd) - 1.5;
    double centerp = 0;
    double tmp_homa = homa;
    double tmp_homb = homb;
    while (tmp_hets < tmp_hets_stop) {
      tmp_hets += 2;
      lastp *= 4 * tmp_homa * tmp_homb / (tmp_hets * (tmp_hets - 1));
      tmp_homa -= 1;
      tmp_homb -= 1;
      if (lastp < 1 + kSmallEpsilon) {
        // TODO: fix this in a5.11, wrong parentheses in a5.10
        tie_ct += (lastp > 1 - kSmallEpsilon);
        break;
      }
      centerp += lastp;
    }
    *tie_ct_ptr = tie_ct;
    *orig_saved_rhets_ptr = tmp_hets;
    *orig_saved_rhoma_ptr = tmp_homa;
    *orig_saved_rhomb_ptr = tmp_homb;
    *orig_base_lnprobr_ptr = log(lastp);
    *tailp_ptr = exp(-starting_lnprob) - centerp;
    return;
  }
  double* orig_saved_rhomr_ptr = orig_saved_rhoma_ptr;
  double* orig_saved_rhomc_ptr = orig_saved_rhomb_ptr;
  double rare_ctd = a_ctd;
  double c_minus_r = (b_ctd - a_ctd) * 0.5;
  double tmp_homr = homa;
  double tmp_homc = homb;
  if (c_minus_r < 0.0) {
    orig_saved_rhomr_ptr = orig_saved_rhomb_ptr;
    orig_saved_rhomc_ptr = orig_saved_rhoma_ptr;
    rare_ctd = b_ctd;
    c_minus_r = -c_minus_r;
    tmp_homr = homb;
    tmp_homc = homa;
  }
  const double orig_homr = tmp_homr;
  double tailp = 1;
  while (tmp_hets > 1.5) {
    tmp_homr += 1;
    tmp_homc += 1;
    lastp *= tmp_hets * (tmp_hets - 1) / (4 * tmp_homr * tmp_homc);
    tmp_hets -= 2;
    const double preaddp = tailp;
    tailp += lastp;
    if (tailp <= preaddp) {
      break;
    }
  }
  // Jump to other tail.
  const double maf = rare_ctd / allele_ctd;
  const double modal_nhet = rare_ctd * (1 - maf);
  {
    const double delta = modal_nhet - 0.5 * (tmp_hets + hetab);
    tmp_homr = 0.5 * (tmp_homr + orig_homr) - delta;
    // Round and clamp.
    tmp_homr = S_CAST(double, S_CAST(int32_t, tmp_homr + 0.5));
    if (tmp_homr < 0) {
      tmp_homr = 0;
    }
  }
  const double max_diff = starting_lnprob_other_component * (kSmallEpsilon / 8);
  const double max_homr = S_CAST(double, S_CAST(int32_t, rare_ctd * 0.5));
  while (1) {
    tmp_hets = rare_ctd - tmp_homr * 2;
    tmp_homc = tmp_homr + c_minus_r;
    const double lnprob_other_component = tmp_hets * kLn2 - Lfact(tmp_hets) - Lfact(tmp_homr) - Lfact(tmp_homc);
    const double lnprob_diff = lnprob_other_component - starting_lnprob_other_component;
    if (lnprob_diff > max_diff) {
      if (tmp_homr <= 0) {
        // All terms on this tail are larger than the starting term.  Exit.
        assert(tmp_homr == 0.0);
        *tie_ct_ptr = 1;
        *orig_saved_rhets_ptr = tmp_hets;
        *orig_saved_rhomr_ptr = tmp_homr;
        *orig_saved_rhomc_ptr = tmp_homc;
        *orig_base_lnprobr_ptr = lnprob_diff;
        *tailp_ptr = tailp;
        return;
      }
      const double ll_deriv = log(4 * tmp_homr * tmp_homc / ((tmp_hets + 2) * (tmp_hets + 1)));
      tmp_homr += S_CAST(int64_t, (1 - kSmallEpsilon) * lnprob_diff / ll_deriv) - 1;
      if (tmp_homr < 0) {
        tmp_homr = 0;
      }
    } else if (lnprob_diff > -62 * kLn2) {
      lastp = exp(lnprob_diff);
      break;
    } else {
      const double ll_deriv = log(4 * (tmp_homr + 1) * (tmp_homc + 1) / (tmp_hets * (tmp_hets - 1)));
      tmp_homr += S_CAST(int64_t, lnprob_diff / ll_deriv);
      if (tmp_homr > max_homr) {
        tmp_homr = max_homr;
      }
    }
  }
  // Sum toward center, until lastp >= 1, or we have returned to the starting
  // cell.
  double lastp_tail = lastp;
  {
    double tmp_homr_center = tmp_homr;
    double tmp_homc_center = tmp_homc;
    double tmp_hets_center = tmp_hets;
    while (tmp_homr_center < orig_homr) {
      if (lastp >= 1 - kSmallEpsilon) {
        if (lastp < 1 + kSmallEpsilon) {
          tailp += lastp;
          ++tie_ct;
        }
        break;
      }
      tailp += lastp;
      tmp_homr_center += 1;
      tmp_homc_center += 1;
      lastp *= tmp_hets_center * (tmp_hets_center - 1) / (4 * tmp_homr_center * tmp_homc_center);
      tmp_hets_center -= 2;
    }
    *tie_ct_ptr = tie_ct;
    *orig_saved_rhets_ptr = tmp_hets_center;
    *orig_saved_rhomr_ptr = tmp_homr_center;
    *orig_saved_rhomc_ptr = tmp_homc_center;
    *orig_base_lnprobr_ptr = log(lastp);
  }
  // Sum away from center, until sums stop changing.
  while (1) {
    tmp_hets += 2;
    lastp_tail *= 4 * tmp_homr * tmp_homc / (tmp_hets * (tmp_hets - 1));
    const double preaddp = tailp;
    tailp += lastp_tail;
    if (tailp <= preaddp) {
      break;
    }
    tmp_homr -= 1;
    tmp_homc -= 1;
  }
  *tailp_ptr = tailp;
  return;
}

int32_t HweXchrLnPTailsum(uint32_t high_het_side, double* base_lnprobp, double* saved_hetsp, double* saved_hom1p, double* saved_hom2p, uint32_t* tie_ctp, double *totalp) {
  // similar to fisher23_tailsum()
  double total = 0;
  double cur_lnprob = *base_lnprobp;
  double tmp_hets = *saved_hetsp;
  double tmp_hom1 = *saved_hom1p;
  double tmp_hom2 = *saved_hom2p;
  double tmps_hets;
  double tmps_hom1;
  double tmps_hom2;
  // identify beginning of tail
  if (high_het_side) {
    if (cur_lnprob > kExactTestEpsilon2) {
      double prev_numer = tmp_hom1 * tmp_hom2;
      // Tried conditionally moving out of log-space, but wasn't able to make
      // that faster.
      while (prev_numer > 0.5) {
        tmp_hets += 2;
        cur_lnprob += log((4 * prev_numer) / (tmp_hets * (tmp_hets - 1)));
        tmp_hom1 -= 1;
        tmp_hom2 -= 1;
        if (cur_lnprob <= kExactTestEpsilon2) {
          break;
        }
        prev_numer = tmp_hom1 * tmp_hom2;
      }
      *base_lnprobp = cur_lnprob;
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
    } else {
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
      while (1) {
        const double prev_lnprob = cur_lnprob;
        tmp_hom1 += 1;
        tmp_hom2 += 1;
        cur_lnprob += log((tmp_hets * (tmp_hets - 1)) / (4 * tmp_hom1 * tmp_hom2));
        if (cur_lnprob < prev_lnprob) {
          // this should never happen, but better to play it safe re: rounding
          // error
          return 1;
        }
        tmp_hets -= 2;
        if (cur_lnprob > -kExactTestEpsilon2) {
          if (cur_lnprob > kExactTestEpsilon2) {
            break;
          }
          *tie_ctp += 1;
        }
        total += exp(cur_lnprob);
      }
      const double prev_lnprob = cur_lnprob;
      cur_lnprob = *base_lnprobp;
      *base_lnprobp = prev_lnprob;
    }
  } else {
    if (cur_lnprob > kExactTestEpsilon2) {
      while (tmp_hets > 1.5) {
        tmp_hom1 += 1;
        tmp_hom2 += 1;
        cur_lnprob += log((tmp_hets * (tmp_hets - 1)) / (4 * tmp_hom1 * tmp_hom2));
        tmp_hets -= 2;
        if (cur_lnprob <= kExactTestEpsilon2) {
          break;
        }
      }
      *base_lnprobp = cur_lnprob;
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
    } else {
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
      while (1) {
        const double prev_lnprob = cur_lnprob;
        tmp_hets += 2;
        cur_lnprob += log((4 * tmp_hom1 * tmp_hom2) / (tmp_hets * (tmp_hets - 1)));
        if (cur_lnprob < prev_lnprob) {
          return 1;
        }
        tmp_hom1 -= 1;
        tmp_hom2 -= 1;
        if (cur_lnprob > -kExactTestEpsilon2) {
          if (cur_lnprob > kExactTestEpsilon2) {
            break;
          }
          *tie_ctp += 1;
        }
        total += exp(cur_lnprob);
      }
      const double prev_lnprob = cur_lnprob;
      cur_lnprob = *base_lnprobp;
      *base_lnprobp = prev_lnprob;
    }
  }
  *saved_hetsp = tmp_hets;
  *saved_hom1p = tmp_hom1;
  *saved_hom2p = tmp_hom2;
  if (cur_lnprob > -kExactTestEpsilon2) {
    if (cur_lnprob > kExactTestEpsilon2) {
      // even most extreme table on this side is too probable
      *totalp = 0;
      return 0;
    }
    *tie_ctp += 1;
  }
  double cur_prob = exp(cur_lnprob);
  // sum tail to floating point precision limit
  if (high_het_side) {
    while (1) {
      const double prev_tot = total;
      total += cur_prob;
      if (total <= prev_tot) {
        break;
      }
      tmps_hets += 2;
      cur_prob *= (4 * tmps_hom1 * tmps_hom2) / (tmps_hets * (tmps_hets - 1));
      tmps_hom1 -= 1;
      tmps_hom2 -= 1;
    }
  } else {
    while (1) {
      const double prev_tot = total;
      total += cur_prob;
      if (total <= prev_tot) {
        break;
      }
      tmps_hom1 += 1;
      tmps_hom2 += 1;
      cur_prob *= (tmps_hets * (tmps_hets - 1)) / (4 * tmps_hom1 * tmps_hom2);
      tmps_hets -= 2;
    }
  }
  *totalp = total;
  return 0;
}

double HweXchrLnP(int32_t female_hets, int32_t female_hom1, int32_t female_hom2, int32_t male1, int32_t male2, uint32_t midp) {
  // See Graffelman J, Weir BS (2016) Testing for Hardy-Weinberg equilibrium at
  // biallelic genetic markers on the X chromosome.
  // Evaluation strategy is similar to fisher23().
  if ((!male1) && (!male2)) {
    return HweLnP(female_hets, female_hom1, female_hom2, midp);
  }
  // 1. Determine relative tail vs. center masses for the male1/male2-unchanged
  //    slice.
  double cur_female_hetd = S_CAST(double, female_hets);
  double cur_female_hom1d = S_CAST(double, female_hom1);
  double cur_female_hom2d = S_CAST(double, female_hom2);
  const double male1d = u31tod(male1);
  const double male2d = u31tod(male2);
  const double n1 = cur_female_hetd + 2 * cur_female_hom1d + male1d;
  const double n2 = cur_female_hetd + 2 * cur_female_hom2d + male2d;
  double tailp;
  double starting_lnprob;
  uint32_t tie_ct;
  // "left" = low hets side, "right" = high hets side
  double orig_base_lnprobl;
  double orig_base_lnprobr;
  double orig_saved_lhets;
  double orig_saved_lhom1;
  double orig_saved_lhom2;
  double orig_saved_rhets;
  double orig_saved_rhom1;
  double orig_saved_rhom2;
  HweLnFirstRow(cur_female_hetd, cur_female_hom1d, cur_female_hom2d, &tailp, &starting_lnprob, &tie_ct, &orig_base_lnprobl, &orig_base_lnprobr, &orig_saved_lhets, &orig_saved_lhom1, &orig_saved_lhom2, &orig_saved_rhets, &orig_saved_rhom1, &orig_saved_rhom2);

  // A "row" holds male1/male2 constant.
  // tailp, exp(orig_base_lnprobl), and exp(orig_base_lnprobr) are in units of
  // exp(starting_lnprob) within the starting row.
  for (uint32_t male1_decreasing = 0; male1_decreasing != 2; ++male1_decreasing) {
    double cur_male1 = male1d;
    double cur_male2 = male2d;
    double ln_row_prob = -starting_lnprob;
    double cur_lhets = orig_saved_lhets;
    double cur_lhom1 = orig_saved_lhom1;
    double cur_lhom2 = orig_saved_lhom2;
    double cur_rhets = orig_saved_rhets;
    double cur_rhom1 = orig_saved_rhom1;
    double cur_rhom2 = orig_saved_rhom2;
    double base_lnprobl = orig_base_lnprobl;
    double base_lnprobr = orig_base_lnprobr;
    uint32_t iter_ct;
    if (male1_decreasing) {
      iter_ct = 2 * female_hom2 + female_hets;
      if (iter_ct > S_CAST(uint32_t, male1)) {
        iter_ct = male1;
      }
    } else {
      iter_ct = 2 * female_hom1 + female_hets;
      if (iter_ct > S_CAST(uint32_t, male2)) {
        iter_ct = male2;
      }
    }
    for (uint32_t iter_idx = 0; iter_idx != iter_ct; ++iter_idx) {
      if (male1_decreasing) {
        const double old_male1 = cur_male1;
        const double old_female2 = n2 - cur_male2;
        cur_male2 += 1;
        cur_male1 -= 1;
        // row likelihood is ((n1 choose male1) * (n2 choose male2)) /
        //   ((n1 + n2) choose (male1 + male2))
        ln_row_prob += log((old_male1 * old_female2) / (cur_male2 * (n1 - cur_male1)));
        // bugfix (19 Apr 2017): We cannot move to the right of the mode here.
        // Otherwise, if the mode itself is more probable than our initial
        // table, but the table to the immediate right of the mode is not,
        // we'll fail to count the mode.
        // ("right" = high het count, "left" = low het count.)
        if (cur_lhets != 0.0) {
          cur_lhom1 += 1;
          base_lnprobl += log((old_male1 * cur_lhets) / (2 * cur_male2 * cur_lhom1));
          cur_lhets -= 1;
        } else {
          cur_lhets += 1;
          base_lnprobl += log((2 * old_male1 * cur_lhom2) / (cur_male2 * cur_lhets));
          cur_lhom2 -= 1;
        }
      } else {
        const double old_male2 = cur_male2;
        const double old_female1 = n1 - cur_male1;
        cur_male1 += 1;
        cur_male2 -= 1;
        ln_row_prob += log((old_male2 * old_female1) / (cur_male1 * (n2 - cur_male2)));
        if (cur_lhets != 0.0) {
          cur_lhom2 += 1;
          base_lnprobl += log((old_male2 * cur_lhets) / (2 * cur_male1 * cur_lhom2));
          cur_lhets -= 1;
        } else {
          cur_lhets += 1;
          base_lnprobl += log((2 * old_male2 * cur_lhom1) / (cur_male1 * cur_lhets));
          cur_lhom1 -= 1;
        }
      }
      double tail_incr1;
      if (HweXchrLnPTailsum(0, &base_lnprobl, &cur_lhets, &cur_lhom1, &cur_lhom2, &tie_ct, &tail_incr1)) {
        // all tables in this row, and all subsequent rows, are less probable
        // than the initial table.
        double cur_female1 = n1 - cur_male1;
        double cur_female2 = n2 - cur_male2;
        double row_prob = exp(ln_row_prob);
        if (male1_decreasing) {
          while (1) {
            const double preaddp = tailp;
            tailp += row_prob;
            if (tailp == preaddp) {
              break;
            }
            cur_male2 += 1;
            cur_female1 += 1;
            row_prob *= (cur_male1 * cur_female2) / (cur_male2 * cur_female1);
            cur_male1 -= 1;
            cur_female2 -= 1;
          }
        } else {
          while (1) {
            const double preaddp = tailp;
            tailp += row_prob;
            if (tailp == preaddp) {
              break;
            }
            cur_male1 += 1;
            cur_female2 += 1;
            row_prob *= (cur_male2 * cur_female1) / (cur_male1 * cur_female2);
            cur_male2 -= 1;
            cur_female1 -= 1;
          }
        }
        break;
      }
      tailp += tail_incr1;
      if (male1_decreasing) {
        const double old_male1 = cur_male1 + 1;
        if (cur_rhom2 != 0.0) {
          cur_rhets += 1;
          base_lnprobr += log((2 * old_male1 * cur_rhom2) / (cur_male2 * cur_rhets));
          cur_rhom2 -= 1;
        } else {
          cur_rhom1 += 1;
          base_lnprobr += log((old_male1 * cur_rhets) / (2 * cur_male2 * cur_rhom1));
          cur_rhets -= 1;
        }
      } else {
        const double old_male2 = cur_male2 + 1;
        if (cur_rhom1 != 0.0) {
          cur_rhets += 1;
          base_lnprobr += log((2 * old_male2 * cur_rhom1) / (cur_male1 * cur_rhets));
          cur_rhom1 -= 1;
        } else {
          cur_rhom2 += 1;
          base_lnprobr += log((old_male2 * cur_rhets) / (2 * cur_male1 * cur_rhom2));
          cur_rhets -= 1;
        }
      }
      double tail_incr2 = 0.0;  // maybe-uninitialized warning
      HweXchrLnPTailsum(1, &base_lnprobr, &cur_rhets, &cur_rhom1, &cur_rhom2, &tie_ct, &tail_incr2);
      tailp += tail_incr2;
    }
  }
  // row likelihood is ((n1 choose male1) * (n2 choose male2)) /
  //   ((n1 + n2) choose (male1 + male2))
  // try to start with smaller terms to reduce cancellation hit
  const double absolute_row_lnprob = -Lfact(male1d) - Lfact(n1 - male1d) + Lfact(n1) - Lfact(male2d) - Lfact(n2 - male2d) + Lfact(n2) + Lfact(male1d + male2d) + Lfact(n1 + n2 - male1d - male2d) - Lfact(n1 + n2);
  if (midp) {
    tailp -= S_CAST(double, tie_ct) * 0.5;
  }
  double result = log(tailp) + absolute_row_lnprob + starting_lnprob;
  if (result > -kBigEpsilon) {
    // true p-value should always be 1 here
    return 0.0;
  }
  return result;
}

#ifdef __cplusplus
}
#endif
