use crate::dyadic::Dyadic;
use crate::error::Error;
use super::{PointNode, PointType};
pub const MAX_ROUNDS: u32 = 1024;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ErrorBound {
pub rounds: [u32; 3],
pub slack: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Term {
Error(usize),
ValueTimesError {
value: usize,
error: usize,
},
ErrorTimesError(usize, usize),
SumRounding(usize, usize),
ProductRounding(usize, usize),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ErrorProgram {
pub(crate) terms: Vec<Vec<Term>>,
pub(crate) rounds: Vec<u32>,
}
impl ErrorProgram {
pub(crate) fn of(point: &PointType) -> ErrorProgram {
let nodes = &point.graph.nodes;
let is_zero = |id: usize| nodes[id] == PointNode::Int(0);
let mut terms: Vec<Vec<Term>> = Vec::with_capacity(nodes.len());
let mut rounds: Vec<u32> = Vec::with_capacity(nodes.len());
for node in nodes {
let has_error = |id: usize, terms: &[Vec<Term>]| !terms[id].is_empty();
let mut node_terms = Vec::new();
match *node {
PointNode::Coord { .. } | PointNode::Scalar(_) | PointNode::Int(_) => {}
PointNode::Neg(x) => {
if has_error(x, &terms) {
node_terms.push(Term::Error(x));
}
}
PointNode::Add(x, y) | PointNode::Sub(x, y) => {
for operand in [x, y] {
if has_error(operand, &terms) {
node_terms.push(Term::Error(operand));
}
}
if !is_zero(x) && !is_zero(y) {
node_terms.push(Term::SumRounding(x, y));
}
}
PointNode::Mul(x, y) => {
let (ex, ey) = (has_error(x, &terms), has_error(y, &terms));
if ey {
node_terms.push(Term::ValueTimesError { value: x, error: y });
}
if ex {
node_terms.push(Term::ValueTimesError { value: y, error: x });
}
if ex && ey {
node_terms.push(Term::ErrorTimesError(x, y));
}
if !is_zero(x) && !is_zero(y) {
node_terms.push(Term::ProductRounding(x, y));
}
}
}
let count = node_terms
.iter()
.map(|&term| term_rounds(term, &rounds))
.reduce(|total, term| total.max(term).saturating_add(1))
.unwrap_or(0);
terms.push(node_terms);
rounds.push(count);
}
ErrorProgram { terms, rounds }
}
}
fn term_rounds(term: Term, rounds: &[u32]) -> u32 {
match term {
Term::Error(x) => rounds[x],
Term::ValueTimesError { error, .. } => rounds[error].saturating_add(2),
Term::ErrorTimesError(x, y) => rounds[x].saturating_add(rounds[y]).saturating_add(2),
Term::SumRounding(..) => 2,
Term::ProductRounding(..) => 3,
}
}
pub fn error_bound(point: &PointType, file: &str) -> Result<ErrorBound, Error> {
check_integers(point, file)?;
let program = ErrorProgram::of(point);
let rounds = point.graph.roots.map(|root| program.rounds[root]);
let most = rounds.into_iter().max().unwrap_or(0);
if most > MAX_ROUNDS {
return Err(Error::at(
file,
point.position,
format!(
"the error bound of `{}` takes {most} roundings, more than the {MAX_ROUNDS} \
for which its slack factor is derived",
point.name
),
));
}
Ok(ErrorBound {
rounds,
slack: slack_factor(most),
})
}
fn check_integers(point: &PointType, file: &str) -> Result<(), Error> {
for node in &point.graph.nodes {
if let PointNode::Int(n) = *node {
if !is_exact_f64(n) {
return Err(Error::at(
file,
point.position,
format!(
"the integer {n} in `{}` is not exactly an `f64`: integers of a point \
must have a magnitude of at most 2^53 or be exactly representable",
point.name
),
));
}
}
}
Ok(())
}
pub(crate) fn is_exact_f64(n: i64) -> bool {
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
let back = (n as f64) as i128;
back == i128::from(n)
}
pub(crate) fn slack_factor(rounds: u32) -> f64 {
let one_minus_u = &Dyadic::integer(1) - &Dyadic::pow2(-53);
let mut power = Dyadic::integer(1);
for _ in 0..rounds {
power = &power * &one_minus_u;
}
let one = Dyadic::integer(1);
let mut slack = 1.0f64;
while &Dyadic::from_f64(slack) * &power < one {
slack = slack.next_up();
}
slack
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_slack_factor_covers_its_rounds_and_no_smaller_factor_does() {
assert_eq!(slack_factor(0), 1.0);
for rounds in [1, 2, 3, 6, 100, MAX_ROUNDS] {
let s = slack_factor(rounds);
let one_minus_u = &Dyadic::integer(1) - &Dyadic::pow2(-53);
let mut power = Dyadic::integer(1);
for _ in 0..rounds {
power = &power * &one_minus_u;
}
assert!(&Dyadic::from_f64(s) * &power >= Dyadic::integer(1));
assert!(&Dyadic::from_f64(s.next_down()) * &power < Dyadic::integer(1));
}
}
#[test]
fn integers_beyond_2_to_the_53_are_exact_only_when_representable() {
assert!(is_exact_f64(1 << 53));
assert!(!is_exact_f64((1 << 53) + 1));
assert!(is_exact_f64(i64::MIN));
assert!(!is_exact_f64(i64::MAX));
}
}