spitzy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spitzy.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Spitzy
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
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: