spitzy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: