mb-math
Mathematical functions such as range-clamping, interpolation, extrapolation, etc. This is companion code to my educational video series about code and sound.
You might also be interested in mb-sound, mb-geometry, and mb-util.
This code is reasonably well-tested, but I recommend using it for non-critical tasks, not for making important decisions or for mission-critical data modeling. Most of the function implementations target 4 to 6 decimals of accuracy, which may be too low for some applications.
Quick start
Clone the repo, follow the standalone installation instructions
below, and run
bin/console
. Use Pry's ls
command to get a list of what's available, and
the show-source -d
command to see a function's documentation).
bin/console
ls
show-source -d clamp
Examples
For typing convenience, and to avoid conflicting with Ruby's top-level Math
module, everything lives under MB::M
instead of MB::Math
.
Smooth interpolation
The interp
method blends between two Numeric
s, Array
s, Hash
es, or
Numo::NArray
s.
Numbers
MB::M.interp(1, 2, 0.5)
# => 1.5
Hashes
a = { x: 0.5, y: 1.5 }
b = { x: 1.0, y: -1.0 }
MB::M.interp(a, b, 0)
# => { x: 0.5, y: 1.5 }
MB::M.interp(a, b, 1)
# => { x: 1.0, y: -1.0 }
MB::M.interp(a, b, 0.5)
# => { x: 0.75, y: 0.25 }
Smoothed interpolation
The :func
keyword argument accepts a tweening function. Anything that
responds to :call
and returns 0.0 if given 0.0 and 1.0 if given 1.0 can be
used here.
a = [-1, -1]
b = [1, 2]
steps = [0, 0.25, 0.5, 0.75, 1]
MB::M.interp(a, b, steps, func: MB::M.method(:smootherstep))
# => [[-1, -1], [-0.79296875, -0.689453125], [0.0, 0.5], [0.79296875, 1.689453125], [1, 2]]
Linear extrapolation
Note: extrapolation doesn't work as one might expect with smoothstep
or
smootherstep
.
a = 1
b = 2
MB::M.interp(a, b, 2)
# => 3
Plotting
A simple wrapper around GNUPlot (or any compatible plotter) is provided that can plot to an image file, a graphical window, or a text console.
The MB::M::Plot#plot
method takes a Hash
mapping dataset names to data
values.
# Standard plot
p = MB::M::Plot.terminal(width_fraction: 1, height_fraction: 1, width: 40, height: 15)
p.plot({noise: Numo::SFloat.zeros(10).rand(-0.9, 0.9)}, columns: 1, rows: 1)
1 +----------------------------+
| + + + + + + + + |
0.5 |-+ noise *******-|
| * * |
| * * * ** |
0 |-* * * * * +*|
| * * * * * *|
-0.5 |*+ * * * * +-|
| * * * |
| + + + *********** + |
-1 +----------------------------+
0 1 2 3 4 5 6 7 8 9
# Scatter plot
p = MB::M::Plot.terminal(width_fraction: 1, height_fraction: 1, width: 40, height: 20)
points = (0..(Math::PI * 2)).step(Math::PI / 8).map { |a| [ 0.9 * Math.cos(a), 0.9 * Math.sin(a) ] }
p.xrange(-1, 1)
p.plot({ circle: points })
1 +----------------------------+
| + + + |
| circle ******* |
| |
0.5 |-+ ********* +-|
| * ** |
| * * |
0 |-+ * * +-|
| * * |
| * * |
| *** ** |
-0.5 |-+ ******* +-|
| |
| |
| + + + |
-1 +----------------------------+
-1 -0.5 0 0.5 1
Scaling ranges
Scales values or Numo::NArray
s from one linear range to another,
extrapolating for values beyond the end of the range.
MB::M.scale(2, 0..4, 10..12)
# => 11
MB::M.scale(-2, 0..4, 10..12)
# => 9
# Reverse ranges work too
MB::M.scale(Numo::SFloat[0, 1, 2, 3, 4], 1..3, 6..2)
# => Numo::SFloat[8, 6, 4, 2, 0]
Finding roots of functions
Quadratic roots
# f(x) = x^2 + 4
MB::M.quadratic_roots(1, 0, 4)
# => [(0.0+2.0i), (0.0-2.0i)]
Polynomials
There is a MB::M::Polynomial
class for representing polynomials of one
variable, and its roots
method can usually find exact or approximate roots
for most polynomials, even Complex
roots.
Note that the MB::M::RootMethods#find_one_root
method that drives this code
is not the fastest, most reliable, or most accurate method possible. This code
is still mainly useful for experimentation and exploration, rather than serious
numerical computing.
p = MB::M::Polynomial.new(1, 0, 0, 0, -1)
puts p.to_s(unicode: true)
# => x⁴ - 1
p.roots
# => [-1, (0-1i), (0+1i), 1]
See experiments/polynomial/*
and spec/lib/mb/m/polynomial_spec.rb
for more
examples.
Other functions
You can find one approximate root at a time for any Ruby function that accepts
and returns a single Numeric
value using MB::M::RootMethods#find_one_root
:
MB::M.find_one_root(0.25, Math.method(:sin))
# => 0
MB::M.find_one_root(7.1-8.2i, ->(x) { CMath.cos(Math::PI * (x - 3)) * CMath.sin(Math::PI * (1i * x + 2)) })
=> (-3.302389731710742e-41-0.9999999999999998i)
There are other examples in spec/lib/mb/m/root_methods_spec.rb
.
Installation and usage
This project contains some useful programs of its own, or you can use it as a Gem (with Git source) in your own projects.
Standalone usage and development
First, install a Ruby version manager like RVM. Using the system's Ruby is not recommended -- that is only for applications that come with the system. You should follow the instructions from https://rvm.io, but here are the basics:
gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | bash -s stable
Next, install Ruby. RVM binary rubies are still broken on Ubuntu 20.04.x, so
use the --disable-binary
option if you are running Ubuntu 20.04.x.
# Also compatible with Ruby 3.3
rvm install --disable-binary 2.7.8
You can tell RVM to isolate all your projects and switch Ruby versions
automatically by creating .ruby-version
and .ruby-gemset
files (already
present in this project):
cd mb-math
cat .ruby-gemset
cat .ruby-version
Now install dependencies:
bundle install
# Ubuntu/Debian
sudo apt-get install gnuplot-qt
# macOS
brew install gnuplot
Using the project as a Gem
To use mb-math in your own Ruby projects, add this Git repo to your
Gemfile
:
# your-project/Gemfile
gem 'mb-math', git: 'https://github.com/mike-bourgeous/mb-math.git
Testing
Run rspec
to run all tests. There is also a GitHub actions pipeline that
runs tests automatically.
Contributing
Pull requests welcome, though development is focused specifically on the needs of my video series.
License
This project is released under a 2-clause BSD license. See the LICENSE file.