#----------------------------------------------------------------------
#
# math/combinatorics.tcl --
#
# This file contains definitions of mathematical functions
# useful in combinatorial problems.
#
# Copyright (c) 2001, by Kevin B. Kenny. All rights reserved.
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
#
# RCS: @(#) $Id: combinatorics.tcl,v 1.5 2004/02/09 19:31:54 hobbs Exp $
#
#----------------------------------------------------------------------
package require Tcl 8.0
namespace eval ::math {
# Commonly used combinatorial functions
# ln_Gamma is spelt thus because it's a capital gamma (\u0393)
namespace export ln_Gamma; # Logarithm of the Gamma function
namespace export factorial; # Factorial
namespace export choose; # Binomial coefficient
# Note that Beta is spelt thus because it's conventionally a
# capital beta (\u0392). It is exported from the package even
# though its name is capitalized.
namespace export Beta; # Beta function
}
#----------------------------------------------------------------------
#
# ::math::InitializeFactorial --
#
# Initialize a table of factorials for small integer arguments.
#
# Parameters:
# None.
#
# Results:
# None.
#
# Side effects:
# The variable, ::math::factorialList, is initialized to hold
# a table of factorial n for 0 <= n <= 170.
#
# This procedure is called once when the 'factorial' procedure is
# being loaded.
#
#----------------------------------------------------------------------
proc ::math::InitializeFactorial {} {
variable factorialList
set factorialList [list 1]
set f 1
for { set i 1 } { $i < 171 } { incr i } {
if { $i > 12. } {
set f [expr { $f * double($i)}]
} else {
set f [expr { $f * $i }]
}
lappend factorialList $f
}
}
#----------------------------------------------------------------------
#
# ::math::InitializePascal --
#
# Precompute the first few rows of Pascal's triangle and store
# them in the variable ::math::pascal
#
# Parameters:
# None.
#
# Results:
# None.
#
# Side effects:
# ::math::pascal is initialized to a flat list containing
# the first 34 rows of Pascal's triangle. C(n,k) is to be found
# at [lindex $pascal $i] where i = n * ( n + 1 ) + k. No attempt
# is made to exploit symmetry.
#
#----------------------------------------------------------------------
proc ::math::InitializePascal {} {
variable pascal
set pascal [list 1]
for { set n 1 } { $n < 34 } { incr n } {
lappend pascal 1
set l2 [list 1]
for { set k 1 } { $k < $n } { incr k } {
set km1 [expr { $k - 1 }]
set c [expr { [lindex $l $km1] + [lindex $l $k] }]
lappend pascal $c
lappend l2 $c
}
lappend pascal 1
lappend l2 1
set l $l2
}
}
#----------------------------------------------------------------------
#
# ::math::ln_Gamma --
#
# Returns ln(Gamma(x)), where x >= 0
#
# Parameters:
# x - Argument to the Gamma function.
#
# Results:
# Returns the natural logarithm of Gamma(x).
#
# Side effects:
# None.
#
# Gamma(x) is defined as:
#
# +inf
# _
# | x-1 -t
# Gamma(x)= _| t e dt
#
# 0
#
# The approximation used here is from Lanczos, SIAM J. Numerical Analysis,
# series B, volume 1, p. 86. For x > 1, the absolute error of the
# result is claimed to be smaller than 5.5 * 10**-10 -- that is, the
# resulting value of Gamma when exp( ln_Gamma( x ) ) is computed is
# expected to be precise to better than nine significant figures.
#
#----------------------------------------------------------------------
proc ::math::ln_Gamma { x } {
# Handle the common case of a real argument that's within the
# permissible range.
if { [string is double -strict $x]
&& ( $x > 0 )
&& ( $x <= 2.5563481638716906e+305 )
} {
set x [expr { $x - 1.0 }]
set tmp [expr { $x + 5.5 }]
set tmp [ expr { ( $x + 0.5 ) * log( $tmp ) - $tmp }]
set ser 1.0
foreach cof {
76.18009173 -86.50532033 24.01409822
-1.231739516 .00120858003 -5.36382e-6
} {
set x [expr { $x + 1.0 }]
set ser [expr { $ser + $cof / $x }]
}
return [expr { $tmp + log( 2.50662827465 * $ser ) }]
}
# Handle the error cases.
if { ![string is double -strict $x] } {
return -code error [expectDouble $x]
}
if { $x <= 0.0 } {
set proc [lindex [info level 0] 0]
return -code error \
-errorcode [list ARITH DOMAIN \
"argument to $proc must be positive"] \
"argument to $proc must be positive"
}
return -code error \
-errorcode [list ARITH OVERFLOW \
"floating-point value too large to represent"] \
"floating-point value too large to represent"
}
#----------------------------------------------------------------------
#
# math::factorial --
#
# Returns the factorial of the argument x.
#
# Parameters:
# x -- Number whose factorial is to be computed.
#
# Results:
# Returns x!, the factorial of x.
#
# Side effects:
# None.
#
# For integer x, 0 <= x <= 12, an exact integer result is returned.
#
# For integer x, 13 <= x <= 21, an exact floating-point result is returned
# on machines with IEEE floating point.
#
# For integer x, 22 <= x <= 170, the result is exact to 1 ULP.
#
# For real x, x >= 0, the result is approximated by computing
# Gamma(x+1) using the ::math::ln_Gamma function, and the result is
# expected to be precise to better than nine significant figures.
#
# It is an error to present x <= -1 or x > 170, or a value of x that
# is not numeric.
#
#----------------------------------------------------------------------
proc ::math::factorial { x } {
variable factorialList
# Common case: factorial of a small integer
if { [string is integer -strict $x]
&& $x >= 0
&& $x < [llength $factorialList] } {
return [lindex $factorialList $x]
}
# Error case: not a number
if { ![string is double -strict $x] } {
return -code error [expectDouble $x]
}
# Error case: gamma in the left half plane
if { $x <= -1.0 } {
set proc [lindex [info level 0] 0]
set message "argument to $proc must be greater than -1.0"
return -code error -errorcode [list ARITH DOMAIN $message] $message
}
# Error case - gamma fails
if { [catch { expr {exp( [ln_Gamma [expr { $x + 1 }]] )} } result] } {
return -code error -errorcode $::errorCode $result
}
# Success - computed factorial n as Gamma(n+1)
return $result
}
#----------------------------------------------------------------------
#
# ::math::choose --
#
# Returns the binomial coefficient C(n,k) = n!/k!(n-k)!
#
# Parameters:
# n -- Number of objects in the sampling pool
# k -- Number of objects to be chosen.
#
# Results:
# Returns C(n,k).
#
# Side effects:
# None.
#
# Results are expected to be accurate to ten significant figures.
# If both parameters are integers and the result fits in 32 bits,
# the result is rounded to an integer.
#
# Integer results are exact up to at least n = 34.
# Floating point results are precise to better than nine significant
# figures.
#
#----------------------------------------------------------------------
proc ::math::choose { n k } {
variable pascal
# Use a precomputed table for small integer args
if { [string is integer -strict $n]
&& $n >= 0 && $n < 34
&& [string is integer -strict $k]
&& $k >= 0 && $k <= $n } {
set i [expr { ( ( $n * ($n + 1) ) / 2 ) + $k }]
return [lindex $pascal $i]
}
# Test bogus arguments
if { ![string is double -strict $n] } {
return -code error [expectDouble $n]
}
if { ![string is double -strict $k] } {
return -code error [expectDouble $k]
}
# Forbid negative n
if { $n < 0. } {
set proc [lindex [info level 0] 0]
set msg "first argument to $proc must be non-negative"
return -code error -errorcode [list ARITH DOMAIN $msg] $msg
}
# Handle k out of range
if { [string is integer -strict $k] && [string is integer -strict $n]
&& ( $k < 0 || $k > $n ) } {
return 0
}
if { $k < 0. } {
set proc [lindex [info level 0] 0]
set msg "second argument to $proc must be non-negative,\
or both must be integers"
return -code error -errorcode [list ARITH DOMAIN $msg] $msg
}
# Compute the logarithm of the desired binomial coefficient.
if { [catch { expr { [ln_Gamma [expr { $n + 1 }]]
- [ln_Gamma [expr { $k + 1 }]]
- [ln_Gamma [expr { $n - $k + 1 }]] } } r] } {
return -code error -errorcode $::errorCode $r
}
# Compute the binomial coefficient itself
if { [catch { expr { exp( $r ) } } r] } {
return -code error -errorcode $::errorCode $r
}
# Round to integer if both args are integers and the result fits
if { $r <= 2147483647.5
&& [string is integer -strict $n]
&& [string is integer -strict $k] } {
return [expr { round( $r ) }]
}
return $r
}
#----------------------------------------------------------------------
#
# ::math::Beta --
#
# Return the value of the Beta function of parameters z and w.
#
# Parameters:
# z, w : Two real parameters to the Beta function
#
# Results:
# Returns the value of the Beta function.
#
# Side effects:
# None.
#
# Beta( w, z ) is defined as:
#
# 1_
# | (z-1) (w-1)
# Beta( w, z ) = Beta( z, w ) = | t (1-t) dt
# _|
# 0
#
# = Gamma( z ) Gamma( w ) / Gamma( z + w )
#
# Results are returned as a floating point number precise to better
# than nine significant figures for w, z > 1.
#
#----------------------------------------------------------------------
proc ::math::Beta { z w } {
# Check form of both args so that domain check can be made
if { ![string is double -strict $z] } {
return -code error [expectDouble $z]
}
if { ![string is double -strict $w] } {
return -code error [expectDouble $w]
}
# Check sign of both args
if { $z <= 0.0 } {
set proc [lindex [info level 0] 0]
set msg "first argument to $proc must be positive"
return -code error -errorcode [list ARITH DOMAIN $msg] $msg
}
if { $w <= 0.0 } {
set proc [lindex [info level 0] 0]
set msg "second argument to $proc must be positive"
return -code error -errorcode [list ARITH DOMAIN $msg] $msg
}
# Compute beta using gamma function, keeping stack trace clean.
if { [catch { expr { exp( [ln_Gamma $z] + [ln_Gamma $w]
- [ln_Gamma [ expr { $z + $w }]] ) } } beta] } {
return -code error -errorcode $::errorCode $beta
}
return $beta
}
#----------------------------------------------------------------------
#
# Initialization of this file:
#
# Initialize the precomputed tables of factorials and binomial
# coefficients.
#
#----------------------------------------------------------------------
namespace eval ::math {
InitializeFactorial
InitializePascal
}