Changeset 144


Ignore:
Timestamp:
12/25/04 00:27:23 (19 years ago)
Author:
kil064
Message:

merge functions 'average' and 'averages' into one that averages
in time, can do scan averages and apply various weighting schemes.
Break some functionality into other functrions

Location:
trunk/src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/SDMath.cc

    r139 r144  
    6464//using namespace asap::SDMath;
    6565
    66 CountedPtr<SDMemTable> SDMath::average(const CountedPtr<SDMemTable>& in)
    67 //
    68 // Average all rows in Table in time
    69 //
    70 {
    71   Table t = in->table();
    72   ROArrayColumn<Float> tsys(t, "TSYS");
    73   ROScalarColumn<Double> mjd(t, "TIME");
    74   ROScalarColumn<String> srcn(t, "SRCNAME");
    75   ROScalarColumn<Double> integr(t, "INTERVAL");
    76   ROArrayColumn<uInt> freqidc(t, "FREQID");
    77   IPosition ip = in->rowAsMaskedArray(0).shape();
    78   Array<Float> outarr(ip); outarr =0.0;
    79   Array<Float> narr(ip);narr = 0.0;
    80   Array<Float> narrinc(ip);narrinc = 1.0;
    81 
    82   Array<Float> tsarr(tsys.shape(0));
    83   Array<Float> outtsarr(tsys.shape(0));
    84   outtsarr =0.0;
    85   tsys.get(0, tsarr);// this is probably unneccessary as tsys should
    86   Double tme = 0.0;
    87   Double inttime = 0.0;
    88 
    89 // Loop over rows
    90 
    91   for (uInt i=0; i < t.nrow(); i++) {
    92 
    93 // Get data and accumulate sums
    94 
    95     MaskedArray<Float> marr(in->rowAsMaskedArray(i));
    96     outarr += marr;
    97     MaskedArray<Float> n(narrinc,marr.getMask());
    98     narr += n;
    99 
    100 // Accumulkate Tsys
    101 
    102     tsys.get(i, tsarr);// this is probably unneccessary as tsys should
    103     outtsarr += tsarr; // be constant
    104     Double tmp;
    105     mjd.get(i,tmp);
    106     tme += tmp;// average time
    107     integr.get(i,tmp);
    108     inttime += tmp;
    109   }
    110 
    111 // Average
    112 
    113   MaskedArray<Float> nma(narr,(narr > Float(0)));
    114   outarr /= nma;
    115 
    116 // Create container and put
    117 
    118   Array<Bool> outflagsb = !(nma.getMask());
    119   Array<uChar> outflags(outflagsb.shape());
    120   convertArray(outflags,outflagsb);
    121   SDContainer sc = in->getSDContainer();
    122   Int n = t.nrow();
    123   outtsarr /= Float(n);
    124   sc.timestamp = tme/Double(n);
    125   sc.interval = inttime;
    126   String tstr; srcn.getScalar(0,tstr);// get sourcename of "mid" point
    127   sc.sourcename = tstr;
    128   Vector<uInt> tvec;
    129   freqidc.get(0,tvec);
    130   sc.putFreqMap(tvec);
    131   sc.putTsys(outtsarr);
    132   sc.scanid = 0;
    133   sc.putSpectrum(outarr);
    134   sc.putFlags(outflags);
    135   SDMemTable* sdmt = new SDMemTable(*in,True);
    136   sdmt->putSDContainer(sc);
    137   return CountedPtr<SDMemTable>(sdmt);
    138 }
     66CountedPtr<SDMemTable> SDMath::average (const Block<CountedPtr<SDMemTable> >& in,
     67                                        const Vector<Bool>& mask, bool scanAv,
     68                                        const std::string& weightStr)
     69//
     70// Weighted averaging of spectra from one or more Tables.
     71//
     72{
     73  weightType wtType = NONE;
     74  String tStr(weightStr);
     75  tStr.upcase();
     76  if (tStr.contains(String("NONE"))) {
     77     wtType = NONE;
     78  } else if (tStr.contains(String("VAR"))) {
     79     wtType = VAR;
     80  } else if (tStr.contains(String("TSYS"))) {
     81     wtType = TSYS;
     82     throw (AipsError("T_sys weighting not yet implemented"));
     83  } else {
     84    throw (AipsError("Unrecognized weighting type"));
     85  }
     86
     87// Create output Table by cloning from the first table
     88
     89  SDMemTable* pTabOut = new SDMemTable(*in[0],True);
     90
     91// Setup
     92
     93  const uInt axis = 3;                                     // Spectral axis
     94  IPosition shp = in[0]->rowAsMaskedArray(0).shape();      // Must not change
     95  Array<Float> arr(shp);
     96  Array<Bool> barr(shp);
     97  const Bool useMask = (mask.nelements() == shp(axis));
     98
     99// Columns from Tables
     100
     101  ROArrayColumn<Float> tSysCol;
     102  ROScalarColumn<Double> mjdCol;
     103  ROScalarColumn<String> srcNameCol;
     104  ROScalarColumn<Double> intCol;
     105  ROArrayColumn<uInt> fqIDCol;
     106
     107// Create accumulation MaskedArray. We accumulate for each channel,if,pol,beam
     108// Note that the mask of the accumulation array will ALWAYS remain ALL True.
     109// The MA is only used so that when data which is masked Bad is added to it,
     110// that data does not contribute.
     111
     112  Array<Float> zero(shp);
     113  zero=0.0;
     114  Array<Bool> good(shp);
     115  good = True;
     116  MaskedArray<Float> sum(zero,good);
     117
     118// Counter arrays
     119
     120  Array<Float> nPts(shp);             // Number of points
     121  nPts = 0.0;
     122  Array<Float> nInc(shp);             // Increment
     123  nInc = 1.0;
     124
     125// Create accumulation Array for variance. We accumulate for
     126// each if,pol,beam, but average over channel.  So we need
     127// a shape with one less axis dropping channels.
     128
     129  const uInt nAxesSub = shp.nelements() - 1;
     130  IPosition shp2(nAxesSub);
     131  for (uInt i=0,j=0; i<(nAxesSub+1); i++) {
     132     if (i!=axis) {
     133       shp2(j) = shp(i);
     134       j++;
     135     }
     136  }
     137  Array<Float> sumSq(shp2);
     138  sumSq = 0.0;
     139  IPosition pos2(nAxesSub,0);                        // For indexing
     140
     141// Time-related accumulators
     142
     143  Double time;
     144  Double timeSum = 0.0;
     145  Double intSum = 0.0;
     146  Double interval = 0.0;
     147
     148// To get the right shape for the Tsys accumulator we need to
     149// access a column from the first table.  The shape of this
     150// array must not change
     151
     152  Array<Float> tSysSum;
     153  {
     154    const Table& tabIn = in[0]->table();
     155    tSysCol.attach(tabIn,"TSYS");
     156    tSysSum.resize(tSysCol.shape(0));
     157  }
     158  tSysSum =0.0;
     159  Array<Float> tSys;
     160
     161// Scan and row tracking
     162
     163  Int oldScanID = 0;
     164  Int outScanID = 0;
     165  Int scanID = 0;
     166  Int rowStart = 0;
     167  Int nAccum = 0;
     168  Int tableStart = 0;
     169
     170// Source and FreqID
     171
     172  String sourceName, oldSourceName, sourceNameStart;
     173  Vector<uInt> freqID, freqIDStart, oldFreqID;
     174
     175// Loop over tables
     176
     177  Float fac = 1.0;
     178  const uInt nTables = in.nelements();
     179  for (uInt iTab=0; iTab<nTables; iTab++) {
     180
     181// Attach columns to Table
     182
     183     const Table& tabIn = in[iTab]->table();
     184     tSysCol.attach(tabIn, "TSYS");
     185     mjdCol.attach(tabIn, "TIME");
     186     srcNameCol.attach(tabIn, "SRCNAME");
     187     intCol.attach(tabIn, "INTERVAL");
     188     fqIDCol.attach(tabIn, "FREQID");
     189
     190// Loop over rows in Table
     191
     192     const uInt nRows = in[iTab]->nRow();
     193     for (uInt iRow=0; iRow<nRows; iRow++) {
     194
     195// Check conformance
     196
     197        IPosition shp2 = in[iTab]->rowAsMaskedArray(iRow).shape();
     198        if (!shp.isEqual(shp2)) {
     199           throw (AipsError("Shapes for all rows must be the same"));
     200        }
     201
     202// If we are not doing scan averages, make checks for source and
     203// frequency setup and warn if averaging across them
     204
     205// Get copy of Scan Container for this row
     206
     207        SDContainer sc = in[iTab]->getSDContainer(iRow);
     208        scanID = sc.scanid;
     209
     210// Get quantities from columns
     211
     212        srcNameCol.getScalar(iRow, sourceName);
     213        mjdCol.get(iRow, time);
     214        tSysCol.get(iRow, tSys);
     215        intCol.get(iRow, interval);
     216        fqIDCol.get(iRow, freqID);
     217
     218// Initialize first source and freqID
     219
     220        if (iRow==0 && iTab==0) {
     221          sourceNameStart = sourceName;
     222          freqIDStart = freqID;
     223        }
     224
     225// If we are doing scan averages, see if we are at the end of an
     226// accumulation period (scan).  We must check soutce names too,
     227// since we might have two tables with one scan each but different
     228// source names; we shouldn't average different sources together
     229
     230        if (scanAv && ( (scanID != oldScanID)  ||
     231                        (iRow==0 && iTab>0 && sourceName!=oldSourceName))) {
     232
     233// Normalize data in 'sum' accumulation array according to weighting scheme
     234
     235           normalize (sum, sumSq, nPts, wtType, axis, nAxesSub);
     236
     237// Fill scan container. The source and freqID come from the
     238// first row of the first table that went into this average (
     239// should be the same for all rows in the scan average)
     240
     241           Float nR(nAccum);
     242           fillSDC (sc, sum.getMask(), sum.getArray(), tSysSum/nR, outScanID,
     243                    timeSum/nR, intSum, sourceNameStart, freqIDStart);
     244
     245// Write container out to Table
     246
     247           pTabOut->putSDContainer(sc);
     248
     249// Reset accumulators
     250
     251           sum = 0.0;
     252           sumSq = 0.0;
     253           nAccum = 0;
     254//
     255           tSysSum =0.0;
     256           timeSum = 0.0;
     257           intSum = 0.0;
     258
     259// Increment
     260
     261           rowStart = iRow;              // First row for next accumulation
     262           tableStart = iTab;            // First table for next accumulation
     263           sourceNameStart = sourceName; // First source name for next accumulation
     264           freqIDStart = freqID;         // First FreqID for next accumulation
     265//
     266           oldScanID = scanID;
     267           outScanID += 1;               // Scan ID for next accumulation period
     268        }
     269
     270// Accumulation step. First get data and deconstruct
     271
     272        MaskedArray<Float> dataIn(in[iTab]->rowAsMaskedArray(iRow));
     273        Array<Float>& valuesIn = dataIn.getRWArray();           // writable reference
     274        const Array<Bool>& maskIn = dataIn.getMask();          // RO reference
     275//
     276        if (wtType==NONE) {
     277           const MaskedArray<Float> n(nInc,dataIn.getMask());
     278           nPts += n;                               // Only accumulates where mask==T
     279        } else if (wtType==VAR) {
     280
     281// We are going to average the data, weighted by the noise for each pol, beam and IF.
     282// So therefore we need to iterate through by spectrum (axis 3)
     283
     284           VectorIterator<Float> itData(valuesIn, axis);
     285           ReadOnlyVectorIterator<Bool> itMask(maskIn, axis);
     286           while (!itData.pastEnd()) {
     287
     288// Make MaskedArray of Vector, optionally apply OTF mask, and find scaling factor
     289
     290             if (useMask) {
     291                MaskedArray<Float> tmp(itData.vector(),mask&&itMask.vector());
     292                fac = 1.0/variance(tmp);
     293             } else {
     294                MaskedArray<Float> tmp(itData.vector(),itMask.vector());
     295                fac = 1.0/variance(tmp);
     296             }
     297
     298// Scale data
     299
     300             itData.vector() *= fac;     // Writes back into 'dataIn'
     301//
     302// Accumulate variance per if/pol/beam averaged over spectrum
     303// This method to get pos2 from itData.pos() is only valid
     304// because the spectral axis is the last one (so we can just
     305// copy the first nAXesSub positions out)
     306
     307             pos2 = itData.pos().getFirst(nAxesSub);
     308             sumSq(pos2) += fac;
     309//
     310             itData.next();
     311             itMask.next();
     312           }
     313        } else if (wtType==TSYS) {
     314        }
     315
     316// Accumulate sum of (possibly scaled) data
     317
     318       sum += dataIn;
     319
     320// Accumulate Tsys, time, and interval
     321
     322       tSysSum += tSys;
     323       timeSum += time;
     324       intSum += interval;
     325
     326// Number of rows in accumulation
     327
     328       nAccum += 1;
     329       oldSourceName = sourceName;
     330       oldFreqID = freqID;
     331    }
     332  }
     333
     334// OK at this point we have accumulation data which is either
     335//   - accumulated from all tables into one row
     336// or
     337//   - accumulated from the last scan average
     338//
     339// Normalize data in 'sum' accumulation array according to weighting scheme
     340
     341  normalize (sum, sumSq, nPts, wtType, axis, nAxesSub);
     342
     343// Create and fill container.  The container we clone will be from
     344// the last Table and the first row that went into the current
     345// accumulation.  It probably doesn't matter that much really...
     346
     347  Float nR(nAccum);
     348  SDContainer sc = in[tableStart]->getSDContainer(rowStart);
     349  fillSDC (sc, sum.getMask(), sum.getArray(), tSysSum/nR, outScanID,
     350           timeSum/nR, intSum, sourceNameStart, freqIDStart);
     351//
     352  pTabOut->putSDContainer(sc);
     353/*
     354   cout << endl;
     355   cout << "Last accumulation for output scan ID " << outScanID << endl;
     356   cout << "   The first row in this accumulation is " << rowStart << endl;
     357   cout << "   The number of rows accumulated is " << nAccum << endl;
     358   cout << "   The first table in this accumulation is " << tableStart << endl;
     359   cout << "   The first source in this accumulation is " << sourceNameStart << endl;
     360   cout << "   The first freqID in this accumulation is " << freqIDStart << endl;
     361   cout  << "   Average time stamp = " << timeSum/nR << endl;
     362   cout << "   Integrated time = " << intSum << endl;
     363*/
     364  return CountedPtr<SDMemTable>(pTabOut);
     365}
     366
     367
    139368
    140369CountedPtr<SDMemTable>
     
    304533
    305534
    306 CountedPtr<SDMemTable> SDMath::averages(const Block<CountedPtr<SDMemTable> >& in,
    307                                         const Vector<Bool>& mask)
    308 //
    309 // Noise weighted averaging of spectra from many Tables.  Tables can have different
    310 // number of rows. 
    311 //
    312 {
    313 
    314 // Setup
    315 
    316   const uInt axis = 3;                                 // Spectral axis
    317   IPosition shp = in[0]->rowAsMaskedArray(0).shape();
    318   Array<Float> arr(shp);
    319   Array<Bool> barr(shp);
    320   Double sumInterval = 0.0;
    321   const Bool useMask = (mask.nelements() == shp(axis));
    322 
    323 // Create data accumulation MaskedArray. We accumulate for each
    324 // channel,if,pol,beam
    325 
    326   Array<Float> zero(shp); zero=0.0;
    327   Array<Bool> good(shp); good = True;
    328   MaskedArray<Float> sum(zero,good);
    329 
    330 // Create accumulation Array for variance. We accumulate for
    331 // each if,pol,beam, but average over channel
    332 
    333   const uInt nAxesSub = shp.nelements() - 1;
    334   IPosition shp2(nAxesSub);
    335   for (uInt i=0,j=0; i<(nAxesSub+1); i++) {
    336      if (i!=axis) {
    337        shp2(j) = shp(i);
    338        j++;
    339      }
    340   }
    341   Array<Float> sumSq(shp2);
    342   sumSq = 0.0;
    343   IPosition pos2(nAxesSub,0);                        // FOr indexing
    344 // 
    345   Float fac = 1.0;
    346   const uInt nTables = in.nelements();
    347   for (uInt iTab=0; iTab<nTables; iTab++) {
    348      const uInt nRows = in[iTab]->nRow();
    349      sumInterval += nRows * in[iTab]->getInterval();   // Sum of time intervals
    350 //
    351      for (uInt iRow=0; iRow<nRows; iRow++) {
    352 
    353 // Check conforms
    354 
    355         IPosition shp2 = in[iTab]->rowAsMaskedArray(iRow).shape();
    356         if (!shp.isEqual(shp2)) {
    357            throw (AipsError("Shapes for all rows must be the same"));
    358         }
    359 
    360 // Get data and deconstruct
    361  
    362         MaskedArray<Float> marr(in[iTab]->rowAsMaskedArray(iRow));
    363         Array<Float>& arr = marr.getRWArray();                     // writable reference
    364         const Array<Bool>& barr = marr.getMask();                  // RO reference
    365 
    366 // We are going to average the data, weighted by the noise for each
    367 // pol, beam and IF. So therefore we need to iterate through by
    368 // spectra (axis 3)
    369 
    370         VectorIterator<Float> itData(arr, axis);
    371         ReadOnlyVectorIterator<Bool> itMask(barr, axis);
    372         while (!itData.pastEnd()) {
    373 
    374 // Make MaskedArray of Vector, optionally apply OTF mask, and find scaling factor
    375 
    376           if (useMask) {
    377              MaskedArray<Float> tmp(itData.vector(),mask&&itMask.vector());
    378              fac = 1.0/variance(tmp);
    379           } else {
    380              MaskedArray<Float> tmp(itData.vector(),itMask.vector());
    381              fac = 1.0/variance(tmp);
    382           }
    383 
    384 // Scale data
    385 
    386           itData.vector() *= fac;
    387 
    388 // Accumulate variance per if/pol/beam averaged over spectrum
    389 // This method to get pos2 from itData.pos() is only valid
    390 // because the spectral axis is the last one (so we can just
    391 // copy the first nAXesSub positions out)
    392 
    393           pos2 = itData.pos().getFirst(nAxesSub);
    394           sumSq(pos2) += fac;
    395 //
    396           itData.next();
    397           itMask.next();
    398         }   
    399 
    400 // Accumulate sums
    401 
    402        sum += marr;
    403     }
    404   }
    405 
    406 // Normalize by the sum of the 1/var. 
    407 
    408   Array<Float>& data = sum.getRWArray();   
    409   VectorIterator<Float> itData(data, axis);
    410   while (!itData.pastEnd()) {
    411      pos2 = itData.pos().getFirst(nAxesSub);           // See comments above
    412      itData.vector() /= sumSq(pos2);
    413      itData.next();
    414   }   
    415 
    416 // Create and fill output
    417 
    418   Array<uChar> outflags(shp);
    419   convertArray(outflags,!(sum.getMask()));
    420 //
    421   SDContainer sc = in[0]->getSDContainer();     // CLone from first container of first Table
    422   sc.putSpectrum(data);
    423   sc.putFlags(outflags);
    424   sc.interval = sumInterval;
    425 //
    426   SDMemTable* sdmt = new SDMemTable(*in[0],True);  // CLone from first Table
    427   sdmt->putSDContainer(sc);
    428   return CountedPtr<SDMemTable>(sdmt);
    429 }
    430 
    431535
    432536CountedPtr<SDMemTable>
     
    655759// Get statistic
    656760
    657      result[ii] = SDMath::theStatistic(which, tmp);
     761     result[ii] = mathutil::statistics(which, tmp);
    658762  }
    659763//
     
    661765}
    662766
    663 
    664 float SDMath::theStatistic(const std::string& which,  const casa::MaskedArray<Float>& data)
    665 {
    666    String str(which);
    667    str.upcase();
    668    if (str.contains(String("MIN"))) {
    669       return min(data);
    670    } else if (str.contains(String("MAX"))) {
    671       return max(data);
    672    } else if (str.contains(String("SUMSQ"))) {
    673       return sumsquares(data);
    674    } else if (str.contains(String("SUM"))) {
    675       return sum(data);
    676    } else if (str.contains(String("MEAN"))) {
    677       return mean(data);
    678    } else if (str.contains(String("VAR"))) {
    679       return variance(data);
    680    } else if (str.contains(String("STDDEV"))) {
    681       return stddev(data);
    682    } else if (str.contains(String("AVDEV"))) {
    683       return avdev(data);
    684    } else if (str.contains(String("RMS"))) {
    685       uInt n = data.nelementsValid();
    686       return sqrt(sumsquares(data)/n);
    687    } else if (str.contains(String("MED"))) {
    688       return median(data);
     767void SDMath::fillSDC (SDContainer& sc,
     768                      const Array<Bool>& mask,
     769                      const Array<Float>& data,
     770                      const Array<Float>& tSys,
     771                      Int scanID, Double timeStamp,
     772                      Double interval, const String& sourceName,
     773                      const Vector<uInt>& freqID)
     774{
     775  sc.putSpectrum(data);
     776//
     777  Array<uChar> outflags(mask.shape());
     778  convertArray(outflags,!mask);
     779  sc.putFlags(outflags);
     780//
     781  sc.putTsys(tSys);
     782
     783// Time things
     784
     785  sc.timestamp = timeStamp;
     786  sc.interval = interval;
     787  sc.scanid = scanID;
     788//
     789  sc.sourcename = sourceName;
     790  sc.putFreqMap(freqID);
     791}
     792
     793void SDMath::normalize (MaskedArray<Float>& sum,
     794                        const Array<Float>& sumSq,
     795                        const Array<Float>& nPts,
     796                        weightType wtType, Int axis,
     797                        Int nAxesSub)
     798{
     799   IPosition pos2(nAxesSub,0);
     800//
     801   if (wtType==NONE) {
     802
     803// We just average by the number of points accumulated.
     804// We need to make a MA out of nPts so that no divide by
     805// zeros occur
     806
     807      MaskedArray<Float> t(nPts, (nPts>Float(0.0)));
     808      sum /= t;
     809   } else if (wtType==VAR) {
     810
     811// Normalize each spectrum by sum(1/var) where the variance
     812// is worked out for each spectrum
     813
     814      Array<Float>& data = sum.getRWArray();
     815      VectorIterator<Float> itData(data, axis);
     816      while (!itData.pastEnd()) {
     817         pos2 = itData.pos().getFirst(nAxesSub);
     818         itData.vector() /= sumSq(pos2);
     819         itData.next();
     820      }
     821   } else if (wtType==TSYS) {
    689822   }
    690823}
     824
  • trunk/src/SDMath.h

    r139 r144  
    4343namespace SDMath {
    4444  //public:
    45   casa::CountedPtr<SDMemTable> average(const casa::CountedPtr<SDMemTable>& in);
    4645  casa::CountedPtr<SDMemTable> quotient(const casa::CountedPtr<SDMemTable>& on,
    4746                                         const casa::CountedPtr<SDMemTable>& off);
     47
    4848  void multiplyInSitu(SDMemTable* in, casa::Float factor);
    4949
    5050  casa::CountedPtr<SDMemTable> multiply(const casa::CountedPtr<SDMemTable>& in,
    5151                                  casa::Float factor);
     52
    5253  casa::CountedPtr<SDMemTable> add(const casa::CountedPtr<SDMemTable>& in,
    5354                             casa::Float offset);
     
    5556  casa::CountedPtr<SDMemTable> hanning(const casa::CountedPtr<SDMemTable>& in);
    5657
    57   casa::CountedPtr<SDMemTable>
    58   averages(const casa::Block<casa::CountedPtr<SDMemTable> >& in,
    59            const casa::Vector<casa::Bool>& mask);
     58  casa::CountedPtr<SDMemTable>
     59  average (const casa::Block<casa::CountedPtr<SDMemTable> >& in,
     60           const casa::Vector<casa::Bool>& mask,
     61           bool scanAverage, const std::string& weightStr);
    6062
    6163  casa::CountedPtr<SDMemTable>
     
    7072// private (not actually...)
    7173
    72   float theStatistic(const std::string& which,  const casa::MaskedArray<casa::Float>& data);
    73  
     74  enum weightType {NONE,VAR,TSYS};
     75
     76  void fillSDC (SDContainer& sc, const casa::Array<casa::Bool>& mask,
     77                const casa::Array<casa::Float>& data,
     78                const casa::Array<casa::Float>& tSys,
     79                casa::Int scanID, casa::Double timeStamp,
     80                casa::Double interval, const casa::String& sourceName,
     81                const casa::Vector<casa::uInt>& freqID);
     82   void normalize (casa::MaskedArray<casa::Float>& data,
     83                   const casa::Array<casa::Float>& sumSq,
     84                   const casa::Array<casa::Float>& nPts,
     85                   weightType wtType, casa::Int axis, casa::Int nAxes);
     86
     87
    7488};
    7589
Note: See TracChangeset for help on using the changeset viewer.