Class: Measurand

Inherits:
Object
  • 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.

Raises:

  • (ArgumentError)


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

#absObject



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.

Returns:

  • (Boolean)


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

Returns:

  • (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

Returns:

  • (Boolean)


68
69
70
# File 'lib/measurand.rb', line 68

def exact?
  uncertainty.zero?
end

#hashObject



124
125
126
# File 'lib/measurand.rb', line 124

def hash
  [@value, uncertainty].hash
end

#inspectObject



59
60
61
# File 'lib/Measurand/Format.rb', line 59

def inspect
  "Measurand(#{@value}, #{uncertainty})"
end

#overlaps?(other) ⇒ Boolean

Returns:

  • (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_uncertaintyObject



63
64
65
66
# File 'lib/measurand.rb', line 63

def relative_uncertainty
  return 0.0 if uncertainty.zero?
  uncertainty / @value.abs
end

#to_parentheticObject



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

#uncertaintyObject



59
60
61
# File 'lib/measurand.rb', line 59

def uncertainty
  @uncertainty ||= ::Math.sqrt(@partials.sum{|source, partial| (partial * source.uncertainty) ** 2})
end

#valueObject



55
56
57
# File 'lib/measurand.rb', line 55

def value
  @value
end