dcc 0.2.1 → 0.2.2
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 +4 -4
- data/dcc.gemspec +2 -1
- data/lib/dcc/cli/cli.rb +122 -14
- data/lib/dcc/extract/formula/ast.rb +137 -0
- data/lib/dcc/extract/formula/evaluator.rb +357 -0
- data/lib/dcc/extract/formula/parser.rb +305 -0
- data/lib/dcc/extract/formula/quantity.rb +89 -0
- data/lib/dcc/extract/formula.rb +110 -0
- data/lib/dcc/quantity_math/complex.rb +252 -0
- data/lib/dcc/quantity_math/matrix.rb +217 -0
- data/lib/dcc/quantity_math.rb +30 -2
- data/lib/dcc/streaming/reader.rb +246 -0
- data/lib/dcc/streaming.rb +79 -0
- data/lib/dcc/version.rb +1 -1
- data/lib/dcc.rb +13 -3
- metadata +27 -4
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Dcc
|
|
6
|
+
module QuantityMath
|
|
7
|
+
# A complex quantity in Cartesian form, backed by a two-element `Matrix`:
|
|
8
|
+
# element 0 is the real part, element 1 the imaginary part, and both
|
|
9
|
+
# share one unit.
|
|
10
|
+
#
|
|
11
|
+
# The full 2x2 covariance is propagated through every operation, so
|
|
12
|
+
# chaining stays correct. A product's real and imaginary parts are
|
|
13
|
+
# correlated, and dropping that off-diagonal term understates the next
|
|
14
|
+
# result — for `(z1 * z2) * z3` by a third in the worked case.
|
|
15
|
+
#
|
|
16
|
+
# `+` and `-` assume the two operands are statistically INDEPENDENT, so
|
|
17
|
+
# `z - z` returns twice the covariance rather than zero.
|
|
18
|
+
class Complex
|
|
19
|
+
ONE = BigDecimal(1)
|
|
20
|
+
private_constant :ONE
|
|
21
|
+
|
|
22
|
+
# Significant digits kept for the division derivatives. They are the
|
|
23
|
+
# only non-terminating arithmetic in the propagation path: Ruby's
|
|
24
|
+
# `Complex#/` runs Smith's algorithm, whose internal `bim/bre` does not
|
|
25
|
+
# terminate, so an unbounded derivative carries ~130 digits of which
|
|
26
|
+
# only ~30 are meaningful. Those digits then multiply forward into
|
|
27
|
+
# every downstream covariance.
|
|
28
|
+
#
|
|
29
|
+
# Bounding them here rather than rounding the covariance is deliberate.
|
|
30
|
+
# Inside `QuantityMath.exact_covariance`, which sets
|
|
31
|
+
# `BigDecimal.limit(0)`, multiplication and addition are exact, so
|
|
32
|
+
# `J C J^T` over bounded derivatives stays exact — and therefore
|
|
33
|
+
# exactly symmetric and exactly positive semi-definite. That exactness
|
|
34
|
+
# comes from the zero limit, not from BigDecimal itself: under a
|
|
35
|
+
# non-zero `BigDecimal.limit` both operations round. Moving any of this
|
|
36
|
+
# arithmetic outside that block reintroduces the asymmetry the
|
|
37
|
+
# covariance validation rejects. Rounding the covariance instead would
|
|
38
|
+
# risk failing PSD validation on the exactly-singular matrices that a
|
|
39
|
+
# component with no uncertainty produces.
|
|
40
|
+
#
|
|
41
|
+
# It happens to equal `SQRT_PRECISION`, but the two are independent:
|
|
42
|
+
# that one sizes a square root of a variance, this one a derivative.
|
|
43
|
+
# Changing either should not drag the other along.
|
|
44
|
+
DERIVATIVE_PRECISION = 30
|
|
45
|
+
|
|
46
|
+
attr_reader :matrix
|
|
47
|
+
|
|
48
|
+
# @param matrix [Matrix] two elements sharing one unit
|
|
49
|
+
def initialize(matrix:)
|
|
50
|
+
ensure_complex_shape!(matrix)
|
|
51
|
+
@matrix = matrix
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param real [Real] real component
|
|
55
|
+
# @param imag [Real] imaginary component, same unit as `real`
|
|
56
|
+
# @param covariance [Array<Array>, nil] 2x2. Defaults to the diagonal
|
|
57
|
+
# built from each component's uncertainty; a component with no
|
|
58
|
+
# uncertainty contributes zero variance, i.e. is treated as exact.
|
|
59
|
+
# @return [Complex]
|
|
60
|
+
def self.cartesian(real:, imag:, covariance: nil)
|
|
61
|
+
new(matrix: Matrix.new(values: [real.value, imag.value],
|
|
62
|
+
units: [real.unit, imag.unit],
|
|
63
|
+
covariance: covariance || diagonal(real, imag)))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.diagonal(real, imag)
|
|
67
|
+
QuantityMath.exact_covariance do
|
|
68
|
+
zero = BigDecimal(0)
|
|
69
|
+
[[variance(real), zero], [zero, variance(imag)]]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.variance(component)
|
|
74
|
+
component.uncertain? ? component.uncertainty**2 : BigDecimal(0)
|
|
75
|
+
end
|
|
76
|
+
private_class_method :diagonal, :variance
|
|
77
|
+
|
|
78
|
+
# @return [String, nil] siunitx expression shared by both components.
|
|
79
|
+
def unit
|
|
80
|
+
matrix.units.first
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Real]
|
|
84
|
+
def real
|
|
85
|
+
matrix[0]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Real]
|
|
89
|
+
def imag
|
|
90
|
+
matrix[1]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Array<Array<BigDecimal>>] the 2x2 covariance.
|
|
94
|
+
def covariance
|
|
95
|
+
matrix.covariance
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reads the variances directly rather than going through
|
|
99
|
+
# `Matrix#uncertainties`: for the non-negative diagonal that validation
|
|
100
|
+
# guarantees, `x.zero?` and `sqrt(x).zero?` agree, and this avoids two
|
|
101
|
+
# 30-digit square roots to answer a zero test.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] whether either component carries uncertainty.
|
|
104
|
+
def uncertain?
|
|
105
|
+
covariance.each_with_index.any? { |row, i| !row[i].zero? }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [::Complex] the value as a Ruby Complex of BigDecimals.
|
|
109
|
+
def to_c
|
|
110
|
+
::Complex.rectangular(*matrix.values)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Assumes independence from `other`.
|
|
114
|
+
# @return [Complex]
|
|
115
|
+
def +(other)
|
|
116
|
+
Complex.new(matrix: matrix + other.matrix)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Assumes independence from `other`.
|
|
120
|
+
# @return [Complex]
|
|
121
|
+
def -(other)
|
|
122
|
+
Complex.new(matrix: matrix - other.matrix)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Complex]
|
|
126
|
+
def *(other)
|
|
127
|
+
combine(other,
|
|
128
|
+
value: to_c * other.to_c,
|
|
129
|
+
unit: compose_unit(unit, other.unit),
|
|
130
|
+
d_self: other.to_c,
|
|
131
|
+
d_other: to_c)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The quotient VALUE follows the caller's ambient BigDecimal context,
|
|
135
|
+
# like every other value here. Its DERIVATIVES feed the covariance, so
|
|
136
|
+
# `1/z2` and `-z1/z2^2` are computed at full precision instead.
|
|
137
|
+
#
|
|
138
|
+
# @raise [ZeroDivisionError] when `other` is exactly 0 + 0i.
|
|
139
|
+
# @return [Complex]
|
|
140
|
+
def /(other)
|
|
141
|
+
ensure_nonzero!(other)
|
|
142
|
+
divisor = other.to_c
|
|
143
|
+
d_self, d_other = division_derivatives(divisor)
|
|
144
|
+
combine(other,
|
|
145
|
+
value: to_c / divisor,
|
|
146
|
+
unit: compose_unit(unit, inverse_unit(other.unit)),
|
|
147
|
+
d_self: d_self, d_other: d_other)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def to_s
|
|
151
|
+
re, im = matrix.values
|
|
152
|
+
sign = im.negative? ? "-" : "+"
|
|
153
|
+
suffix = unit ? " #{unit}" : ""
|
|
154
|
+
"(#{measured(re, 0)}) #{sign} (#{measured(im.abs, 1)})i#{suffix}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Renders one component, dropping the "± u" when its variance is zero.
|
|
160
|
+
# `uncertain?` already treats an all-zero covariance as exact, so
|
|
161
|
+
# printing "± 0.0" would have this class contradicting itself.
|
|
162
|
+
def measured(value, index)
|
|
163
|
+
variance = covariance[index][index]
|
|
164
|
+
return value.to_s if variance.zero?
|
|
165
|
+
|
|
166
|
+
"#{value} ± #{variance.sqrt(SQRT_PRECISION)}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# dw/dz1 = 1/z2 and dw/dz2 = -z1/z2^2, both at full precision because
|
|
170
|
+
# they feed the covariance.
|
|
171
|
+
def division_derivatives(divisor)
|
|
172
|
+
QuantityMath.exact_covariance do
|
|
173
|
+
[bounded(ONE / divisor), bounded(-(to_c / divisor) / divisor)]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Trims a derivative to DERIVATIVE_PRECISION significant digits.
|
|
178
|
+
def bounded(derivative)
|
|
179
|
+
::Complex.rectangular(
|
|
180
|
+
derivative.real.div(ONE, DERIVATIVE_PRECISION),
|
|
181
|
+
derivative.imaginary.div(ONE, DERIVATIVE_PRECISION),
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Stacks self and `other` into one four-element quantity whose
|
|
186
|
+
# covariance is block-diagonal (the operands are assumed independent of
|
|
187
|
+
# each other, though each keeps its own internal correlation), then
|
|
188
|
+
# propagates the 2x4 Jacobian.
|
|
189
|
+
def combine(other, value:, unit:, d_self:, d_other:)
|
|
190
|
+
Complex.new(matrix: stack(other).propagate(
|
|
191
|
+
values: [value.real, value.imaginary],
|
|
192
|
+
units: [unit, unit],
|
|
193
|
+
jacobian: jacobian_for(d_self, d_other),
|
|
194
|
+
))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# `d_self` and `d_other` are the complex derivatives dw/dz1 and dw/dz2.
|
|
198
|
+
# Multiplying by a complex number p has real Jacobian
|
|
199
|
+
# [[Re p, -Im p], [Im p, Re p]]. The signs WITHIN each 2x2 rotation
|
|
200
|
+
# block are load-bearing — unlike a diagonal model, they do not cancel
|
|
201
|
+
# under J C J^T. Negating a whole block is invisible, because `stack`
|
|
202
|
+
# always builds a block-diagonal C and each block contributes
|
|
203
|
+
# `A C A^T`, which is invariant under `A -> -A`. That would stop
|
|
204
|
+
# holding if correlated operands were ever supported.
|
|
205
|
+
def jacobian_for(d_self, d_other)
|
|
206
|
+
[[d_self.real, -d_self.imaginary, d_other.real, -d_other.imaginary],
|
|
207
|
+
[d_self.imaginary, d_self.real, d_other.imaginary, d_other.real]]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def stack(other)
|
|
211
|
+
Matrix.new(values: matrix.values + other.matrix.values,
|
|
212
|
+
units: matrix.units + other.matrix.units,
|
|
213
|
+
covariance: block_diagonal(covariance, other.covariance))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def block_diagonal(first, second)
|
|
217
|
+
zeros = [BigDecimal(0), BigDecimal(0)]
|
|
218
|
+
first.map { |row| row + zeros } + second.map { |row| zeros + row }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def ensure_complex_shape!(matrix)
|
|
222
|
+
unless matrix.values.size == 2
|
|
223
|
+
raise ::ArgumentError,
|
|
224
|
+
"complex quantity needs 2 elements, got #{matrix.values.size}"
|
|
225
|
+
end
|
|
226
|
+
return if matrix.units.uniq.size == 1
|
|
227
|
+
|
|
228
|
+
raise ::ArgumentError, "unit mismatch: #{matrix.units.inspect}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def ensure_nonzero!(other)
|
|
232
|
+
return unless other.to_c.zero?
|
|
233
|
+
|
|
234
|
+
raise ::ZeroDivisionError, "divided by a zero complex quantity"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def compose_unit(left, right)
|
|
238
|
+
return nil if left.nil? && right.nil?
|
|
239
|
+
return left if right.nil?
|
|
240
|
+
return right if left.nil?
|
|
241
|
+
|
|
242
|
+
"#{left}\\cdot#{right}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def inverse_unit(unit)
|
|
246
|
+
return nil if unit.nil?
|
|
247
|
+
|
|
248
|
+
"#{unit}^-1"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Dcc
|
|
6
|
+
module QuantityMath
|
|
7
|
+
# A multivariate quantity: a vector of values with per-element units and
|
|
8
|
+
# a full covariance matrix. Propagation follows the GUM Supplement 2 law
|
|
9
|
+
# `C_y = J C J^T`, so correlation between elements survives.
|
|
10
|
+
#
|
|
11
|
+
# `+` and `-` assume the two operands are statistically INDEPENDENT: the
|
|
12
|
+
# covariance matrices are summed with no cross term, so `x - x` returns
|
|
13
|
+
# `2C` rather than zero. Model correlated quantities as elements of one
|
|
14
|
+
# `Matrix` and combine them with `propagate`.
|
|
15
|
+
class Matrix
|
|
16
|
+
attr_reader :values, :units, :covariance
|
|
17
|
+
|
|
18
|
+
# @param values [Array<BigDecimal, Numeric, String>]
|
|
19
|
+
# @param units [Array<String, nil>] one siunitx expression per element
|
|
20
|
+
# @param covariance [Array<Array<BigDecimal, Numeric, String>>] n by n,
|
|
21
|
+
# symmetric and positive semi-definite
|
|
22
|
+
def initialize(values:, units:, covariance:)
|
|
23
|
+
@values = values.map { |v| decimal(v) }.freeze
|
|
24
|
+
@units = units.map { |u| u&.dup&.freeze }.freeze
|
|
25
|
+
@covariance = decimal_rows(covariance).map(&:freeze).freeze
|
|
26
|
+
validate!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Array<BigDecimal>] standard uncertainty of each element.
|
|
30
|
+
def uncertainties
|
|
31
|
+
covariance.each_with_index.map { |row, i| row[i].sqrt(SQRT_PRECISION) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @raise [IndexError] when `index` is outside the vector.
|
|
35
|
+
# @return [Real] element `index` as a univariate quantity.
|
|
36
|
+
def [](index)
|
|
37
|
+
ensure_index!(index)
|
|
38
|
+
Real.new(value: values[index], unit: units[index],
|
|
39
|
+
uncertainty: covariance[index][index].sqrt(SQRT_PRECISION))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Assumes independence from `other`.
|
|
43
|
+
# @return [Matrix]
|
|
44
|
+
def +(other)
|
|
45
|
+
ensure_same_units!(other)
|
|
46
|
+
combine(other) { |a, b| a + b }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Assumes independence from `other`.
|
|
50
|
+
# @return [Matrix]
|
|
51
|
+
def -(other)
|
|
52
|
+
ensure_same_units!(other)
|
|
53
|
+
combine(other) { |a, b| a - b }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Law of propagation of uncertainty: `C_y = J C J^T`.
|
|
57
|
+
#
|
|
58
|
+
# Only the Jacobian's shape is validated. The output values, the
|
|
59
|
+
# Jacobian entries and the output units are the caller's
|
|
60
|
+
# responsibility: this does not derive units from the Jacobian and does
|
|
61
|
+
# not check that `values` is what `jacobian` actually computes.
|
|
62
|
+
#
|
|
63
|
+
# @param values [Array<BigDecimal, Numeric>] output values, length m
|
|
64
|
+
# @param units [Array<String, nil>] output units, length m
|
|
65
|
+
# @param jacobian [Array<Array<BigDecimal, Numeric>>] m by n
|
|
66
|
+
# @return [Matrix]
|
|
67
|
+
def propagate(values:, units:, jacobian:)
|
|
68
|
+
j = decimal_rows(jacobian)
|
|
69
|
+
ensure_jacobian_shape!(j, values.size)
|
|
70
|
+
propagated = QuantityMath.exact_covariance do
|
|
71
|
+
multiply(multiply(j, covariance), j.transpose)
|
|
72
|
+
end
|
|
73
|
+
Matrix.new(values: values, units: units, covariance: propagated)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_s
|
|
77
|
+
values.each_index.map { |i| self[i].to_s }.join(", ")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Values combine under the caller's operator. The covariance matrices
|
|
83
|
+
# always add, off-diagonals included, for both `+` and `-`.
|
|
84
|
+
def combine(other, &)
|
|
85
|
+
Matrix.new(
|
|
86
|
+
values: values.zip(other.values).map(&),
|
|
87
|
+
units: units,
|
|
88
|
+
covariance: QuantityMath.exact_covariance do
|
|
89
|
+
add(covariance, other.covariance)
|
|
90
|
+
end,
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A matching unit vector implies a matching length, so this also guards
|
|
95
|
+
# the element-wise zips.
|
|
96
|
+
def ensure_same_units!(other)
|
|
97
|
+
return if units == other.units
|
|
98
|
+
|
|
99
|
+
raise ::ArgumentError,
|
|
100
|
+
"unit mismatch: #{units.inspect} vs #{other.units.inspect}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Negative indices would otherwise wrap silently, and an over-large one
|
|
104
|
+
# would reach `nil[index]` instead of saying what went wrong.
|
|
105
|
+
def ensure_index!(index)
|
|
106
|
+
return if index.between?(0, values.size - 1)
|
|
107
|
+
|
|
108
|
+
raise ::IndexError,
|
|
109
|
+
"index #{index} out of range (0...#{values.size})"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def ensure_jacobian_shape!(jacobian, rows)
|
|
113
|
+
n = values.size
|
|
114
|
+
return if jacobian.size == rows && jacobian.all? { |row| row.size == n }
|
|
115
|
+
|
|
116
|
+
raise ::ArgumentError,
|
|
117
|
+
"jacobian shape mismatch: expected #{rows}x#{n}, got " \
|
|
118
|
+
"#{jacobian.size}x#{jacobian.map(&:size).uniq.join(',')}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def validate!
|
|
122
|
+
validate_dimensions!
|
|
123
|
+
covariance.each_with_index { |row, i| validate_covariance_row!(row, i) }
|
|
124
|
+
return if semi_definite?
|
|
125
|
+
|
|
126
|
+
raise ::ArgumentError, "covariance matrix is not positive semi-definite"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_dimensions!
|
|
130
|
+
n = values.size
|
|
131
|
+
return if units.size == n && covariance.size == n &&
|
|
132
|
+
covariance.all? { |row| row.size == n }
|
|
133
|
+
|
|
134
|
+
raise ::ArgumentError,
|
|
135
|
+
"dimension mismatch: expected #{n} units and a #{n}x#{n} " \
|
|
136
|
+
"covariance matrix"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_covariance_row!(row, row_index)
|
|
140
|
+
row.each_with_index do |entry, column|
|
|
141
|
+
validate_covariance_entry!(entry, row_index, column)
|
|
142
|
+
end
|
|
143
|
+
return unless row[row_index].negative?
|
|
144
|
+
|
|
145
|
+
raise ::ArgumentError,
|
|
146
|
+
"covariance[#{row_index}][#{row_index}] is a negative variance"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_covariance_entry!(entry, row_index, column)
|
|
150
|
+
unless entry.finite?
|
|
151
|
+
raise ::ArgumentError,
|
|
152
|
+
"covariance[#{row_index}][#{column}] is not finite"
|
|
153
|
+
end
|
|
154
|
+
return if entry == covariance[column][row_index]
|
|
155
|
+
|
|
156
|
+
raise ::ArgumentError,
|
|
157
|
+
"covariance is not symmetric at [#{row_index}][#{column}]"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# LDL^T elimination in exact Rational arithmetic, so there is no
|
|
161
|
+
# tolerance to tune and a legitimately singular matrix is not
|
|
162
|
+
# rejected. Nothing computed here escapes the predicate.
|
|
163
|
+
def semi_definite?
|
|
164
|
+
work = covariance.map { |row| row.map(&:to_r) }
|
|
165
|
+
work.each_index.all? { |index| eliminate_pivot!(work, index) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Eliminates below the pivot at `index`, returning false when that
|
|
169
|
+
# pivot proves the matrix is not positive semi-definite. A zero pivot
|
|
170
|
+
# is only admissible when the rest of its column is zero too.
|
|
171
|
+
def eliminate_pivot!(work, index)
|
|
172
|
+
pivot = work[index][index]
|
|
173
|
+
below = work[(index + 1)..]
|
|
174
|
+
return false if pivot.negative?
|
|
175
|
+
return below.all? { |row| row[index].zero? } if pivot.zero?
|
|
176
|
+
|
|
177
|
+
below.each { |row| reduce_row!(row, work[index], index, pivot) }
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def reduce_row!(row, pivot_row, index, pivot)
|
|
182
|
+
factor = row[index] / pivot
|
|
183
|
+
(index...row.size).each do |column|
|
|
184
|
+
row[column] -= factor * pivot_row[column]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def multiply(left, right)
|
|
189
|
+
columns = right.transpose
|
|
190
|
+
left.map do |row|
|
|
191
|
+
columns.map { |column| row.zip(column).sum { |x, y| x * y } }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def add(left, right)
|
|
196
|
+
left.zip(right).map { |lr, rr| lr.zip(rr).map { |x, y| x + y } }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def decimal_rows(rows)
|
|
200
|
+
rows.map { |row| row.map { |x| decimal(x) } }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Normalises -0 to 0: it survives the `negative?` variance check but
|
|
204
|
+
# keeps its sign through `sqrt`, which would print an uncertainty of
|
|
205
|
+
# `-0.0`. A non-terminating Rational is rounded to SQRT_PRECISION
|
|
206
|
+
# significant digits.
|
|
207
|
+
def decimal(value)
|
|
208
|
+
decimal = case value
|
|
209
|
+
when ::BigDecimal then value
|
|
210
|
+
when ::Rational then BigDecimal(value, SQRT_PRECISION)
|
|
211
|
+
else BigDecimal(value.to_s)
|
|
212
|
+
end
|
|
213
|
+
decimal.zero? ? BigDecimal(0) : decimal
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/dcc/quantity_math.rb
CHANGED
|
@@ -5,10 +5,38 @@ require "bigdecimal/math"
|
|
|
5
5
|
|
|
6
6
|
module Dcc
|
|
7
7
|
# `Dcc::QuantityMath` provides BigDecimal-based arithmetic on D-SI
|
|
8
|
-
# quantities with proper uncertainty propagation.
|
|
9
|
-
#
|
|
8
|
+
# quantities with proper uncertainty propagation.
|
|
9
|
+
#
|
|
10
|
+
# `Real` is scalar: RSS (root-sum-square) for addition and subtraction,
|
|
11
|
+
# fractional for multiplication and division. `Matrix` is multivariate and
|
|
12
|
+
# carries a full covariance matrix, propagating by `C_y = J C J^T`.
|
|
13
|
+
# `Complex` is a Cartesian complex quantity built on a two-element
|
|
14
|
+
# `Matrix`, so the correlation between its real and imaginary parts
|
|
15
|
+
# survives every operation.
|
|
10
16
|
module QuantityMath
|
|
17
|
+
# Significant digits requested when taking the square root of a
|
|
18
|
+
# BigDecimal variance. Well past double precision; metrology
|
|
19
|
+
# uncertainties never carry this many digits. Ordinary arithmetic
|
|
20
|
+
# follows the caller's ambient BigDecimal context.
|
|
21
|
+
SQRT_PRECISION = 30
|
|
22
|
+
|
|
11
23
|
autoload :Quantity, "dcc/quantity_math/quantity"
|
|
12
24
|
autoload :Real, "dcc/quantity_math/real"
|
|
25
|
+
autoload :Matrix, "dcc/quantity_math/matrix"
|
|
26
|
+
autoload :Complex, "dcc/quantity_math/complex"
|
|
27
|
+
|
|
28
|
+
# Runs `block` with an unlimited BigDecimal precision, restoring the
|
|
29
|
+
# caller's `BigDecimal.limit` on the way out (including when the block
|
|
30
|
+
# raises). Everything feeding a covariance matrix goes through here: a
|
|
31
|
+
# rounded result is no longer exactly symmetric, and `Matrix`'s
|
|
32
|
+
# validation would reject our own output.
|
|
33
|
+
#
|
|
34
|
+
# @return [Object] whatever `block` returns
|
|
35
|
+
def self.exact_covariance
|
|
36
|
+
BigDecimal.save_limit do
|
|
37
|
+
BigDecimal.limit(0)
|
|
38
|
+
yield
|
|
39
|
+
end
|
|
40
|
+
end
|
|
13
41
|
end
|
|
14
42
|
end
|