programmer's documentation
Post-processing output (C functions)

Introduction

C user functions for definition of postprocessing output. These subroutines are called in all cases.

If the Code_Saturne GUI is used, this file is not required (but may be used to override parameters entered through the GUI, and to set parameters not accessible through the GUI).

Several functions are present in the file, each destined to defined specific parameters.

The functions defined in cs_user_postprocess.c, namely cs_user_postprocess_writers, cs_user_postprocess_meshes, cs_user_postprocess_probes and cs_user_postprocess_activate allow for the definition of post-processing output formats and frequency, and for the definition of surface or volume sections, in order to generate chronological outputs in EnSight, MED, or CGNS format, as in-situ visualization using Catalyst.

Point sets (probes and profiles) may also be defined, with outputs in the more classical comma-separated (csv) or white-space-separated (dat) text files, in addition to the aforementioned output types.

The main concepts are those of writers and meshes, which must be associated to produce outputs.

A writer combines the definition of an output type, frequency, path, and name. One or more writers can be defined using the GUI and the cs_user_postprocess_writers user function.

A mesh is based on a subset of the the computational mesh, or point sets such as particles or probe sets. One or more meshes can be defined using the GUI and the cs_user_postprocess_meshes user function.

In order to allow the user to add an output format to the main output format, or to add a mesh to the default output, the lists of standard and user meshes and writers are not separated. Negative numbers are reserved for the non-user items. For instance, the mesh numbers -1 and -2 correspond respectively to the global mesh and to boundary faces, generated by default, and the writer -1 corresponds to the default post-processing writer.

The user chooses the numbers corresponding to the post-processing meshes and writers he wants to create. These numbers must be positive integers. It is possible to associate a user mesh with the standard post-processing case (-1), or to ask for outputs regarding the boundary faces (-2) associated with a user writer.

For safety, the output frequency and the possibility to modify the post-processing meshes are associated with the writers rather than with the meshes. This logic avoids unwanted generation of inconsistent post-processing outputs. For instance, EnSight would not be able to read a case in which one field is output to a given part every 10 time steps, while another field is output to the same part every 200 time steps.

Definition of post-processing writers

Writers may be defined in the cs_user_postprocess_writers function.

Flushing parameters for time plots may also be defined here. By default, for best performance, time plot files are kept open, and flushing is not forced. This behavior may be modified, as in the following example. The default settings should be changed before time plots are defined.

cs_time_plot_set_flush_default(1800, /* flush_wtime */
-1); /* n_buffer_steps */

The following local variable definitions can shared beween several examples:

double frequency_n = -1.0;
double frequency_t = -1.0;

In the following example, the default writer is redefined, so as to modify some parameters which have been set by default or by the GUI:

cs_post_define_writer(-1, /* writer_id */
"results", /* writer name */
"postprocessing", /* directory name */
"EnSight Gold", /* format_name */
"", /* format_options */
true, /* output_at_end */
-1, /* frequency_n */
-1.0); /* frequency_t */

Polygons and polyhedra may also be divided into simpler elements, as in the example below (only for polyhedra here), using MED format output:

cs_post_define_writer(1, /* writer_id */
"user_txt", /* writer name */
"postprocessing", /* directory name */
"MED", /* format name */
"divide_polyhedra",
true, /* output_at_end */
-1, /* frequency_n */
-1.0); /* frequency_t */

In the next example, the defined writer discards polyhedra, and allows for transient mesh connectivity (i.e. meshes changing with time); text output is also forced:

cs_post_define_writer(2, /* writer_id */
"modif", /* writer name */
"postprocessing", /* directory name */
"ensight", /* format name */
"text",
false,
3,
frequency_t);

In this last example, a plot writers is defined. Such a writer will be used to output probe, profile, or other point data (probes may be output to 3D formats, but plot and time plot outputs drop cell or face-based ouptut).

cs_post_define_writer(3, /* writer_id */
"profile", /* writer name */
"postprocessing", /* directory name */
"plot", /* format name */
"", /* format options */
false, // output_at_end
100, // nt_freq
-1.0); // dt_freq

Definition of post-processing and mesh zones

Postprocessing meshes may be defined in the cs_user_postprocess_meshes function, using one of several postprocessing mesh creation functions (cs_post_define_volume_mesh, cs_post_define_volume_mesh_by_func, cs_post_define_surface_mesh, cs_post_define_surface_mesh_by_func, cs_post_define_particles_mesh, cs_post_define_particles_mesh_by_func, cs_post_define_probe_mesh, cs_post_define_alias_mesh, cs_post_define_existing_mesh, and cs_post_define_edges_mesh).

It is possible to output variables which are normally automatically output on the main volume or boundary meshes to a user mesh which is a subset of one of these by setting the auto_variables argument of one of the cs_post_define_..._mesh to true.

It is not possible to mix cells and faces in the same mesh (most of the post-processing tools being perturbed by such a case). More precisely, faces adjacent to selected cells and belonging to face or cell groups may be selected when the add_groups of cs_post_define_..._mesh functions is set to true, so as to maintain group information, but those faces will only be written for formats supporting this (such as MED), and will only bear groups, not variable fields.

The additional variables to post-process on the defined meshes will be specified in the subroutine usvpst in the cs_user_postprocess_var.f90 file.

Warning
In the parallel case, some meshes may not contain any local elements on a given processor. This is not a problem at all, as long as the mesh is defined for all processors (empty or not). It would in fact not be a good idea at all to define a post-processing mesh only if it contains local elements, global operations on that mesh would become impossible, leading to probable deadlocks or crashes.}

Example: reconfigure predefined meshes

In the example below, the default boundary mesh output is suppressed by redefining it, with no writer association:

{
int n_writers = 0;
const int *writer_ids = NULL;
cs_post_define_surface_mesh(-2, /* mesh_id of main boundary mesh */
"Boundary", /* mesh name */
NULL, /* interior face selection criteria */
"all[]", /* boundary face selection criteria */
true, /* add_groups */
true, /* automatic variables output */
n_writers,
writer_ids);
}

Note that the default behavior for meshes -1 and -2 is:

int n_writers = 1;
const int writer_ids[] = {-1});

Example: select interior faces with y = 0.5

In the following example, we use a geometric criteria to output only a subset of the main mesh, and associate the resulting mesh with user-defined writers 1 and 4:

{
const int n_writers = 2;
const int writer_ids[] = {1, 4}; /* Associate to writers 1 and 4 */
const char *interior_criteria = "plane[0, -1, 0, 0.5, "
"epsilon = 0.0001]";
const char *boundary_criteria = NULL;
cs_post_define_surface_mesh(1, /* mesh id */
"Median plane",
interior_criteria,
boundary_criteria,
false, /* add_groups */
false, /* auto_variables */
n_writers,
writer_ids);
}

Example: alias mesh

The same variables will be output through all writers associated to a mesh. In cases where different variables of a same mesh should be output through different writers, the solution is to define one or several "aliases" of that mesh. An alias shares all the attributes of its parent mesh (without duplication), except its number. allowing to assign a different id, writers, and variables to each secondary copy of the mesh, without the overhead of a full copy. The cs_post_define_alias_mesh function may be used for such a purpose. Its use is illustrated in the following example:

{
const int n_writers = 2;
const int writer_ids[] = {1, 4}; /* Associate to writers 1 and 4 */
const char *interior_criteria = "plane[0, -1, 0, 0.5, "
"epsilon = 0.0001]";
const char *boundary_criteria = NULL;
cs_post_define_surface_mesh(1, /* mesh id */
"Median plane",
interior_criteria,
boundary_criteria,
false, /* add_groups */
false, /* auto_variables */
n_writers,
writer_ids);
}

Modification of a post-processing mesh or its alias over time is always limited by the most restrictive "writer" to which its meshes have been associated (parts of the structures being shared in memory). It is possible to define as many aliases as are required for a true mesh, but an alias cannot be defined for another alias.

Advanced definitions of post-processing and mesh zones

More advanced mesh element selection is possible using cs_post_define_volume_mesh_by_func or cs_post_define_surface_mesh_by_func, which allow defining volume or surface meshes using user-defined element lists.

The possibility to modify a mesh over time is limited by the most restrictive writer which is associated with. For instance, if writer 1 allows the modification of the mesh topology (argument time_dep = FVM_WRITER_TRANSIENT_CONNECT in the call to cs_post_define_writer) and writer 2 allows no modification (time_dep = FVM_WRITER_FIXED_MESH), a user post-processing mesh associated with writers 1 and 2 will not be modifiable, but a mesh associated with writer 1 only will be modifiable. The modification can be done by using the advanced cs_post_define_volume_mesh_by_func or cs_post_define_surface_mesh_by_func, associated with a user-defined selection function based on time-varying criteria (such as field values being above a given threshold). If the time_dep argument is set to true, the mesh will be redefined using the selection function at each output time step for every modifiable mesh.

Example: surface mesh with complex selection criteria

In the following example, we build a surface mesh containing interior faces separating cells of group "2" from those of group "3", (assuming no cell has both colors), as well as boundary faces of group "4".

This is done by first defining 2 selection functions, whose arguments and behavior match the cs_post_elt_select_t type.

The function for selection of interior faces separating cells of two groups also illustrates the usage of the cs_selector_get_family_list function to build a mask allowing direct checking of this criterion when comparing cells adjacent to a given face:

static void
_i_faces_select_example(void *input,
cs_lnum_t *n_faces,
cs_lnum_t **face_ids)
{
cs_lnum_t i, face_id;
cs_lnum_t n_families = 0;
cs_int_t *family_list = NULL;
int *family_mask = NULL;
cs_lnum_t n_i_faces = 0;
cs_lnum_t *i_face_ids = NULL;
const cs_mesh_t *m = cs_glob_mesh;
/* Allocate selection list */
BFT_MALLOC(i_face_ids, m->n_i_faces, cs_lnum_t);
/* Build mask on families matching groups "2" (1), "3" (2) */
BFT_MALLOC(family_list, m->n_families, cs_int_t);
BFT_MALLOC(family_mask, m->n_families, int);
for (i = 0; i < m->n_families; i++)
family_mask[i] = 0;
cs_selector_get_family_list("2", &n_families, family_list);
for (i = 0; i < n_families; i++)
family_mask[family_list[i] - 1] += 1;
cs_selector_get_family_list("3", &n_families, family_list);
for (i = 0; i < n_families; i++)
family_mask[family_list[i] - 1] += 2;
BFT_FREE(family_list);
/* Now that mask is built, test for adjacency */
for (face_id = 0; face_id < m->n_i_faces; face_id++) {
/* Adjacent cells and flags */
cs_lnum_t c1 = m->i_face_cells[face_id][0];
cs_lnum_t c2 = m->i_face_cells[face_id][1];
int iflag1 = family_mask[m->cell_family[c1]];
int iflag2 = family_mask[m->cell_family[c2]];
/* Should the face belong to the extracted mesh ? */
if ((iflag1 == 1 && iflag2 == 2) || (iflag1 == 2 && iflag2 == 1)) {
i_face_ids[n_i_faces] = face_id;
n_i_faces += 1;
}
}
/* Free memory */
BFT_FREE(family_mask);
BFT_REALLOC(i_face_ids, n_i_faces, cs_lnum_t);
/* Set return values */
*n_faces = n_i_faces;
*face_ids = i_face_ids;
}

The function for selection of boundary faces is simpler, as it simply needs to apply the selection criterion for boundary faces:

static void
_i_faces_select_example(void *input,
cs_lnum_t *n_faces,
cs_lnum_t **face_ids)
{
cs_lnum_t i, face_id;
cs_lnum_t n_families = 0;
cs_int_t *family_list = NULL;
int *family_mask = NULL;
cs_lnum_t n_i_faces = 0;
cs_lnum_t *i_face_ids = NULL;
const cs_mesh_t *m = cs_glob_mesh;
/* Allocate selection list */
BFT_MALLOC(i_face_ids, m->n_i_faces, cs_lnum_t);
/* Build mask on families matching groups "2" (1), "3" (2) */
BFT_MALLOC(family_list, m->n_families, cs_int_t);
BFT_MALLOC(family_mask, m->n_families, int);
for (i = 0; i < m->n_families; i++)
family_mask[i] = 0;
cs_selector_get_family_list("2", &n_families, family_list);
for (i = 0; i < n_families; i++)
family_mask[family_list[i] - 1] += 1;
cs_selector_get_family_list("3", &n_families, family_list);
for (i = 0; i < n_families; i++)
family_mask[family_list[i] - 1] += 2;
BFT_FREE(family_list);
/* Now that mask is built, test for adjacency */
for (face_id = 0; face_id < m->n_i_faces; face_id++) {
/* Adjacent cells and flags */
cs_lnum_t c1 = m->i_face_cells[face_id][0];
cs_lnum_t c2 = m->i_face_cells[face_id][1];
int iflag1 = family_mask[m->cell_family[c1]];
int iflag2 = family_mask[m->cell_family[c2]];
/* Should the face belong to the extracted mesh ? */
if ((iflag1 == 1 && iflag2 == 2) || (iflag1 == 2 && iflag2 == 1)) {
i_face_ids[n_i_faces] = face_id;
n_i_faces += 1;
}
}
/* Free memory */
BFT_FREE(family_mask);
BFT_REALLOC(i_face_ids, n_i_faces, cs_lnum_t);
/* Set return values */
*n_faces = n_i_faces;
*face_ids = i_face_ids;
}

Given these tow functions, the mesh can be defined using the cs_post_define_surface_mesh_by_func function, passing it the user-defined selection functions (actually, function pointers):

{
const int n_writers = 1;
const int writer_ids[] = {1}; /* Associate to writer 1 */
/* Define postprocessing mesh */
"Mixed surface",
_i_faces_select_example,
_b_faces_select_example,
NULL, /* i_faces_sel_input */
NULL, /* b_faces_sel_input */
false, /* time varying */
false, /* add_groups */
false, /* auto_variables */
n_writers,
writer_ids);
}

Example: time-varying mesh

A mesh defined through the advanced cs_post_define_surface_mesh_by_func, cs_post_define_volume_mesh_by_func, or cs_post_define_particles_mesh_by_func may vary in time, as long as the matching time_varying argument is set to true, and the mesh (or aliases thereof) id only associated to writers defined with the FVM_WRITER_TRANSIENT_CONNECT option. In the case of particles, which always vary in time, this allows also varying the selection (filter) function with time.

In the following example, we build a volume mesh containing cells with values of field named "He_fraction" greater than 0.05.

First, we define the selection function:

static void
_he_fraction_05_select(void *input,
cs_lnum_t *n_cells,
cs_lnum_t **cell_ids)
{
cs_lnum_t _n_cells = 0;
cs_lnum_t *_cell_ids = NULL;
const cs_mesh_t *m = cs_glob_mesh;
cs_field_t *f = cs_field_by_name("He_fraction"); /* Get access to field */
if (f == NULL)
bft_error(__FILE__, __LINE__, 0,
"No field with name \"He_fraction\" defined");
/* Before time loop, field is defined, but has no values yet,
so ignore that case (postprocessing mesh will be initially empty) */
if (f->val != NULL) {
BFT_MALLOC(_cell_ids, m->n_cells, cs_lnum_t); /* Allocate selection list */
for (cs_lnum_t i = 0; i < m->n_cells; i++) {
if (f->val[i] > 5.e-2) {
_cell_ids[_n_cells] = i;
_n_cells += 1;
}
}
BFT_REALLOC(_cell_ids, _n_cells, cs_lnum_t); /* Adjust size (good practice,
but not required) */
}
/* Set return values */
*n_cells = _n_cells;
*cell_ids = _cell_ids;
}

Then, we simply define matching volume mesh passing the associated selection function pointer:

{
const int n_writers = 1;
const int writer_ids[] = {2}; /* Associate to writer 2 */
/* Define postprocessing mesh */
"He_fraction_05",
_he_fraction_05_select,
NULL, /* _c_05_select_input */
true, /* time varying */
false, /* add_groups */
false, /* auto_variables */
n_writers,
writer_ids);
}

The matching function will be called at all time steps requiring output of this mesh.

Warning
some mesh formats do not allow changing meshes (or the implemented output functions do not allow them yet) and some may not allow empty meshes, even if this is only transient.

Example: edges mesh

In cases where a mesh containing polygonal elements is output through a writer configured to divide polygons into triangles (for example when visualization tools do not support polygons, or when highly non convex faces lead to visualization artifacts), it may be useful to extract a mesh containing the edges of the original mesh so as to view the polygon boundaries as an overlay.

In the following example, we build such a mesh (with id 5), based on the faces of a mesh with id 1:

{
const int n_writers = 1;
const int writer_ids[] = {4}; /* Associate to writer 4 */
cs_post_define_edges_mesh(5, /* mesh_id */
1, /* base_mesh_id */
n_writers,
writer_ids);
}

Management of output times

By default, a post-processing frequency is defined for each writer, as defined using the GUI or through the cs_user_postprocess_writers function. For each writer, the user may define if an output is automatically generated at the end of the calculation, even if the last time step is not a multiple of the required time step number of physical time.

For finer control, the cs_user_postprocess_activate function file may be used to specify when post-processing outputs will be generated, overriding the default behavior.

In the following example, all output is deactivated until time step 1000 is reached, at which time the normal behavior resumes:

if (nt_max_abs < 1000) {
int writer_id = 0; /* 0: all writers */
cs_post_activate_writer(writer_id, false);
}

Probes

Sets of probes may also be defined through the cs_user_postprocess_probes function, to allow for extraction and output of values at specific mesh locations, often with a higher time frequency than for volume or surface meshes.

Probe sets, and profiles (which can be viewed as a series of probes lying on a user-defined curve) are handled as a point mesh, which can be associated with plot and time_plot 2D-plot writers, as well as any of the general (3D-output) writer types (the priviledged writer types being plot for a , and time_plot for other probes).

Probe sets may be defined using the cs_probe_set_create_from_array function. In some cases, it might be useful to define thos ses in multiple stages, using first cs_probe_set_create then a series of calls to cs_probe_set_add_probe.

Probe set creation functions return a pointer to a cs_probe_set_t structure which can be used to specify additional options using the cs_probe_set_option function.

Definition of the variables to post-process

For the mesh parts defined using the GUI or in cs_user_postprocess.c, the usvpst subroutine of cs_user_postprocess_var.f90 file may be used to specify the variables to post-process (called for each "part", at every active time step of an associated writer, see cs_user_postprocess.c.

The output of a given variable is generated by means of a call to the subroutine post_write_var}.

Note
To generate outputs of different variables on the same mesh with different frequencies, it is recommended to create an alias of this mesh and to associate it with a different writer using the GUI or in cs_user_postprocess.c.