{ "cells": [ { "cell_type": "markdown", "id": "38aebb1d", "metadata": {}, "source": [ "# Mesh based data handling with Pymicro " ] }, { "cell_type": "markdown", "id": "0945f582", "metadata": {}, "source": [ "This Notebook will introduce you to the creation and handling of mesh based data, *i.e.* a set of data arrays that define fields on a regular grid, defined by additional data arrays which specify the coordinates of the grid nodes, and of the grid elements connectivity. \n" ] }, { "cell_type": "markdown", "id": "61210b73", "metadata": {}, "source": [ "## I - SampleData Mesh Groups " ] }, { "cell_type": "markdown", "id": "19555d85", "metadata": {}, "source": [ "**SampleData Grid Groups** have been introduced in the [last tutorial](./Image_data.ipynb), which also presented in details one of the two type of *Grids*, the *Image Groups*, representing regular grids (rectilinear meshes). This tutorial focuses on the second type on *Grid Groups*, the **Mesh groups**." ] }, { "cell_type": "markdown", "id": "a745a7e7", "metadata": {}, "source": [ "### SampleData meshes" ] }, { "cell_type": "markdown", "id": "e609f874", "metadata": {}, "source": [ "**Mesh Groups** represent unstructured grids, defined by a set of **Nodes** with their associate coordinates, a set of **Elements**, that are defined by a connectivity matrix, indicating which *nodes* define each *element*, and a set of **Fields**, describing data defined on this grid. They are typically used to store **Finite Element meshes**, and **Finite Element fields**. For a precise mathematical definition of what *Nodes* and *Elements* are, the reader can refer to standard concepts of finite element theory, which is not in the scpe of this tutorial.\n", "\n", "Like for *Image Groups*, three types of *Mesh Groups* can be handled by *SampleData*:\n", "\n", "\n", "1. `emptyMesh`: an empty group that can be used to set the organization and metadata of the dataset before adding actual data to it.\n", "2. `2DMesh`: a grid of Nodes defined by two coordinates $x$ and $y$\n", "3. `3DMesh`: a grid of Nodes defined by three coordinates $x$, $y$ and $z$\n", "\n", "An Mesh field is a data array that has one value associated to each node, each element, or each integration point of the mesh. \n", "\n", "For many applications, it may be usefull to define specific sets of *Nodes* or sets of *Elements* (for instance to define distinct components of a microstructure, define an area where specific boundary conditions are applied for numerical simulation etc...). Therefore, *SampleData Mesh Groups* also store data defining those *Node* and *Element* sets. " ] }, { "cell_type": "markdown", "id": "ea282540", "metadata": {}, "source": [ "Hence, *Mesh Groups* contain data and metadata defining the mesh Nodes, Node Sets, Elements, Element types and Sets, Mesh Fields, and metadata to synchronize with the XDMF Grid node associated to the Mesh. To explore in details this data model, we will once again open the reference test dataset of the *SampleData* unit tests." ] }, { "cell_type": "markdown", "id": "1b07b3b1", "metadata": {}, "source": [ "
\n", "\n", "**Warning 1:** \n", " \n", "The SampleData class is not designed to create meshes , in particular complex meshes of microstructures that are of interest for material science. It is designed mainly to serve as a I/O interface to load/store meshes from mesh data files, visualize them, and provide them to other numerical tools that may need them (finite element solvers for instance).\n", " \n", "An automatic mesher that directly outputs a *SampleData Mesh Group* has been implemented in the *Pymicro* package. It relies on an automatic meshing code that is not open-source, and is the property of the laboratory *Centre des Matériaux* of *Mines ParisTech*. Please contact us if you want to know more and use this tool. If you are from *le Centre des Matériaux*, a dedicated tutorial for this tool exists. Please contact H. Proudhon, B. Marchand or L. Lacourt to get it. \n", " \n", "If you need to create a meshing tool that automatically outputs results as a SampleData dataset, please contact us to see if we may help you or provide you with guidelines to develop your mesher interface. \n", "\n", "
" ] }, { "cell_type": "markdown", "id": "27b70a0e", "metadata": {}, "source": [ "
\n", "\n", "**Warning 2:** \n", " \n", "The SampleData class currently relies on the BasicTools package to handle grids. For now, *BasicTools* integration within *SampleData* covers mainly the input and output of *BasicTools* mesh objects in/out of *SampleData* datasets. \n", "\n", "**The *BasicTools* package offers many I/O functionalities with many classical mesh formats, and mesh creation tools. Their are not currently all integrated within the *SampleData* class. They will be in the future, but for now, you will need to directly use *BasicTools* code to access those possibilities.** If you want to do so, feel free to contact us to get help.\n", " \n", "*Basictools* is a required package to use Pymicro, so you should have it installed in your Python environment if you are using Pymicro. \n", "\n", "
" ] }, { "cell_type": "markdown", "id": "247f38fb", "metadata": {}, "source": [ "## II - Mesh Groups Data Model " ] }, { "cell_type": "markdown", "id": "ff14b25e", "metadata": {}, "source": [ "This section will introduce you to the various data items constituting the *Mesh Group* data model, and to the *Sampledata* methods that you can use to retrieve them." ] }, { "cell_type": "code", "execution_count": null, "id": "5a93df00", "metadata": {}, "outputs": [], "source": [ "# Import the Sample Data class\n", "from pymicro.core.samples import SampleData as SD" ] }, { "cell_type": "code", "execution_count": null, "id": "5fc6b8fa", "metadata": {}, "outputs": [], "source": [ "from pymicro import get_examples_data_dir # import file directory path\n", "PYMICRO_EXAMPLES_DATA_DIR = get_examples_data_dir() # get the file directory path\n", "import os\n", "dataset_file = os.path.join(PYMICRO_EXAMPLES_DATA_DIR, 'test_sampledata_ref') # test dataset file path\n", "data = SD(filename=dataset_file)" ] }, { "cell_type": "markdown", "id": "14e7525a", "metadata": {}, "source": [ "Let us print the content of the dataset to remember its composition:" ] }, { "cell_type": "code", "execution_count": null, "id": "7a355401", "metadata": {}, "outputs": [], "source": [ "data.print_index()\n", "data.print_dataset_content(short=True)" ] }, { "cell_type": "markdown", "id": "6daa6d3e", "metadata": {}, "source": [ "The test dataset contains one *3DMesh* Group, the `test_mesh` group, with indexname `mesh`. It has four fields defined on it, `Test_field1`, `Test_field2`, `Test_field3`, `Test_field4`, and a field `test_vector` with two time values $1.0$ and $2.0$." ] }, { "cell_type": "markdown", "id": "8e2a6ba1", "metadata": {}, "source": [ "Let us print more information about this *Mesh Group*:" ] }, { "cell_type": "code", "execution_count": null, "id": "597f8ad1", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh')" ] }, { "cell_type": "markdown", "id": "1e73a243", "metadata": {}, "source": [ "Like for *Image Groups*, a lot of metadata is attached to a *Mesh Group*. All the attribute that you see attached to the `test_mesh` group are part of the *Mesh Group* data model, and are automatically created upon creation of *Mesh Groups*. Like for *Image Groups*, you can see a `group_type` attribute, indicating that our group stores data representing a three dimensional mesh. " ] }, { "cell_type": "markdown", "id": "010ce259", "metadata": {}, "source": [ "### Mesh Group indexnames" ] }, { "cell_type": "markdown", "id": "9b8d26bb", "metadata": {}, "source": [ "Let us print more precisely the index " ] }, { "cell_type": "code", "execution_count": null, "id": "2fc04fd2", "metadata": {}, "outputs": [], "source": [ "data.print_index(local_root='test_mesh')" ] }, { "cell_type": "markdown", "id": "1bb9b56c", "metadata": {}, "source": [ "**As you can see, all the elements of the *Mesh Group* data model have an indexname constructed following the same pattern:\n", "`mesh_indexname + _ + item_name`. Their `item_name` is also their Name in the dataset as you can see in their Pathes.** \n", "\n", "For our reference dataset, and for the *Mesh Group* `test_mesh`, the `mesh_indexname` is `mesh`.\n", "\n", "The various elements of this data model an their `item_name` are all printed above in the `print_index` output, and will be all presented below. " ] }, { "cell_type": "markdown", "id": "d9056cc3", "metadata": {}, "source": [ "### The Mesh Geometry Group" ] }, { "cell_type": "markdown", "id": "1b852341", "metadata": {}, "source": [ "You can see in the output of the `print_dataset_content` method above that the mesh contains 3 *data array* nodes, and one *Group*, which has a lot of childrens. This group, **the Geometry Group, contains all the data necessary to define the grid topology.** This group `item_name` is `Geometry`." ] }, { "cell_type": "code", "execution_count": null, "id": "9aaa74e8", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_Geometry')\n", "data.print_group_content('mesh_Geometry', short=True)" ] }, { "cell_type": "markdown", "id": "faed9ace", "metadata": {}, "source": [ "This Group contains a lot of data arrays, and 2 groups. You see from their names that all of its children are related to the grid *Nodes* and *Elements*. We will now explore in details how this essential data is stored." ] }, { "cell_type": "markdown", "id": "06f75bbe", "metadata": {}, "source": [ "### Mesh Nodes" ] }, { "cell_type": "markdown", "id": "6c4e698e", "metadata": {}, "source": [ "**Mesh Nodes define the geometry of the grid. They are defined by an array of coordinates, of size $[N_{nodes},N_{coordinates}]$**. \n", "\n", "You can see a few cells above that some attributes of the *Mesh Group* provide information on the mesh *Nodes*:\n", "\n", "* `number_of_nodes`: $N_{nodes}$, the number of nodes in the grid\n", "* `nodes_path`: path of a data array containing the node coordinates array\n", "* `nodesID_path`: path of a data array containing Identification numbers for each node of the previous array\n", "\n", "As you can see, the node coordinates and identification number arrays are stored in the *Geometry Group*. These two data items have respectively the *item name* `Nodes` and `Nodes_ID`. We can print more information about them:" ] }, { "cell_type": "code", "execution_count": null, "id": "8443325d", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_Nodes')\n", "data.print_node_info('mesh_Nodes_ID')" ] }, { "cell_type": "markdown", "id": "e7fe1626", "metadata": {}, "source": [ "#### Nodes and Nodes ID arrays" ] }, { "cell_type": "markdown", "id": "d0e14361", "metadata": {}, "source": [ "You see that they are simple *Data array* items, with no specific metadata. You can see that the shape of the `Nodes` array is $[N_{nodes}, N_{coordinates}]$, which is $[6,3]$ in this case. This is consistent with the dimensionality of the mesh, which is 3 (tridimensional mesh). The `Nodes_Id` array has one value per nodes, which is also consistent. \n", "Let us print the content of those arrays:" ] }, { "cell_type": "code", "execution_count": null, "id": "2974a3ae", "metadata": {}, "outputs": [], "source": [ "mesh_nodes = data['mesh_Nodes']\n", "mesh_nodes_id = data['mesh_Nodes_ID']\n", "\n", "for i in range(len(mesh_nodes_id)):\n", " print(f'Mesh node number {mesh_nodes_id[i]} coordinates: {mesh_nodes[i,:]}')" ] }, { "cell_type": "markdown", "id": "7f0b4cbf", "metadata": {}, "source": [ "If you see that the 4 first nodes form a square in the $(X,Y)$ plane, and that two other nodes are symetrically located on top and bottom of the center of this square. They are the vertexes of a regular octahedron, which we will verify later by visualizing the mesh." ] }, { "cell_type": "markdown", "id": "346f453a", "metadata": {}, "source": [ "To get those arrays, the most convenient solution is to use the `get_mesh_nodes` and `get_mesh_nodesID` methods:" ] }, { "cell_type": "code", "execution_count": null, "id": "9f96d527", "metadata": {}, "outputs": [], "source": [ "mesh_nodes = data.get_mesh_nodes('mesh')\n", "mesh_nodes_id = data.get_mesh_nodesID('mesh')\n", "\n", "for i in range(len(mesh_nodes_id)):\n", " print(f'Mesh node number {mesh_nodes_id[i]} coordinates: {mesh_nodes[i,:]}')" ] }, { "cell_type": "markdown", "id": "008127bd", "metadata": {}, "source": [ "#### Node Sets/Tags" ] }, { "cell_type": "markdown", "id": "dc83915d", "metadata": {}, "source": [ "You can also see that the Geometry contains a *String Array* labelled `Nodes_tag_list`, with *item_name* `NodeTagsList`. This group has the following content:" ] }, { "cell_type": "code", "execution_count": null, "id": "ffb313ec", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_NodeTagsList')" ] }, { "cell_type": "markdown", "id": "dd2986aa", "metadata": {}, "source": [ "You see that this node is a *String array*, containing 2 elements. They are the names of the **NODE SETS**, or also **NODE TAGS** (*notation used in SampleData's code*), defined on this mesh, and stored on the dataset. You can access them easily (see *String Array* section in [tutorial 2](./2_SampleData_basic_data_items.ipynb)):" ] }, { "cell_type": "code", "execution_count": null, "id": "d55cb97f", "metadata": {}, "outputs": [], "source": [ "for tag in data['mesh_NodeTagsList']:\n", " print(tag.decode('utf-8'))" ] }, { "cell_type": "markdown", "id": "0e66353a", "metadata": {}, "source": [ "An easier way to get this information is to use the `get_mesh_node_tags` method:" ] }, { "cell_type": "code", "execution_count": null, "id": "2bd77251", "metadata": {}, "outputs": [], "source": [ "data.get_mesh_node_tags_names('test_mesh')" ] }, { "cell_type": "markdown", "id": "94c50805", "metadata": {}, "source": [ "The two node tags are names `Z0_plane`, and `out_of_plane`. From the value of the node coordinates printed a few cells above, we can expect that the Node Set `Z0_plane` contains the 4 first nodes, and that `out_of_plane` set the two last nodes. \n", "\n", "To see their content, you can use the dedicated methods `get_mesh_node_tag` and `get_mesh_node_tag_coordinates`:" ] }, { "cell_type": "code", "execution_count": null, "id": "3a36126d", "metadata": {}, "outputs": [], "source": [ "# Get the node IDs for the Node sets defined on the mesh \n", "Z0_plane = data.get_mesh_node_tag(meshname='test_mesh',node_tag='Z0_plane')\n", "out_of_plane = data.get_mesh_node_tag(meshname='test_mesh',node_tag='out_of_plane')\n", "\n", "# Get the node coordinates for the Node sets defined on the mesh \n", "Z0_plane_xyz = data.get_mesh_node_tag_coordinates(meshname='test_mesh',node_tag='Z0_plane')\n", "out_of_plane_xyz = data.get_mesh_node_tag_coordinates(meshname='test_mesh',node_tag='out_of_plane')\n", "\n", "# Print informations\n", "print(f'Node tag \"Z0_plane\" is composed of nodes {Z0_plane} with coordinates \\n {Z0_plane_xyz}','\\n')\n", "print(f'Node tag \"out_of_plane\" is composed of nodes {out_of_plane} with coordinates \\n {out_of_plane_xyz}')" ] }, { "cell_type": "markdown", "id": "037b20ec", "metadata": {}, "source": [ "The *Node Tags* are stored in the *Group* `NodeTags`, children of the *Group* `Geometry`:" ] }, { "cell_type": "code", "execution_count": null, "id": "c66e76ea", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_NodeTags')\n", "data.print_group_content('mesh_NodeTags')" ] }, { "cell_type": "markdown", "id": "470c0b2a", "metadata": {}, "source": [ "You can observe that the the group contains four data items, two for each node set in our mesh group, as their name indicates.\n", "\n", "Two of those data items have a name that starts with **`NT_`**, which stand for **NodeTag**. They contain the data items associated to the *node tags*, that can be retrieved with the `get_mesh_node_tag` method (see above).\n", "\n", "The two other data item have a name that starts with **`field_`**. They are *Field Arrays* data items. These fields are nodal value fields (see below the section dedicated to *Mesh Fields*), and have a value of **1** on the *node set*, and **0** out of the *node set*." ] }, { "cell_type": "markdown", "id": "7e777831", "metadata": {}, "source": [ "We have now covered all the elements of the *Mesh Group* data model related to mesh *nodes*. " ] }, { "cell_type": "markdown", "id": "1ff997b1", "metadata": {}, "source": [ "### Mesh Elements" ] }, { "cell_type": "markdown", "id": "796037f4", "metadata": {}, "source": [ "**Mesh Elements define the geometry of the geometrical domain represented by the grid. They are polylines, polygons or polyhedra whose vertices are mesh Nodes. They are defined by a connectivity array indicating for each element the IDs of the nodes defining the element.**. \n", "\n", "Let us print again the *Mesh Group* information to find out which data items relate to mesh elements:" ] }, { "cell_type": "code", "execution_count": null, "id": "49aeb109", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh')" ] }, { "cell_type": "markdown", "id": "f04a2aa3", "metadata": {}, "source": [ "As shown by this print, many *Mesh Group* attributes are related to *Mesh Elements*. \n", "\n", "The `elements_path` attribute indicates the path of the *Data Array* item containing the elements connectivity array.\n", "\n", "The `Topology` attribute can have two values, **Uniform** (mesh composed of only one type of elements) or **Mixed** (mesh composed of several types of elements). Here the mesh is *uniform*. You will see an example of *Mixed* topology in the next section of this tutorial. All items of the *Mesh Group data Model* related to mesh elements will be reviewed again for the *Mixed* topology case." ] }, { "cell_type": "markdown", "id": "8d641e3c", "metadata": {}, "source": [ "*****\n", "#### Element types\n", "\n", "You can see 6 attributes that store *list* values. All those have one value for each type of element that composes the mesh. Here, as the mesh has a *Uniform* topology, they have only one value. Two of those attributes provide information on the type of elements:\n", "\n", "* **`elements_type`**: a list of the types of elements that is used in the mesh. These type names consist of a geometrical prefix indicating the geometrical nature of the element, followed by a number indicating the number of nodes in the element. Elements types are stored by decreasding dimensionality, in the case of a *Mixed* topology: the first elements of the lists are the elements types with the higher dimensionality in the mesh (**bulk element types**), and then element types with a lower dimensionality (**boundary element types**) are stored. The different type of elements supported, and their dimensionality, are:\n", " \n", " | Element type | Description |\n", " |---|---|\n", " |`point1` | A simple point |\n", " |`bar2`/`bar3` | A linear/quadratic line segment element (1D) |\n", " |`tri3` and `tri6` | A linear/quadratic triangle element (2D) |\n", " |`quad4` and `quad8` | A linear/quadratic quadrilateral element (2D) |\n", " |`tet4` and `tet10` | A linear/quadratic tetrahedron element (3D) |\n", " |`pyr5` and `pyr13` | A linear/quadratic pyramid (square basis) element (3D) |\n", " |`wed6` and `wed15` | A linear/quadratic wedge element (3D) |\n", " |`hex8` and `hex20` | A linear/quadratic hexaedron element (3D) |\n", " \n", " \n", "* **`Xdmf_elements_code`**: same as `elements_type` but with the element type names of the XDMF data model, [that can be found here](https://www.xdmf.org/index.php/XDMF_Model_and_Format#Topology) (used for synchronization with XDMF file) \n", "\n", "
\n", "\n", "**Note** \n", " \n", "You will find the geometric definition of these nodes into the `BasicTools/Containers/ElementNames.py` file of the *BasicTools* package. They are not presented here for the sake of brevity.\n", " \n", "\n", "
" ] }, { "cell_type": "markdown", "id": "5527c8f6", "metadata": {}, "source": [ "*****\n", "#### Elements count\n", "\n", "Other attributes are used to keep track of the number of elements: `Number_of_elements`, `Number_of_bulk_elements` `Number_of_boundary_elements`. They contain integer values indicating respectively how many elements, bulk elements (*i.e.* that have the same dimensionality of the mesh, 3D elements for 3D mesh for instance) or boundary elements (elements with a lower dimensionality than the mesh, 2D elements for a 3D mesh for instance) compose the mesh. \n", "\n", "These three attributes contain lists, that have a value for each element type of the list, and their order correspond to the element types stored in the `elements_type` attribute list." ] }, { "cell_type": "markdown", "id": "e5d338bd", "metadata": {}, "source": [ "The `Elements_offset` attribute also is a list with one element per element in `elements_type`. It contains the position of the first element of each type of element in the complete set of mesh elements. It is used by the class for consistency when interacting with *BasicTools* Elements objects. It is not relevant for *SampleData* users. " ] }, { "cell_type": "markdown", "id": "48df3710", "metadata": {}, "source": [ "Using all this information, we can determine that our mesh contains 8 elements, that are linear triangle elements (`tri3` elements). They are the 8 faces of our octahedron. A quicker way to retrieve this is to use the `get_mesh_elem_types_and_number` method, that returns a dictionary whose keys are the element type in the mesh, and values are the number of element for each type:" ] }, { "cell_type": "code", "execution_count": null, "id": "4cc3d029", "metadata": {}, "outputs": [], "source": [ "data.get_mesh_elem_types_and_number('mesh')" ] }, { "cell_type": "markdown", "id": "dc31a7cb", "metadata": {}, "source": [ "#### Elements connectivity array" ] }, { "cell_type": "markdown", "id": "2e4579c6", "metadata": {}, "source": [ "Like for the *Mesh Nodes*, all the data describing the *Mesh elements* is also gathered in the *Mesh Geometry Group*. Let us print again the content of this group:" ] }, { "cell_type": "code", "execution_count": null, "id": "cf29475c", "metadata": {}, "outputs": [], "source": [ "data.print_group_content('mesh_Geometry', short=True)\n", "data.print_index(local_root='mesh_Geometry')" ] }, { "cell_type": "markdown", "id": "3f8fc8b3", "metadata": {}, "source": [ "Like the nodes coordinate array, the element connectivity array is a *Data Array* node stored into the *Mesh Geometry Group*. Its *item name* is *Elements*. Let us print the content of this array: " ] }, { "cell_type": "code", "execution_count": null, "id": "05d6aaf9", "metadata": {}, "outputs": [], "source": [ "print(f'The shape of the mesh Elements connectivity array is {data[\"mesh_Elements\"].shape}','\\n')\n", "print(f'Its content is {data[\"mesh_Elements\"]}')" ] }, { "cell_type": "markdown", "id": "72f17714", "metadata": {}, "source": [ "This array contains for each element the IDs of the Nodes that compose the element : 24 nodes IDS, three for each triangle. We can see that the first element of the mesh is composed of the Nodes 0, 1 and 4. \n", "\n", "**The connectivity array is used as grid Topology array in the XDMF file of the dataset. Hence, it must comply with the XDMF format. For this reason, it is always stored as a 1D array. This format is detailed on the dedicated XDMF data format webpage, [here](https://www.xdmf.org/index.php/XDMF_Model_and_Format#Topology).** \n", "\n", "**In the case of a Uniform topology, the connectivities of the elements are stored consecutively.** As the number of nodes of each element is identical, the interpretation of the array is possible. **For the Mixed topology case** *(see example later in this tutorial)*, **the connectivity of each element must be preceded by a number that identifies the element type.** This number is the XMDF code for the element type. " ] }, { "cell_type": "markdown", "id": "2b1c3f47", "metadata": {}, "source": [ "The *SampleData* class offers more convenient ways to get the elements connectivity array. The first is the `get_mesh_xdmf_connectivity` method, that return the *Elements* Data array:" ] }, { "cell_type": "code", "execution_count": null, "id": "ad252623", "metadata": {}, "outputs": [], "source": [ "print(f'The mesh elements connectivity array is {data.get_mesh_xdmf_connectivity(\"mesh\")}')" ] }, { "cell_type": "markdown", "id": "31b459f0", "metadata": {}, "source": [ "The second is the `get_mesh_elements` method. With the meshname and one if its element types as arguments, it allows to get the connectivity of a type of elements with a classical 2D array format:" ] }, { "cell_type": "code", "execution_count": null, "id": "c1e87164", "metadata": {}, "outputs": [], "source": [ "data.get_mesh_elements(meshname='mesh', get_eltype_connectivity='tri3')" ] }, { "cell_type": "markdown", "id": "c09f07c1", "metadata": {}, "source": [ "#### Elements Sets/Tags" ] }, { "cell_type": "markdown", "id": "d87553be", "metadata": {}, "source": [ "Like *Node Tags*, *Mesh Group* can store *Element Tags* (= element sets) in the *Mesh Geometry Group*), that are stored in the *ElementsTags Group*, and listed in the *ElTagsList String array*. A second string array is associated to element tags: the *ElTagsTypeList* that contains the type of element constituting each tag (element tags can only be composed of a single type of elements). Like for node tags, they are explicit methods that allow to retrieve information on element tags:" ] }, { "cell_type": "code", "execution_count": null, "id": "13036f68", "metadata": {}, "outputs": [], "source": [ "data.get_mesh_elem_tags_names('mesh')" ] }, { "cell_type": "markdown", "id": "3c465838", "metadata": {}, "source": [ "Our *Mesh Group* contains 3 element tags, all containing `tri3` elements. The elements IDs and the specific connectivity of an element tag can easily be accessed using their name and the *Mesh Group* name: " ] }, { "cell_type": "code", "execution_count": null, "id": "accb097e", "metadata": {}, "outputs": [], "source": [ "el_tag = data.get_mesh_elem_tag(meshname='mesh', element_tag='2D')\n", "el_tag_connect = data.get_mesh_elem_tag_connectivity(meshname='mesh', element_tag='Bottom')\n", "# el_tag_connect = data.get_mesh_elem_tags_connectivity(meshname='mesh', element_tag='2D')\n", "\n", "print(f'The element tag `2D` contains the elements: {el_tag}.')\n", "print(f'The connectivity of the `Bottom` element tag is: \\n {el_tag_connect}')" ] }, { "cell_type": "markdown", "id": "c538b0ba", "metadata": {}, "source": [ "You can observe that the `get_mesh_elem_tag_connectivity` reshapes the connectivity array to a more usual 2D format, each row of the array corresponding to the connectivity of 1 element. " ] }, { "cell_type": "markdown", "id": "4fc89029", "metadata": {}, "source": [ "Like for *Node Tags*, the arrays containing the *Element Tags* data are stored in a specific group `ElementsTags`, subgroup of the *Mesh Geometry Group*. They contain the ID numbers of the elements constituting the *Element Tag*, and are named following the pattern : `ET_+tag name` (`ET` standing for element tag). \n", "\n", "Like for Nodes, *Mesh Groups* can also store *Mesh fields* for *Element Tags* to allow to visualize them. These fields have one boolean value per mesh element, which is equal to 1 on elements that are part of the *Element Tag*. The associated data arrays are also stored in the `ElementsTags` Group, and are named following the pattern : `field_+tag name`.\n", "\n", "Let us look at the content of this Group:" ] }, { "cell_type": "code", "execution_count": null, "id": "a68d9fb3", "metadata": {}, "outputs": [], "source": [ "data.print_group_content('mesh_ElemTags')" ] }, { "cell_type": "markdown", "id": "c134fdd2", "metadata": {}, "source": [ "We see that our 3 element tags have a field associated in the Mesh Group." ] }, { "cell_type": "markdown", "id": "a593e238", "metadata": {}, "source": [ "### Mesh fields" ] }, { "cell_type": "markdown", "id": "21382a9e", "metadata": {}, "source": [ "Like *Image Groups*, *Mesh Groups* can store fields. The list of the names of fields defined on the mesh group is stored in the *Field_Index* string array, like for *Image Groups* ( see [previous tutorial](./Image_data.ipynb)). It can be accessed easily using the `get_grid_field_list` method:" ] }, { "cell_type": "code", "execution_count": null, "id": "a2e7822c", "metadata": {}, "outputs": [], "source": [ "data.get_grid_field_list('mesh')" ] }, { "cell_type": "markdown", "id": "8eb9e070", "metadata": {}, "source": [ "We see in this list the various Node/Element tag fields, but also additional fields called `mesh_test_fieldX` and the two time values of the field with *indexname* `vector`. A specific section is dedicated to Mesh fields in this tutorial, so no additional detail on them will be provided here. " ] }, { "cell_type": "markdown", "id": "4dd27488", "metadata": {}, "source": [ "### Mesh XDMF grids" ] }, { "cell_type": "markdown", "id": "ad828c6a", "metadata": {}, "source": [ "We have now seen all the data contained in a *Mesh Group* in the HDF5 dataset of a *SampleData* object. To conclude this section on the *Mesh Group* data model, we will observe how this group is stored in the XDMF dataset:" ] }, { "cell_type": "code", "execution_count": null, "id": "d16f39b6", "metadata": {}, "outputs": [], "source": [ "data.print_xdmf()" ] }, { "cell_type": "markdown", "id": "af921e29", "metadata": {}, "source": [ "You can see that the test_mesh is stored as a Grid Node. This node contains a *Geometry* node, defined by a data array, which is the *Node coordinate* data array stored in the HDF5 dataset. It also contains a *Topology* Group, defined by the *Elements connectivity* data array stored in the HDF5 dataset. Finally, it contains one *Attribute* node for each field listed in the *Field Index* string array of the HDF5 dataset, defined by the associated data array in the HDF5 dataset. " ] }, { "cell_type": "markdown", "id": "3c193767", "metadata": {}, "source": [ "As already explained in the [tutorial 1 (sec. V)](./1_Getting_Information_from_SampleData_datasets.ipynb), the XDMF file allows to directly visualize the data described in it with the *Paraview* software. Let us try to visualize the Mesh Group that we have studied, with the `pause_for_visualization` method and the Paraview software.\n", "\n", "You have to choose the **XdmfReader** to open the file, so that Paraview may properly read the data. In the **Property** panel, tick only the `test_mesh` block to only plot the content of the Mesh Group, and then click on the **Apply** button.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "72e82499", "metadata": {}, "outputs": [], "source": [ "# Use the second code line if you want to specify the path of the paraview executable you want to use\n", "# otherwise use the first line \n", "#data.pause_for_visualization(Paraview=True)\n", "#data.pause_for_visualization(Paraview=True, Paraview_path='path to paraview executable')" ] }, { "cell_type": "markdown", "id": "4ab6751e", "metadata": {}, "source": [ "You should be able to produce visualization of the mesh geometry:\n", "\n", "\n", "\n", "or the mesh fields:\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "id": "18c5d8a4", "metadata": {}, "source": [ "Before moving to the next section on the creation of *Mesh Groups*, let us close our reference dataset:" ] }, { "cell_type": "code", "execution_count": null, "id": "d5d63873", "metadata": {}, "outputs": [], "source": [ "del data" ] }, { "cell_type": "markdown", "id": "acd00445", "metadata": {}, "source": [ "## III - Creating Mesh Groups " ] }, { "cell_type": "markdown", "id": "7dc1a5db", "metadata": {}, "source": [ "Like for *Image Groups* (see [tutorial 4 section III](./3_SampleData_Image_groups.ipynb)), *Mesh Groups* are created in *SampleData* instances through *Basictools* **mesh objects**. Creating meshes is a complex geometrical and mathematical task, that require specific tools (meshers like *gmsh* for instance). The *SampleData* code has not been designed to create meshes, but rather to manipulate them. In relevant application of the class, mesh data stored into *SampleData* datasets come from external tools. The main tasks are then to load and manipulate the data. This tutorial will mainly focus on those aspect, rather than mesh creation.\n", "\n", "Hence, in this section, we will only review some quick ways to create mesh objects, from simple mesh data, mesh files or other Python packages; and how to add them to a *SampleData* dataset. " ] }, { "cell_type": "markdown", "id": "27ddd201", "metadata": {}, "source": [ "### Creating mesh objects with BasicTools " ] }, { "cell_type": "markdown", "id": "7afcccdf", "metadata": {}, "source": [ "The *BasicTools* package implements a few methods that allow to create meshes with simple topologies. We will rely on them to recreate the mesh that we studied before, and introduce you to *mesh objects*. " ] }, { "cell_type": "markdown", "id": "a2d5fa80", "metadata": {}, "source": [ "#### BasicTools mesh creation tools" ] }, { "cell_type": "markdown", "id": "ef6d3516", "metadata": {}, "source": [ "The mesh creation tools of the *BasicTools* package can be imported with the following code line:" ] }, { "cell_type": "code", "execution_count": null, "id": "5f85012b", "metadata": {}, "outputs": [], "source": [ "import BasicTools.Containers.UnstructuredMeshCreationTools as UMCT" ] }, { "cell_type": "markdown", "id": "ee65afaa", "metadata": {}, "source": [ "This module contains functions that allow, from a set of points and a connectivity matrix, to create uniform meshes of one type of element. Learning in details how to use them is beyond the scope of this tutorial. Likewise, how to add data such as tags or fields to a mesh object will not be detailed in this tutorial. The reader is refered to *BasicTools* code to learn more on those methods/objects. \n", "\n", "These method include:\n", "\n", "* `CreateUniformMeshOfBars`\n", "* `CreateMeshOfTriangles`\n", "* `CreateMeshOf`\n", "* `CreateSquare`\n", "* `CreateCube`\n", "* `CreateMeshFromConstantRectilinearMesh`\n", "\n", "The `UnstructuredMeshCreationTools` contains test methods for each of these methods that can be read to understand how to use them.\n", "\n", "To recreate the octahedron mesh of the reference dataset studied in section II, we will use the `CreateMeshOfTriangles` method. For that, we will need to create two arrays, one for the node coordinates, one for the elements connectivity, and simply pass them to the *BasicTools* method:" ] }, { "cell_type": "code", "execution_count": null, "id": "8edcc558", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "# Mesh node coordinates array\n", "mesh_nodes = np.array([[-1.,-1., 0.],\n", " [-1., 1., 0.],\n", " [ 1., 1., 0.],\n", " [ 1.,-1., 0.],\n", " [ 0., 0., 1.],\n", " [ 0., 0.,-1.]])\n", "# Mesh connectivity array\n", "mesh_elements = np.array([[0, 1, 4],\n", " [0, 1, 5],\n", " [1, 2, 4],\n", " [1, 2, 5],\n", " [2, 3, 4],\n", " [2, 3, 5],\n", " [3, 0, 4],\n", " [3, 0, 5]])\n", "mesh = UMCT.CreateMeshOfTriangles(mesh_nodes, mesh_elements)" ] }, { "cell_type": "markdown", "id": "d2f71cf3", "metadata": {}, "source": [ "#### Mesh objects" ] }, { "cell_type": "markdown", "id": "6a073b21", "metadata": {}, "source": [ "The method return a *BasicTools* unstructured mesh object, a class dedicated to the manipulation of finite element meshes data. Printing this object provides information on the content of the mesh:" ] }, { "cell_type": "code", "execution_count": null, "id": "77cebf4e", "metadata": {}, "outputs": [], "source": [ "print(mesh)" ] }, { "cell_type": "markdown", "id": "7460c312", "metadata": {}, "source": [ "We will now complete this mesh object with additional data, by defining fields and node/element tags:" ] }, { "cell_type": "code", "execution_count": null, "id": "b3705f82", "metadata": {}, "outputs": [], "source": [ "# Here we recreate the Node tags of the reference dataset `test_mesh`\n", "mesh.nodesTags.CreateTag('Z0_plane', False).SetIds([0,1,2,3])\n", "mesh.nodesTags.CreateTag('out_of_plane', False).SetIds([4,5])\n", "\n", "# Here we recreate the Element tags of the reference dataset `test_mesh`\n", "mesh.GetElementsOfType('tri3').GetTag('Top').SetIds([0,2,4,6])\n", "mesh.GetElementsOfType('tri3').GetTag('Bottom').SetIds([1,3,5,7])\n", "\n", "# Here we add mesh node fields\n", "mesh.nodeFields['Test_field1'] = np.array([0., 0., 0., 0., 1., 0.])\n", "mesh.nodeFields['Test_field2'] = np.array([0., 0., 0., 0., 0., 1.])\n", "# Here we add mesh element fields\n", "mesh.elemFields['Test_field3'] = np.array([0., 1., 2., 3., 4., 5., 6., 7.])\n", "mesh.elemFields['Test_field4'] = np.array([1., 1., -1., -1., 1., 1., -1., -1.])" ] }, { "cell_type": "code", "execution_count": null, "id": "161eba98", "metadata": {}, "outputs": [], "source": [ "print(mesh)" ] }, { "cell_type": "markdown", "id": "fc41ee88", "metadata": {}, "source": [ "### Creating a Mesh Group from a Meshobject" ] }, { "cell_type": "markdown", "id": "d4f4765f", "metadata": {}, "source": [ "Our mesh object now contain all the data that is stored into the `test_mesh` Group of the reference dataset. We can now create a new *Sampledata* instance to see how we can create a *MeshGroup* from this mesh object:" ] }, { "cell_type": "code", "execution_count": null, "id": "b79c82f7", "metadata": {}, "outputs": [], "source": [ "data = SD(filename='test_mesh_dataset', overwrite_hdf5=True, autodelete=True)" ] }, { "cell_type": "markdown", "id": "cc43684c", "metadata": {}, "source": [ "To add a *Mesh Group* into this dataset, use the `add_mesh` method. Its use is very straightforward, you just have to provide the name, indexname, location of the *Mesh Group* you want to create, and the mesh object from which the data must be loaded. Like all the *data item* creation methods, it also has a `replace` argument (see [tutorial 3](./2_SampleData_basic_data_items.ipynb)).\n", "\n", "This methods has one specific argument, `bin_fields_from_sets`, that allow you to control if you want to simply store the Node/Element Tags as data arrays (in this case set it to `False`), or if you want to also store visualization fields for each Node/Element Tag in the mesh object.\n", "\n", "Let us create our mesh group, with visualization fields:" ] }, { "cell_type": "code", "execution_count": null, "id": "72c9a15d", "metadata": {}, "outputs": [], "source": [ "data.add_mesh(mesh_object=mesh, meshname='test_mesh', indexname='mesh', location='/', bin_fields_from_sets=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "50b5717a", "metadata": {}, "outputs": [], "source": [ "print(data)" ] }, { "cell_type": "markdown", "id": "4c5c1b12", "metadata": {}, "source": [ "That is it ! The *Mesh Group* has been created and all data has been stored into the *SampleData* dataset. Let us try to retrieve some information to check the loaded data:" ] }, { "cell_type": "code", "execution_count": null, "id": "8f6f1cd6", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_Geometry')\n", "print('Mesh elements: ',data.get_mesh_elem_types_and_number('mesh'))\n", "print('Mesh node tags: ', data.get_mesh_node_tags_names('mesh'))\n", "print('Mesh element tags:',data.get_mesh_elem_tags_names('mesh'))\n", "print('Element Tag \"Bottom\" connectivity: \\n',data.get_mesh_elem_tag_connectivity('mesh', 'Bottom'))" ] }, { "cell_type": "markdown", "id": "e25a308d", "metadata": {}, "source": [ "### Creating meshes from files" ] }, { "cell_type": "markdown", "id": "40944a18", "metadata": {}, "source": [ "In practice, creating relevant meshes for mechanical or material science application through *BasicTools* mesh creation tools is very limited. The most relevant use case consist in loading meshes from mesh files created by finite element or meshing softwares. Through *Basictools*, the *SampleData* class allows to directly load *Mesh Groups* from various mesh file formats, and the opposite: write mesh files in various formats from *Mesh Groups*. " ] }, { "cell_type": "markdown", "id": "eb4edef7", "metadata": {}, "source": [ "#### Load mesh files" ] }, { "cell_type": "markdown", "id": "eaa81a4d", "metadata": {}, "source": [ "We will use a mesh file used for *Pymicro* unit tests to illustrate the mesh file loading functionality. This file is a *.geof* file, the mesh file format used by the **Zset** finite element software. \n", "\n", "To load a mesh file from into a *SampleData* mesh group, use the `add_mesh` method, but instead of providing a *mesh object*, use the `file` argument and provide the mesh file name:" ] }, { "cell_type": "code", "execution_count": null, "id": "309f75bb", "metadata": {}, "outputs": [], "source": [ "meshfile_name = os.path.join(PYMICRO_EXAMPLES_DATA_DIR, 'cube_ref.geof')\n", "print(meshfile_name)\n", "\n", "data.add_mesh(file=meshfile_name, meshname='loaded_geof_mesh', indexname='mesh_geof', location='/', bin_fields_from_sets=True)\n", "\n", "print(data)" ] }, { "cell_type": "markdown", "id": "504054a0", "metadata": {}, "source": [ "The *Mesh Group* has been loaded as a *Mesh Group* in the dataset. Its data can be explored, and visualized, as presented above:" ] }, { "cell_type": "code", "execution_count": null, "id": "bd354d44", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_geof')\n", "print('Mesh elements: ',data.get_mesh_elem_types_and_number('mesh_geof'))\n", "print('Mesh node tags: ', data.get_mesh_node_tags_names('mesh_geof'))\n", "print('Mesh element tags:',data.get_mesh_elem_tags_names('mesh_geof'))" ] }, { "cell_type": "markdown", "id": "9f547a5d", "metadata": {}, "source": [ "As you can see, this mesh has a mixed topology, and has bulk (linear tetrahedra `tet4`) and boundary (linear triangles `tri3`) elements. This is the mesh of a cube, and it contains element tags to define its faces (`X0`, `X1`, `Y0` ...). As a training exercise, you may try to explore and visualize all the data contained into this mesh group !" ] }, { "cell_type": "markdown", "id": "113b81f0", "metadata": {}, "source": [ "This method will in principle work with the following files extensions :\n", "\n", "`.geof` `.inp`, `.asc`, `.ansys`, `.geo`, `.msh`, `.mesh`, `.meshb`, `.sol`, `.solb`, `.gcode`, `.fem`, `.stl`, `.xdmf`, `.pxdmf`, `.PIPE`, `.odb`, `.ut`, `.utp`, `.vtu`, `.dat`, `.datt`" ] }, { "cell_type": "markdown", "id": "4930b6c9", "metadata": {}, "source": [ "
\n", "\n", "**Note: MeshIO Bridge** \n", " \n", "If you use the *meshio* package to handle meshes, you can interface it with *SampleData* thanks to the *BasicTools* package. For that, you can use the `MeshToMeshIO` and `MeshIOToMesh` methods in the `BasicTools.Containers.MeshIOBridge` module of the *Basictools* package. These methods allow you to transform a *Basictools* mesh object into a *MeshIO* object. Hence, you can load a *meshio* mesh into a *SampleData* dataset by transforming it into a *Basictools* mesh object, and feeding it to the `add_mesh` method.\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "7910e3ed", "metadata": {}, "source": [ "## IV - Mesh Field data model" ] }, { "cell_type": "markdown", "id": "e56092af", "metadata": {}, "source": [ "In this new section, we will now focus on the definition of **mesh fields** data items. They are very similar to *image fields* data items, presented in the [previous tutorial, section IV](./3_SampleData_Image_groups.ipynb). They are data arrays enriched with metadata describing how they relate to the mesh they are associated to. Like *image fields*, these data arrays must follow some shape, ordering and indexing conventions to be added as mesh fields." ] }, { "cell_type": "markdown", "id": "bb0055f7", "metadata": {}, "source": [ "### SampleData Mesh fields conventions" ] }, { "cell_type": "markdown", "id": "f473efd4", "metadata": {}, "source": [ "#### Possible shapes and field types" ] }, { "cell_type": "markdown", "id": "663b36be", "metadata": {}, "source": [ "In the following, $N_n$, $N_e$ and $N_{ip}$ represent respectively the number of nodes, elements and intergration points of the mesh. Mesh fields can be added from *Numpy* array that can have the following shapes:\n", "\n", "1. $(N_n, N_c)$ or $(N_n)$ (equivalent to $N_c=1$). In this case, the field is a **nodal field**, whose values are defined at the mesh nodes\n", "1. $(N_e, N_c)$ or $(N_e)$ (equivalent to $N_c=1$). In this case, the field is an **element field**, whose values are defined on each mesh element (constant per element)\n", "1. $(N_{ip},N_c)$ or $(N_{ip})$ (equivalent to $N_c=1$). In this case, the field is an **integration point field**, whose values are defined at the mesh integration points. In practice, $N_{ip}$ can be any multiple of $N_e$, $N_{ip} = k*N_e$\n", "\n", "**If an array that do not have a compatible shape is added as mesh field, the class will raise an exception.**" ] }, { "cell_type": "markdown", "id": "c4729485", "metadata": {}, "source": [ "#### Field padding" ] }, { "cell_type": "markdown", "id": "0838010c", "metadata": {}, "source": [ "Finite element softwares may output fields only on bulk elements or boundary elements (variable flux, pressure field...) depending on the field nature. If such field must be loaded into a *SampleData* *Mesh Group* that has a mixed topology, with bulk and boundary elements, an *element* or *integration point* field array outputed by the software will not have a number of values compliant with the convention presented in the subsection above. \n", "\n", "The *SampleData* class handles automatically this particular issue. **Element and integration point fields** may be added from field data that are defined only on *bulk* or *boundary* elements. The class will padd them with zeros values for the element over which they are not defined, to get the proper array size.\n", "\n", "In practice, to add *element fields* / *integration point fields*, the input array can have the shape $(N_e, N_c)$ or $(N_e)$ / $(N_{ip}, N_c)$ or $(N_{ip})$, with $N_{ip} = k*N_e$ and $N_e$ being either the total number of elements in the mesh, the number of bulk elements in the mesh, or the number of boundary elements in the mesh. " ] }, { "cell_type": "markdown", "id": "e3a53e13", "metadata": {}, "source": [ "#### Integration points ordering convention and visualization" ] }, { "cell_type": "markdown", "id": "93268a4b", "metadata": {}, "source": [ "For each component of the field, integration point field array values are ordered by increasing integration point index first, and then by increasing element index, as follows:\n", "\n", "$Array[:,c] = [A_{ip1/e1}, A_{ip2/e1}, ..., A_{ip1/e2}, ...,A_{ipM/eN} ]$\n", "\n", "where $A_{ipi/ej}$ denotes the value of the array $A$ for the integration point $i$ of the element $j$, and $M$ and $N$ are respectively the number of integration point per element and the number of elements in the mesh. " ] }, { "cell_type": "markdown", "id": "f2a71636", "metadata": {}, "source": [ "**The XDMF format does not allow to specify integration points values for grids, and hence, these values cannot be directly visualized with Paraview and SampleData dataset yet.** However, when adding a an integration point field to a dataset, the user may chose to associate to it a visualization field that is an *element field* (1 value per element), which is the minimum, maximum or mean of the field value on all element integration points (see next section)." ] }, { "cell_type": "markdown", "id": "8c773fe8", "metadata": {}, "source": [ "#### Field dimensionality" ] }, { "cell_type": "markdown", "id": "f8a27abd", "metadata": {}, "source": [ "The size of the last dimension of the array determines the **number of field components** $N_c$, which defines the **field dimensionality**. If the array is 1D, then $N_c=1$.\n", "\n", "*SampleData* will interpret the field dimensionality (scalar, vector, or tensor) depending on the grid topology and the array shape. All possibilities are listed hereafter:\n", "* $N_c=1$: **scalar field**\n", "* $N_c=3$: **vector field**\n", "* $N_c=6$: **symetric tensor field** (*2nd order tensor*)\n", "* $N_c=9$: **tensor field** (*2nd order tensor*)\n", "\n", "The dimensionality of the field has the following implications:\n", "* **XDMF**: the dimensionality of the field is one of the metadata stored in the XDMF file, for Fields (`Attribute` nodes)\n", "* **visualization**: as it is stored in the XDMF file, Paraview can correctly interpret the fields according to their dimensionality. Il allows to plot separately each field component, and the field norm (magnitude). It also allows to use the Paraview *Glyph* filter to plot vector fields with arrows\n", "* **indexing**: The order of the value in the last dimension for non-scalar fields correspond to a specific order of the field components, according to a specific convention. These conventions will be detailed in the next subsection. \n", "* **compression**: Specific lossy compression options exist for fields. See dedicated tutorial.\n", "* **interface with external tools**: when interfacing SampleData with external softwares, such as numerical simulation tools, it is very practical to have fields with appropriate dimensionality accounted for (for instance to use a displacement or temperature gradient vector field stored in a *SampleData* dataset as input for a finite element simulation). " ] }, { "cell_type": "markdown", "id": "49179f46", "metadata": {}, "source": [ "#### Field components indexing" ] }, { "cell_type": "markdown", "id": "6f9103e5", "metadata": {}, "source": [ "To detail the convention, we will omit the spatial indexes $i,j$ or $k$ and only consider the last dimension of the field: $F[i,c] = F[c]$. The indexing convention to describe field components order in data arrays are:\n", "\n", "\n", "* For **vector fields** (3 components), the convention is $[F_0,F_1,F_2] = [F_x,F_y,F_z]$\n", "* For **symetric tensor fields** (2nd order, 6 components), the convention is\n", " $[F_0,F_1,F_2,F_3,F_4,F_5] = [F_{xx},F_{yy},F_{zz},F_{xy},F_{yz},F_{zx}]$\n", "* For **tensor fields** (2nd order, 9 components), the convention is \n", " $[F_0,F_1,F_2,F_3,F_4,F_5,F_6,F_7,F_8] = [F_{xx},F_{yy},F_{zz},F_{xy},F_{yz},F_{zx},F_{yx},F_{zy},F_{xz}]$\n", "\n", "
\n", "\n", "**Warning** \n", " \n", "Field components (example: $x,y,xx,zy$...) are assumed to be defined in the same frame as the grid. However, that cannot be ensured simply by providing a data array. Hence, the user must ensure to have this coincidence between the grid and the data. It is not mandatory, but not respecting this implicit convention may lead to confusions and geometrical misinterpretation of the data by other users or software. If you want to do it, a good idea would be to add attributes to the field to specify and explain the nature of the field components. \n", "\n", "
" ] }, { "cell_type": "markdown", "id": "4a0ef1b2", "metadata": {}, "source": [ "#### Components transposition" ] }, { "cell_type": "markdown", "id": "fe556af6", "metadata": {}, "source": [ "**Paraview component order convention is different from *SampleData* condition for tensors.** The convention in Paraview is:\n", "\n", "* $[F_0,F_1,F_2,F_3,F_4,F_5] = [F_{xx},F_{xy},F_{xz},F_{yy},F_{yz},F_{zz}]$ for symetric tensors\n", "* $[F_0,F_1,F_2,F_3,F_4,F_5,F_6,F_7,F_8] = [F_{xx},F_{xy},F_{xz},F_{yx},F_{yy},F_{yz},F_{zx},F_{zy},F_{zz}]$ for non symetric tensors\n", " \n", "For these reason, fields are stored with indices transposition in *SampleData* datasets so that their visualization with Paraview yield a rendering that is consistent with the ordering convention presented in the previous subsection. These transposition are automatically handled, as well as the back transposition when a field is retrieved, as you will see in the last section of this tutorial. \n", "\n", "**An attribute `transpose_components` is added to field data items. Its values represent the order in which components or the original array are stored in memory** (see examples below).\n", "\n", "\n", "
\n", "\n", "**Note** \n", " \n", "When visualizing a field in paraview, you may choose which component (including field magnitude) you are plotting in the box (*highlighted in red in the image below*) located in between the boxes for the choice of the visualization mode (*Surface in the image below*) and the data item choice (*tensor_field2D in the image below*). \n", " \n", "In this box, you will have to choose between 9 (0 to 8) components, even for symetric tensors. In the later case, the corresponding equal components (1 and 3: $xy$&$yx$, 2 and 6: $xz$&$zx$, 5 and 7: $yz$&$zy$) will yield the same visualization. \n", "\n", "\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "4c666d2d", "metadata": {}, "source": [ "#### Field attributes" ] }, { "cell_type": "markdown", "id": "7c40738f", "metadata": {}, "source": [ "Let us look more closely on a *Mesh Group Field* data item, from the test mesh that we created earlier:" ] }, { "cell_type": "code", "execution_count": null, "id": "ddb831e8", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_Test_field1')\n", "data.print_node_info('mesh_Test_field3')" ] }, { "cell_type": "markdown", "id": "c6f28cc7", "metadata": {}, "source": [ "As you can see, *Field data item* attributes provide information on the field dimensionality, type and padding. Here for instance, the two fields `Test_field1` and `Test_field3` are scalar fields. The `Test_field3` is an *element field*, that has been inputed with an array containing one value per bulk elemenbt of the mesh." ] }, { "cell_type": "markdown", "id": "3c2adf52", "metadata": {}, "source": [ "The `parent_grid_path` and `xdmf_gridname` attribute provides the path of the *Image Group* and the XDMF Grid Node name to which this field belongs. \n", "The `xdmf_fieldname` provides the name of the XDMF Attribute Node associated to the field.\n", "The `padding` attribute is of no use for Image fields. It will be presented in the next tutorial for mesh fields." ] }, { "cell_type": "markdown", "id": "68867ca6", "metadata": {}, "source": [ "## V - Adding Fields to Image Groups" ] }, { "cell_type": "markdown", "id": "cd5ae12b", "metadata": {}, "source": [ "Like for *Image Group Fields* (see [previous tutorial, section V](./3_SampleData_Image_groups.ipynb)), *Mesh Group Fields* can be created in a *SampleData* dataset using the `add_field` method. " ] }, { "cell_type": "markdown", "id": "53ac5ffd", "metadata": {}, "source": [ "To add a field to a grid from a *numpy* array, you just have to call `add_field` like you would have called `add_data_array`, with an additional argument `gridname`, that indicates the mesh group to which you want to add the field. \n", "\n", "The analysis of the field dimensionality, nature, padding and required transpositions is automatically handled by the class. \n", "\n", "If the added field is an *integration point field*, the value of the `visualisation_type` argument will control the creation of an associated visualization field. Its possible values are:\n", "\n", "* `visualization_type='Elt_mean'`: a visualization field is created by taking the mean of integration point values in each element \n", "* `visualization_type='Elt_max'`: a visualization field is created by taking the mean of integration point values in each element \n", "* `visualization_type='Elt_min'`: a visualization field is created by taking the mean of integration point values in each element \n", "* `visualization_type='None'`: no visualization field is created\n", "\n", "The `add_field` method has already been presented in the last [tutorial](./3_SampleData_Image_groups.ipynb). Please refer to it to see examples on how to use this method, in particular to create Field time series. The rest of this section is dedicated to a few example of *Field data items* creation specificities for mesh groups.\n", "\n", "The fields will be created o,n the mesh `mesh_geof`. Let us print again information on this mesh topology:" ] }, { "cell_type": "code", "execution_count": null, "id": "cb0599eb", "metadata": {}, "outputs": [], "source": [ "data.print_node_info('mesh_geof')" ] }, { "cell_type": "markdown", "id": "fd7af370", "metadata": {}, "source": [ "### Example 1: Creating a vector node field" ] }, { "cell_type": "markdown", "id": "bab3eb03", "metadata": {}, "source": [ "Node vector fields are a very common data in material science and material mechanics. They can be for instance a displacement or velocity field computed from a simulation solver or experimentally measured, a magnetic field...\n", "\n", "To create a node vector field, we need to create a field data array that has one value for each node in the mesh. As we can see above, the *Mesh Group* has a `number_of_nodes` that provides us with this information ($N_n$). To create a vector field, each value of the field must be a 1D vector of 3 values. \n", "\n", "We have to create a data array of shape $(N_n, 3)$. Let us do it by creating a random array:" ] }, { "cell_type": "code", "execution_count": null, "id": "cddae498", "metadata": {}, "outputs": [], "source": [ "# random array creation\n", "array = np.random.rand(data.get_attribute('number_of_nodes','mesh_geof'),3) - 0.5" ] }, { "cell_type": "code", "execution_count": null, "id": "b944fd85", "metadata": {}, "outputs": [], "source": [ "# creation of the mesh field\n", "data.add_field(gridname='mesh_geof', fieldname='nodal_vectorF', array=array, indexname='vectF', replace=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "12f83f74", "metadata": {}, "outputs": [], "source": [ "# Printing information on our created field:\n", "print(f'Is \"vectF\" in the field list of the mesh ? {\"vectF\" in data.get_grid_field_list(\"mesh_geof\")}')\n", "data.print_node_info('vectF')" ] }, { "cell_type": "markdown", "id": "37a0d00d", "metadata": {}, "source": [ "We can verify the field attributes to check that it is indeed a vector field, a nodal field (hence with no padding), that is defined on the `loaded_geof_mesh` *Mesh Group*.\n", "\n", "You can also visualize the field with Paraview :" ] }, { "cell_type": "code", "execution_count": null, "id": "56b71f65", "metadata": {}, "outputs": [], "source": [ "# Use the second code line if you want to specify the path of the paraview executable you want to use\n", "# otherwise use the first line \n", "#data.pause_for_visualization(Paraview=True)\n", "#data.pause_for_visualization(Paraview=True, Paraview_path='path to paraview executable')" ] }, { "cell_type": "markdown", "id": "cc2b631f", "metadata": {}, "source": [ "Open the XDMF dataset with the **XdmfReader** of Paraview, and then, in the *Property* panel, tick only the box associated to the the `loaded_geof_mesh` block. Then you can use the **Glyph** filter, which allows you to plot vector fields with arrows. You should be able to get visualization looking like the image below:\n", "\n", "" ] }, { "cell_type": "markdown", "id": "791ca349", "metadata": {}, "source": [ "### Example 2: Creating a scalar element field for boundary elements" ] }, { "cell_type": "markdown", "id": "77c6c840", "metadata": {}, "source": [ "Scalar fields defined on boundary can correspond for instance to pressure fields applied on a body, or a surface temperature measurement. They are also a common type of data for material mechanics datasets.\n", "\n", "To add one, we need to create a 1D array (as the field is scalar) that has one value per boundary element in the mesh. in the present case, the mesh has as many bulk as boundary elements. To solve the ambiguity, the add field method has a `bulk_padding` argument. If it is `True` (default value), the field is padded as a bulk element field. If it is set to `False`, the field is padded as a boundary element field. " ] }, { "cell_type": "code", "execution_count": null, "id": "6fcfc893", "metadata": {}, "outputs": [], "source": [ "# Random array creation. Warning ! 'Number_of_boundary_elements' attribute is a list !\n", "array = np.random.rand(data.get_attribute('Number_of_boundary_elements','mesh_geof')[0]) \n", "# creation of the mesh field\n", "data.add_field(gridname='mesh_geof', fieldname='boundary_scalarF', array=array, indexname='boundaryF', replace=True,\n", " bulk_padding=False)" ] }, { "cell_type": "code", "execution_count": null, "id": "3b49d0c4", "metadata": {}, "outputs": [], "source": [ "# Printing information on our created field:\n", "print(f'Is \"boundaryF\" in the field list of the mesh ? {\"boundaryF\" in data.get_grid_field_list(\"mesh_geof\")}')\n", "data.print_node_info('boundaryF')" ] }, { "cell_type": "markdown", "id": "fe05d532", "metadata": {}, "source": [ "We can verify the field attributes to check that it is indeed a scalar element field, padded as a boundary element field, that is defined on the `loaded_geof_mesh` *Mesh Group*. \n", "\n", "Once again, you can visualize your field with the Paraview software:" ] }, { "cell_type": "markdown", "id": "d24771e6", "metadata": {}, "source": [ "Open the XDMF dataset with the **XdmfReader** of Paraview, and then, in the *Property* panel, tick only the box associated to the the `loaded_geof_mesh` block. Then you can use the **Clip** filter, which allows you to cut your grid with a customizable plane and hence plot data within the bulk of your mesh, as the right picture below:\n", "\n", "\n", "\n", "As you can see, the visual rendering of the field displays the random values of the field on the boundary triangle elements of the external surface of the mesh, and 0 values within the bulk of the mesh: *SampleData* has automatically padded the field array with 0 at positions corresponding to bulk elements to ensure compatibility with the XDMF format, and hence visualization." ] }, { "cell_type": "markdown", "id": "5b4682ff", "metadata": {}, "source": [ "### Example 3: Creating a time serie for a tensorial integration point field for bulk elements" ] }, { "cell_type": "markdown", "id": "69e60c42", "metadata": {}, "source": [ "In this last example, we will add a field representing a uniaxial stress state to the mesh, proportional to time. We need to create a symetric tensor field, with 6 components. We will suppose that this field comes from a finite element solver output, that provided the full stress tensor at each integration point in the mesh bulk elements. We will suppose that the mesh elements have 4 integration points each, and that the values have been outputed at three instants.\n", "\n", "Hence, we have to create 3 arrays of shape $(4N_{be},6)$, where $N_{be}$ is the number of bulk elements in the mesh. " ] }, { "cell_type": "code", "execution_count": null, "id": "e0be5140", "metadata": {}, "outputs": [], "source": [ "# Arrays creation\n", "array_T0 = np.zeros((4*data.get_attribute('Number_of_bulk_elements','mesh_geof')[0],6)) \n", "array_T1 = np.zeros((4*data.get_attribute('Number_of_bulk_elements','mesh_geof')[0],6))\n", "array_T2 = np.zeros((4*data.get_attribute('Number_of_bulk_elements','mesh_geof')[0],6))\n", "\n", "# time values:\n", "T0 = 0.\n", "T1 = 1.\n", "T2 = 10.\n", "\n", "# Filling stress state values\n", "# at second time increment --> stress = 100*t in direction Z/3 (see indexing conventions)\n", "array_T1[:,2] = 100*T1\n", "# at second time increment --> stress = 100*t in direction Z/3 (see indexing conventions)\n", "array_T2[:,2] = 100*T2\n", "\n", "# create fields\n", "data.add_field(gridname='mesh_geof', fieldname='tensor_ipF', array=array_T0, indexname='tensorF', replace=True,\n", " time=T0)\n", "data.add_field(gridname='mesh_geof', fieldname='tensor_ipF', array=array_T1, indexname='tensorF', replace=True,\n", " time=T1)\n", "data.add_field(gridname='mesh_geof', fieldname='tensor_ipF', array=array_T2, indexname='tensorF', replace=True,\n", " time=T2)" ] }, { "cell_type": "code", "execution_count": null, "id": "15ac80a3", "metadata": {}, "outputs": [], "source": [ "print(data.get_grid_field_list('mesh_geof'))\n", "data.print_node_info('tensorF_T0_0')\n", "data.print_node_info('tensorF_T10_0')" ] }, { "cell_type": "markdown", "id": "36535bc6", "metadata": {}, "source": [ "You can see that the fields have been automatically added as integration point fields symmetric tensor (`Tensor6` `IP_field`). You can also observe the apparition of the `transpose_components` attribute, that has been already introduce in the [last tutorial](./3_SampleData_Image_groups.ipynb). \n", "\n", "You can also observe that the fields have a `visualisation_field_path` and a `visualisation_type` attributes. They indicate respectively the path of the visualization field data item associated to our added field, as well as the type of operation that has been used to create it (here, taking the mean of integration points value within each element to create an element wise constant field: `Elt_mean`).\n", "\n", "You can try to modify the calls to `add_field` in the previous cells by adding the `visualisation_type` argument, and setting its value to `Elt_max`, or `None`, and observe the differences in the field data item attributes, and the field visualization.\n", "\n", "Let us now look at the associated XDMF tree structure:" ] }, { "cell_type": "code", "execution_count": null, "id": "21118017", "metadata": {}, "outputs": [], "source": [ "data.print_xdmf()" ] }, { "cell_type": "markdown", "id": "5f922b0c", "metadata": {}, "source": [ "As expected, the *Attributes* of our grid time collection `loaded_geof_mesh` that contain the tensor field do not point towards the added data array, but towards the visualization field data array: `tensor_ipF_T0_0_Elt_mean`, `tensor_ipF_T1_0_Elt_mean` or `tensor_ipF_T2_0_Elt_mean`." ] }, { "cell_type": "markdown", "id": "72e47573", "metadata": {}, "source": [ "## VI - Getting Mesh Groups and Mesh Fields" ] }, { "cell_type": "markdown", "id": "ecfa668c", "metadata": {}, "source": [ "### Getting mesh objects from Mesh groups" ] }, { "cell_type": "markdown", "id": "d093569a", "metadata": {}, "source": [ "Like *Image groups*, you can easily retrieve a complete *SampleData Mesh Group* under the form of a *BasicTools* Mesh object, with the `get_mesh` method:" ] }, { "cell_type": "code", "execution_count": null, "id": "daa34062", "metadata": {}, "outputs": [], "source": [ "mesh = data.get_mesh('mesh_geof', with_fields=True, with_tags=True)\n", "print(mesh)" ] }, { "cell_type": "markdown", "id": "b9775455", "metadata": {}, "source": [ "This method has two arguments that allows you to control how rich you want your output to be. By setting `with_fields` to `False`, the return mesh object will not contain the field arrays stored on the *Mesh Group*. By setting `with_tags` to `False`, the return mesh object will not contain the Node and Element tags stored in the *Mesh Group*. If both are `False`, the mesh object will only contain the mesh nodes and elements. " ] }, { "cell_type": "markdown", "id": "1ea2165a", "metadata": {}, "source": [ "### Getting mesh Fields" ] }, { "cell_type": "markdown", "id": "2387b026", "metadata": {}, "source": [ "As for image fields, retrieveing mesh fields from *Sampledata* dataset is done with the `get_field` method. All padding, transpositions etc... are automatically reverted to return the originally inputed data array.\n", "\n", "In the case of mesh fields, you can prevent the reversion of field padding with the `unpad_field` argument (set it to `False`). You can also choose, when getting an integration point field, to get the complete data array, or on the contrary, only the associated visualization field data array, by setting the `get_visualisation_field` argument to `True`.\n", "\n", "Let us show these various use of the method by trying to get the tensor field created in the third exampe above." ] }, { "cell_type": "code", "execution_count": null, "id": "997b01f0", "metadata": {}, "outputs": [], "source": [ "# getting the inputed array --> no options\n", "O_array = data.get_field('tensorF_T1_0')\n", "# getting the visualization array \n", "V_array = data.get_field('tensorF_T1_0', get_visualisation_field=True)\n", "# getting the unpadded visualization array\n", "UPV_array = data.get_field('tensorF_T1_0', unpad_field=False, get_visualisation_field=True)\n", "\n", "print(f' Original array shape is {O_array.shape}, unpadded array shape is {V_array.shape},'\n", " f' unpadded visualization array shape is {UPV_array.shape}')\n", "\n", "print(f'\\nIs it the original inputed array ? {np.all(O_array == array_T1)} \\n')" ] }, { "cell_type": "markdown", "id": "7667e85e", "metadata": {}, "source": [ "As you can see, using `get_field` allow you to retrieve exactly the original array that you inputed, reverting all transformations ($1536 = 4*384$ here). Using the `get_visualisation_field` argument, you retrieve the unpadded visualization field with values only for the mesh bulk elements, as inputted ($384$ values here); and adding the `unpad_field=False` argument, you get the field with the added 0, that has one value per element in the mesh ($768$ values here). " ] }, { "cell_type": "markdown", "id": "f3825eec", "metadata": {}, "source": [ "****\n", "\n", "**This is the end of this tutorial on SampleData Mesh Groups ! We can now close our dataset to conclude it.** " ] }, { "cell_type": "code", "execution_count": null, "id": "bef6a7cb", "metadata": {}, "outputs": [], "source": [ "del data" ] } ], "metadata": { "kernelspec": { "display_name": "env_dev", "language": "python", "name": "env_dev" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.17" } }, "nbformat": 4, "nbformat_minor": 5 }