.. _using-CelloArray: **************** Using CelloArray **************** ========== Background ========== Historically, to access elements at a given location, ``(ix, iy, iz)`` of an array in Enzo, the developer would need to explicitly calculate the index of the pointer using knowledge of the underlying shape of the array represented by the pointer. To simplify and enhance the readability of code in Enzo-E, we have implemented ``CelloArray``, a Multi-dimensional Array class template that wraps the data and encapsulate array operations. This class template draw’s loose inspiration from Athena++’s ``AthenaArray`` and numpy’s ``ndarray``. See the first two cases listed in :ref:`array-examples` for comparisons of snipets written using ``CelloArray`` and traditional pointer operations. These examples reflect operations performed in Enzo-E. Throughout the Enzo portion of the codebase, we extensively use the type ``EFlt3DArray`` which acts is an alias for ``CelloArray``. ============ Design Goals ============ The design of the class was primarily driven by the following specifications: * Emphasize fast access to array elements by passing the index along each dimension to the ``operator()`` method. * This method can be inlined within for-loops and for 3D arrays it has the same complexity as that of ``AthenaArray``. * Simple benchmarks show that the current implementation achieves performance comparable to c-style array access * The CelloArray needs tp be able to allocate and manage its own memory AND wrap existing pointers (namely the pointers allocated by the Cello’s Field framework) * This allows code using the ``CelloArray`` to coexist alongside code which use pointers in a more conventional way. * ``CelloArray`` needs be able to represent a view of a (mostly contiguous) subarray of a pre-existing instance of ``CelloArray``. This facilitates the encapsulation of a directional mesh operation in a single generalized function (e.g. writing a single flux function for all directions rather than separate functions to compute flux along the x, y, and z directions). ============== User Interface ============== The class template is formally defined as ``CelloArray`` where ``T`` is the contained type (frequently ``enzo_float``) and ``D`` is the number of dimensionsions of the array. At a high-level, this template class has semantics like a pointer or ``std::shared_ptr`` (there are also similarities to numpy's , ``ndarray``). These objects serve as a smart-pointer with methods for treating the data as a specialized array. These semantics explicitly differ from the C++ standard library containers (like ``std::vector``). In other words, ``CelloArray`` acts as an address for the underlying data. The copy constructor and copy assignment operation effectively make shallow copies and deepcopies are made by explicitly invoking special methods. A consequnce of this is that any modifications made to the elements of an array within a function, where the array had been passed by value, will affect the value of the array outside of the function. We will return to this topic below in :ref:`CelloArray-Pointer-Semantics` To provide a more detailed description of ``CelloArray``'s user interface it is most straightforward to describe the different operations with examples (rather than providing a detailed API). Array Creation -------------- Simplest initialization: * Use the constructor ``CelloArray(Args... args)`` to construct an array of 0s of shape ``(arg0, arg1, ... arg{D-1})``. The resulting array owns the underlying memory and deallocation is entirely taken care of * Examples: * Construct an array of shape ``(2,3,4)`` that holds doubles: .. code-block:: c++ CelloArray arr(2,3,4); * Construct an array of shape ``(5,)`` that holds ints: .. code-block:: c++ CelloArray arr(5); CelloArray arr2 = CelloArray(5); // yields same result Wrap a pre-existing pointer: * Use the constructor ``CelloArray(T* array, Args... args);`` to wrap the pointer ``array`` which represents an array with shape ``(arg0, arg1, ... arg{D-1})``. * Example: Construct an array representing ``[[0,1,2],[3,4,5]]``: .. code-block:: c++ int data[] = {0,1,2,3,4,5}; CelloArray arr(data,2,3); We can also forward declare an array and assign values to it later. .. code-block:: c++ int data[] = {0,1,2,3,4,5}; CelloArray arr; arr = CelloArray(data,2,3); Dimension Size -------------- To get the length along a dimension (or axis), call ``arr.shape(unsigned int dim)``, where ``dim`` is the number of the dimension. Dimensions numbers start at ``0`` and are ordered with increasing indexing speed (``dim=D-1`` is the dimension with fastest indexing). Element Access -------------- To access an element pass indices to the ``operator()(Args... args)`` method. As many indices should be specified as there are dimensions in the array (the number of args **must** match the number of dimensions. The ``operator()(Args... args)`` method returns a reference or copy (depending on the circumstance) of the element. **Example:** print element ``(0,2)`` of the array ``[[0,1,2],[3,4,5]]``: .. code-block:: c++ int data[] = {0,1,2,3,4,5}; CelloArray arr(data,2,3); printf("%d\n", arr(0,2)); // prints "2" // printf("%d\n", arr(2)); This would fail to compile // printf("%d\n", arr(0,0,2)); This would fail to compile Simple Assignment - Shallow/Deep Copies --------------------------------------- Shallow copies are produced via ordinary assignment. .. code-block:: c++ int data[] = {0,1,2,3,4,5}; CelloArray a(data,2,3); CelloArray b = a; // b is now a shallow copy of a CelloArray c(2,2); // c represents [[0,0],[0,0]] CelloArray d = c; // d is now a shallow copy of c c = a; // c is now a shallow copy of a When ``c`` is assinged the contents of ``a``, ``c`` becomes a shallow copy of ``a``. However the contents of ``d`` are unaffected. It still represents the array ``[[0,0],[0,0]]``. To perform a deepcopy, assign the the results of the ``deepcopy`` method. .. code-block:: c++ int data[] = {0,1,2,3,4,5}; CelloArray a(data,2,3); CelloArray e = a.deepcopy(); // e is now a deep copy of a Modifications to the contents of ``e`` will not be reflected in ``a`` or ``data`` (and vice-versa) Creating Subarrays ------------------ Calling ``arr.subarray(Args... args)`` returns a (mostly contiguous) view of a subarray specified by ``args``, where ``args`` represent the slices along each dimension. Each ``arg`` should be an instance of ``CSlice`` and the number of ``args`` **must** match the number of dimensions of the array. ``CSlice`` is a class that represents the start and stop points along a given dimension. The standard constructor is simply: ``CSlice(int start, int stop)``. As an aside, when ``arr`` has 2 or more dimensions, ``arr.subarray`` has an overload that accepts a single integer argument ``i``. The returned subarray is roughly equivalent to the view returned by ``arr.subarray(CSlice(i,i+1), ...)`` where the omitted arguments are slices that include all of the elements along the corresponding dimensions. The *only* difference is that the resulting array has 1 fewer dimensions than ``arr``. Subarray Examples ~~~~~~~~~~~~~~~~~ We present an extended example below. We start by defining a subarray, ``sub`` of an array ``arr`` (which wraps an existing pointer of data and represents the array ``[[0,1,2],[3,4,5]]``). .. code-block:: c++ int data[] = {0,1,2,3,4,5}; CelloArray arr(data,2,3); CelloArray sub = arr.subarray(CSlice(0,2),CSlice(1,3)); printf("%d\n", sub(1,0)) // prints "4"; At this point ``sub`` represents the subarray ``[[1,2],[4,5]]`` of the full array held by ``arr``. ``sub`` is truly a "view" of ``arr``. Modifications to the elements of ``sub`` and modifications to elements in ``arr`` (if it lies in the subarray), are reflected in both locations. .. code-block:: c++ arr(1,3) *= -3; sub(0,0) = -100; After executing the above block of code, ``arr`` now represents ``[[0,-100,2],[3,4,-15]]`` and ``sub`` represents the subarray ``[[-100,2],[4,-15]]``. ``CelloArray`` also provides support for taking subarrays of subarrays (or taking subarrays of shallow copies). If we define a subarray of ``sub`` the result will represent a view of the same underlying data .. code-block:: c++ CelloArray sub_of_sub = sub.subarray(CSlice(0,2),CSlice(0,1)); sub_of_sub(1,0) +=8; After the above operations, ``arr`` now reflects the full array ``[[0,-100,2],[3,12,-15]]``, while ``sub`` and ``sub_of_sub`` represent the subarrays ``[[-100,2],[12,-15]]`` and ``[[-100],[12]]``. Continuing to make shallow copies or subarrays of ``sub_of_sub`` and its derivatives will still yield views of the original array. If we assign ``arr`` the value of an unrelated array, the data tracked by all subarrays and subcopies are unaffected. .. code-block:: c++ CelloArray sub2 = arr.subarray(CSlice(1,2),CSlice(0,3)); arr = CelloArray(3,3); // setting arr equal to another array sub(1,0) /= -2; After execution of the preceeding block of code, ``sub`` represents ``[[-100,2],[-6,-15]]`` of the full array, ``sub_of_sub`` represents ``[[-100],[-6]]``, and ``sub2`` represents ``[[3,-6,-15]]`` (at this point the ``data`` pointer holds ``[0, -100, 2, 3, -6, -15]``). The fact that ``arr`` originally wrapped ``data`` has no bearing on the outcomes described above for each instance of ``CelloArray``. We illustrate this below with an analogous abreviated example, where the analog to ``arr``, called ``array``, originally owns its data. .. code-block:: c++ CelloArray array(2,3); array(0,0) = 0; array(0,1) = 1; array(0,2) = 2; array(1,0) = 3; array(1,1) = 4; array(1,2) = 5; CelloArray subarray = array.subarray(CSlice(0,2), CSlice(1,3)); array(1,3) *= -3; subarray(0,0) = -100; CelloArray subarray_of_subarray = subarray.subarray(CSlice(0,2), CSlice(0,1)); subarray_of_subarray(1,0) += 8; After executing the preceeding block of code, ``array`` reflects ``[[0,-100,2],[3,12,-15]]``, while ``subarray`` and ``subarray_of_subarray`` represent the subarrays ``[[-100,2],[12,-15]]`` and ``[[-100],[12]]``. If this was all the code we executed, the memory of ``array`` would be freed after its destructor and the destructors of all of subarrays or shallowcopies are called. If we reassign ``array`` to a different array, just like before, the values of its subarrays and shallow copies will be unaffected. .. code-block:: c++ CelloArray subarray2 = array.subarray(CSlice(1,2),CSlice(0,3)); array = CelloArray(3,3); subarray(1,0) /= -2; Now, ``subarray`` represents ``[[-100,2],[-6,-15]]`` from the full array, ``subarray_of_subarray`` represents ``[[-100],[-6]]``, and ``subarray2`` represents ``[[3,-6,-15]]``. We note that no memory has been deallocated. The memory will only be deallocated after ``subarray``, ``subarray_of_subarray``, and ``subarray2`` have all had their deconstructor called and/or been assigned unrelated arrays, assuming no additional subarrays or shallowcopies of any of the 3 variables are made in the meantime (in that case the memory would still not be deallocated until any additional subarrays/shallowcopies that view the original data are destroyed). Additional CSlice features ~~~~~~~~~~~~~~~~~~~~~~~~~~ ``CSlice`` provides two additional features to simplify code when the generating subarrays of a ``CelloArray`` instance. These are 1. The constructor supports negative indexing. For example ``CSlice(1,-1)`` represents a slice starting at the second element and stopping at (does not include) the last element along a dimension. Additionally, ``CSlice(-3,-1)`` represents starting from the third-to-last and stopping at the last element along a given dimension. 2. The constructor accepts the ``NULL`` and ``nullptr`` as the ``stop`` argument and understands it to mean that the last element along the axis. For example, ``CSlice(1, NULL)`` and ``CSlice(1,nullptr)`` both represent slices from the second element through the last element of the dimension. ``CSlice(-3,NULL)`` and ``CSlice(-3,nullptr)`` both represent slices extending from the third-to-last element through the last element of a dimension. Additionally, if ``NULL`` or ``nullptr`` are passed as the ``start`` argument, they are understood to mean that the slice starts at the first element (``CSlice(0,NULL)``, ``CSlice(0,nullptr)``, ``CSlice(NULL,NULL)``, & ``CSlice(nullptr,nullptr)`` are all equivalent). Finally, we note that ``CSlice`` provides a default constructor to simplify the construction of arrays of slices. However, to help avoid bugs, we require that any default-constructed ``CSlice`` must be assigned a non-default constructed value (or an error will be raised). Copying Elements between arrays ------------------------------- We also provide the ``copy_to`` instance method in order to copy elements between elements between two ``CelloArray`` instances. An example is illustrated below: .. code-block:: c++ int data[] = {0,1,2,3,4,5,6,7,8,9,10,11}; CelloArray arr(data,3,4); // arr reflects: [[0,1,2,3],[4,5,6,7],[8,9,10,11]] CelloArray arr2(2,2); // arr2 is initially [[0,0],[0,0]] arr2(0,0) = 7; arr2(0,1) = 7; arr2(1,0) = 7; arr2(1,1) = 7; // arr2 is now [[7,7],[7,7]] arr2.copy_to(arr.subarray(CSlice(1,3), CSlice(0,2))); // arr now reflects: [[0,1,2,3],[7,7,6,7],[7,7,10,11]] arr2(0,1) = 4; // arr2 is now [[7,4],[7,7]] and arr is unaffected .. _CelloArray-Pointer-Semantics: Pointer Semantics ----------------- The following table is provided to highlight some of the differences between the ``CelloArray``'s semantics and the semantics of a standard library container. .. list-table:: Semantic Comparison Table :widths: 12 44 44 :header-rows: 1 * - - ``CelloArray`` Semantics - Container Semantics * - Null-State - * a ``CelloArray`` technically supports an "null" state, which signals that it's uninitialized. (This is directly analogous to a ``nullptr``). * the ``CelloArray::is_null()`` method is provided for checking this condition. * The default constructor will create an uninitialized CelloArray - A container always has a valid state. A default-constructed container is simply an empty container. * - Copy constructor & assignment - These are shallow copies - These are deep copies * - ``const`` correctness - * like a ``float * const`` or a ``const std::shared_ptr``, a ``const CelloArray`` points to values at a fixed location in memory. While the memory address can’t be modified, the values stored at that address can still be mutated. * like a ``const float*`` or a ``std::shared_ptr``, a ``CelloArray`` points to a region in memory whose values cannot be modified. :superscript:`1` In other words the compiler will raise errors if you try to modify any of the values within the array. * Note: a ``CelloArray`` can be implicitly converted to a ``CelloArray`` (e.g. you can pass the former to a function that expecting the latter). It’s about as seemless as converting a ``float*`` to a ``const float*``. :superscript:`2` (In contrast, ``std::const_pointer_cast`` is required for converting a ``std::shared_ptr`` to a ``std::shared_ptr``) - The contents of a ``const`` container are immutable. For example, a ``const std::vector``, won't let you modify it's values. :superscript:`1` For completeness, we note that there's technically nothing stopping you from having a ``CelloArray`` that aliases the same data as a ``CelloArray``. In that case, you are could modify the values using the ``CelloArray``. :superscript:`2` In contrast, ``std::const_pointer_cast`` is required for converting a ``std::shared_ptr`` to a ``std::shared_ptr`` =========== Convenience =========== In the Enzo layer of the codebase, we provide several short-cuts for performing frequent actions related to the ``CelloArray`` to reduce boilerplate code. * We define and make extensive use of the type ``EFlt3DArray`` which is an alias for ``CelloArray``. * We define the class ``EnzoFieldArrayFactory`` which drastically reduces the boilerplate code associated with the initialization of instances of ``CelloArray`` that wrap Cello fields. * We define the class ``EnzoPermutedCoordinates`` convenience class which helps reduce boilerplate code associated with writing functions using instances of ``CelloArray`` that are generalized with respect to dimension. Two additional, features that can be enabled at compile-time to assist with debugging by defining macros before the inclusion of the ``CelloArray`` header file. * Defining the ``CHECK_BOUNDS`` macro, will cause checks of the validity of indices every time an element is accessed and will raise an error when it detects that an element that lies outside of the array bounds. * Defining the ``CHECK_FINITE_ELEMENTS`` macro will cause a check during retrieval of array elements that they are not ``NaN`` or ``inf`` .. _array-examples: ======== Examples ======== Below, we show some factored out, simplified examples, ways in which how ``CelloArray`` might simplify code: Copying Elements ---------------- This example illustrates how ``CelloArray`` simplifies the code required to copy elements between arrays. (We illustrate how one might write Nearest Neighbor reconstruction along the x-direction). This code assumes a mesh with shape ``(mz, my, mx)``. These are the dimensions of the entire mesh, including the ghost zones. Suppose we have: * An ``(mz,my,mx)`` array of cell-centered primitives ``w`` * An ``(mz,my,mx-1)`` array of left reconstructed values, ``wl`` * An ``(mz,my,mx-1)`` array of right reconstructed values, ``wr`` First is an the ``CelloArray`` version: .. code-block:: c++ typedef double enzo_float; typedef CelloArray EFlt3DArray; void reconstruct_NN_x(EFlt3DArray &w, EFlt3DArray &wl, EFlt3DArray &wr){ w.subarray(CSlice(0,w.shape(0)), CSlice(0,w.shape(1)), CSlice(0,-1)).copy_to(wl); w.subarray(CSlice(0,w.shape(0)), CSlice(0,w.shape(1)), CSlice(1,w.shape(2))).copy_to(wr); } The analogous code using conventional pointer operations is: .. code-block:: c++ typedef double enzo_float; void reconstruct_NN_x(enzo_float *w, enzo_float *wl, enzo_float *wr, int mx, int my, int mz){ int offset = 1; for (int iz=0; iz EFlt3DArray; void update_cons(EFlt3DArray &u, EFlt3DArray &out, EFlt3DArray &xflux, EFlt3DArray &yflux, EFlt3DArray &zflux, enzo_float dt, enzo_float dx, enzo_float dy, enzo_float dz){ enzo_float dtdx = dt/dx; enzo_float dtdy = dt/dy; enzo_float dtdz = dt/dz; for (int iz=1; iz EFlt3DArray; void update_cons(enzo_float *u, enzo_float *out, enzo_float *xflux, enzo_float *yflux, enzo_float *zflux, enzo_float dt, enzo_float dx, enzo_float dy, enzo_float dz, int mx, int my, int mz){ enzo_float dtdx = dt/dx; enzo_float dtdy = dt/dy; enzo_float dtdz = dt/dz; int x_offset = 1; int y_offset = mx; int z_offset = my*mx; for (int iz=1; iz EFlt3DArray; void calc_center_bfield(EFlt3DArray &bc, EFlt3DArray &bi, int dim){ EFlt3DArray bi_l = bi; // The following is a repeating pattern that gets factored out into // a helper function EFlt3DArrau bi_r; if (dim == 0) { bi_r = bi.subarray(CSlice(0,NULL), CSlice(0,NULL), CSlice(1,NULL)); } else if (dim == 1) { bi_r = bi.subarray(CSlice(0,NULL), CSlice(1,NULL), CSlice(0,NULL)); } else { bi_r = bi.subarray(CSlice(1,NULL), CSlice(0,NULL), CSlice(0,NULL)); } for (int iz=0; iz``). The keys of the map are always strings. Overview -------- This class provides some features that are atypical of maps, but are useful for our applications: * All values have the same shape. * All key-value pairs must be specified at construction. After construction: * key-value pairs can't be inserted/deleted. * the ``EFlt3DArray`` associated a with a key can't be overwritten with a different ``EFlt3DArray`` * Of course, the elements of the contained ``EFlt3DArray`` can still be modified. * The user specifies the ordering of the keys at construction. As a result of these features this class act like a dynamically configurable "struct of arrays". .. note:: In the future, we may replace this ``EnzoEFltArrayMap`` with a template class (e.g. ``CelloArrayMap``) that can represent a map of ``CelloArray``\s that have a datatype other than ``enzo_float`` and numbers of dimensions other than 3. In that case, we would probably define ``EnzoEFltArrayMap`` as an alias to maintain backwards compatability. Basic Usage ----------- Below, we provide a brief (non-exhaustive) overview of how the ``EnzoEFltArrayMap`` class is used. This is not as detailed as the description for the ``CelloArray`` template class. Creation ~~~~~~~~ There are 2 primary ways to construct a new ``EnzoEFltArrayMap`` instance. 1. The following code snippet illustrates how to construct an instance that holds existing ``CelloArray`` instances. .. code-block:: c++ // let's assume we have arrays holding density and velocity_x // (it does NOT matter whether any of these arrays allocate their own // data or wrap a pre-existing pointer) CelloArray density_arr(4,5,6); CelloArray velocity_x_arr(4,5,6); CelloArray velocity_y_arr(4,5,6); CelloArray velocity_z_arr(4,5,6); std::string map_name = "My Wrapper Map"; std::vector key_l = {"density", "velocity_x", "velocity_y", "velocity_z"}; std::vector> arr_l = {density_arr, velocity_x_arr, velocity_y_arr, velocity_z_arr}; EnzoEFltArrayMap wrapper_arr_map(map_name, key_l, arr_l); In the above example, we gave our array map the name ``"My Wrapper Map"``. This is completely optional and primarily for debugging purposes. We could replace the last line from the above block with the following, if we didn't want to name the map: .. code-block:: c++ EnzoEFltArrayMap unnamed_wrapper_arr_map(key_l, arr_l); **Note:** If ``key_l`` and ``arr_l`` did not have the same number of entries OR one of the arrays in ``arr_l`` had a shape that differed from any of the arrays in the list, the program would abort with an error message. 2. The other way to construct a new ``EnzoEFltArrayMap`` has the constructor allocate memory for all of the arrays in the map. This is illustrated below: .. code-block:: c++ std::string map_name = "My Scratch Map"; std::vector key_l = {"density", "velocity_x", "velocity_y", "velocity_z"}; std::array shape = {4,5,6}; EnzoEFltArrayMap scratch_arr_map(map_name, key_l, shape); In the above code-block, we gave our array map the name ``"My Scratch Map"``. ``scratch_arr_map`` contains the same keys as ``wrapper_arr_map`` and each of the contained arrays have the same shape. The values inside each array of ``scratch_arr_map`` were set by the constructor of ``CelloArray``. If we didn't want to name our array map, we could alternatively use: .. code-block:: c++ EnzoEFltArrayMap unnamed_scratch_arr_map(key_l, shape); Element Access ~~~~~~~~~~~~~~ The following snippet shows two ways to access a ``CelloArray`` associated with a given key .. code-block:: c++ std::vector key_l = {"density", "velocity_x", "velocity_y", "velocity_z"}; std::array shape = {4,5,6}; EnzoEFltArrayMap scratch_arr_map(map_name, key_l, shape); CelloArray my_arr1 = scratch_arr_map["density"]; CelloArray my_arr2 = scratch_arr_map.at("density"); Due to the pointer-semantics of ``CelloArray``, ``my_arr1`` and ``my_arr2`` are shallow-copies of one-another. For the same reason, ``other_arr1`` and ``other_arr2`` in the following snipet are also shallow copies of ``density_arr``. .. code-block:: c++ CelloArray density_arr(4,5,6); CelloArray velocity_x_arr(4,5,6); std::vector key_l = {"density", "velocity_x"}; std::vector> arr_l = {density_arr, velocity_x_arr}; EnzoEFltArrayMap other_arr_map(key_l, arr_l); CelloArray other_arr1 = scratch_arr_map["density"]; CelloArray other_arr2 = scratch_arr_map.at("density"); Unlike the element access methods of something like ``std::map``, these methods cannot be used to add new key-value pairs to an ``EnzoEFltArrayMap`` or to replace the ``CelloArray`` associated with a given key. (naturally, you can still change elements within the retrieved ``CelloArray`` instances). ``EnzoEFltArrayMap`` also supports index-access to it's contents. ``scratch_arr_map[i]`` accesses the ``CelloArray`` associated with the ``i``th key (using the order specified during construction). Note that we don't support passing an integer value to ``EnzoEFltArrayMap::at``. Copy and ``const`` Semantics ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Making a copy of an ``EnzoEFltArrayMap`` instance (e.g. with a copy constructor) always effectively produces a shallow copy. This is a natural consequnce of the ``CelloArray``\'s pointer semantics. For example, each element in a copy of a ``std::vector>`` would be a shallow copy of the corresponding element in the orginal vector. A ``const EnzoEFltArrayMap`` is effectively read-only. For reference, element-access of an ``EnzoEFltArrayMap`` instance yields a ``CelloArray`` instance (whose elements can be modified). In comparison, element-access of a ``const EnzoEFltArrayMap`` yields a ``CelloArray`` which prevents direct modification of array elements. Other Utilities ~~~~~~~~~~~~~~~ ``EnzoEFltArrayMap`` also provides a series of methods to query information about an instance's contents. We describe these methods for a hypothetical instance, ``arr_map``: * ``arr_map.size()`` specifies the number of key-value pairs in ``arr_map``. * ``arr_map.contains(const std::string& key)`` returns whether ``arr_map`` holds some key, ``key``. * ``arr_map.array_shape(unsigned int dim)`` returns the value that would be returned by calling ``arr.shape(dim)`` for any array contained within ``arr_map``. Some other utilities include: * the ``EnzoEFltArrayMap::subarray_map`` method. This constructs a new ``EnzoEFltArrayMap`` object that holds subarrays. * the ``EnzoEFltArrayMap::name`` method specifies the name associated with an array map. If there isn't an associated name, an empty string is returned. Internal Data Organization -------------------------- This class *currently* supports two approaches for internally storing the values of the map: 1. The default, flexible approach stores the ``CelloArray`` values in a ``vector``. This storage approach is analogous to having an array of pointers. This is the approach that is used when a ``EnzoEFltArrayMap`` is constructed that wraps pre-existing ``CelloArray`` instances. 2. The secondary, more specialized approach stores the individual ``CelloArray`` values in a single ``CelloArray`` instance. Access of individual ``CelloArray`` values is accomplished with the overload of the ``CelloArray::subarray`` method. This approach is used when you construct an ``EnzoEFltArrayMap`` that allocates memory for the contained ``CelloArray``\s. From an API-perspective, both approaches are nearly interchangable. However, the second approach should theoretically provide better data locality. The **only** API difference introduced by these approaches is the instances using the latter one supports the ``EnzoEFltArrayMap::get_backing_array()`` method, which provides access to the underlying ``CelloArray``. If that method is invoked on an instance that uses the first approach, the program will abort and print an error message. To that end, the ``EnzoEFltArrayMap::contiguous_arrays()`` instance method let's you determine which approach is being used. .. note:: The ``EnzoEFltArrayMap::get_backing_array()`` method was introduced as an "escape-hatch" to facillitate optimizations in particularly performance critical parts of the code (e.g. a Riemann Solver). Whenever this function is used, it introduces implicit assumptions about the properties of an ``EnzoEFltArrayMap`` instance (in addition to requiring a particular data organization, it usually introduces an assumption about the underlying key ordering). We **strongly** advise that you avoid using this method unless you deem it absolutely necessary. In many cases, the API of ``EnzoEFltArrayMap`` is sufficiently fast for retrieving the required ``CelloArray``\s before an expensive nested for-loop or in the outermost level of a nested for-loop. As an aside, the way that ``EnzoEFltArrayMap`` implements key-lookups is very crude. The implemenation could be refactored and sped up considerably.