Class: Measurand
- Inherits:
-
Object
show all
- Includes:
- Comparable
- Defined in:
- lib/measurand.rb,
lib/Measurand/Math.rb,
lib/Measurand/Parse.rb,
lib/Measurand/Format.rb,
lib/Measurand/Source.rb,
lib/Measurand/Numeric.rb,
lib/Measurand/VERSION.rb
Overview
An independent source of uncertainty. Its identity is load-bearing: two
Sources are never equal unless they are the same object, so error
propagation can tell whether the same measurement reappears in an
expression. Deliberately does not define ==, eql? or hash.
Defined Under Namespace
Modules: Enumerable, Format, Math, Numeric
Classes: Source
Constant Summary
collapse
- NUMBER =
'[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?'
- PLUS_MINUS =
'(?:±|\+/-|\+-)'
- VERSION =
'0.0.0'
Class Method Summary
collapse
Instance Method Summary
collapse
Class Method Details
.cast(values, uncertainty) ⇒ Object
37
38
39
|
# File 'lib/measurand.rb', line 37
def cast(values, uncertainty)
values.map{|value| new(value, uncertainty)}
end
|
.cast_relative(values, relative_uncertainty) ⇒ Object
41
42
43
|
# File 'lib/measurand.rb', line 41
def cast_relative(values, relative_uncertainty)
values.map{|value| relative(value, relative_uncertainty)}
end
|
.decimal_places(number) ⇒ Object
34
35
36
37
|
# File 'lib/Measurand/Parse.rb', line 34
def self.decimal_places(number)
point = number.index('.')
point ? number.length - point - 1 : 0
end
|
.derived(value, partials) ⇒ Object
Bypasses the one-source initializer to build a Measurand directly from
a value and an already-computed partials map. Used by every operator.
25
26
27
28
29
|
# File 'lib/measurand.rb', line 25
def derived(value, partials)
measurand = allocate
measurand.send(:build, value, partials)
measurand
end
|
.from_samples(values) ⇒ Object
Mean +/- standard error of the mean, from repeated readings.
46
47
48
49
50
51
52
|
# File 'lib/measurand.rb', line 46
def from_samples(values)
raise ArgumentError, "need at least two samples" if values.size < 2
floats = values.map(&:to_f)
mean = floats.sum / floats.size
variance = floats.map{|value| (value - mean) ** 2}.sum / (floats.size - 1)
new(mean, ::Math.sqrt(variance / floats.size))
end
|
.parse(string) ⇒ Object
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
# File 'lib/Measurand/Parse.rb', line 17
def self.parse(string)
string = string.strip
case string
when /\A(#{NUMBER})\((\d+)\)\z/
value, digits = $1, $2
new(value.to_f, digits.to_i * 10.0 ** -decimal_places(value))
when /\A(#{NUMBER})\s*#{PLUS_MINUS}\s*(#{NUMBER})%\z/
relative($1.to_f, $2.to_f / 100)
when /\A(#{NUMBER})\s*#{PLUS_MINUS}\s*(#{NUMBER})\z/
new($1.to_f, $2.to_f)
when /\A#{NUMBER}\z/
new(string.to_f)
else
raise ArgumentError, "can't parse measurand: #{string.inspect}"
end
end
|
.relative(value, relative_uncertainty) ⇒ Object
A value whose uncertainty is a fraction of the value itself, which is
how instruments are usually specified: relative(5.0, 0.02) is 5.0 +/- 2%.
33
34
35
|
# File 'lib/measurand.rb', line 33
def relative(value, relative_uncertainty)
new(value, value.abs * relative_uncertainty)
end
|
Instance Method Details
#*(other) ⇒ Object
82
83
84
85
|
# File 'lib/measurand.rb', line 82
def *(other)
other = wrap(other)
self.class.derived(@value * other.value, combine(other.value, other, @value))
end
|
#**(other) ⇒ Object
92
93
94
95
96
97
98
99
100
|
# File 'lib/measurand.rb', line 92
def **(other)
other = wrap(other)
dself = other.value * @value ** (other.value - 1)
if other.exact?
self.class.derived(@value ** other.value, scale(dself))
else
self.class.derived(@value ** other.value, combine(dself, other, @value ** other.value * ::Math.log(@value)))
end
end
|
#+(other) ⇒ Object
72
73
74
75
|
# File 'lib/measurand.rb', line 72
def +(other)
other = wrap(other)
self.class.derived(@value + other.value, combine(1.0, other, 1.0))
end
|
#-(other) ⇒ Object
77
78
79
80
|
# File 'lib/measurand.rb', line 77
def -(other)
other = wrap(other)
self.class.derived(@value - other.value, combine(1.0, other, -1.0))
end
|
#-@ ⇒ Object
102
103
104
|
# File 'lib/measurand.rb', line 102
def -@
self.class.derived(-@value, scale(-1.0))
end
|
#/(other) ⇒ Object
87
88
89
90
|
# File 'lib/measurand.rb', line 87
def /(other)
other = wrap(other)
self.class.derived(@value / other.value, combine(1.0 / other.value, other, -@value.to_f / (other.value ** 2)))
end
|
#<=>(other) ⇒ Object
By value only, so Comparable's < > sort etc. order by magnitude. Note this
is intentionally looser than ==: two measurands with equal values but
different uncertainties compare 0 here yet are not ==.
131
132
133
134
135
|
# File 'lib/measurand.rb', line 131
def <=>(other)
other = wrap(other) if other.is_a?(Numeric)
return nil unless other.is_a?(Measurand)
@value <=> other.value
end
|
#==(other) ⇒ Object
116
117
118
|
# File 'lib/measurand.rb', line 116
def ==(other)
other.is_a?(Measurand) && @value == other.value && uncertainty == other.uncertainty
end
|
#abs ⇒ Object
106
107
108
|
# File 'lib/measurand.rb', line 106
def abs
self.class.derived(@value.abs, scale(@value < 0 ? -1.0 : 1.0))
end
|
#apply(value, derivative) ⇒ Object
Applies a differentiable unary function: the caller supplies f(value) and
f'(value). This is how Measurand::Math builds transcendentals.
112
113
114
|
# File 'lib/measurand.rb', line 112
def apply(value, derivative)
self.class.derived(value, scale(derivative))
end
|
#coerce(other) ⇒ Object
Ruby calls this when a bare number is on the left: 2 * measurand.
153
154
155
|
# File 'lib/measurand.rb', line 153
def coerce(other)
[wrap(other), self]
end
|
#consistent_with?(other, sigmas: 1) ⇒ Boolean
Are these the same measurement within error? Uses subtraction so shared
sources correlate correctly, then asks whether zero lies within sigmas of
the difference.
140
141
142
143
|
# File 'lib/measurand.rb', line 140
def consistent_with?(other, sigmas: 1)
difference = self - wrap(other)
difference.value.abs <= sigmas * difference.uncertainty
end
|
#eql?(other) ⇒ Boolean
120
121
122
|
# File 'lib/measurand.rb', line 120
def eql?(other)
other.instance_of?(Measurand) && @value.eql?(other.value) && uncertainty.eql?(other.uncertainty)
end
|
#exact? ⇒ Boolean
68
69
70
|
# File 'lib/measurand.rb', line 68
def exact?
uncertainty.zero?
end
|
#hash ⇒ Object
124
125
126
|
# File 'lib/measurand.rb', line 124
def hash
[@value, uncertainty].hash
end
|
#inspect ⇒ Object
59
60
61
|
# File 'lib/Measurand/Format.rb', line 59
def inspect
"Measurand(#{@value}, #{uncertainty})"
end
|
#overlaps?(other) ⇒ Boolean
145
146
147
148
149
150
|
# File 'lib/measurand.rb', line 145
def overlaps?(other)
other = wrap(other)
low, high = @value - uncertainty, @value + uncertainty
other_low, other_high = other.value - other.uncertainty, other.value + other.uncertainty
low <= other_high && other_low <= high
end
|
#relative_uncertainty ⇒ Object
63
64
65
66
|
# File 'lib/measurand.rb', line 63
def relative_uncertainty
return 0.0 if uncertainty.zero?
uncertainty / @value.abs
end
|
#to_parenthetic ⇒ Object
51
52
53
54
55
56
57
|
# File 'lib/Measurand/Format.rb', line 51
def to_parenthetic
return @value.to_s if exact?
rounded_uncertainty, place = Format.round_uncertainty(uncertainty)
digits = Format.decimals(place)
parenthetic_digits = (rounded_uncertainty / 10.0 ** place).round
"#{fixed(Format.round_to_place(@value, place), digits)}(#{parenthetic_digits})"
end
|
#to_s(ascii: false) ⇒ Object
44
45
46
47
48
49
|
# File 'lib/Measurand/Format.rb', line 44
def to_s(ascii: false)
return @value.to_s if exact?
rounded_uncertainty, place = Format.round_uncertainty(uncertainty)
digits = Format.decimals(place)
"#{fixed(Format.round_to_place(@value, place), digits)} #{ascii ? '+/-' : '±'} #{fixed(rounded_uncertainty, digits)}"
end
|
#uncertainty ⇒ Object
59
60
61
|
# File 'lib/measurand.rb', line 59
def uncertainty
@uncertainty ||= ::Math.sqrt(@partials.sum{|source, partial| (partial * source.uncertainty) ** 2})
end
|
#value ⇒ Object
55
56
57
|
# File 'lib/measurand.rb', line 55
def value
@value
end
|