// Find the minimum eigenvalue of second derivative matrix -- called from shouldSplit public void getEigenvaluefromMatrix(double[,] SecondDerivMatrix) { Eigenvector = new double[Program.ParameterVectorDimension]; if (!Program.CalculateEigenvaluesfromMatrix) { // Re-use Earlier Calculation of results for all clusters } // Calculate Eigenvalues from Matrix if (Program.ParameterVectorDimension != 2) { Exception e = DAVectorUtility.SALSAError(" Illegal Vector Dimension " + Program.ParameterVectorDimension.ToString()); throw(e); } // Case of Two Dimensions EigenStatus = 1; double tmp = SecondDerivMatrix[0, 0] - SecondDerivMatrix[1, 1]; tmp = tmp * tmp + 4.0 * SecondDerivMatrix[0, 1] * SecondDerivMatrix[0, 1]; Eigenvalue = 0.5 * (SecondDerivMatrix[0, 0] + SecondDerivMatrix[1, 1] - Math.Sqrt(tmp)); Eigenvector[0] = -SecondDerivMatrix[0, 1]; Eigenvector[1] = SecondDerivMatrix[1, 1] - Eigenvalue; // Normalize tmp = 0.0; for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { tmp += Eigenvector[VectorIndex] * Eigenvector[VectorIndex]; } tmp = 1.0 / Math.Sqrt(tmp); for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { Eigenvector[VectorIndex] *= tmp; } } // End getEigenvalue(double[,] SecondDerivMatrix)
} // End getEigenvalue(double[,] SecondDerivMatrix) public void SetAllEigenvaluesIteratively(ClusteringSolution Solution) { if (Solution.DistributedExecutionMode) { Exception e = DAVectorUtility.SALSAError(" Illegal Eigenvalue and Parallelization Combination "); throw (e); } if (Program.SigmaMethod > 0) { Exception e = DAVectorUtility.SALSAError(" Illegal Eigenvalue and Sigma Method Combination " + Program.SigmaMethod.ToString()); throw (e); } this.CurrentSolution = Solution; this.CenterEigenvector = this.CurrentSolution.Eigenvector_k_i; this.CenterEigenvalue = this.CurrentSolution.Eigenvalue_k; this.InitVector = new double[Program.ParameterVectorDimension]; this.FirstTerm = new double[this.CurrentSolution.Ncent_Global]; this.CenterEigenstatus = new int[this.CurrentSolution.Ncent_Global]; this.CenterEigenconvergence = new int[this.CurrentSolution.Ncent_Global]; Random random = new Random(); double InitNorm = 0.0; for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { InitVector[VectorIndex] = -0.5 + random.NextDouble(); InitNorm += InitVector[VectorIndex] * InitVector[VectorIndex]; } InitNorm = 1.0 / Math.Sqrt(InitNorm); for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { InitVector[VectorIndex] *= InitNorm; } // Initialization Loop over Clusters int somethingtodo = 0; for (int ClusterIndex = 0; ClusterIndex < this.CurrentSolution.Ncent_Global; ClusterIndex++) { this.CenterEigenconvergence[ClusterIndex] = 0; this.CenterEigenstatus[ClusterIndex] = 0; this.FirstTerm[ClusterIndex] = 0; if (this.CurrentSolution.Splittable_k_[ClusterIndex] != 1) { continue; } ++somethingtodo; for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { this.CenterEigenvector[ClusterIndex][VectorIndex] = InitVector[VectorIndex]; } } // End Loop over Clusters if (somethingtodo == 0) { return; } GlobalReductions.FindVectorDoubleSum FindClusterFirstTerm = new GlobalReductions.FindVectorDoubleSum(DAVectorUtility.ThreadCount, this.CurrentSolution.Ncent_Global); GlobalReductions.FindDoubleSum FindNumberScalarProducts = new GlobalReductions.FindDoubleSum(DAVectorUtility.ThreadCount); for (int NumPowerIterations = 0; NumPowerIterations < Program.PowerIterationLimit; NumPowerIterations++) { somethingtodo = 0; for (int ClusterIndex = 0; ClusterIndex < this.CurrentSolution.Ncent_Global; ClusterIndex++) { if (this.CurrentSolution.LocalStatus[ClusterIndex] != 1) { continue; } if (this.CurrentSolution.Splittable_k_[ClusterIndex] != 1) { continue; } if (this.CenterEigenconvergence[ClusterIndex] == 0) { ++somethingtodo; } } if (somethingtodo == 0) { break; } GlobalReductions.FindVectorDoubleSum3 FindNewPowerVectors = new GlobalReductions.FindVectorDoubleSum3(DAVectorUtility.ThreadCount, Program.ParameterVectorDimension, this.CurrentSolution.Ncent_Global); Parallel.For(0, Program.ParallelOptions.MaxDegreeOfParallelism, Program.ParallelOptions, (ThreadNo) => { FindNewPowerVectors.startthread(ThreadNo); double[] PartVector = new double[Program.ParameterVectorDimension]; int indexlen = DAVectorUtility.PointsperThread[ThreadNo]; int beginpoint = DAVectorUtility.StartPointperThread[ThreadNo] - DAVectorUtility.PointStart_Process; for (int alpha = beginpoint; alpha < indexlen + beginpoint; alpha++) { int IndirectSize = this.CurrentSolution.NumClusters_alpha_[alpha]; for (int IndirectClusterIndex = 0; IndirectClusterIndex < IndirectSize; IndirectClusterIndex++) { // Loop over Clusters for this point int RealClusterIndex = -1; int RemoteIndex = -1; int ActiveClusterIndex = -1; VectorAnnealIterate.ClusterPointersforaPoint(alpha, IndirectClusterIndex, ref RealClusterIndex, ref ActiveClusterIndex, ref RemoteIndex); if (this.CurrentSolution.Splittable_k_[RealClusterIndex] != 1) { continue; } double Mvalue = this.CurrentSolution.M_alpha_kpointer_[alpha][IndirectClusterIndex]; if (NumPowerIterations == 0) { FindClusterFirstTerm.addapoint(ThreadNo, Mvalue, RealClusterIndex); } double multiplier = 0.0; for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { PartVector[VectorIndex] = this.CurrentSolution.Y_k_i_[RealClusterIndex][VectorIndex] - Program.PointPosition[alpha][VectorIndex]; multiplier += PartVector[VectorIndex] * CenterEigenvector[RealClusterIndex][VectorIndex]; } FindNumberScalarProducts.addapoint(ThreadNo, 1.0); double wgt = Mvalue * multiplier; for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { PartVector[VectorIndex] *= wgt; } FindNewPowerVectors.addapoint(ThreadNo, PartVector, RealClusterIndex); } } // End Loop over points }); // End loop initialing Point dependent quantities FindNewPowerVectors.sumoverthreadsandmpi(); for (int ClusterIndex = 0; ClusterIndex < this.CurrentSolution.Ncent_Global; ClusterIndex++) { if (this.CurrentSolution.LocalStatus[ClusterIndex] != 1) { continue; } if ((this.CurrentSolution.Splittable_k_[ClusterIndex] != 1) || (this.CenterEigenconvergence[ClusterIndex] != 0)) { continue; } double[] sums = new double[3]; // Old.New Old.Old New.New for (int loop = 0; loop < 3; loop++) { sums[loop] = 0.0; } for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { int TotalIndex = VectorIndex + ClusterIndex * Program.ParameterVectorDimension; double newvalue = FindNewPowerVectors.TotalVectorSum[TotalIndex]; double oldvalue = CenterEigenvector[ClusterIndex][VectorIndex]; sums[0] += oldvalue * newvalue; sums[1] += oldvalue * oldvalue; sums[2] += newvalue * newvalue; CenterEigenvector[ClusterIndex][VectorIndex] = newvalue; } // Decide if finished and set eigenvalue double CandidateEigenvalue = sums[0] / sums[1]; bool LegalEigenvalue = (CandidateEigenvalue > 0.0); DAVectorUtility.SynchronizeMPIvariable(ref LegalEigenvalue); // Check if converged // Do this in one process ONLY if ((NumPowerIterations > 5) && LegalEigenvalue) { // Arbitrary choice for Number of Power Iterations Cut int EigenvalueDone = 0; if (DAVectorUtility.MPI_Rank == 0) { // Decisions can only be made in one process if (Math.Abs(CandidateEigenvalue - this.CenterEigenvalue[ClusterIndex]) > CandidateEigenvalue * Program.eigenvaluechange) { ++EigenvalueDone; } double delta = sums[2] - 2.0 * sums[0] * CandidateEigenvalue + sums[1] * CandidateEigenvalue * CandidateEigenvalue; // (Ax- Eigenvalue*Axold)**2 if (Math.Abs(delta) > CandidateEigenvalue * CandidateEigenvalue * Program.eigenvectorchange) { ++EigenvalueDone; } } // End Test on Convergence DAVectorUtility.SynchronizeMPIvariable(ref EigenvalueDone); if (EigenvalueDone == 0) { this.CenterEigenconvergence[ClusterIndex] = 1 + NumPowerIterations; } } this.CenterEigenvalue[ClusterIndex] = CandidateEigenvalue; // Normalize current Power Vector to 1 double wgt = 1.0 / Math.Sqrt(sums[2]); for (int VectorIndex = 0; VectorIndex < Program.ParameterVectorDimension; VectorIndex++) { CenterEigenvector[ClusterIndex][VectorIndex] *= wgt; } } // End Loop over Clusters } // End Loop over NumPowerIterations FindClusterFirstTerm.sumoverthreadsandmpi(); FindNumberScalarProducts.sumoverthreadsandmpi(); Program.SumEigenSPCalcs += FindNumberScalarProducts.Total; for (int ClusterIndex = 0; ClusterIndex < this.CurrentSolution.Ncent_Global; ClusterIndex++) { this.CenterEigenstatus[ClusterIndex] = 0; if (this.CurrentSolution.LocalStatus[ClusterIndex] != 1) { continue; } if ((this.CurrentSolution.Splittable_k_[ClusterIndex] != 1) || (this.CenterEigenconvergence[ClusterIndex] <= 0)) { continue; } this.CenterEigenstatus[ClusterIndex] = 1; this.FirstTerm[ClusterIndex] = FindClusterFirstTerm.TotalVectorSum[ClusterIndex]; double tmp = this.CenterEigenvalue[ClusterIndex] / this.CurrentSolution.Temperature; this.CenterEigenvalue[ClusterIndex] = this.FirstTerm[ClusterIndex] - tmp; } } // End SetEigenvaluesIteratively(ClusteringSolution Solution)
} // End FindClusterCenters(int[] NearestCentertoPoint, double[][] LastClusterCenter) public static void ReadDataFromFile(string fname, int ClusterPosition, int FirstClustervalue, int StartPointPosition) { char[] _sep = new[] { ' ', ',', '\t' }; int FirstPointPosition = 0; int TotalNumberPointstoRead = 0; FirstPointPosition = DAVectorUtility.PointStart_Process; TotalNumberPointstoRead = DAVectorUtility.PointCount_Process; Random RandomObject = new Random(10101010 + DAVectorUtility.MPI_Rank); if (ClusterPosition < 0) { DAVectorUtility.SALSAPrint(0, "Random Start 10101010 plus rank ******************* Option " + ClusterPosition.ToString()); } int MinSplitSize = ClusterPosition + 1; if (StartPointPosition >= 0) { MinSplitSize = Math.Max(MinSplitSize, StartPointPosition + Kmeans.ParameterVectorDimension); } else { Exception e = DAVectorUtility.SALSAError("Illegal Start Position on Points file " + fname + " Rank " + DAVectorUtility.MPI_Rank.ToString() + " POsition " + StartPointPosition.ToString() + " Number to Read " + TotalNumberPointstoRead.ToString()); throw (e); } bool success = false; string line = " Unset"; int CountLinesinFile = 0; try { StreamReader sr = null; if (!string.IsNullOrEmpty(fname)) { Stream stream = File.Open(fname, FileMode.Open, FileAccess.Read, FileShare.Read); sr = new StreamReader(stream); } if (sr != null) { while (!sr.EndOfStream) { line = sr.ReadLine(); if (!string.IsNullOrEmpty(line)) { string[] splits = line.Trim().Split(_sep, StringSplitOptions.RemoveEmptyEntries); if (splits.Length < MinSplitSize) { DAVectorUtility.SALSAPrint(0, "Count " + CountLinesinFile.ToString() + " Illegal data length on Point file " + splits.Length.ToString() + " " + MinSplitSize.ToString() + " " + line); continue; } // Skip header lines double junk; if (!Double.TryParse(splits[StartPointPosition], out junk)) { continue; // Skip header lines } if (CountLinesinFile < FirstPointPosition) { CountLinesinFile += 1; continue; } int ActualPointPosition = CountLinesinFile - FirstPointPosition; int label = 0; Kmeans.PointPosition[ActualPointPosition][0] = double.Parse(splits[StartPointPosition]); Kmeans.PointPosition[ActualPointPosition][1] = double.Parse(splits[StartPointPosition + 1]); if (Kmeans.ParameterVectorDimension > 2) { for (int VectorIndex = 2; VectorIndex < Kmeans.ParameterVectorDimension; VectorIndex++) { Kmeans.PointPosition[ActualPointPosition][VectorIndex] = double.Parse(splits[VectorIndex + StartPointPosition]); } } if (ClusterPosition >= 0) { if (!Int32.TryParse(splits[ClusterPosition], out label)) { label = FirstClustervalue; } Kmeans.InitialPointAssignment[ActualPointPosition] = label - FirstClustervalue; } else { Kmeans.InitialPointAssignment[ActualPointPosition] = RandomObject.Next(Program.InitialNcent); if (ClusterPosition == -2) { // Force each cluster to have one point if (CountLinesinFile < Program.InitialNcent) { Kmeans.InitialPointAssignment[ActualPointPosition] = CountLinesinFile; } } if (ClusterPosition == -3) { int divisor = Program.NumberDataPoints / Program.InitialNcent; if (CountLinesinFile % divisor == 0) { Kmeans.InitialPointAssignment[ActualPointPosition] = CountLinesinFile / divisor; } } if (ClusterPosition == -4) { int divisor = Program.NumberDataPoints / Program.InitialNcent; Kmeans.InitialPointAssignment[ActualPointPosition] = CountLinesinFile / divisor; } } ++ActualPointPosition; ++CountLinesinFile; if (CountLinesinFile >= (FirstPointPosition + TotalNumberPointstoRead)) { break; } } } if (CountLinesinFile != (FirstPointPosition + TotalNumberPointstoRead)) { Exception e = DAVectorUtility.SALSAError("Illegal count on Points file " + fname + " Rank " + DAVectorUtility.MPI_Rank.ToString() + " Lines in File " + CountLinesinFile.ToString() + " Number to Read " + TotalNumberPointstoRead.ToString()); throw (e); } success = true; } sr.Close(); } catch (Exception e) { Console.WriteLine("Failed reading Points data " + DAVectorUtility.MPI_Rank.ToString() + " " + CountLinesinFile.ToString() + " Start " + FirstPointPosition.ToString() + " Number " + TotalNumberPointstoRead.ToString() + " " + line + e); throw (e); } if (!success) { Exception e = DAVectorUtility.SALSAError("DA Vector File read error " + fname); throw (e); } } // End ReadDataFromFile
} // End GetClusterRadius // Use LastClusterCenter if Size 0 public static void FindClusterCenters(bool begin, int[] NearestCentertoPoint, double[] Distance_NearestCentertoPoint, double[][] LastClusterCenter) { // Calculate Cluster Parameters GlobalReductions.FindVectorDoubleSum3 FindCenterVectorSums = new GlobalReductions.FindVectorDoubleSum3(DAVectorUtility.ThreadCount, Kmeans.ParameterVectorDimension, Kmeans.Ncent_Global); GlobalReductions.FindVectorIntSum FindCenterSizeSums = new GlobalReductions.FindVectorIntSum(DAVectorUtility.ThreadCount, Kmeans.Ncent_Global); GlobalReductions.FindVectorDoubleMax FindClusterRadius = new GlobalReductions.FindVectorDoubleMax(DAVectorUtility.ThreadCount, Kmeans.Ncent_Global); GlobalReductions.FindVectorDoubleSum FindClusterWidth = new GlobalReductions.FindVectorDoubleSum(DAVectorUtility.ThreadCount, Kmeans.Ncent_Global); ; Parallel.For(0, Kmeans._parallelOptions.MaxDegreeOfParallelism, Kmeans._parallelOptions, (ThreadIndex) => { FindCenterVectorSums.startthread(ThreadIndex); FindCenterSizeSums.startthread(ThreadIndex); int indexlen = DAVectorUtility.PointsperThread[ThreadIndex]; int beginpoint = DAVectorUtility.StartPointperThread[ThreadIndex] - DAVectorUtility.PointStart_Process; for (int alpha = beginpoint; alpha < indexlen + beginpoint; alpha++) { int ClusterIndex = NearestCentertoPoint[alpha]; if ((ClusterIndex >= Kmeans.Ncent_Global) || (ClusterIndex < 0)) { Exception e = DAVectorUtility.SALSAError("Illegal Cluster Index " + ClusterIndex.ToString() + " Number " + Kmeans.Ncent_Global.ToString() + " Point " + (alpha + DAVectorUtility.PointStart_Process).ToString() + " Rank " + DAVectorUtility.MPI_Rank.ToString()); throw (e); } FindCenterVectorSums.addapoint(ThreadIndex, PointPosition[alpha], ClusterIndex); FindCenterSizeSums.addapoint(ThreadIndex, 1, ClusterIndex); if (!begin) { FindClusterRadius.addapoint(ThreadIndex, Distance_NearestCentertoPoint[alpha], ClusterIndex); FindClusterWidth.addapoint(ThreadIndex, Distance_NearestCentertoPoint[alpha] * Distance_NearestCentertoPoint[alpha], ClusterIndex); } } // End loop over Points }); // End Sum over Threads FindCenterVectorSums.sumoverthreadsandmpi(); FindCenterSizeSums.sumoverthreadsandmpi(); if (!begin) { FindClusterRadius.sumoverthreadsandmpi(); FindClusterWidth.sumoverthreadsandmpi(); } Kmeans.AverageRadius = 0.0; Kmeans.AverageWidth = 0.0; Kmeans.AverageCenterChange = 0.0; if (Kmeans.UseParallelismoverCenters) { // Centers Parallel over Threads NOT nodes double[] AccumulateRadius = new double[DAVectorUtility.ThreadCount]; double[] AccumulateWidth = new double[DAVectorUtility.ThreadCount]; double[] AccumulateCenterChange = new double[DAVectorUtility.ThreadCount]; for (int ThreadIndex = 0; ThreadIndex < DAVectorUtility.ThreadCount; ThreadIndex++) { AccumulateRadius[ThreadIndex] = 0.0; AccumulateWidth[ThreadIndex] = 0.0; AccumulateCenterChange[ThreadIndex] = 0.0; } Parallel.For(0, Kmeans._parallelOptions.MaxDegreeOfParallelism, Kmeans._parallelOptions, (ThreadIndex) => { int indexlen = KmeansTriangleInequality.LocalParallel_CentersperThread[ThreadIndex]; int beginpoint = KmeansTriangleInequality.LocalParallel_StartCenterperThread[ThreadIndex]; for (int CenterIndex = beginpoint; CenterIndex < indexlen + beginpoint; CenterIndex++) { Kmeans.ClusterSize[CenterIndex] = FindCenterSizeSums.TotalVectorSum[CenterIndex]; if (Kmeans.ClusterSize[CenterIndex] > 0) { double divisor = 1.0 / (double)Kmeans.ClusterSize[CenterIndex]; for (int VectorIndex = 0; VectorIndex < Kmeans.ParameterVectorDimension; VectorIndex++) { int totalindex = VectorIndex + CenterIndex * Kmeans.ParameterVectorDimension; Kmeans.ClusterCenter[CenterIndex][VectorIndex] = FindCenterVectorSums.TotalVectorSum[totalindex] * divisor; } if (!begin) { Kmeans.ClusterRadius[CenterIndex] = FindClusterRadius.TotalVectorMax[CenterIndex]; Kmeans.ClusterWidth[CenterIndex] = FindClusterWidth.TotalVectorSum[CenterIndex]; AccumulateRadius[ThreadIndex] += Kmeans.ClusterRadius[CenterIndex]; AccumulateWidth[ThreadIndex] += Kmeans.ClusterWidth[CenterIndex]; AccumulateCenterChange[ThreadIndex] += DAVectorParallelism.getNOTSquaredUNScaledDistancebetweenVectors(Kmeans.ClusterCenter[CenterIndex], LastClusterCenter[CenterIndex]); } } else { // Zero Size Cluster if (begin) { Exception e = DAVectorUtility.SALSAError("Empty Input Cluster " + CenterIndex.ToString() + " Number " + Kmeans.Ncent_Global.ToString() + " Rank " + DAVectorUtility.MPI_Rank.ToString()); throw (e); } for (int VectorIndex = 0; VectorIndex < Kmeans.ParameterVectorDimension; VectorIndex++) { Kmeans.ClusterCenter[CenterIndex][VectorIndex] = LastClusterCenter[CenterIndex][VectorIndex]; } Kmeans.ClusterRadius[CenterIndex] = 0.0; Kmeans.ClusterWidth[CenterIndex] = 0.0; } } }); // End Sum over Threads if (!begin) { for (int ThreadIndex = 0; ThreadIndex < DAVectorUtility.ThreadCount; ThreadIndex++) { Kmeans.AverageRadius += AccumulateRadius[ThreadIndex]; Kmeans.AverageWidth += AccumulateWidth[ThreadIndex]; Kmeans.AverageCenterChange += AccumulateCenterChange[ThreadIndex]; } } } else { // Centers Sequential for (int CenterIndex = 0; CenterIndex < Kmeans.Ncent_Global; CenterIndex++) { Kmeans.ClusterSize[CenterIndex] = FindCenterSizeSums.TotalVectorSum[CenterIndex]; if (Kmeans.ClusterSize[CenterIndex] > 0) { double divisor = 1.0 / (double)Kmeans.ClusterSize[CenterIndex]; for (int VectorIndex = 0; VectorIndex < Kmeans.ParameterVectorDimension; VectorIndex++) { int totalindex = VectorIndex + CenterIndex * Kmeans.ParameterVectorDimension; Kmeans.ClusterCenter[CenterIndex][VectorIndex] = FindCenterVectorSums.TotalVectorSum[totalindex] * divisor; } if (!begin) { Kmeans.ClusterRadius[CenterIndex] = FindClusterRadius.TotalVectorMax[CenterIndex]; Kmeans.ClusterWidth[CenterIndex] = FindClusterWidth.TotalVectorSum[CenterIndex]; Kmeans.AverageRadius += Kmeans.ClusterRadius[CenterIndex]; Kmeans.AverageWidth += Kmeans.ClusterWidth[CenterIndex]; Kmeans.AverageCenterChange += DAVectorParallelism.getNOTSquaredUNScaledDistancebetweenVectors(Kmeans.ClusterCenter[CenterIndex], LastClusterCenter[CenterIndex]); } } else { if (begin) { Exception e = DAVectorUtility.SALSAError("Empty Input Cluster " + CenterIndex.ToString() + " Number " + Kmeans.Ncent_Global.ToString() + " Rank " + DAVectorUtility.MPI_Rank.ToString()); throw (e); } Kmeans.ClusterRadius[CenterIndex] = 0.0; Kmeans.ClusterWidth[CenterIndex] = 0.0; for (int VectorIndex = 0; VectorIndex < Kmeans.ParameterVectorDimension; VectorIndex++) { Kmeans.ClusterCenter[CenterIndex][VectorIndex] = LastClusterCenter[CenterIndex][VectorIndex]; } } } // End Sequential Center Loop } if (begin) { return; } Kmeans.AverageCenterChange = Kmeans.AverageCenterChange / Kmeans.Ncent_Global; Kmeans.AverageRadius = Kmeans.AverageRadius / Kmeans.Ncent_Global; Kmeans.AverageWidth = Kmeans.AverageWidth / DAVectorUtility.PointCount_Global; return; } // End FindClusterCenters(int[] NearestCentertoPoint, double[][] LastClusterCenter)