将任意精度有理数(OCaml,zarith)转换为近似浮点数



我正在使用Zarith库进行任意精度的有理运算。假设我有一个类型为Q.t的有理数q,它是两个大整数的比率(Q是Zarith的任意精度有理数模)。有时,为了可读性,我想将这个数字打印为浮点数字,有时我需要将这个数字转换为浮点,以便以后进行非任意精度计算。有没有办法将q转换为达到一定精度的浮点数?

我将q转换为浮点的方式现在没有保证,并且可以创建未定义的浮点数(Z是任意精度整数模块):

let to_float q =
  let n, d = num q, den q in
  (* check if d is zero and raise an error if it is *)
  let nf, df = Z.to_float n, Z.to_float d in
  nf /. df

有没有更好的方法来处理这个问题,我可以获得一个最准确地近似任何q的浮点值?

编辑

如果有人感兴趣的话,我很快在OCaml上写下了马克·狄金森的答案。它可能(肯定)可以改进和清理。如果我这样做,或者有人有任何改进建议,我会编辑。但现在这已经解决了我的问题!

let to_float q = 
  let n, d = num q, den q in
  let n_sign = Z.sign n in
  let d_sign = Z.sign d in (* always >= 0 *)
  if d_sign = 0 then raise Division_by_zero;
  let n = Z.abs n in
  if n_sign = 0 then 0. else
    let shift = (Z.numbits n) - (Z.numbits d) - 55 in
    let is_subnormal = shift < -1076 in
    let shift = if is_subnormal then -1076 else shift in
    let d = if shift >= 0 then Z.shift_left d shift else d in
    let n = if shift < 0 then Z.shift_left n (-shift)
      else n in
    let quotient, remainder = Z.div_rem n d in
    let quotient = if (Z.compare remainder (Z.zero)) = 0 && Z.is_even quotient then
        Z.add Z.one quotient else quotient in
    let quotient = if not is_subnormal then quotient else
        let round_select = Z.to_int @@ Z.rem quotient @@ Z.of_int 8 in
        Z.add quotient [|Z.zero;Z.minus_one;Z.of_int (-2);Z.one;Z.zero
                        ;Z.minus_one;Z.of_int 2;Z.one|].(round_select)
    in
    let unsigned_res = ldexp (Z.to_float quotient) shift in                                                                                                             
    if n_sign = 1 then unsigned_res else -.unsigned_res

稍后我将研究为GMP的mpq_get_d函数编写一个接口,但我不完全确定如何做到这一点。我看到如何做到这一点的唯一方法是将q : Q.t转换为字符串并将其传递给:

int mpq_set_str (mpq_t rop, const char *str, int base)

有人知道如何在OCaml中将rop传递到mpq_get_d吗?或者有描述如何做到这一点的参考资料吗?我浏览了RWO的第19章,没有看到这样的情况。

如果您可以访问

  • 整数log2运算,以及
  • 将整数左移给定位数的能力

然后它相对容易滚动你自己正确的四舍五入转换。简而言之,方法看起来是这样的:

  1. 减少到n > 0d > 0的情况;过滤掉明显的下溢/溢出
  2. 选择一个整数shift,使2^-shift*n/d位于2^542^56之间
  3. 使用整数算术计算x = 2^-shift*n/d,使用四舍五入到奇数的舍入方法舍入到最接近的整数
  4. x转换为最接近的IEEE 754双精度值dx,使用通常的四舍五入关系到偶数舍入模式
  5. 返回ldexp(dx, shift)

恐怕我对OCaml不太熟悉,但下面的Python代码说明了积极输入的想法。我让你们对负输入和除以零做明显的修改。您可能还想提前返回极端上溢和下溢的情况:通过在下面查找shift的超大或小值,可以很容易地检测到这些情况。

from math import ldexp
def to_float(numerator, denominator):
    """
    Convert numerator / denominator to float, correctly rounded.
    For simplicity, assume both inputs are positive.
    """
    # Shift satisfies 2**54 < (numerator / denominator) / 2**shift < 2**56
    shift = numerator.bit_length() - denominator.bit_length() - 55
    # Divide the fraction by 2**shift.
    if shift >= 0:
        denominator <<= shift
    else:
        numerator <<= -shift
    # Convert to the nearest integer, using round-to-odd.
    q, r = divmod(numerator, denominator)
    if r != 0 and q % 2 == 0:
        q += 1
    # Now convert to the nearest float and shift back.
    return ldexp(float(q), shift)

一些注意事项:

  • 正整数n上的bit_length方法给出表示n或换句话说1 + floor(log2(n))所需的比特数
  • divmod是一个Python函数,它同时计算整数除法的商和余数
  • 数量CCD_ 29(容易)适合64位整数
  • 我们四舍五入两次:一次是将移位后的numerator / denominator转换为最接近的整数,另一次是在将该整数四舍五进为浮点值时。第一轮使用四舍五入到奇数的方法;这确保了第二轮(隐含在从int到float的转换中)给出的结果与我们直接将分数四舍五入为float的结果相同
  • 上述算法不能正确处理转换后的浮点值低于正常值的分数:在这种情况下,ldexp运算可能会引入第三次舍入。这是有可能处理的,只要小心。请参阅下面的一些代码

事实上,上面是Python在将一个(大)整数除以另一个以获得浮点结果时使用的算法的简化版本。你可以在这里看到来源。long_true_divide函数开头的注释概述了该方法。

为了完整性,这里有一个变体,它也正确地处理了次正规结果。

def to_float(numerator, denominator):
    """
    Convert numerator / denominator to float, correctly rounded.
    For simplicity, assume both inputs are positive.
    """
    # Choose shift so that 2**54 < numerator / denominator / 2**shift < 2**56
    shift = numerator.bit_length() - denominator.bit_length() - 55
    # The 'treat_as_subnormal' flag catches all cases of subnormal results,
    # along with some cases where the result is not subnormal but *is* still
    # smaller than 2**-1021. In all these cases, it's sufficient to find the
    # closest integer multiple of 2**-1074. We first round to the nearest
    # multiple of 2**-1076 using round-to-odd.
    treat_as_subnormal = shift < -1076
    if treat_as_subnormal:
        shift = -1076
    # Divide the fraction by 2**shift.
    if shift >= 0:
        denominator <<= shift
    else:
        numerator <<= -shift
    # Convert to the nearest integer, using round-to-odd.
    q, r = divmod(numerator, denominator)
    if r != 0 and q % 2 == 0:
        q += 1
    # Now convert to the nearest float and shift back.
    if treat_as_subnormal:
        # Round to the nearest multiple of 4, rounding ties to
        # the nearest multiple of 8. This avoids double rounding
        # from the ldexp call below.
        q += [0, -1, -2, 1, 0, -1, 2, 1][q%8]
    return ldexp(float(q), shift)

这不是一个完整的答案,但环顾四周,我发现Zarith在内部使用GMP。有一个名为mpq_get_d的GMP函数可以将有理数转换为二重数。如果它不能直接在Zarith中使用,应该可以(给一段时间)为它添加一个接口。

相关内容

  • 没有找到相关文章

最新更新