static Tutorial 01: Basic usage.#

Contributors:
    Mauricio Aristizabal
        University of Texas at San Antonio
        Universidad EAFIT, Colombia.

Date of creation:  Jun 20 2024
Last modification: Jun 20 2024

Introduction#

The main purpose of this document is to show the basic operation of the static ‘Order truncated Imaginary’ (OTI) module in pyoti . Multiple examples are given in order to show and understand the capabilities of the library, as well as its basic advantages.

The sparse implementation provides a robust and dynamic interface to the OTI numbers. However, a significant performance gain can be obtained when using the static library implementation. That is the reasoning behind supporting a static OTI implementation. The word ‘static’ is used in connotation of two things:

  • Static memory usage: The amount of memory used is fixed and no allocations are done to the

  • Static algebra: OTIs have the capability to change truncation order and number of basis according to the needed applications. A static implementation means that the number of basis and truncation order is fixed.

Importing the library#

The static module is composed of multiple submodules that suport a specific combination of OTI numbers with number of imaginary basis \(m\) and truncation order \(n\). To get that specific version of the library, you must load the version with the name onumm<nbasis>n<order>. For example, plain dual numbers which is the equivalent of an OTI with \(m=1\) and truncation order \(n=1\), you should load the static submodule onumm1n1. This will allow the computations of first order derivatives with respect to a single variable, with \(a^*\in \mathbb{OTI}^1_1\):

\[a^*=a_r+ a_{\epsilon_1} \epsilon_1\]

Similarly, you can import a module for the static support of OTis with \(m=2\) and \(n=3\), i.e. \(a^*\in \mathbb{OTI}^3_2\) where

\[a^*=a_r+ a_{\epsilon_1} \epsilon_1 + a_{\epsilon_2} \epsilon_2 + a_{\epsilon_1^2} \epsilon_1^2 + a_{\epsilon_1\epsilon_2} \epsilon_1\epsilon_2 + a_{\epsilon_2^2} \epsilon_2^2 + a_{\epsilon_1^3} \epsilon_1^3 + a_{\epsilon_1^2\epsilon_2} \epsilon_1^2\epsilon_2 + a_{\epsilon_1\epsilon_2^2} \epsilon_1\epsilon_2^2 + a_{\epsilon_2^3} \epsilon_2^3\]

The contents of static module mimic most of the functions on the sparse module. It is recommended the reader also looks at the tutorials of the sparse implementation.

The static implementation of the library in the current version of pyoti comes with support for

m/n

1

2

3

4

1

onumm1n1

onumm1n2

onumm1n3

onumm1n4

2

onumm2n1

onumm2n2

onumm2n3

onumm2n4

3

onumm3n1

onumm3n2

onumm3n3

onumm3n4

To print the available static modules, use the following

[1]:
help('pyoti.static')
Help on package pyoti.static in pyoti:

NAME
    pyoti.static

PACKAGE CONTENTS
    onumm10n1
    onumm10n2
    onumm1n1
    onumm1n10
    onumm1n2
    onumm1n20
    onumm1n3
    onumm1n30
    onumm1n4
    onumm2n1
    onumm2n2
    onumm2n3
    onumm2n4
    onumm3n1
    onumm3n2
    onumm3n3
    onumm3n4

FILE
    (built-in)


For the this tutorial, \(m=2\) and \(n=2\) will be used, \(a^*\in \mathbb{OTI}^2_2\) where

\[a^*=a_r+ a_{\epsilon_1} \epsilon_1 + a_{\epsilon_2} \epsilon_2 + a_{\epsilon_1^2} \epsilon_1^2 + a_{\epsilon_1\epsilon_2} \epsilon_1\epsilon_2 + a_{\epsilon_2^2} \epsilon_2^2\]

thus the module onumm2n2 is imported using:

[2]:
import pyoti.static.onumm2n2 as oti  # Import the static library for m=2 and n=3.

Basics#

Scalar creation#

In order to create an OTI scalar, the easiest way to do so is as follows

[3]:
a = 10 + oti.e(1) + 5.2* oti.e([1,2]) + 3*oti.e([2,2])
print(a)
10.0000 + 1.0000 * e([1]) + 0.0000 * e([2]) + 0.0000 * e([[1,2]]) + 5.2000 * e([1,2]) + 3.0000 * e([[2,2]])

The OTI number a has been created. This number represents the following:

\[a = 10 + \epsilon_1 + 5.2 \ \epsilon_1\epsilon_2 + 3 \ \epsilon_2^2\]

The ‘direction array’ given as a parameter to the function oti.e(dirArray) is the same as the one used in the sparse module, and can be defined as follows:

  • oti.e(i) :\(\epsilon_i\) (unsigned integer)

  • oti.e([i,j,k,l]) : \(\epsilon_i\epsilon_j\epsilon_k\epsilon_l\) (plain list of integers, with \(n\) elements)

  • oti.e([[i,m],j,[k,n],l]) : \(\epsilon_i^m\epsilon_j\epsilon_k^n\epsilon_l\) (the items with exponents other than 1, are defined as a 2-element list representing the base and exponent pair. Usually \(m\), \(n\) are greater than 1, but can be any integer \(m,n\geq0\))

Note: the e(dirArray) function supports the same inputs as the sparse function. However, it will always create a static number (meaning all its directions will be created and will be zero). Therefore, there is no need to assign the truncation order for static implementations, as it is defined in the module itself.

Additionally, creating directions that are outside of the truncation conditions of the static library selected, the result will be an OTI number filled with zeros.

For example

[4]:
oti.e([1,1,1]) # Notice this is third order, but the module
               # has truncation order 2.
[4]:
0.0000 + 0.0000 * e([1]) + 0.0000 * e([2]) + 0.0000 * e([[1,2]]) + 0.0000 * e([1,2]) + 0.0000 * e([[2,2]])

The number has been created with the same truncation order of the moudle. This can be requested from the object with the atribute order

[5]:
print(a.order)
2

Some users may be interested in knowing specific information about the onumm2n2 object. pyoti provides a short_repr() method to show basic information about the object. It shows the real coefficient, the total number of non-zero imaginary coefficients and its truncation order.

[6]:
print(a.short_repr())
onumm2n2(10.0, nnz: 3, order: 2)

Usign OTI numbers.#

In pyoti, OTI numbers have overloaded operators so that operations work as similar as other scalar types in Python, like float or int. The followign provides a summary of arithmetic operations supported in pyoti:

Operation

Symbol

Addition

a+b

Subtraction

a-b

Multiplication

a*b

Division

a/b

Power

a**b

Arithmetic operations#

Addition#

Add two OTIs using the + operator. For example, adding the numbers

\[\begin{split}\begin{array}{rcl} a &=& 10 + \phantom{2.5 } \epsilon_1 + 3 \ \epsilon_1\epsilon_2+5.2 \ \epsilon_2^2 \\ b &=& 10 + 2.5 \ \epsilon_1 - 3 \ \epsilon_1\epsilon_2 \phantom{+3 \ \epsilon_2^2} \\ a+b &=& 20 + 3.5 \ \epsilon_1 +0 \ \epsilon_1\epsilon_2 + 5.2 \ \epsilon_2^2 \end{array}\end{split}\]
[7]:
a = 10.0 +     oti.e([1]) + 3.0*oti.e([1,2]) + 5.2*oti.e([2,2])
b = 10.0 + 2.5*oti.e([1]) - 3.0*oti.e([1,2])

sum_result = a+b
print('The result is:', sum_result)
The result is: 20.0000 + 3.5000 * e([1]) + 0.0000 * e([2]) + 0.0000 * e([[1,2]]) + 0.0000 * e([1,2]) + 5.2000 * e([[2,2]])

Subtraction#

To subtract two OTI numbers, use - operator:

[8]:
sub_result = a-b

print("The result of a-b:",sub_result)
print("Order of a-b:     ",sub_result.order)
The result of a-b: 0.0000 - 1.5000 * e([1]) + 0.0000 * e([2]) + 0.0000 * e([[1,2]]) + 6.0000 * e([1,2]) + 5.2000 * e([[2,2]])
Order of a-b:      2

Multiplication#

In order to Multiply two OTI numbers in the library, just use the * operator:

[9]:
mult_result = a*b
print("The result of a*b:")
print(mult_result)
The result of a*b:
100.0000 + 35.0000 * e([1]) + 0.0000 * e([2]) + 2.5000 * e([[1,2]]) + 0.0000 * e([1,2]) + 52.0000 * e([[2,2]])

Division#

Division between two OTIs is performed as follows:

[10]:
div_result = a/b
print("The result of a/b:")
print(div_result)
The result of a/b:
1.0000 - 0.1500 * e([1]) + 0.0000 * e([2]) + 0.0375 * e([[1,2]]) + 0.6000 * e([1,2]) + 0.5200 * e([[2,2]])

Elementary functions#

pyoti supports multiple functions, such as sine, cosine, etc. All functions are accessed from the module. For instance, using the imported pyoti.sparse module as oti, functions can be called using e.g. oti.sin(). Supported functions are given in the following table:

Trigonometric functions

Function

Symbol

Sine

sin(x)

Cosine

cos(x)

Tangent

tan(x)

Arcsine

asin(x)

Arccosine

acos(x)

Arctangent

atan(x)

Hyperbolic functions

Function

Symbol

Hyperbolic sine

sinh(x)

Hyperbolic cosine

cosh(x)

Hyperbolic tangent

tanh(x)

Inverse hyperbolic sine

asinh(x)

Inverse hyperbolic cosine

acosh(x)

Inverse hyperbolic tangent

atanh(x)

Logarithmic/Exponential functions

Function

Symbol

Exponential

exp(x)

Natural logarithm

log(x)

Logarithm base 10

log10(x)

Logarithm base b

logb(x,b)

Power functions

Function

Symbol

Power

pow(x,e) or x**e

Square root

sqrt(x)

Cubic root

cbrt(x)

[11]:
x = 3 + 2*oti.e([1]) - 4.3 * oti.e([2,2])
f = oti.sin(x)
print(f)
0.1411 - 1.9800 * e([1]) - 0.0000 * e([2]) - 0.2822 * e([[1,2]]) - 0.0000 * e([1,2]) + 4.2570 * e([[2,2]])

Get and set coefficients of an OTI number.#

To get an imaginary coefficient, use the method ‘get_im(dirArray)’ and place the direction array of the direction you want

[12]:
a = 10.0 + oti.e([1]) + 3.0*oti.e([1,2]) + 5.2*oti.e([2,2])
print(a)
10.0000 + 1.0000 * e([1]) + 0.0000 * e([2]) + 0.0000 * e([[1,2]]) + 3.0000 * e([1,2]) + 5.2000 * e([[2,2]])

To get the coefficient of the direction \(\epsilon_1\) use:

[13]:
print(a.get_im(1))
1.0

or

[14]:
print(a.get_im([1]))
1.0

To get the coefficient of \(\epsilon_2^2\) use:

[15]:
print(a.get_im([[2,2]]))
5.2

or

[16]:
print(a.get_im([2,2]))
5.2

Coefficients that are not explicitly shown in the number are zero. For example, the coefficient along direction \(\epsilon_1\epsilon_2^2\) is:

[17]:
print(a.get_im([1,2,2]))
0.0

There are two ways to get the real coefficient: Get the real

[18]:
print(a.get_im(0))
10.0
[19]:
a.real
[19]:
10.0

Set coefficients of an OTI number#

Similar to the methods of getting coefficients, setting the values of coefficients works in the same way. If the coefficient already exist, it will be modified, and if the coefficient does not exist, it will be created. Use the method set_im(newValue,dirArray) to do so.

[20]:
a = 10.0 + oti.e([1]) + 3.0*oti.e([1,2]) + 5.2*oti.e([2,2])
print(a)
10.0000 + 1.0000 * e([1]) + 0.0000 * e([2]) + 0.0000 * e([[1,2]]) + 3.0000 * e([1,2]) + 5.2000 * e([[2,2]])
[21]:
a.set_im(7.3,[1,1])
print(a)
10.0000 + 1.0000 * e([1]) + 0.0000 * e([2]) + 7.3000 * e([[1,2]]) + 3.0000 * e([1,2]) + 5.2000 * e([[2,2]])
[22]:
a.set_im(4.2,[2])
print(a)
10.0000 + 1.0000 * e([1]) + 4.2000 * e([2]) + 7.3000 * e([[1,2]]) + 3.0000 * e([1,2]) + 5.2000 * e([[2,2]])

Alternative way to get and set components.#

There is an alternative way to get and set the imaginary coefficients within pyoti. This approach uses the “index, order” pairs of an imagianry direction, instead of the “direction array” components.

The index-order pair is the index according to the sorted imaginary directions of an OTI number. For a given order, the index is related to the position of the direction. For first order it is trivial, as the index corresponds to the basis of the imaginary direction (minus one). for Higher order, the index is determined by the appearance of the new imaginary basis. For instance:

For order 2

dirArray

Index

order

[1,1]

0

2

[1,2]

1

2

[2,2]

2

2

[1,3]

3

2

[2,3]

4

2

[3,3]

5

2

For order 3.

dirArray

Index

order

[1,1,1]

0

3

[1,1,2]

1

3

[1,2,2]

2

3

[2,2,2]

3

3

[1,1,3]

4

3

[1,2,3]

5

3

[2,2,3]

6

3

[1,3,3]

7

3

[2,3,3]

8

3

[3,3,3]

9

3

Index-order pairs are defined as a list of two elements, first the index (0-based) and then the order: [idx,order].

[23]:
a[ [ 1, 2 ] ] # gets e([1,2]) coefficient
[23]:
3.0
[24]:
a[ [ 2, 2 ] ] = 55 # Sets e([2,2]) coefficient
a
[24]:
10.0000 + 1.0000 * e([1]) + 4.2000 * e([2]) + 7.3000 * e([[1,2]]) + 3.0000 * e([1,2]) + 55.0000 * e([[2,2]])