Skip to main content

semantic_operators/
bench.rs

1//! Offline scoring and explicit, bounded benchmark execution.
2//!
3//! A benchmark runner invokes its provider: default warm-up adds one invocation.
4//! Provider failures abort; they are never folded into abstention denominators.
5use crate::flow::{FlowFailure, FlowOutcome};
6use crate::{
7    Answer, AnswerValue, Answers, Error, ErrorKind, IndexMap, Provider, Question, Questions,
8    Request, Result, State, ask,
9};
10use futures_util::{StreamExt, TryStreamExt, stream};
11use std::{future::Future, num::NonZeroUsize, time::Instant};
12/// One discrete non-abstention label. Scores use zero-based rubric indices.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum Decision {
15    /// Boolean label.
16    Boolean(bool),
17    /// Choice outcome name.
18    Choice(String),
19    /// Score rubric index.
20    Score(usize),
21}
22/// One labeled input; omitted question labels do not contribute to metrics.
23#[derive(Clone)]
24pub struct Case {
25    state: State,
26    expected: IndexMap<String, Decision>,
27}
28impl std::fmt::Debug for Case {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("Case")
31            .field("labels", &self.expected.len())
32            .finish_non_exhaustive()
33    }
34}
35impl Case {
36    /// Retain state and labels; question-specific validation happens before scoring or dispatch.
37    pub fn new(state: State, expected: IndexMap<String, Decision>) -> Self {
38        Self { state, expected }
39    }
40    /// Input state.
41    pub fn state(&self) -> &State {
42        &self.state
43    }
44    /// Supplied labels; may be an empty subset of the questions.
45    pub fn expected(&self) -> &IndexMap<String, Decision> {
46        &self.expected
47    }
48}
49/// Counts and probabilities for the labeled subset of one question.
50#[derive(Clone, Debug, Default)]
51pub struct QuestionStats {
52    correct: usize,
53    undecided: usize,
54    total: usize,
55    p_correct: Vec<f64>,
56}
57impl QuestionStats {
58    /// Correct decided answers.
59    pub fn correct(&self) -> usize {
60        self.correct
61    }
62    /// Undecided labeled cases.
63    pub fn undecided(&self) -> usize {
64        self.undecided
65    }
66    /// Number of cases carrying this question's label.
67    pub fn total(&self) -> usize {
68        self.total
69    }
70    /// Labeled cases with a decision.
71    pub fn answered(&self) -> usize {
72        self.total - self.undecided
73    }
74    /// Correct divided by all labeled cases; zero for an empty denominator.
75    pub fn accuracy(&self) -> f64 {
76        ratio(self.correct, self.total)
77    }
78    /// Correct divided by answered labeled cases; zero for an empty denominator.
79    pub fn accuracy_when_answered(&self) -> f64 {
80        ratio(self.correct, self.answered())
81    }
82    /// Mean probability assigned to the label, including abstentions.
83    pub fn mean_p_correct(&self) -> f64 {
84        if self.p_correct.is_empty() {
85            0.0
86        } else {
87            accurate_sum(self.p_correct.iter().copied()).unwrap_or(0.0)
88                / self.p_correct.len() as f64
89        }
90    }
91    /// Per-labeled-case probabilities in case order.
92    pub fn p_correct(&self) -> &[f64] {
93        &self.p_correct
94    }
95}
96fn ratio(n: usize, d: usize) -> f64 {
97    if d == 0 { 0.0 } else { n as f64 / d as f64 }
98}
99/// A wrong decided answer; abstentions are not misses.
100#[derive(Clone, Debug)]
101pub struct Miss {
102    /// Case position.
103    pub case: usize,
104    /// Question name.
105    pub question: String,
106    /// Correct label.
107    pub expected: Decision,
108    /// Full observed answer.
109    pub got: Answer,
110}
111/// A scored batch, retaining answers, decisions and observed latency.
112#[derive(Clone, Debug)]
113pub struct Report {
114    questions: IndexMap<String, QuestionStats>,
115    latencies_ms: Vec<f64>,
116    total_ms: f64,
117    misses: Vec<Miss>,
118    answers: Vec<Answers>,
119    decisions: Vec<IndexMap<String, Option<Decision>>>,
120    warmup_calls: usize,
121}
122impl Report {
123    /// Metrics per question, in question order.
124    pub fn questions(&self) -> &IndexMap<String, QuestionStats> {
125        &self.questions
126    }
127    /// Per-call latencies; excludes time waiting for a runner concurrency slot.
128    pub fn latencies_ms(&self) -> &[f64] {
129        &self.latencies_ms
130    }
131    /// Measured batch wall time, excluding warm-up.
132    pub fn total_ms(&self) -> f64 {
133        self.total_ms
134    }
135    /// Wrong decided answers only.
136    pub fn misses(&self) -> &[Miss] {
137        &self.misses
138    }
139    /// Full answers for every case, including unlabeled questions.
140    pub fn answers(&self) -> &[Answers] {
141        &self.answers
142    }
143    /// Discrete decisions for every case and question.
144    pub fn decisions(&self) -> &[IndexMap<String, Option<Decision>>] {
145        &self.decisions
146    }
147    /// Extra provider calls made by runner warm-up; zero for purely scored data.
148    pub fn warmup_calls(&self) -> usize {
149        self.warmup_calls
150    }
151    /// Provider invocations represented by this report, including warm-up.
152    pub fn calls(&self) -> usize {
153        self.answers.len() + self.warmup_calls
154    }
155}
156/// Reduce a normalized answer to a label. Score rounding is half up.
157/// Box is unsupported even when its answer is undecided.
158///
159/// # Errors
160/// Returns `InvalidArgument` for Box questions, or a normalization error if
161/// the answer is incompatible with the question.
162pub fn decide(question: &Question, answer: &Answer) -> Result<Option<Decision>> {
163    if matches!(question, Question::Box(_)) {
164        return Err(invalid("Box questions cannot be benchmarked"));
165    }
166    let checked = answer.revalidate(question)?;
167    Ok(match checked.value() {
168        None => None,
169        Some(AnswerValue::Boolean(v)) => Some(Decision::Boolean(*v)),
170        Some(AnswerValue::Choice(v)) => Some(Decision::Choice(v.clone())),
171        Some(AnswerValue::Score(v)) => Some(Decision::Score((v + 0.5).floor() as usize)),
172        Some(AnswerValue::Box(_)) => return Err(invalid("Box questions cannot be benchmarked")),
173    })
174}
175fn label_key<'a>(q: &'a Question, label: &'a Decision) -> Result<&'a str> {
176    match (q, label) {
177        (Question::Boolean(_), Decision::Boolean(v)) => Ok(if *v { "true" } else { "false" }),
178        (Question::Choice(q), Decision::Choice(v)) if q.options().contains_key(v) => Ok(v),
179        (Question::Score(q), Decision::Score(i)) => q
180            .levels()
181            .get(*i)
182            .map(String::as_str)
183            .ok_or_else(|| invalid("Score label is outside the rubric")),
184        _ => Err(invalid("label does not match the question outcomes")),
185    }
186}
187fn validate(questions: &Questions, cases: &[Case]) -> Result<()> {
188    if questions.keys().any(String::is_empty) {
189        return Err(invalid("question names must be nonempty"));
190    }
191    if questions.values().any(|q| matches!(q, Question::Box(_))) {
192        return Err(invalid("Box questions cannot be benchmarked"));
193    }
194    for case in cases {
195        for (name, label) in &case.expected {
196            let q = questions
197                .get(name)
198                .ok_or_else(|| invalid("case label names an unknown question"))?;
199            label_key(q, label)?;
200        }
201    }
202    Ok(())
203}
204fn check_latencies(count: usize, latencies: &[f64], total: f64) -> Result<()> {
205    if latencies.len() != count
206        || !total.is_finite()
207        || total < 0.0
208        || latencies.iter().any(|v| !v.is_finite() || *v < 0.0)
209    {
210        return Err(invalid(
211            "latencies must align with cases and be finite nonnegative milliseconds",
212        ));
213    }
214    Ok(())
215}
216/// Score already-collected answers. Label subsets and empty case lists are valid.
217/// Answer and latency lengths must exactly match cases, and answer keys must match questions.
218///
219/// # Errors
220/// Returns `InvalidArgument` for unsupported Box questions, invalid labels,
221/// mismatched case/answer/latency lengths, missing or extra answer names, or
222/// nonfinite/negative timing values. Incompatible answers return their
223/// normalization error.
224pub fn score(
225    questions: &Questions,
226    cases: &[Case],
227    answers: Vec<Answers>,
228    latencies_ms: Vec<f64>,
229    total_ms: f64,
230) -> Result<Report> {
231    validate(questions, cases)?;
232    check_latencies(cases.len(), &latencies_ms, total_ms)?;
233    if answers.len() != cases.len() {
234        return Err(invalid("case and answer counts must match"));
235    }
236    let answers: Vec<Answers> = answers
237        .into_iter()
238        .map(|observed| {
239            if observed.len() != questions.len()
240                || questions.keys().any(|name| !observed.contains_key(name))
241            {
242                return Err(invalid(
243                    "answer keys must exactly match benchmark questions",
244                ));
245            }
246            questions
247                .iter()
248                .map(|(name, q)| Ok((name.clone(), observed[name].revalidate(q)?)))
249                .collect()
250        })
251        .collect::<Result<_>>()?;
252    let mut stats: IndexMap<String, QuestionStats> = questions
253        .keys()
254        .map(|name| (name.clone(), QuestionStats::default()))
255        .collect();
256    let mut misses = Vec::new();
257    let mut decisions = Vec::with_capacity(cases.len());
258    for (i, (case, observed)) in cases.iter().zip(&answers).enumerate() {
259        if observed.len() != questions.len()
260            || questions.keys().any(|name| !observed.contains_key(name))
261        {
262            return Err(invalid(
263                "answer keys must exactly match benchmark questions",
264            ));
265        }
266        let row: IndexMap<String, Option<Decision>> = questions
267            .iter()
268            .map(|(name, q)| Ok((name.clone(), decide(q, &observed[name])?)))
269            .collect::<Result<_>>()?;
270        for (name, label) in &case.expected {
271            let answer = &observed[name];
272            let q = &questions[name];
273            let s = stats
274                .get_mut(name)
275                .ok_or_else(|| invalid("case label names an unknown question"))?;
276            let p = answer
277                .probabilities()
278                .get(label_key(q, label)?)
279                .ok_or_else(|| invalid("answer has no probability for label"))?;
280            s.total += 1;
281            s.p_correct.push(*p);
282            match &row[name] {
283                None => s.undecided += 1,
284                Some(value) if value == label => s.correct += 1,
285                Some(_) => misses.push(Miss {
286                    case: i,
287                    question: name.clone(),
288                    expected: label.clone(),
289                    got: answer.clone(),
290                }),
291            }
292        }
293        decisions.push(row);
294    }
295    Ok(Report {
296        questions: stats,
297        latencies_ms,
298        total_ms,
299        misses,
300        answers,
301        decisions,
302        warmup_calls: 0,
303    })
304}
305/// Apply a confidence threshold without provider calls or restoring old abstentions.
306///
307/// # Errors
308/// Returns `InvalidArgument` for a nonfinite threshold or one outside `[0, 1]`.
309/// Also propagates [`score`] errors if the supplied questions or cases do not
310/// match the retained report evidence.
311pub fn at_min_confidence(
312    report: &Report,
313    questions: &Questions,
314    cases: &[Case],
315    threshold: f64,
316) -> Result<Report> {
317    if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
318        return Err(invalid("threshold must be finite and from zero to one"));
319    }
320    let answers = report
321        .answers
322        .iter()
323        .map(|row| {
324            row.iter()
325                .map(|(name, answer)| {
326                    Ok((name.clone(), answer.clone().with_min_confidence(threshold)?))
327                })
328                .collect::<Result<Answers>>()
329        })
330        .collect::<Result<Vec<_>>>()?;
331    let mut scored = score(
332        questions,
333        cases,
334        answers,
335        report.latencies_ms.clone(),
336        report.total_ms,
337    )?;
338    scored.warmup_calls = report.warmup_calls;
339    Ok(scored)
340}
341/// Compare decisions across aligned nonempty reports, including identical abstentions.
342/// One report is perfectly stable; stability does not measure correctness.
343///
344/// # Errors
345/// Returns `InvalidArgument` when no reports or cases are supplied, or the
346/// reports differ in case count or question names.
347pub fn stability(reports: &[Report]) -> Result<IndexMap<String, f64>> {
348    let first = reports
349        .first()
350        .ok_or_else(|| invalid("stability needs at least one report"))?;
351    let count = first.decisions.len();
352    if count == 0 {
353        return Err(invalid("stability needs at least one case"));
354    }
355    let names = &first.questions;
356    for report in reports {
357        if report.decisions.len() != count
358            || report.questions.len() != names.len()
359            || names.keys().any(|n| !report.questions.contains_key(n))
360        {
361            return Err(invalid(
362                "stability reports have mismatched cases or question names",
363            ));
364        }
365    }
366    Ok(names
367        .keys()
368        .map(|name| {
369            let stable = (0..count)
370                .filter(|i| {
371                    reports
372                        .iter()
373                        .all(|r| r.decisions[*i][name] == first.decisions[*i][name])
374                })
375                .count();
376            (name.clone(), stable as f64 / count as f64)
377        })
378        .collect())
379}
380/// Discounted cumulative gain at `k`; missing IDs have zero grade.
381/// Returns None when ideal gain is zero. Grades and intermediate gains must be finite.
382///
383/// # Errors
384/// Returns `InvalidArgument` if a relevance grade, cumulative gain, or final
385/// ratio cannot be represented as a finite number.
386pub fn ndcg(order: &[String], grades: &IndexMap<String, u32>, k: usize) -> Result<Option<f64>> {
387    let gains: IndexMap<String, f64> = grades
388        .iter()
389        .map(|(id, g)| {
390            let exponent =
391                i32::try_from(*g).map_err(|_| invalid("relevance grade overflows gain"))?;
392            let gain = 2.0f64.powi(exponent) - 1.0;
393            if !gain.is_finite() {
394                return Err(invalid("relevance grade overflows gain"));
395            }
396            Ok((id.clone(), gain))
397        })
398        .collect::<Result<_>>()?;
399    let dcg = |ids: &[&String]| -> Result<f64> {
400        accurate_sum(
401            ids.iter()
402                .take(k)
403                .enumerate()
404                .map(|(i, id)| gains.get(*id).copied().unwrap_or(0.0) / ((i + 2) as f64).log2()),
405        )
406    };
407    let mut ideal: Vec<_> = grades.keys().collect();
408    ideal.sort_by_key(|id| std::cmp::Reverse(grades[*id]));
409    let ideal = dcg(&ideal)?;
410    if ideal == 0.0 {
411        return Ok(None);
412    }
413    let actual: Vec<_> = order.iter().collect();
414    let result = dcg(&actual)? / ideal;
415    if !result.is_finite() {
416        return Err(invalid("NDCG is not finite"));
417    }
418    Ok(Some(result))
419}
420/// NDCG with the Python default cutoff of ten.
421///
422/// # Errors
423/// Propagates [`ndcg`] errors when gains or the final ratio are not finite.
424pub fn ndcg_default(order: &[String], grades: &IndexMap<String, u32>) -> Result<Option<f64>> {
425    ndcg(order, grades, 10)
426}
427/// Explicit benchmark work budget settings. Warm-up defaults to one extra call.
428#[derive(Clone, Debug)]
429pub struct RunOptions {
430    /// First cases additionally invoked before timing, capped at the case count.
431    pub warmup: usize,
432    /// Maximum concurrently polled measured requests; defaults to four.
433    pub concurrency: NonZeroUsize,
434    /// Request-local images for each independent benchmark case.
435    #[cfg(feature = "images")]
436    #[cfg_attr(docsrs, doc(cfg(feature = "images")))]
437    pub assets: crate::image::Assets,
438}
439impl Default for RunOptions {
440    fn default() -> Self {
441        Self {
442            warmup: 1,
443            concurrency: NonZeroUsize::new(4).expect("four is nonzero"),
444            #[cfg(feature = "images")]
445            assets: Default::default(),
446        }
447    }
448}
449/// Invoke a provider for all cases, aborting on failure and dropping pending work.
450/// Validate labels and request assets before any warm-up or measured call.
451///
452/// # Errors
453/// Returns label/request validation errors before dispatch, and propagates
454/// any provider, timeout, or malformed-response error during warm-up or
455/// measurement. On failure, pending local futures are dropped; no partial
456/// report or synthetic abstention is returned.
457pub async fn run(
458    provider: &dyn Provider,
459    questions: &Questions,
460    cases: &[Case],
461    options: &RunOptions,
462) -> Result<Report> {
463    validate(questions, cases)?;
464    let requests = cases
465        .iter()
466        .map(|case| {
467            let request = Request::new(case.state.clone(), questions.clone())?;
468            #[cfg(feature = "images")]
469            let request = request.with_assets(options.assets.clone());
470            request.validate()?;
471            Ok(request)
472        })
473        .collect::<Result<Vec<_>>>()?;
474    let warmup = options.warmup.min(requests.len());
475    for request in requests.iter().take(warmup) {
476        ask(provider, request).await?;
477    }
478    let begin = Instant::now();
479    let mut observed: Vec<_> =
480        stream::iter(requests.iter().enumerate().map(|(i, request)| async move {
481            let start = Instant::now();
482            let answers = ask(provider, request).await?;
483            Ok::<_, Error>((i, answers, start.elapsed().as_secs_f64() * 1000.0))
484        }))
485        .buffer_unordered(options.concurrency.get())
486        .try_collect()
487        .await?;
488    let total_ms = begin.elapsed().as_secs_f64() * 1000.0;
489    observed.sort_unstable_by_key(|(i, _, _)| *i);
490    let (answers, latencies) = observed.into_iter().map(|(_, a, ms)| (a, ms)).unzip();
491    let mut report = score(questions, cases, answers, latencies, total_ms)?;
492    report.warmup_calls = warmup;
493    Ok(report)
494}
495/// One wrong completed flow result.
496#[derive(Clone, Debug)]
497pub struct FlowMiss<T> {
498    /// Case position.
499    pub case: usize,
500    /// Expected application result.
501    pub expected: T,
502    /// Observed completed result.
503    pub got: T,
504}
505/// Scored flow outcomes with their traces, expected results and latencies.
506#[derive(Clone, Debug)]
507pub struct FlowReport<T> {
508    results: Vec<FlowOutcome<T>>,
509    expected: Vec<T>,
510    latencies_ms: Vec<f64>,
511}
512impl<T: PartialEq + Clone> FlowReport<T> {
513    /// Completed or stopped outcomes, retaining every call trace.
514    pub fn results(&self) -> &[FlowOutcome<T>] {
515        &self.results
516    }
517    /// Expected application outcomes.
518    pub fn expected(&self) -> &[T] {
519        &self.expected
520    }
521    /// Latency per flow case, including all its calls.
522    pub fn latencies_ms(&self) -> &[f64] {
523        &self.latencies_ms
524    }
525    /// Number of evaluated cases.
526    pub fn total(&self) -> usize {
527        self.results.len()
528    }
529    /// Number of uncertainty stops.
530    pub fn stopped(&self) -> usize {
531        self.results.iter().filter(|r| !r.decided()).count()
532    }
533    /// Number of completed matching application values.
534    pub fn correct(&self) -> usize {
535        self.results
536            .iter()
537            .zip(&self.expected)
538            .filter(|(r, e)| r.decided() && r.value.as_ref() == Some(*e))
539            .count()
540    }
541    /// Wrong completed cases; stops are excluded.
542    pub fn misses(&self) -> Vec<FlowMiss<T>> {
543        self.results
544            .iter()
545            .zip(&self.expected)
546            .enumerate()
547            .filter_map(|(i, (r, e))| match &r.value {
548                Some(v) if r.decided() && v != e => Some(FlowMiss {
549                    case: i,
550                    expected: e.clone(),
551                    got: v.clone(),
552                }),
553                _ => None,
554            })
555            .collect()
556    }
557    /// Correct divided by completed cases, zero for no completed cases.
558    pub fn accuracy_when_answered(&self) -> f64 {
559        ratio(self.correct(), self.total() - self.stopped())
560    }
561    /// Total provider-call attempts retained in all flow traces.
562    pub fn calls(&self) -> usize {
563        self.results.iter().map(FlowOutcome::calls).sum()
564    }
565}
566/// Score existing flow outcomes. Inconsistent completion/value states and failed traces are rejected.
567///
568/// # Errors
569/// Returns `InvalidArgument` for mismatched outcome/label/latency lengths,
570/// invalid timing values, inconsistent completion/value states, or a trace
571/// containing a failed call.
572pub fn score_flows<T: PartialEq + Clone>(
573    results: Vec<FlowOutcome<T>>,
574    expected: Vec<T>,
575    latencies_ms: Vec<f64>,
576) -> Result<FlowReport<T>> {
577    if results.len() != expected.len() {
578        return Err(invalid("flow outcomes and labels must align"));
579    }
580    check_latencies(results.len(), &latencies_ms, 0.0)?;
581    for result in &results {
582        validate_flow_outcome(result)?;
583    }
584    Ok(FlowReport {
585        results,
586        expected,
587        latencies_ms,
588    })
589}
590fn validate_flow_outcome<T>(result: &FlowOutcome<T>) -> Result<()> {
591    if result.decided() != result.value.is_some()
592        || result.trace.iter().any(|step| step.result.is_err())
593    {
594        return Err(invalid(
595            "flow scoring requires consistent successful or stopped outcomes",
596        ));
597    }
598    Ok(())
599}
600/// Run caller-supplied flow futures sequentially, retaining traces and aborting on failure.
601/// There is no automatic flow warm-up or retry. Inputs are borrowed by each future.
602///
603/// # Errors
604/// Propagates a callback [`FlowFailure`] immediately. An inconsistent outcome
605/// or failed trace also becomes a failure, retaining that outcome's trace and
606/// stopping before the next case.
607pub async fn run_flow<'a, I, T, F, Fut>(
608    cases: &'a [(I, T)],
609    mut flow: F,
610) -> std::result::Result<FlowReport<T>, FlowFailure>
611where
612    T: PartialEq + Clone,
613    F: FnMut(&'a I) -> Fut,
614    Fut: Future<Output = std::result::Result<FlowOutcome<T>, FlowFailure>>,
615{
616    let mut results = Vec::with_capacity(cases.len());
617    let mut expected = Vec::with_capacity(cases.len());
618    let mut latencies = Vec::with_capacity(cases.len());
619    for (input, label) in cases {
620        let start = Instant::now();
621        let outcome = flow(input).await?;
622        if let Err(error) = validate_flow_outcome(&outcome) {
623            return Err(FlowFailure {
624                error,
625                trace: outcome.trace,
626            });
627        }
628        results.push(outcome);
629        latencies.push(start.elapsed().as_secs_f64() * 1000.0);
630        expected.push(label.clone());
631    }
632    // Every outcome is checked before another invocation; these aligned vectors
633    // and monotonic-clock latencies are constructed by this runner.
634    Ok(FlowReport {
635        results,
636        expected,
637        latencies_ms: latencies,
638    })
639}
640fn invalid(message: &str) -> Error {
641    Error::new(ErrorKind::InvalidArgument, message)
642}
643// CPython math.fsum-style nonoverlapping partials and half-even final correction.
644// Adapted from CPython math.fsum; see NOTICE.md and LICENSE-PYTHON.
645fn accurate_sum(values: impl IntoIterator<Item = f64>) -> Result<f64> {
646    let mut partials: Vec<f64> = Vec::new();
647    for mut x in values {
648        if !x.is_finite() {
649            return Err(invalid("benchmark sum is not finite"));
650        }
651        let mut i = 0;
652        for j in 0..partials.len() {
653            let mut y = partials[j];
654            if x.abs() < y.abs() {
655                std::mem::swap(&mut x, &mut y);
656            }
657            let hi = x + y;
658            if !hi.is_finite() {
659                return Err(invalid("benchmark sum overflow"));
660            }
661            let lo = y - (hi - x);
662            if lo != 0.0 {
663                partials[i] = lo;
664                i += 1;
665            }
666            x = hi;
667        }
668        partials.truncate(i);
669        partials.push(x);
670    }
671    let mut hi = partials.pop().unwrap_or(0.0);
672    let mut lo = 0.0;
673    while let Some(y) = partials.pop() {
674        let x = hi;
675        hi = x + y;
676        lo = y - (hi - x);
677        if lo != 0.0 {
678            break;
679        }
680    }
681    if partials
682        .last()
683        .is_some_and(|last| (lo < 0.0 && *last < 0.0) || (lo > 0.0 && *last > 0.0))
684    {
685        let y = lo * 2.0;
686        let x = hi + y;
687        if y == x - hi {
688            hi = x;
689        }
690    }
691    Ok(hi)
692}