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.
data/lib/sy/mapping.rb ADDED
@@ -0,0 +1,84 @@
1
+ #encoding: utf-8
2
+
3
+ # Represents relationship of two quantities. Provides import and export
4
+ # conversion closures. Instances are immutable and have 2 attributes:
5
+ #
6
+ # * im - import closure, converting amount of quantity 1 into quantity 2
7
+ # * ex - export closure, converting amount of quantity 2 into quantity 1
8
+ #
9
+ # Convenience methods for mapping magnitudes are:
10
+ #
11
+ # * import - like im, but operates on magnitudes
12
+ # * export - like ex, but operates on magnitudes
13
+ #
14
+ class SY::Mapping
15
+ class << self
16
+ def identity
17
+ new 1
18
+ end
19
+ end
20
+
21
+ attr_reader :ex, :im, :ratio
22
+
23
+ # Takes either a magnitude (1 argument), or 2 named arguments :im, :ex
24
+ # speficying amount import and export closure. For a magnitude, these
25
+ # closures are constructed automatically, assuming simple ratio rule.
26
+ #
27
+ def initialize arg
28
+ case arg
29
+ when Hash then
30
+ @ex, @im = arg[:ex], arg[:im]
31
+ else
32
+ @ratio = r = arg
33
+ @ex = lambda { |amount1| amount1 * r }
34
+ @im = lambda { |amount2| amount2 / r }
35
+ end
36
+ end
37
+
38
+ def import magnitude, from_quantity
39
+ from_quantity.magnitude @im.( magnitude.amount )
40
+ end
41
+
42
+ def export magnitude, to_quantity
43
+ to_quantity.magnitude @ex.( magnitude.amount )
44
+ end
45
+
46
+ def inverse
47
+ self.class.new begin
48
+ 1 / @ratio
49
+ rescue NoMethodError, TypeError
50
+ i, e = im, ex
51
+ { im: e, ex: i } # swap closures
52
+ end
53
+ end
54
+
55
+ def * r2 # mapping composition
56
+ ç.new begin
57
+ @ratio * r2.ratio
58
+ rescue NoMethodError, TypeError
59
+ i1, i2, e1, e2 = im, r2.im, ex, r2.ex
60
+ { ex: lambda { |a1| e2.( e1.( a1 ) ) }, # export compose
61
+ im: lambda { |a2| i1.( i2.( a2 ) ) } } # import compose
62
+ end
63
+ end
64
+
65
+ def / r2
66
+ self * r2.inverse
67
+ end
68
+
69
+ def ** n
70
+ ç.new begin
71
+ n == 1 ? @ratio * 1 : @ratio ** n
72
+ rescue NoMethodError, TypeError
73
+ i, e = im, ex
74
+ { ex: lambda { |a1| n.times.reduce a1 do |m, _| e.( m ) end },
75
+ im: lambda { |a2| n.times.reduce a2 do |m, _| i.( m ) end } }
76
+ end
77
+ end
78
+
79
+ protected
80
+
81
+ def []( *args )
82
+ send *args
83
+ end
84
+ end # class SY::Mapping
data/lib/sy/matrix.rb ADDED
@@ -0,0 +1,69 @@
1
+ #encoding: utf-8
2
+
3
+ require 'matrix'
4
+
5
+ # As a matter of fact, current version of the Matrix class (by Marc-Andre
6
+ # Lafortune) does not work with physical magnitudes. It is a feature of the
7
+ # physical magnitudes, that they do not allow themselves summed with plain
8
+ # numbers or incompatible magnitudes. But current version of Matrix class,
9
+ # upon matrix multiplication, performs needless addition of the matrix elements
10
+ # to literal numeric 0.
11
+ #
12
+ # The obvious solution is to patch Matrix class so that the needless addition
13
+ # to literal 0 is no longer performed.
14
+ #
15
+ # More systematically, abstract algebra is to be added to Ruby, and Matrix is
16
+ # to require that its elements comply with monoid, group, ring, field, depending
17
+ # on the operation one wants to do with such matrices.
18
+ #
19
+ class Matrix
20
+ # Matrix multiplication.
21
+ #
22
+ def * arg # arg is matrix or vector or number
23
+ case arg
24
+ when Numeric
25
+ rows = @rows.map { |row| row.map { |e| e * arg } }
26
+ return new_matrix rows, column_size
27
+ when Vector
28
+ arg = Matrix.column_vector arg
29
+ result = self * arg
30
+ return result.column 0
31
+ when Matrix
32
+ Matrix.Raise ErrDimensionMismatch if column_size != arg.row_size
33
+ if empty? then # if empty?, then reduce uses WILDCARD_ZERO
34
+ rows = Array.new row_size do |i|
35
+ Array.new arg.column_size do |j|
36
+ ( 0...column_size ).reduce WILDCARD_ZERO do |sum, c|
37
+ sum + arg[c, j] * self[i, c]
38
+ end
39
+ end
40
+ end
41
+ else # if non-empty, reduce proceeds without WILDCARD_ZERO
42
+ rows = Array.new row_size do |i|
43
+ Array.new arg.column_size do |j|
44
+ ( 0...column_size ).map { |c| arg[c, j] * self[i, c] }.reduce :+
45
+ end
46
+ end
47
+ end
48
+ return new_matrix( rows, arg.column_size )
49
+ when SY::Magnitude # newly added - multiplication by a magnitude
50
+ # I am not happy with this explicit switch on SY::Magnitude type here.
51
+ # Perhaps coerce should handle this?
52
+ rows = Array.new row_size do |i|
53
+ Array.new column_size do |j|
54
+ self[i, j] * arg
55
+ end
56
+ end
57
+ return self.class[ *rows ]
58
+ else
59
+ compat_1, compat_2 = arg.coerce self
60
+ return compat_1 * compat_2
61
+ end
62
+ end
63
+
64
+ # Creates a matrix of prescribed dimensions filled with wildcard zeros.
65
+ #
66
+ def Matrix.wildcard_zero r_count, c_count=r_count
67
+ build r_count, c_count do |r, c| WILDCARD_ZERO end
68
+ end
69
+ end
@@ -0,0 +1,455 @@
1
+ #encoding: utf-8
2
+
3
+ # Quantity.
4
+ #
5
+ class SY::Quantity
6
+ include NameMagic
7
+
8
+ # name_set_closure do |name, new_instance, old_name|
9
+ # new_instance.protect!; name
10
+ # end
11
+
12
+ RELATIVE_QUANTITY_NAME_SUFFIX = "±"
13
+
14
+ attr_reader :MagnitudeModule, :Magnitude, :Unit
15
+ attr_reader :dimension, :composition, :mapping
16
+
17
+ class << self
18
+ # Dimension-based quantity constructor. Examples:
19
+ # <tt>Quantity.of Dimension.new( "L.T⁻²" )</tt>
20
+ # <tt>Quantity.of "L.T⁻²"</tt>
21
+ #
22
+ def of *args
23
+ ꜧ = args.extract_options!
24
+ dim = case args.size
25
+ when 0 then
26
+ ꜧ.must_have :dimension, syn!: :of
27
+ ꜧ.delete :dimension
28
+ else args.shift end
29
+ args << ꜧ.merge!( dimension: SY::Dimension.new( dim ) )
30
+ return new( *args ).protect!
31
+ end
32
+
33
+ # Standard quantity. Example:
34
+ # <tt>Quantity.standard of: Dimension.new( "L.T⁻²" )</tt>
35
+ # or
36
+ # <tt>Quantity.standard of: "L.T⁻²"
37
+ # (Both should give Acceleration as their result.)
38
+ #
39
+ def standard *args
40
+ ꜧ = args.extract_options!
41
+ dim = case args.size
42
+ when 0 then
43
+ ꜧ.must_have :dimension, syn!: :of
44
+ ꜧ.delete :dimension
45
+ else args.shift end
46
+ return SY.Dimension( dim ).standard_quantity
47
+ end
48
+
49
+ # Dimensionless quantity constructor alias.
50
+ #
51
+ def dimensionless *args
52
+ ꜧ = args.extract_options!
53
+ raise TErr, "Dimension not zero!" unless ꜧ[:dimension].zero? if
54
+ ꜧ.has? :dimension, syn!: :of
55
+ new( *( args << ꜧ.merge!( dimension: SY::Dimension.zero ) ) ).protect!
56
+ end
57
+ alias :zero :dimensionless
58
+ end
59
+
60
+ # Standard constructor of a metrological quantity. A quantity may have
61
+ # a name and a dimension.
62
+ #
63
+ def initialize args
64
+ puts "Quantity init #{args}" if SY::DEBUG
65
+ @relative = args[:relative]
66
+ comp = args[:composition]
67
+ if comp.nil? then # composition not given
68
+ puts "Composition not received, dimension expected." if SY::DEBUG
69
+ dim = args[:dimension] || args[:of]
70
+ @dimension = SY.Dimension( dim )
71
+ else
72
+ puts "Composition received (#{comp})." if SY::DEBUG
73
+ @composition = SY::Composition[ comp ]
74
+ @dimension = @composition.dimension
75
+ end
76
+ rel = args[:mapping] || args[:ratio]
77
+ @mapping = SY::Mapping.new( rel ) if rel
78
+ puts "Composition of the initialized instance is #{composition}." if SY::DEBUG
79
+ end
80
+
81
+ # Simple quantity is one with simple composition. If nontrivial composition
82
+ # is known for the colleague, it is assumed that the same composition would
83
+ # apply for this quantity, so it is not simple.
84
+ #
85
+ def simple?
86
+ cᴍ = composition
87
+ cᴍ.empty? || cᴍ.singular? && cᴍ.first[0] == self
88
+ end
89
+
90
+ # Protected quantity is not allowed to be decomposed in the process of quantity
91
+ # simplification.
92
+ #
93
+ def protected?
94
+ @protected
95
+ end
96
+
97
+ # Protects quantity from decomposition.
98
+ #
99
+ def protect!
100
+ @protected = true
101
+ @composition ||= SY::Composition.singular self
102
+ return self
103
+ end
104
+
105
+ # Unprotects quantity from decomposition.
106
+ #
107
+ def unprotect!
108
+ @protected = false
109
+ @composition = nil if @composition == SY::Composition.singular( self )
110
+ return self
111
+ end
112
+
113
+ # Irreducible quantity is one which cannot or <em>should not</em> be reduced
114
+ # to its components in the process of quantity simplification.
115
+ #
116
+ def irreducible?
117
+ simple? or protected?
118
+ end
119
+
120
+ # Creates a composition from a dimension, or acts as composition getter
121
+ # if this has already been specified.
122
+ #
123
+ def composition
124
+ @composition || dimension.to_composition
125
+ end
126
+
127
+ # Acts as composition setter (dimension must match).
128
+ #
129
+ def set_composition comp
130
+ @composition = SY::Composition[ comp ]
131
+ .aT "composition, when redefined after initialization,",
132
+ "match the dimension" do |comp| comp.dimension == dimension end
133
+ end
134
+
135
+ # Acts as mapping setter.
136
+ #
137
+ def set_mapping mapping
138
+ @mapping = SY::Mapping.new( mapping )
139
+ end
140
+
141
+ def import magnitude2
142
+ quantity2, amount2 = magnitude2.quantity, magnitude2.amount
143
+ magnitude mapping_to( quantity2 ).im.( amount2 )
144
+ end
145
+
146
+ def export amount1, quantity2
147
+ mapping_to( quantity2 ).export( magnitude( amount1 ), quantity2 )
148
+ end
149
+
150
+ # Asks for a mapping of this quantity to another quantity.
151
+ #
152
+ def mapping_to( q2 )
153
+ puts "#{self.inspect} asked about mapping to #{q2}" if SY::DEBUG
154
+ return SY::Mapping.identity if q2 == self or q2 == colleague
155
+ puts "this mapping is not an identity" if SY::DEBUG
156
+ raise SY::DimensionError, "#{self} vs. #{q2}!" unless same_dimension? q2
157
+ if standardish? then
158
+ puts "#{self} is a standardish quantity, will invert the #{q2} mapping" if SY::DEBUG
159
+ return q2.mapping_to( self ).inverse
160
+ end
161
+ puts "#{self} is not a standardish quantity" if SY::DEBUG
162
+ m1 = begin
163
+ if @mapping then
164
+ puts "#{self} has @mapping defined" if SY::DEBUG
165
+ @mapping
166
+ elsif colleague.mapping then
167
+ puts "#{colleague} has @mapping defined" if SY::DEBUG
168
+ colleague.mapping
169
+ else
170
+ puts "Neither #{self} nor its colleague has @mapping defined" if SY::DEBUG
171
+ puts "Will ask #{self}.composition to infer the mapping" if SY::DEBUG
172
+ composition.infer_mapping
173
+ end
174
+ rescue NoMethodError
175
+ raise SY::QuantityError,"Mapping from #{self} to #{q2} cannot be inferred!"
176
+ end
177
+ if q2.standardish? then
178
+ puts "#{q2} is standardish, obtained mapping can be returned directly." if SY::DEBUG
179
+ return m1
180
+ else
181
+ puts "#{q2} not standardish, obtained mapping maps only to #{standard}, and " +
182
+ "therefrom, composition with mapping from #{standard} to #{q2} will be needed" if SY::DEBUG
183
+ return m1 * standard.mapping_to( q2 )
184
+ end
185
+ end
186
+
187
+ # Is the quantity relative?
188
+ #
189
+ def relative?
190
+ @relative ? true : false
191
+ end
192
+
193
+ # Is the quantity absolute? (Opposite of #relative?)
194
+ #
195
+ def absolute?
196
+ not relative?
197
+ end
198
+
199
+ # Relative quantity related to this quantity.
200
+ #
201
+ def relative
202
+ relative? ? self : colleague
203
+ end
204
+
205
+ # For an absolute quantity, colleague is the corresponding relative quantity.
206
+ # Vice-versa, for a relative quantity, colleague is its absolute quantity.
207
+ #
208
+ def colleague
209
+ @colleague ||= construct_colleague
210
+ end
211
+
212
+ # Acts as colleague setter.
213
+ #
214
+ def set_colleague q2
215
+ raise SY::DimensionError, "Mismatch: #{self}, #{q2}!" unless
216
+ same_dimension? q2
217
+ raise SY::QuantityError, "#{self} an #{q2} are both " +
218
+ "{relative? ? 'relative' : 'absolute'}!" if relative? == q2.relative?
219
+ if mapping && q2.mapping then
220
+ raise SY::QuantityError, "Mapping mismatch: #{self}, #{q2}!" unless
221
+ mapping == q2.mapping
222
+ end
223
+ @colleague = q2
224
+ q2.instance_variable_set :@colleague, self
225
+ end
226
+
227
+ # Absolute quantity related to this quantity.
228
+ #
229
+ def absolute
230
+ absolute? ? self : colleague
231
+ end
232
+
233
+ # Reader of standard unit.
234
+ #
235
+ def standard_unit
236
+ Unit().standard
237
+ end
238
+
239
+ # Presents an array of units ordered as favored by this quantity.
240
+ #
241
+ def units
242
+ @units ||= []
243
+ end
244
+
245
+ # Constructs a new absolute magnitude of this quantity.
246
+ #
247
+ def magnitude arg
248
+ Magnitude().new quantity: self, amount: arg
249
+ end
250
+
251
+ # Constructs a new unit of this quantity.
252
+ #
253
+ def unit args={}
254
+ Unit().new( args.merge( quantity: self ) ).tap { |u| ( units << u ).uniq! }
255
+ end
256
+
257
+ # Constructor of a new standard unit (replacing the current @standard_unit).
258
+ # For standard units, amount is implicitly 1. So :amount name argument, when
259
+ # supplied, has a different meaning – sets the mapping of its quantity.
260
+ #
261
+ def new_standard_unit args={}
262
+ explain_amount_of_standard_units if args[:amount].is_a? Numeric # n00b help
263
+ # For standard units, amount has special meaning of setting up mapping.
264
+ args.may_have( :mapping, syn!: :amount )
265
+ ᴍ = args.delete( :mapping )
266
+ set_mapping( ᴍ.amount ) if ᴍ
267
+ args.update amount: 1 # substitute amount 1 as required for standard units
268
+ # Replace @standard_unit with the newly constructed unit.
269
+ Unit().instance_variable_set( :@standard,
270
+ unit( args )
271
+ .tap { |u| ( units.unshift u ).uniq! } )
272
+ end
273
+
274
+ # Quantity multiplication.
275
+ #
276
+ def * q2
277
+ puts "#{self.name} * #{q2.name}" if SY::DEBUG
278
+ rel = [ self, q2 ].any? &:relative
279
+ ( SY::Composition[ self => 1 ] + SY::Composition[ q2 => 1 ] )
280
+ .to_quantity relative: rel
281
+ end
282
+
283
+ # Quantity division.
284
+ #
285
+ def / q2
286
+ puts "#{self.name} / #{q2.name}" if SY::DEBUG
287
+ rel = [ self, q2 ].any? &:relative?
288
+ ( SY::Composition[ self => 1 ] - SY::Composition[ q2 => 1 ] )
289
+ .to_quantity relative: rel
290
+ end
291
+
292
+ # Quantity raising to a number.
293
+ #
294
+ def ** num
295
+ puts "#{self.name} ** #{num}" if SY::DEBUG
296
+ SY::Composition[ self => num ].to_quantity relative: relative?
297
+ end
298
+
299
+ # Is the quantity dimensionless?
300
+ #
301
+ def dimensionless?
302
+ dimension.zero?
303
+ end
304
+
305
+ # Make the quantity standard for its dimension.
306
+ #
307
+ def standard!
308
+ SY::Dimension.standard_quantities[ dimension ] = self
309
+ end
310
+
311
+ # Returns the standard quantity for this quantity's dimension.
312
+ #
313
+ def standard
314
+ dimension.standard_quantity
315
+ end
316
+
317
+ # Is the dimension standard?
318
+ #
319
+ def standard?
320
+ self == standard
321
+ end
322
+
323
+ # Is the dimension or its colleague standard?
324
+ #
325
+ def standardish?
326
+ standard? || colleague.standard?
327
+ end
328
+
329
+ # A string briefly describing the quantity.
330
+ #
331
+ def to_s
332
+ name.nil? ? "[#{dimension}]" : name.to_s
333
+ end
334
+
335
+ # Inspect string.
336
+ #
337
+ def inspect
338
+ "#<Quantity: #{to_s} >"
339
+ end
340
+
341
+ def coerce other
342
+ case other
343
+ when Numeric then
344
+ return SY::Amount.relative, self
345
+ when SY::Quantity then
346
+ # By default, coercion between quantities doesn't exist. The basic
347
+ # purpose of having quantities is to avoid mutual mixing of
348
+ # incompatible magnitudes, as in "one cannot sum pears with apples".
349
+ #
350
+ if other == self then
351
+ return other, self
352
+ else
353
+ raise SY::QuantityError, "#{other} and #{self} do not mix!"
354
+ end
355
+ else
356
+ raise TErr, "#{self} cannot be coerced into a #{other.class}!"
357
+ end
358
+ end
359
+
360
+ protected
361
+
362
+ # Main parametrized (ie. quantity-specific) module for magnitudes.
363
+ #
364
+ def MagnitudeModule
365
+ @MagnitudeModule ||= if absolute? then
366
+ Module.new { include SY::Magnitude }
367
+ else
368
+ absolute.MagnitudeModule
369
+ end
370
+ end
371
+
372
+ # Parametrized magnitude class.
373
+ #
374
+ def Magnitude
375
+ if @Magnitude then @Magnitude else
376
+ mmod = MagnitudeModule()
377
+ mixin = relative? ? SY::SignedMagnitude : SY::AbsoluteMagnitude
378
+ qnt_ɴ_λ = -> { name ? "#{name}@%s" : "#<Quantity:#{object_id}@%s>" }
379
+
380
+ @Magnitude = Class.new do
381
+ include mmod
382
+ include mixin
383
+
384
+ singleton_class.class_exec do
385
+ define_method :zero do # Costructor of zero magnitudes
386
+ new amount: 0
387
+ end
388
+
389
+ define_method :to_s do # Customized #to_s. It must be a proc,
390
+ qnt_ɴ_λ.call % "Magnitude" # since the quantity owning @Magnitude
391
+ end # might not be named yet as of now.
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ # Parametrized unit class.
398
+ #
399
+ def Unit
400
+ @Unit ||= if relative? then absolute.Unit else
401
+ qnt = self
402
+ ɴλ = -> { name ? "#{name}@%s" : "#<Quantity:#{object_id}@%s>" }
403
+
404
+ Class.new Magnitude() do # Unit class.
405
+ include SY::Unit
406
+
407
+ singleton_class.class_exec do
408
+ define_method :standard do |args={}| # Customized #standard.
409
+ @standard ||= new args.merge( quantity: qnt )
410
+ end
411
+
412
+ define_method :to_s do # Customized #to_s. (Same consideration
413
+ ɴλ.call % "Unit" # as for @Magnitude applies.)
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end
419
+
420
+ private
421
+
422
+ def construct_colleague
423
+ puts "#{self}#construct_colleague" if SY::DEBUG
424
+ ɴ = name
425
+ ʀsuffix = SY::Quantity::RELATIVE_QUANTITY_NAME_SUFFIX
426
+ rel = relative?
427
+ puts "#{self} is #{rel ? 'relative' : 'absolute'}" if SY::DEBUG
428
+ # Here, it is impossible to rely on Composition::QUANTITY_TABLE –
429
+ # on the contrary, the table relies on Quantity#colleague.
430
+ constr_ɴ = ->( ɴ, ʀ ) { ç.new composition: composition, ɴ: ɴ, relative: ʀ }
431
+ constr_anon = ->( ʀ ) { ç.new composition: composition, relative: ʀ }
432
+ # enough of preliminaries
433
+ if not rel then
434
+ inst = ɴ ? constr_ɴ.( "#{ɴ}#{ʀsuffix}", true ) : constr_anon.( true )
435
+ inst.aT { relative? }
436
+ elsif ɴ.to_s.ends_with?( ʀsuffix ) && ɴ.size > ʀsuffix.size
437
+ inst = constr_ɴ.( ɴ.to_s[0..ɴ.size-ʀsuffix.size-1], false )
438
+ inst.aT { absolute? }
439
+ else inst = constr_anon.( false ).aT { absolute? } end
440
+ inst.instance_variable_set :@colleague, self
441
+ return inst
442
+ end
443
+
444
+ def same_dimension? other
445
+ dimension == other.dimension
446
+ end
447
+
448
+ def explain_amount_of_standard_units
449
+ raise TErr, "The amount of standard units is, by definition, 1. When " +
450
+ ":amount named parameter fis supplied to the construtor of a " +
451
+ "standard unit, it has different meaning: It must be given as " +
452
+ "a magnitude of another quantity of the same dimension, and it " +
453
+ "establishes relationship between this and the other quantity."
454
+ end
455
+ end # class SY::Quantity