Skip to content

Assemblies

Greg Sjaardema edited this page Nov 8, 2019 · 40 revisions

NOTE: The logical name of this concept would be group, but there is already an experimental group use in Exodus which use the NetCDF group implementation.

The existing experimental grouping concept is basically a hierarchy of Exodus (NetCDF) files inside an Exodus file. This group has never been officially released and I am probably the only one to use it, so an option would be to rename the existing Exodus experimental group to be file_group or db_group and then use group instead of assembly.

For now, the exodus grouping concept will be called assembly

Motivation

Currently, the primary "grouping" entity in an exodus model is an element block which describes a collection of one or more elements of homogenous topology (i.e., all hexes or all tets or all quads). The model also contains a global "block" of nodes, and various sets and blocks consisting of nodes (sets only), edges, faces, and elements; however, the element block is the primary grouping mechanism.

As model complexity increases, a mesh (or exodus model) can contain tens to hundreds to thousands of element blocks. As geometric model complexity increases, it may be necessary to split geometric volumes into multiple "meshable volumes" each of which becomes an individual element block (for example a complicated volume may be mostly meshable using hexes, but may in addition have a few areas that require tets and then some transition wedges from the tets to hexes. Since element blocks are homogenous topology, that volume would consist of at least three element blocks). It can become very difficult for an analyst to remember the mapping from element blocks back to the geometric volume in the solid model.

Another issue is that the assembly hierarchy in the original solid model or geometry is lost in the exodus file where there is simply a flat list of element blocks.

The assembly concept described herein is an attempt to solve these issues in the exodus model. An assembly will be a homogenous collection of one or more element blocks OR one or more assemblies. Note that a mixed collection of element blocks AND assemblies is not allowed. An assembly cannot contain itself, either directly or indirectly.

An assembly will have:

  • Member list. Either:
    • non-empty list of element blocks belonging to assembly
    • non-empty list of assemblies belonging to the assembly.
  • unique id (required, positive, unique among assemblies)
  • unique name (required, unique among all blocks/sets/assemblies)
  • zero or more named attributes (optional)
    • attribute is per assembly, not a value for each entity in the assembly.
    • attribute can be of any supported NetCDF type (integer, char*, double)
    • Also add named entity attributes to other blocks and sets and GLOBAL.
  • transient data, zero or more named real variables which are output on each timestep. (optional)
    • The variables can be unique to a specific assembly
    • Not all variables must be on all assemblies.
    • Names should be unique across all transient variable names in the model.
    • Value is per assembly, not per entity in the assembly. Reduction variable
    • [future] Also add transient reduction variables to other blocks and sets.

Note that although the assembly feature will represent a directed acyclic graph of assemblies / element blocks, the exodus file might not know how to manipulate this graph or do any queries based on the graph. Exodus might not know about parent-child relations or roots or leaves of the tree/graph. The structure of this graph; however, will be able to be constructed based on the data stored in the file.

Note that, conceptually, there is a "global assembly" which contains all element blocks in the model. The current "global variables" are in essence the reduction variables for this global assembly. Not sure yet if this provides any benefit in the implementation...

  • An assembly or element block can be in multiple assemblies. The structure is a directed acyclic graph
  • Assemblies will be homogenous -- all members of a specific assembly will be all element blocks or all assemblies.
  • An assembly or element block can appear multiple times in a single assembly. Potentially useful for a part containing multiple copies of a part -- perhaps offset for each instance. Example is a bolt circle of multiple instances of the same bolt each offset by a specified angle from a specified center point.

Parallel

The assembly data will be replicated on all processors--Each processor will have the same data.

Undecided

  • As implemented, any entity type can be in an assembly. Must be homogenous type within an assembly.
    • There are benefits to allowing assemblies of side sets, especially if move to using the Block Dominant models.
    • What is homogenous in this case. Can an assembly contain an "assembly of element blocks" and an "assembly of sidesets" or is that a non-homogenous collection...? As implemented, can have an assembly containing an assembly of element blocks and an assembly of sidesets.

C API

  • Will also have to provide wrapper functions for the fortran API.

Determine number of assemblies on an exodus file:

 int num_assembly = ex_inquire_int(exoid, EX_INQ_ASSEMBLY);

Define number of assemblies which will be defined in an exodus file:

The ex_init_params struct will be extended with an additional field int64_t num_assembly; The call to the ex_put_init_ext() function with that struct will then define the number of assemblies.

Note that possible that an older code will not know about added struct field and therefore will not initialize it so it is possible that uninitialized value will result in large number of unintended assemblies. Maybe add a function which enables use of assemblies (and future extensions). If not set, then the assembly field is ignored...

Get / Put names of all assemblies or an individual assembly:

NOTE: Assembly names can also be set and queried via the ex_put_assemblies and ex_get_assemblies functions.

May only be called after call to ex_put_init_ext() for output file.

 ex_put_name(exoid, EX_ASSEMBLY, assembly_id, name);
 ex_put_names(exoid, EX_ASSEMBLY,  names[]);
 ex_get_name(exoid, EX_ASSEMBLY, assembly_id, name);
 ex_get_names(exoid, EX_ASSEMBLY,  names[]);

Define and output assembly

typedef struct ex_assembly
{
  int64_t        id;
  char           *name;
  ex_entity_type type; /* EX_ELEM_BLOCK or EX_ASSEMBLY */
  int64_t        entity_count;
  void_int       *entity_list;
} ex_assembly;

ex_put_assembly(exoid, ex_assembly assembly);
ex_put_assemblies(exoid, num_assembly, ex_assembly *assemblies);

Defines and outputs an assembly with id id and name name consisting of entity_count entities of type entity_type (EX_ASSEMBLY or EX_ELEM_BLOCK). The list of entity_count entities is given by the ids in entity_id_list.

   ex_init_params init;
   init.num_assembly = 3;
   ex_put_init_ext(exoid, init);

   /* conceptual; not real C code */
   ex_assembly assembly[3];
   assembly.id = {11, 22, 3};
   assembly.name = {"Fred";
   assembly.type = {EX_ELEM_BLOCK, EX_ELEM_BLOCK, EX_ASSEMBLY};
   assembly.entity_count = {2, 2, 2};
   assembly[0].entity_list = {10, 20};
   assembly[1].entity_list = {30, 40};
   assembly[2].entity_list = {11, 22};
   ex_put_assemblies(exoid, 3, assembly);

Read all assemblies

   int num_assembly = ex_inquire_int(exoid, EX_INQ_ASSEMBLY);
   int64_t ids[num_assembly];
   ex_get_ids(exoid, EX_ASSEMBLY, ids);

   int name_len = ex_inquire_int(exoid, EX_INQ_DB_MAX_USED_NAME_LENGTH);
   struct ex_assembly assemblies[num_assembly];
   for (int i=0; i < num_assembly; i++) {
      assemblies[i].entity_list = NULL;
      assemblies[i].name = malloc(name_len + 1);
   } 
   /* Get parameters for all assemblies, but not entity_list */
   ex_get_assemblies(exoid, assemblies);

   for (i = 0; i < num_assembly; i++) {
      int entity_count = assemblies[i].entity_count;
      assemblies[i].entity_list = malloc(entity_count * sizeof(INT));
   }

   /* Now get the entity_lists */
   ex_get_assemblies(exoid, assemblies);

Define/Query assembly attributes and attribute names

An assembly attribute is similar to an IOSS property consisting of a name, a type, and a value or values. It is not a value per entity in the assembly, but a value for the assembly. For now, they types will be limited to text, integer, and double to provide capability without the complexity of supporting the many types available in NetCDF-4 including user-defined types. Note that an attribute can have multiple values, for example if the attribute is a range, it could have the value {1.0, 100.0}

NOTE: This type of attribute (value on entity instead of value per entities members, for example nodes in a nodeset) will also be added to the other entity types (blocks and sets) when implemented for assemblies.

NOTE: Need a better name or way of distinguishing from the attributes which are currently supported in Exodus.

  • define and output an attribute
   ex_attribute attribute = {EX_ASSEMBLY, 100, "Offset", EX_DOUBLE, 3, {1.1, 2.2, 3.3}};
   ex_put_attribute(exoid, attribute);
  • define and output multiple attributes
   ex_attribute attributes[10];
   /* ... Initialize `attributes` */
   ex_put_attributes(exoid, 10, attributes);
  • define and output a double attribute
   ex_put_double_attribute(exoid, EX_ASSEMBLY, id, name, num_values, values);
  • define and output an integer attribute
   ex_put_integer_attribute(exoid, EX_ASSEMBLY, id, name, num_values, values);
  • [The size of the integers used in values is based on int-size of the database]
  • define and output a text attribute....
   ex_put_text_attribute(exoid, EX_ASSEMBLY, id, name, char *values);
  • Query number of attributes on an assembly
   int num_attr = ex_get_attribute_count(exoid, EX_ASSEMBLY, id);
  • Query names, types, size/length, values of all attributes on an assembly
   int num_attr = ex_get_attribute_count(exoid, EX_ASSEMBLY, id);
   ex_attribute attributes[num_attr];
   ex_get_attributes(exoid, EX_ASSEMBLY, id, attributes);
  • Get values of all attributes on the specified entity (ASSEMBLY 100)
   int count = ex_get_attribute_count(exoid, EX_ASSEMBLY, 100)
   ex_attribute attributes[count];
   memset(attributes, 0, sizeof(ex_attribute)*count);
   ex_get_attribute_param(exoid, EX_ASSEMBLY, 100, attributes);
   /* Get attribute values for all attributes listed in `attributes`
      `ex_get_attributes()` will allocate memory for `attributes[i].values` 
      which must be freed by caller.
    */
   ex_get_attributes(exoid, count, attributes);
  • Get value of specific attribute on the specified entity (ASSEMBLY 100)
   int count = ex_get_attribute_count(exoid, EX_ASSEMBLY, 100)
   ex_attribute attributes[count];
   ex_get_attribute_param(exoid, EX_ASSEMBLY, 100, attributes);
   /* ... allocate space on specific attribute to store values. If
    *     not allocated, it will be allocated by `ex_get_attribute()`
    */
   attributes[3].values = calloc(attributes[3].value_count, sizeof(double));
   /* Get attribute values for attribute at index 3 */
   ex_get_attribute(exoid, attributes[3]);

The ex_attribute argument is a struct similar to:

typedef struct ex_attribute
{
  ex_entity_type entity_type,
  ex_entity_id   entity_id,
  char  name[EX_MAX_NAME];
  ex_type type; /* EX_INTEGER, EX_DOUBLE, EX_CHAR */
  int   value_count;
  void* values; /* not accessed if NULL */
} ex_attribute;

NOTE: is there benefit to getting all attribute names/types/size in single call, or can we simplify API and just provide the query of individual attribute. Can do this in single call if only fill in non-NULL arguments

Define/Query number of variables on an assembly

 ex_put_variable_param(exoid, EX_ASSEMBLY, num_variables);
 ex_put_all_var_param_ext(exoid, &variable_params);
 ex_get_variable_param(exoid, EX_ASSEMBLY, &num_variables);

Can also define and query the assembly truth table which will specify which variables are defined on which assemblies. Note that there will be a value written to and read from the database for all variables on all assemblies, but only the values corresponding to a defined variable will be valid values. This is done so that we can access the chunk of num_vars * num_assemblies in a single write/read per timestep instead of doing num_vars * num_assemblies individual writes. [Is it possible / wanted to write / read the variables on an assembly by assembly basis...]

Define / Query names of variables on an assembly

 ex_put_variable_names(exoid, EX_ASSEMBLY, num_variables, names);
 ex_get_variable_names(exoid, EX_ASSEMBLY, num_variables, names);
 ex_put_variable_name(exoid, EX_ASSEMBLY, index, name);
 ex_get_variable_name(exoid, EX_ASSEMBLY, index, name);

Write / Read data for assembly variable(s) at a time step

All variables for all assemblies at one time:

  ex_put_var(exoid, step, EX_ASSEMBLY, 0, 0, num_assem_vars, values);
  ex_get_var(exoid, step, EX_ASSEMBLY, 0, 0, num_assem_vars, values);

All variables for a single assembly (with id id):

  ex_put_var(exoid, step, EX_ASSEMBLY, id, id, num_assem_vars, values);
  ex_put_var(exoid, step, EX_ASSEMBLY, id, id, num_assem_vars, values);

[Not sure which or both of these should be supported. Need to distinguish somehow whether client is specifying all assemblies, or only a specific assembly. For blocks and sets, the first argument following type is variable index and the second is the entity id. Above I am using convention that if index is 0, then we are accessing all variables on all assemblies. In second use, using the convention that if index and id match and are non-zero, then specifying all values on a certain assembly. Is this flexibility worth the complexity...]

Representation options

In the examples that follow, assume for illustration that there are 3 assemblies and 4 element blocks. Assembly 1 is blocks 1 and 3; Assembly 2 is blocks 2 and 4; Assembly 3 is assemblies 1 and 2. Assume blocks hasve ids 10, 20, 30, 40 and assemblies have ids 11, 22, and 33.]

There are a few ways to represent an assembly:

  • Boolean membership in union of assemblies + element blocks. Every assembly would have a list that is num_assemblies + num_elem_blk in length; the list would have a 1 if the corresponding entity is in the assembly. Ordering is assemblies followed by element blocks and ordering is based on file order:
A1 A2 A3 E1 E2 E3 E4
A1 0 0 0 1 0 1 0
A2 0 0 0 0 1 0 1
A3 1 1 0 0 0 0 0
  • Boolean membership in a list of either element blocks or assemblies. Each assembly would have a type of either EX_ELEM_BLOCK or EX_ASSEMBLY which would determine the length of the entity list and the type of entities represented by the list.
type 1 2 3 4
A1 element 1 0 1 0
A2 element 0 1 0 1
A3 assembly 1 1 0
  • Explicit listing of the entity type (EX_ELEM_BLOCK or EX_ASSEMBLY), count, and a list of the ids of the entities.
type count members
A1 element 2 10, 30
A2 element 2 20, 40
A3 assembly 2 11, 22

CDF Representation options

dimensions:
	num_assembly = 3 ;
	num_assembly_members = 11 ; # num_assembly + num_elem_blk

variables:
# How do we represent an assembly in the file:
# Note that assemblies are homogenous, so members are either all element blocks or all assemblies
# 1. Define each assembly individually.  Either a set of element blocks or a set of assemblies
#    Similar to option 3, but not as many NetCDF dimensions needed, but a little wasteful in space.
	int assembly1(num_elem_blk) ;
	    assembly1:name = "Fireset" ;
	    assembly1:type = "element" ;

	int assembly2(num_assembly) ;
	    assembly2:name = "Stronglink" ;
	    assembly2:type = "assembly" ;
	    
# 2. Define all assemblies at one time.
#    + can access all assemblies and all assembly names with single calls
     	int assembly(num_assembly, num_assembly_members) ;  # `num_assembly_members = num_assembly + num_elem_blk`
        char assembly_names(num_assembly, len_name) ;
	char name_assembly(num_assembly, len_name) ;
		name_assembly:_FillValue = "" ;

# Option 3: Each assembly individually with a dimension for each giving member count.
# o efficient if define all at once; efficient in space.
# + easier to define per-assembly attributes.
        int assembly_1(num_entities_1);
                assembly_1:entity_type = "element_block" ;
                assembly_1:name = "My Name" ;
                assembly_1:my_double_attribute = 3.14159, 42.0 ;
                assembly_1:part_number = 1239484764 ;

	# Option 1 -- assembly vars per assembly per variable
	# + Can specify only vars that exist on an assembly
	# + best granularity for type, dimension, unit [if ever supported in exodus]
	# - Verbose O(#assembly * #var) variables
	# - small output size -- double / timestep
	double vals_assembly_var1a1(time_step) ;
	double vals_assembly_var2a1(time_step) ;
	double vals_assembly_var3a1(time_step) ;
	double vals_assembly_var1a2(time_step) ;
	double vals_assembly_var3a2(time_step) ;
	double vals_assembly_var1a3(time_step) ;
	double vals_assembly_var2a3(time_step) ;

	# Option 2 -- assembly vars blob of all vars on all assemblies -- zero-fill if not valid for particular assembly
	# - Data for a variable even if doesn't exist on a specific assembly
	# + Compact O(1) variable
	# - All vars on all assemblies are of same type, dimension, unit [if ever supported in exodus]
	# + largest output size -- num_var * num_assembly doubles / timestep
	double vals_assembly(time_step, num_assembly, num_var) ;

	# Option 3 -- assembly vars -- all vars on a particular assembly.
	# + Can specify only vars that exist on an assembly
	# - All vars are of same type, dimension, unit [if ever supported in exodus]
	# + can have different variables on each assembly
	# o Somewhat Compact O(#assemblies) variables
	# o smallish output size -- num_var / timestep
	double vals_assembly1(time_step, num_vars_assembly1) ;

	# Option 3 -- assembly vars -- all vars on a particular assembly.
	# + Can specify only vars that exist on an assembly
	# + var for all assemblies are of same type, dimension, unit [if ever supported in exodus]
	# o Somewhat Compact O(#assemblies) variables
	# o smallish output size -- num_assembly / timestep
	double vals_assembly_var1(time_step, num_assemblies) ;

Additional Changes

IOSS

  • Add an Assembly Ioss::GroupingEntity class
  • Read assembly information from Exodus file and create the assembly graph in an Ioss Region.
  • IOSS will know parent/child information so could support graph-structure queries.
  • Client can modify assembly structure which will be persisted to the output database.
    • Modifying assembly structure of an input database will not modify structure in existing exodus file.

SEACAS Applications

Applications will need to be modified to support assemblies. Unless noted below, a SEACAS application will not know about assemblies or replicate them to an output file the application creates (i.e., assembly information will be lost).

  • EPU: Assume assembly structure the same on all processor files; replicate to the output file(s)

  • CONJOIN: Assembly structure in the first database will be replicated to output file. If any part subsetting, then the part will be removed from any assemblies it was in.

  • EJOIN: Output file will have merged assemblies from input files.

    • Maybe create new assembly consisting of element blocks from each input file to make it easy to see where blocks came from...
  • EXODIFF: Can do diff of assembly attributes and transient data.

  • IO_INFO: Show assembly graph.

  • IO_SHELL: replicate to output file.

  • Scoping Needed:

    • EXPLORE: Scope what is needed to show assembly structure.
    • GREPOS: what is needed to replicate assembly structure to output file.
    • APREPRO: Is there a benefit to having aprepro access to assembly data.

Creation / Modification of Assemblies

  • Could modify grepos to add assembly information to the output file
  • Could modify io_shell
  • Create new application which adds assembly information to existing file without rewriting.