@@ -466,4 +466,107 @@ public void ZeroHandling()
466466
467467 Assert . Equal ( 2 , histogram . ZeroCount ) ;
468468 }
469+
470+ [ Fact ]
471+ public void ScaleOneIndexesWithPowerOfTwoLowerBound ( )
472+ {
473+ /*
474+ The range of indexes tested is fixed to evaluate values recorded
475+ within [2^-25, 2^25] or approximately [0.00000002980232239, 33554432].
476+
477+ For perspective, assume the unit of values recorded is seconds then this
478+ test represents the imprecision of MapToIndex for values recorded between
479+ approximately 29.80 nanoseconds and 1.06 years.
480+
481+ The output of this test is as follows:
482+
483+ Scale: 1
484+ Indexes per power of 2: 2
485+ Index range: [-50, 50]
486+ Value range: [2.9802322387695312E-08, 33554432]
487+ Successes: 18
488+ Failures: 33
489+ Average number of values near a bucket boundary that are off by one: 2.878787878787879
490+ Average range of values near a bucket boundary that are off by one: 3.0880480139955346E-09
491+
492+ That is, there are ~2.89 values near each bucket boundary tested that
493+ are mapped to an index off by one. The range of these incorrectly mapped
494+ values, assuming seconds as the unit, is ~3.09 nanoseconds.
495+ */
496+
497+ // This test only tests scale 1, but it can be adjusted to test any
498+ // positive scale by changing the scale of the histogram. The output
499+ // and results are identical for all positive scales.
500+ var scale = 1 ;
501+ var histogram = new Base2ExponentialBucketHistogram ( scale : scale ) ;
502+
503+ // These are used to capture stats for an analysis for where MapToIndex is off by one.
504+ var successes = 0 ;
505+ var failures = 0 ;
506+ var diffs = new List < double > ( ) ;
507+ var numValuesOffByOne = new List < int > ( ) ;
508+
509+ // Only indexes with a lower bound that is an exact power of two are tested.
510+ var indexesPerPowerOf2 = 1 << scale ;
511+ var exp = - 25 ;
512+
513+ var index = exp * indexesPerPowerOf2 ;
514+ var endIndex = Math . Abs ( index ) ;
515+ var lowerBound = Math . Pow ( 2 , exp ) ;
516+
517+ this . output . WriteLine ( string . Empty ) ;
518+ this . output . WriteLine ( $ "Scale: { scale } ") ;
519+ this . output . WriteLine ( $ "Indexes per power of 2: { indexesPerPowerOf2 } ") ;
520+ this . output . WriteLine ( $ "Index range: [{ index } , { endIndex } ]") ;
521+ this . output . WriteLine ( $ "Value range: [{ lowerBound } , { Math . Pow ( 2 , Math . Abs ( exp ) ) } ]") ;
522+
523+ for ( ; index <= endIndex ; index += indexesPerPowerOf2 , lowerBound = Math . Pow ( 2 , ++ exp ) )
524+ {
525+ // Buckets are lower bound exclusive, therefore
526+ // MapToIndex(LowerBoundOfBucketN) = IndexOfBucketN - 1.
527+ Assert . Equal ( index - 1 , histogram . MapToIndex ( lowerBound ) ) ;
528+
529+ // If MapToIndex was mathematically precise, the following assertion would pass.
530+ // BitIncrement(lowerBound) increments lowerBound by the smallest increment possible.
531+ // MapToIndex(BitIncrement(LowerBoundOfBucketN)) should equal IndexOfBucketN.
532+ // However, because MapToIndex at positive scales is imprecise, the assertion can fail
533+ // for values very close to a bucket boundary.
534+
535+ // Assert.Equal(index, histogram.MapToIndex(BitIncrement(lowerBound)));
536+
537+ // Knowing that MapToIndex is imprecise near bucket boundaries,
538+ // the following produces an analysis of the magnitude of imprecision.
539+
540+ var incremented = BitIncrement ( lowerBound ) ;
541+
542+ if ( index == histogram . MapToIndex ( incremented ) )
543+ {
544+ // This is a scenario where the assertion above would have passed.
545+ ++ successes ;
546+ }
547+ else
548+ {
549+ // This is a scenario where the assertion above would have failed.
550+ ++ failures ;
551+
552+ // Count the number of values near the bucket boundary
553+ // for which MapToIndex produces a result that is off by one.
554+ var increments = 1 ;
555+ while ( index != histogram . MapToIndex ( incremented ) )
556+ {
557+ incremented = BitIncrement ( incremented ) ;
558+ increments ++ ;
559+ }
560+
561+ // Capture stats for this bucket index.
562+ numValuesOffByOne . Add ( increments - 1 ) ;
563+ diffs . Add ( incremented - lowerBound ) ;
564+ }
565+ }
566+
567+ this . output . WriteLine ( $ "Successes: { successes } ") ;
568+ this . output . WriteLine ( $ "Failures: { failures } ") ;
569+ this . output . WriteLine ( $ "Average number of values near a bucket boundary that are off by one: { numValuesOffByOne . Average ( ) } ") ;
570+ this . output . WriteLine ( $ "Average range of values near a bucket boundary that are off by one: { diffs . Average ( ) } ") ;
571+ }
469572}
0 commit comments