Drawing

Introduction to SoftPosit

SoftPosit (C/C++) : https://gitlab.com/cerlane/SoftPosit

SoftPosit-Python : https://gitlab.com/cerlane/SoftPosit-Python

SoftFloat-Python: https://gitlab.com/cerlane/SoftFloat-Python

More information: https://posithub.org

Questions: contact@posithub.org

Prepared by: LEONG Siew Hoon (Cerlane)

Date: 31 Aug 2018

Introduction to Posits using SoftPosit-Python

Installation

pip install softposit --user

Part 1:

Importing SoftPosit library

In [1]:
import softposit as sp

In order to faciliate the comparison between Posits and Floats, please also install SoftFloat-Python.

Installation

pip install softfloat --user

Importing SoftFloat library

In [2]:
import softfloat as sf

1. Posit Types

The 3 recommended posit types are

Type Posit Size (ps) Exponent Size (es) Precisions
posit8 8 bits 0 bits ~3 decimal digits
posit16 16 bits 1 bits ~5 decimal digits
posit32 32 bits 2 bits ~10 decimal digits

There are two ways to set the values of a posit type.

(1) Assign decimal values in integers or floats to a posit variable. In event that there is no exact value to represent that value for the particular ps and es, the value will be rounded.

(2) Set the actual bits of the posit using decimal values in integers.

Note: Posits have only one rounding mode, i.e. round to nearest even bit (Banker's rounding).

In [3]:
#Different Standard Posit Types
p8=sp.posit8(0.3)
print("Posit8:", p8)
p8.toBinaryFormatted()

p16=sp.posit16(0.3)
print("\nPosit16:", p16)
p16.toBinaryFormatted()

p32=sp.posit32(0.3)
print("\nPosit32:", p32)
p32.toBinaryFormatted()
Posit8: 0.296875
00010011

Posit16: 0.29998779296875
00100011 00110011

Posit32: 0.30000000074505806
00110001 10011001 10011001 10011010
In [4]:
#Assign values by bits

print('Using Integers:')
p8.fromBits(6)
p8.toBinaryFormatted()


print('\nUsing Hex:')
p8.fromBits(6)
p8.toBinaryFormatted()

print('\nUsing Binary String:')
p8.fromBits(int("10110011", 2))
p8.toBinaryFormatted()
Using Integers:
00000110

Using Hex:
00000110

Using Binary String:
10110011
In [5]:
# Checking  Posit types

print("p8 type:", p8.type(), "\n")

print("p16 type:", p16.type(), "\n")

print("p32 type:", p32.type(), "\n")
p8 type: posit8 

p16 type: posit16 

p32 type: posit32 

2. Posit Format and 754 Float format

Bit Type Posit Float
Sign bit Yes (1-bit) Yes (1-bit)
Regime bits Yes
(1 to (ps-1) bits
No
Exponent bits Yes/No
Depending on posit type and regime length
Predeterminded (fixed) length
Yes
Depending on float (16/32/64) type
Predetermined (fixed) length
Fraction bits Yes/No
Depending on regime length
Dynamic length
Yes
Depending on float (16/32/64) type
Predetermined (fixed) length
2.1. Sign

Negating a posit is similar to negating an integer value, i.e. Performing a Two's complement. This is different from IEEE 754 Float where negating a float involves inverting only the sign bit.

2.1.1. Posit (16-bit Posit, es=1)

The nearest value to ± 0.3 for Posit16:

In [6]:
#Sign bit: positive and negative posits
p16=sp.posit16(0.3)
print("For a value of", p16, ":")
p16.toBinaryFormatted()

print("\nFor a value of", -p16, ":")
(-p16).toBinaryFormatted()
For a value of 0.29998779296875 :
00100011 00110011

For a value of -0.29998779296875 :
11011100 11001101
2.1.2. IEEE Float 754 (16-bit Float)

The nearest value to ± 0.3 for Float16:

In [7]:
#Sign bit: positive and negative posits
f16=sf.float16(0.3)
print("For a value of", f16, ":")
f16.toBinaryFormatted()

print("\nFor a value of", -f16, ":")
(-f16).toBinaryFormatted()
For a value of 0.300048828125 :
00110100 11001101

For a value of -0.300048828125 :
10110100 11001101
2.2. Regime

Regime bits is a continuous sequence of the same sign bit and is terminated by a different sign bit.

Regime length is dynamic and can be as long as the length of ps (i.e. ps-1).

Regime is the main reason why posits can have a bigger dynamic range than floats.

Note: Regime bits are a purely posit type of bits.

In [8]:
#Regime bits
print("posit16: 0.00390625")
p16 = sp.posit16(0.00390625);
p16.toBinaryFormatted()
print("Four 0-bit terminated by a 1-bit. This implies regime runlength is 4.\n")

print("posit16: -0.00390625")
p16 = sp.posit16(-0.00390625);
p16.toBinaryFormatted()
print("Five 1-bit terminated by a 0-bit. This implies regime runlength is 5.\n")

print("posit16: 268435456")
p16 = sp.posit16(268435456) #maxpos
p16.toBinaryFormatted()
print("Fifteen 1-bit without a terminating bit, exponent bit and fraction bits. Regime runlength is 15 (ps-1).")
posit16: 0.00390625
00000100 00000000
Four 0-bit terminated by a 1-bit. This implies regime runlength is 4.

posit16: -0.00390625
11111100 00000000
Five 1-bit terminated by a 0-bit. This implies regime runlength is 5.

posit16: 268435456
01111111 11111111
Fifteen 1-bit without a terminating bit, exponent bit and fraction bits. Regime runlength is 15 (ps-1).
2.3. Exponent

Exponent has a fixed size/length that is determined when the posit type is chosen.

In the case of posit8, posit16 and posit32, es is 0, 1 and 2 respectively.

As regime size is dynamic, the exponent bit(s) can be pushed out of the range of ps.

In [9]:
print("posit16: 4.76837158203125e-7")
p16 = sp.posit16(4.76837158203125e-7)
p16.toBinaryFormatted()

print("\nShift bit to the right 3 times:");
p16>>=3
p16.toBinaryFormatted()
print("Exponent bit is being pushed out.");
print("posit16: ", p16)


p32.fromBits(0x7)
print("\nposit32: ", p32)
p32.toBinaryFormatted()

print("\nShift bit to the right 1 time:");
p32>>=1
p32.toBinaryFormatted()
print("One exponent bit is being pushed out.");
print("posit32: ", p32)
posit16: 4.76837158203125e-7
00000000 00001100

Shift bit to the right 3 times:
00000000 00000001
Exponent bit is being pushed out.
posit16:  3.725290298461914e-09

posit32:  1.5407439555097887e-33
00000000 00000000 00000000 00000111

Shift bit to the right 1 time:
00000000 00000000 00000000 00000011
One exponent bit is being pushed out.
posit32:  4.81482486096809e-35
2.4. Fraction

The length of fraction is determined by regime and exponent.

The minimum length is 0 and the maximum length for posit8, posit16 and posit32 is 5, 12 and 27 bits (excluding the hidden bit).

As regime size is dynamic, the fraction bits can be pushed out of the range of ps.

In [10]:
p8.fromBits(0x3F)
print("Posit8:", p8)
p8.toBinaryFormatted()

p8.fromBits(0x1)
print("\nPosit8:", p8)
p8.toBinaryFormatted()
Posit8: 0.984375
00111111

Posit8: 0.015625
00000001

3. Special bit patterns:

3.1. Zero

Unlike IEEE 754, where there is $\pm 0$, posit has only one bit pattern to represents $0$.

IEEE 754 has two bit patterns to represent positive and negative zero respectively.

In [11]:
print("Float: Positive zero:")
f16 = sf.float16(+0.0)
f16.toBinaryFormatted()

print("\nFloat: Negative zero:")
f16 = sf.float16(-0.0)
f16.toBinaryFormatted()

print("\nPosit: Positive zero:")
p16 = sp.posit16(+0.0)
p16.toBinaryFormatted()

print("\nPosit: Negative zero:")
p16 = sp.posit16(-0.0)
p16.toBinaryFormatted()
Float: Positive zero:
00000000 00000000

Float: Negative zero:
10000000 00000000

Posit: Positive zero:
00000000 00000000

Posit: Negative zero:
00000000 00000000
3.2. NaR

Posit has only one bit pattern to represents infinity, undefined and unrepresented values.

$NaR$ -> 10000000 00000000

3.2.1. IEEE 754

IEEE 754 uses $NaN$ (Not-a-Number) to represent undefined and unrepresentable values.

  • sign = either 0 or 1.
  • exponent = all 1 bits.
  • fraction = anything except all 0 bits

Infinity

  • sign = either 0 or 1.
  • exponent = all 1 bits.
  • fraction = all 0 bits
In [12]:
#Posit
print("Posit: NaR")
p16.toNaR()
p16.toBinaryFormatted()

print("\nPosit: Divide by zero")
p16=sp.posit16(1)
p16/=0
p16.toBinaryFormatted()
print(p16)

print("\nPosit: Square root(negative number)")
p16=sp.posit16(-1)
p16.sqrt()
p16.toBinaryFormatted()
print(p16)

#Float
print("\nFloat: NaN - Divide by zero")
f16 = sf.float16(1)
f16/=0
f16.toBinaryFormatted()
print(f16)

print("\nFloat: NaN - Divide by zero")
f16 = sf.float16(-1)
f16/=0
f16.toBinaryFormatted()
print(f16)

print("\nFloat: Square root(negative number)")
f16 = sf.float16(-1)
f16.sqrt()
f16.toBinaryFormatted()
print(f16)


#try a different bit pattern for NaN
print("\n")
f16.fromBits(0xFE02)
f16.toBinaryFormatted()
print(f16)
Posit: NaR
10000000 00000000

Posit: Divide by zero
10000000 00000000
NaR

Posit: Square root(negative number)
10000000 00000000
NaR

Float: NaN - Divide by zero
01111100 00000000
inf

Float: NaN - Divide by zero
11111100 00000000
-inf

Float: Square root(negative number)
11111110 00000000
nan


11111110 00000010
nan

4. A little Math: Calculating the value of a posit

Although computers store all numbers as binary (base 2), most of us use decimal (base 10) to visual the value of a number.

To convert a posit from base 2 to base 10, the following has to be done:

$p = 0$,                                                                     if $\textit{bit}_0..\textit{bit}_{n-1} = 0$,

$p = \textit{NaR}$,                                                                if $\textit{bit}_0=1, \textit{bit}_1..\textit{bit}_{n-1} = 0$,

$p = (-1)^{\textit{sign}} \times \textit{useed}^k \times 2^e \times (1 + \frac{f}{2^\textit{fs}} )$,         otherwise (note that posit should be in 2's complement form if sign=1)

where
   $p$ is the posit value represented,
   $n$ is the posit size (total number of bits in the posit),
   $\textit{NaR}$ is Not-a-Real,
   $\textit{sign}$ is $\textit{bit}_{0}$,
   $\textit{useed}$ is ${2^2}^\textit{es}$ and $\textit{es}$ is the exponent size,
   $e$ is the exponent value in the $\textit{es}$ bits,
   $k$ is the scaling factor expressed by the regime (refer to pseudo code),
   $f$ is the fraction value and
   $\textit{fs}$ is the fraction size in bits.

Calculate k

if (first regime bit == 0)
    k = - runlength
else if (first regime bit == 1)
    k = runlength -1
Exercise:

Using the same method as example one, please compute the value of the following posit32 (es=2) binary:

00000001 01010111 00001111 10111000

In [13]:
#Example One
p16 = sp.posit16(0.3)
p16.toBinaryFormatted()
print("Actual p16 value: ", p16)

es = 1
sign=0
useed=pow(2, pow(2,es))
e=0
k=-1
f=int("001100110011", 2)
p = pow((-1), sign) * pow(useed, k) * pow(2, e) * (1+ (f/(int("111111111111",2))))
print("Rounded p value:", p)
print("P value to 14 decimal points: {0:.14f}".format(round(p,2)))
print("P value to 14 decimal points: {0:.20f}".format(round(p,2)))

#Exercise - Please write your code below:
00100011 00110011
Actual p16 value:  0.29998779296875
Rounded p value: 0.3
P value to 14 decimal points: 0.30000000000000
P value to 14 decimal points: 0.29999999999999998890