- It is not ready for any practical use yet
- When/if it is finished it will still only deal with a particular class of PDEs
- It will use finite-volume methods on rectangular grids, so the PDE must be fit for such a treatment
- It will be limited to one temporal and two spatial dimensions for practical reasons
- You will be able to deal with systems of PDEs
- Both Dirichlet and Neumann boundary conditions are supported
- It will easy to define the particular PDEs
du d2u d2u du du
---- = f(x,y,u) * --- + g(x,y,u) * --- + h(x,y,u) * ---- + j(x,y,u) ---- + k(x,y,u)
dt dx2 dy2 dx dywhere u is the dependent variable or a set of such variables.Lars H: Is there a reason for not allowing mixed second order derivatives? Even if they're not used in the basic PDEs, I think they tend to pop out if you have to do a nontrivial change of variables.AM Hm, you are right about that. Mixed derivatives do have a tendency though to mess up the discretisation. I will need to think about it. They will certainly even be in the basic PDE if there is anisotropy.Without further ado, here is the code I have so far (limited to 1D, no first-order derivatives yet). It relies on NAP for the heavy-duty computations.
# pde.tcl --
# Implementation of a basic solver for partial differential
# equations (PDE), using NAP
#
# This solver is meant for quasilinear parabolic PDEs, but
# it can handle systems of PDes. Via a simple transformation
# a hyperbolic equation, like the wave equation, can be
# written as a system of two or more equations that are first-order
# in the time.
#
# The current implementation is limited to equidistant grids in
# one spatial dimension. The types of boundary conditions are
# - Dirichlet (value given at the boundary)
# - Neumann (value of the flux given at the boundary)
# The same type is assumed for all equations, but the boundaries
# are independent.
#
# Because of the amount of information involved in solving PDEs
# we store everything in a separate namespace (this is partly
# required because of the properties of NAP). This is one of the
# tasks taken care of in the initialisation phase.
#
# TODO:
# nextTime
# getValues
#
package require nap
namespace eval PDE {
namespace import ::NAP::*
variable pdeCount 0
#
# Public procedures
#
namespace export initPde1D boundary timestep values initial
}
# initPde1D --
# Create a namespace to solve a one-dimensional PDE
# Arguments:
# None
# Result:
# The name of the namespace dedicated to the PDE
# Note:
# The result is required for accessing the PDE solution system.
# Pass it as the first argument.
#
proc ::PDE::initPde1D {} {
variable pdeCount
namespace eval $pdeCount [list \
variable _dimension 1 \
variable _pde ::PDE::$pdeCount \
]
set ns "::PDE::$pdeCount"
incr pdeCount
return $ns
}
# timestep --
# Set the value of the time step for integration
# Arguments:
# pde Name of the PDE solution system
# deltt Time step to be used
# Result:
# None
#
proc ::PDE::timestep {pde deltt} {
set [set pde]::_deltt $deltt
}
# printPde --
# Print the PDE information array
# Arguments:
# pde Name of the array holding all information
# Result:
# None
# Side effects:
# Prints the information contained in the array on stdout
#
proc ::PDE::printPde {pde} {
set [set pde]::_pde $pde
namespace eval $pde {
puts "PDE: $_pde"
puts " -- grid coordinates (cell boundaries) --"
puts "[$_grid value]"
puts " -- volumes of cells --"
puts "[$_volume value]"
puts " -- grid distances --"
puts "[$_dx value]"
if { [info exists _statevars] } {
puts " -- state variables --"
foreach v $_statevars {
puts "$v: [[set $v] value]"
}
}
}
}
# values --
# Return the values of a state variables as a list
# Arguments:
# pde Name of the array holding all information
# name Name of the state variable
# Result:
# None
#
proc ::PDE::values {pde name} {
::PDE::HasParameter $_pde $name
set [set pde]::_name $name
namespace eval $pde {
return [[set $_name] value]
}
}
# HasParameter --
# Check that a correct parameter name is given
# Arguments:
# pde Name of the PDE system
# name Parameter name (empty: any parameter)
# hasvalue Does it have a value? (optional)
# Result:
# 1 if there is a parameter by that name, error otherwise
#
proc ::PDE::HasParameter {pde name {hasvalue 0}} {
set [set pde]::_name $name
set [set pde]::_hasvalue $hasvalue
namespace eval $pde {
if { ! [info exists _statevars] } {
return -code error "No state variables defined yet"
}
if { $_name != {} } {
if { [lsearch $_statevars $_name] < 0 } {
return -code error "No such state variable: $_name"
}
if { $_hasvalue && ! [info exists $_name] } {
return -code error "State variable $_name has no values yet"
}
}
}
}
# HasGrid --
# Check that the PDE system has a grid
# Arguments:
# pde Name of the PDE system
# Result:
# 1 if there is a grid, error otherwise
#
proc ::PDE::HasGrid {pde} {
namespace eval $pde {
if { ! [info exists _grid] } {
return -code error "No grid defined yet"
}
}
}
# boundary --
# Set the type and value for the boundaries
# Arguments:
# pde Name of the array holding all information
# name Which parameter
# bound Which boundary (left, right, top or bottom)
# type Type of condition (dirichlet or neumann)
# value Value of the variable or the derivative
# Result:
# None
#
proc ::PDE::boundary {pde name bound type value} {
switch -re $bound {
"l|left" { set b left }
"r|right" { set b right }
"t|top" { set b top }
"b|bottom" { set b bottom }
default {
return -code error "Boundary must be left, right, top or bottom"
}
}
switch -re $type {
"d|dirichlet" { set t dirichlet }
"n|neumann" { set n neumann }
default {
return -code error "Type must be dirichlet or neumann"
}
}
set [set pde]::_btype($b,$name) $t
set [set pde]::_bvalue($b,$name) $value
}
# SetBoundaryValues --
# Set the correct values for the boundary elements
# Arguments:
# pde Name of the PDE system
# Result:
# None
#
proc ::PDE::SetBoundaryValues {pde} {
#
# TODO: Neumann boundaries!
#
namespace eval $pde {
set _maxcellp1 [expr {$_maxcell + 1}]
foreach _v $_statevars {
foreach _b {left right} _idx [list 0 $_maxcellp1] {
if { $_btype($_b,$_v) == "dirichlet" } {
[set $_v] set value $_bvalue($_b,$_v) $_idx
}
}
}
}
}
# initial --
# Set the initial value for a particular state variable
# Arguments:
# pde Name of the array holding all information
# name Which variable
# value Initial value
# Result:
# None
# Notes:
# Useful only when the grid has been defined and the state variables
#
proc ::PDE::initial {pde name value} {
set [set pde]::_name $name
set [set pde]::_value $value
HasParameter $pde $name
HasGrid $pde
namespace eval $pde {
set $_name $_value
nap "$_name = $_value + 0.0 * $_volume"
}
}
# zeroDerivs --
# Set the derivatives for all state variables to zero
# Arguments:
# pde Name of the array holding all information
# Result:
# None
#
proc ::PDE::zeroDerivs {pde} {
HasParameter $pde {}
HasGrid $pde
namespace eval $pde {
foreach name $_statevars {
nap "_d_$name = 0.0 * $_volume"
}
}
}
# addTerm --
# Add a term to the derivative of a particular state variable
# Arguments:
# pde Name of the array holding all information
# name Which variable
# term The value in each cell to be added
# Result:
# None
#
proc ::PDE::addTerm {pde name term} {
HasParameter $pde $name 1
HasGrid $pde
set [set pde]::_name $name
set [set pde]::_term $term
namespace eval $pde {
[set _d_$_name] \
set value [string map [list Name $_name] "_d_Name(1 .. _maxcell) + _term"] \
"(1 .. _maxcell)"
}
}
# addSecondDeriv --
# Compute the second order derivative of a particular state variable,
# multiplied by a factor (for convenience)
# Arguments:
# pde Name of the array holding all information
# name Which variable
# factor Expression for multiplication factor
# dname Which variable to take the derivative of
# Result:
# None
#
proc ::PDE::addSecondDeriv {pde name factor dname} {
set [set pde]::_name $name
set [set pde]::_dname $dname
set [set pde]::_factor $factor
HasParameter $pde $name 1
HasParameter $pde $dname 1
HasGrid $pde
SetBoundaryValues $pde
namespace eval $pde {
set _maxcellm1 [expr {$_maxcell-1}]
nap [string map [list Name $_dname] \
"_term1 = (Name(1 .. _maxcell) - Name(0 .. _maxcellm1)) /
_dx(1 .. _maxcell)"]
nap [string map [list Name $_dname] \
"_term2 = (Name(2 .. _nocells) - Name(1 .. _maxcell)) /
_dx(2 .. _nocells)"]
nap "_term = $_factor * (_term2 - _term1)"
[set _d_$_name] \
set value [string map [list Name $_name] "_d_Name(1 .. _maxcell) + _term"] \
"(1 .. _maxcell)"
}
}
# nextStep --
# Compute for all state variables the values in the next time step
# Arguments:
# pde Name of the array holding all information
# Result:
# None
#
proc ::PDE::nextStep {pde} {
HasParameter $pde {} 0
HasGrid $pde
namespace eval $pde {
foreach _v $_statevars {
nap "$_v = $_v + $_deltt * _d_$_v"
}
}
}
# statevars --
# Set the names of the state variables
# Arguments:
# pde Name of the array holding all information
# args Names of the variables
# Result:
# None
#
proc ::PDE::statevars {pde args} {
foreach name $args {
if { [string index $name 0] == "_" } {
return -code error "Names starting with an underscore are reserved - do not use them for state variables"
}
}
set [set pde]::_statevars $args
}
# grid1D --
# Create a one-dimensional grid
# Arguments:
# pde Name of the array holding all information
# nocells Number of grid cells
# xmin Minimum x-coordinate (left boundary)
# xmax Maximum x-coordinate (right boundary)
# Result:
# None
#
proc ::PDE::grid1D {pde nocells xmin xmax} {
#
# Create a namespace to hold the NAP variables
#
namespace eval $pde {
namespace import ::NAP::nap
}
#
# Check the consistency
#
if { $nocells < 1 } {
return -code error "Number of cells must be at least 1"
}
if { $xmin >= $xmax } {
return -code error "Minimum x-coordinate must be smaller than maximum"
}
set [set pde]::_nocells $nocells
set [set pde]::_maxcell [expr {$nocells-1}]
set [set pde]::_xmax $xmax
set [set pde]::_xmin $xmin
namespace eval $pde {
set _deltx [expr {($_xmax-$_xmin)/double($_nocells)}]
nap "_grid = _xmin + _deltx * (0 .. _nocells)"
nap "_dx = _deltx + 0.0 * (0 .. _maxcell)"
nap "_volume = _deltx + 0.0 * (0 .. _maxcell)"
}
}
# main --
# Testing:
# - Generation of the grid must work properly
#
set g [::PDE::initPde1D]
::PDE::grid1D $g 20 -10 10
::PDE::statevars $g u
::PDE::initial $g u 2.1
::PDE::boundary $g u left dirichlet 0
::PDE::boundary $g u right dirichlet 0
::PDE::timestep $g 0.1
::PDE::printPde $g
set i 0
while { $i < 10 } {
::PDE::zeroDerivs $g
::PDE::addTerm $g u 1.0
::PDE::addSecondDeriv $g u 1.0 u
::PDE::nextStep $g
incr i
}
::PDE::printPde $g
