sy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ #encoding: utf-8
2
+
3
+ # Adds abstract algebra concepts to Ruby.
4
+ #
5
+ module Algebra
6
+ # A Monoid requires:
7
+ #
8
+ # Closed and associative addition: #add method
9
+ # Additive identity element: #additive_identity
10
+ #
11
+ module Monoid
12
+ def + summand; add summand end
13
+ def zero; additive_identity end
14
+ end
15
+
16
+ # A group is a monoid with additive inverse.
17
+ #
18
+ # additive inversion: #additive_inverse
19
+ #
20
+ module Group
21
+ include Monoid
22
+ def -@; additive_inverse end
23
+ def - subtrahend; add subtrahend.additive_inverse end
24
+ end
25
+
26
+ # A group that fulfills the condition of commutativity
27
+ #
28
+ # ( a.add b == b.add a ).
29
+ #
30
+ module AbelianGroup
31
+ include Group
32
+ end
33
+
34
+ # A ring is a commutative group with multiplication.
35
+ #
36
+ # multiplication: #multiply (associative, distributive)
37
+ # multiplicative identity element: #multiplicative_identity
38
+ #
39
+ module Ring
40
+ include AbelianGroup
41
+ def * multiplicand; multiply multiplicand end
42
+ def one; multiplicative_identity end
43
+ end
44
+
45
+ # A field is a ring that can do division.
46
+ #
47
+ module Field
48
+ include Ring
49
+ def inverse; multiplicative_inverse end
50
+ def / divisor; multiply divisor.multiplicative_inverse end
51
+ end
52
+ end
53
+
54
+ # Patching Integer with Algebra::Ring compliance methods.
55
+ #
56
+ class << Integer
57
+ def additive_identity; 0 end
58
+ alias zero additive_identity
59
+ def add( other ); self + other end
60
+ def additive_inverse; -self end
61
+ def multiply( other ); self * other end
62
+ def multiplicative_identity; 1 end
63
+ end
64
+
65
+ # Patching Float with Algebra::Field compliance methods.
66
+ #
67
+ class << Float
68
+ def additive_identity; 0.0 end
69
+ alias zero additive_identity
70
+ def add( other ); self + other end
71
+ def additive_inverse; -self end
72
+ def multiply( other ); self * other end
73
+ def multiplicative_identity; 1.0 end
74
+ alias one multiplicative_identity
75
+ def multiplicative_inverse; 1.0 / self end
76
+ end
77
+
78
+ # Patching Rational with Algebra::Field compliance methods.
79
+ #
80
+ class << Rational
81
+ def additive_identity; Rational 0, 1 end
82
+ alias zero additive_identity
83
+ def add( other ); self + other end
84
+ def additive_inverse; -self end
85
+ def multiply( other ); self * other end
86
+ def multiplicative_identity; Rational 1, 1 end
87
+ alias one multiplicative_identity
88
+ def multiplicative_inverse; Rational( 1, 1 ) / self end
89
+ end
90
+
91
+ # Patching Complex with #zero method.
92
+ #
93
+ class << Complex
94
+ def additive_identity; Complex 0.0, 0.0 end
95
+ alias zero additive_identity
96
+ def add( other ); self + other end
97
+ def additive_inverse; -self end
98
+ def multiply( other ); self * other end
99
+ def multiplicative_identity; Complex 1, 0 end
100
+ alias one multiplicative_identity
101
+ def multiplicative_inverse; Complex( 1, 0 ) / self end
102
+ end
@@ -0,0 +1,219 @@
1
+ #encoding: utf-8
2
+
3
+ # Composition of quantities.
4
+ #
5
+ class SY::Composition < Hash
6
+ # Simplification rules for quantity combinations.
7
+ #
8
+ SR = SIMPLIFICATION_RULES = []
9
+
10
+ # SY::Amount and SY::Amount.relative are disposable:
11
+ SR << -> ꜧ {
12
+ ꜧ.reject! { |qnt, _| qnt == SY::Amount || qnt == SY::Amount.relative }
13
+ }
14
+ # Relative quantities of the composition are absolutized:
15
+ SR << -> ꜧ {
16
+ ꜧ.select { |qnt, _| qnt.relative? }.each { |qnt, exp|
17
+ ꜧ.delete qnt
18
+ ꜧ.update qnt.absolute => exp
19
+ }
20
+ }
21
+ # Any quantities with exponent zero can be deleted:
22
+ SR << -> ꜧ {
23
+ ꜧ.reject! { |_, exp| exp == 0 }
24
+ }
25
+ #
26
+ SR << -> ꜧ {
27
+ begin
28
+ q1, q2, q3 = SY::MoleAmount, SY::LitreVolume, SY::Molarity
29
+ rescue NameError
30
+ return ꜧ
31
+ end
32
+ # transform = -> e1, e2 {
33
+ # return e1, e2 unless e1 != 0 && e2 != 0 && ( e1 <=> 0 ) != ( e2 <=> 0 )
34
+ # n = e1 <=> 0
35
+ # return e1 - n, e2 + n
36
+ # }
37
+ # e1 = ꜧ.delete q1
38
+ # e2 = ꜧ.delete q2
39
+ # ꜧ.merge! q1 => e1, q2 => e2
40
+ # else
41
+ # ꜧ.update q1 => e1, q2 => e2
42
+ # end
43
+ e1 = ꜧ.delete q1
44
+ e2 = ꜧ.delete q2
45
+ if e1 && e2 && e1 > 0 && e2 < 0 then
46
+ e1 -= 1
47
+ e2 += 1
48
+ e3 = ꜧ.delete q3
49
+ ꜧ.update q3 => ( e3 ? e3 + 1 : 1 )
50
+ end
51
+ ꜧ.update q1 => e1 if e1 && e1 != 0
52
+ ꜧ.update q2 => e2 if e2 && e2 != 0
53
+ return ꜧ
54
+ }
55
+
56
+ class << self
57
+ def singular quantity
58
+ self[ SY.Quantity( quantity ) => 1 ]
59
+ end
60
+
61
+ def empty
62
+ self[]
63
+ end
64
+ end
65
+
66
+ # Cache for quantity construction.
67
+ #
68
+ QUANTITY_TABLE = Hash.new { |ꜧ, args|
69
+ if args.keys.include? [:name] || args.keys.include?( :ɴ ) then
70
+ ɴ = args.delete( :name ) || args.delete( :ɴ ) # won't cache name
71
+ ꜧ[args].tap { |ɪ| ɪ.name = ɴ } # recursion
72
+ elsif args.keys.include? :mapping then
73
+ ᴍ = args.delete( :mapping ) # won't cache mapping
74
+ ꜧ[args].tap { |ɪ| ɪ.set_mapping ᴍ } # recursion
75
+ elsif args.keys.include? :relative then
76
+ ʀ = args.delete( :relative ) ? true : false # won't cache :relative
77
+ ꜧ[args].send ʀ ? :relative : :absolute # recursion
78
+ else
79
+ cᴍ = SY::Composition[ args ].simplify
80
+ ꜧ[args] = if cᴍ != args then ꜧ[ cᴍ ] # recursion while #simplify
81
+ else x = cᴍ.expand # we'll try to #expand now
82
+ if x != cᴍ then ꜧ[ x ] # recursion while #expand
83
+ else
84
+ if x.empty? then # use std. ∅ quantity
85
+ SY.Dimension( :∅ ).standard_quantity
86
+ elsif x.singular? then
87
+ x.first[0] # unwrap the quantity
88
+ else # create new quantity
89
+ SY::Quantity.new composition: x
90
+ end
91
+ end
92
+ end
93
+ end
94
+ }
95
+
96
+ # Singular compositions consist of only one quantity.
97
+ #
98
+ def singular?
99
+ size == 1 && first[1] == 1
100
+ end
101
+
102
+ # Atomic compositions are singular compositions, whose quantity dimension
103
+ # is a base dimension.
104
+ #
105
+ def atomic?
106
+ singular? && first[0].dimension.base?
107
+ end
108
+
109
+ # Returns a new instance with same hash.
110
+ #
111
+ def +@
112
+ self.class[ self ]
113
+ end
114
+
115
+ # Negates hash exponents.
116
+ #
117
+ def -@
118
+ self.class[ self.with_values do |v| -v end ]
119
+ end
120
+
121
+ # Merges two compositions.
122
+ #
123
+ def + other
124
+ self.class[ self.merge( other ) { |_, v1, v2| v1 + v2 } ]
125
+ end
126
+
127
+ # Subtracts two compositions.
128
+ #
129
+ def - other
130
+ self + -other
131
+ end
132
+
133
+ # Multiplication by a number.
134
+ #
135
+ def * number
136
+ self.class[ self.with_values do |v| v * number end ]
137
+ end
138
+
139
+ # Division by a number.
140
+ #
141
+ def / number
142
+ self.class[ self.with_values do |val|
143
+ raise TErr, "Compositions with rational exponents " +
144
+ "not implemented!" if val % number != 0
145
+ val / number
146
+ end ]
147
+ end
148
+
149
+ # Simplifies a quantity hash by applying simplification rules.
150
+ #
151
+ def simplify
152
+ ꜧ = self.to_hash
153
+ puts "simplifying #{ꜧ}" if SY::DEBUG
154
+ SIMPLIFICATION_RULES.each { |rule| rule.( ꜧ ) }
155
+ self.class[ ꜧ ].tap { |_| puts "result is #{_}" if SY::DEBUG }
156
+ end
157
+
158
+ # Returns the quantity appropriate to this composition.
159
+ #
160
+ def to_quantity args={}
161
+ # All the work is delegated to the quantity table:
162
+ QUANTITY_TABLE[ args.merge( self ) ]
163
+ end
164
+
165
+ # Directly (without attempts to simplify) creates a new quantity from self.
166
+ # If self is empty or singular, SY::Amount, resp. the singular quantity
167
+ # in question is returned.
168
+ #
169
+ def new_quantity args={}
170
+ SY::Quantity.new args.merge( composition: self )
171
+ end
172
+
173
+ # Dimension of a quantity composition is the sum of its dimensions.
174
+ #
175
+ def dimension
176
+ map { |qnt, exp| qnt.dimension * exp }.reduce SY::Dimension.zero, :+
177
+ end
178
+
179
+ # Infers the mapping of the composition's quantity. (Mapping
180
+ # defines mapping of a quantity to the standard quantity of its dimension.)
181
+ #
182
+ def infer_mapping
183
+ puts "#infer_mapping; hash is #{self}" if SY::DEBUG
184
+ map do |qnt, exp|
185
+ qnt.standardish? ? SY::Mapping.identity :
186
+ qnt.mapping_to( qnt.standard ) ** exp
187
+ end.reduce( SY::Mapping.identity, :* )
188
+ end
189
+
190
+ # Simple composition is one that is either empty or singular.
191
+ #
192
+ def simple?
193
+ empty? or singular?
194
+ end
195
+
196
+ # Whether it is possible to expand this
197
+ def irreducible?
198
+ all? { |qnt, exp| qnt.irreducible? }
199
+ end
200
+
201
+ # Try to simplify the composition by decomposing its quantities.
202
+ #
203
+ def expand
204
+ return self if irreducible?
205
+ puts "#expand: #{self} not irreducible" if SY::DEBUG
206
+ self.class[ reduce( self.class.empty ) { |cᴍ, pair|
207
+ qnt, exp = pair
208
+ puts "#expand: qnt: #{qnt}, exp: #{exp}" if SY::DEBUG
209
+ puts "cᴍ is #{cᴍ}" if SY::DEBUG
210
+ ( cᴍ + if qnt.irreducible? then
211
+ self.class.singular( qnt ) * exp
212
+ else
213
+ qnt.composition * exp
214
+ end.tap { |x| puts "Adding #{x}." if SY::DEBUG }
215
+ ).tap { |x| puts "Result is #{x}." if SY::DEBUG }
216
+ } ]
217
+ .tap{ |rslt| puts "#expand: result is #{rslt}" if SY::DEBUG }
218
+ end
219
+ end # class SY::Composition
@@ -0,0 +1,182 @@
1
+ #encoding: utf-8
2
+
3
+ # This class represents physical dimension of a metrological quantity.
4
+ #
5
+ class SY::Dimension
6
+ # Let's set up an instance variable (of the class) that will hold standard
7
+ # quantities of selected dimensions:
8
+ #
9
+ @standard_quantities ||= Hash.new { |ꜧ, missing_key|
10
+ if missing_key.is_a? SY::Dimension then
11
+ # Create a new quantity of that dimension:
12
+ ꜧ[missing_key] = SY::Quantity.of missing_key
13
+ else
14
+ # Otherwise, let SY.Dimension constructor judge:
15
+ ꜧ[ SY.Dimension missing_key ]
16
+ end
17
+ }
18
+
19
+ class << self
20
+ alias __new__ new
21
+ attr_reader :standard_quantities
22
+
23
+ # The #new constructor of SY::Dimension has been changed, so that the
24
+ # same instance is returned, if that dimension has already been created.
25
+ #
26
+ def new dim={}
27
+ ꜧ = case dim
28
+ when Hash then dim
29
+ when self then return dim # it is a dimension instance
30
+ else # we'll treat dimension_specification as SPS
31
+ SY::BASE_DIMENSIONS.parse_sps( dim )
32
+ end
33
+ # Zeros by default:
34
+ ꜧ.default! Hash[ SY::BASE_DIMENSIONS.base_symbols.map { |ß| [ß, 0] } ]
35
+ # Now we can see whether the instance of this dimension already exists.
36
+ return instances.find { |inst| inst.to_hash == ꜧ } ||
37
+ __new__( ꜧ ).tap { |inst| instances << inst }
38
+ end
39
+
40
+ # Presents class-owned instances (array).
41
+ #
42
+ def instances
43
+ return @instances ||= []
44
+ end
45
+
46
+ # Base dimension constructor. Base dimension symbol is expeced as argument.
47
+ #
48
+ def base symbol
49
+ raise AErr, "Unknown base dimension: #{symbol}" unless
50
+ SY::BASE_DIMENSIONS.base_symbols.include? symbol
51
+ return new( symbol => 1 )
52
+ end
53
+ alias basic base
54
+
55
+ # Constructor for zero dimension ("dimensionless").
56
+ #
57
+ def zero; new end
58
+ end
59
+
60
+ attr_accessor *SY::BASE_DIMENSIONS.base_symbols
61
+
62
+ # Dimension can be initialized either by supplying a hash
63
+ # (such as Dimension.new L: 1, T: -2) or by supplying a SPS
64
+ # (superscripted product string), such as Dimension.new( "L.T⁻²" ).
65
+ # It is also possible to supply a Dimension instance, which will
66
+ # result in a new Dimension instance equal to the supplied one.
67
+ #
68
+ def initialize hash
69
+ SY::BASE_DIMENSIONS.base_symbols.each { |ß|
70
+ instance_variable_set "@#{ß}", hash[ß]
71
+ }
72
+ end
73
+
74
+ # #[] method provides access to the dimension components, such as
75
+ # d = Dimension.new( L: 1, T: -2 ), d[:T] #=> -2
76
+ #
77
+ def [] ß
78
+ return send ß if SY::BASE_DIMENSIONS.letters.include? ß
79
+ raise AErr, "Unknown basic dimension: #{ß}"
80
+ end
81
+
82
+ #Two dimensions are equal, if their exponents are equal.
83
+ #
84
+ def == other
85
+ to_a == other.to_a
86
+ end
87
+
88
+ # Dimension arithmetic: addition. (+, -, *, /).
89
+ #
90
+ def + other
91
+ ç.new Hash[ SY::BASE_DIMENSIONS.base_symbols.map do |l|
92
+ [ l, self.send( l ) + other.send( l ) ]
93
+ end ]
94
+ end
95
+
96
+ # Dimension arithmetic: subtraction.
97
+ #
98
+ def - other
99
+ ç.new Hash[ SY::BASE_DIMENSIONS.base_symbols.map do |l|
100
+ [ l, self.send( l ) - other.send( l ) ]
101
+ end ]
102
+ end
103
+
104
+ # Dimension arithmetic: multiplication by a number.
105
+ #
106
+ def * number
107
+ ç.new Hash[ SY::BASE_DIMENSIONS.base_symbols.map do |l|
108
+ [ l, self.send( l ) * number ]
109
+ end ]
110
+ end
111
+
112
+ # Dimension arithmetic: division by a number.
113
+ #
114
+ def / number
115
+ ç.new Hash[ SY::BASE_DIMENSIONS.base_symbols.map do |l|
116
+ exp = send l
117
+ raise TErr, "Dimensions with rational exponents " +
118
+ "not implemented!" if exp % number != 0
119
+ [ l, exp / number ]
120
+ end ]
121
+ end
122
+
123
+ # Conversion to an array (of exponents of in the order of the
124
+ # basic dimension letters).
125
+ #
126
+ def to_a
127
+ SY::BASE_DIMENSIONS.base_symbols.map { |l| self.send l }
128
+ end
129
+
130
+ # Conversion to a hash (eg. { L: 1, M: 0, T: -2, Q: 0, Θ: 0 } ).⁻³
131
+ #
132
+ def to_hash
133
+ SY::BASE_DIMENSIONS.base_symbols.each_with_object Hash.new do |l, ꜧ|
134
+ ꜧ[ l ] = self.send( l )
135
+ end
136
+ end
137
+
138
+ # True if the dimension is zero ("dimensionless"), otherwise false.
139
+ #
140
+ def zero?
141
+ to_a.all? { |e| e.zero? }
142
+ end
143
+
144
+ # True if the dimension is basic, otherwise false.
145
+ #
146
+ def base?
147
+ to_a.count( 1 ) == 1 &&
148
+ to_a.count( 0 ) == SY::BASE_DIMENSIONS.base_symbols.size - 1
149
+ end
150
+ alias basic? base?
151
+
152
+ # Converts the dimension into its SPS.
153
+ #
154
+ def to_s
155
+ sps = SY::SPS.( SY::BASE_DIMENSIONS.base_symbols, to_a )
156
+ return sps == "" ? "∅" : sps
157
+ end
158
+
159
+ # Produces the inspect string of the dimension.
160
+ #
161
+ def inspect
162
+ "#<Dimension: #{self} >"
163
+ end
164
+
165
+ # Returns dimension's standard quantity.
166
+ #
167
+ def standard_quantity
168
+ self.class.standard_quantities[ self ]
169
+ end
170
+
171
+ # Returns default quantity composition for this dimension.
172
+ #
173
+ def to_composition
174
+ SY::Composition[ SY::BASE_DIMENSIONS.base_symbols
175
+ .map { |l| self.class.base l }
176
+ .map { |dim| self.class.standard_quantities[ dim ] }
177
+ .map { |qnt| qnt.absolute }
178
+ .zip( to_a ).delete_if { |qnt, exp| exp.zero? } ]
179
+ end
180
+
181
+ delegate :standard_unit, to: :standard_quantity
182
+ end # class SY::Dimension