core Tutorial 01: Core module utilities#

Author:
    Mauricio Aristizabal Cano
        University of Texas at San Antonio
        Universidad EAFIT

Date of creation:  Jun 19 2024
Last modification: Aug 12 2024

Introduction#

This document showcases and describes some of the capabilities of the core module in the pyoti library.

The reasoning behind the core module is to provide a unique module that contains the core functionality and classes that that are common to the different implementations of the OTI numbers (sparse, dense, etc), centralizing some of the calculations. Some of the capabilities are:

  • Manipulation of imaginary direcitions (definition, multiplication/division between directions, etc)

  • Translation layer between precomputed data and the OTI number implementation

  • Utility functions, e.g. getting total number of directions for m basis and n order, translating human-readable notation to faster imaginary direction indexing, etc.

  • Inquiring the capabilities of the implementation.

  • many others

In this document some of these capabilities will be shown.

Start by importing the module using the following code:

[1]:
import pyoti.core as coti  # Import the core library module.

Basics: inquiring library capabilities.#

An important information is to know what is the maximum number of bases and maximum truncation order supported by the precomputed data. This information is accessed via the function print_capabilities(). This function returns a string with the maximum number of basis that can be operated for a given order.

The default version of the library can compute derivatives of up-to 150th order, which are limited as follows:

[2]:
coti.print_capabilities()
 Order  Nbases    Ndir
     1   65000   65000
     2    1000  500500
     3     100  171700
     4     100 4421275
     5      10    2002
     6      10    5005
     7      10   11440
     8      10   24310
     9      10   48620
    10      10   92378
    11       5    1365
    12       5    1820
    13       5    2380
    14       5    3060
    15       5    3876
    16       5    4845
    17       5    5985
    18       5    7315
    19       5    8855
    20       5   10626
    21       3     253
    22       3     276
    23       3     300
    24       3     325
    25       3     351
    26       3     378
    27       3     406
    28       3     435
    29       3     465
    30       3     496
    31       3     528
    32       3     561
    33       3     595
    34       3     630
    35       3     666
    36       3     703
    37       3     741
    38       3     780
    39       3     820
    40       3     861
    41       3     903
    42       3     946
    43       3     990
    44       3    1035
    45       3    1081
    46       3    1128
    47       3    1176
    48       3    1225
    49       3    1275
    50       3    1326
    51       2      52
    52       2      53
    53       2      54
    54       2      55
    55       2      56
    56       2      57
    57       2      58
    58       2      59
    59       2      60
    60       2      61
    61       2      62
    62       2      63
    63       2      64
    64       2      65
    65       2      66
    66       2      67
    67       2      68
    68       2      69
    69       2      70
    70       2      71
    71       2      72
    72       2      73
    73       2      74
    74       2      75
    75       2      76
    76       2      77
    77       2      78
    78       2      79
    79       2      80
    80       2      81
    81       2      82
    82       2      83
    83       2      84
    84       2      85
    85       2      86
    86       2      87
    87       2      88
    88       2      89
    89       2      90
    90       2      91
    91       2      92
    92       2      93
    93       2      94
    94       2      95
    95       2      96
    96       2      97
    97       2      98
    98       2      99
    99       2     100
   100       2     101
   101       2     102
   102       2     103
   103       2     104
   104       2     105
   105       2     106
   106       2     107
   107       2     108
   108       2     109
   109       2     110
   110       2     111
   111       2     112
   112       2     113
   113       2     114
   114       2     115
   115       2     116
   116       2     117
   117       2     118
   118       2     119
   119       2     120
   120       2     121
   121       2     122
   122       2     123
   123       2     124
   124       2     125
   125       2     126
   126       2     127
   127       2     128
   128       2     129
   129       2     130
   130       2     131
   131       2     132
   132       2     133
   133       2     134
   134       2     135
   135       2     136
   136       2     137
   137       2     138
   138       2     139
   139       2     140
   140       2     141
   141       2     142
   142       2     143
   143       2     144
   144       2     145
   145       2     146
   146       2     147
   147       2     148
   148       2     149
   149       2     150
   150       2     151

From this output, notice that for first order directions, upto 65000 independent basis can be used. In contrast, for third order this number is reduced to 100. This number depends on the pre-computed data information, which can be re-generated using the otigen executable (see compilation instructions).

Another inquiry utility is the get_trunc_order() function, which returns the maximum truncation order of the implementation that is currently running.

[3]:
coti.get_trunc_order()
[3]:
150

Getting number of imaginary directions.#

A fully dense OTI number with truncation order \(n\) and a number of basis \(m\) will have a total of \(n_{\mbox{total}}\) imaginary directions (including the real direction). This number is given by:

\[n_{\mbox{total}} = {n+m\choose m} = \frac{(n+m)!}{n!m!}\]

This number can be efficiently evaluated using the function ndir_total( nbasis, order ). For instance, for a total of \(m=2\) basis and truncation order \(n=3\), the number of imaginary directions is 10. For an OTI number \(a^*\), with \(a^*\in \mathbb{OTI}^{n=2}_{m=3}\), it is as follows:

\[a^*=a_r + \underbrace{ a_{\epsilon_1}\epsilon_1+ a_{\epsilon_2}\epsilon_2 }_{\mbox{Order 1}} + \underbrace{ a_{\epsilon_1^2}\epsilon_1^2+ a_{\epsilon_1\epsilon_2}\epsilon_1\epsilon_2+ a_{\epsilon_2^2}\epsilon_2^2 }_{\mbox{Order 2}}+ \underbrace{ 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 }_{\mbox{Order 3}}\]
[4]:
n_total = coti.ndir_total(2,3)
n_total
[4]:
10

The total number of directions with a specific order \(p\), \(n_{p}\) for a number of basis \(m\) is given by

\[n_{p} = {p+m-1\choose m-1} = \frac{(p+m-1)!}{p!(m-1)!}\]

This number can be efficiently evaluated using the function ndir_order( nbasis, order_p ). For the prevous case, we have that

\[a^*=a_r + \underbrace{ a_{\epsilon_1}\epsilon_1+ a_{\epsilon_2}\epsilon_2 }_{n_1 = 2} + \underbrace{ a_{\epsilon_1^2}\epsilon_1^2+ a_{\epsilon_1\epsilon_2}\epsilon_1\epsilon_2+ a_{\epsilon_2^2}\epsilon_2^2 }_{n_2 = 3}+ \underbrace{ 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 }_{n_3 = 4}\]
[5]:
n_1 = coti.ndir_order(2,1)
n_2 = coti.ndir_order(2,2)
n_3 = coti.ndir_order(2,3)
print(f"n_1 = {n_1}, n_2 = {n_2}, n_3 = {n_3} ")
n_1 = 2, n_2 = 3, n_3 = 4

Direction helper#

The direction helper is an object of class dHelp, that loads and manipulates the precomputed data to speedup calculations. This object loads a particular set of precomputed data helper for each order of imaignary directions. Therefore, if the maximum truncation order is 150, the number of interal helpers in dHelp will be 150.

Here are described some uses of the direction helper.

Getting the direction helper#

Use the function get_dHelp() to get the direction helper object.

[6]:
dh = coti.get_dHelp()
dh
[6]:
<dhelp with 150 helpers loaded>
[7]:
# You can also use print(dh). Here, only a portion of the output is displayed.
print(str(dh)[:490])
Direction Helper object with 150 helpers:

Helper 1:
  - Truncation order: 1
  - Number of imaginary bases: 65000
  - Total number of imaginary directions: 65000
  - Total number of multiplication tables: 0

Helper 2:
  - Truncation order: 2
  - Number of imaginary bases: 1000
  - Total number of imaginary directions: 500500
  - Total number of multiplication tables: 1

Helper 3:
  - Truncation order: 3
  - Number of imaginary bases: 100
  - Total number of imaginary directions: 171700

Extracting the full directions given an index-order pair:#

The full direction of a “index,order” pair can be obtained from the precomputed data. For this, give the index, order pair and the direction helper will return the corresponding

Index \(\rightarrow\)

\(0\)

\(1\)

\(2\)

\(3\)

\(4\)

\(5\)

\(6\)

\(7\)

\(8\)

\(9\)

Order \(1\)

\(\epsilon_{1}\)

\(\epsilon_{2}\)

\(\epsilon_{3}\)

\(\epsilon_{4}\)

\(\epsilon_{5}\)

\(\epsilon_{6}\)

\(\epsilon_{7}\)

\(\epsilon_{8}\)

\(\epsilon_{9}\)

\(\epsilon_{10}\)

Order \(2\)

\(\epsilon_{1}^{2}\)

\(\epsilon_{1}\epsilon_{2}\)

\(\epsilon_{2}^{2}\)

\(\epsilon_{1}\epsilon_{3}\)

\(\epsilon_{2}\epsilon_{3}\)

\(\epsilon_{3}^{2}\)

\(\epsilon_{1}\epsilon_{4}\)

\(\epsilon_{2}\epsilon_{4}\)

\(\epsilon_{3}\epsilon_{4}\)

\(\epsilon_{4}^{2}\)

Order \(3\)

\(\epsilon_{1}^{3}\)

\(\epsilon_{1}^{2}\epsilon_{2}\)

\(\epsilon_{1}\epsilon_{2}^{2}\)

\(\epsilon_{2}^{3}\)

\(\epsilon_{1}^{2}\epsilon_{3}\)

\(\epsilon_{1}\epsilon_{2}\epsilon_{3}\)

\(\epsilon_{2}^{2}\epsilon_{3}\)

\(\epsilon_{1}\epsilon_{3}^{2}\)

\(\epsilon_{2}\epsilon_{3}^{2}\)

\(\epsilon_{3}^{3}\)

Order \(4\)

\(\epsilon_{1}^{4}\)

\(\epsilon_{1}^{3}\epsilon_{2}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{2}\)

\(\epsilon_{1}\epsilon_{2}^{3}\)

\(\epsilon_{2}^{4}\)

\(\epsilon_{1}^{3}\epsilon_{3}\)

\(\epsilon_{1}^{2}\epsilon_{2}\epsilon_{3}\)

\(\epsilon_{1}\epsilon_{2}^{2}\epsilon_{3}\)

\(\epsilon_{2}^{3}\epsilon_{3}\)

\(\epsilon_{1}^{2}\epsilon_{3}^{2}\)

Order \(5\)

\(\epsilon_{1}^{5}\)

\(\epsilon_{1}^{4}\epsilon_{2}\)

\(\epsilon_{1}^{3}\epsilon_{2}^{2}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{3}\)

\(\epsilon_{1}\epsilon_{2}^{4}\)

\(\epsilon_{2}^{5}\)

\(\epsilon_{1}^{4}\epsilon_{3}\)

\(\epsilon_{1}^{3}\epsilon_{2}\epsilon_{3}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{2}\epsilon_{3}\)

\(\epsilon_{1}\epsilon_{2}^{3}\epsilon_{3}\)

Order \(6\)

\(\epsilon_{1}^{6}\)

\(\epsilon_{1}^{5}\epsilon_{2}\)

\(\epsilon_{1}^{4}\epsilon_{2}^{2}\)

\(\epsilon_{1}^{3}\epsilon_{2}^{3}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{4}\)

\(\epsilon_{1}\epsilon_{2}^{5}\)

\(\epsilon_{2}^{6}\)

\(\epsilon_{1}^{5}\epsilon_{3}\)

\(\epsilon_{1}^{4}\epsilon_{2}\epsilon_{3}\)

\(\epsilon_{1}^{3}\epsilon_{2}^{2}\epsilon_{3}\)

Order \(7\)

\(\epsilon_{1}^{7}\)

\(\epsilon_{1}^{6}\epsilon_{2}\)

\(\epsilon_{1}^{5}\epsilon_{2}^{2}\)

\(\epsilon_{1}^{4}\epsilon_{2}^{3}\)

\(\epsilon_{1}^{3}\epsilon_{2}^{4}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{5}\)

\(\epsilon_{1}\epsilon_{2}^{6}\)

\(\epsilon_{2}^{7}\)

\(\epsilon_{1}^{6}\epsilon_{3}\)

\(\epsilon_{1}^{5}\epsilon_{2}\epsilon_{3}\)

Order \(8\)

\(\epsilon_{1}^{8}\)

\(\epsilon_{1}^{7}\epsilon_{2}\)

\(\epsilon_{1}^{6}\epsilon_{2}^{2}\)

\(\epsilon_{1}^{5}\epsilon_{2}^{3}\)

\(\epsilon_{1}^{4}\epsilon_{2}^{4}\)

\(\epsilon_{1}^{3}\epsilon_{2}^{5}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{6}\)

\(\epsilon_{1}\epsilon_{2}^{7}\)

\(\epsilon_{2}^{8}\)

\(\epsilon_{1}^{7}\epsilon_{3}\)

Order \(9\)

\(\epsilon_{1}^{9}\)

\(\epsilon_{1}^{8}\epsilon_{2}\)

\(\epsilon_{1}^{7}\epsilon_{2}^{2}\)

\(\epsilon_{1}^{6}\epsilon_{2}^{3}\)

\(\epsilon_{1}^{5}\epsilon_{2}^{4}\)

\(\epsilon_{1}^{4}\epsilon_{2}^{5}\)

\(\epsilon_{1}^{3}\epsilon_{2}^{6}\)

\(\epsilon_{1}^{2}\epsilon_{2}^{7}\)

\(\epsilon_{1}\epsilon_{2}^{8}\)

\(\epsilon_{2}^{9}\)

From the Table above, the index-order pair <9, 5> (index 9, order 5) corresponds to the direction \(\epsilon_{1}\epsilon_{2}^{3}\epsilon_{3}\). Therefore, its compact dirArray representation is [1,[2,3],3]. This can be easily retrieved from the direction helper object using the get_compact_fulldir(indx,order) method:

[8]:
indx  = 9
order = 5
dh.get_compact_fulldir(indx, order)
[8]:
[1, [2, 3], 3]

The expanded full dir can be obtained using the get_fulldir(indx,order) method

[9]:
dh.get_fulldir(indx, order)
[9]:
array([1, 2, 2, 2, 3], dtype=uint16)

Imaginary directions can become extensive. An ideal representation is by defining its bases and exponents. This information is also efficiently provided by the direction helper. Use dh.get_base(indx, order)

[10]:
bases,exponents = dh.get_base_exp(indx,order)
print(f'Bases {bases}, exponents {exponents}')
Bases [1 2 3], exponents [1 3 1]

Converting imaginary direction notations#

The direction helper provides an easy way to converting from index-order pairs to human readable compact and full direction expressions. However, the inverse operation is not trivial.

Converting from index-order to human readable dirArray#

Use the direction helper’s get_compact_fulldir(indx, order) or get_fulldir(indx, order) methods:

[11]:
indx  = 9
order = 5
dh.get_compact_fulldir(indx, order)
[11]:
[1, [2, 3], 3]

Converting from human readable dirArray to index-order#

Use the function imdir(dirArray) in the core module (this is not a method in the direction helper). This function uses the dirArray input and returns a 2-element list with the corresponding index-order pair of this direction.

[12]:
coti.imdir([1,[2,3],3])
[12]:
[9, 5]

Operating Imaginary directions#

Imagianry directions can be multiplied and divided. Here are some functions in the core pyoti module that can provide this functionality.

Multiplying imaginary directions#

Imaginary directions can be multiplied using the direction helper and the method mult_dir(idx1,ord1,idx2,ord2). The first two inputs corrspond to the index order pairs of the first imatginary direction, and the last two inputs correspond to the second imaginary direction. The result is the index-order pair of the resulting imaginary direction.

For example multiplying \(\epsilon_{1}^{3} \times \left( \epsilon_{2}^2\epsilon_{3}\right)\) is \((0,3) \times (6,3)\) is the direction \(\epsilon_{1}^{3}\epsilon_{2}^{2}\epsilon_{3}\) which corresponds to the \((9,6)\) direction.

[13]:
# Multipying two

ridx,rord = dh.mult_dir( 0,3, 6,3 )

print(f'Result index: {ridx} order {rord} -->  {dh.get_compact_fulldir(ridx, rord)}')
Result index: 9 order 6 -->  [[1, 3], [2, 2], 3]

Dividing imaginary directions#

At first glance, the user might think that this is a useless operation. However, division of two imaginary directions is necessary to determine whether a direction is multiple of another direction.

Let’s consider the following example: Divide the direction \(\epsilon_{1}^{3}\epsilon_{2}^{2}\epsilon_{3}\) (9,6) by \(\epsilon_{1}^{2}\epsilon_{2}\) (1,3). The result is \(\epsilon_{1}\epsilon_{2}\epsilon_{3}\).

This operation can be performed using the functions div_imdir_idxord(num_idx,num_ord, den_idx,den_ord) where num_idx,num_ord are the index-order pairs of the numerator direction, and den_idx,den_ord are index-order pair of the denominator direction. The result is a 2-element list with the index-order pair of the resulting direction.

[14]:
coti.div_imdir_idxord(9,6, 1,3)
[14]:
[5, 3]

The same operation can be achieved but in human readable form using the function div_imdir(numDirArr,denDirArr) where numDirArr is the dirArray of the numerator direction, and denDirArr is the dirArray of the denominator direction. For the same example, the result is the dirArray of the resulting direction.

[15]:
coti.div_imdir([[1,3],[2,2],3], [[1,2],2])
[15]:
[1, 2, 3]

In the case the division of the imaginary directions is not possible, both functions return an integer -1. For example, dividing \(\epsilon_{1}^{3}\) by \(\epsilon_{2}\) is not possible, and therefore the result is

[16]:
coti.div_imdir([[1,3]], [2])
[16]:
-1