source: XIOS/dev/dev_olga/src/distribution_client.cpp @ 1589

Last change on this file since 1589 was 1589, checked in by oabramkina, 3 years ago

Backporting r1578 and r1586 to dev, cleaning the code before merging it to XIOS 2.5.

File size: 19.7 KB
Line 
1/*!
2   \file distribution_client.cpp
3   \author Ha NGUYEN
4   \since 13 Jan 2015
5   \date 09 Mars 2015
6
7   \brief Index distribution on client side.
8 */
9#include "distribution_client.hpp"
10
11namespace xios {
12
13CDistributionClient::CDistributionClient(int rank, CGrid* grid)
14   : CDistribution(rank, 0)
15   , axisDomainOrder_()
16   , nLocal_(), nGlob_(), nBeginLocal_(), nBeginGlobal_()
17   , dataNIndex_(), dataDims_(), dataBegin_(), dataIndex_()
18   , gridMask_(), indexMap_()
19   , isDataDistributed_(true), axisNum_(0), domainNum_(0)
20   , localDataIndex_(), localMaskIndex_()
21   , globalLocalDataSendToServerMap_()
22   , infoIndex_(), isComputed_(false)
23   , elementLocalIndex_(), elementGlobalIndex_(), elementIndexData_()
24   , elementNLocal_(), elementNGlobal_()
25{
26  readDistributionInfo(grid);
27  createGlobalIndex();
28}
29
30CDistributionClient::~CDistributionClient()
31{ /* Nothing to do */ }
32
33void CDistributionClient::partialClear()
34{
35  GlobalLocalMap void1 ;
36  GlobalLocalMap void2 ;
37  std::vector<int> void3 ;
38  std::vector<bool> void4 ;
39
40  globalLocalDataSendToServerMap_.swap(void1) ;
41  globalDataIndex_.swap(void2) ;
42  localDataIndex_.swap(void3);
43  localMaskIndex_.swap(void4) ;
44}
45
46/*!
47  Read information of a grid to generate distribution.
48  Every grid is composed of several axis or/and domain(s). Their information are processed
49stored and used to calculate index distribution between client and server
50  \param [in] grid Grid to read
51*/
52void CDistributionClient::readDistributionInfo(CGrid* grid)
53{
54  std::vector<CDomain*> domList = grid->getDomains();
55  std::vector<CAxis*> axisList = grid->getAxis();
56  std::vector<CScalar*> scalarList = grid->getScalars();
57  CArray<int,1> axisDomainOrder = grid->axis_domain_order;
58
59  readDistributionInfo(domList, axisList, scalarList, axisDomainOrder);
60
61  // Then check mask of grid
62  int gridDim = domList.size() * 2 + axisList.size();
63  switch (gridDim) {
64    case 0:
65      gridMask_.resize(1);
66      gridMask_(0) = true;
67      break;
68    case 1:
69      if (!grid->mask_1d.isEmpty()) readGridMaskInfo(grid->mask_1d);
70      break;
71    case 2:
72      if (!grid->mask_2d.isEmpty()) readGridMaskInfo(grid->mask_2d);
73      break;
74    case 3:
75      if (!grid->mask_3d.isEmpty()) readGridMaskInfo(grid->mask_3d);
76      break;
77    case 4:
78      if (!grid->mask_4d.isEmpty()) readGridMaskInfo(grid->mask_4d);
79      break;
80    case 5:
81      if (!grid->mask_5d.isEmpty()) readGridMaskInfo(grid->mask_5d);
82      break;
83    case 6:
84      if (!grid->mask_6d.isEmpty()) readGridMaskInfo(grid->mask_6d);
85      break;
86    case 7:
87      if (!grid->mask_7d.isEmpty()) readGridMaskInfo(grid->mask_7d);
88      break;
89    default:
90      break;
91  }
92}
93
94/*!
95  Read information from domain(s) and axis to generate distribution.
96  All information related to domain, e.g ibegin, jbegin, ni, nj, ni_glo, nj_glo
97as well as related to axis, e.g dataNIndex, dataIndex will be stored to compute
98the distribution between clients and servers. Till now, every data structure of domain has been kept
99like before, e.g: data_n_index to make sure a compability, however, it should be changed?
100  \param [in] domList List of domains of grid
101  \param [in] axisList List of axis of grid
102  \param [in] scalarList List of scalar of grid
103  \param [in] axisDomainOrder order of axis and domain inside a grid. 2 if domain, 1 if axis and zero if scalar
104//  \param [in] gridMask Mask of grid, for now, keep it 3 dimension, but it needs changing
105*/
106void CDistributionClient::readDistributionInfo(const std::vector<CDomain*>& domList,
107                                               const std::vector<CAxis*>& axisList,
108                                               const std::vector<CScalar*>& scalarList,
109                                               const CArray<int,1>& axisDomainOrder)
110{
111  domainNum_ = domList.size();
112  axisNum_   = axisList.size();
113  numElement_ = axisDomainOrder.numElements(); // Number of element, e.x: Axis, Domain
114
115  axisDomainOrder_.resize(numElement_);
116  axisDomainOrder_ = axisDomainOrder;
117
118  // Because domain and axis can be in any order (axis1, domain1, axis2, axis3, )
119  // their position should be specified. In axisDomainOrder, domain == true, axis == false
120  int idx = 0;
121  indexMap_.resize(numElement_);
122  this->dims_ = numElement_;
123  for (int i = 0; i < numElement_; ++i)
124  {
125    indexMap_[i] = idx;
126    if (2 == axisDomainOrder(i))
127    {
128      ++(this->dims_);
129      idx += 2;
130    }
131    else ++idx;
132  }
133
134  // Size of each dimension (local and global)
135  nLocal_.resize(this->dims_);
136  nGlob_.resize(this->dims_);
137  nBeginLocal_.resize(this->dims_,0);
138  nBeginGlobal_.resize(this->dims_,0);
139
140  // Data_n_index of domain or axis (For now, axis uses its size as data_n_index
141  dataNIndex_.resize(numElement_);
142  dataDims_.resize(numElement_);
143  dataBegin_.resize(this->dims_);
144
145  // Data_*_index of each dimension
146  dataIndex_.resize(this->dims_);
147  infoIndex_.resize(this->dims_);
148
149  // A trick to determine position of each domain in domainList
150  int domIndex = 0, axisIndex = 0, scalarIndex = 0;
151  idx = 0;
152
153  elementLocalIndex_.resize(numElement_);
154  elementGlobalIndex_.resize(numElement_);
155  elementIndexData_.resize(numElement_);
156  elementNLocal_.resize(numElement_);
157  elementNGlobal_.resize(numElement_);
158  elementNLocal_[0] = 1;
159  elementNGlobal_[0] = 1;
160  size_t localSize = 1, globalSize = 1;
161
162  isDataDistributed_ = false;
163  // Update all the vectors above
164  for (idx = 0; idx < numElement_; ++idx)
165  {
166    int eleDim = axisDomainOrder(idx);
167    elementNLocal_[idx] = localSize;
168    elementNGlobal_[idx] = globalSize;
169
170    // If this is a domain
171    if (2 == eleDim)
172    {
173      // On the j axis
174      nLocal_.at(indexMap_[idx]+1) = domList[domIndex]->nj.getValue();
175      nGlob_.at(indexMap_[idx]+1)  = domList[domIndex]->nj_glo.getValue();
176      nBeginLocal_.at(indexMap_[idx]+1) = 0;
177      nBeginGlobal_.at(indexMap_[idx]+1) = domList[domIndex]->jbegin;
178
179      dataBegin_.at(indexMap_[idx]+1) = domList[domIndex]->data_jbegin.getValue();
180      dataIndex_.at(indexMap_[idx]+1).reference(domList[domIndex]->data_j_index);
181      infoIndex_.at(indexMap_[idx]+1).reference(domList[domIndex]->j_index);
182
183      // On the i axis
184      nLocal_.at(indexMap_[idx]) = domList[domIndex]->ni.getValue();
185      nGlob_.at(indexMap_[idx]) = domList[domIndex]->ni_glo.getValue();
186      nBeginLocal_.at(indexMap_[idx]) = 0;
187      nBeginGlobal_.at(indexMap_[idx]) = domList[domIndex]->ibegin;
188
189      dataBegin_.at(indexMap_[idx]) = domList[domIndex]->data_ibegin.getValue();
190      dataIndex_.at(indexMap_[idx]).reference(domList[domIndex]->data_i_index);
191      infoIndex_.at(indexMap_[idx]).reference(domList[domIndex]->i_index);
192
193      dataNIndex_.at(idx) = domList[domIndex]->data_i_index.numElements();
194      dataDims_.at(idx) = domList[domIndex]->data_dim.getValue();
195
196      isDataDistributed_ |= domList[domIndex]->isDistributed();
197
198      localSize *= nLocal_.at(indexMap_[idx]+1)* nLocal_.at(indexMap_[idx]);
199      globalSize *= nGlob_.at(indexMap_[idx]+1)* nGlob_.at(indexMap_[idx]);
200      ++domIndex;
201    }
202    else if (1 == eleDim)// So it's an axis
203    {
204      nLocal_.at(indexMap_[idx]) = axisList[axisIndex]->n.getValue();
205      nGlob_.at(indexMap_[idx]) = axisList[axisIndex]->n_glo.getValue();
206      nBeginLocal_.at(indexMap_[idx]) = 0;
207      nBeginGlobal_.at(indexMap_[idx]) = axisList[axisIndex]->begin.getValue();
208
209      dataBegin_.at(indexMap_[idx]) = axisList[axisIndex]->data_begin.getValue();
210      dataIndex_.at(indexMap_[idx]).reference(axisList[axisIndex]->data_index);
211      infoIndex_.at(indexMap_[idx]).reference(axisList[axisIndex]->index);
212      dataNIndex_.at(idx) = axisList[axisIndex]->data_index.numElements();
213      dataDims_.at(idx) = 1;
214
215      isDataDistributed_ |= axisList[axisIndex]->isDistributed();
216
217      localSize *= nLocal_.at(indexMap_[idx]);
218      globalSize *= nGlob_.at(indexMap_[idx]);
219
220      ++axisIndex;
221    }
222    else // scalar
223    {
224      nLocal_.at(indexMap_[idx]) = 1;
225      nGlob_.at(indexMap_[idx]) = 1;
226      nBeginLocal_.at(indexMap_[idx]) = 0;
227      nBeginGlobal_.at(indexMap_[idx]) = 1;
228
229      dataBegin_.at(indexMap_[idx]) = 0;
230      dataIndex_.at(indexMap_[idx]).resize(1); dataIndex_.at(indexMap_[idx])(0) = 0;
231      infoIndex_.at(indexMap_[idx]).resize(1); infoIndex_.at(indexMap_[idx])(0) = 0;
232      dataNIndex_.at(idx) = 1;
233      dataDims_.at(idx) = 1;
234
235      isDataDistributed_ |= false;
236
237      localSize *= nLocal_.at(indexMap_[idx]);
238      globalSize *= nGlob_.at(indexMap_[idx]);
239
240      ++scalarIndex;
241    }
242  }
243}
244
245/*!
246  Create local index of domain(s).
247  A domain can have data index which even contains the "ghost" points. Very often, these
248data surround the true data. In order to send correct data to server,
249a client need to know index of the true data.
250*/
251void CDistributionClient::createLocalDomainDataIndex()
252{
253  int idxDomain = 0;
254  for (int i = 0; i < axisDomainOrder_.numElements(); ++i)
255  {
256    if (2 == axisDomainOrder_(i))
257    {
258      elementIndexData_[i].resize(dataNIndex_[i]);
259      elementIndexData_[i] = false;
260      int iIdx, jIdx = 0, count = 0, localIndex;
261      for (int j = 0; j < dataNIndex_[i]; ++j)
262      {
263        iIdx = getDomainIndex((dataIndex_[indexMap_[i]])(j), (dataIndex_[indexMap_[i]+1])(j),
264                              dataBegin_[indexMap_[i]], dataBegin_[indexMap_[i]+1],
265                              dataDims_[i], nLocal_[indexMap_[i]], jIdx);
266
267        if ((iIdx >= nBeginLocal_[indexMap_[i]]) && (iIdx < nLocal_[indexMap_[i]]) &&
268           (jIdx >= nBeginLocal_[indexMap_[i]+1]) && (jIdx < nLocal_[indexMap_[i]+1]))
269        {
270          ++count;
271          elementIndexData_[i](j) = true;
272        }
273      }
274
275      elementLocalIndex_[i].resize(count);
276      elementGlobalIndex_[i].resize(count);
277      count = 0;
278      CArray<bool,1>& tmpIndexElementData = elementIndexData_[i];
279      CArray<int,1>& tmpLocalElementIndex = elementLocalIndex_[i];
280      CArray<size_t,1>& tmpGlobalElementIndex = elementGlobalIndex_[i];
281      for (int j = 0; j < dataNIndex_[i]; ++j)
282      {
283        if (tmpIndexElementData(j))
284        {
285          iIdx = getDomainIndex((dataIndex_[indexMap_[i]])(j), (dataIndex_[indexMap_[i]+1])(j),
286                                dataBegin_[indexMap_[i]], dataBegin_[indexMap_[i]+1],
287                                dataDims_[i], nLocal_[indexMap_[i]], jIdx);
288          localIndex = tmpLocalElementIndex(count) = iIdx + jIdx * nLocal_[indexMap_[i]];
289          tmpGlobalElementIndex(count) = (infoIndex_[indexMap_[i]])(localIndex) + ((infoIndex_[indexMap_[i]+1])(localIndex))*nGlob_[indexMap_[i]];
290          ++count;
291        }
292      }
293      ++idxDomain;
294    }
295  }
296}
297
298/*!
299  Create local index of axis.
300*/
301void CDistributionClient::createLocalAxisDataIndex()
302{
303  int idxAxis = 0;
304  for (int i = 0; i < axisDomainOrder_.numElements(); ++i)
305  {
306    if (1 == axisDomainOrder_(i))
307    {
308      elementIndexData_[i].resize(dataNIndex_[i]);
309      elementIndexData_[i] = false;
310      int iIdx = 0, count = 0;
311      for (int j = 0; j < dataNIndex_[i]; ++j)
312      {
313        iIdx = getAxisIndex((dataIndex_[indexMap_[i]])(j), dataBegin_[indexMap_[i]], nLocal_[indexMap_[i]]);
314        if ((iIdx >= nBeginLocal_[indexMap_[i]]) &&
315           (iIdx < nLocal_[indexMap_[i]]) )//&& (axisMasks_[idxAxis](iIdx)))
316        {
317          ++count;
318          elementIndexData_[i](j) = true;
319        }
320      }
321
322      elementLocalIndex_[i].resize(count);
323      elementGlobalIndex_[i].resize(count);
324      count = 0;
325      CArray<bool,1>& tmpIndexElementData = elementIndexData_[i];
326      CArray<int,1>& tmpLocalElementIndex = elementLocalIndex_[i];
327      CArray<size_t,1>& tmpGlobalElementIndex = elementGlobalIndex_[i];
328      for (int j = 0; j < dataNIndex_[i]; ++j)
329      {
330        if (tmpIndexElementData(j))
331        {
332          iIdx = tmpLocalElementIndex(count) = getAxisIndex((dataIndex_[indexMap_[i]])(j), dataBegin_[indexMap_[i]], nLocal_[indexMap_[i]]);
333          tmpGlobalElementIndex(count) = (infoIndex_[indexMap_[i]])(iIdx);
334          ++count;
335        }
336      }
337      ++idxAxis;
338    }
339  }
340}
341
342/*!
343  Create local index of scalar.
344*/
345void CDistributionClient::createLocalScalarDataIndex()
346{
347  int idxAxis = 0;
348  for (int i = 0; i < axisDomainOrder_.numElements(); ++i)
349  {
350    if (0 == axisDomainOrder_(i))
351    {
352      elementIndexData_[i].resize(dataNIndex_[i]);
353      elementIndexData_[i] = true;
354      int count = 1;
355
356      elementLocalIndex_[i].resize(count);
357      elementLocalIndex_[i] = 0;
358      elementGlobalIndex_[i].resize(count);
359      elementGlobalIndex_[i] = 0;
360    }
361  }
362}
363
364/*!
365   Create global index on client
366   In order to do the mapping between client-server, each client creates its own
367global index of sending data. This global index is then used to calculate to which server
368the client needs to send it data as well as which part of data belongs to the server.
369So as to make clients and server coherent in order of index, global index is calculated by
370take into account of C-convention, the rightmost dimension varies faster.
371*/
372void CDistributionClient::createGlobalIndexSendToServer()
373{
374  if (isComputed_) return;
375  isComputed_ = true;
376  createLocalDomainDataIndex();
377  createLocalAxisDataIndex();
378  createLocalScalarDataIndex();
379
380  int idxDomain = 0, idxAxis = 0;
381  std::vector<int> eachElementSize(numElement_);
382
383  // Precompute size of the loop
384  for (int i = 0; i < numElement_; ++i)
385  {
386    eachElementSize[i] = elementLocalIndex_[i].numElements();
387  }
388
389  //   Compute size of the global index on client
390  std::vector<StdSize> idxLoop(numElement_,0);
391  std::vector<StdSize> currentIndex(numElement_,0);
392  std::vector<StdSize> currentGlobalIndex(numElement_,0);
393  int innerLoopSize = eachElementSize[0];
394  size_t idx = 0, indexLocalDataOnClientCount = 0;
395  size_t ssize = 1;
396
397  for (int i = 0; i < numElement_; ++i) ssize *= eachElementSize[i];
398
399  localDataIndex_.resize(ssize);
400  if (!gridMask_.isEmpty()) localMaskIndex_.resize(ssize);
401  localMaskedDataIndex_.resize(ssize);
402  globalDataIndex_.rehash(std::ceil(ssize/globalDataIndex_.max_load_factor()));
403  globalLocalDataSendToServerMap_.rehash(std::ceil(ssize/globalLocalDataSendToServerMap_.max_load_factor()));
404
405
406  // We need to loop with data index
407  idxLoop.assign(numElement_,0);
408  idx = indexLocalDataOnClientCount = 0;
409  ssize = 1; for (int i = 0; i < numElement_; ++i) ssize *= dataNIndex_[i];
410  innerLoopSize = dataNIndex_[0];
411  int countLocalData = 0;
412  std::vector<int> correctIndexOfElement(numElement_,0);
413  bool isOuterIndexCorrect = true;
414  while (idx < ssize)
415  {
416    for (int i = 0; i < numElement_-1; ++i)
417    {
418      if (idxLoop[i] == dataNIndex_[i])
419      {
420        idxLoop[i] = 0;
421        correctIndexOfElement[i] = 0;
422        ++idxLoop[i+1];
423        if (isOuterIndexCorrect) ++correctIndexOfElement[i+1];
424      }
425    }
426
427    // Depending the inner-most element axis or domain,
428    // The outer loop index begins correspondingly at one (1) or zero (0)
429    bool isIndexElementDataCorrect = true;
430    for (int i = 1; i < numElement_; ++i)
431    {
432      if (elementIndexData_[i](idxLoop[i]))
433      {
434        currentIndex[i] = elementLocalIndex_[i](correctIndexOfElement[i]);
435        currentGlobalIndex[i] = elementGlobalIndex_[i](correctIndexOfElement[i]);
436        isIndexElementDataCorrect &= true;
437      }
438      else isIndexElementDataCorrect = false;
439    }
440
441    isOuterIndexCorrect = isIndexElementDataCorrect;
442
443    if (isOuterIndexCorrect)
444    {
445      // Inner most index
446      int correctIndexInnerElement = 0;
447      for (int i = 0; i < innerLoopSize; ++i)
448      {
449        bool isCurrentIndexDataCorrect = isOuterIndexCorrect;
450        if (elementIndexData_[0](i))
451        {
452          currentIndex[0] = elementLocalIndex_[0](correctIndexInnerElement);
453          currentGlobalIndex[0] = elementGlobalIndex_[0](correctIndexInnerElement);
454          isCurrentIndexDataCorrect &= true;
455          ++correctIndexInnerElement;
456        }
457        else isCurrentIndexDataCorrect = false;
458
459        if (isCurrentIndexDataCorrect)
460        {
461          bool maskTmp = true;
462          bool maskGridTmp = true;
463          size_t globalIndex = 0;
464          for (int k = 0; k < numElement_; ++k)
465          {
466            globalIndex += (currentGlobalIndex[k])*elementNGlobal_[k];
467          }
468          globalDataIndex_[globalIndex] = indexLocalDataOnClientCount;
469          localDataIndex_[indexLocalDataOnClientCount] = countLocalData;
470          globalLocalDataSendToServerMap_[globalIndex] = indexLocalDataOnClientCount;
471          localMaskedDataIndex_[indexLocalDataOnClientCount] = indexLocalDataOnClientCount;
472
473          // Grid mask: unmasked values will be replaces by NaN and then all values will be sent
474          if (!gridMask_.isEmpty())
475          {
476            int gridMaskIndex = 0;
477            for (int k = 0; k < this->numElement_; ++k)
478            {
479              gridMaskIndex += (currentIndex[k])*elementNLocal_[k];
480            }
481            maskGridTmp =  gridMask_(gridMaskIndex);
482            if (maskGridTmp)
483              localMaskIndex_[indexLocalDataOnClientCount] = true;
484            else
485              localMaskIndex_[indexLocalDataOnClientCount] = false;
486          }
487
488          ++indexLocalDataOnClientCount;
489
490        }
491        ++countLocalData;
492        correctIndexOfElement[0] = correctIndexInnerElement;;
493      }
494    }
495    idxLoop[0] += innerLoopSize;
496    idx += innerLoopSize;
497  }
498}
499
500void CDistributionClient::createGlobalIndex()
501{
502}
503
504/*!
505  Retrieve index i and index j of a domain from its data index
506  Data contains not only true data, which are sent to servers, but also ghost data, which
507very often play a role of border of each local data, so does data index. Because data of a domain
508can be one dimension, or two dimensions, there is a need to convert data index to domain index
509  \param [in] dataIIndex index of i data
510  \param [in] dataJIndex index of j data
511  \param [in] dataIBegin index begin of i data
512  \param [in] dataJBegin index begin of j data
513  \param [in] dataDim dimension of data (1 or 2)
514  \param [in] ni local size ni of domain
515  \param [out] j j index of domain
516  \return i index of domain
517*/
518int CDistributionClient::getDomainIndex(const int& dataIIndex, const int& dataJIndex,
519                                        const int& dataIBegin, const int& dataJBegin,
520                                        const int& dataDim, const int& ni, int& j)
521{
522  int i;
523  int tempI = dataIIndex + dataIBegin,
524      tempJ = (dataJIndex + dataJBegin);
525  if (ni == 0)
526  {
527    i = -1;
528    j = -1;
529    return i;
530  }
531  if ((tempI < 0) || (tempJ < 0))
532  {
533    i = -1;
534    j = -1;
535    return i;
536  }
537  else
538  {
539    i = (dataDim == 1) ? (tempI) % ni : (tempI) ;
540    j = (dataDim == 1) ? (tempI) / ni : (tempJ) ;
541  }
542  return i;
543}
544
545/*!
546  Retrieve index of an axis from its data index
547  \param [in] dataIndex index of data
548  \param [in] dataBegin index begin of data
549  \param [in] ni local size of axis
550  \return index of domain
551*/
552int CDistributionClient::getAxisIndex(const int& dataIndex, const int& dataBegin, const int& ni)
553{
554  if (ni == 0)
555  {
556    return -1;
557  }
558  int tempI = dataIndex;
559  if ((tempI < 0) || (tempI > ni))
560    return -1;
561  else
562    return tempI;
563}
564
565/*!
566  Return global local data mapping of client
567*/
568CDistributionClient::GlobalLocalDataMap& CDistributionClient::getGlobalLocalDataSendToServer()
569{
570  if (!isComputed_) createGlobalIndexSendToServer();
571  return globalLocalDataSendToServerMap_;
572}
573
574CDistributionClient::GlobalLocalDataMap& CDistributionClient::getGlobalDataIndexOnClient()
575{
576  if (!isComputed_) createGlobalIndexSendToServer();
577  return globalDataIndex_;
578}
579
580/*!
581  Return local data index of client
582*/
583const std::vector<int>& CDistributionClient::getLocalDataIndexOnClient()
584{
585  if (!isComputed_) createGlobalIndexSendToServer();
586  return localDataIndex_;
587}
588
589/*!
590  Return local mask index of client
591*/
592const std::vector<bool>& CDistributionClient::getLocalMaskIndexOnClient()
593{
594  if (!isComputed_) createGlobalIndexSendToServer();
595  return localMaskIndex_;
596}
597
598/*!
599  Return local mask index of client
600*/
601const std::vector<int>& CDistributionClient::getLocalMaskedDataIndexOnClient()
602{
603  if (!isComputed_) createGlobalIndexSendToServer();
604  return localMaskedDataIndex_;
605}
606
607} // namespace xios
608
Note: See TracBrowser for help on using the repository browser.