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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -2
- data/lib/fnc-base.rb +133 -12
- data/lib/fnc-branch.rb +198 -0
- data/lib/fnc-trig.rb +44 -8
- data/lib/fnc-trsc.rb +12 -0
- data/lib/numbers.rb +91 -15
- data/lib/op.rb +206 -9
- data/lib/ragni-cas.rb +50 -0
- data/lib/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
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
|
-
|
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
|
-
|
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
|
-
|
106
|
-
|
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
|
-
|
164
|
-
|
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
|
-
|
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
|
-
|
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
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.
|
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-
|
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
|