Junctions for Interval Arithmetic

Body mass index for a person 1.80 m tall in relation to body weight m (in kilograms)

Recently, there was a discussion asking why, if raku Ranges can be used in arithmetic operations with Real values …

(1..2) + 3 => (4..5)

And yet when it’s a calculation with two Ranges, the operator applies Numeric context which then does the calculation on the length .elems and not the contents! …

(1..2) + (3..4) => 4   #not (4..6)!

Only when one of the participants pointed me to the excellent Wikipedia article on Interval Arithmetic did it dawn on me why. I encourage anyone who uses real-world measurements with errors and uncertainty to read that page before continuing.

Let’s take it one step at a time:

Addition

That seems simple enough, here it is in raku code from the Math::Interval module:

method add {
    Interval.new: (x1 + y1) .. (x2 + y2)
}

---
use Math::Interval;

say (1..2) + (2..4);        #3..6

Subtraction

Hmmm – that’s a tad counter intuitive, not quite as simple as making a new Range by subtracting start of one from the start of the other and the end of one from the end of the other. I had to reread the Wikipedia page to grok this. And this potential for confusion is why I suspect that the raku language designers decided to duck the idea of having Range op Range in the language core.

It was about now that I thought – OK I think we need Range op Range operations as Interval op Interval, but not in the raku core – and so I started work on a new module – Math::Interval

Anyway, here’s the code for subtract:

method sub {
    Interval.new: (x1 - y2) .. (x2 - y1)
}

---
use Math::Interval;

say (2..4) - (1..2);        #0..3  (yes - this is weird!)

Multiply

The weirdness of Interval Arithmetic steps up another notch with multiply – now its a min, max of the cross product over the Range|Interval arguments endpoints:

method mul {
    #| make cross product, ie. x0*y0,x1*y0...
    @!xXy = (x1, x2) X* (y1, y2);

    Interval.new: @!xXy.min .. @!xXy.max
}

---
use Math::Interval;

say (2..4) * (1..2);        #2..8

Again, the Wikipedia page does a great job of explaining why.

Divide

And then I felt my brain explode:

So, we need to unpack this a little:

  • the tricky part is the inverse (1/[y1..y2])
  • things are OK until 0 appears in the span or endpoints of the divisor interval
  • so this is like divide by zero on steroids
  • if either endpoint y1 or y2 are 0 then we can use ±Inf
  • if both are 0 that’s a divide by zero error
  • but if they span 0, then the result is a disjoint multi-interval

A What?

I went to bed that night feeling afraid, very afraid… and then bingo I realised that this is a job for raku Junctions

… I also realised that a variant of the built in raku class Range was needed – since a continuous class Interval should not have Positional and Iterator roles.

Junctions

According to the raku guide

8.6. Junctions

A junction is a logical superposition of values.

In the below example 1|2|3 is a junction.

my $var = 2;
if $var == 1|2|3 {
say “The variable is 1 or 2 or 3”
}
The use of junctions usually triggers autothreading; the operation is carried out for each junction element, and all the results are combined into a new junction and returned.

https://raku.guide/#_junctions

Junction of Intervals

So, to cut a long story short, here is the module code:

#| make inverse, ie. 1/[y1..y2]
sub inverse($y) {
    my \ss = (y1.sign == y2.sign);      # same sign

    given       y1, y2  {
        # continuous
        when    !0, !0  &&  ss  { Interval.new: 1/y2 .. 1/y1 }
        when    !0,  0          { Interval.new: -Inf .. 1/y1 }
        when     0, !0          { Interval.new: 1/y2 .. Inf  }

        # disjoint
        when    !0, !0  && !ss  {
            warn "divisor contains 0, returning a multi Interval Junction";
            Interval.new(-Inf..1/y1) | Interval.new(1/y2..Inf)
        }

        # div 0 error
            when     0,  0   { die "Divide by zero attempt." }
    }
}

method div {
    Interval.new: $!x * inverse($!y)
}

Now, here is a naughty divide in action:

# a divisor that spans 0 produces a disjoint multi-interval
my $j1 = (2..4)/(-2..4);
ddt $j1;            #any(-Inf..-1.0, 0.5..Inf).Junction
say 3 ~~ $j1;       #True

# Junction[Interval] can still be used
my $j2 = $j1 + 2;
ddt $j2;            #any(-Inf..1.0, 2.5..Inf).Junction
say 5 ~~ $j2;       #True

# but this can only go so far...
my $j3 = $j1 * (-2..4);
ddt $j3;            #any(-Inf..Inf, -Inf..Inf).Junction
say 3 ~~ $j3;       #True (but meaningless!)

Conclusion

Anyway, for me, it was an amazing surprise that raku Junctions could be so helpful to represent these disjoint multi-intervals. I get the feeling that I am walking in the footsteps of some very wise language design (both to provide the tool of Junction but also not to overstep the remit of a general purpose coding language).

Here are a couple of extra links if you would like to learn a bit more about Junctions:

And, as ever, please do comment, like and feedback on this post.

~librasteve

1 Comment

Leave a Comment