spitzy 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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +83 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/spitzy.rb +12 -0
- data/lib/spitzy/advection_eq.rb +178 -0
- data/lib/spitzy/bvp.rb +242 -0
- data/lib/spitzy/ode.rb +290 -0
- data/lib/spitzy/poissons_eq.rb +204 -0
- data/lib/spitzy/version.rb +3 -0
- data/spitzy.gemspec +32 -0
- data/spitzy.jpg +0 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9b4c4514dc16190a33abdb5c74642714f42c7909
|
4
|
+
data.tar.gz: 7e121d89a995d6f40a13862e3d5b4e9e5fc2499c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3c646c95d3d443eadad900249330828f561de508690fe91e19649039b90867c2dfdded1c8d23d3da1a90776f670f92527d5ad0513f713cd80156d01379ba6cdf
|
7
|
+
data.tar.gz: 8bcd7262839166a08c38c91edd57f79d0d933381e33c23b1c2d898387b9e62b6d38db2b119617e188dac06f8a1ce8b9fb7b67ac7de248be2056b9e5896c781b4
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.1
|
4
|
+
- 2.2
|
5
|
+
before_install:
|
6
|
+
- sudo apt-get update -qq
|
7
|
+
- sudo apt-get install -y liblapack-dev
|
8
|
+
# for ATLAS implementation of LAPACK one can use;
|
9
|
+
# - sudo apt-get install -y libatlas-base-dev
|
10
|
+
install:
|
11
|
+
- gem install bundler
|
12
|
+
- bundle install
|
13
|
+
- bundle exec rake install
|
14
|
+
script:
|
15
|
+
- bundle exec rspec spec
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Alexej Gossmann
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Spitzy
|
2
|
+
|
3
|
+
[](https://travis-ci.org/agisga/mixed_models)
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
Spitzy is this cute pomeranian.
|
8
|
+
Spitzy reads backwards as *yztips*, which translates into:
|
9
|
+
|
10
|
+
***Y*our *Z*appy-*T*appy *I*nitial and boundary value *P*artial (and ordinary) differential equation *S*olver**
|
11
|
+
|
12
|
+
Now, spitzy is also a growing collection of numerical methods for differential equations, written in Ruby.
|
13
|
+
To my knowledge, apart from an [interface with the DASPK Fortran library](https://rubygems.org/gems/rb-daspk/versions/0.0.7-x86-mswin32-60), there currently does not exist another differential equation solver gem for Ruby.
|
14
|
+
|
15
|
+
<!--
|
16
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/spitzy`. To experiment with that code, run `bin/console` for an interactive prompt.
|
17
|
+
|
18
|
+
TODO: Delete this and the text above, and describe your gem
|
19
|
+
-->
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
<!--
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'spitzy'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install spitzy
|
37
|
+
-->
|
38
|
+
|
39
|
+
Ruby is required in version >=2.0 because keyword arguments are excessively used in `spitzy`.
|
40
|
+
Moreover, prior to the installation of `spitzy`, currently the `NMatrix` gem needs to be installed in its development version (because `Poissons_eq` uses `#meshgrid`)
|
41
|
+
from <https://github.com/SciRuby/nmatrix.git>.
|
42
|
+
|
43
|
+
Then `spitzy` can be installed using the command line (or something similar):
|
44
|
+
|
45
|
+
```
|
46
|
+
git clone https://github.com/agisga/spitzy.git
|
47
|
+
cd spitzy/
|
48
|
+
bundle install
|
49
|
+
bundle exec rake install
|
50
|
+
```
|
51
|
+
|
52
|
+
Feel free to contact me at alexej.go [at] googlemail.com, in case of difficulties with the installation.
|
53
|
+
|
54
|
+
## Documentation, Tutorials and Usage Examples
|
55
|
+
|
56
|
+
* Technical Report: <http://agisga.github.io/spitzy/documentation/>
|
57
|
+
|
58
|
+
* Slide show: <http://agisga.github.io/presentations/spitzy.html#/>
|
59
|
+
|
60
|
+
* Blog posts about some of the implemented methods:
|
61
|
+
|
62
|
+
- [Ordinary differential equations](http://agisga.github.io/ODE/)
|
63
|
+
|
64
|
+
- [Two point boundary value problem](http://agisga.github.io/BVP/)
|
65
|
+
|
66
|
+
- [Advection equation](http://agisga.github.io/Advection-Equation/)
|
67
|
+
|
68
|
+
|
69
|
+
<!--
|
70
|
+
## Development
|
71
|
+
|
72
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
73
|
+
|
74
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
75
|
+
-->
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
1. Fork it
|
80
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
81
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
82
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
83
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "spitzy"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/spitzy.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Copyright (c) 2015 Alexej Gossmann
|
2
|
+
|
3
|
+
require "spitzy/version"
|
4
|
+
|
5
|
+
require 'nmatrix/nmatrix'
|
6
|
+
require 'nmatrix/lapack_plugin'
|
7
|
+
|
8
|
+
require "spitzy/advection_eq.rb"
|
9
|
+
require "spitzy/ode.rb"
|
10
|
+
require "spitzy/bvp.rb"
|
11
|
+
require "spitzy/poissons_eq.rb"
|
12
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# Copyright (c) 2015 Alexej Gossmann
|
2
|
+
|
3
|
+
module Spitzy
|
4
|
+
# Numerically solves the 1D linear advection equation:
|
5
|
+
#
|
6
|
+
# * PDE: du/dt + a * du/dx = 0,
|
7
|
+
# * on the domain: 0 < x < xmax and 0 < t < tmax,
|
8
|
+
# * with periodic boundary consitions: u(0,t) = u(xmax, t),
|
9
|
+
# * with initial condition as supplied by the user.
|
10
|
+
#
|
11
|
+
class AdvectionEq
|
12
|
+
|
13
|
+
# time step in x
|
14
|
+
attr_reader :dx
|
15
|
+
# time step in t
|
16
|
+
attr_reader :dt
|
17
|
+
# parameter in the 1D linear advection equation
|
18
|
+
attr_reader :a
|
19
|
+
# array of space points
|
20
|
+
attr_reader :x
|
21
|
+
# array of time points
|
22
|
+
attr_reader :t
|
23
|
+
# number of space points (i.e. length of +x+)
|
24
|
+
attr_reader :mx
|
25
|
+
# number of time points (i.e. length of +t+)
|
26
|
+
attr_reader :mt
|
27
|
+
# the numerical solution as an array of arrays
|
28
|
+
attr_reader :u
|
29
|
+
# the numerical scheme applied
|
30
|
+
attr_reader :method
|
31
|
+
|
32
|
+
# Constructor for all solver routines for the 1D linear advection equation:
|
33
|
+
# * PDE: du/dt + a * du/dx = 0,
|
34
|
+
# * on the domain: xmin < x < xmax and tmin < t < tmax,
|
35
|
+
# * with periodic boundary consitions: u(xmin,t) = u(xmax,t),
|
36
|
+
# * with initial condition u(x,tmin) as supplied by the user.
|
37
|
+
#
|
38
|
+
# It initializes the parameters and solves the equation
|
39
|
+
# using one of the methods:
|
40
|
+
# Upwind, Leapfrog, Lax-Friedrichs, Lax-Wendroff.
|
41
|
+
#
|
42
|
+
# === Arguments
|
43
|
+
#
|
44
|
+
# * +xrange+ - An array of the form [xmin, xmax]. The space domain xmin<=x<=xmax
|
45
|
+
# on which the solution will be evaluated.
|
46
|
+
#
|
47
|
+
# * +trange+ - An array of the form [tmin, tmax]. The time domain tmin<=t<=tmax
|
48
|
+
# on which the solution will be evaluated.
|
49
|
+
#
|
50
|
+
# * +dx+ - Stepsize in space, i.e. in x
|
51
|
+
#
|
52
|
+
# * +dt+ - Stepsize in time, i.e. in t
|
53
|
+
#
|
54
|
+
# * +a+ - The constant speed +a+ in the PDE du/dt + a * du/dx = 0
|
55
|
+
#
|
56
|
+
# * +method+ - The numerical scheme used to solve the PDE. Possible values are:
|
57
|
+
# +:upwind+, +:lax_friedrichs+, +:leapfrog+ or +:lax_wendroff+ (default is +:lax_wendroff+).
|
58
|
+
#
|
59
|
+
# * +&ic+ - The initial condition which must be supplied as a +proc+ object.
|
60
|
+
# The initial condition is a function in x at time t=0.
|
61
|
+
#
|
62
|
+
# ==== Usage
|
63
|
+
#
|
64
|
+
# ic = proc { |x| Math::cos(2*Math::PI*x) + 0.2*Math::cos(10*Math::PI*x) }
|
65
|
+
# numsol = Spitzy::AdvectionEq.new(xrange: [0.0,1.0], trange: [0.0,10.0], dx: 1.0/101,
|
66
|
+
# dt: 0.95/101, a: 1.0, method: :upwind, &ic)
|
67
|
+
#
|
68
|
+
def initialize(xrange: , trange: , dx: , dt: , a: , method: :lax_wendroff, &ic)
|
69
|
+
raise(ArgumentError, "Expected xrange to be an array of length 2") unless xrange.length == 2
|
70
|
+
raise(ArgumentError, "Expected trange to be an array of length 2") unless trange.length == 2
|
71
|
+
@dx = dx # x step size
|
72
|
+
@dt = dt #t step size
|
73
|
+
@a = a # parameter in the 1-dim linear advection eq.
|
74
|
+
@ic = ic # initial condition
|
75
|
+
@x = (xrange[0]..xrange[1]).step(dx).to_a # x steps
|
76
|
+
@x << xrange[1] if @x.last < xrange[1]
|
77
|
+
@mx = @x.length # Number of x steps
|
78
|
+
@t = (trange[0]..trange[1]).step(dt).to_a # t steps
|
79
|
+
@t << trange[1] if @t.last < trange[1]
|
80
|
+
@mt = @t.length # Number of t steps
|
81
|
+
@u = [] # Stores numerical solution
|
82
|
+
|
83
|
+
@method = method
|
84
|
+
case @method
|
85
|
+
when :upwind then upwind
|
86
|
+
when :lax_friedrichs then lax_friedrichs
|
87
|
+
when :leapfrog then leapfrog
|
88
|
+
when :lax_wendroff then lax_wendroff
|
89
|
+
else raise(ArgumentError, "#{@method} is not a valid method.")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Evaluate the initial condition at x0
|
94
|
+
#
|
95
|
+
# ==== Arguments
|
96
|
+
#
|
97
|
+
# * +x0+ - A floating point number
|
98
|
+
#
|
99
|
+
def ic(x0)
|
100
|
+
@ic.call(x0)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get a character string representing the PDE, its domain and boundary condition.
|
104
|
+
def equation
|
105
|
+
"du/dt + #{@a} * du/dx = 0, #{@x[0]} < x < #{@x[-1]} and #{@t[0]} < t < #{@t[-1]}, u(0,t) = u(#{@x[-1]}, t)"
|
106
|
+
end
|
107
|
+
|
108
|
+
######### Available numeric schemes ##########
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# Solves the 1D linear advection equation
|
113
|
+
# du/dt + a * du/dx = 0
|
114
|
+
# with the Upwind scheme.
|
115
|
+
# Requires: a > 0
|
116
|
+
def upwind
|
117
|
+
raise(ArgumentError, "the upwind scheme requires a>0.") if @a<=0
|
118
|
+
@u[0] = @mx.times.map { |j| self.ic(@x[j]) } # IC: u(x,0)
|
119
|
+
alpha = @a*@dt/@dx
|
120
|
+
0.upto(@mt-2) do |n|
|
121
|
+
@u[n+1] = (1.upto(@mx-2)).to_a
|
122
|
+
@u[n+1].map! { |j| @u[n][j]-alpha*(@u[n][j]-@u[n][j-1])}
|
123
|
+
@u[n+1].unshift @u[n][0]-alpha*(@u[n][0]-@u[n][@mx-2])
|
124
|
+
@u[n+1].push @u[n+1][0] # periodic BC
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Solves the 1D linear advection equation
|
129
|
+
# du/dt + a * du/dx = 0
|
130
|
+
# with the Lax-Friedrichs scheme.
|
131
|
+
def lax_friedrichs
|
132
|
+
@u[0] = @mx.times.map { |j| self.ic(@x[j]) } # IC: u(x,0)
|
133
|
+
alpha = @a*@dt/@dx
|
134
|
+
0.upto(@mt-2) do |n|
|
135
|
+
@u[n+1] = (1.upto(@mx-3)).to_a
|
136
|
+
@u[n+1].map! { |j| 0.5*(@u[n][j+1]+u[n][j-1])-0.5*alpha*(@u[n][j+1]-@u[n][j-1]) }
|
137
|
+
@u[n+1].unshift 0.5*(@u[n][1]+u[n][@mx-2])-0.5*alpha*(@u[n][1]-@u[n][@mx-2]) # u(0,t)
|
138
|
+
@u[n+1].push 0.5*(@u[n][0]+u[n][@mx-3])-0.5*alpha*(@u[n][0]-@u[n][@mx-3]) # u(max(x), t)
|
139
|
+
@u[n+1].push @u[n+1][0] # periodic BC
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Solves the 1D linear advection equation
|
144
|
+
# du/dt + a * du/dx = 0
|
145
|
+
# with the Leapfrog scheme.
|
146
|
+
def leapfrog
|
147
|
+
@u[0] = @mx.times.map { |j| self.ic(@x[j]) } # IC: u(x,0)
|
148
|
+
alpha = @a*@dt/@dx
|
149
|
+
@u[1] = (1.upto(@mx-3)).to_a # u(x, t1)
|
150
|
+
@u[1].map! { |j| 0.5*(@u[0][j+1]+u[0][j-1])-0.5*alpha*(@u[0][j+1]-@u[0][j-1]) }
|
151
|
+
@u[1].unshift 0.5*(@u[0][1]+u[0][@mx-2])-0.5*alpha*(@u[0][1]-@u[0][@mx-2]) # u(0,t1)
|
152
|
+
@u[1].push 0.5*(@u[0][0]+u[0][@mx-3])-0.5*alpha*(@u[0][0]-@u[0][@mx-3]) # u(max(x), t1)
|
153
|
+
@u[1].push @u[1][0] # periodic BC
|
154
|
+
1.upto(@mt-2) do |n|
|
155
|
+
@u[n+1] = (1.upto(@mx-3)).to_a
|
156
|
+
@u[n+1].map! { |j| @u[n-1][j]-alpha*(@u[n][j+1]-@u[n][j-1])}
|
157
|
+
@u[n+1].unshift @u[n-1][0]-alpha*(@u[n][1]-@u[n][@mx-2]) # u(0,t)
|
158
|
+
@u[n+1].push @u[n-1][@mx-2]-alpha*(@u[n][0]-@u[n][@mx-3]) # u(max(x), t)
|
159
|
+
@u[n+1].push @u[n+1][0] # periodic BC
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Solves the 1D linear advection equation
|
164
|
+
# du/dt + a * du/dx = 0
|
165
|
+
# with the Lax-Wendroff scheme.
|
166
|
+
def lax_wendroff
|
167
|
+
@u[0] = @mx.times.map { |j| self.ic(@x[j]) } # IC: u(x,0)
|
168
|
+
alpha = @a*@dt/@dx
|
169
|
+
0.upto(@mt-2) do |n|
|
170
|
+
@u[n+1] = (1.upto(@mx-3)).to_a
|
171
|
+
@u[n+1].map! { |j| @u[n][j]-0.5*alpha*(@u[n][j+1]-@u[n][j-1])+0.5*(alpha**2)*(u[n][j+1]-2*u[n][j]+u[n][j-1]) }
|
172
|
+
@u[n+1].unshift @u[n][0]-0.5*alpha*(@u[n][1]-@u[n][@mx-2])+0.5*(alpha**2)*(u[n][1]-2*u[n][0]+u[n][@mx-2]) # u(0,t)
|
173
|
+
@u[n+1].push @u[n][@mx-2]-0.5*alpha*(@u[n][0]-@u[n][@mx-3])+0.5*(alpha**2)*(u[n][0]-2*u[n][@mx-2]+u[n][@mx-3]) # u(max(x), t)
|
174
|
+
@u[n+1].push @u[n+1][0] # periodic BC
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/spitzy/bvp.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
# Copyright (c) 2015 Alexej Gossmann
|
2
|
+
|
3
|
+
module Spitzy
|
4
|
+
# Numerically solves a boundary value problem of the general form:
|
5
|
+
#
|
6
|
+
# * ODE: -(a*u')' + b*u' + c*u = f, xmin<x<xmax,
|
7
|
+
# * where a, b, c, f and u are functions of x,
|
8
|
+
# * with Dirichlet boundary conditions: u(xmin) = bc[1], u(xmax) = bc[2].
|
9
|
+
#
|
10
|
+
class Bvp
|
11
|
+
|
12
|
+
# array of the points in the domain at which the numerical solution was evaluated
|
13
|
+
attr_reader :x
|
14
|
+
# number of grid points (i.e. length of +x+)
|
15
|
+
attr_reader :mx
|
16
|
+
# step size in x
|
17
|
+
attr_reader :dx
|
18
|
+
# the boundary conditions (an array of length two)
|
19
|
+
attr_reader :bc
|
20
|
+
# the numerical scheme applied
|
21
|
+
attr_reader :method
|
22
|
+
# the numerical solution as an array. It is obtained as a solution to a linear system.
|
23
|
+
attr_reader :u
|
24
|
+
# the right hand side of the solved linear system
|
25
|
+
attr_reader :rhs
|
26
|
+
# the stiffness matrix, i.e. the matrix of the solved linear system
|
27
|
+
attr_reader :mat
|
28
|
+
|
29
|
+
# Constructor for all solver routines for the boundary value problem:
|
30
|
+
#
|
31
|
+
# * ODE: -(a*u')' + b*u' + c*u = f, xmin<x<xmax,
|
32
|
+
# * where a, b, c, f and u are functions of x,
|
33
|
+
# * with Dirichlet boundary conditions: u(xmin) = bc[1], u(xmax) = bc[2].
|
34
|
+
#
|
35
|
+
# It initializes the parameters and solves the equation
|
36
|
+
# using one of the methods:
|
37
|
+
# Linear Finite Elements.
|
38
|
+
#
|
39
|
+
# === Arguments
|
40
|
+
#
|
41
|
+
# * +xrange+ - An array of the form [xmin, xmax]. The interval on which the solution will be evaluated.
|
42
|
+
#
|
43
|
+
# * +mx+ - Number of grid points. Determines the array x of equally spaced values on which the numerical
|
44
|
+
# solution can be evaluated.
|
45
|
+
#
|
46
|
+
# * +bc+ - An array of length two, specifying the two boundary conditions.
|
47
|
+
#
|
48
|
+
# * +method+ - The numerical scheme used to solve the BVP. Possible values are:
|
49
|
+
# +:lin_fin_elt+ (linear finite elements).
|
50
|
+
#
|
51
|
+
# * +a+ - A +Proc+ or +Numeric+ object. The function a(x) in the left hand side
|
52
|
+
# of the differential equation which can be supplied as a +Proc+ object.
|
53
|
+
# If a +Numeric+ object is supplied then a(x) is assumed to be constant equal to that number.
|
54
|
+
#
|
55
|
+
# * +b+ - A +Proc+ or +Numeric+ object. The function b(x) in the left hand side
|
56
|
+
# of the differential equation which can be supplied as a +Proc+ object.
|
57
|
+
# If a +Numeric+ object is supplied then b(x) is assumed to be constant equal to that number.
|
58
|
+
#
|
59
|
+
# * +c+ - A +Proc+ or +Numeric+ object. The function c(x) in the left hand side
|
60
|
+
# of the differential equation which can be supplied as a +Proc+ object.
|
61
|
+
# If a +Numeric+ object is supplied then c(x) is assumed to be constant equal to that number.
|
62
|
+
#
|
63
|
+
# * +f+ - A +Proc+ or +Numeric+ object. The right hand side f(x) of the differential
|
64
|
+
# equation, which can be supplied as a +Proc+ object. If a +Numeric+ object is
|
65
|
+
# supplied then f(x) is assumed to be constant equal to that number.
|
66
|
+
#
|
67
|
+
# ==== Usage
|
68
|
+
#
|
69
|
+
# # a,b,c and f are Numeric
|
70
|
+
# bvp_sol = Spitzy::Bvp.new(xrange: [0.0, 100.0], mx: 100, bc: [10.0, 0.00090799859],
|
71
|
+
# a: 800.0*Math::PI, b: 0.0, c: 8.0*Math::PI, f: 0.0)
|
72
|
+
#
|
73
|
+
# # a,b,c and f are Proc objects
|
74
|
+
# a = Proc.new { |x| -Math::cos(x) }
|
75
|
+
# b = Proc.new { |x| Math::sin(x) }
|
76
|
+
# c = Proc.new { |x| Math::cos(x) }
|
77
|
+
# f = Proc.new { |x| -2.0*(Math::cos(x))**2 }
|
78
|
+
# xrange = [0.0, 10.0]
|
79
|
+
# bc = [0.0, (1.0 - 10.0) * Math::sin(10.0)]
|
80
|
+
# bvp_sol = Spitzy::Bvp.new(xrange: xrange, mx: 100, bc: bc,
|
81
|
+
# a: a, b: b, c: c, f: f)
|
82
|
+
#
|
83
|
+
def initialize(xrange:, mx:, bc:, method: :lin_fin_elt, a:, b:, c:, f:)
|
84
|
+
|
85
|
+
raise(ArgumentError, "Expected xrange to be an array of length 2") unless xrange.length == 2
|
86
|
+
raise(ArgumentError, "Expected bc to be an array of length 2") unless bc.length == 2
|
87
|
+
@mx = mx
|
88
|
+
@xmin = xrange[0].to_f
|
89
|
+
@xmax = xrange[1].to_f
|
90
|
+
@dx = (@xmax-@xmin) / (@mx-1)
|
91
|
+
@x = (@xmin..@xmax).step(@dx).to_a # x steps
|
92
|
+
@bc = bc
|
93
|
+
@u = [] # Stores numerical solution
|
94
|
+
|
95
|
+
if a.is_a? Proc then
|
96
|
+
@a = a
|
97
|
+
elsif a.is_a? Numeric then
|
98
|
+
@a = Proc.new { |x| a }
|
99
|
+
else
|
100
|
+
raise(ArgumentError, "Expected Numeric or Proc input for a")
|
101
|
+
end
|
102
|
+
if b.is_a? Proc then
|
103
|
+
@b = b
|
104
|
+
elsif b.is_a? Numeric then
|
105
|
+
@b = Proc.new { |x| b }
|
106
|
+
else
|
107
|
+
raise(ArgumentError, "Expected Numeric or Proc input for b")
|
108
|
+
end
|
109
|
+
if c.is_a? Proc then
|
110
|
+
@c = c
|
111
|
+
elsif c.is_a? Numeric then
|
112
|
+
@c = Proc.new { |x| c }
|
113
|
+
else
|
114
|
+
raise(ArgumentError, "Expected Numeric or Proc input for c")
|
115
|
+
end
|
116
|
+
if f.is_a? Proc then
|
117
|
+
@f = f
|
118
|
+
elsif f.is_a? Numeric then
|
119
|
+
@f = Proc.new { |x| f }
|
120
|
+
else
|
121
|
+
raise(ArgumentError, "Expected Numeric or Proc input for f")
|
122
|
+
end
|
123
|
+
|
124
|
+
@method = method
|
125
|
+
case @method
|
126
|
+
when :lin_fin_elt then lin_fin_elt
|
127
|
+
else raise(ArgumentError, "#{@method} is not a valid method.")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Evaluate the function a(x) at x=x0
|
132
|
+
#
|
133
|
+
# ==== Arguments
|
134
|
+
#
|
135
|
+
# * +x0+ - A floating point number
|
136
|
+
#
|
137
|
+
def a(x0)
|
138
|
+
@a.call(x0)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Evaluate the function b(x) at x=x0
|
142
|
+
#
|
143
|
+
# ==== Arguments
|
144
|
+
#
|
145
|
+
# * +x0+ - A floating point number
|
146
|
+
#
|
147
|
+
def b(x0)
|
148
|
+
@b.call(x0)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Evaluate the function c(x) at x=x0
|
152
|
+
#
|
153
|
+
# ==== Arguments
|
154
|
+
#
|
155
|
+
# * +x0+ - A floating point number
|
156
|
+
#
|
157
|
+
def c(x0)
|
158
|
+
@c.call(x0)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Evaluate the function f(x) at x=x0
|
162
|
+
#
|
163
|
+
# ==== Arguments
|
164
|
+
#
|
165
|
+
# * +x0+ - A floating point number
|
166
|
+
#
|
167
|
+
def f(x0)
|
168
|
+
@f.call(x0)
|
169
|
+
end
|
170
|
+
|
171
|
+
######### Available numeric schemes ##########
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
# Solve the boundary value problem
|
176
|
+
#
|
177
|
+
# * ODE: -(a*u')' + b*u' + c*u = f, xmin<x<xmax,
|
178
|
+
# * where a, b, c, f and u are functions of x,
|
179
|
+
# * with Dirichlet boundary conditions: u(xmin) = bc[1], u(xmax) = bc[2]
|
180
|
+
#
|
181
|
+
# using the finite elements method with piecewise linear elements.
|
182
|
+
#
|
183
|
+
# === References
|
184
|
+
#
|
185
|
+
# * A Quarteroni, R Sacco, F Saleri, "Numerical Mathematics", 2nd ed., section 7.4.
|
186
|
+
#
|
187
|
+
def lin_fin_elt
|
188
|
+
gridcenters = ((@xmin+@dx/2)..(@xmax-@dx/2)).step(@dx).to_a
|
189
|
+
a_gridcenters = NMatrix.new([@mx-1,1], gridcenters.map {|x0| self.a(x0) }, dtype: :float64)
|
190
|
+
b_gridcenters = NMatrix.new([@mx-1,1], gridcenters.map {|x0| self.b(x0) }, dtype: :float64)
|
191
|
+
c_gridcenters = NMatrix.new([@mx-1,1], gridcenters.map {|x0| self.c(x0) }, dtype: :float64)
|
192
|
+
f_gridcenters = NMatrix.new([@mx-1,1], gridcenters.map {|x0| self.f(x0) }, dtype: :float64)
|
193
|
+
|
194
|
+
# Construct the RHS for the linear system
|
195
|
+
@rhs = (f_gridcenters[0..-2] + f_gridcenters[1..-1]) * @dx * 0.5
|
196
|
+
@rhs[0] = @rhs[0] - @bc[0] * (-a_gridcenters[0] / @dx - b_gridcenters[0] / 2.0 + @dx * c_gridcenters[0] / 3.0)
|
197
|
+
@rhs[-1] = @rhs[-1] - @bc[1] * (-a_gridcenters[-1] / @dx + b_gridcenters[-1] / 2.0 + @dx * c_gridcenters[-1] / 3.0)
|
198
|
+
|
199
|
+
# Construct the tridiagonal stiffness matrix
|
200
|
+
# TODO: the matrix should be saved as a sparse stype
|
201
|
+
dd = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
202
|
+
dc = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
203
|
+
dr = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
204
|
+
ld = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
205
|
+
lc = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
206
|
+
lr = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
207
|
+
ud = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
208
|
+
uc = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
209
|
+
ur = NMatrix.new([@mx-2,1], 0.0, dtype: :float64)
|
210
|
+
1.upto(@mx-2) do |i|
|
211
|
+
dd[i-1] = (a_gridcenters[i-1] + a_gridcenters[i]) / @dx
|
212
|
+
dc[i-1] = (b_gridcenters[i-1] - b_gridcenters[i]) / 2.0
|
213
|
+
dr[i-1] = @dx * (c_gridcenters[i-1] + c_gridcenters[i]) / 3.0
|
214
|
+
if i>1 then
|
215
|
+
ld[i-2] = -a_gridcenters[i-1] / @dx
|
216
|
+
lc[i-2] = -b_gridcenters[i-1] / 2.0
|
217
|
+
lr[i-2] = @dx * c_gridcenters[i-1] / 6.0
|
218
|
+
end
|
219
|
+
if i<(@mx-2) then
|
220
|
+
ud[i-1] = -a_gridcenters[i] / @dx
|
221
|
+
uc[i-1] = b_gridcenters[i] / 2.0
|
222
|
+
ur[i-1] = @dx * c_gridcenters[i] / 6.0
|
223
|
+
end
|
224
|
+
end
|
225
|
+
diag = dd + dc + dr
|
226
|
+
ldiag = ld + lc + lr
|
227
|
+
udiag = ud + uc + ur
|
228
|
+
@mat = NMatrix.diagonal(diag.to_a, dtype: :float64)
|
229
|
+
1.upto(@mx-3) do |j|
|
230
|
+
@mat[j,j-1] = ldiag[j-1]
|
231
|
+
@mat[j-1,j] = udiag[j-1]
|
232
|
+
end
|
233
|
+
|
234
|
+
# Solve the system
|
235
|
+
# TODO: The system is tridiagonal and should be solved accordingly
|
236
|
+
w = @mat.solve(@rhs)
|
237
|
+
@u = w.to_flat_a
|
238
|
+
@u.insert(0, @bc[0])
|
239
|
+
@u.insert(-1, @bc[1])
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
data/lib/spitzy/ode.rb
ADDED
@@ -0,0 +1,290 @@
|
|
1
|
+
# Copyright (c) 2015 Alexej Gossmann
|
2
|
+
|
3
|
+
module Spitzy
|
4
|
+
# Numerically solves an initial value problem of the form
|
5
|
+
#
|
6
|
+
# * dy/dx = f(x,y), xmin < x < xmax,
|
7
|
+
# * y(xmin) = yini
|
8
|
+
#
|
9
|
+
class Ode
|
10
|
+
|
11
|
+
# array of the points in the domain at which the numerical solution was evaluated
|
12
|
+
attr_reader :x
|
13
|
+
# number of points (i.e. length of +x+)
|
14
|
+
attr_reader :mx
|
15
|
+
# value of f at every point in x
|
16
|
+
attr_reader :fx
|
17
|
+
# the numerical solution as an array
|
18
|
+
attr_reader :u
|
19
|
+
# the error tolerance of the method (only applicable for methods with automatic step size adjustment)
|
20
|
+
attr_reader :tol
|
21
|
+
# the numerical scheme applied
|
22
|
+
attr_reader :method
|
23
|
+
|
24
|
+
# Constructor for all solver routines for the initial value problem:
|
25
|
+
# * dy/dx = f(x,y), xmin < x < xmax,
|
26
|
+
# * y(xmin) = yini
|
27
|
+
#
|
28
|
+
# It initializes the parameters and solves the equation
|
29
|
+
# using one of the methods:
|
30
|
+
# Dormand-Prince, Forward Euler, Adams-Bashforth order 2.
|
31
|
+
#
|
32
|
+
# ==== Arguments
|
33
|
+
#
|
34
|
+
# * +xrange+ - An array of the form [xmin, xmax]. The interval on which the solution will be evaluated.
|
35
|
+
#
|
36
|
+
# * +dx+ - Determines the grid of x values. The length of the x step for methods without automatic step size adjustment,
|
37
|
+
# or the maximum step size for methods with automatic step size adjustment.
|
38
|
+
#
|
39
|
+
# * +yini+ - Initial value for y, i.e. y(xmin).
|
40
|
+
#
|
41
|
+
# * +tol+ - The desired error tolerance of the method (only for methods with automatic step size correction).
|
42
|
+
#
|
43
|
+
# * +maxiter+ - The maximal number of performed iterations for methods with automatic step size adjustment.
|
44
|
+
#
|
45
|
+
# * +method+ - The numerical scheme used to solve the ODE. Possible values are:
|
46
|
+
# +:dopri+ (Dormand-Prince), +:euler+ (Forward Euler), +:ab2+ (Adams-Bashforth of order 2).
|
47
|
+
# +:dopri+ performs an automatic step size adjustment and error estimation in order to
|
48
|
+
# keep the error of the numerical solution below +tol+. +:euler+ and +:ab2+ operate on a
|
49
|
+
# fixed step size +dx+, and cannot estimate the error of the numerical solution.
|
50
|
+
# Currently, the default is +:dopri+ (Dormand-Prince is currently also the default method in MATLAB
|
51
|
+
# and GNU Octave's ode45 solver and is the default choice for the Simulink's model explorer solver).
|
52
|
+
#
|
53
|
+
# * +&f+ - The right hand side of the differential equation which must be supplied as a +proc+ object.
|
54
|
+
# It is a function of x and y, where x should be the _first_ argument, and y the _second_.
|
55
|
+
#
|
56
|
+
# ==== Usage
|
57
|
+
#
|
58
|
+
# # Dormand-Prince example 1
|
59
|
+
# f = proc { |t,y| 1.0 + y/t + (y/t)**2 }
|
60
|
+
# dopri_sol = Spitzy::Ode.new(xrange: [1.0,4.0], dx: 0.1, yini: 0.0, &f)
|
61
|
+
# puts "The numerical solution at x=4 is u(4)=#{dopri_sol.u[-1]}"
|
62
|
+
#
|
63
|
+
# # Dormand-Prince example 2
|
64
|
+
# f = proc { |t,y| -2.0 * y + Math::exp(-2.0 * (t - 6.0)**2) }
|
65
|
+
# dopri_sol = Spitzy::Ode.new(xrange: [0.0,10.0], dx: 1.5, yini: 1.0,
|
66
|
+
# tol: 1e-6, maxiter: 1e6, &f)
|
67
|
+
# # Plot dopri_sol.x agains dopri_sol.u to observe step size adaptivity
|
68
|
+
#
|
69
|
+
# # Euler example 1
|
70
|
+
# f = proc { |t,y| 1.0 + y/t + (y/t)**2 }
|
71
|
+
# euler_sol = Spitzy::Ode.new(xrange: [1.0,4.0], dx: 0.01,
|
72
|
+
# yini: 0.0, method: :euler, &f)
|
73
|
+
#
|
74
|
+
# # Adams-Bashforth example 1
|
75
|
+
# f = proc { |t,y| -2.0*t*y }
|
76
|
+
# ab2_sol = Spitzy::Ode.new(xrange: [0.0,4.0], dx: 0.1,
|
77
|
+
# yini: 1.0, method: :ab2, &f)
|
78
|
+
# exact_sol = ab2_sol.x.map { |tt| Math::exp(-(tt**2)) }
|
79
|
+
# maxerror1 = exact_sol1.each_with_index.map {|n,i| n - ab2_sol1.u[i] }.max.abs
|
80
|
+
# puts "Number of x steps: #{ab2_sol1.mx}"
|
81
|
+
# puts "Error: #{maxerror1}maxerror2}"
|
82
|
+
# puts "Error ratio: #{maxerror1/maxerror2}"
|
83
|
+
|
84
|
+
def initialize(xrange:, dx:, yini:, tol: 1e-2, maxiter: 1e6, method: :dopri, &f)
|
85
|
+
|
86
|
+
raise(ArgumentError, "Expected xrange to be an array of length 2") unless xrange.length == 2
|
87
|
+
@dx = dx
|
88
|
+
@xmin = xrange[0]
|
89
|
+
@xmax = xrange[1]
|
90
|
+
@yini = yini
|
91
|
+
@maxiter = maxiter
|
92
|
+
@tol = tol
|
93
|
+
@f = f
|
94
|
+
@x = [] # Stores the x grid
|
95
|
+
@fx = [] # Stores the values of f at every point in x
|
96
|
+
@u = [] # Stores numerical solution
|
97
|
+
|
98
|
+
@method = method
|
99
|
+
case @method
|
100
|
+
when :euler then euler
|
101
|
+
when :ab2 then ab2
|
102
|
+
when :am3 then am3
|
103
|
+
when :dopri then dopri
|
104
|
+
else raise(ArgumentError, "#{@method} is not a valid method.")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Evaluate the function f(x,y) at x=x0 and y=y0
|
109
|
+
#
|
110
|
+
# ==== Arguments
|
111
|
+
#
|
112
|
+
# * +x0+ - A floating point number
|
113
|
+
#
|
114
|
+
# * +y0+ - A floating point number representing y(x0)
|
115
|
+
#
|
116
|
+
def f(x0, y0)
|
117
|
+
@f.call(x0, y0)
|
118
|
+
end
|
119
|
+
|
120
|
+
######### Available numeric schemes ##########
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Solve the initial value problem
|
125
|
+
# * dy/dx = f(x,y), xmin < x < xmax,
|
126
|
+
# * y(xmin) = yini
|
127
|
+
# with the forward Euler scheme u[n+1] = u[n] + dx * f[n].
|
128
|
+
def euler
|
129
|
+
@x = (@xmin..@xmax).step(@dx).to_a # x steps
|
130
|
+
@x << @xmax if @x.last < @xmax
|
131
|
+
@mx = @x.length # Number of x steps
|
132
|
+
@u[0] = @yini
|
133
|
+
@fx[0] = self.f(@x[0], @u[0])
|
134
|
+
0.upto(@mx-2) do |n|
|
135
|
+
@u[n+1] = @u[n] + @dx * @fx[n]
|
136
|
+
@fx[n+1] = self.f(@x[n+1], @u[n+1])
|
137
|
+
end
|
138
|
+
#tol and maxiter not relevant for the Euler method
|
139
|
+
@tol = nil
|
140
|
+
@maxiter = nil
|
141
|
+
end
|
142
|
+
|
143
|
+
# Solve the initial value problem
|
144
|
+
# * dy/dx = f(x,y), xmin < x < xmax,
|
145
|
+
# * y(xmin) = yini
|
146
|
+
# with the Adams-Bashforth method of order 2,
|
147
|
+
# given by the explicit formula u[n+1] = u[n] + dx/2 * (3*f[n] - f[n-1]).
|
148
|
+
# Since the method requires the last two functional values for the approximation
|
149
|
+
# of the next functional value, a Runge-Kutta method of order 2 (Heun’s method)
|
150
|
+
# is applied to approximate the functional value at the second time step.
|
151
|
+
#
|
152
|
+
def ab2
|
153
|
+
@x = (@xmin..@xmax).step(@dx).to_a # x steps
|
154
|
+
@x << @xmax if @x.last < @xmax
|
155
|
+
@mx = @x.length # Number of x steps
|
156
|
+
|
157
|
+
@u[0] = @yini
|
158
|
+
@fx[0] = self.f(@x[0], @u[0])
|
159
|
+
# Runge-Kutta of order 2 for the second time step
|
160
|
+
@u[1] = @u[0] + @dx/2.0 * (@fx[0] + self.f(@x[1], @u[0] + @dx*@fx[0]))
|
161
|
+
@fx[1] = self.f(@x[1], @u[1])
|
162
|
+
|
163
|
+
1.upto(@mx-2) do |n|
|
164
|
+
@u[n+1] = @u[n] + @dx/2.0 * (3.0*@fx[n] - @fx[n-1])
|
165
|
+
@fx[n+1] = self.f(@x[n+1], @u[n+1])
|
166
|
+
end
|
167
|
+
|
168
|
+
#tol and maxiter not relevant for the this method
|
169
|
+
@tol = nil
|
170
|
+
@maxiter = nil
|
171
|
+
end
|
172
|
+
|
173
|
+
# Solve the initial value problem
|
174
|
+
# * dy/dx = f(x,y), xmin < x < xmax,
|
175
|
+
# * y(xmin) = yini
|
176
|
+
# with the Dormand-Prince method.
|
177
|
+
#
|
178
|
+
# This method automatically adapts the step size in order to keep the error
|
179
|
+
# of the numerical solution below the tolerance level +tol+. However, the step size
|
180
|
+
# is not allowed to exceed the specified maximal step size +dx+ (+dx+ is also the
|
181
|
+
# initial step size). The algorithm throws an exception if it fails to compute the
|
182
|
+
# numerical solution on the given x domain in less than or equal to +maxiter+ iterations.
|
183
|
+
#
|
184
|
+
# === Reference
|
185
|
+
#
|
186
|
+
# * J R Dormand, P J Prince, "A family of embedded Runge-Kutta formulae"
|
187
|
+
#
|
188
|
+
# * E Hairer, S P Norsett and G Wanner, "Solving Differential Equations I, Nonstiff Problems", p. 176.
|
189
|
+
#
|
190
|
+
def dopri
|
191
|
+
a21 = 1.0/5.0
|
192
|
+
a31 = 3.0/40.0
|
193
|
+
a32 = 9.0/40.0
|
194
|
+
a41 = 44.0/45.0
|
195
|
+
a42 = -56.0/15.0
|
196
|
+
a43 = 32.0/9.0
|
197
|
+
a51 = 19372.0/6561.0
|
198
|
+
a52 = -25360.0/2187.0
|
199
|
+
a53 = 64448.0/6561.0
|
200
|
+
a54 = -212.0/729.0
|
201
|
+
a61 = 9017.0/3168.0
|
202
|
+
a62 = -355.0/33.0
|
203
|
+
a63 = 46732.0/5247.0
|
204
|
+
a64 = 49.0/176.0
|
205
|
+
a65 = -5103.0/18656.0
|
206
|
+
a71 = 35.0/384.0
|
207
|
+
a72 = 0.0
|
208
|
+
a73 = 500.0/1113.0
|
209
|
+
a74 = 125.0/192.0
|
210
|
+
a75 = -2187.0/6784.0
|
211
|
+
a76 = 11.0/84.0
|
212
|
+
|
213
|
+
c2 = 1.0 / 5.0
|
214
|
+
c3 = 3.0 / 10.0
|
215
|
+
c4 = 4.0 / 5.0
|
216
|
+
c5 = 8.0 / 9.0
|
217
|
+
c6 = 1.0
|
218
|
+
c7 = 1.0
|
219
|
+
|
220
|
+
b1order5 = 35.0/384.0
|
221
|
+
b2order5 = 0.0
|
222
|
+
b3order5 = 500.0/1113.0
|
223
|
+
b4order5 = 125.0/192.0
|
224
|
+
b5order5 = -2187.0/6784.0
|
225
|
+
b6order5 = 11.0/84.0
|
226
|
+
b7order5 = 0.0
|
227
|
+
|
228
|
+
b1order4 = 5179.0/57600.0
|
229
|
+
b2order4 = 0.0
|
230
|
+
b3order4 = 7571.0/16695.0
|
231
|
+
b4order4 = 393.0/640.0
|
232
|
+
b5order4 = -92097.0/339200.0
|
233
|
+
b6order4 = 187.0/2100.0
|
234
|
+
b7order4 = 1.0/40.0
|
235
|
+
|
236
|
+
@x[0] = @xmin
|
237
|
+
@u[0] = @yini
|
238
|
+
@fx[0] = self.f(@x[0], @u[0])
|
239
|
+
h = @dx
|
240
|
+
i = 0
|
241
|
+
|
242
|
+
0.upto(@maxiter) do |iter|
|
243
|
+
# Compute the function values
|
244
|
+
k1 = @fx[i]
|
245
|
+
k2 = self.f(@x[i] + c2*h, @u[i] + h*(a21*k1))
|
246
|
+
k3 = self.f(@x[i] + c3*h, @u[i] + h*(a31*k1+a32*k2))
|
247
|
+
k4 = self.f(@x[i] + c4*h, @u[i] + h*(a41*k1+a42*k2+a43*k3))
|
248
|
+
k5 = self.f(@x[i] + c5*h, @u[i] + h*(a51*k1+a52*k2+a53*k3+a54*k4))
|
249
|
+
k6 = self.f(@x[i] + h, @u[i] + h*(a61*k1+a62*k2+a63*k3+a64*k4+a65*k5))
|
250
|
+
k7 = self.f(@x[i] + h, @u[i] + h*(a71*k1+a72*k2+a73*k3+a74*k4+a75*k5+a76*k6))
|
251
|
+
|
252
|
+
error = (b1order5 - b1order4)*k1 + (b3order5 - b3order4)*k3 + (b4order5 - b4order4)*k4 +
|
253
|
+
(b5order5 - b5order4)*k5 + (b6order5 - b6order4)*k6 + (b7order5 - b7order4)*k7
|
254
|
+
error = error.abs
|
255
|
+
|
256
|
+
# error control
|
257
|
+
if error < @tol then
|
258
|
+
@x[i+1] = @x[i] + h
|
259
|
+
@u[i+1] = @u[i] + h * (b1order5*k1 + b3order5*k3 + b4order5*k4 + b5order5*k5 + b6order5*k6)
|
260
|
+
@fx[i+1] = self.f(@x[i+1], @u[i+1])
|
261
|
+
i = i+1
|
262
|
+
end
|
263
|
+
|
264
|
+
delta = 0.84 * (@tol / error)**0.2
|
265
|
+
if delta <= 0.1 then
|
266
|
+
h = h * 0.1
|
267
|
+
elsif delta >= 4.0 then
|
268
|
+
h = h * 4.0
|
269
|
+
else
|
270
|
+
h = delta * h
|
271
|
+
end
|
272
|
+
|
273
|
+
# set h to the user specified maximal allowed value
|
274
|
+
h = @dx if h > @dx
|
275
|
+
|
276
|
+
if @x[i] >= @xmax then
|
277
|
+
break
|
278
|
+
elsif @x[i] + h > @xmax then
|
279
|
+
h = @xmax - @x[i]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
@mx = @x.length # Number of x steps
|
284
|
+
|
285
|
+
raise(RuntimeError, "Maximal number of iterations reached
|
286
|
+
before evaluation of the solution on the entire x interval
|
287
|
+
was completed (try to increase maxiter or use a different method") if @x.last < @xmax
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# Copyright (c) 2015 Alexej Gossmann
|
2
|
+
|
3
|
+
module Spitzy
|
4
|
+
# Numerically solves the 2D Poisson's equation:
|
5
|
+
#
|
6
|
+
# * "Laplacian of u" = d^2 u / d^2 x + d^2 u / d^2 y = f(x,y),
|
7
|
+
# * on the rectangular domain [xmin,xmax] x [ymin,ymax],
|
8
|
+
# * with Dirichlet boundary conditions: u(x,y) = v(x,y) if
|
9
|
+
# (x,y) is on the boundary of the rectangular domain.
|
10
|
+
#
|
11
|
+
class PoissonsEq
|
12
|
+
|
13
|
+
# array of the x coordinates of points in the domain at which the numerical solution was evaluated
|
14
|
+
attr_reader :x
|
15
|
+
# array of the y coordinates of points in the domain at which the numerical solution was evaluated
|
16
|
+
attr_reader :y
|
17
|
+
# number of points on the side of the rectangular domain in x direction
|
18
|
+
attr_reader :mx
|
19
|
+
# number of points on the side of the rectangular domain in y direction
|
20
|
+
attr_reader :my
|
21
|
+
# step size in x and y
|
22
|
+
attr_reader :h
|
23
|
+
# the matrix of the solved linear system
|
24
|
+
attr_reader :mat
|
25
|
+
# the right hand side of the solved linear system
|
26
|
+
attr_reader :rhs
|
27
|
+
# the numerical scheme applied
|
28
|
+
attr_reader :method
|
29
|
+
# the numerical solution as an array (the values are ordered as in +x+ and +y+). It is obtained as a solution to a linear system.
|
30
|
+
attr_reader :u
|
31
|
+
|
32
|
+
# Constructor for all solver routines for the 2D Poisson's equation:
|
33
|
+
#
|
34
|
+
# * "Laplacian of u" = d^2 u / d^2 x + d^2 u / d^2 y = f(x,y),
|
35
|
+
# * on the rectangular domain [xmin,xmax] x [ymin,ymax],
|
36
|
+
# * with Dirichlet boundary conditions: u(x,y) = v(x,y) if
|
37
|
+
# (x,y) is on the boundary of the rectangular domain.
|
38
|
+
#
|
39
|
+
# It initializes the parameters and solves the equation
|
40
|
+
# using one of the methods:
|
41
|
+
# :five_pt (5-point Laplacian), :nine_pt (9-point Laplacian)
|
42
|
+
#
|
43
|
+
# ==== Arguments
|
44
|
+
#
|
45
|
+
# * +xrange+ - An array of the form [xmin, xmax]. Defines the domain
|
46
|
+
# on which the solution will be evaluated.
|
47
|
+
#
|
48
|
+
# * +yrange+ - An array of the form [ymin, ymax]. Defines the domain
|
49
|
+
# on which the solution will be evaluated.
|
50
|
+
#
|
51
|
+
# * +h+ - The step size in x and y direction. It should be chosen such that
|
52
|
+
# subintervals of length +h+ cover the entire x range as well as the
|
53
|
+
# entire y range.
|
54
|
+
#
|
55
|
+
# * +method+ - The numerical scheme used to solve the differential equation. Possible values are:
|
56
|
+
# +:five_pt+ (5-point Laplacian), +:nine_pt+ (9-point Laplacian)
|
57
|
+
# Default is +:five_pt+.
|
58
|
+
#
|
59
|
+
# * +bc+ - A +Proc+ or +Numeric+ object. The boundary condition v(x,y) of the differential
|
60
|
+
# equation, which can be supplied as a +Proc+ object. If a +Numeric+ object is
|
61
|
+
# supplied then v(x,y) is assumed to be constant equal to that number.
|
62
|
+
#
|
63
|
+
# * +f+ - A +Proc+ or +Numeric+ object. The right hand side f(x,y) of the differential
|
64
|
+
# equation, which can be supplied as a +Proc+ object. If a +Numeric+ object is
|
65
|
+
# supplied then f(x,y) is assumed to be constant equal to that number.
|
66
|
+
#
|
67
|
+
# === Usage
|
68
|
+
#
|
69
|
+
# f = Proc.new { |x,y| Math::exp(-0.5*(x**2.0 + y**2.0)) * (x**2.0 + y**2.0 - 2.0) }
|
70
|
+
# bc = Proc.new { |x,y| Math::exp(-0.5*(x**2.0 + y**2.0)) }
|
71
|
+
# numsol = Spitzy::PoissonsEq.new(xrange: [-1.0,1.0], yrange: [-5.0, 5.0], h: 0.2, bc: bc, f: f)
|
72
|
+
#
|
73
|
+
def initialize(xrange:, yrange:, h:, method: :five_pt, bc:, f:)
|
74
|
+
raise(ArgumentError, "Expected xrange to be an array of length 2") unless xrange.length == 2
|
75
|
+
raise(ArgumentError, "Expected yrange to be an array of length 2") unless yrange.length == 2
|
76
|
+
@h = h
|
77
|
+
@xmin, @xmax = xrange[0].to_f, xrange[1].to_f
|
78
|
+
@ymin, @ymax = yrange[0].to_f, yrange[1].to_f
|
79
|
+
|
80
|
+
xval = (@xmin..@xmax).step(@h).to_a
|
81
|
+
yval = (@ymin..@ymax).step(@h).to_a
|
82
|
+
@mx, @my = xval.length, yval.length
|
83
|
+
unless xval[-1]==@xmax and yval[-1]==@ymax then
|
84
|
+
raise(ArgumentError, "The step size +h+ should be chosen such that subintervals of
|
85
|
+
length +h+ cover the entire x range as well as the entire y range.")
|
86
|
+
end
|
87
|
+
x, y = NMatrix::meshgrid([xval, yval], indexing: :ij)
|
88
|
+
@x, @y = x.to_a, y.to_a
|
89
|
+
# interior points
|
90
|
+
@x_interior, @y_interior = NMatrix::meshgrid([xval[1..-2], yval[1..-2]], indexing: :ij)
|
91
|
+
|
92
|
+
if f.is_a? Proc then
|
93
|
+
@f = f
|
94
|
+
elsif f.is_a? Numeric then
|
95
|
+
@f = Proc.new { |x| f }
|
96
|
+
else
|
97
|
+
raise(ArgumentError, "Expected Numeric or Proc input for a")
|
98
|
+
end
|
99
|
+
|
100
|
+
if bc.is_a? Proc then
|
101
|
+
@bc = bc
|
102
|
+
elsif bc.is_a? Numeric then
|
103
|
+
@bc = Proc.new { |x| bc }
|
104
|
+
else
|
105
|
+
raise(ArgumentError, "Expected Numeric or Proc input for a")
|
106
|
+
end
|
107
|
+
|
108
|
+
@method = method
|
109
|
+
case @method
|
110
|
+
when :five_pt then five_pt
|
111
|
+
when :nine_pt then nine_pt
|
112
|
+
else raise(ArgumentError, "#{@method} is not a valid method.")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Evaluate the function f(x,y) at x=x0 and y=y0
|
117
|
+
#
|
118
|
+
# ==== Arguments
|
119
|
+
#
|
120
|
+
# * +x0+ - A floating point number
|
121
|
+
#
|
122
|
+
# * +y0+ - A floating point number
|
123
|
+
#
|
124
|
+
def f(x0, y0)
|
125
|
+
@f.call(x0, y0)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Evaluate the function bc(x,y) at x=x0 and y=y0
|
129
|
+
#
|
130
|
+
# ==== Arguments
|
131
|
+
#
|
132
|
+
# * +x0+ - A floating point number
|
133
|
+
#
|
134
|
+
# * +y0+ - A floating point number
|
135
|
+
#
|
136
|
+
def bc(x0, y0)
|
137
|
+
@bc.call(x0, y0)
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
######### Available numeric schemes ##########
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# Use the five-point Laplacian to solve the 2D Poisson's equation:
|
146
|
+
#
|
147
|
+
# * "Laplacian of u" = d^2 u / d^2 x + d^2 u / d^2 y = f(x,y),
|
148
|
+
# * on the rectangular domain [xmin,xmax] x [ymin,ymax],
|
149
|
+
# * with Dirichlet boundary conditions: u(x,y) = v(x,y) if
|
150
|
+
# (x,y) is on the boundary of the rectangular domain.
|
151
|
+
#
|
152
|
+
def five_pt
|
153
|
+
# Number of interior points
|
154
|
+
mx_interior = @mx-2
|
155
|
+
my_interior = @my-2
|
156
|
+
m_interior = mx_interior*my_interior
|
157
|
+
h2 = @h**2.0
|
158
|
+
|
159
|
+
# Construct the right hand side for the linear system (only required at interiour points)
|
160
|
+
@rhs = NMatrix.new([mx_interior, my_interior], dtype: :float64)
|
161
|
+
@x_interior.each_with_indices do |v,i,j|
|
162
|
+
@rhs[i,j] = self.f(v, @y_interior[i,j])
|
163
|
+
@rhs[i,j] -= self.bc(v, @ymin)/h2 if j==0
|
164
|
+
@rhs[i,j] -= self.bc(v, @ymax)/h2 if j==my_interior-1
|
165
|
+
@rhs[i,j] -= self.bc(@xmin, @y_interior[i,j])/h2 if i==0
|
166
|
+
@rhs[i,j] -= self.bc(@xmax, @y_interior[i,j])/h2 if i==mx_interior-1
|
167
|
+
end
|
168
|
+
@rhs = @rhs.transpose
|
169
|
+
@rhs.reshape!([m_interior, 1])
|
170
|
+
|
171
|
+
# Construct the matrix for the linear system
|
172
|
+
# TODO: the matrix is pentadiagonal (i.e. sparse) and should be stored accordingly
|
173
|
+
@mat = NMatrix.new([m_interior, m_interior], dtype: :float64)
|
174
|
+
(0..(m_interior-1)).each {|i| @mat[i,i] = -4.0 / h2 }
|
175
|
+
(0...(m_interior-1)).each {|i| @mat[i,i+1] = 1.0 / h2 }
|
176
|
+
(0...(m_interior-1)).each {|i| @mat[i+1,i] = 1.0 / h2 }
|
177
|
+
(0...(m_interior-mx_interior)).each {|i| @mat[i,i+mx_interior] = 1.0 / h2 }
|
178
|
+
(0...(m_interior-mx_interior)).each {|i| @mat[i+mx_interior,i] = 1.0 / h2 }
|
179
|
+
(1...my_interior).each do |i|
|
180
|
+
@mat[mx_interior*i-1, mx_interior*i] = 0
|
181
|
+
@mat[mx_interior*i, mx_interior*i-1] = 0
|
182
|
+
end
|
183
|
+
|
184
|
+
# Compute the solution at the interior points
|
185
|
+
# TODO: @mat is pentadiagonal, and consequently, there are more
|
186
|
+
# efficient ways to solve the linear system (e.g. successive over-relaxation)
|
187
|
+
u_interior = @mat.solve(@rhs)
|
188
|
+
# x corresponds to columns, y corresponds to rows
|
189
|
+
u = NMatrix.new([@mx,@my], dtype: :float64)
|
190
|
+
# enforce boundary considitions
|
191
|
+
(0...@mx).each { |i| u[i,0] = self.bc(@x[i][0],@y[i][0]) }
|
192
|
+
(0...@mx).each { |i| u[i,-1] = self.bc(@x[i][-1],@y[i][-1]) }
|
193
|
+
(0...@my).each { |j| u[0,j] = self.bc(@x[0][j],@y[0][j]) }
|
194
|
+
(0...@my).each { |j| u[-1,j] = self.bc(@x[-1][j],@y[-1][j]) }
|
195
|
+
# populate the interior
|
196
|
+
1.upto(@my-2) do |j|
|
197
|
+
1.upto(@mx-2) do |i|
|
198
|
+
u[i,j] = u_interior[(j-1)*mx_interior+(i-1)]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
@u = u.to_a
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
data/spitzy.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'spitzy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "spitzy"
|
8
|
+
spec.version = Spitzy::VERSION
|
9
|
+
spec.authors = ["Alexej Gossmann"]
|
10
|
+
spec.email = ["alexej.go@googlemail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Solve differential equations in pure Ruby.}
|
13
|
+
spec.description = %q{A toolbox of numerical differential equation solvers written in pure Ruby. Currently there are multiple methods available to solve initial value ODEs (Dormand-Prince, Forward Euler, 2nd order Adams-Bashforth), boundary value ODEs (Linear Finite Element Galerkin), 2D Poisson's equation (5-point Laplacian), and the 1D advection equation (Upwind, Lax-Friedrichs, Leapfrog, Lax-Wendroff).}
|
14
|
+
spec.homepage = "https://github.com/agisga/spitzy.git"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "nmatrix", "~> 0.2.0"
|
27
|
+
spec.add_runtime_dependency "nmatrix-lapacke", "~> 0.2.0"
|
28
|
+
|
29
|
+
# This gem will work with Ruby 2.1 or newer, because it uses required
|
30
|
+
# keyword arguments (i.e. keyword arguments without default values)
|
31
|
+
spec.required_ruby_version = '>= 2.1'
|
32
|
+
end
|
data/spitzy.jpg
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spitzy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexej Gossmann
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-13 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.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
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: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nmatrix
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.2.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.2.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: nmatrix-lapacke
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.2.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.2.0
|
83
|
+
description: A toolbox of numerical differential equation solvers written in pure
|
84
|
+
Ruby. Currently there are multiple methods available to solve initial value ODEs
|
85
|
+
(Dormand-Prince, Forward Euler, 2nd order Adams-Bashforth), boundary value ODEs
|
86
|
+
(Linear Finite Element Galerkin), 2D Poisson's equation (5-point Laplacian), and
|
87
|
+
the 1D advection equation (Upwind, Lax-Friedrichs, Leapfrog, Lax-Wendroff).
|
88
|
+
email:
|
89
|
+
- alexej.go@googlemail.com
|
90
|
+
executables: []
|
91
|
+
extensions: []
|
92
|
+
extra_rdoc_files: []
|
93
|
+
files:
|
94
|
+
- ".gitignore"
|
95
|
+
- ".rspec"
|
96
|
+
- ".travis.yml"
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- bin/console
|
102
|
+
- bin/setup
|
103
|
+
- lib/spitzy.rb
|
104
|
+
- lib/spitzy/advection_eq.rb
|
105
|
+
- lib/spitzy/bvp.rb
|
106
|
+
- lib/spitzy/ode.rb
|
107
|
+
- lib/spitzy/poissons_eq.rb
|
108
|
+
- lib/spitzy/version.rb
|
109
|
+
- spitzy.gemspec
|
110
|
+
- spitzy.jpg
|
111
|
+
homepage: https://github.com/agisga/spitzy.git
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '2.1'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.4.5
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Solve differential equations in pure Ruby.
|
135
|
+
test_files: []
|
136
|
+
has_rdoc:
|