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\):
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
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 |
|
|
|
|
2 |
|
|
|
|
3 |
|
|
|
|
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
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:
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 |
|
Subtraction |
|
Multiplication |
|
Division |
|
Power |
|
Arithmetic operations#
Addition#
Add two OTIs using the +
operator. For example, adding the numbers
[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 |
|
Cosine |
|
Tangent |
|
Arcsine |
|
Arccosine |
|
Arctangent |
|
Hyperbolic functions
Function |
Symbol |
---|---|
Hyperbolic sine |
|
Hyperbolic cosine |
|
Hyperbolic tangent |
|
Inverse hyperbolic sine |
|
Inverse hyperbolic cosine |
|
Inverse hyperbolic tangent |
|
Logarithmic/Exponential functions
Function |
Symbol |
---|---|
Exponential |
|
Natural logarithm |
|
Logarithm base 10 |
|
Logarithm base b |
|
Power functions
Function |
Symbol |
---|---|
Power |
|
Square root |
|
Cubic root |
|
[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 |
---|---|---|
|
0 |
2 |
|
1 |
2 |
|
2 |
2 |
|
3 |
2 |
|
4 |
2 |
|
5 |
2 |
For order 3.
dirArray |
Index |
order |
---|---|---|
|
0 |
3 |
|
1 |
3 |
|
2 |
3 |
|
3 |
3 |
|
4 |
3 |
|
5 |
3 |
|
6 |
3 |
|
7 |
3 |
|
8 |
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]])