spitzy 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/.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
|
+
[![Build Status](https://travis-ci.org/agisga/mixed_models.svg?branch=master)](https://travis-ci.org/agisga/mixed_models)
|
4
|
+
|
5
|
+
![Spitzy](spitzy.jpg?raw=true "Optional Title")
|
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:
|