rb_maxima 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c2001c84271ebb26844cf1217d8e5ae5c7844987c65d4fe14723700996e45d6a
4
+ data.tar.gz: 579008bc8d879af91514e27597a6887c9cbcb9aebfac62091b4354ce0e304b0c
5
+ SHA512:
6
+ metadata.gz: 4bee94b71cc6bb0e107c2f0408d88a6fada488868cdf4c25d005f5275f4ef212e1a75ea8212fea9a301daa703c0fb8849b0e9189a4a4e97cd7baadf702cae969
7
+ data.tar.gz: 46afece761a21a62d9d7d018317d2ca74470fe2069028c7af4d767ae2ced93a56ce2dd90da91c2a5c48fd3d0c7dbc830aa6bbe958fbe6d2f9b87df2d66036be2
@@ -0,0 +1,22 @@
1
+ require "securerandom"
2
+ require "set"
3
+ require "csv"
4
+ require "numo/gnuplot"
5
+
6
+ require "maxima/version"
7
+ require "maxima/core"
8
+
9
+ require "maxima/command"
10
+
11
+ require "maxima/unit"
12
+ require "maxima/float"
13
+ require "maxima/rational"
14
+ require "maxima/complex"
15
+ require "maxima/boolean"
16
+
17
+ require "maxima/function"
18
+
19
+ require "maxima/histogram"
20
+ require "maxima/polynomial"
21
+
22
+ require "maxima/helper"
@@ -0,0 +1,12 @@
1
+ module Maxima
2
+ class Boolean
3
+
4
+ TRUE_REGEX = /\s*true\s*/i
5
+
6
+ def self.parse(string)
7
+ !!(TRUE_REGEX =~ string)
8
+ rescue
9
+ nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,179 @@
1
+ module Maxima
2
+ class Command
3
+ attr_accessor :dependencies, :commands, :assigned_variables, :options
4
+
5
+ def initialize
6
+ @dependencies = []
7
+ @assigned_variables = Set.new()
8
+ @commands = []
9
+ @options = {}
10
+ end
11
+
12
+ def self.output(*v)
13
+ Command.new()
14
+ .with_options(
15
+ use_fast_arrays: true,
16
+ float: true,
17
+ ).tap do |c|
18
+ yield c
19
+ end.output_variables(*v)
20
+ end
21
+
22
+ def with_options(options)
23
+ self.tap { @options.merge!(options) }
24
+ end
25
+
26
+ def add_variable(variable)
27
+ case variable
28
+ when Enumerable
29
+ @assigned_variables.merge(variable)
30
+ else
31
+ @assigned_variables.add(variable)
32
+ end
33
+ end
34
+
35
+ def let(variable_or_variables, expression, *unary_operations, **unary_operations_options)
36
+ add_variable(variable_or_variables)
37
+ expression = _apply_unary_operations(expression, *unary_operations, **unary_operations_options)
38
+
39
+ variable = Maxima.mformat(variable_or_variables)
40
+ expression = Maxima.mformat(expression)
41
+
42
+ _let(variable, expression)
43
+ end
44
+
45
+ def let_simplified(variable, expression, *unary_operations, **unary_operations_options)
46
+ unary_operations_options[:expand] = true
47
+
48
+ let(
49
+ variable,
50
+ expression,
51
+ *unary_operations,
52
+ **unary_operations_options
53
+ )
54
+ end
55
+
56
+ def _let(variable, expression)
57
+ @commands << "#{variable} : #{expression}"
58
+ end
59
+
60
+ def <<(expression)
61
+ @commands << expression
62
+ end
63
+
64
+ def _apply_unary_operations(expression, *unary_operations, **unary_operations_options)
65
+ unary_operations = Set.new(unary_operations)
66
+ unary_operations_options.map do |option, is_enabled|
67
+ unary_operations.add(option) if is_enabled
68
+ end
69
+
70
+ [
71
+ unary_operations.map { |unary_operation| "#{unary_operation}(" },
72
+ expression,
73
+ ")" * unary_operations.count
74
+ ].join()
75
+ end
76
+
77
+ OPTIONS = {
78
+ float: -> (enabled) { "float: #{enabled}" },
79
+ use_fast_arrays: -> (enabled) { "use_fast_arrays: #{enabled}" }
80
+ }
81
+
82
+ def options_commands()
83
+ [].each do |commands|
84
+ @options.each do |option, configuration|
85
+ next unless OPTIONS[option]
86
+ commands << OPTIONS[option].call(configuration)
87
+ end
88
+ end
89
+ end
90
+
91
+ def run_shell(extract_variables = nil, debug: ENV["DEBUG"])
92
+ inputs = [*dependencies_input, *options_commands(), *@commands]
93
+
94
+ inputs << "grind(#{extract_variables.join(', ')})" if extract_variables
95
+ input = inputs.join("$\n") + "$\n"
96
+
97
+ output = with_debug(debug, input) do
98
+ Helper.spawn_silenced_shell_process("maxima --quiet --run-string '#{input}'")
99
+ end
100
+
101
+ {
102
+ input: input,
103
+ output: output
104
+ }
105
+ end
106
+
107
+ def with_debug(debug, input)
108
+ return yield unless debug
109
+
110
+ uuid = SecureRandom.uuid[0..6]
111
+ puts input.lines.map { |s| "#{uuid}>>>\t#{s}" }.join
112
+
113
+ yield.tap do |output|
114
+ puts output.lines.map { |s| "#{uuid}<<<\t#{s}" }.join
115
+ end
116
+ end
117
+
118
+ MATCH_REGEX = -> (eol_maxima_characters) { /(?<=\(%i#{eol_maxima_characters}\)).*(?=\$|\Z)/m.freeze }
119
+ GSUB_REGEX = Regexp.union(/\s+/, /\(%(i|o)\d\)|done/)
120
+ def self.extract_outputs(output, eol_maxima_characters)
121
+ MATCH_REGEX.call(eol_maxima_characters)
122
+ .match(output)[0]
123
+ .gsub(GSUB_REGEX, "")
124
+ .split("$")
125
+ end
126
+
127
+ def self.convert_output_to_variables(output_variable_map, raw_output)
128
+ {}.tap do |result|
129
+ output_variable_map.each_with_index do |(variable, klazz), index|
130
+ output = raw_output[index]
131
+ output = klazz.respond_to?(:parse) ? klazz.parse(output) : klazz.new(output)
132
+ result[variable] = output
133
+ end
134
+ end
135
+ end
136
+
137
+ def self.expand_output_variable_map(output_variable_map)
138
+ {}.tap do |expanded_output_variable_map|
139
+ add_key = -> (k,v) {
140
+ if expanded_output_variable_map.has_key?(k)
141
+ throw :key_used_twice
142
+ else
143
+ expanded_output_variable_map[k] = v
144
+ end
145
+ }
146
+
147
+ output_variable_map.each do |output_key, parsed_into_class|
148
+ case output_key
149
+ when Array
150
+ output_key.each do |output_subkey|
151
+ add_key.call(output_subkey, parsed_into_class)
152
+ end
153
+ else
154
+ add_key.call(output_key, parsed_into_class)
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ def self.eol_maxima_characters(input)
161
+ input.count("$") + input.count(";")
162
+ end
163
+
164
+ def output_variables(output_variable_map)
165
+ output_variable_map = Command.expand_output_variable_map(output_variable_map)
166
+
167
+ input, output = run_shell(output_variable_map.keys).values_at(:input, :output)
168
+
169
+ eol_maxima_characters = Command.eol_maxima_characters(input)
170
+ extracted_outputs = Command.extract_outputs(output, eol_maxima_characters)
171
+
172
+ Command.convert_output_to_variables(output_variable_map, extracted_outputs)
173
+ end
174
+
175
+ def dependencies_input
176
+ @dependencies.map { |s| "load(#{s})" }
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,85 @@
1
+ module Maxima
2
+
3
+ def self.Complex(real, imaginary)
4
+ Complex.new(real, imaginary)
5
+ end
6
+
7
+ class Complex < Unit
8
+ attr_accessor :real, :imaginary
9
+
10
+ def initialize(real, imaginary, **options)
11
+ super(**options)
12
+ @real = real
13
+ @imaginary = imaginary
14
+ end
15
+
16
+ WHITESPACE_OR_PARENTHESES_REGEX = /(\s|\(|\))/
17
+ COMPLEX_REGEX = /(-?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?)((?:\*)?(?:%)?i)?/
18
+
19
+ def self.parse(maxima_output)
20
+ maxima_output = maxima_output.to_s unless maxima_output.is_a?(String)
21
+ string = maxima_output.gsub(WHITESPACE_OR_PARENTHESES_REGEX, "")
22
+
23
+ real = 0
24
+ imaginary = 0
25
+
26
+ string.scan(COMPLEX_REGEX) do |(float, is_imaginary)|
27
+ if is_imaginary
28
+ imaginary += float.to_f
29
+ else
30
+ real += float.to_f
31
+ end
32
+ end
33
+
34
+ if imaginary == 0
35
+ Float.new(real, maxima_output: maxima_output)
36
+ else
37
+ Complex.new(real, imaginary, maxima_output: maxima_output)
38
+ end
39
+ end
40
+
41
+ def pretty_to_s
42
+ if real == 0
43
+ "#{@imaginary}i"
44
+ else
45
+ operand = @real.positive? ? '+' : '-'
46
+ "#{@imaginary}i #{operand} #{@real.abs}"
47
+ end
48
+ end
49
+
50
+ def to_maxima_input
51
+ return "#{@imaginary} * %i" if real == 0
52
+
53
+ operand = @real.positive? ? '+' : '-'
54
+ "(#{@imaginary} * %i #{operand} #{@real.abs})"
55
+ end
56
+
57
+ def ==(other)
58
+ @real == other.real && @imaginary == other.imaginary
59
+ end
60
+
61
+ # Definitions are somewhat contrived and not per se mathematically accurate.
62
+
63
+ def positive?
64
+ !negative?
65
+ end
66
+
67
+ # At least one scalar must be negative & the others non positive
68
+ def negative?
69
+ (@real < 0 && @imaginary <= 0) ||
70
+ (@imaginary < 0 && @real <= 0)
71
+ end
72
+
73
+ def zero?
74
+ @real == 0 && @imaginary == 0
75
+ end
76
+
77
+ def imaginary?
78
+ @imaginary != 0
79
+ end
80
+
81
+ def real?
82
+ @imaginary == 0
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,153 @@
1
+ module Maxima
2
+ def self.bin_op(e_1, e_2, bin_op)
3
+ Maxima::Function.new("((#{Maxima.mformat(e_1)}) #{bin_op} (#{Maxima.mformat(e_2)}))")
4
+ end
5
+
6
+ def self.cobyla(minimize_function, variables, constraint_function, initial_guess)
7
+ Command.output(variables => Unit) do |c|
8
+ initial_guess = mformat(initial_guess)
9
+
10
+ c.dependencies << "cobyla"
11
+
12
+ c.let :output, "fmin_cobyla(#{minimize_function}, #{mformat(variables)}, #{initial_guess},constraints = #{constraint_function})"
13
+ c.let variables, "sublis(output[1], #{variables})"
14
+ end
15
+ end
16
+
17
+ def self.interpolate(array)
18
+ Command.output(lagrangian: Function) do |c|
19
+ c.dependencies << "interpol"
20
+ c.let :array, array.to_a
21
+ c.let_simplified :lagrangian, "lagrange(array)"
22
+ end
23
+ end
24
+
25
+ def self.integrate(function, t0 = nil, t1 = nil, v: "x")
26
+ expression = (t0 && t1) ? "integrate(function, #{v}, #{t0}, #{t1})" : "integrate(function, #{v})"
27
+
28
+ Command.output(integral: Function) do |c|
29
+ c.let :function, function
30
+ c.let :integral, expression
31
+ end
32
+ end
33
+
34
+ def self.diff(function, v: "x")
35
+ Command.output(diff: Function) do |c|
36
+ c.let :function, function
37
+ c.let :diff, "derivative(function, #{v})"
38
+ end
39
+ end
40
+
41
+ def self.plot(*maxima_objects, from_x: nil, from_y: nil)
42
+ maxima_objects << [[from_x.min, 0], [from_x.max, 0]] if from_x
43
+ maxima_objects << [[0, from_y.min], [0, to_y.max]] if from_y
44
+
45
+ maxima_objects = maxima_objects.map do |k|
46
+ if k.respond_to?(:to_gnu_plot)
47
+ k.to_gnu_plot
48
+ elsif k.is_a?(Array) && !k.first.is_a?(String)
49
+ [*k.transpose, w: "points"]
50
+ else
51
+ k
52
+ end
53
+ end
54
+
55
+ Helper.stfu do
56
+ Numo.gnuplot do |c|
57
+ c.debug_on
58
+ c.set title: "Maxima Plot"
59
+ c.plot(*maxima_objects)
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.lsquares_estimation(points, variables, equation, outputs, equation_for_mse: equation)
65
+ Command.output(outputs => Complex, :mse => Float) do |c|
66
+ formatted_points = points.map { |a| mformat(a) }.join(",")
67
+ formatted_variables = mformat(variables)
68
+ formatted_outputs = mformat(outputs)
69
+
70
+ c.dependencies << "lsquares"
71
+ c.let :M, "matrix(#{formatted_points})"
72
+ c.let :lsquares_estimation, "lsquares_estimates(M, #{formatted_variables}, #{equation}, #{formatted_outputs})"
73
+ if outputs.count == 1
74
+ c << "map (lhs, lsquares_estimation) :: map (rhs, lsquares_estimation)"
75
+ else
76
+ c << "map (lhs, lsquares_estimation[1]) :: map (rhs, lsquares_estimation[1])"
77
+ end
78
+ c.let :mse, "lsquares_residual_mse(M, #{formatted_variables}, #{equation_for_mse}, first (lsquares_estimation))"
79
+ end
80
+ end
81
+
82
+ def self.equivalence(expression_one, expression_two)
83
+ Command.output(:is_equal => Boolean) do |c|
84
+ formatted_expression_one = mformat(expression_one)
85
+ formatted_expression_two = mformat(expression_two)
86
+
87
+ c.let :is_equal, "is(equal(#{formatted_expression_one}, #{formatted_expression_two}))"
88
+ end
89
+ end
90
+
91
+ def self.lagrangian(minimize_function, variables, constraint_function, initial_guess, iterations: 5)
92
+ Command.output(variables => Unit) do |c|
93
+ initial_guess = mformat(initial_guess)
94
+ constraint_function = mformat(Array(constraint_function))
95
+ optional_args = mformat(niter: iterations)
96
+
97
+ c.dependencies << "lbfgs"
98
+ c.dependencies << "augmented_lagrangian"
99
+
100
+ c.let :output, "augmented_lagrangian_method(#{minimize_function}, #{variables}, #{constraint_function}, #{initial_guess}, #{optional_args})"
101
+ c.let variables, "sublis(output[1], #{variables})"
102
+ end
103
+ end
104
+
105
+ def self.mformat(variable)
106
+ case variable
107
+ when String, Symbol
108
+ variable # the only truly `valid` input is a string
109
+ when Hash
110
+ variable.map do |key,value|
111
+ "#{mformat(key)} = #{mformat(value)}"
112
+ end.join(", ")
113
+ when Array
114
+ "[" + variable.map { |v| mformat(v) }.join(",") + "]"
115
+ when ::Complex
116
+ mformat Complex.new(variable, variable.real, variable.imag)
117
+ when Numeric
118
+ mformat Float(variable)
119
+ when Complex, Float, Function
120
+ variable.to_maxima_input
121
+ when nil
122
+ throw :cannot_format_nil
123
+ else
124
+ variable
125
+ end
126
+ end
127
+
128
+ def self.solve_polynomial(equations, variables_to_solve_for, ignore: nil, real_only: false)
129
+ # regex match extract
130
+ output = Command
131
+ .with_options(real_only: real_only)
132
+ .output(output: Unit) do |c|
133
+ variables = mformat(Array(variables_to_solve_for))
134
+ equations = mformat(Array(equations))
135
+
136
+ c.let :output, "algsys(#{equations},#{variables})"
137
+ end
138
+
139
+ output = output[:output]
140
+
141
+ variables_to_solve_for -= Array(ignore)
142
+
143
+ variable_regexes = variables_to_solve_for.map do |variable|
144
+ "#{variable}=(-?.*?)(?:\,|\\])"
145
+ end
146
+
147
+ regex = Regexp.new(variable_regexes.reduce(&:+))
148
+
149
+ output = output.to_s.gsub(" ", "")
150
+
151
+ output.scan(regex).map { |row| variables_to_solve_for.zip(row.map { |v| Unit.parse(v) }).to_h }
152
+ end
153
+ end
@@ -0,0 +1,45 @@
1
+ module Maxima
2
+
3
+ def self.Float(real)
4
+ Float.new(real)
5
+ end
6
+
7
+ class Float < Unit
8
+ ZERO = Float.new(0).freeze
9
+
10
+ attr_accessor :real
11
+
12
+ def initialize(real = nil, **options)
13
+ options[:maxima_output] ||= real&.to_s
14
+ super(**options)
15
+ @real = (real || @maxima_output).to_f
16
+ end
17
+
18
+ def <=>(other)
19
+ case other
20
+ when ::Float
21
+ @real <=> other
22
+ when Float
23
+ @real <=> other.real
24
+ else
25
+ -1
26
+ end
27
+ end
28
+
29
+ def to_f
30
+ @real
31
+ end
32
+
33
+ def real?
34
+ true
35
+ end
36
+
37
+ def imaginary?
38
+ false
39
+ end
40
+
41
+ def derivative(v: nil)
42
+ ZERO
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,91 @@
1
+ module Maxima
2
+ class Function < Unit
3
+ attr_accessor :string, :variables
4
+
5
+ def initialize(string, variables = nil, **options)
6
+ string = string.to_s
7
+ options[:maxima_output] ||= string
8
+ super(**options)
9
+ @variables = variables || Function.variables_in_string(string)
10
+ end
11
+
12
+ # This strategy fails for functions (cos etc.). However, that does not impact it's actual usage.
13
+ VARIABLE_REGEX = /[%|a-z|A-Z]+/.freeze
14
+ IGNORE_VARIABLES = %w(%e %i).freeze
15
+ def self.variables_in_string(string)
16
+ (string.scan(VARIABLE_REGEX) - IGNORE_VARIABLES).to_set
17
+ end
18
+
19
+ def integral(t0 = nil, t1 = nil, v: "x")
20
+ if t0 && t1
21
+ Maxima.integrate(to_maxima_input, t0, t1, v: v)[:integral]
22
+ else
23
+ Maxima.integrate(to_maxima_input, v: v)[:integral]
24
+ end
25
+ end
26
+
27
+ def definite_integral(t0, t1, v: "x")
28
+ i_v = self.integral(v: v)
29
+ i_v.at(v => t1) - i_v.at(v => t0)
30
+ end
31
+
32
+ def derivative(variable = nil, v: "x")
33
+ Maxima.diff(to_maxima_input, v: (variable || v))[:diff]
34
+ end
35
+
36
+ def between(min, max, steps)
37
+ step = (max - min).fdiv(steps)
38
+
39
+ Command.output(r: Histogram) do |c|
40
+ c.let :r, "makelist([x,float(#{self})],x, #{min}, #{max}, #{step})"
41
+ end[:r]
42
+ end
43
+
44
+ # Assume what we get is what we need
45
+ def self.parse(string)
46
+ variables = variables_in_string(string)
47
+
48
+ if variables.any?
49
+ Function.new(string, variables)
50
+ else
51
+ Unit.parse_float(string)
52
+ end
53
+ rescue
54
+ nil
55
+ end
56
+
57
+ def gnu_plot_text
58
+ super.gsub("^", "**")
59
+ end
60
+
61
+ def gnu_plot_w
62
+ "lines"
63
+ end
64
+
65
+ def to_maxima_input
66
+ self.to_s
67
+ end
68
+
69
+ def at(v)
70
+ s = self.to_s.dup
71
+
72
+ case v
73
+ when Hash
74
+ v.each do |k,t|
75
+ k = k.to_s
76
+ if @variables.include?(k)
77
+ s.gsub!(k, "(#{t})")
78
+ end
79
+ end
80
+ else
81
+ throw :must_specify_variables_in_hash if @variables.length != 1
82
+ s.gsub!(@variables.first, "(#{v})")
83
+ end
84
+ Function.parse(s).simplified
85
+ end
86
+
87
+ def ==(other)
88
+ to_s == other.to_s
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,63 @@
1
+ $interrupted = false
2
+
3
+ Signal.trap("INT") {
4
+ $interrupted = true
5
+ }
6
+
7
+ module Maxima
8
+ module Helper
9
+
10
+ def self.stfu
11
+ result = nil
12
+ begin
13
+ orig_stderr = $stderr.clone
14
+ orig_stdout = $stdout.clone
15
+ $stderr.reopen File.new('/dev/null', 'w')
16
+ $stdout.reopen File.new('/dev/null', 'w')
17
+ result = yield
18
+ rescue Exception => e
19
+ if $interrupted
20
+ throw :interrupted
21
+ else
22
+ $stdout.reopen orig_stdout
23
+ $stderr.reopen orig_stderr
24
+ raise e
25
+ end
26
+ ensure
27
+ if $interrupted
28
+ throw :interrupted
29
+ else
30
+ $stdout.reopen orig_stdout
31
+ $stderr.reopen orig_stderr
32
+ end
33
+ end
34
+ if $interrupted
35
+ throw :interrupted
36
+ else
37
+ result
38
+ end
39
+ end
40
+
41
+ def self.spawn_silenced_shell_process(shell_command)
42
+ stfu do
43
+ rout, wout = IO.pipe
44
+ result = nil
45
+ begin
46
+ pid = Process.spawn(shell_command, out: wout)
47
+ _, _ = Process.wait2(pid)
48
+ ensure
49
+ wout.close
50
+ if $interrupted
51
+ rout.close
52
+ throw :interrupted
53
+ else
54
+ result = rout.readlines.join("\n")
55
+ rout.close
56
+ end
57
+ end
58
+ result
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,78 @@
1
+ module Maxima
2
+ class Histogram < Unit
3
+ attr_accessor :points
4
+
5
+ def self.between(min, max, function = ->(x) { x }, steps = 100)
6
+ Histogram.new(
7
+ *[].tap do |points|
8
+ (min..max).step((max - min).fdiv(steps)).each do |x|
9
+ points.push([x, function.call(x)])
10
+ end
11
+ end
12
+ )
13
+ end
14
+
15
+ def polynomial_fit(degrees)
16
+ Polynomial.fit(self, degrees)[:function]
17
+ end
18
+
19
+ def self.from_csv(csv)
20
+ Histogram.new(
21
+ *CSV.read(csv).map { |array| array.map(&:to_i) }
22
+ )
23
+ end
24
+
25
+ def self.parse(s)
26
+ Histogram.new((eval s), maxima_output: s)
27
+ end
28
+
29
+ def initialize(*points, **options)
30
+ super(**options)
31
+
32
+ while points.is_a?(Array) && points.first.is_a?(Array) && points.first.first.is_a?(Array)
33
+ points = points.flatten(1)
34
+ end
35
+
36
+ unless points.is_a?(Array) && points.first.is_a?(Array) && points.first.length == 2
37
+ throw :invalid_histogram_points
38
+ end
39
+
40
+ @points = points
41
+ end
42
+
43
+ def to_percentage()
44
+ @to_percentage ||=
45
+ begin
46
+ sum = points.sum(&:x)
47
+ Histogram.new(
48
+ points.map do |point|
49
+ Point.new(
50
+ point.x,
51
+ point.y.fdiv(sum)
52
+ )
53
+ end
54
+ )
55
+ end
56
+ end
57
+
58
+ def to_a
59
+ @points
60
+ end
61
+
62
+ def integral()
63
+ begin
64
+ sum = 0
65
+ Histogram.new(
66
+ points.map do |(one, two)|
67
+ sum += two
68
+ [one, sum]
69
+ end
70
+ )
71
+ end
72
+ end
73
+
74
+ def to_gnu_plot()
75
+ [*points.map(&:to_a).transpose, w: "points"]
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ module Maxima
2
+ class Polynomial < Unit
3
+
4
+ def self.fit(histogram, degrees)
5
+ throw :degrees_must_be_zero_or_positive if degrees < 0
6
+
7
+ equation_string, variables = polynomial_equation(degrees)
8
+ results = Maxima.lsquares_estimation(histogram.to_a, [:x, :y], "y = #{equation_string}", variables)
9
+ mse = results.delete(:mse)
10
+
11
+ results.each do |variable, value|
12
+ equation_string.gsub!("(#{variable})", value.to_s)
13
+ end
14
+
15
+ {
16
+ function: Maxima::Function.new(equation_string),
17
+ mse: mse
18
+ }
19
+ end
20
+
21
+ def self.fit_function(min, max, x: "x")
22
+ Enumerator.new do |e|
23
+ (min..max).each do |degrees|
24
+ equation_string, variables = polynomial_equation(degrees, f_of: x)
25
+ e.<<(equation_string, variables)
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.polynomial_equation(degrees, f_of: "x")
31
+ constant_variables = degrees.times.map { |degree| "c#{degree}" }
32
+
33
+ free_polynomial = constant_variables.each do |degree|
34
+ case degree
35
+ when 0
36
+ "(#{constant_variable})"
37
+ when 1
38
+ "(#{constant_variable}) * #{f_of}"
39
+ else
40
+ "(#{constant_variable}) * #{f_of} ^ #{degree}"
41
+ end
42
+ end
43
+
44
+ [
45
+ free_polynomial.join(" + "),
46
+ variables
47
+ ]
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,39 @@
1
+ module Maxima
2
+ class Rational < Unit
3
+
4
+ attr_accessor :numerator, :denominator
5
+
6
+ def initialize(string, numerator, denominator, title = nil)
7
+ super(string, title)
8
+ @numerator = numerator
9
+ @denominator = denominator
10
+ end
11
+
12
+ REGEX = /(\d+)\/(\d+)/
13
+ def self.parse(input_string)
14
+ _, numerator, denominator = REGEX.match(input_string).to_a
15
+
16
+ return nil if numerator.nil? || denominator.nil?
17
+
18
+ if numerator == 0
19
+ Float.new(input_string, 0)
20
+ else
21
+ Rational.new(input_string, numerator.to_i, denominator.to_i)
22
+ end
23
+ rescue StandardError
24
+ nil
25
+ end
26
+
27
+ def real?
28
+ true
29
+ end
30
+
31
+ def imaginary?
32
+ false
33
+ end
34
+
35
+ def to_f
36
+ @to_f ||= numerator.fdiv(denominator)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,124 @@
1
+ module Maxima
2
+ class Unit
3
+ attr_accessor :maxima_output, :plot_title
4
+
5
+ def initialize(inline_maxima_output = nil, plot_title: nil, maxima_output: nil)
6
+ @maxima_output = inline_maxima_output || maxima_output
7
+ @plot_title = plot_title
8
+ end
9
+
10
+ def inspect
11
+ if plot_title.nil? || plot_title == ""
12
+ "#{self.class}(#{self})"
13
+ else
14
+ "#{self.class}[#{plot_title}](#{self})"
15
+ end
16
+ end
17
+
18
+ def self.parse(m)
19
+ Function.parse(m)
20
+ end
21
+
22
+ def self.parse_float(m)
23
+ Rational.parse(m) || Complex.parse(m)
24
+ end
25
+
26
+ def to_s
27
+ @maxima_output
28
+ end
29
+
30
+ def with_plot_title(plot_title)
31
+ self.class.new(@maxima_output, plot_title)
32
+ end
33
+
34
+ %w(* / ** + -).each do |operation|
35
+ define_method(operation) do |other|
36
+ Maxima.bin_op(self, other, operation)
37
+ end
38
+ end
39
+
40
+ # def absolute_difference(other)
41
+ # Function.new("abs(#{self - other})")
42
+ # end
43
+
44
+ def simplified
45
+ @simplified ||= through_maxima(:expand)
46
+ end
47
+
48
+ # ~~ *unary_operations, **unary_operations_options
49
+ def through_maxima(*array_options, **options)
50
+ @after_maxima ||= Command.output(itself: Unit) do |c|
51
+ c.let :itself, self.to_s, *array_options, **options
52
+ end[:itself]
53
+ end
54
+
55
+ def simplified!
56
+ simplified.to_s
57
+ end
58
+
59
+ def to_maxima_input
60
+ to_s
61
+ end
62
+
63
+ def to_gnu_plot
64
+ [gnu_plot_text, gnu_plot_options]
65
+ end
66
+
67
+ def at(_)
68
+ self
69
+ end
70
+
71
+ def to_pdf(t0, t1, v: "x")
72
+ (self / integral(t0, t1)).definite_integral(t0, v)
73
+ end
74
+
75
+ # private
76
+ def gnu_plot_text
77
+ to_s
78
+ end
79
+
80
+ def gnu_plot_options
81
+ { w: gnu_plot_w }.tap do |options|
82
+ options[:plot_title] = @plot_title if @plot_title
83
+ end
84
+ end
85
+
86
+ def gnu_plot_w
87
+ "points"
88
+ end
89
+
90
+ def to_f
91
+ throw "cannot_cast_#{self.class}_to_float"
92
+ end
93
+
94
+ def real?
95
+ throw "real_is_undecidable_for_#{self.class}"
96
+ end
97
+
98
+ def imaginary?
99
+ throw "imaginary_is_undecidable_for_#{self.class}"
100
+ end
101
+
102
+ def positive?
103
+ to_f > 0
104
+ end
105
+
106
+ def zero?
107
+ to_f == 0
108
+ end
109
+
110
+ def negative?
111
+ to_f < 0
112
+ end
113
+
114
+ # Basic string identity
115
+ def ==(other)
116
+ (self <=> other) == 0
117
+ end
118
+
119
+ # True mathematical equivalence
120
+ def ===(other)
121
+ (self == other) || Maxima.equivalence(self, other)[:is_equal]
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ module Maxima
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rb_maxima
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Ackerman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-nav
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.16'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.16'
125
+ - !ruby/object:Gem::Dependency
126
+ name: numo-gnuplot
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.2'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.2'
139
+ description: Ruby developers have, for as long as I can remember, had a disheveled
140
+ heap of scientific and mathematical libraries - many of which operate in pure ruby
141
+ code. Given a problem we either kludge together some cobbled mess or turn to Python/R/etc.
142
+ And to this I say no more! `rb_maxima` allows a ruby developer to directly leverage
143
+ the unbridled power of the open source, lisp powered, computer algebra system that
144
+ is `Maxima`!
145
+ email:
146
+ - daniel.joseph.ackerman@gmail.com
147
+ executables: []
148
+ extensions: []
149
+ extra_rdoc_files: []
150
+ files:
151
+ - lib/maxima.rb
152
+ - lib/maxima/boolean.rb
153
+ - lib/maxima/command.rb
154
+ - lib/maxima/complex.rb
155
+ - lib/maxima/core.rb
156
+ - lib/maxima/float.rb
157
+ - lib/maxima/function.rb
158
+ - lib/maxima/helper.rb
159
+ - lib/maxima/histogram.rb
160
+ - lib/maxima/polynomial.rb
161
+ - lib/maxima/rational.rb
162
+ - lib/maxima/unit.rb
163
+ - lib/maxima/version.rb
164
+ homepage:
165
+ licenses:
166
+ - MIT
167
+ metadata: {}
168
+ post_install_message:
169
+ rdoc_options: []
170
+ require_paths:
171
+ - lib
172
+ required_ruby_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ requirements: []
183
+ rubyforge_project:
184
+ rubygems_version: 2.7.3
185
+ signing_key:
186
+ specification_version: 4
187
+ summary: A gem that allows for mathematical calculations using the open source `Maxima`
188
+ library!
189
+ test_files: []