ragni-cas 0.1.7 → 0.1.8

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/op.rb CHANGED
@@ -11,6 +11,14 @@ module CAS
11
11
  class Op
12
12
  attr_reader :x
13
13
 
14
+ # Initialize a new empty operation container. This is a virtual
15
+ # class and the other must inherit from this basic container.
16
+ # Some methods raise a `CAS::CASError` if called.
17
+ # The input element is a Numric, to create a constant.
18
+ # `CAS::Op` specifies operations with a single variable
19
+ #
20
+ # <- `Numeric` to be converted in `CAS::Constant` or `CAS::Op` child operation
21
+ # -> `CAS::Op` instance
14
22
  def initialize(x)
15
23
  if x.is_a? Numeric
16
24
  case x
@@ -22,61 +30,149 @@ module CAS
22
30
  x = CAS.const x
23
31
  end
24
32
  end
33
+ CAS::Help.assert(x, CAS::Op)
34
+
25
35
  @x = x
26
36
  end
27
37
 
38
+ # Return the dependencies of the operation. Requires a `CAS::Variable`
39
+ # and it is one of the recursve method (implicit tree resolution)
40
+ #
41
+ # <- `CAS::Variable` instance
42
+ # -> `TrueClass` if depends, `FalseClass` if not
28
43
  def depend?(v)
44
+ CAS::Help.assert(v, CAS::Variable)
45
+
29
46
  @x.depend? v
30
47
  end
31
48
 
49
+ # Return the derivative of the operation using the chain rule
50
+ # The input is a `CAS::Op` because it can handle derivatives
51
+ # with respect to functions. E.g.:
52
+ #
53
+ # ```
54
+ # f(x) = (ln(x))**2
55
+ # g(x) = ln(x)
56
+ #
57
+ # d f(x)
58
+ # ------ = 2 ln(x)
59
+ # d g(x)
60
+ # ```
61
+ #
62
+ # <- `CAS::Op` object of the derivative
63
+ # -> `CAS::Op` a derivated object, or `CAS::Zero` for constants
32
64
  def diff(v)
65
+ CAS::Help.assert(v, CAS::Op)
66
+
33
67
  if @x.depend? v
34
68
  return @x.diff(v)
35
69
  end
36
70
  CAS::Zero
37
71
  end
38
72
 
73
+ # Call resolves the operation tree in a `Numeric` (if `Fixnum`)
74
+ # or `Float` depends upon promotions).
75
+ # As input, it requires an hash with `CAS::Variable` or `CAS::Variable#name`
76
+ # as keys, and a `Numeric` as a value
77
+ #
78
+ # ``` ruby
79
+ # x, y = CAS::vars :x, :y
80
+ # f = (x ** 2) + (y ** 2)
81
+ # f.call({x => 1, y => 2})
82
+ # # => 2
83
+ # ```
84
+ # <- `Hash` with feed dictionary
85
+ # -> `Numeric`
39
86
  def call(f)
87
+ CAS::Help.assert(f, Hash)
88
+
40
89
  @x.call(f)
41
90
  end
42
91
 
92
+ # Perform substitution of a part of the graph using a data table:
93
+ #
94
+ # ``` ruby
95
+ # x, y = CAS::vars :x, :y
96
+ # f = (x ** 2) + (y ** 2)
97
+ # puts f
98
+ # # => (x^2) + (y^2)
99
+ # puts f.subs({x => CAS::ln(y)})
100
+ # # => (ln(y)^2) + (y^2)
101
+ # ```
102
+ #
103
+ # <- `Hash` with substitution table
104
+ # -> `CAS::Op` (`self`) with substitution performed
43
105
  def subs(dt)
44
- @x == dt[@x] if dt.keys.include? @x
106
+ CAS::Help.assert(dt, Hash)
107
+
108
+ @x == dt[@x].dup if dt.keys.include? @x
45
109
  return self
46
110
  end
47
111
 
112
+ # Convert expression to string
113
+ #
114
+ # -> `String` to print on screen
48
115
  def to_s
49
116
  "#{@x}"
50
117
  end
51
118
 
119
+ # Convert expression to code (internal, for `CAS::Op#to_proc` method)
120
+ #
121
+ # -> `String` that represent Ruby code to be parsed in `CAS::Op#to_proc`
52
122
  def to_code
53
123
  "#{@x}"
54
124
  end
55
125
 
126
+ # Returns a sum of two `CAS::Op`s
127
+ #
128
+ # <- `CAS::Op` tree
129
+ # -> `CAS::Op` new object
56
130
  def +(op)
57
131
  CAS::Sum.new self, op
58
132
  end
59
133
 
134
+ # Returns a difference of two `CAS::Op`s
135
+ #
136
+ # <- `CAS::Op` tree
137
+ # -> `CAS::Op` new object
60
138
  def -(op)
61
139
  CAS::Diff.new self, op
62
140
  end
63
141
 
142
+ # Returns a product of two `CAS::Op`s
143
+ #
144
+ # <- `CAS::Op` tree
145
+ # -> `CAS::Op` new object
64
146
  def *(op)
65
147
  CAS::Prod.new self, op
66
148
  end
67
149
 
150
+ # Returns a division of two `CAS::Op`s
151
+ #
152
+ # <- `CAS::Op` tree
153
+ # -> `CAS::Op` new object
68
154
  def /(op)
69
155
  CAS::Div.new self, op
70
156
  end
71
157
 
158
+ # Returns the power of two `CAS::Op`s
159
+ #
160
+ # <- `CAS::Op` tree
161
+ # -> `CAS::Op` new object
72
162
  def **(op)
73
163
  CAS.pow(self, op)
74
164
  end
75
165
 
166
+ # Unary operator for inversion of a `CAS::Op`
167
+ #
168
+ # -> `CAS::Op` new object
76
169
  def -@
77
170
  CAS.invert(self)
78
171
  end
79
172
 
173
+ # Simplification callback. It simplify the subgraph of each node
174
+ # until all possible simplification are performed (thus the execution
175
+ # time is not deterministic).
80
176
  def simplify # TODO: improve this
81
177
  hash = @x.to_s
82
178
  @x = @x.simplify
@@ -86,30 +182,68 @@ module CAS
86
182
  end
87
183
  end
88
184
 
185
+ # Inspector for the current object
186
+ #
187
+ # -> `String`
89
188
  def inspect
90
189
  "#{self.class}(#{@x.inspect})"
91
190
  end
92
191
 
93
-
192
+ # Equality operator, the standard operator is overloaded
193
+ # :warning: this operates on the graph, not on the math
194
+ # See `CAS::equal`, etc.
195
+ #
196
+ # <- `CAS::Op` to be tested against
197
+ # -> `TrueClass` if equal, `FalseClass` if differs
94
198
  def ==(op)
199
+ CAS::Help.assert(op, CAS::Op)
200
+
95
201
  self.class == op.class and @x == op.x
96
202
  end
97
203
 
204
+ # Disequality operator, the standard operator is overloaded
205
+ # :warning: this operates on the graph, not on the math
206
+ # See `CAS::equal`, etc.
207
+ #
208
+ # <- `CAS::Op` to be tested against
209
+ # -> `FalseClass` if equal, `TrueClass` if differs
98
210
  def !=(op)
99
211
  not self.==(op)
100
212
  end
101
213
 
102
- def as_proc(bind)
214
+ # Evaluates the proc against a given context. It is like having a
215
+ # snapshot of the tree transformed in a callable object.
216
+ # Obviously **if the tree changes, the generated proc does notchanges**.
217
+ # The proc takes as input a feed dictionary in which each variable
218
+ # is identified through the `CAS::Variable#name` key.
219
+ #
220
+ # The proc is evaluated in the context devined by the input `Binding` object
221
+ # If `nil` is passed, the `eval` will run in this local context
222
+ #
223
+ # <- `Binding` or `NilClass` that is the context of the Ruby VM
224
+ # -> `Proc` object with a single argument as an `Hash`
225
+ def as_proc(bind=nil)
103
226
  args_ext = self.args.map { |e| "#{e} = fd[\"#{e}\"];" }
104
227
  code = "Proc.new do |fd|; #{args_ext.join " "} #{self.to_code}; end"
105
- puts code
106
- bind.eval(code)
228
+ if bind
229
+ CAS::Help.assert(bind, Binding)
230
+ bind.eval(code)
231
+ else
232
+ eval(code)
233
+ end
107
234
  end
108
235
 
236
+ # Returns a list of all `CAS::Variable`s of the current tree
237
+ #
238
+ # -> `Array` of `CAS::Variable`s
109
239
  def args
110
240
  @x.args.uniq
111
241
  end
112
242
 
243
+ # Return the local Graphviz node of the tree
244
+ #
245
+ # <- `?` unused variable (TODO: to be removed)
246
+ # -> `String` of local Graphiz node
113
247
  def dot_graph(node)
114
248
  cls = "#{self.class.to_s.gsub("CAS::", "")}_#{self.object_id}"
115
249
  "#{cls} -> #{@x.dot_graph node}\n"
@@ -124,6 +258,15 @@ module CAS
124
258
  class BinaryOp < CAS::Op
125
259
  attr_reader :x, :y
126
260
 
261
+ # The binary operator inherits from the `CAS::Op`, even
262
+ # if it is defined as a node with two possible branches. This
263
+ # is particular of the basic operations. The two basic nodes
264
+ # shares the **same** interface, so all the operations do not
265
+ # need to know which kind of node they are handling.
266
+ #
267
+ # <- `CAS::Op` left argument of the node or `Numeric` to be converted in `CAS::Constant`
268
+ # <- `CAS::Op` right argument of the node or `Numeric` to be converted in `CAS::Constant`
269
+ # -> `CAS::BinaryOp` instance
127
270
  def initialize(x, y)
128
271
  if x.is_a? Numeric
129
272
  case x
@@ -145,50 +288,100 @@ module CAS
145
288
  y = CAS.const y
146
289
  end
147
290
  end
291
+ CAS::Help.assert(x, CAS::Op)
292
+ CAS::Help.assert(y, CAS::Op)
293
+
148
294
  @x = x
149
295
  @y = y
150
296
  end
151
297
 
298
+ # Same as `CAS::Op#depend?`
152
299
  def depend?(v)
300
+ CAS::Help.assert(v, CAS::Op)
301
+
153
302
  @x.depend? v or @y.depend? v
154
303
  end
155
304
 
305
+ # This method returns an array with the derivatives of the two branches
306
+ # of the node. This method is usually called by child classes, and it is not
307
+ # intended to be used directly.
308
+ #
309
+ # <- `CAS::Op` operation to differentiate against
310
+ # -> `Array` of differentiated branches ([0] for left, [1] for right)
156
311
  def diff(v)
312
+ CAS::Help.assert(v, CAS::Op)
313
+ left, right = CAS::Zero, CAS::Zero
314
+
157
315
  left = @x.diff(v) if @x.depend? v
158
316
  right = @y.diff(v) if @y.depend? v
317
+
159
318
  return left, right
160
319
  end
161
320
 
321
+ # Substituitions for both branches of the graph, same as `CAS::Op#subs`
322
+ #
323
+ # <- `Hash` of substitutions
324
+ # -> `CAS::BinaryOp`, in practice `self`
162
325
  def subs(dt)
163
- @x = dt[@x] if dt.keys.include? @x
164
- @y = dt[@y] if dt.keys.include? @y
326
+ CAS::Help.assert(dt, Hash)
327
+
328
+ @x = dt[@x].dup if dt.keys.include? @x
329
+ @y = dt[@y].dup if dt.keys.include? @y
165
330
  return self
166
331
  end
167
332
 
168
- def call
333
+ # Same `CAS::Op#call`
334
+ #
335
+ # <- `Hash` of values
336
+ # -> `Numeric` for result
337
+ def call(fd)
169
338
  raise CASError, "Not Implemented. This is a virtual method"
170
339
  end
171
340
 
341
+ # String representation of the tree
342
+ #
343
+ # -> `String`
172
344
  def to_s
173
345
  raise CASError, "Not Implemented. This is a virtual method"
174
346
  end
175
347
 
348
+ # Code to be used in `CAS::BinaryOp#to_proc`
349
+ #
350
+ # -> `String`
176
351
  def to_code
177
352
  raise CASError, "Not implemented. This is a virtual method"
178
353
  end
179
354
 
355
+ # Returns an array of all the variables that are in the graph
356
+ #
357
+ # -> `Array` of `CAS::Variable`s
180
358
  def args
181
359
  (@x.args + @y.args).uniq
182
360
  end
183
361
 
362
+ # Inspector
363
+ #
364
+ # -> `String`
184
365
  def inspect
185
366
  "#{self.class}(#{@x.inspect}, #{@y.inspect})"
186
367
  end
187
368
 
369
+ # Comparison with other `CAS::Op`. This is **not** a math operation.
370
+ #
371
+ # <- `CAS::Op` to be compared against
372
+ # -> `TrueClass` if equal, `FalseClass` if different
188
373
  def ==(op)
189
- self.class == op.class and @x == op.x and @y == op.y
374
+ CAS::Help.assert(op, CAS::Op)
375
+ if op.is_a? CAS::BinaryOp
376
+ return (self.class == op.class and @x == op.x and @y == op.y)
377
+ else
378
+ return false
379
+ end
190
380
  end
191
381
 
382
+ # Executes simplifications of the two branches of the graph
383
+ #
384
+ # -> `CAS::BinaryOp` as `self`
192
385
  def simplify # TODO: improve this
193
386
  hash = @x.to_s
194
387
  @x = @x.simplify
@@ -204,6 +397,10 @@ module CAS
204
397
  end
205
398
  end
206
399
 
400
+ # Returns the graphviz representation of the current node
401
+ #
402
+ # <- `?` to be removed
403
+ # -> `String`
207
404
  def dot_graph(node)
208
405
  cls = "#{self.class.to_s.gsub("CAS::", "")}_#{self.object_id}"
209
406
  "#{cls} -> #{@x.dot_graph node}\n #{cls} -> #{@y.dot_graph node}"
data/lib/ragni-cas.rb CHANGED
@@ -1,13 +1,45 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # ragni-cas
4
+ # A very simple CAS engine with encapsuled graph
5
+ # representation. This will make impossible to
6
+ # perform complex high level simplifications, but
7
+ # it is powerful enough to define simple algorithm
8
+ # in a symbolic way.
9
+ #
10
+ # Mathematically, this is an implementation of the
11
+ # forward chain rule for automatic differentiaition.
12
+ # Each function is a container of function and the
13
+ # derivation is in the form:
14
+ #
15
+ # ```
16
+ # d(f(g(x))
17
+ # --------- = g'(x) * f'(g(x))
18
+ # dx
19
+ # ```
20
+ #
21
+ # Author:: Matteo Ragni (mailto:info@ragni.me)
22
+ # Copyright:: Copyright (c) 2016 Matteo Ragni
23
+ # License:: Distributed under MIT license term
24
+
3
25
  require_relative 'op.rb'
4
26
  require_relative 'numbers.rb'
5
27
  require_relative 'fnc-base.rb'
6
28
  require_relative 'fnc-trig.rb'
7
29
  require_relative 'fnc-trsc.rb'
30
+ require_relative 'fnc-branch.rb'
8
31
 
9
32
  module CAS
33
+
34
+ # Return a string representation of the graph that is
35
+ # a Graphviz tree. Requires a `CAS::Op` as argument.
36
+ # In the next releases probably it will be moved inside
37
+ # `CAS::Op`.
38
+ # <- `CAS::Op` instance
39
+ # -> `String`
10
40
  def self.to_dot(op)
41
+ CAS::Help.assert(op, CAS::Op)
42
+
11
43
  node = {}
12
44
  string = op.dot_graph(node)
13
45
  labels = ""
@@ -64,7 +96,25 @@ digraph Op {
64
96
  EOG
65
97
  end
66
98
 
99
+ # Export the input `CAS::Op` graphviz representation to a file.
100
+ # <- `String` with filename
101
+ # <- `CAS::Op` with the tree
102
+ # -> `CAS::Op` in input
67
103
  def self.export_dot(fl, op)
104
+ CAS::Help.assert(fl, String)
105
+ CAS::Help.assert(op, CAS::Op)
106
+
68
107
  File.open(fl, "w") do |f| f.puts CAS.to_dot(op) end
108
+ return op
109
+ end
110
+
111
+
112
+ # Support functions are in this separate Helper class
113
+ module Help
114
+ # Check input `obj.class` against a `type` class
115
+ # raises an ArgumentError if check fails
116
+ def self.assert(obj, type)
117
+ raise ArgumentError, "required #{type}, received #{obj.class}" unless obj.is_a? type
118
+ end
69
119
  end
70
120
  end
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
  #!/usr/bin/env ruby
3
3
 
4
4
  module CAS
5
- VERSION = [0, 1, 7]
5
+ VERSION = [0, 1, 8]
6
6
  end
7
7
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ragni-cas
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matteo Ragni
@@ -29,7 +29,7 @@ cert_chain:
29
29
  XorZtzkkLImvKFj35xKLFfVkv0Vd8tGQoiL8vdmQNJjAjtE+C+Y7OI4dpiZPKO4G
30
30
  R/8JOvUuk9jPbyLxjQH/sFaFqqYGX2xo1zk2CRy/A0WhJrSaXVw1r5lEi7b0W5gg
31
31
  -----END CERTIFICATE-----
32
- date: 2016-07-22 00:00:00.000000000 Z
32
+ date: 2016-07-25 00:00:00.000000000 Z
33
33
  dependencies: []
34
34
  description:
35
35
  email: info@ragni.me
@@ -38,6 +38,7 @@ extensions: []
38
38
  extra_rdoc_files: []
39
39
  files:
40
40
  - lib/fnc-base.rb
41
+ - lib/fnc-branch.rb
41
42
  - lib/fnc-trig.rb
42
43
  - lib/fnc-trsc.rb
43
44
  - lib/numbers.rb
metadata.gz.sig CHANGED
Binary file