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.
- checksums.yaml +7 -0
- data/lib/maxima.rb +22 -0
- data/lib/maxima/boolean.rb +12 -0
- data/lib/maxima/command.rb +179 -0
- data/lib/maxima/complex.rb +85 -0
- data/lib/maxima/core.rb +153 -0
- data/lib/maxima/float.rb +45 -0
- data/lib/maxima/function.rb +91 -0
- data/lib/maxima/helper.rb +63 -0
- data/lib/maxima/histogram.rb +78 -0
- data/lib/maxima/polynomial.rb +50 -0
- data/lib/maxima/rational.rb +39 -0
- data/lib/maxima/unit.rb +124 -0
- data/lib/maxima/version.rb +3 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/maxima.rb
ADDED
@@ -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,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
|
data/lib/maxima/core.rb
ADDED
@@ -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
|
data/lib/maxima/float.rb
ADDED
@@ -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
|
data/lib/maxima/unit.rb
ADDED
@@ -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
|
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: []
|