Creating a CAE Plugin – Understanding The Pointwise Grid Model API (Part 2)

This post will cover the structured Pointwise grid model (PWGM). In Part 1 of Understanding The Pointwise Grid Model API, I covered the unstructured Pointwise grid model. If you have not done so already, I suggest you read Part 1 before continuing.

This discussion assumes you are already familiar with the i-j-k coordinate system used by structured grids. This discussion also assumes you are familiar with the six faces of a structured Block. The PWGM refers to them as the i-min, j-min, k-min, i-max, j-max, and k-max block faces. If you do not understand structured Blocks, you may want to do some homework first.

Grid Handles

Throughout this post, I will be referring to Handles. Handles are a compact way to uniquely identify a PWGM object without using pointers. For those of you familiar with C/C++, a Handle can be considered the SDK’s equivalent of a C++ this pointer. The unstructured PWGM supports five Handle types:

  • PWGM_HGRIDMODEL
  • PWGM_HBLOCK
  • PWGM_HBNDRY
  • PWGM_HCNXN

Each Handle type has one or more SDK functions that is used to query the entity that the Handle represents. You can also extract information from a Handle using various SDK macros. See the Modules/PWGM-API Opaque Data Handle Types section of the CAE Plugin SDK documentation for the details.

Grid Dimensionality

As in Pointwise itself, the structured PWGM supports both 2D and 3D meshes. However, there is one key difference: The structured PWGM provides the same interface to the mesh data independently of its 2D or 3D dimensionality. The uniformity of the PWGM is accomplished using five generic entities; Blocks, Connections, Boundaries, Vertices and Conditions. Specifically, from the structured PWGM’s point of view:

  • A Block is collection of block Vertices, Connections, and Boundaries. Each Block is assigned a volume Condition and is identified by an unique, integer index. The indices range from 0 to the number of blocks in the model (Nb) minus 1 (0 … Nb-1).
  • A Connection defines a rectangular portion of a Block’s perimeter vertices that are “shared” with a neighboring Block. A Block can have multiple Connections. A top level Connection is identified by an unique, integer index. The indices range from 0 to the number of Connections in the model (Nc) minus 1 (0 … Nc-1). A Block level Connection is identified by an integer index unique within each Block. The indices range from 0 to the number of Connections in the Block (Ncb) minus 1 (0 … Ncb-1).
  • A Boundary defines a rectangular portion of a Block’s perimeter vertices that are unique to the Block and are not “shared” with a neighboring Block. Each Boundary is assigned a boundary Condition. A top level Boundary is identified by an unique, integer index. The indices range from 0 to the number of Boundaries in the model (Nd) minus 1 (0 … Nd-1). A Block level Boundary is identified by an integer index unique within each Block. The indices range from 0 to the number of Boundaries in the Block (Ndb) minus 1 (0 … Ndb-1).
  • A Vertex is a single XYZ Block coordinate identified by a PWGM_INDEX3 value. A PWGM_INDEX3 value represents the vertex’s [i, j, k] index within a Block. The ijk component values range from zero to the number of ijk points minus 1 ([0,0,0] to [Ni-1,Nj-1,Nk-1]). The same datatype is used for both 2D and 3D grids. However, for 2D grids, the k-component of the index is always zero.
  • A Condition is a set of two user defined and two solver defined attributes.

A plugin accesses this entity hierarchy using the same function calls. The distinction between 2D and 3D only comes into play when dealing with Vertices, Connections and Boundaries.

Grid Hierarchy

The structured PWGM is arranged in a hierarchy. The root of this hierarchy is the grid model itself. At export, the grid model’s Handle is passed to the plugin’s runtimeWrite() function using the PWGM_HGRIDMODEL model parameter.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
PWP_BOOL runtimeWrite(CAEP_RTITEM *pRti, PWGM_HGRIDMODEL model, const CAEP_WRITEINFO *pWriteInfo);
// model and pWriteInfo are also available using pRti->model and pRti->pWriteInfo
[/sourcecode]

The structured PWGM provides a single, block centric view of the grid data. This view consists of

  • One top level array of Connections.
  • One top level array of Boundaries.
  • One top level array of Blocks in which each Block contains
    • One PWGM_BLOCKDATA instance.
    • One array of Block Vertices
    • One array of Block Connections.
      • Each array entry represents a Connection this Block makes with itself or other Blocks.
      • This array is a subset of the top-level Connections array.
    • One array of Block Boundaries.
      • This array is a subset of the top-level Boundaries array.

The structured grid model hierarchy is represented in the figure below.

The structured grid model hierarchy

The structured grid model hierarchy

Structured Datatypes

Each Vertex in the array-like layout of a structured block is accessed using an [i,j,k] index. The PWGM uses the PWGM_INDEX3 datatype to represent an [i,j,k] index. The PwBlkNdxVertData() function is used to get a vertex’s PWGM_VERTDATA information.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
typedef struct PWGM_INDEX3_t {
PWP_INT32 i; // i-coordinate used for 3D and 2D grids
PWP_INT32 j; // j-coordinate used for 3D and 2D grids
PWP_INT32 k; // k-coordinate used for 3D grids only
}
PWGM_INDEX3;

PWP_BOOL PwBlkNdxVertData(PWGM_HBLOCK block, PWGM_INDEX3 ndx3, PWGM_VERTDATA *pVertData);
[/sourcecode]

Similarly, the PWGM_STR_SIZE datatype is used to represent the [i,j,k] vertex size of a Block. A PWGM_STR_SIZE is used for both 2D and 3D grids. However, for 2D grids, the k-component of the size is always one. The PwBlkSize() function is used to get the size of a Block.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
typedef PWGM_INDEX3 PWGM_STR_SIZE;

PWP_BOOL PwBlkSize (PWGM_HBLOCK block, PWGM_STR_SIZE *pSize);
[/sourcecode]

A PWGM_STR_RANGE datatype represents a Block sub-region. A range is defined by its diagonal beginning (beg) and ending (end) indices.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
typedef struct PWGM_STR_RANGE_t {
PWGM_INDEX3 beg; // beginning index
PWGM_INDEX3 end; // ending index
}
PWGM_STR_RANGE;
[/sourcecode]

To be valid, the beginning index of a range must be less than or equal to the ending index.

  • beg.i <= end.i
  • beg.j <= end.j
  • beg.k <= end.k

In the PWGM, ranges are used primarily when dealing with Boundaries and Connections.

It’s All About Connections

In the PWGM, each structured Block has its own local ijk coordinate system. As a result, each vertex in a Connection between two neighboring Blocks can be accessed using multiple PWGM_INDEX3 indices. This can also happen when a Block makes a Connection with itself (e.g. C, O or H topology). Working with multiple local coordinate systems often makes exporting Connections the most complicated part of a structured plugin.

The image below shows a simple 2D model containing two, connected Blocks. Each Block has a different ij orientations. As required, the right-handed normal (i cross j) for each 2D Block points towards the same “side” of the grid model (out of the screen in this example).

2D Structured Blocks

Two, 2D structured blocks with their vertex indices labeled.

Each of these Blocks has one Connection. These two, individual, Block level Connections are also present in the top level Connections array. The two Connections are listed below.

  • Block[0] from its i-max face to the j-max face of Block[1]
  • Block[1] from its j-max face to the i-max face of Block[0]

As you can see, the Vertices of this connection are indexed differently through Block[0] than they are through Block[1]. Given that k is 0 for 2D grids,

  • Vertex[2,0,0] in Block[0] is the same as Vertex[0,2,0] in Block[1]
  • Vertex[2,1,0] in Block[0] is the same as Vertex[1,2,0] in Block[1]
  • Vertex[2,2,0] in Block[0] is the same as Vertex[2,2,0] in Block[1]

The PWGM uses a transformation matrix to convert Connection ijk indices between Blocks. Using this matrix, along with some SDK matrix utility functions, a Connection ijk index in one block can be converted to the equivalent ijk index in the neighboring Block. A Connection is defined using the PWGM_CNXNDATA datatype.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
typedef struct PWGM_CNXNDATA_t {
const char *name; // connection name
PWGM_HBLOCK block1; // connection block 1 (source)
PWGM_FACE_ID face1; // face Id on block 1
PWGM_STR_RANGE range1; // ijk connection range in block1 index space
PWGM_INDEX_XFORM from1to2; // transforms block1 index to block2 index
PWGM_HBLOCK block2; // connection block 2 (donor)
PWGM_FACE_ID face2; // face Id on block2
PWGM_STR_RANGE range2; // ijk connection range in block2 index space
PWGM_INDEX_XFORM from2to1; // transforms block2 index to block1 index (== inverse(from1to2)
}
PWGM_CNXNDATA;
[/sourcecode]

In PWGM_CNXNDATA above, the PWGM_INDEX_XFORM from1to2 and PWGM_INDEX_XFORM from2to1 matrices are 3D transforms. If your solver requires 2D matrices (PWGM_INDEX_XFORM2), matrix conversion utility functions are available (see below).

The following PWGM functions are used to access information about Connections:

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
// top level Connection functions
PWP_UINT32 PwModConnectionCount (PWGM_HGRIDMODEL model);
PWGM_HCNXN PwModEnumConnections (PWGM_HGRIDMODEL model, PWP_UINT32 ndx);
PWP_BOOL PwModNdxConnection (PWGM_HGRIDMODEL model, PWP_UINT32 ndx, PWGM_CNXNDATA *pCnxnData);

// Block level Connection functions
PWP_UINT32 PwBlkConnectionCount (PWGM_HBLOCK block);
PWGM_HCNXN PwBlkEnumConnections (PWGM_HBLOCK block, PWP_UINT32 ndx);
PWP_BOOL PwBlkNdxConnection (PWGM_HBLOCK block, PWP_UINT32 ndx, PWGM_CNXNDATA *pCnxnData);

// Get a handle’s Connection data
PWP_BOOL PwConnection (PWGM_HCNXN connection, PWGM_CNXNDATA *pCnxnData);
[/sourcecode]

To work with matrices, the PWGM provides the following structured grid utility functions.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]

PWP_BOOL PwXform2to3 (const PWGM_INDEX_XFORM2 *pX2, PWGM_INDEX_XFORM *pX3);
PWP_BOOL PwXform3to2 (const PWGM_INDEX_XFORM *pX3, PWGM_INDEX_XFORM2 *pX2);

PWGM_INDEX3 PwXformApply (const PWGM_INDEX_XFORM *pX3, PWGM_INDEX3 ijk);
PWGM_ENUM_IJK PwXformFollows (const PWGM_INDEX_XFORM *pX3, PWGM_ENUM_IJK localAxis, PWP_BOOL *pFlipped);

PWGM_INDEX3 PwXform2Apply (const PWGM_INDEX_XFORM2 *pX2, PWGM_INDEX3 ijk);
PWGM_ENUM_IJK PwXform2Follows (const PWGM_INDEX_XFORM2 *pX2, PWGM_ENUM_IJK localAxis, PWP_BOOL *pFlipped);

PWP_BOOL PwInRange (PWGM_INDEX3 ijk, const PWGM_STR_RANGE *pRange);

[/sourcecode]

This example refers to the 2D grid model from above and shows how to use the index transform functions.

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
// get handle to Block[0]
PWGM_HBLOCK blk0 = PwModEnumBlocks(model, 0);

// get Block[0].Connection[0]
PWGM_CNXNDATA cnxnData;
if (PwBlkNdxConnection(blk0, 0, &cnxnData)) {
// init index blk0Ndx using Block[0]’s ijk space
PWGM_INDEX3 blk0Ndx = {2, 1, 0};

// map blk0Ndx to the same index in blk1Ndx’s ijk space
PWGM_INDEX3 blk1Ndx = PwXformApply(&cnxnData.from1to2, blk0Ndx);

// blk1Ndx is now set to {1, 2, 0}
}
[/sourcecode]

Consider yourself fortunate if the solver your plugin is exporting to also uses Connection matrices. However, if your solver uses a different mapping method, your plugin must convert each PWGM Connection matrix to a form suitable for the solver. Sadly, the specifics of converting a PWGM Connection matrix to a solver’s non-matrix mapping scheme can be complex and is beyond the scope of this post.

Boundaries Are Limiting

A Boundary is any portion of a Block’s face that lies on the grid model’s perimeter that does not have any vertices in common with a neighboring Block. A Boundary is accessed using a PWGM_HBNDRY handle. With this handle, you can obtain a Boundary’s shape and condition information using the PWGM_BNDRYDATA and PWGM_CONDDATA datatypes. The Boundary API datatypes and functions are listed below:

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
typedef enum PWGM_FACE_ID_e {
PWGM_FACE_KMIN,  // min K
PWGM_FACE_KMAX,  // max K
PWGM_FACE_IMIN,  // min I
PWGM_FACE_IMAX,  // max I
PWGM_FACE_JMIN,  // min J
PWGM_FACE_JMAX,  // max J
}
PWGM_FACE_ID;

typedef struct PWGM_BNDRYDATA_t {
const char *name; // boundary name
PWGM_HBLOCK block; // boundary block
PWGM_FACE_ID face; // boundary face id
PWGM_STR_RANGE range; // boundary ijk range
}
PWGM_BNDRYDATA;

typedef struct PWGM_CONDDATA_t {
const char *name; // grid-defined condition name
PWP_UINT32 id; // grid-defined condition id
const char *type; // cae-defined condition physical type name
PWP_UINT32 tid; // cae-defined condition id
}
PWGM_CONDDATA;

PWP_BOOL PwModNdxBoundary (PWGM_HGRIDMODEL model, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData);
PWP_BOOL PwModNdxBoundaryAndCondition (PWGM_HGRIDMODEL model, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData, PWGM_CONDDATA *pCondData);

PWP_BOOL PwBlkNdxBoundary (PWGM_HBLOCK block, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData);
PWP_BOOL PwBlkNdxBoundaryAndCondition (PWGM_HBLOCK block, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData, PWGM_CONDDATA *pCondData);

PWP_BOOL PwBoundary (PWGM_HBNDRY boundary, PWGM_BNDRYDATA *pBndryData);
PWP_BOOL PwBndryCondition (PWGM_HBNDRY boundary, PWGM_CONDDATA *pCondData);
[/sourcecode]

Enumerating Entities

The PWGM provides enumerated, random access to all grid entities. Using this scheme, an SDK function is provided for each entity to ascertain the number of items that exist in the grid model. The count then is used with other related enumeration SDK functions to step through the item hierarchy one by one. For example, the number of grid model blocks is determined using the PwModBlockCount() function. With this count, you then enumerate the blocks using the PwModEnumBlocks() function. The PWGM enum functions return a Handle to the requested item. The top, model level function names begin with the PwMod prefix. These functions all take a grid model Handle as their input and return either a count or a Handle to an enumerated grid model entity. The top, model level functions currently supported by the structured PWGM are

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
PWP_UINT32 PwModBlockCount (PWGM_HGRIDMODEL model)
PWGM_HBLOCK PwModEnumBlocks (PWGM_HGRIDMODEL model, PWP_UINT32 ndx)

PWP_UINT32 PwModConnectionCount (PWGM_HGRIDMODEL model);
PWGM_HCNXN PwModEnumConnections (PWGM_HGRIDMODEL model, PWP_UINT32 ndx);

PWP_UINT32 PwModBoundaryCount (PWGM_HGRIDMODEL model);
PWGM_HBNDRY PwModEnumBoundaries (PWGM_HGRIDMODEL model, PWP_UINT32 ndx);
PWP_BOOL PwModNdxBoundary (PWGM_HGRIDMODEL model, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData);
PWP_BOOL PwModNdxBoundaryAndCondition (PWGM_HGRIDMODEL model, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData, PWGM_CONDDATA *pCondData);
[/sourcecode]

Enumerating the Connections and Boundaries of a Block is done in a similar manner. First, you get the desired Block Connection or Boundary count using the appropriate Block Handle (using PwModEnumBlocks). Then, using the count, you enumerate each Connection or Boundary, one by one, obtaining it’s Handle. Finally, the Handle is used to obtain the corresponding Connection or Boundary data. The Block functions currently supported by the PWGM are

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
PWP_BOOL PwBlkSize (PWGM_HBLOCK block, PWGM_STR_SIZE *pSize);
PWP_BOOL PwBlock (PWGM_HBLOCK block, PWGM_BLOCKDATA *pBlockData);

PWP_UINT32 PwBlkBoundaryCount (PWGM_HBLOCK block);
PWGM_HBNDRY PwBlkEnumBoundaries (PWGM_HBLOCK block, PWP_UINT32 ndx);
PWP_BOOL PwBlkNdxBoundary (PWGM_HBLOCK block, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData);
PWP_BOOL PwBlkNdxBoundaryAndCondition (PWGM_HBLOCK block, PWP_UINT32 ndx, PWGM_BNDRYDATA *pBndryData, PWGM_CONDDATA *pCondData);

PWP_UINT32 PwBlkConnectionCount (PWGM_HBLOCK block);
PWGM_HCNXN PwBlkEnumConnections (PWGM_HBLOCK block, PWP_UINT32 ndx);
PWP_BOOL PwBlkNdxConnection (PWGM_HBLOCK block, PWP_UINT32 ndx, PWGM_CNXNDATA *pCnxnData);
[/sourcecode]

Each Block has an associated PWGM_BLOCKDATA instance. The PWGM_BLOCKDATA datatype is defined as:

[sourcecode language=”cpp” gutter=”true” toolbar=”false” wraplines=”false”]
typedef struct PWGM_BLOCKDATA_t {
const char *name; // name
PWGM_HBLOCK block; // handle
PWGM_ENUM_GRIDTYPE gridType; // grid type
PWGM_STR_SIZE size; // vertex-size
}
PWGM_BLOCKDATA;
[/sourcecode]

Don’t Be Fooled

The simple, 2D example grid used above is great for explaining ideas. However, its simplicity is deceiving. A typical structured grid model will have many Blocks, Connections and Boundaries. In addition, any given Block face is often connected to multiple neighboring Blocks as shown in the slightly more complex 3D example below.

A 3D grid model example and i-min face details

A 3D grid model example and i-min face details

In this example, the large Block’s i-min face is divided into two Connections (colored gray) and five Boundaries. The large number of i-min Boundaries results from the rectangular nature of ranges in structured grid models.

Degenerate Hexes

All voodoo aside, Pointwise supports the creation of pole grid entities. That is

  • A connector can be collapsed to a one-point pole.
  • A domain can be collapsed to an edge or a one-point pole.

This means that degenerate (a.k.a collapsed) hex cells may exist in the grid model. The PWGM makes no distinction between grid models that contain poles and those that don’t. The PWGM assumes that structured solvers can handle these types of grids. As such, there is no efficient way to find collapsed Connections or Boundaries without comparing physical XYZ coordinates. Please let Pointwise Support know if this limitation in the PWGM prevents you from creating a plugin for your solver.

My Brain Hurts

My Brain Hurts (youtube)

D. P. Gumby

When I started this post, I underestimated how difficult it was going to be to clearly describe the structured PWGM. Many hours and revisions later, I hope you find the explanation useful. With this information, you will be able to leverage the Pointwise CAE plugin SDK to create a plugin that adds your structured solver to Pointwise. As always, if you have any further questions feel free to contact Pointwise Support. We are here to help!

I am not yet sure what the topic of my next post will be. If you have any suggestions, please let me know. In the meantime, you can learn more about the Pointwise CAE Plugin SDK and the PWGM by clicking on the button below.

Learn more about the Pointwise CAE Plugin SDK

About David Garlisch

Illini by birth, Texan by choice.
This entry was posted in Applications, Software and tagged , , , , , , , , , . Bookmark the permalink.

4 Responses to Creating a CAE Plugin – Understanding The Pointwise Grid Model API (Part 2)

  1. karan says:

    I have created a structured grid in Pointwise. When I export it to Ansys Fluent, I get a fatal error saying,-“Block contains unsupported topology.. Block contains collapsed edge.” For info,In one of the block I have a domain collapsed as edge. Is it Because Fluent may not handle the poles or collapsed domain or is it something else.?? Please let me know..

    • John Chawner says:

      karan:

      If I remember correctly, Fluent doesn’t like singularities in hex cells (i.e. one face collapsed to an edge). In a structured hex volume grid with a pole, the cells near the poles look like that. Our technical support team can help you with this issue. Please email them at support@pointwise.com.

  2. Pingback: Plugin How-To – Adding Solver Attributes | Another Fine Mesh

Leave a Reply