measurand.rb 0.1.4
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 +7 -0
- data/CHANGELOG +73 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +206 -0
- data/lib/Measurand/Format.rb +43 -0
- data/lib/Measurand/Math.rb +53 -0
- data/lib/Measurand/Numeric.rb +26 -0
- data/lib/Measurand/Parse.rb +38 -0
- data/lib/Measurand/Source.rb +19 -0
- data/lib/Measurand/VERSION.rb +3 -0
- data/lib/measurand.rb +238 -0
- data/measurand.rb.gemspec +41 -0
- data/test/Measurand/Format_test.rb +81 -0
- data/test/Measurand/Math_test.rb +41 -0
- data/test/Measurand/Measurand_test.rb +234 -0
- data/test/Measurand/Numeric_test.rb +32 -0
- data/test/Measurand/Parse_test.rb +46 -0
- data/test/Measurand/Source_test.rb +23 -0
- data/test/test_helper.rb +6 -0
- metadata +89 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d342e21e71e18b32c4e5cb2ded4762cfdf527342fb3fd057112392cd49cc9b23
|
|
4
|
+
data.tar.gz: 7e3c0829b85f6172d5deb62473ab0979747d5643441ad15cf62c82ebcd0297c5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 73075c9f00f95b600e973aea6a6f41d82956d4870ea3ab3297c475cbed012a54f06515a7a8d12297628ae7262cbd498a810e96591dcf3cbef45c667156df59e9
|
|
7
|
+
data.tar.gz: b33a77c18c347f24bc801b561c74c33b948649bcac8de01c0d3c3d087fe4c2c8edd0bf0c7f832ae41a33fe70cb5582d9107d1e9cb20296832bf9662fd23e78b1
|
data/CHANGELOG
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 20260826
|
|
4
|
+
|
|
5
|
+
0.1.4: + measurand.rb.gemspec, so that the gem resolves under both names.
|
|
6
|
+
|
|
7
|
+
1. + measurand.rb.gemspec
|
|
8
|
+
2. ~ Gemfile: /gemspec/gemspec name: 'measurand'/, bundler refusing to parse a Gemfile whose bare gemspec directive finds more than one gemspec to choose from.
|
|
9
|
+
3. ~ Measurand::VERSION: /0.1.3/0.1.4/
|
|
10
|
+
|
|
11
|
+
0.1.3: + minitest and rake as development dependencies, the Gemfile having resolved nothing.
|
|
12
|
+
|
|
13
|
+
1. ~ measurand.gemspec: + spec.development_dependencies, + Gem::Specification#development_dependencies= helper.
|
|
14
|
+
2. ~ Measurand::VERSION: /0.1.2/0.1.3/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## 20260817
|
|
18
|
+
|
|
19
|
+
0.1.2: Measurand#/ divided two integer values as integers.
|
|
20
|
+
|
|
21
|
+
1. ~ Measurand#/: through Numeric#quo rather than #/, so that two Integer values divide exactly. A ratio of two integer counts came back as nothing at all — 9579583 over 10900458 gave 0 rather than 0.8788 — which is what a speedup or a rate is made of, and integer counts are what a clock reads in. A Float value stays a Float, and the partials were already in Float and are unaffected. duration.rb met the same defect at its 0.5.0 and took the same fix.
|
|
22
|
+
2. + test/Measurand/Measurand_test.rb: that two integer values divide exactly, and that two exact ones keep a Rational.
|
|
23
|
+
3. + test/Measurand/Measurand_test.rb: that Measurand#** carries no such defect, an integer raised to a negative exponent giving a Rational rather than truncating, in the value and in the derivative alike. Integer#** promotes where Integer#/ truncates, which is Ruby's inconsistency and not this gem's, and nothing here would have said so.
|
|
24
|
+
4. ~ Measurand::VERSION: /0.1.1/0.1.2/
|
|
25
|
+
|
|
26
|
+
0.1.1: Populate the .gitignore, which had been committed empty.
|
|
27
|
+
|
|
28
|
+
1. ~ .gitignore: 47 lines, covering gem builds, bundler, RDoc and YARD output, .DS_Store, editor files, the Ruby version managers, test artefacts and .claude. The file was created empty by the root commit and nothing had since filled it in.
|
|
29
|
+
2. ~ Measurand::VERSION: /0.1.0/0.1.1/
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## 20260815
|
|
33
|
+
|
|
34
|
+
0.1.0: + Measurand#place, Measurand#rounded_uncertainty.
|
|
35
|
+
|
|
36
|
+
1. + Measurand#place: the power of ten of the least significant digit worth showing, negative for digits after the point, and nil where the measurand is exact — every digit being real and there being no last one. It is what a caller wants in order to render a quantity in a unit of its own choosing rather than take #to_s, and monotonic.rb was reaching into Measurand::Format to get at it.
|
|
37
|
+
2. + Measurand#rounded_uncertainty: the uncertainty as #to_s shows it, rounded by the Particle Data Group convention, and zero where the measurand is exact.
|
|
38
|
+
3. ~ Measurand#to_s and #to_parenthetic: built upon the two above rather than calling Measurand::Format themselves, so that what is published and what is rendered cannot drift apart.
|
|
39
|
+
4. + test/Measurand/Format_test.rb: #place and #rounded_uncertainty, including the exact case and that #place agrees with the digits #to_s renders.
|
|
40
|
+
5. ~ lib/Measurand/Format.rb --> lib/measurand.rb: #place, #rounded_uncertainty, #to_s, #to_parenthetic, #inspect and the private #fixed, which are Measurand's and not Format's. The file is named for the module and now holds only it, as Monotonic/Time.rb holds Monotonic::Time. Naming them by the file they sat in was already a mistake waiting to be made.
|
|
41
|
+
6. + CHANGELOG, and - CHANGELOG.md. Both were present at 0.0.0 and only the .md shipped; the gem is what is immutable, so 0.0.0 has been rewritten to carry the .md alone and the change of file belongs here. CHANGELOG is what every other gem here is spelt.
|
|
42
|
+
7. ~ measurand.gemspec: /CHANGELOG.md/CHANGELOG/, following the above.
|
|
43
|
+
8. ~ README.md: + #place and #rounded_uncertainty to the Presentation section.
|
|
44
|
+
9. ~ Measurand::VERSION: /0.0.0/0.1.0/
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## 20260715
|
|
48
|
+
|
|
49
|
+
0.0.0: + Measurand: measured values with uncertainty and GUM-conformant error propagation.
|
|
50
|
+
|
|
51
|
+
1. + Measurand: Measured values with uncertainty and GUM-conformant error propagation, 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).
|
|
52
|
+
2. + Measurand construction methods: .new, .relative, .cast, .cast\_relative, .from\_samples (mean ± standard error of the mean), .derived.
|
|
53
|
+
3. + Measurand arithmetic methods: +, -, \*, /, \*\*, -@, abs, apply — chain-rule propagation; independent uncertainties combine in quadrature; scalars are exact.
|
|
54
|
+
4. + Measurand comparison methods: ==/eql?/hash (exact: value and uncertainty), <=> (by value, Comparable), consistent\_with?, overlaps?, coerce (scalar-on-the-left arithmetic).
|
|
55
|
+
5. + Measurand reader methods: value, uncertainty, relative\_uncertainty, exact?
|
|
56
|
+
6. + Measurand::Format: Uncertainty-driven, PDG-rounded to\_s (ascii: keyword emits +/- instead of ±), to\_parenthetic, inspect (full precision).
|
|
57
|
+
7. + Measurand::Math: Measurand::Math.sqrt, exp, log, log10, sin, cos, tan — chain-rule propagation with no Math monkey-patching.
|
|
58
|
+
8. + Measurand::Numeric: opt-in Numeric#pm / #± and Enumerable#pm (separate require, so nothing is monkey-patched unless asked).
|
|
59
|
+
9. + Measurand::Parse: Measurand.parse for ±, +/-, parenthetic (10), relative %, and exact forms; raises ArgumentError rather than returning nil.
|
|
60
|
+
10. + Measurand::Source: Opaque per-source identity carrying its own uncertainty; deliberately no ==/eql?/hash, so shared sources correlate and independently-constructed ones never collapse.
|
|
61
|
+
11. + Measurand::VERSION: 0.0.0
|
|
62
|
+
12. + measurand.gemspec
|
|
63
|
+
13. + Gemfile
|
|
64
|
+
14. + LICENSE
|
|
65
|
+
15. + Rakefile
|
|
66
|
+
16. + README.md
|
|
67
|
+
17. + test/test_helper.rb
|
|
68
|
+
18. + test/Measurand/Format_test.rb
|
|
69
|
+
19. + test/Measurand/Measurand_test.rb
|
|
70
|
+
20. + test/Measurand/Math_test.rb
|
|
71
|
+
21. + test/Measurand/Numeric_test.rb
|
|
72
|
+
22. + test/Measurand/Parse_test.rb
|
|
73
|
+
23. + test/Measurand/Source_test.rb
|
data/Gemfile
ADDED
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,206 @@
|
|
|
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
|
+
|
|
117
|
+
Where a caller wants to render the quantity in some unit of its own rather than
|
|
118
|
+
take the string here, the two figures `to_s` rests upon are readable:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
m.place # => -3 the power of ten of the last real digit
|
|
122
|
+
m.rounded_uncertainty # => 0.01 the uncertainty as `to_s` shows it
|
|
123
|
+
|
|
124
|
+
Measurand.new(5.129213).place # => nil exact: every digit is real
|
|
125
|
+
Measurand.new(5.129213).rounded_uncertainty # => 0
|
|
126
|
+
```
|
|
127
|
+
stream that isn't UTF-8. `parse` reads both forms, so either round-trips.
|
|
128
|
+
|
|
129
|
+
### Transcendentals
|
|
130
|
+
|
|
131
|
+
Kept in a separate module rather than monkeypatching `Math`:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
Measurand::Math.sqrt(m)
|
|
135
|
+
Measurand::Math.exp(m)
|
|
136
|
+
Measurand::Math.log(m)
|
|
137
|
+
Measurand::Math.log10(m)
|
|
138
|
+
Measurand::Math.sin(m)
|
|
139
|
+
Measurand::Math.cos(m)
|
|
140
|
+
Measurand::Math.tan(m)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Numeric sugar — opt-in
|
|
144
|
+
|
|
145
|
+
Required separately, so nothing is monkeypatched unless you ask:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
require 'Measurand/Numeric'
|
|
149
|
+
|
|
150
|
+
5.129213.pm(0.01) # => Measurand(5.129213, 0.01)
|
|
151
|
+
5.129213.±(0.01) # alias; not infix — Ruby won't allow it
|
|
152
|
+
[5.129, 5.132].pm(0.001) # Enumerable#pm — array of measurands
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Design notes
|
|
156
|
+
|
|
157
|
+
- **Forward-mode automatic differentiation.** Each measurand carries a map of
|
|
158
|
+
partial derivatives with respect to the independent sources it depends on.
|
|
159
|
+
The uncertainty is derived from those partials and the sources' own
|
|
160
|
+
uncertainties. Shared sources cancel correctly.
|
|
161
|
+
- **`==` is exact, `consistent_with?` is the science.** Equality compares both
|
|
162
|
+
value and uncertainty; consistency is the interesting question and gets its
|
|
163
|
+
own name.
|
|
164
|
+
- **The value is not coerced to `Float` at construction.** A reading from a
|
|
165
|
+
6-digit instrument keeps its type; coercion happens at the point of
|
|
166
|
+
arithmetic.
|
|
167
|
+
- **No `Math` monkeypatching.** `Measurand::Math` is a separate module.
|
|
168
|
+
|
|
169
|
+
## Limitations
|
|
170
|
+
|
|
171
|
+
**Uncertainty is symmetric.** A measurand carries a single standard
|
|
172
|
+
uncertainty, so it is always `value ± σ` — never `value +a −b`. This is the
|
|
173
|
+
first-order (GUM) model, and it matches the reference tools
|
|
174
|
+
([`uncertainties`](https://pypi.org/project/uncertainties/),
|
|
175
|
+
[`errors`](https://cran.r-project.org/package=errors)), which are symmetric by
|
|
176
|
+
design.
|
|
177
|
+
|
|
178
|
+
Two consequences worth knowing:
|
|
179
|
+
|
|
180
|
+
- **Quoted asymmetric errors are not represented.** Literature values written
|
|
181
|
+
`5.3 +0.4 −0.2` — from non-Gaussian likelihoods, low-count Poisson
|
|
182
|
+
statistics, or parameters near a physical boundary — cannot be stored as
|
|
183
|
+
such.
|
|
184
|
+
- **Nonlinear propagation is linearized, and therefore symmetrized.** Even from
|
|
185
|
+
symmetric inputs, a nonlinear function produces an asymmetric interval that
|
|
186
|
+
first-order propagation flattens back to a single σ. For example,
|
|
187
|
+
`Measurand::Math.exp(Measurand.new(0.0, 1.0))` reports `1.0 ± 1.0`, but the
|
|
188
|
+
true ±1σ interval is `[exp(−1), exp(+1)] = [0.368, 2.718]`, i.e. `+1.72 /
|
|
189
|
+
−0.63`. Near strong nonlinearity or large *relative* uncertainty, read the
|
|
190
|
+
single σ as a linear approximation, not an exact interval.
|
|
191
|
+
|
|
192
|
+
Asymmetric and second-order propagation are deferred: combining asymmetric
|
|
193
|
+
errors correctly is genuinely unsettled (see Barlow, *Asymmetric Errors*), and
|
|
194
|
+
doing it properly means Monte-Carlo sampling or second-order moments rather
|
|
195
|
+
than the quadrature used here.
|
|
196
|
+
|
|
197
|
+
## Related
|
|
198
|
+
|
|
199
|
+
- Python [`uncertainties`](https://pypi.org/project/uncertainties/) — the
|
|
200
|
+
reference implementation of this approach.
|
|
201
|
+
- R [`errors`](https://cran.r-project.org/package=errors) — GUM-conformant,
|
|
202
|
+
and, correctly, leaves units to a separate package.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
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
|
data/lib/measurand.rb
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
# Through #quo rather than #/, so that two Integer values divide exactly:
|
|
88
|
+
# 1.quo(3) is (1/3) where 1 / 3 is 0, and a ratio of two integer counts would
|
|
89
|
+
# otherwise come back as nothing at all. A Float value stays a Float. The
|
|
90
|
+
# partials were already in Float and are unaffected.
|
|
91
|
+
def /(other)
|
|
92
|
+
other = wrap(other)
|
|
93
|
+
self.class.derived(@value.quo(other.value), combine(1.0 / other.value, other, -@value.to_f / (other.value ** 2)))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def **(other)
|
|
97
|
+
other = wrap(other)
|
|
98
|
+
dself = other.value * @value ** (other.value - 1)
|
|
99
|
+
if other.exact?
|
|
100
|
+
self.class.derived(@value ** other.value, scale(dself))
|
|
101
|
+
else
|
|
102
|
+
self.class.derived(@value ** other.value, combine(dself, other, @value ** other.value * ::Math.log(@value)))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def -@
|
|
107
|
+
self.class.derived(-@value, scale(-1.0))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def abs
|
|
111
|
+
self.class.derived(@value.abs, scale(@value < 0 ? -1.0 : 1.0))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Applies a differentiable unary function: the caller supplies f(value) and
|
|
115
|
+
# f'(value). This is how Measurand::Math builds transcendentals.
|
|
116
|
+
def apply(value, derivative)
|
|
117
|
+
self.class.derived(value, scale(derivative))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ==(other)
|
|
121
|
+
other.is_a?(Measurand) && @value == other.value && uncertainty == other.uncertainty
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def eql?(other)
|
|
125
|
+
other.instance_of?(Measurand) && @value.eql?(other.value) && uncertainty.eql?(other.uncertainty)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def hash
|
|
129
|
+
[@value, uncertainty].hash
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# By value only, so Comparable's < > sort etc. order by magnitude. Note this
|
|
133
|
+
# is intentionally looser than ==: two measurands with equal values but
|
|
134
|
+
# different uncertainties compare 0 here yet are not ==.
|
|
135
|
+
def <=>(other)
|
|
136
|
+
other = wrap(other) if other.is_a?(Numeric)
|
|
137
|
+
return nil unless other.is_a?(Measurand)
|
|
138
|
+
@value <=> other.value
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Are these the same measurement within error? Uses subtraction so shared
|
|
142
|
+
# sources correlate correctly, then asks whether zero lies within sigmas of
|
|
143
|
+
# the difference.
|
|
144
|
+
def consistent_with?(other, sigmas: 1)
|
|
145
|
+
difference = self - wrap(other)
|
|
146
|
+
difference.value.abs <= sigmas * difference.uncertainty
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def overlaps?(other)
|
|
150
|
+
other = wrap(other)
|
|
151
|
+
low, high = @value - uncertainty, @value + uncertainty
|
|
152
|
+
other_low, other_high = other.value - other.uncertainty, other.value + other.uncertainty
|
|
153
|
+
low <= other_high && other_low <= high
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Ruby calls this when a bare number is on the left: 2 * measurand.
|
|
157
|
+
def coerce(other)
|
|
158
|
+
[wrap(other), self]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The power of ten of the least significant digit worth showing, by the
|
|
162
|
+
# convention above: negative for digits after the point, so a place of -3
|
|
163
|
+
# means thousandths are the last real thing. nil where the measurand is exact,
|
|
164
|
+
# every digit being real and there being no last one.
|
|
165
|
+
#
|
|
166
|
+
# This is what a caller wants in order to render a quantity in some unit of
|
|
167
|
+
# its own choosing, rather than taking the string here.
|
|
168
|
+
def place
|
|
169
|
+
return nil if exact?
|
|
170
|
+
@place ||= Format.round_uncertainty(uncertainty).last
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The uncertainty as #to_s shows it, rounded to one or two significant figures
|
|
174
|
+
# by the same convention. Zero where the measurand is exact.
|
|
175
|
+
def rounded_uncertainty
|
|
176
|
+
return 0 if exact?
|
|
177
|
+
@rounded_uncertainty ||= Format.round_uncertainty(uncertainty).first
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_s(ascii: false)
|
|
181
|
+
return @value.to_s if exact?
|
|
182
|
+
digits = Format.decimals(place)
|
|
183
|
+
"#{fixed(Format.round_to_place(@value, place), digits)} #{ascii ? '+/-' : '±'} #{fixed(rounded_uncertainty, digits)}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def to_parenthetic
|
|
187
|
+
return @value.to_s if exact?
|
|
188
|
+
digits = Format.decimals(place)
|
|
189
|
+
parenthetic_digits = (rounded_uncertainty / 10.0 ** place).round
|
|
190
|
+
"#{fixed(Format.round_to_place(@value, place), digits)}(#{parenthetic_digits})"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def inspect
|
|
194
|
+
"Measurand(#{@value}, #{uncertainty})"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
protected
|
|
199
|
+
|
|
200
|
+
def partials
|
|
201
|
+
@partials
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def fixed(number, digits)
|
|
207
|
+
format("%.#{digits}f", number)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def initialize(value, uncertainty = 0)
|
|
211
|
+
@value = value
|
|
212
|
+
@partials = uncertainty.zero? ? {} : {Source.new(uncertainty) => 1.0}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def build(value, partials)
|
|
216
|
+
@value = value
|
|
217
|
+
@partials = partials
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def wrap(other)
|
|
221
|
+
other.is_a?(Measurand) ? other : self.class.new(other)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# New partials for a two-operand function f, given df/dself and df/dother.
|
|
225
|
+
# Sources shared between the operands sum here, which is where correlation
|
|
226
|
+
# is actually handled.
|
|
227
|
+
def combine(dself, other, dother)
|
|
228
|
+
result = Hash.new(0.0)
|
|
229
|
+
@partials.each{|source, partial| result[source] += partial * dself}
|
|
230
|
+
other.partials.each{|source, partial| result[source] += partial * dother}
|
|
231
|
+
result
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# New partials for a unary function, scaling every partial by df/dself.
|
|
235
|
+
def scale(dself)
|
|
236
|
+
@partials.each_with_object({}){|(source, partial), result| result[source] = partial * dself}
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# measurand.rb.gemspec
|
|
2
|
+
|
|
3
|
+
require_relative './lib/Measurand/VERSION'
|
|
4
|
+
|
|
5
|
+
class Gem::Specification
|
|
6
|
+
def development_dependencies=(gems)
|
|
7
|
+
gems.each{|gem| add_development_dependency(*gem)}
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Gem::Specification.new do |spec|
|
|
12
|
+
spec.name = 'measurand.rb'
|
|
13
|
+
spec.version = Measurand::VERSION
|
|
14
|
+
|
|
15
|
+
spec.summary = "Measured values with uncertainty and GUM-conformant error propagation."
|
|
16
|
+
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."
|
|
17
|
+
|
|
18
|
+
spec.author = 'thoran'
|
|
19
|
+
spec.email = 'code@thoran.com'
|
|
20
|
+
spec.homepage = 'http://github.com/thoran/measurand'
|
|
21
|
+
spec.license = 'MIT'
|
|
22
|
+
|
|
23
|
+
spec.required_ruby_version = '>= 2.5'
|
|
24
|
+
|
|
25
|
+
spec.files = [
|
|
26
|
+
Dir['lib/**/*.rb'],
|
|
27
|
+
Dir['test/**/*.rb'],
|
|
28
|
+
'CHANGELOG',
|
|
29
|
+
'Gemfile',
|
|
30
|
+
'LICENSE',
|
|
31
|
+
'measurand.rb.gemspec',
|
|
32
|
+
'README.md',
|
|
33
|
+
].flatten
|
|
34
|
+
|
|
35
|
+
spec.require_paths = ['lib']
|
|
36
|
+
|
|
37
|
+
spec.development_dependencies = %w{
|
|
38
|
+
minitest
|
|
39
|
+
rake
|
|
40
|
+
}
|
|
41
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
describe "#place" do
|
|
55
|
+
it "is the power of ten of the least significant digit worth showing" do
|
|
56
|
+
_(Measurand.new(1_274_625, 84).place).must_equal(1)
|
|
57
|
+
_(Measurand.new(5.129213, 0.01).place).must_equal(-3)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "is nil where the measurand is exact, every digit being real" do
|
|
61
|
+
_(Measurand.new(5.129213).place).must_be_nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "agrees with what #to_s renders" do
|
|
65
|
+
subject = Measurand.new(5.129213, 0.01)
|
|
66
|
+
_(subject.to_s.split(" ").first.split(".").last.length).must_equal(-subject.place)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "#rounded_uncertainty" do
|
|
71
|
+
it "is the uncertainty as #to_s shows it" do
|
|
72
|
+
_(Measurand.new(1_274_625, 84).rounded_uncertainty).must_equal(80)
|
|
73
|
+
_(Measurand.new(5.129213, 0.01).rounded_uncertainty).must_equal(0.01)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "is zero where the measurand is exact" do
|
|
77
|
+
_(Measurand.new(5.129213).rounded_uncertainty).must_equal(0)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
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,234 @@
|
|
|
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
|
+
|
|
207
|
+
describe "division of integer values" do
|
|
208
|
+
it "divides exactly rather than as integers" do
|
|
209
|
+
_((Measurand.new(9_579_583, 84) / Measurand.new(10_900_458, 84)).value.to_f) \
|
|
210
|
+
.must_be_close_to(0.878824, 0.000001)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "keeps a Rational where both are exact" do
|
|
214
|
+
_((Measurand.new(1) / Measurand.new(3)).value).must_equal(Rational(1, 3))
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Integer#** promotes to Rational where Integer#/ truncates, so the defect
|
|
219
|
+
# fixed above has no counterpart here. Held under test all the same, since
|
|
220
|
+
# nothing in Measurand says so and the two operators read alike.
|
|
221
|
+
describe "exponentiation of integer values" do
|
|
222
|
+
it "stays exact under a negative exponent" do
|
|
223
|
+
_((Measurand.new(2, 0.1) ** -1).value).must_equal(Rational(1, 2))
|
|
224
|
+
_((Measurand.new(8, 0.1) ** -1).value).must_equal(Rational(1, 8))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "propagates through a derivative which is exact for the same reason" do
|
|
228
|
+
_((Measurand.new(2, 0.1) ** -1).uncertainty).must_be_close_to(0.025, 1e-9)
|
|
229
|
+
_((Measurand.new(8, 0.1) ** -1).uncertainty).must_be_close_to(0.0015625, 1e-9)
|
|
230
|
+
_((Measurand.new(2, 0.1) ** 3).uncertainty).must_be_close_to(1.2, 1e-9)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
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
|
data/test/test_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: measurand.rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
description: Measured values with uncertainty, propagated by first-order (GUM) error
|
|
41
|
+
propagation over an automatic-differentiation graph, so that shared sources correlate
|
|
42
|
+
correctly. Quadrature done right, not linear addition. Parsing, PDG-rounded presentation,
|
|
43
|
+
and transcendentals.
|
|
44
|
+
email: code@thoran.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- CHANGELOG
|
|
50
|
+
- Gemfile
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/Measurand/Format.rb
|
|
54
|
+
- lib/Measurand/Math.rb
|
|
55
|
+
- lib/Measurand/Numeric.rb
|
|
56
|
+
- lib/Measurand/Parse.rb
|
|
57
|
+
- lib/Measurand/Source.rb
|
|
58
|
+
- lib/Measurand/VERSION.rb
|
|
59
|
+
- lib/measurand.rb
|
|
60
|
+
- measurand.rb.gemspec
|
|
61
|
+
- test/Measurand/Format_test.rb
|
|
62
|
+
- test/Measurand/Math_test.rb
|
|
63
|
+
- test/Measurand/Measurand_test.rb
|
|
64
|
+
- test/Measurand/Numeric_test.rb
|
|
65
|
+
- test/Measurand/Parse_test.rb
|
|
66
|
+
- test/Measurand/Source_test.rb
|
|
67
|
+
- test/test_helper.rb
|
|
68
|
+
homepage: http://github.com/thoran/measurand
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata: {}
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '2.5'
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 4.0.19
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Measured values with uncertainty and GUM-conformant error propagation.
|
|
89
|
+
test_files: []
|