Overview of Antares basic classes

In this tutorial you will learn how to use the main classes of the Antares Application Programming Interface.

We will detail the creation of Base, Zone, and Instant objects.

First, we have to load the antares package.

import antares as ant  # if you only want to type 3 letters instead of 7 in the following
# or import antares
# but not: from antares import * (cf. https://www.python.org/dev/peps/pep-0008/)

All objects, classes, and functions exposed to the developer can be found with

print(ant.__all__)

The Antares API is based on a data structure. It is a CFD-oriented data structure, but formally not restricted to CFD data.

The main class is called the Base class, which is the top-level node of the data structure. An instance of the Base class (named a Base in the following) is essentially a container of Zone instances.

Each Zone is basically a collection of Instant instances.

A Zone is typically a block of a mesh (a sub-domain of the overall mesh): + a block in a multiblock mesh, or + a parallel partition of an unstructured mesh

but it can also be a set of probes, etc.

Each Instant is a container of variables.

image

image

Base Class

When you create a Base object, you are creating an instance of the Base class, or you are instantiating the Base class.

In the following, we will indifferently refer to the class or the object by the word Base. The same thing will apply to other Antares objects in general.

First, we instantiate a Base and print the object to see what this base contains.

base = ant.Base()
print(base)
print(base.grid_points)
print(base.families)

What more can I do with a Base object ?

You can see all the attributes and methods of the Base with the next instructions.

# base.[TAB] -> try completion key
# base.
# or python help function
help(base)

Zone Class

The Zone is the first sub-level of a Base.

It can contain many Instant instances.

For example, a Zone can be seen as a block within a multi-block configuration or as a parallel partition of an unstructured mesh in the context of domain decomposition.

zone = ant.Zone()
print(zone)
print(zone.boundaries)
# zone.[TAB] -> try completion key
# zone.
# or
help(zone)

Instant Class

The Instant is the last level of the Antares Base data structure.

The term Instant, although it has a connotation of time, should not be seen as such.

For example, it can be seen as a temporal snapshot of an unsteady phenomenon, or as a sample from a sampling carried out on a steady configuration.

It can contain many variables stored as numpy (https://numpy.org) arrays.

inst = ant.Instant()
print(inst)
print(inst.connectivity)
# instant.[TAB] -> try completion key
# inst.
# or
help(inst)

We have seen how to build instances of Base, Zone, and Instant classes.

Let’s go further and look at their interactions.

First, a very important thing to know is that these three classes derived from a Python ordered dict. So, their instances are ordered dictionaries.

Adding a zone to a base is simply mapping a string (the zone name) to a Zone object.

base['z1'] = zone  # add an entry to the dictionary
print(base)

You can not use an integer key to map the Zone object.

Please see the python documentation on dict.

# remove the comment to check that a zone must have a name
# base[1] = zone

In the Base, each Zone is associated to a string (here ‘z1’).

There are two ways to reference zones: with a string (str) and with an integer (int)

print(base['z1'])  # Python dict!
print(base[0])  # Python ordered dict!

If the reference does not exist in the base, then you get an error message.

# remove the comment to check that the zone with this name is not in the base
# print(base['unknown_zone_name'])
# remove the comment to check that there is only one zone in the base
# print(base[1])

Adding an instant to a zone is simply mapping a string (the instant name) to an Instant object.

zone['t1'] = inst
print(zone)

In the Zone, each Instant is associated to a string (here ‘t1’).

There are two ways to reference instants: with a string (str) and with an integer (int)

If the reference does not exist in the zone, then you get an error message.

print(zone['t1'])
print(zone[0])

At this point, we have a base that contains one zone that contains one Instant.

Let us put some data in the instant.

Variables are defined by a tuple of a name and a location: (‘name’, ‘location’)

The name of a variable is a string.

The location can be ‘node’, by default, or ‘cell’. (CFD-oriented)

A geometric element has vertices (nodes), edges, and faces.

‘nodes’ are for variables located at the vertices, and ‘cell’ for variables located at the element barycenter (1 degree of freedom (DOF) per element).

If only the name is used, then ‘node’ is automatically added.

inst['v1'] = 1
print(inst)

All variables are stored in numpy arrays.

That way, we can benefit from all features of the numpy library. In particular, it is very effective for large datasets.

type(inst['v1'])
inst[('v2', 'cell')] = [2]
print(inst)

Variables that have the same name but a different location are not the same variables. They are stored in different arrays.

import numpy as np
inst[('v1','cell')] = np.arange(4)
print(inst)

(‘v1’, ‘node’) and (‘v1’, ‘cell’) are different arrays.

Of course, you can look at the content of variables.

print(inst[('v1','cell')])
print(inst['v1'])
print(inst['v2'])
print(inst[('v2','cell')])

Now, we have all material to build a basic base.

Let us see some more details about the Instant.

When you print an instant, then you see the shape.

The shape of the instant is the size of node values.

For example, it corresponds to the number of nodes (points, vertices) for unstructured grids.

import numpy as np
inst = ant.Instant()
inst['v1'] = np.arange(6)
inst[('v2','cell')] = np.arange(2)
print(inst)

For example, it shows a tuple (Imax, Jmax, Kmax) with the number of nodes in each direction of a block for structured grids.

import numpy as np
inst = ant.Instant()
inst['v1'] = np.arange(6).reshape(3,2)
inst[('v2','cell')] = np.arange(2)
print(inst)

The other way round. Look at the differences compared to the previous results.

inst = ant.Instant()
inst[('v2','cell')] = np.arange(2)
print(inst)
inst['v1'] = np.arange(6)
print(inst)

Once the shape is set, you cannot set another variable located at nodes with another shape.

# remove the comment to check that another variable located at nodes with another shape cannot be set
# inst['v3'] = np.arange(3)

Once the shape is set, you cannot change it

# remove the comment to check that the shape cannot be changed
# inst['v1'] = np.arange(3)

The shape can only be set if there is no shape set beforehand.

del inst['v1']
print(inst)
inst['v1'] = np.arange(3)
print(inst)

As already seen, the variables can be multi-dimensional numpy arrays. For example, 2D or 3D structured grids.

inst = ant.Instant()
inst['v2'] = np.arange(4).reshape(2, 2)
print(inst)
inst = ant.Instant()
inst['v2'] = np.arange(16).reshape(2, 2, 4)
print(inst)

shared Instant

When a Zone contains many instants, it is sometimes desired to share some data between the Instants without duplicating them. This is the purpose of the shared Instant that is an attribute of the class Zone: Zone.shared. The shared Instant is an Instant that is not stored with the other instants in the Zone dictionary.

The attribute Zone.shared is an instance of the class Instant.

Setting a variable on a shared Instant works like setting a variable on a classical Instant.

However, all the Instants of the Zone will see the shared Instant variable as if it was one of them.

Warning! When the shared Instant has a shape set (by adding a variable in it for example), all the Instants of the Zone must have the same shape.

For example, it is very used to store the mesh coordinates in an undeformable reference frame, which are shared between many unsteady snapshots. In other words, it enables to handle time-independent variables shared between Instants.

image

image

Set the following zone and note that the shared Instant is empty and hence has no shape for the moment.

zone = ant.Zone()
zone['t1'] = ant.Instant()
zone[0]['v1'] = np.arange(2)
print(zone[0])
print(zone.shared)

Setting a variable in a shared Instant works like setting a variable in a classical Instant. However, all the Instants of the Zone will see the shared Instant variable as if it was one of them.

zone.shared['v2'] = np.arange(2)
print(zone[0])
zone['t2'] = ant.Instant()
print(zone[1])

The method keys() in the class Instant accepts an optional argument with_shared to get the shared variables or not. This way, you can separate regular Instant variables from shared variables.

print(zone[0])
print(zone[0].keys(with_shared=False))

If you want to develop some prototypes, then you can use the method init() of the class Base to quickly populate a Base.

base = ant.Base()
base.init()
print(base)
print(base[0])
print(base[0][0])
base = ant.Base()
base.init(zones=['zone_0', 'zone_1'], instants=['t00', 't01'])
print(base)
print(base[0])
print(base[0][0])
base[0][0]['v1'] = np.arange(2)

Python is built around dictionaries. Get used with python dictionaries to get the most of Antares.

Most of the Antares objects inherits from the python dictionary class. Most of the Antares objects are dictionaries.

This means that the main methods of dictionaries can be used.

This notebook is not dedicated to parse all functionalities of a python dictionary. Please refer to the related python documentation for more information.

The keys() method returns the key names associated to the various objects contained in the dictionary.

An element in Antares objects is accessed like with a python dictionary.

print(base.keys())
print(base[0].keys())
print(base[0][0].keys())

As the keys() method returns the key names associated to the various objects contained in the dictionary, the values() method returns the objects themselves.

for zone_value in base.values():
    print(zone_value)
print(base.values())

The items() method returns the pair (key, value) at each iteration of a loop.

for zone_name, zone_value in base.items():
    print(zone_name)
    print(zone_value)
print(base.items())

You can check if a key exists in the dictionary using the in statement.

print('zone_1' in base)  # True
print('zone_2' in base)  # False

A useful function is the enumerate() function. It allows you to loop on a dictionary and know the value of the iterative index.

for zone_idx, zone_key in enumerate(base.keys()):
    print(zone_idx, zone_key)

Finally, you can delete an element from a dictionary using the del statement. This might be very useful for memory performance reasons.

base['zone_tmp'] = ant.Zone()
print(base)
del base['zone_tmp']
print(base)