Zoltan2
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
Zoltan2_AlgParMA.hpp
Go to the documentation of this file.
1 // @HEADER
2 // *****************************************************************************
3 // Zoltan2: A package of combinatorial algorithms for scientific computing
4 //
5 // Copyright 2012 NTESS and the Zoltan2 contributors.
6 // SPDX-License-Identifier: BSD-3-Clause
7 // *****************************************************************************
8 // @HEADER
9 
10 #ifndef _ZOLTAN2_ALGPARMA_HPP_
11 #define _ZOLTAN2_ALGPARMA_HPP_
12 
13 #include <Zoltan2_Algorithm.hpp>
15 #include <Zoltan2_Util.hpp>
16 #include <Zoltan2_TPLTraits.hpp>
17 
21 //
22 // This design creates an apf mesh to run the ParMA algorithms on. The
23 // final solution is determined by changes from beginning to end of the mesh.
24 // This approach allows development closer to that of PUMI setup but at the
25 // cost of creating an extra mesh representation.
26 //
27 // Available ParMA algorithms are given by setting the parma_method parameter
28 // of the sublist parma_paramaters to one of the following:
29 // Vertex - Balances targeting vertex imbalance
30 // Element - Balances targeting element imbalance
31 // VtxElm - Balances targeting vertex and element imbalance
32 // VtxEdgeElm - Balances targeting vertex, edge, and element imbalance
33 // Ghost - Balances using ghost element aware diffusion
34 // Shape - Optimizes shape of parts by increasing the size of small part boundaries
35 // Centroid - Balances using centroid diffusion
37 
38 #ifndef HAVE_ZOLTAN2_PARMA
39 
40 // Error handling for when ParMA is requested
41 // but Zoltan2 not built with ParMA.
42 
43 namespace Zoltan2 {
44 template <typename Adapter>
45 class AlgParMA : public Algorithm<Adapter>
46 {
47 public:
48  typedef typename Adapter::user_t user_t;
49 
50  AlgParMA(const RCP<const Environment> &/* env */,
51  const RCP<const Comm<int> > &/* problemComm */,
52  const RCP<const BaseAdapter<user_t> > &/* adapter */)
53  {
54  throw std::runtime_error(
55  "BUILD ERROR: ParMA requested but not compiled into Zoltan2.\n"
56  "Please set CMake flag Zoltan2_ENABLE_ParMA:BOOL=ON.");
57  }
58 };
59 }
60 
61 #endif
62 
63 #ifdef HAVE_ZOLTAN2_PARMA
64 
65 
66 #include <apf.h>
67 #include <gmi_null.h>
68 #include <apfMDS.h>
69 #include <apfMesh2.h>
70 #include <apfNumbering.h>
71 #include <PCU.h>
72 #include <parma.h>
73 #include <apfConvert.h>
74 #include <apfShape.h>
75 #include <map>
76 #include <cassert>
77 
78 namespace Zoltan2 {
79 
80 template <typename Adapter>
81 class AlgParMA : public Algorithm<Adapter>
82 {
83 
84 private:
85 
86  typedef typename Adapter::lno_t lno_t;
87  typedef typename Adapter::gno_t gno_t;
88  typedef typename Adapter::scalar_t scalar_t;
89  typedef typename Adapter::offset_t offset_t;
90  typedef typename Adapter::part_t part_t;
91  typedef typename Adapter::user_t user_t;
92  typedef typename Adapter::userCoord_t userCoord_t;
93 
94  const RCP<const Environment> env;
95  const RCP<const Comm<int> > problemComm;
96  const RCP<const MeshAdapter<user_t> > adapter;
97 
98  apf::Mesh2* m;
99  apf::Numbering* gids;
100  apf::Numbering* origin_part_ids;
101  std::map<gno_t, lno_t> mapping_elm_gids_index;
102 
103  MPI_Comm mpicomm;
104  bool pcu_outside;
105 
106  void setMPIComm(const RCP<const Comm<int> > &problemComm__) {
107 # ifdef HAVE_ZOLTAN2_MPI
108  mpicomm = Teuchos::getRawMpiComm(*problemComm__);
109 # else
110  mpicomm = MPI_COMM_WORLD; // taken from siMPI
111 # endif
112  }
113  // provides conversion from an APF entity dimension to a Zoltan2 entity type
114  enum MeshEntityType entityAPFtoZ2(int dimension) const {return static_cast<MeshEntityType>(dimension);}
115 
116  //provides a conversion from the Zoltan2 topology type to and APF type
117  // throws an error on topology types not supported by APF
118  enum apf::Mesh::Type topologyZ2toAPF(enum EntityTopologyType ttype) const {
119  if (ttype==POINT)
120  return apf::Mesh::VERTEX;
121  else if (ttype==LINE_SEGMENT)
122  return apf::Mesh::EDGE;
123  else if (ttype==TRIANGLE)
124  return apf::Mesh::TRIANGLE;
125  else if (ttype==QUADRILATERAL)
126  return apf::Mesh::QUAD;
127  else if (ttype==TETRAHEDRON)
128  return apf::Mesh::TET;
129  else if (ttype==HEXAHEDRON)
130  return apf::Mesh::HEX;
131  else if (ttype==PRISM)
132  return apf::Mesh::PRISM;
133  else if (ttype==PYRAMID)
134  return apf::Mesh::PYRAMID;
135  else
136  throw std::runtime_error("APF does not support this topology type");
137 
138  }
139 
140  //Sets the weights of each entity in dimension 'dim' to those provided by the mesh adapter
141  //sets all weights in the mesh adapter but currently only one is considered by ParMA
142  void setEntWeights(int dim, apf::MeshTag* tag) {
143  MeshEntityType etype = entityAPFtoZ2(dim);
144  for (int i=0;i<m->getTagSize(tag);i++) {
145  apf::MeshIterator* itr = m->begin(dim);
146  apf::MeshEntity* ent;
147  const scalar_t* ws=NULL;
148  int stride;
149  if (i<adapter->getNumWeightsPerOf(etype))
150  adapter->getWeightsViewOf(etype,ws,stride,i);
151  int j=0;
152  while ((ent= m->iterate(itr))) {
153  double w = 1.0;
154  if (ws!=NULL)
155  w = static_cast<double>(ws[j]);
156  m->setDoubleTag(ent,tag,&w);
157  j++;
158  }
159  m->end(itr);
160  }
161  }
162 
163  //Helper function to set the weights of each dimension needed by the specific parma algorithm
164  apf::MeshTag* setWeights(bool vtx, bool edge, bool face, bool elm) {
165  int num_ws=1;
166  if (vtx)
167  num_ws = std::max(num_ws,adapter->getNumWeightsPerOf(MESH_VERTEX));
168  if (edge)
169  num_ws = std::max(num_ws,adapter->getNumWeightsPerOf(MESH_EDGE));
170  if (face)
171  num_ws = std::max(num_ws,adapter->getNumWeightsPerOf(MESH_FACE));
172  if (elm)
173  num_ws = std::max(num_ws,adapter->getNumWeightsPerOf(entityAPFtoZ2(m->getDimension())));
174  apf::MeshTag* tag = m->createDoubleTag("parma_weight",num_ws);
175  if (vtx)
176  setEntWeights(0,tag);
177  if (edge)
178  setEntWeights(1,tag);
179  if (face)
180  setEntWeights(2,tag);
181  if (elm) {
182  setEntWeights(m->getDimension(),tag);
183  }
184  return tag;
185  }
186 
187 
188  //APF Mesh construction helper functions modified and placed here to support arbitrary entity types
189  void constructElements(const gno_t* conn, lno_t nelem, const offset_t* offsets,
190  const EntityTopologyType* tops, apf::GlobalToVert& globalToVert)
191  {
192  apf::ModelEntity* interior = m->findModelEntity(m->getDimension(), 0);
193  for (lno_t i = 0; i < nelem; ++i) {
194  apf::Mesh::Type etype = topologyZ2toAPF(tops[i]);
195  apf::Downward verts;
196  for (offset_t j = offsets[i]; j < offsets[i+1]; ++j)
197  verts[j-offsets[i]] = globalToVert[conn[j]];
198  buildElement(m, interior, etype, verts);
199  }
200  }
201  int getMax(const apf::GlobalToVert& globalToVert)
202  {
203  int max = -1;
204  APF_CONST_ITERATE(apf::GlobalToVert, globalToVert, it)
205  max = std::max(max, it->first);
206  PCU_Max_Ints(&max, 1); // this is type-dependent
207  return max;
208  }
209  void constructResidence(apf::GlobalToVert& globalToVert)
210  {
211  int max = getMax(globalToVert);
212  int total = max + 1;
213  int peers = PCU_Comm_Peers();
214  int quotient = total / peers;
215  int remainder = total % peers;
216  int mySize = quotient;
217  int self = PCU_Comm_Self();
218  if (self == (peers - 1))
219  mySize += remainder;
220  typedef std::vector< std::vector<int> > TmpParts;
221  TmpParts tmpParts(mySize);
222  /* if we have a vertex, send its global id to the
223  broker for that global id */
224  PCU_Comm_Begin();
225  APF_ITERATE(apf::GlobalToVert, globalToVert, it) {
226  int gid = it->first;
227  int to = std::min(peers - 1, gid / quotient);
228  PCU_COMM_PACK(to, gid);
229  }
230  PCU_Comm_Send();
231  int myOffset = self * quotient;
232  /* brokers store all the part ids that sent messages
233  for each global id */
234  while (PCU_Comm_Receive()) {
235  int gid;
236  PCU_COMM_UNPACK(gid);
237  int from = PCU_Comm_Sender();
238  tmpParts.at(gid - myOffset).push_back(from);
239  }
240  /* for each global id, send all associated part ids
241  to all associated parts */
242  PCU_Comm_Begin();
243  for (int i = 0; i < mySize; ++i) {
244  std::vector<int>& parts = tmpParts[i];
245  for (size_t j = 0; j < parts.size(); ++j) {
246  int to = parts[j];
247  int gid = i + myOffset;
248  int nparts = parts.size();
249  PCU_COMM_PACK(to, gid);
250  PCU_COMM_PACK(to, nparts);
251  for (size_t k = 0; k < parts.size(); ++k)
252  PCU_COMM_PACK(to, parts[k]);
253  }
254  }
255  PCU_Comm_Send();
256  /* receiving a global id and associated parts,
257  lookup the vertex and classify it on the partition
258  model entity for that set of parts */
259  while (PCU_Comm_Receive()) {
260  int gid;
261  PCU_COMM_UNPACK(gid);
262  int nparts;
263  PCU_COMM_UNPACK(nparts);
264  apf::Parts residence;
265  for (int i = 0; i < nparts; ++i) {
266  int part;
267  PCU_COMM_UNPACK(part);
268  residence.insert(part);
269  }
270  apf::MeshEntity* vert = globalToVert[gid];
271  m->setResidence(vert, residence);
272  }
273  }
274 
275  /* given correct residence from the above algorithm,
276  negotiate remote copies by exchanging (gid,pointer)
277  pairs with parts in the residence of the vertex */
278  void constructRemotes(apf::GlobalToVert& globalToVert)
279  {
280  int self = PCU_Comm_Self();
281  PCU_Comm_Begin();
282  APF_ITERATE(apf::GlobalToVert, globalToVert, it) {
283  int gid = it->first;
284  apf::MeshEntity* vert = it->second;
285  apf::Parts residence;
286  m->getResidence(vert, residence);
287  APF_ITERATE(apf::Parts, residence, rit)
288  if (*rit != self) {
289  PCU_COMM_PACK(*rit, gid);
290  PCU_COMM_PACK(*rit, vert);
291  }
292  }
293  PCU_Comm_Send();
294  while (PCU_Comm_Receive()) {
295  int gid;
296  PCU_COMM_UNPACK(gid);
297  apf::MeshEntity* remote;
298  PCU_COMM_UNPACK(remote);
299  int from = PCU_Comm_Sender();
300  apf::MeshEntity* vert = globalToVert[gid];
301  m->addRemote(vert, from, remote);
302  }
303  }
304 
305 public:
306 
312  AlgParMA(const RCP<const Environment> &env__,
313  const RCP<const Comm<int> > &problemComm__,
314  const RCP<const IdentifierAdapter<user_t> > &adapter__)
315  {
316  throw std::runtime_error("ParMA needs a MeshAdapter but you haven't given it one");
317  }
318 
319  AlgParMA(const RCP<const Environment> &env__,
320  const RCP<const Comm<int> > &problemComm__,
321  const RCP<const VectorAdapter<user_t> > &adapter__)
322  {
323  throw std::runtime_error("ParMA needs a MeshAdapter but you haven't given it one");
324  }
325 
326  AlgParMA(const RCP<const Environment> &env__,
327  const RCP<const Comm<int> > &problemComm__,
328  const RCP<const GraphAdapter<user_t,userCoord_t> > &adapter__)
329  {
330  throw std::runtime_error("ParMA needs a MeshAdapter but you haven't given it one");
331  }
332 
333  AlgParMA(const RCP<const Environment> &env__,
334  const RCP<const Comm<int> > &problemComm__,
335  const RCP<const MatrixAdapter<user_t,userCoord_t> > &adapter__)
336  {
337  throw std::runtime_error("ParMA needs a MeshAdapter but you haven't given it one");
338 
339  }
340 
341  AlgParMA(const RCP<const Environment> &env__,
342  const RCP<const Comm<int> > &problemComm__,
343  const RCP<const MeshAdapter<user_t> > &adapter__) :
344  env(env__), problemComm(problemComm__), adapter(adapter__)
345  {
346  setMPIComm(problemComm__);
347 
348  //Setup PCU communications
349  //If PCU was already initialized outside (EX: for the APFMeshAdapter)
350  // we don't initialize it again.
351  pcu_outside=false;
352  if (!PCU_Comm_Initialized())
353  PCU_Comm_Init();
354  else
355  pcu_outside=true;
356  PCU_Switch_Comm(mpicomm);
357 
358  //Find the mesh dimension based on if there are any regions or faces in the part
359  // an all reduce is needed in case one part is empty (Ex: after hypergraph partitioning)
360  int dim;
361  if (adapter->getLocalNumOf(MESH_REGION)>0)
362  dim=3;
363  else if (adapter->getLocalNumOf(MESH_FACE)>0)
364  dim=2;
365  else
366  dim=0;
367  PCU_Max_Ints(&dim,1);
368  if (dim<2)
369  throw std::runtime_error("ParMA neeeds faces or region information");
370 
371  //GFD Currently not allowing ParMA to balance non element primary types
372  if (dim!=adapter->getPrimaryEntityType())
373  throw std::runtime_error("ParMA only supports balancing primary type==mesh element");
374 
375  //Create empty apf mesh
376  gmi_register_null();
377  gmi_model* g = gmi_load(".null");
378  enum MeshEntityType primary_type = entityAPFtoZ2(dim);
379  m = apf::makeEmptyMdsMesh(g,dim,false);
380 
381  //Get entity topology types
382  const EntityTopologyType* tops;
383  try {
384  adapter->getTopologyViewOf(primary_type,tops);
385  }
387 
388  //Get element global ids and part ids
389  const gno_t* element_gids;
390  const part_t* part_ids;
391  adapter->getIDsViewOf(primary_type,element_gids);
392  adapter->getPartsView(part_ids);
393  for (size_t i =0;i<adapter->getLocalNumOf(primary_type);i++)
394  mapping_elm_gids_index[element_gids[i]] = i;
395 
396  //get vertex global ids
397  const gno_t* vertex_gids;
398  adapter->getIDsViewOf(MESH_VERTEX,vertex_gids);
399 
400  //Get vertex coordinates
401  int c_dim = adapter->getDimension();
402  const scalar_t ** vertex_coords = new const scalar_t*[c_dim];
403  int* strides = new int[c_dim];
404  for (int i=0;i<c_dim;i++)
405  adapter->getCoordinatesViewOf(MESH_VERTEX,vertex_coords[i],strides[i],i);
406 
407  //Get first adjacencies from elements to vertices
408  if (!adapter->availAdjs(primary_type,MESH_VERTEX))
409  throw "APF needs adjacency information from elements to vertices";
410  const offset_t* offsets;
411  const gno_t* adjacent_vertex_gids;
412  adapter->getAdjsView(primary_type, MESH_VERTEX,offsets,adjacent_vertex_gids);
413 
414  //build the apf mesh
415  apf::GlobalToVert vertex_mapping;
416  apf::ModelEntity* interior = m->findModelEntity(m->getDimension(), 0);
417  for (size_t i=0;i<adapter->getLocalNumOf(MESH_VERTEX);i++) {
418  apf::MeshEntity* vtx = m->createVert_(interior);
419  scalar_t temp_coords[3];
420  for (int k=0;k<c_dim&&k<3;k++)
421  temp_coords[k] = vertex_coords[k][i*strides[k]];
422 
423  for (int k=c_dim;k<3;k++)
424  temp_coords[k] = 0;
425  apf::Vector3 point(temp_coords[0],temp_coords[1],temp_coords[2]);
426  m->setPoint(vtx,0,point);
427  vertex_mapping[vertex_gids[i]] = vtx;
428  }
429  //Call modified helper functions to build the mesh from element to vertex adjacency
430  constructElements(adjacent_vertex_gids, adapter->getLocalNumOf(primary_type), offsets, tops, vertex_mapping);
431  constructResidence(vertex_mapping);
432  constructRemotes(vertex_mapping);
433  stitchMesh(m);
434  m->acceptChanges();
435 
436 
437  //Setup numberings of global ids and original part ids
438  // for use after ParMA is run
439  apf::FieldShape* s = apf::getConstant(dim);
440  gids = apf::createNumbering(m,"global_ids",s,1);
441  origin_part_ids = apf::createNumbering(m,"origin",s,1);
442 
443  //number the global ids and original part ids
444  apf::MeshIterator* itr = m->begin(dim);
445  apf::MeshEntity* ent;
446  int i = 0;
447  while ((ent = m->iterate(itr))) {
448  apf::number(gids,ent,0,0,element_gids[i]);
449  apf::number(origin_part_ids,ent,0,0,PCU_Comm_Self());
450  i++;
451  }
452  m->end(itr);
453 
454  //final setup for apf mesh
455  apf::alignMdsRemotes(m);
456  apf::deriveMdsModel(m);
457  m->acceptChanges();
458  m->verify();
459 
460  //cleanup temp storage
461  delete [] vertex_coords;
462  delete [] strides;
463  }
464  void partition(const RCP<PartitioningSolution<Adapter> > &solution);
465 
466 };
467 
469 template <typename Adapter>
471  const RCP<PartitioningSolution<Adapter> > &solution
472 )
473 {
474  //Get parameters
475  std::string alg_name = "VtxElm";
476  double imbalance = 1.1;
477  double step = .5;
478  int ghost_layers=3;
479  int ghost_bridge=m->getDimension()-1;
480 
481  //Get the parameters for ParMA
482  const Teuchos::ParameterList &pl = env->getParameters();
483  try {
484  const Teuchos::ParameterList &ppl = pl.sublist("parma_parameters");
485  for (ParameterList::ConstIterator iter = ppl.begin();
486  iter != ppl.end(); iter++) {
487  const std::string &zname = pl.name(iter);
488  if (zname == "parma_method") {
489  std::string zval = pl.entry(iter).getValue(&zval);
490  alg_name = zval;
491  }
492  else if (zname == "step_size") {
493  double zval = pl.entry(iter).getValue(&zval);
494  step = zval;
495  }
496  else if (zname=="ghost_layers" || zname=="ghost_bridge") {
497  int zval = pl.entry(iter).getValue(&zval);
498  if (zname=="ghost_layers")
499  ghost_layers = zval;
500  else
501  ghost_bridge = zval;
502  }
503  }
504  }
505  catch (std::exception &e) {
506  //No parma_parameters sublist found
507  }
508 
509  const Teuchos::ParameterEntry *pe2 = pl.getEntryPtr("imbalance_tolerance");
510  if (pe2){
511  imbalance = pe2->getValue<double>(&imbalance);
512  }
513 
514  //booleans for which dimensions need weights
515  bool weightVertex,weightEdge,weightFace,weightElement;
516  weightVertex=weightEdge=weightFace=weightElement=false;
517 
518  //Build the selected balancer
519  apf::Balancer* balancer;
520  const int verbose = 1;
521  if (alg_name=="Vertex") {
522  balancer = Parma_MakeVtxBalancer(m, step, verbose);
523  weightVertex = true;
524  }
525  else if (alg_name=="Element") {
526  balancer = Parma_MakeElmBalancer(m, step, verbose);
527  weightElement=true;
528  }
529  else if (alg_name=="VtxElm") {
530  balancer = Parma_MakeVtxElmBalancer(m,step,verbose);
531  weightVertex = weightElement=true;
532  }
533  else if (alg_name=="VtxEdgeElm") {
534  balancer = Parma_MakeVtxEdgeElmBalancer(m, step, verbose);
535  weightVertex=weightEdge=weightElement=true;
536  }
537  else if (alg_name=="Ghost") {
538  balancer = Parma_MakeGhostDiffuser(m, ghost_layers, ghost_bridge, step, verbose);
539  weightVertex=weightEdge=weightFace=true;
540  if (3 == m->getDimension()) {
541  weightElement=true;
542  }
543  }
544  else if (alg_name=="Shape") {
545  balancer = Parma_MakeShapeOptimizer(m,step,verbose);
546  weightElement=true;
547  }
548  else if (alg_name=="Centroid") {
549  balancer = Parma_MakeCentroidDiffuser(m,step,verbose);
550  weightElement=true;
551  }
552  else {
553  throw std::runtime_error("No such parma method defined");
554  }
555 
556  //build the weights
557  apf::MeshTag* weights = setWeights(weightVertex,weightEdge,weightFace,weightElement);
558 
559  //balance the apf mesh
560  balancer->balance(weights, imbalance);
561  delete balancer;
562 
563  // Load answer into the solution.
564  int num_local = adapter->getLocalNumOf(adapter->getPrimaryEntityType());
565  ArrayRCP<part_t> partList(new part_t[num_local], 0, num_local, true);
566 
567  //Setup for communication
568  PCU_Comm_Begin();
569  apf::MeshEntity* ent;
570  apf::MeshIterator* itr = m->begin(m->getDimension());
571  //Pack information back to each elements original owner
572  while ((ent=m->iterate(itr))) {
573  if (m->isOwned(ent)) {
574  part_t target_part_id = apf::getNumber(origin_part_ids,ent,0,0);
575  gno_t element_gid = apf::getNumber(gids,ent,0,0);
576  PCU_COMM_PACK(target_part_id,element_gid);
577  }
578  }
579  m->end(itr);
580 
581  //Send information off
582  PCU_Comm_Send();
583 
584  //Unpack information and set new part ids
585  while (PCU_Comm_Receive()) {
586  gno_t global_id;
587  PCU_COMM_UNPACK(global_id);
588  lno_t local_id = mapping_elm_gids_index[global_id];
589  part_t new_part_id = PCU_Comm_Sender();
590  partList[local_id] = new_part_id;
591  }
592  //construct partition solution
593  solution->setParts(partList);
594 
595  // Clean up
596  apf::destroyNumbering(gids);
597  apf::destroyNumbering(origin_part_ids);
598  apf::removeTagFromDimension(m, weights, m->getDimension());
599  m->destroyTag(weights);
600  m->destroyNative();
601  apf::destroyMesh(m);
602  //only free PCU if it isn't being used outside
603  if (!pcu_outside)
604  PCU_Comm_Free();
605 }
606 
607 } // namespace Zoltan2
608 
609 #endif // HAVE_ZOLTAN2_PARMA
610 
611 #endif
virtual void partition(const RCP< PartitioningSolution< Adapter > > &)
Partitioning method.
AlgParMA(const RCP< const Environment > &, const RCP< const Comm< int > > &, const RCP< const BaseAdapter< user_t > > &)
#define Z2_FORWARD_EXCEPTIONS
Forward an exception back through call stack.
static ArrayRCP< ArrayRCP< zscalar_t > > weights
map_t::global_ordinal_type gno_t
Definition: mapRemotes.cpp:27
Defines the PartitioningSolution class.
Adapter::user_t user_t
SparseMatrixAdapter_t::part_t part_t
Adapter::scalar_t scalar_t
Adapter::part_t part_t
Algorithm defines the base class for all algorithms.
map_t::local_ordinal_type lno_t
Definition: mapRemotes.cpp:26
EntityTopologyType
Enumerate entity topology types for meshes: points,lines,polygons,triangles,quadrilaterals, polyhedrons, tetrahedrons, hexhedrons, prisms, or pyramids.
Traits class to handle conversions between gno_t/lno_t and TPL data types (e.g., ParMETIS&#39;s idx_t...
MeshEntityType
Enumerate entity types for meshes: Regions, Faces, Edges, or Vertices.
A gathering of useful namespace methods.
Zoltan2::BasicUserTypes< zscalar_t, zlno_t, zgno_t > user_t
Definition: Metric.cpp:39