Numeric error propagation
You are encouraged to solve this task according to the task description, using any language you may know.
If f, a, and b are values with uncertainties σf, σa, and σb. and c is a constant; then if f is derived from a, b, and c in the following ways, then σf can be calculated as follows:
- Addition/Subtraction
- If f = a ± c, or f = c ± a then σf = σa
- If f = a ± b then σf2 = σa2 + σb2
- Multiplication/Division
- If f = ca or f = ac then σf = |cσa|
- If f = ab or f = a / b then σf2 = f2( (σa / a)2 + (σb / b)2)
- Exponentiation
- If f = ac then σf = |fc(σa / a)|
- Task details
- Add an uncertain number type to your language that can support addition, subtraction, multiplication, division, and exponentiation between numbers with an associated error term together with 'normal' floating point numbers without an associated error term.
Implement enough functionality to perform the following calculations. - Given coordinates and their errors:
x1 = 100 ± 1.1
y1 = 50 ± 1.2
x2 = 200 ± 2.2
y2 = 100 ± 2.3
if point p1 is located at (x1, y1) and p2 is at (x2, y2); calculate the distance between the two points using the classic pythagorean formula:
d = √((x1 - x2)2 + (y1 - y2)2) - Print and display both d and its error.
- References
- A Guide to Error Propagation B. Keeney, 2005.
- Propagation of uncertainty Wikipedia.
- Cf.
Ada
Specification of a generic type Approximation.Number, providing all the operations required to solve the task .., and some more operations, for completeness.
<lang Ada>generic
type Real is digits <>; with function Sqrt(X: Real) return Real; with function "**"(X: Real; Y: Real) return Real;
package Approximation is
type Number is private;
-- create an approximation function Approx(Value: Real; Sigma: Real) return Number;
-- unary operations and conversion Real to Number function "+"(X: Real) return Number; function "-"(X: Real) return Number; function "+"(X: Number) return Number; function "-"(X: Number) return Number;
-- addition / subtraction function "+"(X: Number; Y: Number) return Number; function "-"(X: Number; Y: Number) return Number;
-- multiplication / division function "*"(X: Number; Y: Number) return Number; function "/"(X: Number; Y: Number) return Number;
-- exponentiation function "**"(X: Number; Y: Positive) return Number; function "**"(X: Number; Y: Real) return Number;
-- Output to Standard IO (wrapper for Ada.Text_IO and Ada.Text_IO.Float_IO) procedure Put_Line(Message: String; Item: Number; Value_Fore: Natural := 7; Sigma_Fore: Natural := 4; Aft: Natural := 2; Exp: Natural := 0); procedure Put(Item: Number; Value_Fore: Natural := 7; Sigma_Fore: Natural := 3; Aft: Natural := 2; Exp: Natural := 0);
private
type Number is record Value: Real; Sigma: Real; end record;
end Approximation;</lang>
The implementation:
<lang Ada>with Ada.Text_IO;
package body Approximation is
package RIO is new Ada.Text_IO.Float_IO(Real);
-- create an approximation
function Approx(Value: Real; Sigma: Real) return Number is begin return (Value, Sigma); end Approx;
-- unary operations and conversion Real to Number
function "+"(X: Real) return Number is begin return Approx(X, 0.0); end "+";
function "-"(X: Real) return Number is begin return Approx(-X, 0.0); end "-";
function "+"(X: Number) return Number is begin return X; end "+";
function "-"(X: Number) return Number is begin return Approx(-X.Value, X.Sigma); end "-";
-- addition / subtraction
function "+"(X: Number; Y: Number) return Number is Z: Number; begin Z.Value := X.Value + Y.Value; Z.Sigma := Sqrt(X.Sigma*X.Sigma + Y.Sigma*Y.Sigma); return Z; end "+";
function "-"(X: Number; Y: Number) return Number is begin return X + (-Y); end "-";
-- multiplication / division
function "*"(X: Number; Y: Number) return Number is Z: Number; begin Z.Value := X.Value * Y.Value; Z.Sigma := Z.Value * Sqrt((X.Sigma/X.Value)**2 + (Y.Sigma/Y.Value)**2); return Z; end "*";
function "/"(X: Number; Y: Number) return Number is Z: Number; begin Z.Value := X.Value / Y.Value; Z.Sigma := Z.Value * Sqrt((X.Sigma/X.Value)**2 + (Y.Sigma/Y.Value)**2); return Z; end "/";
-- exponentiation
function "**"(X: Number; Y: Positive) return Number is Z: Number; begin Z.Value := X.Value ** Y ; Z.Sigma := Z.Value * Real(Y) * (X.Sigma/X.Value); if Z.Sigma < 0.0 then Z.Sigma := - Z.Sigma; end if; return Z; end "**";
function "**"(X: Number; Y: Real) return Number is Z: Number; begin Z.Value := X.Value ** Y ; Z.Sigma := Z.Value * Y * (X.Sigma/X.Value); if Z.Sigma < 0.0 then Z.Sigma := - Z.Sigma; end if; return Z; end "**";
-- Output to Standard IO (wrapper for Ada.Text_IO.Float_IO)
procedure Put_Line(Message: String; Item: Number; Value_Fore: Natural := 7; Sigma_Fore: Natural := 4; Aft: Natural := 2; Exp: Natural := 0) is begin Ada.Text_IO.Put(Message); Put(Item, Value_Fore, Sigma_Fore, Aft, Exp); Ada.Text_IO.New_Line; end Put_Line;
procedure Put(Item: Number; Value_Fore: Natural := 7; Sigma_Fore: Natural := 3; Aft: Natural := 2; Exp: Natural := 0) is begin RIO.Put(Item.Value, Value_Fore, Aft, Exp); Ada.Text_IO.Put(" (+-"); RIO.Put(Item.Sigma, Sigma_Fore, Aft, Exp); Ada.Text_IO.Put(")"); end Put;
end Approximation;</lang>
Instantiating the package with Float operations, to compute the distance:
<lang Ada>with Approximation, Ada.Numerics.Elementary_Functions;
procedure Test_Approximations is
package A is new Approximation(Float, Ada.Numerics.Elementary_Functions.Sqrt, Ada.Numerics.Elementary_Functions."**"); use type A.Number; X1: A.Number := A.Approx(100.0, 1.1); Y1: A.Number := A.Approx( 50.0, 1.2); X2: A.Number := A.Approx(200.0, 2.2); Y2: A.Number := A.Approx(100.0, 2.3);
begin
A.Put_Line("Distance:", ((X1-X2)**2 + (Y1 - Y2)**2)**0.5, Sigma_Fore => 1);
end Test_Approximations;</lang>
Output:
Distance: 111.80 (+-2.49)
Go
Variance from task requirements is that the following does not "extend the language." It simply defines a type with associated functions and methods as required to solve the remainder of the task. <lang go>package main
import (
"fmt" "math"
)
// "uncertain number type" // a little optimization is to represent the error term with its square. // this saves some taking of square roots in various places. type unc struct {
n float64 // the number s float64 // *square* of one sigma error term
}
// constructor, nice to have so it can handle squaring of error term func newUnc(n, s float64) *unc {
return &unc{n, s * s}
}
// error term accessor method, nice to have so it can handle recovering // (non-squared) error term from internal (squared) representation func (z *unc) errorTerm() float64 {
return math.Sqrt(z.s)
}
// Arithmetic methods are modeled on the Go big number package. // The basic scheme is to pass all operands as method arguments, compute // the result into the method receiver, and then return the receiver as // the result of the method. This has an advantage of letting the programer // determine allocation and use of temporary objects, reducing garbage; // and has the convenience and efficiency of allowing operations to be chained.
// addition/subtraction func (z *unc) addC(a *unc, c float64) *unc {
*z = *a z.n += c return z
}
func (z *unc) subC(a *unc, c float64) *unc {
*z = *a z.n -= c return z
}
func (z *unc) addU(a, b *unc) *unc {
z.n = a.n + b.n z.s = a.s + b.s return z
} func (z *unc) subU(a, b *unc) *unc {
z.n = a.n - b.n z.s = a.s + b.s return z
}
// multiplication/division func (z *unc) mulC(a *unc, c float64) *unc {
z.n = a.n * c z.s = a.s * c * c return z
}
func (z *unc) divC(a *unc, c float64) *unc {
z.n = a.n / c z.s = a.s / (c * c) return z
}
func (z *unc) mulU(a, b *unc) *unc {
prod := a.n * b.n z.n, z.s = prod, prod*prod*(a.s/(a.n*a.n)+b.s/(b.n*b.n)) return z
}
func (z *unc) divU(a, b *unc) *unc {
quot := a.n / b.n z.n, z.s = quot, quot*quot*(a.s/(a.n*a.n)+b.s/(b.n*b.n)) return z
}
// exponentiation func (z *unc) expC(a *unc, c float64) *unc {
f := math.Pow(a.n, c) g := f * c / a.n z.n = f z.s = a.s * g * g return z
}
func main() {
x1 := newUnc(100, 1.1) x2 := newUnc(200, 2.2) y1 := newUnc(50, 1.2) y2 := newUnc(100, 2.3) var d, d2 unc d.expC(d.addU(d.expC(d.subU(x1, x2), 2), d2.expC(d2.subU(y1, y2), 2)), .5) fmt.Println("d: ", d.n) fmt.Println("error:", d.errorTerm())
}</lang> Output:
d: 111.80339887498948 error: 2.487167063146342
J
J's built in operators cannot be overloaded to deal with user defined types. So we will have to create new operators. Here's one approach, which is sufficient for this example:
First, we will need some utilities. num
will extract the number part of a number. unc
will extract the uncertainty part of a number, and will also be used to associate uncertainty with a number. dist
will compute the distance between two numbers (which is needed for multiplicative uncertainty).
<lang j>num=: {."1 unc=: {:@}."1 : ,. dist=: +/&.:*:</lang>
Jumping into the example values, for illustration purposes:
<lang j>x1=: 100 unc 1.1 y1=: 50 unc 1.2
x2=: 200 unc 2.2 y2=: 100 unc 2.3</lang>
Above, we see unc
being used to associate a number with its uncertainty. Here's how to take them apart again:
<lang j> num x1 100
unc x1
1.1</lang>
Note that these operations "do the right thing" for normal numbers:
<lang j> num 100 100
unc 100
0</lang>
And, a quick illustration of the distance function: <lang> 3 dist 4 5</lang>
Next, we need to define our arithmetic operations:
<lang j>add=: +&num unc dist&unc sub=: -&num unc dist&unc mul=: *&num unc |@(*&num * dist&(unc%num)) div=: %&num unc |@(%&num * dist&(unc%num)) exp=: ^&num unc |@(^&num * dist&(unc%num))</lang>
Finally, our required example:
<lang j> exp&0.5 (x1 sub x2) add&(exp&2) y1 sub y2 111.803 2.48717</lang>
Perl
Following code keeps track of covariance between variables. Each variable with error contains its mean value and components of error source from a set of indepentent variables. It's more than what the task requires. <lang perl>use utf8; package ErrVar; use strict;
- helper function, apply f to pairs (a, b) from listX and listY
sub zip(&$$) { my ($f, $x, $y) = @_; my $l = $#$x; if ($l < $#$y) { $l = $#$y };
my @out; for (0 .. $l) { local $a = $x->[$_]; local $b = $y->[$_]; push @out, $f->(); } \@out }
use overload '""' => \&_str, '+' => \&_add, '-' => \&_sub, '*' => \&_mul, '/' => \&_div, 'bool' => \&_bool, '<=>' => \&_ncmp, 'neg' => \&_neg,
'sqrt' => \&_sqrt, 'log' => \&_log, 'exp' => \&_exp, '**' => \&_pow,
- make a variable with mean value and a list of coefficient to
- variables providing independent errors
sub make { my $x = shift; bless [$x, [@{+shift}]] }
sub _str { sprintf "%g±%.3g", $_[0][0], sigma($_[0]) }
- mean value of the var, or just the input if it's not of this class
sub mean { my $x = shift; ref($x) && $x->isa(__PACKAGE__) ? $x->[0] : $x }
- return variance index array
sub vlist { my $x = shift; ref($x) && $x->isa(__PACKAGE__) ? $x->[1] : []; }
sub variance { my $x = shift; return 0 unless ref($x) and $x->isa(__PACKAGE__); my $s; $s += $_ * $_ for (@{$x->[1]}); $s }
sub covariance { my ($x, $y) = @_; return 0 unless ref($x) && $x->isa(__PACKAGE__); return 0 unless ref($y) && $y->isa(__PACKAGE__);
my $s; zip { $s += $a * $b } vlist($x), vlist($y); $s }
sub sigma { sqrt variance(shift) }
- to determine if a var is probably zero. we use 1σ here
sub _bool { my $x = shift; return abs(mean($x)) > sigma($x); }
sub _ncmp { my $x = shift() - shift() or return 0; return mean($x) > 0 ? 1 : -1; }
sub _neg { my $x = shift; bless [ -mean($x), [map(-$_, @{vlist($x)}) ] ]; }
sub _add { my ($x, $y) = @_; my ($x0, $y0) = (mean($x), mean($y)); my ($xv, $yv) = (vlist($x), vlist($y)); bless [$x0 + $y0, zip {$a + $b} $xv, $yv]; }
sub _sub { my ($x, $y, $swap) = @_; if ($swap) { ($x, $y) = ($y, $x) } my ($x0, $y0) = (mean($x), mean($y)); my ($xv, $yv) = (vlist($x), vlist($y)); bless [$x0 - $y0, zip {$a - $b} $xv, $yv]; }
sub _mul { my ($x, $y) = @_; my ($x0, $y0) = (mean($x), mean($y)); my ($xv, $yv) = (vlist($x), vlist($y));
$xv = [ map($y0 * $_, @$xv) ]; $yv = [ map($x0 * $_, @$yv) ];
bless [$x0 * $y0, zip {$a + $b} $xv, $yv]; }
sub _div { my ($x, $y, $swap) = @_; if ($swap) { ($x, $y) = ($y, $x) }
my ($x0, $y0) = (mean($x), mean($y)); my ($xv, $yv) = (vlist($x), vlist($y));
$xv = [ map($_/$y0, @$xv) ]; $yv = [ map($x0 * $_/$y0/$y0, @$yv) ];
bless [$x0 / $y0, zip {$a + $b} $xv, $yv]; }
sub _sqrt { my $x = shift; my $x0 = mean($x); my $xv = vlist($x); $x0 = sqrt($x0); $xv = [ map($_ / 2 / $x0, @$xv) ]; bless [$x0, $xv] }
sub _pow { my ($x, $y, $swap) = @_; if ($swap) { ($x, $y) = ($y, $x) } if ($x < 0) { if (int($y) != $y || ($y & 1)) { die "Can't take pow of negative number $x"; } $x = -$x; } exp($y * log $x) }
sub _exp { my $x = shift; my $x0 = exp(mean($x)); my $xv = vlist($x); bless [ $x0, [map($x0 * $_, @$xv) ] ] }
sub _log { my $x = shift; my $x0 = mean($x); my $xv = vlist($x); bless [ log($x0), [ map($_ / $x0, @$xv) ] ] }
"If this package were to be in its own file, you need some truth value to end it like this.";
package main;
sub e { ErrVar::make @_ };
- x1 is of mean value 100, containing error 1.1 from source 1, etc.
- all error sources are independent.
my $x1 = e 100, [1.1, 0, 0, 0 ]; my $x2 = e 200, [0, 2.2, 0, 0 ]; my $y1 = e 50, [0, 0, 1.2, 0 ]; my $y2 = e 100, [0, 0, 0, 2.3];
my $z1 = sqrt(($x1 - $x2) ** 2 + ($y1 - $y2) ** 2); print "distance: $z1\n\n";
- this is not for task requirement
my $a = $x1 + $x2; my $b = $y1 - 2 * $x2; print "covariance between $a and $b: ", $a->covariance($b), "\n";</lang>output<lang>distance: 111.803±2.49
covariance between 300±2.46 and -350±4.56: -9.68</lang>
Python
<lang python>from collections import namedtuple import math
class I(namedtuple('Imprecise', 'value, delta')):
'Imprecise type: I(value=0.0, delta=0.0)' __slots__ = () def __new__(_cls, value=0.0, delta=0.0): 'Defaults to 0.0 ± delta' return super().__new__(_cls, float(value), abs(float(delta))) def reciprocal(self): return I(1. / self.value, self.delta / (self.value**2)) def __str__(self): 'Shorter form of Imprecise as string' return 'I(%g, %g)' % self def __neg__(self): return I(-self.value, self.delta) def __add__(self, other): if type(other) == I: return I( self.value + other.value, (self.delta**2 + other.delta**2)**0.5 ) try: c = float(other) except: return NotImplemented return I(self.value + c, self.delta)
def __sub__(self, other): return self + (-other) def __radd__(self, other): return I.__add__(self, other) def __mul__(self, other): if type(other) == I: #if id(self) == id(other): # return self ** 2 a1,b1 = self a2,b2 = other f = a1 * a2 return I( f, f * ( (b1 / a1)**2 + (b2 / a2)**2 )**0.5 ) try: c = float(other) except: return NotImplemented return I(self.value * c, self.delta * c) def __pow__(self, other): if type(other) == I: return NotImplemented try: c = float(other) except: return NotImplemented f = self.value ** c return I(f, f * c * (self.delta / self.value)) def __rmul__(self, other): return I.__mul__(self, other) def __truediv__(self, other): if type(other) == I: return self.__mul__(other.reciprocal()) try: c = float(other) except: return NotImplemented return I(self.value / c, self.delta / c) def __rtruediv__(self, other): return other * self.reciprocal() __div__, __rdiv__ = __truediv__, __rtruediv__
Imprecise = I
def distance(p1, p2):
x1, y1 = p1 x2, y2 = p2 return ((x1 - x2)**2 + (y1 - y2)**2)**0.5
x1 = I(100, 1.1) x2 = I(200, 2.2) y1 = I( 50, 1.2) y2 = I(100, 2.3)
p1, p2 = (x1, y1), (x2, y2) print("Distance between points\n p1: %s\n and p2: %s\n = %r" % (
p1, p2, distance(p1, p2)))</lang>
- Sample output
Distance between points p1: (I(value=100.0, delta=1.1), I(value=50.0, delta=1.2)) and p2: (I(value=200.0, delta=2.2), I(value=100.0, delta=2.3)) = I(value=111.80339887498948, delta=2.4871670631463423)
Tcl
Firstly, some support code for doing RAII-like things, evolved from code in the quaternion solution: <lang tcl>package require Tcl 8.6 oo::class create RAII-support {
constructor {} {
upvar 1 { end } end lappend end [self] trace add variable end unset [namespace code {my DieNicely}]
} destructor {
catch { upvar 1 { end } end trace remove variable end unset [namespace code {my DieNicely}] }
} method return Template:Level 1 {
incr level upvar 1 { end } end upvar $level { end } parent trace remove variable end unset [namespace code {my DieNicely}] lappend parent [self] trace add variable parent unset [namespace code {my DieNicely}] return -level $level [self]
} # Swallows arguments method DieNicely args {tailcall my destroy}
} oo::class create RAII-class {
superclass oo::class method return args {
[my new {*}$args] return 2
} method unknown {m args} {
if {[string is double -strict $m]} { return [tailcall my new $m {*}$args] } next $m {*}$args
} unexport create unknown self method create args {
set c [next {*}$args] oo::define $c superclass {*}[info class superclass $c] RAII-support return $c
}
}
- Makes a convenient scope for limiting RAII lifetimes
proc scope {script} {
foreach v [info global] {
if {[array exists ::$v] || [string match { * } $v]} continue lappend vars $v lappend vals [set ::$v]
} tailcall apply [list $vars [list \
try $script on ok msg {$msg return}
] [uplevel 1 {namespace current}]] {*}$vals
}</lang> The implementation of the number+error class itself: <lang tcl>RAII-class create Err {
variable N E constructor {number {error 0.0}} {
next namespace import ::tcl::mathfunc::* ::tcl::mathop::* variable N $number E [abs $error]
} method p {} {
return "$N \u00b1 $E"
}
method n {} { return $N } method e {} { return $E }
method + e {
if {[info object isa object $e]} { Err return [+ $N [$e n]] [hypot $E [$e e]] } else { Err return [+ $N $e] $E }
} method - e {
if {[info object isa object $e]} { Err return [- $N [$e n]] [hypot $E [$e e]] } else { Err return [- $N $e] $E }
} method * e {
if {[info object isa object $e]} { set f [* $n [$E n]] Err return $f [expr {hypot($E*$f/$N, [$e e]*$f/[$e n])}] } else { Err return [* $N $e] [abs [* $E $e]] }
} method / e {
if {[info object isa object $e]} { set f [/ $n [$E n]] Err return $f [expr {hypot($E*$f/$N, [$e e]*$f/[$e n])}] } else { Err return [/ $N $e] [abs [/ $E $e]] }
} method ** c {
set f [** $N $c] Err return $f [abs [* $f $c [/ $E $N]]]
}
export + - * / **
}</lang> Demonstrating: <lang tcl>set x1 [Err 100 1.1] set x2 [Err 200 2.2] set y1 [Err 50 1.2] set y2 [Err 100 2.3]
- Evaluate in a local context to clean up intermediate objects
set d [scope {
[[[$x1 - $x2] ** 2] + [[$y1 - $y2] ** 2]] ** 0.5
}] puts "d = [$d p]"</lang> Output:
d = 111.80339887498948 ± 2.4871670631463423