dcc 0.2.0 → 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/.github/workflows/ci.yml +19 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +560 -10
- data/CHANGELOG.adoc +34 -1
- data/CONTRIBUTING.adoc +8 -1
- data/Gemfile +5 -7
- data/README.adoc +162 -7
- data/dcc.gemspec +9 -2
- data/docs/schema-versions.adoc +20 -7
- data/examples/build_minimal_dcc.rb +0 -0
- data/examples/convert_all_formats.rb +2 -1
- data/examples/parse_and_inspect.rb +0 -0
- data/examples/quantity_math_demo.rb +7 -7
- data/examples/validate_full.rb +1 -1
- data/lib/dcc/base/calibration_laboratory.rb +2 -1
- data/lib/dcc/base/condition.rb +5 -2
- data/lib/dcc/base/core_data.rb +8 -3
- data/lib/dcc/base/formula.rb +3 -3
- data/lib/dcc/base/identification.rb +6 -3
- data/lib/dcc/base/mathml.rb +33 -0
- data/lib/dcc/base/quantity.rb +2 -1
- data/lib/dcc/base/resp_person.rb +4 -2
- data/lib/dcc/base/text.rb +1 -1
- data/lib/dcc/base.rb +2 -1
- data/lib/dcc/builder/dsl.rb +1 -1
- data/lib/dcc/builder/session.rb +16 -14
- data/lib/dcc/builder.rb +3 -3
- data/lib/dcc/cli/cli.rb +152 -26
- data/lib/dcc/cli/formatters.rb +35 -7
- data/lib/dcc/cli.rb +1 -1
- data/lib/dcc/context_configuration.rb +2 -1
- data/lib/dcc/context_options.rb +4 -2
- data/lib/dcc/convert/csv.rb +27 -9
- data/lib/dcc/convert/html.rb +26 -110
- data/lib/dcc/convert/json.rb +10 -7
- data/lib/dcc/convert/liquid_filters.rb +58 -0
- data/lib/dcc/convert/result.rb +1 -1
- data/lib/dcc/convert/templates/report.liquid +250 -0
- data/lib/dcc/convert/yaml.rb +1 -1
- data/lib/dcc/convert.rb +1 -0
- data/lib/dcc/diff/change.rb +1 -1
- data/lib/dcc/diff/result.rb +8 -4
- data/lib/dcc/diff.rb +43 -11
- data/lib/dcc/error.rb +7 -0
- data/lib/dcc/extract/file.rb +3 -4
- 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/extract/ring.rb +1 -1
- data/lib/dcc/extract.rb +1 -1
- data/lib/dcc/i18n/text_lookup.rb +8 -4
- data/lib/dcc/i18n.rb +1 -1
- data/lib/dcc/inspect/report.rb +51 -14
- data/lib/dcc/inspect.rb +1 -1
- data/lib/dcc/migrate/route.rb +44 -0
- data/lib/dcc/migrate/v2_to_v3.rb +445 -0
- data/lib/dcc/migrate.rb +130 -26
- data/lib/dcc/plugin/base.rb +64 -0
- data/lib/dcc/plugin/registry.rb +7 -2
- data/lib/dcc/plugin.rb +6 -2
- data/lib/dcc/quantity_format/formatter.rb +16 -10
- data/lib/dcc/quantity_format.rb +1 -1
- data/lib/dcc/quantity_math/complex.rb +252 -0
- data/lib/dcc/quantity_math/matrix.rb +217 -0
- data/lib/dcc/quantity_math/quantity.rb +1 -1
- data/lib/dcc/quantity_math/real.rb +6 -6
- data/lib/dcc/quantity_math.rb +31 -3
- data/lib/dcc/schema/version.rb +2 -2
- data/lib/dcc/server/app.html +282 -0
- data/lib/dcc/server/app.rb +171 -87
- data/lib/dcc/server/demo_cert.xml +244 -0
- data/lib/dcc/server/storage/entry.rb +1 -1
- data/lib/dcc/server/storage/memory.rb +3 -2
- data/lib/dcc/server/storage.rb +1 -1
- data/lib/dcc/server.rb +1 -1
- data/lib/dcc/si/base/complex.rb +1 -1
- data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
- data/lib/dcc/si/base/constant.rb +1 -1
- data/lib/dcc/si/base/coverage_interval.rb +36 -0
- data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_unc.rb +34 -0
- data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
- data/lib/dcc/si/base/hybrid.rb +3 -2
- data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
- data/lib/dcc/si/base/real.rb +13 -3
- data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
- data/lib/dcc/si/base/si_list.rb +3 -2
- data/lib/dcc/si/base/standard_mu.rb +1 -1
- data/lib/dcc/si/base.rb +5 -2
- data/lib/dcc/si/v1/coverage_interval.rb +8 -0
- data/lib/dcc/si/v1/expanded_unc.rb +1 -16
- data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v1.rb +5 -2
- data/lib/dcc/si/v2/coverage_interval.rb +8 -0
- data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
- data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
- data/lib/dcc/si/v2/expanded_unc.rb +8 -0
- data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v2.rb +8 -3
- data/lib/dcc/signature/result.rb +2 -2
- data/lib/dcc/signature/signer.rb +31 -18
- data/lib/dcc/signature/verifier.rb +23 -21
- data/lib/dcc/signature.rb +1 -1
- data/lib/dcc/streaming/reader.rb +246 -0
- data/lib/dcc/streaming.rb +79 -0
- data/lib/dcc/transform/result.rb +1 -1
- data/lib/dcc/transform/xslt.rb +22 -12
- data/lib/dcc/transform.rb +5 -4
- data/lib/dcc/type/base64_binary.rb +5 -3
- data/lib/dcc/type/decimal_xml_list.rb +99 -13
- data/lib/dcc/type/iso_country_code.rb +3 -2
- data/lib/dcc/type/iso_language_code.rb +3 -2
- data/lib/dcc/type/schema_version.rb +3 -2
- data/lib/dcc/type/si_unit.rb +3 -2
- data/lib/dcc/type_guards.rb +3 -3
- data/lib/dcc/v2/calibration_laboratory.rb +2 -1
- data/lib/dcc/v2/condition.rb +22 -1
- data/lib/dcc/v2/configuration.rb +1 -1
- data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v2/identification.rb +14 -1
- data/lib/dcc/v2/mathml.rb +8 -0
- data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v2/quantity.rb +4 -2
- data/lib/dcc/v2.rb +4 -2
- data/lib/dcc/v3/calibration_laboratory.rb +2 -1
- data/lib/dcc/v3/configuration.rb +1 -1
- data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v3/mathml.rb +8 -0
- data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v3/quantity.rb +4 -2
- data/lib/dcc/v3.rb +12 -2
- data/lib/dcc/validate/business_rules.rb +23 -9
- data/lib/dcc/validate/issue.rb +2 -1
- data/lib/dcc/validate/result.rb +6 -4
- data/lib/dcc/validate/schematron/profile.rb +73 -18
- data/lib/dcc/validate/schematron/rule.rb +13 -3
- data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
- data/lib/dcc/validate/schematron/rules/base.rb +1 -2
- data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
- data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
- data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
- data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
- data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
- data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
- data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
- data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
- data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
- data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
- data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
- data/lib/dcc/validate/schematron/rules.rb +28 -14
- data/lib/dcc/validate/schematron.rb +1 -1
- data/lib/dcc/validate/xsd.rb +9 -3
- data/lib/dcc/validate.rb +1 -1
- data/lib/dcc/version.rb +1 -1
- data/lib/dcc/versioned_parser.rb +1 -1
- data/lib/dcc.rb +68 -10
- data/sample_dcc.xml +140 -0
- data/web/index.html +12 -0
- data/web/package-lock.json +1599 -0
- data/web/package.json +22 -0
- data/web/src/App.vue +349 -0
- data/web/src/lib/builder.ts +171 -0
- data/web/src/lib/dcc.ts +338 -0
- data/web/src/lib/units.json +5581 -0
- data/web/src/main.ts +5 -0
- data/web/src/style.css +27 -0
- data/web/tsconfig.json +15 -0
- data/web/vite.config.ts +14 -0
- metadata +148 -9
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dcc
|
|
4
|
+
module Plugin
|
|
5
|
+
# Included by a plugin class so it can declare what it adds to Dcc.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class MyPlugin
|
|
9
|
+
# include Dcc::Plugin::Base
|
|
10
|
+
# register_validator MyRule
|
|
11
|
+
# end
|
|
12
|
+
module Base
|
|
13
|
+
# @param base [Class] the including class.
|
|
14
|
+
def self.included(base)
|
|
15
|
+
base.extend(ClassMethods)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# `Profile#call` does `rule_class.new.check_on(dcc)`, so a validator
|
|
19
|
+
# has to be a class. An instance responds to `#check_on` and so looks
|
|
20
|
+
# right, but would fail deep inside validation with a message naming
|
|
21
|
+
# neither the plugin nor the author's line.
|
|
22
|
+
#
|
|
23
|
+
# @param entry [Object] the candidate rule.
|
|
24
|
+
# @raise [ArgumentError] unless entry is a class defining #check_on.
|
|
25
|
+
# @return [Class] the entry.
|
|
26
|
+
def self.rule_class!(entry)
|
|
27
|
+
return entry if rule_class?(entry)
|
|
28
|
+
|
|
29
|
+
raise ::ArgumentError,
|
|
30
|
+
"expected a rule class responding to #check_on, " \
|
|
31
|
+
"got #{describe(entry)}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param entry [Object]
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def self.rule_class?(entry)
|
|
37
|
+
entry.is_a?(::Class) && entry.method_defined?(:check_on)
|
|
38
|
+
end
|
|
39
|
+
private_class_method :rule_class?
|
|
40
|
+
|
|
41
|
+
# @param entry [Object]
|
|
42
|
+
# @return [String]
|
|
43
|
+
def self.describe(entry)
|
|
44
|
+
return entry.inspect if entry.is_a?(::Class)
|
|
45
|
+
|
|
46
|
+
"an instance of #{entry.class}"
|
|
47
|
+
end
|
|
48
|
+
private_class_method :describe
|
|
49
|
+
|
|
50
|
+
# Declaration helpers available on the including class.
|
|
51
|
+
module ClassMethods
|
|
52
|
+
# Add a Schematron rule to the active validation profile.
|
|
53
|
+
#
|
|
54
|
+
# @param rule_class [Class] a rule class defining `#check_on(dcc)`.
|
|
55
|
+
# @raise [ArgumentError] unless rule_class is such a class.
|
|
56
|
+
# @return [Class] the rule class.
|
|
57
|
+
def register_validator(rule_class)
|
|
58
|
+
checked = ::Dcc::Plugin::Base.rule_class!(rule_class)
|
|
59
|
+
::Dcc::Plugin.register(:validators, checked)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/dcc/plugin/registry.rb
CHANGED
|
@@ -9,12 +9,17 @@ module Dcc
|
|
|
9
9
|
@mutex = ::Mutex.new
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
|
+
# Idempotent per (category, entry): registering the same entry in
|
|
13
|
+
# the same category twice is a no-op, so a doubly-loaded plugin does
|
|
14
|
+
# not run its validators twice.
|
|
15
|
+
#
|
|
12
16
|
# @param category [Symbol]
|
|
13
17
|
# @param entry [Class, Object]
|
|
14
18
|
# @return [Class, Object]
|
|
15
19
|
def register(category, entry)
|
|
16
20
|
@mutex.synchronize do
|
|
17
|
-
@categories[category.to_sym]
|
|
21
|
+
entries = @categories[category.to_sym]
|
|
22
|
+
entries << entry unless entries.include?(entry)
|
|
18
23
|
entry
|
|
19
24
|
end
|
|
20
25
|
end
|
|
@@ -42,4 +47,4 @@ module Dcc
|
|
|
42
47
|
end
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
|
-
end
|
|
50
|
+
end
|
data/lib/dcc/plugin.rb
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
# without modifying the core codebase (Open/Closed Principle).
|
|
6
6
|
#
|
|
7
7
|
# @example Register a custom validator
|
|
8
|
-
#
|
|
8
|
+
# class MyPlugin
|
|
9
|
+
# include Dcc::Plugin::Base
|
|
10
|
+
# register_validator MyCustomRule
|
|
11
|
+
# end
|
|
9
12
|
#
|
|
10
13
|
# @example List all registered validators
|
|
11
14
|
# Dcc::Plugin.all(:validators)
|
|
12
15
|
module Dcc
|
|
13
16
|
module Plugin
|
|
14
17
|
autoload :Registry, "dcc/plugin/registry"
|
|
18
|
+
autoload :Base, "dcc/plugin/base"
|
|
15
19
|
|
|
16
20
|
class << self
|
|
17
21
|
# @param category [Symbol] e.g. :validators, :converters, :cli_commands.
|
|
@@ -39,4 +43,4 @@ module Dcc
|
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
|
-
end
|
|
46
|
+
end
|
|
@@ -45,25 +45,24 @@ module Dcc
|
|
|
45
45
|
private
|
|
46
46
|
|
|
47
47
|
def format_value
|
|
48
|
-
# Two decimals by default
|
|
49
|
-
|
|
48
|
+
# Two decimals by default, tightened to the uncertainty's precision
|
|
49
|
+
# when it has any. Neither a nil nor a NaN uncertainty has one.
|
|
50
|
+
return @value.round(2).to_s("F") if uncertainty.nil? || uncertainty.nan?
|
|
50
51
|
|
|
51
|
-
# Decimal places = 1 - floor(log10(uncertainty))
|
|
52
|
-
decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
53
52
|
@value.round(decimal_places).to_s("F")
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def format_uncertainty
|
|
57
56
|
return "" if uncertainty.nil?
|
|
57
|
+
return "NaN" if uncertainty.nan?
|
|
58
58
|
|
|
59
|
-
decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
60
59
|
uncertainty.round(decimal_places).to_s("F")
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def align_value_and_uncertainty
|
|
64
63
|
return [@value.to_s("F"), uncertainty.to_s("F")] if uncertainty.nil?
|
|
64
|
+
return [format_value, "NaN"] if uncertainty.nan?
|
|
65
65
|
|
|
66
|
-
decimal_places = [0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
67
66
|
v_str = @value.round(decimal_places).to_s("F")
|
|
68
67
|
u_str = uncertainty.round(decimal_places).to_s("F")
|
|
69
68
|
# Short form: digits of uncertainty after the last common digit
|
|
@@ -71,6 +70,13 @@ module Dcc
|
|
|
71
70
|
[v_str, last_digits]
|
|
72
71
|
end
|
|
73
72
|
|
|
73
|
+
# Decimal places = 1 - floor(log10(uncertainty)). D-SI writes `NaN` for a
|
|
74
|
+
# not-measured entry (SI_Format.xsd:1195) and `Math.log10` cannot answer
|
|
75
|
+
# for it, so every caller excludes nil and NaN first.
|
|
76
|
+
def decimal_places
|
|
77
|
+
[0, 1 - Integer(Math.log10(uncertainty.to_f).floor)].max
|
|
78
|
+
end
|
|
79
|
+
|
|
74
80
|
def unit_suffix
|
|
75
81
|
return "" if unit.nil? || unit.empty?
|
|
76
82
|
|
|
@@ -81,10 +87,10 @@ module Dcc
|
|
|
81
87
|
unit
|
|
82
88
|
.to_s
|
|
83
89
|
.gsub(/\\([a-z]+)/, '\\1') # \kelvin → kelvin
|
|
84
|
-
.gsub(
|
|
85
|
-
.gsub(
|
|
86
|
-
.gsub(
|
|
90
|
+
.gsub("\\per", "/")
|
|
91
|
+
.gsub("\\cubic", "^3")
|
|
92
|
+
.gsub("\\square", "^2")
|
|
87
93
|
end
|
|
88
94
|
end
|
|
89
95
|
end
|
|
90
|
-
end
|
|
96
|
+
end
|
data/lib/dcc/quantity_format.rb
CHANGED
|
@@ -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
|