# machineparameters.tcl --
#   Compute double precision machine parameters.
#
# Description
#   This the Tcl equivalent of the DLAMCH LAPCK function.
#   In floating point systems, a floating point number is represented
#   by
#   x = +/- d1 d2 ... dt basis^e
#   where digits satisfy
#   0 <= di <= basis - 1, i = 1, t
#   with the convention :
#   - t is the size of the mantissa
#   - basis is the basis (the "radix")
#
# References
#
#   "Algorithms to Reveal Properties of Floating-Point Arithmetic"
#   Michael A. Malcolm
#   Stanford University
#   Communications of the ACM
#   Volume 15 ,  Issue 11  (November 1972)
#   Pages: 949 - 951
#
#   "More on Algorithms that Reveal Properties of Floating
#   Point Arithmetic Units"
#   W. Morven Gentleman, University of Waterloo
#   Scott B. Marovich, Purdue University
#   Communications of the ACM
#   Volume 17 ,  Issue 5  (May 1974)
#   Pages: 276 - 277
#
# Example
#
#   In the following example, one compute the parameters of a desktop
#   under Linux with the following Tcl 8.4.19 properties :
#
#% parray tcl_platform
#tcl_platform(byteOrder) = littleEndian
#tcl_platform(machine)   = i686
#tcl_platform(os)        = Linux
#tcl_platform(osVersion) = 2.6.24-19-generic
#tcl_platform(platform)  = unix
#tcl_platform(tip,268)   = 1
#tcl_platform(tip,280)   = 1
#tcl_platform(user)      = <username>
#tcl_platform(wordSize)  = 4
#
#   The following example creates a machineparameters object,
#   computes the properties and displays it.
#
#     set pp [machineparameters create %AUTO%]
#     $pp compute
#     $pp print
#     $pp destroy
#
#   This prints out :
#
#     Machine parameters
#     Epsilon : 1.11022302463e-16
#     Beta : 2
#     Rounding : proper
#     Mantissa : 53
#     Maximum exponent : 1024
#     Minimum exponent : -1021
#     Overflow threshold : 8.98846567431e+307
#     Underflow threshold : 2.22507385851e-308
#
#   That compares well with the results produced by Lapack 3.1.1 :
#
#     Epsilon                      =   1.11022302462515654E-016
#     Safe minimum                 =   2.22507385850720138E-308
#     Base                         =    2.0000000000000000
#     Precision                    =   2.22044604925031308E-016
#     Number of digits in mantissa =    53.000000000000000
#     Rounding mode                =   1.00000000000000000
#     Minimum exponent             =   -1021.0000000000000
#     Underflow threshold          =   2.22507385850720138E-308
#     Largest exponent             =    1024.0000000000000
#     Overflow threshold           =   1.79769313486231571E+308
#     Reciprocal of safe minimum   =   4.49423283715578977E+307
#
# Copyright 2008 Michael Baudin
#
package require snit
package provide math::machineparameters 0.1

snit::type machineparameters {
  # Epsilon is the smallest value so that 1+epsilon>1 is false
  variable epsilon 0
  # basis is the basis of the floating-point representation.
  # basis is usually 2, i.e. binary representation (for example IEEE 754 machines),
  # but some machines (like HP calculators for example) uses 10, or 16, etc...
  variable basis 0
  # The rounding mode used on the machine.
  # The rounding occurs when more than t digits would be required to
  # represent the number.
  # Two modes can be determined with the current system :
  # "chop" means than only t digits are kept, no matter the value of the number
  # "proper" means that another rounding mode is used, be it "round to nearest",
  #   "round up", "round down".
  variable rounding ""
  # the size of the mantissa
  variable mantissa 0
  # The first non-integer is A = 2^m with m is the
  # smallest positive integer so that fl(A+1)=A
  variable firstnoninteger 0
  # Maximum number of iterations in loops
  option -maxiteration 10000
  # Set to 1 to enable verbose logging
  option -verbose -default 0
  # The largest positive exponent before overflow occurs
  variable exponentmax 0
  # The largest negative exponent before (gradual) underflow occurs
  variable exponentmin 0
  # Largest positive value before overflow occurs
  variable vmax
  # Largest negative value before (gradual) underflow occurs
  variable vmin
  #
  # compute --
  #   Computes the machine parameters.
  #
  method compute {} {
    $self log "compute"
    $self computeepsilon
    $self computefirstnoninteger
    $self computebasis
    $self computerounding
    $self computemantissa
    $self computeemax
    $self computeemin
    return ""
  }
  #
  # computeepsilon --
  #   Find epsilon the minimum value for which 1.0 + epsilon > 1.0
  #
  method computeepsilon {} {
    $self log "computeepsilon"
    set factor 2.
    set epsilon 0.5
    for {set i 0} {$i<$options(-maxiteration)} {incr i} {
      $self log "$i/$options(-maxiteration) : $epsilon"
      set epsilon [expr {$epsilon / $factor}]
      set inequality [expr {1.0+$epsilon>1.0}]
      if {$inequality==0} then {
        break
      }
    }
    $self log "epsilon : $epsilon (after $i loops)"
    return ""
  }
  #
  # computefirstnoninteger --
  #   Compute the first positive non-integer real.
  #   It is the smallest a such that (a+1)-a is different from 1
  #
  method computefirstnoninteger {} {
    $self log "computefirstnoninteger"
    set firstnoninteger 2.
    for {set i 0} {$i < $options(-maxiteration)} {incr i} {
      $self log "$i/$options(-maxiteration) : $firstnoninteger"
      set firstnoninteger [expr {2.*$firstnoninteger}]
      set one [expr {($firstnoninteger+1.)-$firstnoninteger}]
      if {$one!=1.} then {
        break
      }
    }
    $self log "Found firstnoninteger : $firstnoninteger"
    return ""
  }
  #
  # computebasis --
  #   Compute the basis (basis)
  #
  method computebasis {} {
    $self log "computebasis"
    #
    # Compute b where b is the smallest real so that fl(a+b)> a,
    # where a is the first non integer.
    # Note :
    #  With floating point numbers, a+1==a !
    #  b is denoted by "B" in Malcolm's algorithm
    #
    set b 1
    for {set i 0} {$i < $options(-maxiteration)} {incr i} {
      $self log "$i/$options(-maxiteration) : $b"
      set basis [expr {int(($firstnoninteger+$b)-$firstnoninteger)}]
      if {$basis!=0.} then {
        break
      }
      incr b
    }
    $self log "Found basis : $basis"
    return ""
  }
  #
  # computerounding --
  #   Compute the rounding mode.
  # Note:
  #   This corresponds to DLAMCH implementation (DLAMC1 exactly).
  #
  method computerounding {} {
    $self log "computerounding"
    # Now determine whether rounding or chopping occurs,  by adding a
    # bit  less  than  beta/2  and a  bit  more  than  beta/2  to  a (=firstnoninteger).
    set F [expr {$basis/2.0 - $basis/100.0}]
    set C [expr {$F + $firstnoninteger}]
    if {$C==$firstnoninteger} then {
      set rounding "proper"
    } else {
      set rounding "chop"
    }
    set F [expr {$basis/2.0 + $basis/100.0}]
    set C [expr {$F + $firstnoninteger}]
    if {$rounding=="proper" && $C==$firstnoninteger} then {
      set rounding "chop"
    }
    $self log "Found rounding : $rounding"
    return ""
  }
  #
  # computemantissa --
  #   Compute the mantissa size
  #
  method computemantissa {} {
    $self log "computemantissa"
    set a 1.
    set mantissa 0
    for {set i 0} {$i < $options(-maxiteration)} {incr i} {
      incr mantissa
      $self log "$i/$options(-maxiteration) : $mantissa"
      set a [expr {$a * double($basis)}]
      set one [expr {($a+1)-$a}]
      if {$one!=1.} then {
        break
      }
    }
    $self log "Found mantissa : $mantissa"
    return ""
  }
  #
  # computeemax --
  #   Compute the maximum exponent before overflow
  #
  method computeemax {} {
    $self log "computeemax"
    set vmax 1.
    set exponentmax 1
    for {set i 0} {$i < $options(-maxiteration)} {incr i} {
      $self log "Iteration #$i , exponentmax = $exponentmax, vmax = $vmax"
      incr exponentmax
      # Condition #1 : no exception is generated
      set errflag [catch {
        set new [expr {$vmax * $basis}]
      }]
      if {$errflag!=0} then {
        break
      }
      # Condition #2 : one can recover the original number
      if {$new / $basis != $vmax} then {
        break
      }
      set vmax $new
    }
    incr exponentmax -1
    $self log "Exponent maximum : $exponentmax"
    $self log "Value maximum : $vmax"
    return ""
  }
  #
  # computeemin --
  #   Compute the minimum exponent before underflow
  #
  method computeemin {} {
    $self log "computeemin"
    set vmin 1.
    set exponentmin 1
    for {set i 0} {$i < $options(-maxiteration)} {incr i} {
      $self log "Iteration #$i , exponentmin = $exponentmin, vmin = $vmin"
      incr exponentmin -1
      # Condition #1 : no exception is generated
      set errflag [catch {
        set new [expr {$vmin / $basis}]
      }]
      if {$errflag!=0} then {
        break
      }
      # Condition #2 : one can recover the original number
      if {$new * $basis != $vmin} then {
        break
      }
      set vmin $new
    }
    incr exponentmin +1
    # See in DMALCH.f, DLAMC2 relative to IEEE machines.
    # TODO : what happens on non-IEEE machine ?
    set exponentmin [expr {$exponentmin - 1 + $mantissa}]
    set vmin [expr {$vmin * pow($basis,$mantissa-1)}]
    $self log "Exponent minimum : $exponentmin"
    $self log "Value minimum : $vmin"
    return ""
  }
  #
  # log --
  #   Puts the given message on standard output.
  #
  method log {msg} {
    if {$options(-verbose)==1} then {
      puts "(mp) $msg"
    }
    return ""
  }
  #
  # get --
  #   Return value for key
  #
  method get {key} {
    $self log "get $key"
    switch -- $key {
      -epsilon {
        set result $epsilon
      }
      -rounding {
        set result $rounding
      }
      -basis {
        set result $basis
      }
      -mantissa {
        set result $mantissa
      }
      -exponentmax {
        set result $exponentmax
      }
      -exponentmin {
        set result $exponentmin
      }
      -vmax {
        set result $vmax
      }
      -vmin {
        set result $vmin
      }
      default {
        error "Unknown key $key"
      }
    }
    return $result
  }
  #
  # print --
  #   Print machine parameters on standard output
  #
  method print {} {
    set str [$self tostring]
    puts "$str"
    return ""
  }
  #
  # tostring --
  #   Return a report for machine parameters
  #
  method tostring {} {
    set str ""
    append str "Machine parameters\n"
    append str  "Epsilon : $epsilon\n"
    append str "Basis : $basis\n"
    append str "Rounding : $rounding\n"
    append str "Mantissa : $mantissa\n"
    append str "Maximum exponent before overflow : $exponentmax\n"
    append str "Minimum exponent before underflow : $exponentmin\n"
    append str "Overflow threshold : $vmax\n"
    append str "Underflow threshold : $vmin\n"
    return $str
  }
}
