measurand
Measured values with uncertainty, and GUM-conformant error propagation.
A Measurand is a value with an associated uncertainty. Arithmetic on
measurands propagates the uncertainty correctly, so a calculation chain can
tell the truth about how well its result is known.
Uncertainty is propagated by first-order (GUM) error propagation over an
automatic-differentiation graph. Because each measurand remembers which
independent sources it depends on, shared sources correlate correctly:
x - x is exactly 0 ± 0, not 0 ± √2·σ.
Installation
gem install measurand
Or add to your Gemfile:
gem 'measurand'
Usage
require 'measurand'
Construction
Measurand.new(5.129213, 0.01) # value ± uncertainty
Measurand.new(5.129213) # exact; uncertainty 0
Measurand.relative(5.129213, 0.02) # 2% relative — how instruments are spec'd
Measurand.cast([5.129, 5.132, 5.128], 0.001) # shared absolute uncertainty
Measurand.cast_relative([5.129, 5.132], 0.0002) # shared relative uncertainty
Measurand.from_samples([5.129, 5.132, 5.128]) # mean ± standard error of the mean
Parsing
Measurand.parse('5.129213 ± 0.01')
Measurand.parse('5.129213 +/- 0.01') # ASCII, since ± is awkward to type
Measurand.parse('5.129213(10)') # parenthetic — CODATA/NIST notation
Measurand.parse('5.129213 ± 1.9%') # relative
Measurand.parse('5.129213') # exact
parse raises ArgumentError on anything it does not recognise, rather than
returning nil.
Arithmetic
a + b
a - b
a * b
a / b
a ** b
-a
a.abs
Measurand.new(3.2, 0.1) * 2 # => 6.4 ± 0.2 — scalars are exact
2 * Measurand.new(3.2, 0.1) # => 6.4 ± 0.2 — either side
Independent uncertainties combine in quadrature. Shared sources are handled by the derivative graph, not assumed independent:
x = Measurand.new(5.0, 0.1)
x - x # => 0.0 ± 0.0
x * 2 - x # => 5.0 ± 0.1 (exactly x)
This is the difference from the old plusminus gem, which added errors
linearly: 3.2±0.1 + 4.5±0.1 is 7.7 ± 0.1414, not 7.7 ± 0.2.
Comparison
a == b # exact: value AND uncertainty match
a.eql?(b)
a <=> b # by value; Comparable (so <, >, sort order by value)
a.consistent_with?(b) # same measurement within combined uncertainty?
a.overlaps?(b) # do the ± intervals intersect?
Note that == is stricter than <=>: two measurands with equal values but
different uncertainties compare 0 under <=> yet are not ==. The
scientific question — "are these the same within error?" — is
consistent_with?, which subtracts (so correlation is handled) and asks
whether zero lies within the combined uncertainty.
Presentation
Digits follow the uncertainty — showing more precision than the measurement supports would be a lie. The uncertainty is rounded by the Particle Data Group convention, and the value is rounded to match:
m = Measurand.new(5.129213, 0.01)
m.to_s # => "5.129 ± 0.010"
m.to_s(ascii: true) # => "5.129 +/- 0.010" — for ASCII-only sinks
m.to_parenthetic # => "5.129(10)"
m.inspect # => "Measurand(5.129213, 0.01)" — full precision
to_s emits the ± character by default; pass ascii: true when writing to a
stream that isn't UTF-8. parse reads both forms, so either round-trips.
Transcendentals
Kept in a separate module rather than monkeypatching Math:
Measurand::Math.sqrt(m)
Measurand::Math.exp(m)
Measurand::Math.log(m)
Measurand::Math.log10(m)
Measurand::Math.sin(m)
Measurand::Math.cos(m)
Measurand::Math.tan(m)
Numeric sugar — opt-in
Required separately, so nothing is monkeypatched unless you ask:
require 'Measurand/Numeric'
5.129213.pm(0.01) # => Measurand(5.129213, 0.01)
5.129213.±(0.01) # alias; not infix — Ruby won't allow it
[5.129, 5.132].pm(0.001) # Enumerable#pm — array of measurands
Design notes
- Forward-mode automatic differentiation. Each measurand carries a map of partial derivatives with respect to the independent sources it depends on. The uncertainty is derived from those partials and the sources' own uncertainties. Shared sources cancel correctly.
==is exact,consistent_with?is the science. Equality compares both value and uncertainty; consistency is the interesting question and gets its own name.- The value is not coerced to
Floatat construction. A reading from a 6-digit instrument keeps its type; coercion happens at the point of arithmetic. - No
Mathmonkeypatching.Measurand::Mathis a separate module.
Limitations
Uncertainty is symmetric. A measurand carries a single standard
uncertainty, so it is always value ± σ — never value +a −b. This is the
first-order (GUM) model, and it matches the reference tools
(uncertainties,
errors), which are symmetric by
design.
Two consequences worth knowing:
- Quoted asymmetric errors are not represented. Literature values written
5.3 +0.4 −0.2— from non-Gaussian likelihoods, low-count Poisson statistics, or parameters near a physical boundary — cannot be stored as such. - Nonlinear propagation is linearized, and therefore symmetrized. Even from
symmetric inputs, a nonlinear function produces an asymmetric interval that
first-order propagation flattens back to a single σ. For example,
Measurand::Math.exp(Measurand.new(0.0, 1.0))reports1.0 ± 1.0, but the true ±1σ interval is[exp(−1), exp(+1)] = [0.368, 2.718], i.e.+1.72 / −0.63. Near strong nonlinearity or large relative uncertainty, read the single σ as a linear approximation, not an exact interval.
Asymmetric and second-order propagation are deferred: combining asymmetric errors correctly is genuinely unsettled (see Barlow, Asymmetric Errors), and doing it properly means Monte-Carlo sampling or second-order moments rather than the quadrature used here.
Related
- Python
uncertainties— the reference implementation of this approach. - R
errors— GUM-conformant, and, correctly, leaves units to a separate package.
License
MIT