rb_maxima 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|