Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
DLR-AMR
GitHub Repository: DLR-AMR/t8code
Path: blob/main/tutorials/general/t8_tutorial_search.cxx
903 views
/*
  This file is part of t8code.
  t8code is a C library to manage a collection (a forest) of multiple
  connected adaptive space-trees of general element types in parallel.

  Copyright (C) 2015 the developers

  t8code is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  t8code is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with t8code; if not, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

/* See also: https://github.com/DLR-AMR/t8code/wiki/Tutorial:-Search
 * In this tutorial we discuss t8code's search algorithm.
 * search is a powerful tool to identify leaf elements that match a given condition
 * and execute a function on them.
 *
 * In the example we will create random 'particles', thus points in a subdomain of our domain,
 * and then use search to find those leaf elements that contain particles and for each element
 * count the number of particles it contains.
 *
 * How you can experiment here:
 *    - Load the generated vtu files with paraview and add a threshold to the
 *      "Number of particles" field to display the elements with 1 or more particles.
 *      This shows you the distribution of the particles in the domain and which elements
 *      contain particles.
 *    - Change the number of particles, the subdomain of the particles etc.
 *    - Try to find a different criterion to search for and implement it.
 *    - Do not count particles in elements whose x-coordinates are all smaller than 0.5 .
 *    - Advanced: Use adapt to refine elements with more than 1 particle in them recursively until each
 *                element contains at most 1 particle.
 *
 *
 * The search algorithm does not iterate linearly through our elements, instead
 * it traverses recursively through the mesh hierarchy. This has the benefit, that we
 * can exclude whole regions of the mesh from our search as soon as we can determine that
 * they will not match the criterion.
 * You can see in the output of this program that the number of searched elements is
 * significantly smaller than the actual number of elements.
 *
 * In each (process local) tree of the forest, search will create the level 0 element that
 * coincides with the tree and call the search-callback function on it.
 * In the callback the user decides whether to continue the search or not.
 * If we continue the search, the children of this level 0 element are created and the
 * search callback will be called for them -- again deciding whether to continue or not.
 * This process repeats recursively and stops at those fine elements that are actually contained
 * in the forest (leaf elements).
 *
 * The search algorithm can be given an array of 'queries' and a query-callback.
 * These queries can be arbitrarily defined data. In our case this will be the array of particles.
 * If queries and a query-callback are provided, then for each element first the search-callback
 * is called to decide whether or not to continue searching. If it returns true, the query-callback
 * will be called once for each active query object.
 * If the query object returns 0, this query object will get deactivated for this element and its
 * recursive children. The recursion stops when no queries are active anymore.
 *
 * It is probably best to illustrate this with an example. Let the following forest
 * with 2 quad trees be given.
 *  __ __ __ __ __________
 * |__|__|  *  |          |
 * |_*|_*|__ __|  *       |
 * |__|__|__|__|    *     |
 * |__|__|__|__|__________|
 *
 * The '*' should mark particles for which we want to find the elements containing them.
 * These particles are our queries. Let us enumerate them 0, 1, 2, 3, 4 from left to right.
 * The search should always continue for an element as long as we still may have particles to look for,
 * thus the search callback can always return true. The query-callback will return true if
 * and only if the current particle is contained inside the element.
 * We discuss the search process in the left tree.
 *
 *   __ __ __ __                                         __ __ __ __
 *  |        *  |                                       |     |  *  |
 *  | *  *      |  *                                    |_* _*|__ __|
 *  |           |      *                        --->    |     |     |
 *  |__ __ __ __|                                       |__ __|__ __|
 *
 *  At first, the tree element is considered            The children are created and the search is called
 *  and all queries are active. The first 3 are         for each one of them. For the upper left, queries 0 and 1
 *  inside the element, the last 2 not. Thus the        will remain active. The upper right is the final forest leaf, so the search stops.
 *  search will continue, but only queries 0, 1, 2      For the bottom two children no queries will remain active
 *  remain active.                                      and hence the search won't continue here.
 *
 *  __ __
 * |__|__|
 * |_*|_*|
 *
 *  The search continues with the children of the upper left element.
 *  Those are all leaves in the forest and hence the search will stop after
 *  executing the query callback.
 *
 *  Afterwards the search will continue similarly in the second tree.
 *
 *  Note that the performance of the search could be improved by using an approximative check
 *  on all but the leaf elements.
 *  This check should return true whenever a particle is inside an element, but may have false positives.
 *
 */

#include <t8.h>                                 /* General t8code header, always include this. */
#include <t8_cmesh/t8_cmesh.h>                  /* cmesh definition and basic interface. */
#include <t8_cmesh/t8_cmesh_examples.h>         /* A collection of exemplary cmeshes */
#include <t8_forest/t8_forest_general.h>        /* forest definition and basic interface. */
#include <t8_forest/t8_forest_io.h>             /* save forest */
#include <t8_schemes/t8_default/t8_default.hxx> /* default refinement scheme. */
#include <t8_types/t8_vec.hxx>                  /* Basic operations on 3D vectors. */
#include <t8_forest/t8_forest_iterate.h>        /* For the search algorithm. */
#include <tutorials/general/t8_step3.h>         /* Example forest adaptation from step 3 */

/* Our search query, a particle together with a flag. */
struct t8_tutorial_search_particle_t
{
  double coordinates[3];   /* The coordinates of our particle. */
  int is_inside_partition; /* Will be set to true if the particles lies inside this process' parallel partition. */
};

/* Additional user data that we process during search.
 * For each element we count the number of particles that it contains
 * and we count the total number of elements that we constructed during search. */
struct t8_tutorial_search_user_data_t
{
  sc_array *particles_per_element;   /* For each element the number of particles inside it. */
  t8_locidx_t num_elements_searched; /* The total number of elements created. */
};

/*
 * The search callback.
 * It will be called once per element and generally decides whether or not
 * to continue the search with the children of the element.
 * Since we will continue as long as there are particles left to consider,
 * we always return 1 here.
 * The search will then only stop when no queries are active (thus, no particles
 * could be in this element) or the element is a leaf element.
 *
 * We also increase a counter by one in order to count how many elements we
 * looked at during the search process.
 */
static int
t8_tutorial_search_callback (t8_forest_t forest, [[maybe_unused]] const t8_locidx_t ltreeid,
                             [[maybe_unused]] const t8_element_t *element, [[maybe_unused]] const int is_leaf,
                             [[maybe_unused]] const t8_element_array_t *leaf_elements,
                             [[maybe_unused]] const t8_locidx_t tree_leaf_index)
{

  /* Get a pointer to our user data and increase the counter of searched elements. */
  t8_tutorial_search_user_data_t *user_data = (t8_tutorial_search_user_data_t *) t8_forest_get_user_data (forest);
  T8_ASSERT (user_data != NULL);
  user_data->num_elements_searched++;
  return 1;
}

/* The query callback. This will be called for each element once per active query
 * (= particle that may be inside this element).
 * The return value determines whether or not the query remains active for the children
 * of this element.
 * In our example this is the case if the particle is inside the element.
 * The additional input parameter 'is_leaf' will be true if the given element is a leaf
 * element (= an actual element in our forest, not a 'virtual' element in the hierarchy).
 * If the element is a leaf and the particle is contained in it, then we will increase
 * a counter for this element by one.
 * These counters are provided in an sc_array as user data of the input forest.
 */
static void
t8_tutorial_search_query_callback (t8_forest_t forest, const t8_locidx_t ltreeid, const t8_element_t *element,
                                   const int is_leaf, [[maybe_unused]] const t8_element_array_t *leaf_elements,
                                   const t8_locidx_t tree_leaf_index, sc_array_t *queries, sc_array_t *query_indices,
                                   int *query_matches, const size_t num_active_queries)
{
  /* Build an array of all particle-coords, necessary for t8_forest_element_point_batch_inside */
  double *coords = T8_ALLOC (double, 3 * num_active_queries);
  for (size_t particle_iter = 0; particle_iter < num_active_queries; particle_iter++) {
    /* Get the query at the current query-index (particle_iter in this case). */
    const size_t particle_id = *(size_t *) sc_array_index_int (query_indices, particle_iter);
    /* Cast the query into a particle*/
    t8_tutorial_search_particle_t *particle
      = (t8_tutorial_search_particle_t *) sc_array_index ((sc_array_t *) queries, particle_id);
    /* extract the coordinates of the particle struct */
    coords[3 * particle_iter] = particle->coordinates[0];
    coords[3 * particle_iter + 1] = particle->coordinates[1];
    coords[3 * particle_iter + 2] = particle->coordinates[2];
  }
  /* Numerical tolerance for the is inside element check. */
  const double tolerance = 1e-8;
  /* Get the user data pointer that stores the number of particles per element
   * and number of elements searched. */
  t8_tutorial_search_user_data_t *user_data = (t8_tutorial_search_user_data_t *) t8_forest_get_user_data (forest);
  /* Ensure user_data is present. */
  T8_ASSERT (user_data != NULL);
  sc_array *particles_per_element = user_data->particles_per_element;
  /* Ensure that the data is actually set. */
  T8_ASSERT (particles_per_element != NULL);
  T8_ASSERT (queries != NULL);

  /* Test whether the particles are inside this element. */
  t8_forest_element_points_inside (forest, ltreeid, element, coords, num_active_queries, query_matches, tolerance);
  T8_FREE (coords);
  for (size_t matches_id = 0; matches_id < num_active_queries; matches_id++) {
    if (query_matches[matches_id]) {
      if (is_leaf) {
        /* The particle is inside and this element is a leaf element.
       * We mark the particle for being inside the partition and we increase
       * the particles_per_element counter of this element. */
        /* In order to find the index of the element inside the array, we compute the
       * index of the first element of this tree plus the index of the element within
       * the tree. */
        t8_locidx_t element_index = t8_forest_get_tree_element_offset (forest, ltreeid) + tree_leaf_index;
        /* Get the correct particle_id */
        size_t particle_id = *(size_t *) sc_array_index_int (query_indices, matches_id);
        t8_tutorial_search_particle_t *particle
          = (t8_tutorial_search_particle_t *) sc_array_index ((sc_array_t *) queries, particle_id);
        particle->is_inside_partition = 1;
        *(double *) t8_sc_array_index_locidx (particles_per_element, element_index) += 1;
      }
    }
  }
}

/* Write the forest to vtu files and also write the particles_per_element
 * data. */
static void
t8_tutorial_search_vtk (t8_forest_t forest, sc_array *particles_per_element, const char *prefix)
{
  /* Define the additional vtu data that we want to write. */
  t8_vtk_data_field_t vtk_data;

  vtk_data.data = (double *) particles_per_element->array;
  strcpy (vtk_data.description, "Number of particles");
  vtk_data.type = T8_VTK_SCALAR;
  /* Write vtu files with our user define number of particles data. */
  t8_forest_write_vtk_ext (forest, prefix, 1, 1, 1, 1, 0, 0, 0, 1, &vtk_data);

  t8_global_productionf (" [search] Wrote forest and number of particles per element to %s*\n", prefix);
}

/* Perform the actual search and write the forest with the number of particles per element
 * to vtu files. */
static void
t8_tutorial_search_for_particles (t8_forest_t forest, sc_array *particles)
{
  sc_array particles_per_element;
  t8_locidx_t num_local_elements = t8_forest_get_local_num_leaf_elements (forest);
  t8_locidx_t ielement;
  t8_locidx_t global_num_searched_elements;
  t8_gloidx_t global_num_elements;
  const char *prefix = "t8_search_for_particles";
  t8_tutorial_search_user_data_t user_data;

  t8_global_productionf (" [search] Starting search for %zd particles.\n", particles->elem_count);

  /*
   * Init
   */

  /* Initialize the particles_per_element array to store one double per element.
   * Note, we only use double here, since we later want to write it out to vtu files
   * and vtu output currently only supports writing doubles. */
  sc_array_init_count (&particles_per_element, sizeof (double), num_local_elements);
  /* Set the entry of each element to 0 */
  for (ielement = 0; ielement < num_local_elements; ++ielement) {
    *(double *) t8_sc_array_index_locidx (&particles_per_element, ielement) = 0;
  }
  /* Initialize the user data with the particles per element array and 0 elements searched. */
  user_data.particles_per_element = &particles_per_element;
  user_data.num_elements_searched = 0;
  /* Store this user data as the user data of the forest such that we can
   * access it in the search callbacks. */
  t8_forest_set_user_data (forest, &user_data);

  /*
   * search
   */

  /* Perform the search of the forest. The second argument is the search callback function,
   * then the query callback function and the last argument is the array of queries. */
  t8_forest_search (forest, t8_tutorial_search_callback, t8_tutorial_search_query_callback, particles);
  /*
   * Output
   */

  /* Write the forest and particles per element to vtu. */
  t8_tutorial_search_vtk (forest, &particles_per_element, prefix);

  /* Compute the process global number of searched elements. */
  sc_MPI_Reduce (&user_data.num_elements_searched, &global_num_searched_elements, 1, T8_MPI_LOCIDX, sc_MPI_SUM, 0,
                 t8_forest_get_mpicomm (forest));

  /* Print the number of elements and number of searched elements. */
  global_num_elements = t8_forest_get_global_num_leaf_elements (forest);
  t8_global_productionf (" [search] Searched forest with %" T8_GLOIDX_FORMAT " global elements.\n",
                         global_num_elements);
  t8_global_errorf (" [search] Looked at %i elements during search.\n", global_num_searched_elements);

  /*
   * Clean up
   */
  sc_array_reset (&particles_per_element);
}

/** Create an array of a given number of particles on the root process
 * and broadcast it to all other processes.
 * \param [in] num_particles  The number of particles to create.
 * \param [in] seed           The seed to be used for the random number generator.
 * \param [in] comm           MPI communicator to specify on which processes we create this array.
 */
static sc_array *
t8_tutorial_search_build_particles (size_t num_particles, unsigned int seed, sc_MPI_Comm comm)
{
  /* Specify lower and upper bounds for the coordinates in each dimension. */
  double boundary_low[3] = { 0.2, 0.3, 0.1 };
  double boundary_high[3] = { 0.8, 0.75, 0.9 };
  int mpirank;
  int mpiret;
  sc_array *particles;

  /* Get the MPI rank. */
  mpiret = sc_MPI_Comm_rank (comm, &mpirank);
  SC_CHECK_MPI (mpiret);

  /* Create an array for num_particles many particles. */
  particles = sc_array_new_count (sizeof (t8_tutorial_search_particle_t), num_particles);

  /* We build the array on rank 0 and broadcast it to the other ranks.
   * This ensures that all ranks have the same randomly generated particles. */
  if (mpirank == 0) {
    /* Rank 0 fills this array with random particles. */
    size_t iparticle;
    srand (seed);
    for (iparticle = 0; iparticle < num_particles; ++iparticle) {
      int dim;
      /* Get this particle's pointer. */
      t8_tutorial_search_particle_t *particle
        = (t8_tutorial_search_particle_t *) sc_array_index_int (particles, iparticle);
      for (dim = 0; dim < 3; ++dim) {
        /* Create a random value between boundary_low[dim] and boundary_high[dim] */
        particle->coordinates[dim]
          = (double) rand () / RAND_MAX * (boundary_high[dim] - boundary_low[dim]) + boundary_low[dim];
        /* Initialize the is_inside_partition flag. */
        particle->is_inside_partition = 0;
      }
    }
  }
  /* Broadcast this array to all other processes. */
  mpiret
    = sc_MPI_Bcast (particles->array, sizeof (t8_tutorial_search_particle_t) * num_particles, sc_MPI_BYTE, 0, comm);
  SC_CHECK_MPI (mpiret);

  t8_global_productionf (
    " [search] Created %zd random particles inside the box [%.2f,%.2f] x [%.2f,%.2f] x [%.2f,%.2f].\n", num_particles,
    boundary_low[0], boundary_high[0], boundary_low[1], boundary_high[1], boundary_low[2], boundary_high[2]);

  return particles;
}

int
main (int argc, char **argv)
{
  int mpiret;
  sc_MPI_Comm comm;
  t8_cmesh_t cmesh;
  t8_forest_t forest;
  /* The uniform refinement level of the forest. */
  const int level = 5;
  /* The number of particles to generate. */
  const size_t num_particles = 2000;
  /* The seed for the random number generator. */
  const unsigned seed = 0;

  /*
   * Init
   */

  /* Initialize MPI. This has to happen before we initialize sc or t8code. */
  mpiret = sc_MPI_Init (&argc, &argv);
  /* Error check the MPI return value. */
  SC_CHECK_MPI (mpiret);

  /* Initialize the sc library, has to happen before we initialize t8code. */
  sc_init (sc_MPI_COMM_WORLD, 1, 1, NULL, SC_LP_ESSENTIAL);
  /* Initialize t8code with log level SC_LP_PRODUCTION. See sc.h for more info on the leg levels. */
  t8_init (SC_LP_PRODUCTION);

  /* We will use MPI_COMM_WORLD as a communicator. */
  comm = sc_MPI_COMM_WORLD;

  /* Print a message on the root process. */
  t8_global_productionf (" [search] \n");
  t8_global_productionf (" [search] Hello, this is the search example of t8code.\n");
  t8_global_productionf (
    " [search] We will search for all elements in a forest that contain randomly created particles.\n");
  t8_global_productionf (" [search] \n");

  /*
   *  Build forest and particles.
   */

  /* Build a cube cmesh with tet, hex, and prism trees. */
  cmesh = t8_cmesh_new_hypercube_hybrid (comm, 0, 0);
  /* Build a uniform forest on it. */
  forest = t8_forest_new_uniform (cmesh, t8_scheme_new_default (), level, 0, comm);

  /* Adapt the forest. We can reuse the forest variable, since the new adapted
   * forest will take ownership of the old forest and destroy it.
   * Note that the adapted forest is a new forest, though. */
  forest = t8_step3_adapt_forest (forest);

  /* Print information of our new forest. */
  t8_global_productionf (" [search] Created an adapted forest with hybrid elements on a unit cube geometry.\n");
  t8_step3_print_forest_information (forest);

  /* Create an array with random particles. */
  sc_array_t *particles = t8_tutorial_search_build_particles (num_particles, seed, comm);

  /*
   * Search for particles.
   */

  t8_tutorial_search_for_particles (forest, particles);

  /*
   * clean-up
   */

  /* Destroy the forest. */
  t8_forest_unref (&forest);
  sc_array_destroy (particles);

  sc_finalize ();

  mpiret = sc_MPI_Finalize ();
  SC_CHECK_MPI (mpiret);

  return 0;
}