sy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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