ball_float_checked チュートリアル

このチュートリアルでは、不正な入力や認証できないステップをエラーとして報告しつつ、すべての有効な結果を保証付きの包含区間のまま保つ区間計算の実行方法を示します。検証付きのコンストラクタで区間を構築し、区間算術と初等関数を BallFloatResult の中で連鎖させ、bind で独自のチェックを加え、結果を一度だけ読み取ります。算術は ball_float に由来します。どの結果がエラーになるかは設計ページで説明し、すべてのメソッドは API リファレンスに一覧があります。

クイックスタート

moon add Luna-Flow/floating@0.8.0
import {
  "Luna-Flow/floating/bin_float",
  "Luna-Flow/floating/ball_float_checked",
}

ln⁡2\ln 2 を 53 ビットで包含します。

///|
test "quick start: an enclosure of ln 2" {
  let r = @ball_float_checked.BallFloatResult::from_int(2, precision=53).ln()
  match r.result() {
    Ok(x) => {
      inspect(x.lower_bound().to_string(), content="6243314768165359p-53")
      inspect(x.upper_bound().to_string(), content="390207173010335p-49")
    }
    Err(e) => fail(e.message)
  }
}

既約形で出力された上界は 6243314768165360⋅2−536243314768165360 \cdot 2^{-53} なので、二つの上下界は隣接する 53 ビットの数であり、ln⁡2\ln 2 はその間にあります。

日常的なタスク

残りの例では次のヘルパーで結果を出力します。有界な区間は厳密な二進表記で centre +/- radius として出力されます。

///|
fn show(r : @ball_float_checked.BallFloatResult) -> String {
  match r.result() {
    Ok(v) => v.to_string()
    Err(e) => "error: " + e.message
  }
}

境界で測定値を検証する

入力データは from_bounds、from_double、exact を通じて入ってきます。逆転した上下界や有限でない入力元は、計算が始まる前にエラーになります。

///|
fn reading(lo : Double, hi : Double) -> @ball_float_checked.BallFloatResult {
  @ball_float_checked.BallFloatResult::from_bounds(
    @bin_float.BinFloat::from_double(lo),
    @bin_float.BinFloat::from_double(hi),
  )
}

///|
test "validated readings" {
  inspect(show(reading(1.0, 3.0)), content="1p1 +/- 1p0")
  inspect(show(reading(3.0, 1.0)), content="error: ball lower bound must not exceed upper bound")
  inspect(
    show(reading(0.0 / 0.0, 1.0)),
    content="error: ball bounds must not be NaN",
  )
}

式を通して不確かさを伝播させる

演算子はラッパーに対して動作します。結果は、入力が区間内にあるときに式が取りうるすべての値を包含します。

///|
test "area of an uncertain rectangle" {
  let width = reading(1.0, 3.0)
  let height = reading(2.0, 2.5)
  let area = width * height
  match area.result() {
    Ok(x) => {
      inspect(x.lower_bound().to_string(), content="1p1")
      inspect(x.upper_bound().to_string(), content="15p-1")
    }
    Err(e) => fail(e.message)
  }
}

面積は二つの範囲の積である [2,7.5][2, 7.5] の中にあります。

「情報がない」ことはエラーではないと知っておく

区間演算は集合の意味論に従います。関数の定義域外の点は捨てられ、何も言えない演算は実数直線全体を返します。これらは正しい包含区間なので成功です。

///|
test "empty and whole results are successes" {
  let negative = @ball_float_checked.BallFloatResult::from_int(-2)
  inspect(show(negative.ln()), content="[empty]")
  inspect(negative.ln().is_ok(), content="true")
  let one = @ball_float_checked.BallFloatResult::from_int(1)
  inspect(show(one / reading(-1.0, 1.0)), content="[-inf, inf]")
}

空または非有界な結果があなたのアプリケーションにとって失敗を意味する場合は、bind でそのことを表明してください。

アプリケーションの規則をエラーに変える

///|
fn require_nonempty(
  x : @ball_float.BallFloat,
) -> @ball_float_checked.BallFloatResult {
  if x.is_empty() {
    @ball_float_checked.BallFloatResult::err(
      @lf_arith.ArithmeticError::domain_error("no admissible value"),
    )
  } else {
    @ball_float_checked.BallFloatResult::ok(x)
  }
}

///|
test "reject empty enclosures" {
  let r = @ball_float_checked.BallFloatResult::from_int(-2).ln().bind(require_nonempty)
  inspect(show(r), content="error: no admissible value")
  let ok = reading(1.0, 3.0).ln().bind(require_nonempty)
  inspect(ok.is_ok(), content="true")
}

さらに進んで

認証の失敗

初等関数は認証付きの誤差限界で評価されます。ライブラリが予算内で包含区間を認証できない場合は、誤った区間や不必要に広い区間を返す代わりに、詳細レコード付きの CertificationFailure を報告します。

///|
test "certification failure carries a detail record" {
  let huge = @ball_float_checked.BallFloatResult::exact(
    @bin_float.BinFloat::make(@bin_float.BinCoeff::one(), 70000, 53),
  )
  match huge.sin().error() {
    Some(e) => {
      inspect(e.is_certification_failure(), content="true")
      inspect(e.certification_failure_detail().unwrap().operation(), content="sin")
    }
    None => fail("expected a failure")
  }
}

算術トレイトによるべき乗

pow_nat と pow_int は Luna-Flow/arithmetic の PowNatChecked トレイトと PowIntChecked トレイトを経由し、BallFloat はそれらを区間全体のべき乗を計算することで実装しています。ゼロを含む区間では、二つの因子を独立とみなす繰り返し乗算よりも狭い結果になります。

///|
test "a power is tighter than a product" {
  let x = reading(-1.0, 1.0)
  match (x.pow_int(2).result(), (x * x).result()) {
    (Ok(square), Ok(product)) => {
      inspect(square.lower_bound().to_string(), content="0")
      inspect(product.lower_bound().to_string(), content="-1p0")
    }
    _ => fail("unexpected error")
  }
}

包含関係を使ったジェネリックなコード

BallFloat は Luna-Flow/arithmetic の包含関係(Contains、DefinitelyLt、…)を実装しています。パイプラインの最後で区間を取り出して判定します。

///|
test "decide with an enclosure" {
  match reading(1.0, 3.0).rootn(2).result() {
    Ok(root) => {
      let two = @ball_float.BallFloat::from_int(2)
      inspect(@lf_arith.DefinitelyLt::definitely_lt(root, two), content="true")
    }
    Err(e) => fail(e.message)
  }
}

よくある落とし穴

  • 小さなデフォルト精度。 from_int と from_coefficient のデフォルトは 16 ビットです。現在のブランチでは、それより多くのビットを必要とする整数は包含される前に最近接に丸められるため、from_int(100001) は 100001100001 を含みません。大きな整数には precision=53(またはそれ以上)を渡してください。
  • 空と全体は成功である。 最終的な区間に対して is_empty / is_entire を判定するか、bind によるチェックを追加してください。
  • 装飾もフラグもない。 ラッパーは IEEE 1788 の装飾も BallFlags も保持しません。必要な場合は ball_float の装飾付き API と contextual API を使ってください。
  • 残るのは最初のエラーだけです。これはすべての checked ラッパーと同じです。
  • [+inf, +inf] は拒否される。 IEEE 1788 の区間は実数直線の部分集合です。代わりに whole() または半無限区間を使ってください。
  • flat_map は非推奨。 bind を使ってください。

次のステップ