measurand 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 77e11fd475e1104e6ff1b26e9d0528c9e6918a0340832106b58eefe09b46da1e
4
+ data.tar.gz: ca6538ff0f68ec369df90ced58ab97aa8ed4083680a3c4f4f555f5af80b7ee63
5
+ SHA512:
6
+ metadata.gz: 48bf9b4e4110cede8150f8abd1521d39e9e39c701d3db7cd0e9697fef8e59aba0e2ac21dd60590b8b71be8d6845bc14212ecc896f1a3085c45cb85ca433dc19e
7
+ data.tar.gz: 99f5e0811d117c252c0d703b144f30f1283f72d47ffb65b2b07e27d603357f7ac6cc4aa4c4413e1754c54d9734a2f7e9cf7db3abf754c89eb74fae73dc233762
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # CHANGELOG
2
+
3
+ ## [0.0.0] - 20260716
4
+ ## + Measurand: measured values with uncertainty and GUM-conformant error propagation.
5
+
6
+ 1. + lib/measurand.rb: Measurand — forward-mode automatic differentiation over a {Source => partial} map; the uncertainty is derived from the partials, not stored, so shared sources correlate (x - x is exactly 0 ± 0).
7
+ 2. + Measurand construction: .new, .relative, .cast, .cast\_relative, .from\_samples (mean ± standard error of the mean), .derived
8
+ 3. + Measurand arithmetic: +, -, \*, /, \*\*, -@, abs, apply — chain-rule propagation; independent uncertainties combine in quadrature; scalars are exact
9
+ 4. + Measurand comparison: ==/eql?/hash (exact: value and uncertainty), <=> (by value, Comparable), consistent\_with?, overlaps?, coerce (scalar-on-the-left arithmetic)
10
+ 5. + Measurand readers: value, uncertainty, relative\_uncertainty, exact?
11
+ 6. + lib/Measurand/Source.rb: opaque per-source identity carrying its own uncertainty; deliberately no ==/eql?/hash, so shared sources correlate and independently-constructed ones never collapse
12
+ 7. + lib/Measurand/Format.rb: uncertainty-driven, PDG-rounded to\_s (ascii: keyword emits +/- instead of ±), to\_parenthetic, inspect (full precision)
13
+ 8. + lib/Measurand/Parse.rb: Measurand.parse for ±, +/-, parenthetic (10), relative %, and exact forms; raises ArgumentError rather than returning nil
14
+ 9. + lib/Measurand/Math.rb: Measurand::Math.sqrt, exp, log, log10, sin, cos, tan — chain-rule propagation with no Math monkeypatching
15
+ 10. + lib/Measurand/Numeric.rb: opt-in Numeric#pm / #± and Enumerable#pm (separate require, so nothing is monkeypatched unless asked)
16
+ 11. + lib/Measurand/VERSION.rb
17
+ 12. + measurand.gemspec
18
+ 13. + Gemfile
19
+ 14. + LICENSE
20
+ 15. + README.md
21
+ 16. + test/test\_helper.rb, test/test\_all.rb
22
+ 17. + test/Measurand/Measurand\_test.rb
23
+ 18. + test/Measurand/Source\_test.rb
24
+ 19. + test/Measurand/Format\_test.rb
25
+ 20. + test/Measurand/Parse\_test.rb
26
+ 21. + test/Measurand/Math\_test.rb
27
+ 22. + test/Measurand/Numeric\_test.rb
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://www.rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thoran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # measurand
2
+
3
+ Measured values with uncertainty, and GUM-conformant error propagation.
4
+
5
+ A `Measurand` is a value with an associated uncertainty. Arithmetic on
6
+ measurands propagates the uncertainty correctly, so a calculation chain can
7
+ tell the truth about how well its result is known.
8
+
9
+ Uncertainty is propagated by first-order (GUM) error propagation over an
10
+ automatic-differentiation graph. Because each measurand remembers which
11
+ independent sources it depends on, shared sources correlate correctly:
12
+ `x - x` is exactly `0 ± 0`, not `0 ± √2·σ`.
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ gem install measurand
18
+ ```
19
+
20
+ Or add to your Gemfile:
21
+
22
+ ```ruby
23
+ gem 'measurand'
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ruby
29
+ require 'measurand'
30
+ ```
31
+
32
+ ### Construction
33
+
34
+ ```ruby
35
+ Measurand.new(5.129213, 0.01) # value ± uncertainty
36
+ Measurand.new(5.129213) # exact; uncertainty 0
37
+ Measurand.relative(5.129213, 0.02) # 2% relative — how instruments are spec'd
38
+
39
+ Measurand.cast([5.129, 5.132, 5.128], 0.001) # shared absolute uncertainty
40
+ Measurand.cast_relative([5.129, 5.132], 0.0002) # shared relative uncertainty
41
+
42
+ Measurand.from_samples([5.129, 5.132, 5.128]) # mean ± standard error of the mean
43
+ ```
44
+
45
+ ### Parsing
46
+
47
+ ```ruby
48
+ Measurand.parse('5.129213 ± 0.01')
49
+ Measurand.parse('5.129213 +/- 0.01') # ASCII, since ± is awkward to type
50
+ Measurand.parse('5.129213(10)') # parenthetic — CODATA/NIST notation
51
+ Measurand.parse('5.129213 ± 1.9%') # relative
52
+ Measurand.parse('5.129213') # exact
53
+ ```
54
+
55
+ `parse` raises `ArgumentError` on anything it does not recognise, rather than
56
+ returning `nil`.
57
+
58
+ ### Arithmetic
59
+
60
+ ```ruby
61
+ a + b
62
+ a - b
63
+ a * b
64
+ a / b
65
+ a ** b
66
+ -a
67
+ a.abs
68
+
69
+ Measurand.new(3.2, 0.1) * 2 # => 6.4 ± 0.2 — scalars are exact
70
+ 2 * Measurand.new(3.2, 0.1) # => 6.4 ± 0.2 — either side
71
+ ```
72
+
73
+ Independent uncertainties combine in quadrature. Shared sources are handled by
74
+ the derivative graph, not assumed independent:
75
+
76
+ ```ruby
77
+ x = Measurand.new(5.0, 0.1)
78
+ x - x # => 0.0 ± 0.0
79
+ x * 2 - x # => 5.0 ± 0.1 (exactly x)
80
+ ```
81
+
82
+ This is the difference from the old `plusminus` gem, which added errors
83
+ linearly: `3.2±0.1 + 4.5±0.1` is `7.7 ± 0.1414`, not `7.7 ± 0.2`.
84
+
85
+ ### Comparison
86
+
87
+ ```ruby
88
+ a == b # exact: value AND uncertainty match
89
+ a.eql?(b)
90
+ a <=> b # by value; Comparable (so <, >, sort order by value)
91
+ a.consistent_with?(b) # same measurement within combined uncertainty?
92
+ a.overlaps?(b) # do the ± intervals intersect?
93
+ ```
94
+
95
+ Note that `==` is stricter than `<=>`: two measurands with equal values but
96
+ different uncertainties compare `0` under `<=>` yet are not `==`. The
97
+ scientific question — "are these the same within error?" — is
98
+ `consistent_with?`, which subtracts (so correlation is handled) and asks
99
+ whether zero lies within the combined uncertainty.
100
+
101
+ ### Presentation
102
+
103
+ Digits follow the uncertainty — showing more precision than the measurement
104
+ supports would be a lie. The uncertainty is rounded by the Particle Data Group
105
+ convention, and the value is rounded to match:
106
+
107
+ ```ruby
108
+ m = Measurand.new(5.129213, 0.01)
109
+ m.to_s # => "5.129 ± 0.010"
110
+ m.to_s(ascii: true) # => "5.129 +/- 0.010" — for ASCII-only sinks
111
+ m.to_parenthetic # => "5.129(10)"
112
+ m.inspect # => "Measurand(5.129213, 0.01)" — full precision
113
+ ```
114
+
115
+ `to_s` emits the `±` character by default; pass `ascii: true` when writing to a
116
+ stream that isn't UTF-8. `parse` reads both forms, so either round-trips.
117
+
118
+ ### Transcendentals
119
+
120
+ Kept in a separate module rather than monkeypatching `Math`:
121
+
122
+ ```ruby
123
+ Measurand::Math.sqrt(m)
124
+ Measurand::Math.exp(m)
125
+ Measurand::Math.log(m)
126
+ Measurand::Math.log10(m)
127
+ Measurand::Math.sin(m)
128
+ Measurand::Math.cos(m)
129
+ Measurand::Math.tan(m)
130
+ ```
131
+
132
+ ### Numeric sugar — opt-in
133
+
134
+ Required separately, so nothing is monkeypatched unless you ask:
135
+
136
+ ```ruby
137
+ require 'Measurand/Numeric'
138
+
139
+ 5.129213.pm(0.01) # => Measurand(5.129213, 0.01)
140
+ 5.129213.±(0.01) # alias; not infix — Ruby won't allow it
141
+ [5.129, 5.132].pm(0.001) # Enumerable#pm — array of measurands
142
+ ```
143
+
144
+ ## Design notes
145
+
146
+ - **Forward-mode automatic differentiation.** Each measurand carries a map of
147
+ partial derivatives with respect to the independent sources it depends on.
148
+ The uncertainty is derived from those partials and the sources' own
149
+ uncertainties. Shared sources cancel correctly.
150
+ - **`==` is exact, `consistent_with?` is the science.** Equality compares both
151
+ value and uncertainty; consistency is the interesting question and gets its
152
+ own name.
153
+ - **The value is not coerced to `Float` at construction.** A reading from a
154
+ 6-digit instrument keeps its type; coercion happens at the point of
155
+ arithmetic.
156
+ - **No `Math` monkeypatching.** `Measurand::Math` is a separate module.
157
+
158
+ ## Limitations
159
+
160
+ **Uncertainty is symmetric.** A measurand carries a single standard
161
+ uncertainty, so it is always `value ± σ` — never `value +a −b`. This is the
162
+ first-order (GUM) model, and it matches the reference tools
163
+ ([`uncertainties`](https://pypi.org/project/uncertainties/),
164
+ [`errors`](https://cran.r-project.org/package=errors)), which are symmetric by
165
+ design.
166
+
167
+ Two consequences worth knowing:
168
+
169
+ - **Quoted asymmetric errors are not represented.** Literature values written
170
+ `5.3 +0.4 −0.2` — from non-Gaussian likelihoods, low-count Poisson
171
+ statistics, or parameters near a physical boundary — cannot be stored as
172
+ such.
173
+ - **Nonlinear propagation is linearized, and therefore symmetrized.** Even from
174
+ symmetric inputs, a nonlinear function produces an asymmetric interval that
175
+ first-order propagation flattens back to a single σ. For example,
176
+ `Measurand::Math.exp(Measurand.new(0.0, 1.0))` reports `1.0 ± 1.0`, but the
177
+ true ±1σ interval is `[exp(−1), exp(+1)] = [0.368, 2.718]`, i.e. `+1.72 /
178
+ −0.63`. Near strong nonlinearity or large *relative* uncertainty, read the
179
+ single σ as a linear approximation, not an exact interval.
180
+
181
+ Asymmetric and second-order propagation are deferred: combining asymmetric
182
+ errors correctly is genuinely unsettled (see Barlow, *Asymmetric Errors*), and
183
+ doing it properly means Monte-Carlo sampling or second-order moments rather
184
+ than the quadrature used here.
185
+
186
+ ## Related
187
+
188
+ - Python [`uncertainties`](https://pypi.org/project/uncertainties/) — the
189
+ reference implementation of this approach.
190
+ - R [`errors`](https://cran.r-project.org/package=errors) — GUM-conformant,
191
+ and, correctly, leaves units to a separate package.
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,68 @@
1
+ # Measurand/Format.rb
2
+ # Measurand::Format
3
+
4
+ # Uncertainty-driven display. The value is only shown to the precision the
5
+ # uncertainty supports; showing more digits than the measurement supports
6
+ # would be a lie. Rounding of the uncertainty follows the Particle Data Group
7
+ # convention on its three leading digits:
8
+ # 100..354 -> keep two significant figures
9
+ # 355..949 -> keep one significant figure
10
+ # 950..999 -> round up to 1000, keep two significant figures
11
+ # The value is then rounded to the same decimal place as the uncertainty.
12
+
13
+ class Measurand
14
+ module Format
15
+ module_function
16
+
17
+ # Returns [rounded_uncertainty, place], where place is the power of ten of
18
+ # the least significant shown digit (negative for digits after the point).
19
+ def round_uncertainty(uncertainty)
20
+ exponent = ::Math.log10(uncertainty).floor
21
+ leading = (uncertainty / 10.0 ** (exponent - 2)).round
22
+ if leading < 355
23
+ significant_figures = 2
24
+ elsif leading < 950
25
+ significant_figures = 1
26
+ else
27
+ significant_figures = 2
28
+ exponent += 1
29
+ end
30
+ place = exponent - significant_figures + 1
31
+ [round_to_place(uncertainty, place), place]
32
+ end
33
+
34
+ def round_to_place(number, place)
35
+ factor = 10.0 ** place
36
+ (number / factor).round * factor
37
+ end
38
+
39
+ def decimals(place)
40
+ place < 0 ? -place : 0
41
+ end
42
+ end
43
+
44
+ def to_s(ascii: false)
45
+ return @value.to_s if exact?
46
+ rounded_uncertainty, place = Format.round_uncertainty(uncertainty)
47
+ digits = Format.decimals(place)
48
+ "#{fixed(Format.round_to_place(@value, place), digits)} #{ascii ? '+/-' : '±'} #{fixed(rounded_uncertainty, digits)}"
49
+ end
50
+
51
+ def to_parenthetic
52
+ return @value.to_s if exact?
53
+ rounded_uncertainty, place = Format.round_uncertainty(uncertainty)
54
+ digits = Format.decimals(place)
55
+ parenthetic_digits = (rounded_uncertainty / 10.0 ** place).round
56
+ "#{fixed(Format.round_to_place(@value, place), digits)}(#{parenthetic_digits})"
57
+ end
58
+
59
+ def inspect
60
+ "Measurand(#{@value}, #{uncertainty})"
61
+ end
62
+
63
+ private
64
+
65
+ def fixed(number, digits)
66
+ format("%.#{digits}f", number)
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ # Measurand/Math.rb
2
+ # Measurand::Math
3
+
4
+ # Transcendental functions that propagate uncertainty by the chain rule. Kept
5
+ # in a separate module rather than monkeypatching the standard Math, so that
6
+ # requiring measurand changes nothing you did not ask it to.
7
+ #
8
+ # Each function evaluates f(value) and multiplies the partials by f'(value).
9
+
10
+ class Measurand
11
+ module Math
12
+ module_function
13
+
14
+ def sqrt(measurand)
15
+ measurand = wrap(measurand)
16
+ measurand.apply(::Math.sqrt(measurand.value), 1.0 / (2 * ::Math.sqrt(measurand.value)))
17
+ end
18
+
19
+ def exp(measurand)
20
+ measurand = wrap(measurand)
21
+ measurand.apply(::Math.exp(measurand.value), ::Math.exp(measurand.value))
22
+ end
23
+
24
+ def log(measurand)
25
+ measurand = wrap(measurand)
26
+ measurand.apply(::Math.log(measurand.value), 1.0 / measurand.value)
27
+ end
28
+
29
+ def log10(measurand)
30
+ measurand = wrap(measurand)
31
+ measurand.apply(::Math.log10(measurand.value), 1.0 / (measurand.value * ::Math.log(10)))
32
+ end
33
+
34
+ def sin(measurand)
35
+ measurand = wrap(measurand)
36
+ measurand.apply(::Math.sin(measurand.value), ::Math.cos(measurand.value))
37
+ end
38
+
39
+ def cos(measurand)
40
+ measurand = wrap(measurand)
41
+ measurand.apply(::Math.cos(measurand.value), -::Math.sin(measurand.value))
42
+ end
43
+
44
+ def tan(measurand)
45
+ measurand = wrap(measurand)
46
+ measurand.apply(::Math.tan(measurand.value), 1.0 / (::Math.cos(measurand.value) ** 2))
47
+ end
48
+
49
+ def wrap(measurand)
50
+ measurand.is_a?(Measurand) ? measurand : Measurand.new(measurand)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ # Measurand/Numeric.rb
2
+ # Measurand::Numeric
3
+
4
+ # Opt-in sugar. Required separately so that nobody gets a monkeypatch they did
5
+ # not ask for. pm is primary; ± is an alias, since Ruby's parser won't accept
6
+ # it infix (5.129213 ± 0.01 is a SyntaxError) but 5.129213.±(0.01) parses.
7
+
8
+ require_relative '../measurand'
9
+
10
+ class Measurand
11
+ module Numeric
12
+ def pm(uncertainty)
13
+ Measurand.new(self, uncertainty)
14
+ end
15
+ alias_method :"±", :pm
16
+ end
17
+
18
+ module Enumerable
19
+ def pm(uncertainty)
20
+ map{|value| Measurand.new(value, uncertainty)}
21
+ end
22
+ end
23
+ end
24
+
25
+ Numeric.include(Measurand::Numeric)
26
+ Enumerable.include(Measurand::Enumerable)
@@ -0,0 +1,38 @@
1
+ # Measurand/Parse.rb
2
+ # Measurand.parse
3
+
4
+ # Parses the notations a measurand is written in. Raises on anything it does
5
+ # not recognise, rather than returning nil: the whole job of parse is to fail
6
+ # informatively.
7
+ #
8
+ # "5.129213 ± 0.01" "5.129213 +/- 0.01" absolute
9
+ # "5.129213 ± 1.9%" relative
10
+ # "5.129213(10)" parenthetic (CODATA/NIST)
11
+ # "5.129213" exact
12
+
13
+ class Measurand
14
+ NUMBER = '[-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?'
15
+ PLUS_MINUS = '(?:±|\+/-|\+-)'
16
+
17
+ def self.parse(string)
18
+ string = string.strip
19
+ case string
20
+ when /\A(#{NUMBER})\((\d+)\)\z/
21
+ value, digits = $1, $2
22
+ new(value.to_f, digits.to_i * 10.0 ** -decimal_places(value))
23
+ when /\A(#{NUMBER})\s*#{PLUS_MINUS}\s*(#{NUMBER})%\z/
24
+ relative($1.to_f, $2.to_f / 100)
25
+ when /\A(#{NUMBER})\s*#{PLUS_MINUS}\s*(#{NUMBER})\z/
26
+ new($1.to_f, $2.to_f)
27
+ when /\A#{NUMBER}\z/
28
+ new(string.to_f)
29
+ else
30
+ raise ArgumentError, "can't parse measurand: #{string.inspect}"
31
+ end
32
+ end
33
+
34
+ def self.decimal_places(number)
35
+ point = number.index('.')
36
+ point ? number.length - point - 1 : 0
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # Measurand/Source.rb
2
+ # Measurand::Source
3
+
4
+ # An independent source of uncertainty. Its identity is load-bearing: two
5
+ # Sources are never equal unless they are the same object, so error
6
+ # propagation can tell whether the same measurement reappears in an
7
+ # expression. Deliberately does not define ==, eql? or hash.
8
+
9
+ class Measurand
10
+ class Source
11
+ attr_reader :uncertainty
12
+
13
+ private
14
+
15
+ def initialize(uncertainty)
16
+ @uncertainty = uncertainty.abs
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ class Measurand
2
+ VERSION = '0.0.0'
3
+ end
data/lib/measurand.rb ADDED
@@ -0,0 +1,193 @@
1
+ # measurand.rb
2
+ # Measurand
3
+
4
+ require_relative 'Measurand/Format'
5
+ require_relative 'Measurand/Math'
6
+ require_relative 'Measurand/Parse'
7
+ require_relative 'Measurand/Source'
8
+ require_relative 'Measurand/VERSION'
9
+
10
+ # A measured value with an associated uncertainty. Arithmetic propagates the
11
+ # uncertainty by first-order (GUM) error propagation, using forward-mode
12
+ # automatic differentiation so that shared sources correlate correctly:
13
+ # x - x is exactly 0 ± 0, not 0 ± sqrt(2)*sigma.
14
+ #
15
+ # Internally a Measurand carries a central @value and a map of @partials,
16
+ # {Source => dvalue/dsource}. The uncertainty is derived from the partials
17
+ # and the sources' own uncertainties, not stored.
18
+
19
+ class Measurand
20
+ include Comparable
21
+
22
+ class << self
23
+ # Bypasses the one-source initializer to build a Measurand directly from
24
+ # a value and an already-computed partials map. Used by every operator.
25
+ def derived(value, partials)
26
+ measurand = allocate
27
+ measurand.send(:build, value, partials)
28
+ measurand
29
+ end
30
+
31
+ # A value whose uncertainty is a fraction of the value itself, which is
32
+ # how instruments are usually specified: relative(5.0, 0.02) is 5.0 +/- 2%.
33
+ def relative(value, relative_uncertainty)
34
+ new(value, value.abs * relative_uncertainty)
35
+ end
36
+
37
+ def cast(values, uncertainty)
38
+ values.map{|value| new(value, uncertainty)}
39
+ end
40
+
41
+ def cast_relative(values, relative_uncertainty)
42
+ values.map{|value| relative(value, relative_uncertainty)}
43
+ end
44
+
45
+ # Mean +/- standard error of the mean, from repeated readings.
46
+ def from_samples(values)
47
+ raise ArgumentError, "need at least two samples" if values.size < 2
48
+ floats = values.map(&:to_f)
49
+ mean = floats.sum / floats.size
50
+ variance = floats.map{|value| (value - mean) ** 2}.sum / (floats.size - 1)
51
+ new(mean, ::Math.sqrt(variance / floats.size))
52
+ end
53
+ end
54
+
55
+ def value
56
+ @value
57
+ end
58
+
59
+ def uncertainty
60
+ @uncertainty ||= ::Math.sqrt(@partials.sum{|source, partial| (partial * source.uncertainty) ** 2})
61
+ end
62
+
63
+ def relative_uncertainty
64
+ return 0.0 if uncertainty.zero?
65
+ uncertainty / @value.abs
66
+ end
67
+
68
+ def exact?
69
+ uncertainty.zero?
70
+ end
71
+
72
+ def +(other)
73
+ other = wrap(other)
74
+ self.class.derived(@value + other.value, combine(1.0, other, 1.0))
75
+ end
76
+
77
+ def -(other)
78
+ other = wrap(other)
79
+ self.class.derived(@value - other.value, combine(1.0, other, -1.0))
80
+ end
81
+
82
+ def *(other)
83
+ other = wrap(other)
84
+ self.class.derived(@value * other.value, combine(other.value, other, @value))
85
+ end
86
+
87
+ def /(other)
88
+ other = wrap(other)
89
+ self.class.derived(@value / other.value, combine(1.0 / other.value, other, -@value.to_f / (other.value ** 2)))
90
+ end
91
+
92
+ def **(other)
93
+ other = wrap(other)
94
+ dself = other.value * @value ** (other.value - 1)
95
+ if other.exact?
96
+ self.class.derived(@value ** other.value, scale(dself))
97
+ else
98
+ self.class.derived(@value ** other.value, combine(dself, other, @value ** other.value * ::Math.log(@value)))
99
+ end
100
+ end
101
+
102
+ def -@
103
+ self.class.derived(-@value, scale(-1.0))
104
+ end
105
+
106
+ def abs
107
+ self.class.derived(@value.abs, scale(@value < 0 ? -1.0 : 1.0))
108
+ end
109
+
110
+ # Applies a differentiable unary function: the caller supplies f(value) and
111
+ # f'(value). This is how Measurand::Math builds transcendentals.
112
+ def apply(value, derivative)
113
+ self.class.derived(value, scale(derivative))
114
+ end
115
+
116
+ def ==(other)
117
+ other.is_a?(Measurand) && @value == other.value && uncertainty == other.uncertainty
118
+ end
119
+
120
+ def eql?(other)
121
+ other.instance_of?(Measurand) && @value.eql?(other.value) && uncertainty.eql?(other.uncertainty)
122
+ end
123
+
124
+ def hash
125
+ [@value, uncertainty].hash
126
+ end
127
+
128
+ # By value only, so Comparable's < > sort etc. order by magnitude. Note this
129
+ # is intentionally looser than ==: two measurands with equal values but
130
+ # different uncertainties compare 0 here yet are not ==.
131
+ def <=>(other)
132
+ other = wrap(other) if other.is_a?(Numeric)
133
+ return nil unless other.is_a?(Measurand)
134
+ @value <=> other.value
135
+ end
136
+
137
+ # Are these the same measurement within error? Uses subtraction so shared
138
+ # sources correlate correctly, then asks whether zero lies within sigmas of
139
+ # the difference.
140
+ def consistent_with?(other, sigmas: 1)
141
+ difference = self - wrap(other)
142
+ difference.value.abs <= sigmas * difference.uncertainty
143
+ end
144
+
145
+ def overlaps?(other)
146
+ other = wrap(other)
147
+ low, high = @value - uncertainty, @value + uncertainty
148
+ other_low, other_high = other.value - other.uncertainty, other.value + other.uncertainty
149
+ low <= other_high && other_low <= high
150
+ end
151
+
152
+ # Ruby calls this when a bare number is on the left: 2 * measurand.
153
+ def coerce(other)
154
+ [wrap(other), self]
155
+ end
156
+
157
+ protected
158
+
159
+ def partials
160
+ @partials
161
+ end
162
+
163
+ private
164
+
165
+ def initialize(value, uncertainty = 0)
166
+ @value = value
167
+ @partials = uncertainty.zero? ? {} : {Source.new(uncertainty) => 1.0}
168
+ end
169
+
170
+ def build(value, partials)
171
+ @value = value
172
+ @partials = partials
173
+ end
174
+
175
+ def wrap(other)
176
+ other.is_a?(Measurand) ? other : self.class.new(other)
177
+ end
178
+
179
+ # New partials for a two-operand function f, given df/dself and df/dother.
180
+ # Sources shared between the operands sum here, which is where correlation
181
+ # is actually handled.
182
+ def combine(dself, other, dother)
183
+ result = Hash.new(0.0)
184
+ @partials.each{|source, partial| result[source] += partial * dself}
185
+ other.partials.each{|source, partial| result[source] += partial * dother}
186
+ result
187
+ end
188
+
189
+ # New partials for a unary function, scaling every partial by df/dself.
190
+ def scale(dself)
191
+ @partials.each_with_object({}){|(source, partial), result| result[source] = partial * dself}
192
+ end
193
+ end
data/measurand.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ require_relative './lib/Measurand/VERSION'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'measurand'
5
+ spec.version = Measurand::VERSION
6
+
7
+ spec.summary = "Measured values with uncertainty and GUM-conformant error propagation."
8
+ spec.description = "Measured values with uncertainty, propagated by first-order (GUM) error propagation over an automatic-differentiation graph, so that shared sources correlate correctly. Quadrature done right, not linear addition. Parsing, PDG-rounded presentation, and transcendentals."
9
+
10
+ spec.author = 'thoran'
11
+ spec.email = 'code@thoran.com'
12
+ spec.homepage = 'http://github.com/thoran/measurand'
13
+ spec.license = 'MIT'
14
+
15
+ spec.required_ruby_version = '>= 2.5'
16
+
17
+ spec.files = [
18
+ 'measurand.gemspec',
19
+ 'Gemfile',
20
+ Dir['lib/**/*.rb'],
21
+ 'CHANGELOG.md',
22
+ 'LICENSE',
23
+ 'README.md',
24
+ Dir['test/**/*.rb']
25
+ ].flatten
26
+ spec.require_paths = ['lib']
27
+ end
@@ -0,0 +1,54 @@
1
+ # test/Measurand/Format_test.rb
2
+
3
+ require_relative '../test_helper'
4
+
5
+ describe "Measurand formatting" do
6
+ describe "to_s, PDG-rounded" do
7
+ it "keeps two significant figures when the leading digits are 100..354" do
8
+ _(Measurand.new(5.129213, 0.01).to_s).must_equal "5.129 ± 0.010"
9
+ _(Measurand.new(1.23456, 0.0123).to_s).must_equal "1.235 ± 0.012"
10
+ end
11
+
12
+ it "keeps one significant figure when the leading digits are 355..949" do
13
+ _(Measurand.new(1.23456, 0.0356).to_s).must_equal "1.23 ± 0.04"
14
+ end
15
+
16
+ it "rounds up to two significant figures when the leading digits are 950..999" do
17
+ _(Measurand.new(1.23456, 0.096).to_s).must_equal "1.23 ± 0.10"
18
+ end
19
+
20
+ it "handles uncertainty larger than one" do
21
+ _(Measurand.new(1234.4, 12.3).to_s).must_equal "1234 ± 12"
22
+ end
23
+
24
+ it "shows only the value when exact" do
25
+ _(Measurand.new(5.0).to_s).must_equal "5.0"
26
+ _(Measurand.new(5).to_s).must_equal "5"
27
+ end
28
+
29
+ it "emits ASCII +/- on request, and parse reads it back" do
30
+ m = Measurand.new(5.129213, 0.01)
31
+ _(m.to_s(ascii: true)).must_equal "5.129 +/- 0.010"
32
+ reparsed = Measurand.parse(m.to_s(ascii: true))
33
+ _(reparsed.value).must_be_close_to 5.129
34
+ _(reparsed.uncertainty).must_be_close_to 0.010
35
+ end
36
+ end
37
+
38
+ describe "to_parenthetic" do
39
+ it "expresses the uncertainty in units of the last shown digit" do
40
+ _(Measurand.new(5.129213, 0.01).to_parenthetic).must_equal "5.129(10)"
41
+ _(Measurand.new(1.23456, 0.0356).to_parenthetic).must_equal "1.23(4)"
42
+ end
43
+
44
+ it "shows only the value when exact" do
45
+ _(Measurand.new(5.0).to_parenthetic).must_equal "5.0"
46
+ end
47
+ end
48
+
49
+ describe "inspect" do
50
+ it "shows the full-precision value and uncertainty" do
51
+ _(Measurand.new(5.129213, 0.01).inspect).must_equal "Measurand(5.129213, 0.01)"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ # test/Measurand/Math_test.rb
2
+
3
+ require_relative '../test_helper'
4
+
5
+ describe Measurand::Math do
6
+ it "propagates through sqrt: d/dx sqrt(x) = 1/(2 sqrt(x))" do
7
+ m = Measurand::Math.sqrt(Measurand.new(4.0, 0.1))
8
+ _(m.value).must_equal 2.0
9
+ _(m.uncertainty).must_be_close_to 0.025
10
+ end
11
+
12
+ it "propagates through exp: d/dx exp(x) = exp(x)" do
13
+ m = Measurand::Math.exp(Measurand.new(1.0, 0.1))
14
+ _(m.value).must_be_close_to ::Math::E
15
+ _(m.uncertainty).must_be_close_to ::Math::E * 0.1, 0.0001
16
+ end
17
+
18
+ it "propagates through log: d/dx ln(x) = 1/x" do
19
+ m = Measurand::Math.log(Measurand.new(2.0, 0.1))
20
+ _(m.value).must_be_close_to ::Math.log(2.0)
21
+ _(m.uncertainty).must_be_close_to 0.05
22
+ end
23
+
24
+ it "propagates through sin: d/dx sin(x) = cos(x)" do
25
+ m = Measurand::Math.sin(Measurand.new(0.0, 0.1))
26
+ _(m.value).must_equal 0.0
27
+ _(m.uncertainty).must_be_close_to 0.1
28
+ end
29
+
30
+ it "propagates through cos: d/dx cos(x) = -sin(x)" do
31
+ m = Measurand::Math.cos(Measurand.new(0.0, 0.1))
32
+ _(m.value).must_equal 1.0
33
+ _(m.uncertainty).must_equal 0.0
34
+ end
35
+
36
+ it "accepts a bare number and returns an exact result" do
37
+ m = Measurand::Math.sqrt(4.0)
38
+ _(m.value).must_equal 2.0
39
+ _(m.exact?).must_equal true
40
+ end
41
+ end
@@ -0,0 +1,206 @@
1
+ # test/Measurand/Measurand_test.rb
2
+
3
+ require_relative '../test_helper'
4
+
5
+ describe Measurand do
6
+ describe "construction" do
7
+ it "holds a value and an uncertainty" do
8
+ m = Measurand.new(5.129213, 0.01)
9
+ _(m.value).must_equal 5.129213
10
+ _(m.uncertainty).must_equal 0.01
11
+ end
12
+
13
+ it "is exact with no uncertainty" do
14
+ m = Measurand.new(5.129213)
15
+ _(m.uncertainty).must_equal 0.0
16
+ _(m.exact?).must_equal true
17
+ end
18
+
19
+ it "does not coerce the value to Float" do
20
+ _(Measurand.new(5).value).must_be_kind_of Integer
21
+ _(Measurand.new(Rational(1, 3)).value).must_be_kind_of Rational
22
+ end
23
+
24
+ it "builds from a relative uncertainty" do
25
+ m = Measurand.relative(5.0, 0.02)
26
+ _(m.uncertainty).must_be_close_to 0.1
27
+ end
28
+
29
+ it "casts a collection with a shared uncertainty" do
30
+ ms = Measurand.cast([5.129, 5.132, 5.128], 0.001)
31
+ _(ms.map(&:uncertainty)).must_equal [0.001, 0.001, 0.001]
32
+ end
33
+
34
+ it "casts a collection with a shared relative uncertainty" do
35
+ ms = Measurand.cast_relative([10.0, 20.0], 0.01)
36
+ _(ms.map(&:uncertainty)).must_equal [0.1, 0.2]
37
+ end
38
+ end
39
+
40
+ describe "readers" do
41
+ it "reports relative uncertainty" do
42
+ _(Measurand.new(10.0, 0.5).relative_uncertainty).must_be_close_to 0.05
43
+ end
44
+
45
+ it "reports zero relative uncertainty when exact" do
46
+ _(Measurand.new(10.0).relative_uncertainty).must_equal 0.0
47
+ end
48
+ end
49
+
50
+ describe "from_samples" do
51
+ it "is the mean plus or minus the standard error of the mean" do
52
+ m = Measurand.from_samples([5.0, 5.0, 5.0, 5.0])
53
+ _(m.value).must_equal 5.0
54
+ _(m.uncertainty).must_equal 0.0
55
+ end
56
+
57
+ it "computes the standard error of the mean" do
58
+ # sample sd of [2,4,4,4,5,5,7,9] is 2.138; sem = 2.138/sqrt(8)
59
+ m = Measurand.from_samples([2, 4, 4, 4, 5, 5, 7, 9])
60
+ _(m.value).must_equal 5.0
61
+ _(m.uncertainty).must_be_close_to 0.7559, 0.001
62
+ end
63
+
64
+ it "needs at least two samples" do
65
+ _{Measurand.from_samples([5.0])}.must_raise ArgumentError
66
+ end
67
+ end
68
+
69
+ describe "arithmetic" do
70
+ it "adds in quadrature, not linearly (the plusminus regression)" do
71
+ sum = Measurand.new(3.2, 0.1) + Measurand.new(4.5, 0.1)
72
+ _(sum.value).must_equal 7.7
73
+ _(sum.uncertainty).must_be_close_to 0.14142, 0.00001
74
+ end
75
+
76
+ it "subtracts in quadrature" do
77
+ difference = Measurand.new(4.5, 0.1) - Measurand.new(3.2, 0.1)
78
+ _(difference.value).must_be_close_to 1.3
79
+ _(difference.uncertainty).must_be_close_to 0.14142, 0.00001
80
+ end
81
+
82
+ it "combines relative uncertainties in quadrature when multiplying" do
83
+ product = Measurand.new(10.0, 0.1) * Measurand.new(20.0, 0.4)
84
+ _(product.value).must_equal 200.0
85
+ _(product.relative_uncertainty).must_be_close_to 0.02236, 0.00001
86
+ end
87
+
88
+ it "combines relative uncertainties in quadrature when dividing" do
89
+ quotient = Measurand.new(10.0, 0.1) / Measurand.new(20.0, 0.4)
90
+ _(quotient.value).must_equal 0.5
91
+ _(quotient.relative_uncertainty).must_be_close_to 0.02236, 0.00001
92
+ end
93
+
94
+ it "propagates through a scalar power" do
95
+ squared = Measurand.new(3.0, 0.1) ** 2
96
+ _(squared.value).must_equal 9.0
97
+ _(squared.uncertainty).must_be_close_to 0.6, 0.00001
98
+ end
99
+
100
+ it "propagates through an uncertain power" do
101
+ result = Measurand.new(2.0, 0.1) ** Measurand.new(3.0, 0.1)
102
+ _(result.value).must_be_close_to 8.0
103
+ # df/da = 3*2^2 = 12, df/db = 2^3*ln2 = 5.545
104
+ # unc = sqrt((12*0.1)^2 + (5.545*0.1)^2) = sqrt(1.44 + 0.3075)
105
+ _(result.uncertainty).must_be_close_to 1.3222, 0.001
106
+ end
107
+
108
+ it "negates" do
109
+ m = -Measurand.new(3.0, 0.1)
110
+ _(m.value).must_equal(-3.0)
111
+ _(m.uncertainty).must_equal 0.1
112
+ end
113
+
114
+ it "takes an absolute value, preserving uncertainty" do
115
+ m = Measurand.new(-3.0, 0.1).abs
116
+ _(m.value).must_equal 3.0
117
+ _(m.uncertainty).must_equal 0.1
118
+ end
119
+ end
120
+
121
+ describe "scalars are exact" do
122
+ it "scales value and uncertainty together without adding uncertainty" do
123
+ m = Measurand.new(3.2, 0.1) * 2
124
+ _(m.value).must_equal 6.4
125
+ _(m.uncertainty).must_equal 0.2
126
+ end
127
+
128
+ it "works with the scalar on the left, through coerce" do
129
+ m = 2 * Measurand.new(3.2, 0.1)
130
+ _(m.value).must_equal 6.4
131
+ _(m.uncertainty).must_equal 0.2
132
+ end
133
+
134
+ it "adds an exact scalar without adding uncertainty" do
135
+ m = Measurand.new(3.2, 0.1) + 1
136
+ _(m.value).must_equal 4.2
137
+ _(m.uncertainty).must_equal 0.1
138
+ end
139
+ end
140
+
141
+ describe "correlation" do
142
+ it "cancels a variable against itself: x - x is exactly zero" do
143
+ x = Measurand.new(5.0, 0.1)
144
+ difference = x - x
145
+ _(difference.value).must_equal 0.0
146
+ _(difference.uncertainty).must_equal 0.0
147
+ end
148
+
149
+ it "does not cancel independently constructed measurands of equal value" do
150
+ difference = Measurand.new(5.0, 0.1) - Measurand.new(5.0, 0.1)
151
+ _(difference.uncertainty).must_be_close_to 0.14142, 0.00001
152
+ end
153
+
154
+ it "handles 2x - x as exactly x" do
155
+ x = Measurand.new(5.0, 0.1)
156
+ result = x * 2 - x
157
+ _(result.value).must_equal 5.0
158
+ _(result.uncertainty).must_be_close_to 0.1
159
+ end
160
+ end
161
+
162
+ describe "comparison" do
163
+ it "is equal when value and uncertainty both match" do
164
+ _(Measurand.new(5.0, 0.1)).must_equal Measurand.new(5.0, 0.1)
165
+ end
166
+
167
+ it "is not equal when the uncertainty differs" do
168
+ _(Measurand.new(5.0, 0.1)).wont_equal Measurand.new(5.0, 0.2)
169
+ end
170
+
171
+ it "orders by value alone, looser than equality" do
172
+ a = Measurand.new(5.0, 0.1)
173
+ b = Measurand.new(5.0, 0.2)
174
+ _(a <=> b).must_equal 0
175
+ _(a == b).must_equal false
176
+ end
177
+
178
+ it "is Comparable by value" do
179
+ _(Measurand.new(3.0, 0.5) < Measurand.new(4.0, 0.1)).must_equal true
180
+ end
181
+
182
+ it "eql? and hash agree" do
183
+ a = Measurand.new(5.0, 0.1)
184
+ b = Measurand.new(5.0, 0.1)
185
+ _(a.eql?(b)).must_equal true
186
+ _(a.hash).must_equal b.hash
187
+ end
188
+
189
+ it "asks whether two measurements are consistent within error" do
190
+ a = Measurand.new(5.0, 0.1)
191
+ _(a.consistent_with?(Measurand.new(5.05, 0.1))).must_equal true
192
+ _(a.consistent_with?(Measurand.new(5.5, 0.1))).must_equal false
193
+ end
194
+
195
+ it "is consistent with itself" do
196
+ x = Measurand.new(5.0, 0.1)
197
+ _(x.consistent_with?(x)).must_equal true
198
+ end
199
+
200
+ it "reports overlapping intervals" do
201
+ a = Measurand.new(5.0, 0.1)
202
+ _(a.overlaps?(Measurand.new(5.15, 0.1))).must_equal true
203
+ _(a.overlaps?(Measurand.new(5.3, 0.1))).must_equal false
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,32 @@
1
+ # test/Measurand/Numeric_test.rb
2
+
3
+ require_relative '../test_helper'
4
+ require_relative '../../lib/Measurand/Numeric'
5
+
6
+ describe Measurand::Numeric do
7
+ it "builds a measurand from a number with pm" do
8
+ m = 5.129213.pm(0.01)
9
+ _(m).must_be_instance_of Measurand
10
+ _(m.value).must_equal 5.129213
11
+ _(m.uncertainty).must_equal 0.01
12
+ end
13
+
14
+ it "aliases pm as ±" do
15
+ m = 5.129213.±(0.01)
16
+ _(m).must_be_instance_of Measurand
17
+ _(m.uncertainty).must_equal 0.01
18
+ end
19
+
20
+ it "reproduces the plusminus regression correctly" do
21
+ sum = 3.2.pm(0.1) + 4.5.pm(0.1)
22
+ _(sum.value).must_equal 7.7
23
+ _(sum.uncertainty).must_be_close_to 0.14142, 0.00001
24
+ end
25
+
26
+ it "builds an array of measurands from an Enumerable with pm" do
27
+ ms = [5.129, 5.132, 5.128].pm(0.001)
28
+ _(ms).must_be_kind_of Array
29
+ _(ms.map(&:value)).must_equal [5.129, 5.132, 5.128]
30
+ _(ms.map(&:uncertainty)).must_equal [0.001, 0.001, 0.001]
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ # test/Measurand/Parse_test.rb
2
+
3
+ require_relative '../test_helper'
4
+
5
+ describe "Measurand.parse" do
6
+ it "parses the unicode plus-minus notation" do
7
+ m = Measurand.parse('5.129213 ± 0.01')
8
+ _(m.value).must_equal 5.129213
9
+ _(m.uncertainty).must_equal 0.01
10
+ end
11
+
12
+ it "parses the ASCII plus-minus notation" do
13
+ m = Measurand.parse('5.129213 +/- 0.01')
14
+ _(m.value).must_equal 5.129213
15
+ _(m.uncertainty).must_equal 0.01
16
+ end
17
+
18
+ it "parses parenthetic CODATA/NIST notation" do
19
+ m = Measurand.parse('5.129213(10)')
20
+ _(m.value).must_equal 5.129213
21
+ _(m.uncertainty).must_be_close_to 0.00001
22
+ end
23
+
24
+ it "parses relative notation" do
25
+ m = Measurand.parse('5.0 ± 2%')
26
+ _(m.value).must_equal 5.0
27
+ _(m.uncertainty).must_be_close_to 0.1
28
+ end
29
+
30
+ it "parses an exact value" do
31
+ m = Measurand.parse('5.129213')
32
+ _(m.value).must_equal 5.129213
33
+ _(m.exact?).must_equal true
34
+ end
35
+
36
+ it "raises on unparseable input, rather than returning nil" do
37
+ _{Measurand.parse('not a measurand')}.must_raise ArgumentError
38
+ end
39
+
40
+ it "round-trips through to_parenthetic at display precision" do
41
+ m = Measurand.new(5.129213, 0.01)
42
+ reparsed = Measurand.parse(m.to_parenthetic)
43
+ _(reparsed.value).must_be_close_to 5.129
44
+ _(reparsed.uncertainty).must_be_close_to 0.010
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # test/Measurand/Source_test.rb
2
+
3
+ require_relative '../test_helper'
4
+
5
+ describe Measurand::Source do
6
+ it "is equal only to itself, never to another source of the same size" do
7
+ a = Measurand::Source.new(0.1)
8
+ b = Measurand::Source.new(0.1)
9
+ _(a).must_equal a
10
+ _(a).wont_equal b
11
+ end
12
+
13
+ it "keeps distinct sources distinct as hash keys" do
14
+ a = Measurand::Source.new(0.1)
15
+ b = Measurand::Source.new(0.1)
16
+ hash = {a => 1.0, b => 1.0}
17
+ _(hash.size).must_equal 2
18
+ end
19
+
20
+ it "stores uncertainty as a magnitude" do
21
+ _(Measurand::Source.new(-0.1).uncertainty).must_equal 0.1
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ # test/test_helper.rb
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/spec'
5
+
6
+ require_relative '../lib/measurand'
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: measurand
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - thoran
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Measured values with uncertainty, propagated by first-order (GUM) error
13
+ propagation over an automatic-differentiation graph, so that shared sources correlate
14
+ correctly. Quadrature done right, not linear addition. Parsing, PDG-rounded presentation,
15
+ and transcendentals.
16
+ email: code@thoran.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - LICENSE
24
+ - README.md
25
+ - lib/Measurand/Format.rb
26
+ - lib/Measurand/Math.rb
27
+ - lib/Measurand/Numeric.rb
28
+ - lib/Measurand/Parse.rb
29
+ - lib/Measurand/Source.rb
30
+ - lib/Measurand/VERSION.rb
31
+ - lib/measurand.rb
32
+ - measurand.gemspec
33
+ - test/Measurand/Format_test.rb
34
+ - test/Measurand/Math_test.rb
35
+ - test/Measurand/Measurand_test.rb
36
+ - test/Measurand/Numeric_test.rb
37
+ - test/Measurand/Parse_test.rb
38
+ - test/Measurand/Source_test.rb
39
+ - test/test_helper.rb
40
+ homepage: http://github.com/thoran/measurand
41
+ licenses:
42
+ - MIT
43
+ metadata: {}
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '2.5'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 4.0.16
59
+ specification_version: 4
60
+ summary: Measured values with uncertainty and GUM-conformant error propagation.
61
+ test_files: []