rb_maxima 0.1.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.
@@ -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: []