LArSoft  v09_90_00
Liquid Argon Software toolkit - https://larsoft.org/
TwoDSlidingFitSplittingAndSplicingAlgorithm.cc
Go to the documentation of this file.
1 
9 #include "Pandora/AlgorithmHeaders.h"
10 
13 
15 
16 using namespace pandora;
17 
18 namespace lar_content
19 {
20 
21 TwoDSlidingFitSplittingAndSplicingAlgorithm::TwoDSlidingFitSplittingAndSplicingAlgorithm() :
22  m_shortHalfWindowLayers(10),
23  m_longHalfWindowLayers(20),
24  m_minClusterLength(7.5f),
25  m_vetoDisplacement(1.5f),
26  m_runCosmicMode(false)
27 {
28 }
29 
30 //------------------------------------------------------------------------------------------------------------------------------------------
31 
33 {
34  const ClusterList *pClusterList = NULL;
35  PANDORA_RETURN_RESULT_IF(STATUS_CODE_SUCCESS, !=, PandoraContentApi::GetCurrentList(*this, pClusterList));
36 
37  TwoDSlidingFitResultMap branchSlidingFitResultMap, replacementSlidingFitResultMap;
38 
39  unsigned int nIterations(0);
40 
41  while (++nIterations < 100) // Protect against flip-flopping between two answers
42  {
43  // Get ordered list of candidate clusters
44  ClusterVector clusterVector;
45  this->GetListOfCleanClusters(pClusterList, clusterVector);
46 
47  // Calculate sliding fit results for branch clusters (use a soft sliding fit for these)
48  this->BuildSlidingFitResultMap(clusterVector, m_shortHalfWindowLayers, branchSlidingFitResultMap);
49 
50  // Calculate sliding fit results for replacement clusters (use a hard linear fit for these)
51  this->BuildSlidingFitResultMap(clusterVector, m_longHalfWindowLayers, replacementSlidingFitResultMap);
52 
53  // Compile a list of possible splits
54  ClusterExtensionList splitList;
55 
56  if (m_runCosmicMode)
57  {
58  this->BuildClusterExtensionList(clusterVector, branchSlidingFitResultMap, replacementSlidingFitResultMap, splitList);
59  }
60  else
61  {
62  ClusterExtensionList intermediateList;
63  this->BuildClusterExtensionList(clusterVector, branchSlidingFitResultMap, replacementSlidingFitResultMap, intermediateList);
64  this->PruneClusterExtensionList(intermediateList, branchSlidingFitResultMap, replacementSlidingFitResultMap, splitList);
65  }
66 
67  // Run splitting and extension
68  if (STATUS_CODE_SUCCESS != this->RunSplitAndExtension(splitList, branchSlidingFitResultMap, replacementSlidingFitResultMap))
69  break;
70  }
71 
72  return STATUS_CODE_SUCCESS;
73 }
74 
75 //------------------------------------------------------------------------------------------------------------------------------------------
76 
77 void TwoDSlidingFitSplittingAndSplicingAlgorithm::GetListOfCleanClusters(const ClusterList *const pClusterList, ClusterVector &clusterVector) const
78 {
79  for (ClusterList::const_iterator iter = pClusterList->begin(), iterEnd = pClusterList->end(); iter != iterEnd; ++iter)
80  {
81  const Cluster *const pCluster = *iter;
82 
84  continue;
85 
86  clusterVector.push_back(pCluster);
87  }
88 
89  std::sort(clusterVector.begin(), clusterVector.end(), LArClusterHelper::SortByNHits);
90 }
91 
92 //------------------------------------------------------------------------------------------------------------------------------------------
93 
95  const ClusterVector &clusterVector, const unsigned int halfWindowLayers, TwoDSlidingFitResultMap &slidingFitResultMap) const
96 {
97  for (ClusterVector::const_iterator iter = clusterVector.begin(), iterEnd = clusterVector.end(); iter != iterEnd; ++iter)
98  {
99  if (slidingFitResultMap.end() == slidingFitResultMap.find(*iter))
100  {
101  try
102  {
103  const float slidingFitPitch(LArGeometryHelper::GetWirePitch(this->GetPandora(), LArClusterHelper::GetClusterHitType(*iter)));
104  const TwoDSlidingFitResult slidingFitResult(*iter, halfWindowLayers, slidingFitPitch);
105 
106  if (!slidingFitResultMap.insert(TwoDSlidingFitResultMap::value_type(*iter, slidingFitResult)).second)
107  throw StatusCodeException(STATUS_CODE_FAILURE);
108  }
109  catch (StatusCodeException &statusCodeException)
110  {
111  if (STATUS_CODE_FAILURE == statusCodeException.GetStatusCode())
112  throw statusCodeException;
113  }
114  }
115  }
116 }
117 
118 //------------------------------------------------------------------------------------------------------------------------------------------
119 
121  const TwoDSlidingFitResultMap &branchSlidingFitResultMap, const TwoDSlidingFitResultMap &replacementSlidingFitResultMap,
122  ClusterExtensionList &clusterExtensionList) const
123 {
124  // Loop over each possible pair of clusters
125  for (ClusterVector::const_iterator iterI = clusterVector.begin(), iterEndI = clusterVector.end(); iterI != iterEndI; ++iterI)
126  {
127  const Cluster *const pClusterI = *iterI;
128 
129  for (ClusterVector::const_iterator iterJ = iterI, iterEndJ = clusterVector.end(); iterJ != iterEndJ; ++iterJ)
130  {
131  const Cluster *const pClusterJ = *iterJ;
132 
133  if (pClusterI == pClusterJ)
134  continue;
135 
136  // Get the branch and replacement sliding fits for this pair of clusters
137  TwoDSlidingFitResultMap::const_iterator iterBranchI = branchSlidingFitResultMap.find(*iterI);
138  TwoDSlidingFitResultMap::const_iterator iterBranchJ = branchSlidingFitResultMap.find(*iterJ);
139 
140  TwoDSlidingFitResultMap::const_iterator iterReplacementI = replacementSlidingFitResultMap.find(*iterI);
141  TwoDSlidingFitResultMap::const_iterator iterReplacementJ = replacementSlidingFitResultMap.find(*iterJ);
142 
143  if (branchSlidingFitResultMap.end() == iterBranchI || branchSlidingFitResultMap.end() == iterBranchJ ||
144  replacementSlidingFitResultMap.end() == iterReplacementI || replacementSlidingFitResultMap.end() == iterReplacementJ)
145  {
146  // TODO May want to raise an exception under certain conditions
147  continue;
148  }
149 
150  const TwoDSlidingFitResult &branchSlidingFitI(iterBranchI->second);
151  const TwoDSlidingFitResult &branchSlidingFitJ(iterBranchJ->second);
152 
153  const TwoDSlidingFitResult &replacementSlidingFitI(iterReplacementI->second);
154  const TwoDSlidingFitResult &replacementSlidingFitJ(iterReplacementJ->second);
155 
156  // Search for a split in clusterI
157  float branchChisqI(0.f);
158  CartesianVector branchSplitPositionI(0.f, 0.f, 0.f);
159  CartesianVector branchSplitDirectionI(0.f, 0.f, 0.f);
160  CartesianVector replacementStartPositionJ(0.f, 0.f, 0.f);
161 
162  try
163  {
164  this->FindBestSplitPosition(branchSlidingFitI, replacementSlidingFitJ, replacementStartPositionJ, branchSplitPositionI, branchSplitDirectionI);
165  branchChisqI = this->CalculateBranchChi2(pClusterI, branchSplitPositionI, branchSplitDirectionI);
166  }
167  catch (StatusCodeException &)
168  {
169  }
170 
171  // Search for a split in clusterJ
172  float branchChisqJ(0.f);
173  CartesianVector branchSplitPositionJ(0.f, 0.f, 0.f);
174  CartesianVector branchSplitDirectionJ(0.f, 0.f, 0.f);
175  CartesianVector replacementStartPositionI(0.f, 0.f, 0.f);
176 
177  try
178  {
179  this->FindBestSplitPosition(branchSlidingFitJ, replacementSlidingFitI, replacementStartPositionI, branchSplitPositionJ, branchSplitDirectionJ);
180  branchChisqJ = this->CalculateBranchChi2(pClusterJ, branchSplitPositionJ, branchSplitDirectionJ);
181  }
182  catch (StatusCodeException &)
183  {
184  }
185 
186  // Re-calculate chi2 values if both clusters have a split
187  if (branchChisqI > 0.f && branchChisqJ > 0.f)
188  {
189  const CartesianVector relativeDirection((branchSplitPositionJ - branchSplitPositionI).GetUnitVector());
190 
191  if (branchSplitDirectionI.GetDotProduct(relativeDirection) > 0.f && branchSplitDirectionJ.GetDotProduct(relativeDirection) < 0.f)
192  {
193  try
194  {
195  const float newBranchChisqI(this->CalculateBranchChi2(pClusterI, branchSplitPositionI, relativeDirection));
196  const float newBranchChisqJ(this->CalculateBranchChi2(pClusterJ, branchSplitPositionJ, relativeDirection * -1.f));
197  branchChisqI = newBranchChisqI;
198  branchChisqJ = newBranchChisqJ;
199  }
200  catch (StatusCodeException &)
201  {
202  }
203  }
204  }
205 
206  // Select the overall best split position
207  if (branchChisqI > branchChisqJ)
208  {
209  clusterExtensionList.push_back(
210  ClusterExtension(pClusterI, pClusterJ, replacementStartPositionJ, branchSplitPositionI, branchSplitDirectionI));
211  }
212 
213  else if (branchChisqJ > branchChisqI)
214  {
215  clusterExtensionList.push_back(
216  ClusterExtension(pClusterJ, pClusterI, replacementStartPositionI, branchSplitPositionJ, branchSplitDirectionJ));
217  }
218  }
219  }
220 }
221 
222 //------------------------------------------------------------------------------------------------------------------------------------------
223 
225  const TwoDSlidingFitResultMap &branchMap, const TwoDSlidingFitResultMap &replacementMap, ClusterExtensionList &outputList) const
226 {
227  ClusterList branchList;
228  for (const auto &mapEntry : branchMap)
229  branchList.push_back(mapEntry.first);
230  branchList.sort(LArClusterHelper::SortByNHits);
231 
232  ClusterList replacementList;
233  for (const auto &mapEntry : replacementMap)
234  replacementList.push_back(mapEntry.first);
235  replacementList.sort(LArClusterHelper::SortByNHits);
236 
237  for (const ClusterExtension &thisSplit : inputList)
238  {
239  const CartesianVector &branchVertex = thisSplit.GetBranchVertex();
240  const CartesianVector &replacementVertex = thisSplit.GetReplacementVertex();
241 
242  const float distanceSquared((branchVertex - replacementVertex).GetMagnitudeSquared());
243  const float vetoDistanceSquared(m_vetoDisplacement * m_vetoDisplacement);
244 
245  bool branchVeto(false), replacementVeto(false);
246 
247  // Veto the merge if another cluster is closer to the replacement vertex
248  for (const Cluster *const pBranchCluster : branchList)
249  {
250  const TwoDSlidingFitResult &slidingFit(branchMap.at(pBranchCluster));
251 
252  if (slidingFit.GetCluster() == thisSplit.GetReplacementCluster() || slidingFit.GetCluster() == thisSplit.GetBranchCluster())
253  continue;
254 
255  const float minDistanceSquared((replacementVertex - slidingFit.GetGlobalMinLayerPosition()).GetMagnitudeSquared());
256  const float maxDistanceSquared((replacementVertex - slidingFit.GetGlobalMaxLayerPosition()).GetMagnitudeSquared());
257 
258  if (std::min(minDistanceSquared, maxDistanceSquared) < std::max(distanceSquared, vetoDistanceSquared))
259  {
260  branchVeto = true;
261  break;
262  }
263  }
264 
265  // Veto the merge if another cluster is closer to the branch vertex
266  for (const Cluster *const pReplacementCluster : replacementList)
267  {
268  const TwoDSlidingFitResult &slidingFit(replacementMap.at(pReplacementCluster));
269 
270  if (slidingFit.GetCluster() == thisSplit.GetReplacementCluster() || slidingFit.GetCluster() == thisSplit.GetBranchCluster())
271  continue;
272 
273  const float minDistanceSquared((branchVertex - slidingFit.GetGlobalMinLayerPosition()).GetMagnitudeSquared());
274  const float maxDistanceSquared((branchVertex - slidingFit.GetGlobalMaxLayerPosition()).GetMagnitudeSquared());
275 
276  if (std::min(minDistanceSquared, maxDistanceSquared) < std::max(distanceSquared, vetoDistanceSquared))
277  {
278  replacementVeto = true;
279  break;
280  }
281  }
282 
283  if (branchVeto || replacementVeto)
284  continue;
285 
286  outputList.push_back(thisSplit);
287  }
288 }
289 
290 //------------------------------------------------------------------------------------------------------------------------------------------
291 
293  const Cluster *const pCluster, const CartesianVector &splitPosition, const CartesianVector &splitDirection) const
294 {
295  CaloHitList principalCaloHitList, branchCaloHitList;
296 
297  this->SplitBranchCluster(pCluster, splitPosition, splitDirection, principalCaloHitList, branchCaloHitList);
298 
299  float totalChi2(0.f);
300  float totalHits(0.f);
301 
302  for (CaloHitList::const_iterator iter = branchCaloHitList.begin(), iterEnd = branchCaloHitList.end(); iter != iterEnd; ++iter)
303  {
304  const CaloHit *const pCaloHit = *iter;
305 
306  const CartesianVector hitPosition(pCaloHit->GetPositionVector());
307  const CartesianVector projectedPosition(splitPosition + splitDirection * splitDirection.GetDotProduct(hitPosition - splitPosition));
308 
309  totalChi2 += (hitPosition - projectedPosition).GetMagnitudeSquared();
310  totalHits += 1.f;
311  }
312 
313  if (totalHits > 0.f)
314  return std::sqrt(totalChi2 / totalHits);
315 
316  throw StatusCodeException(STATUS_CODE_NOT_ALLOWED);
317 }
318 
319 //------------------------------------------------------------------------------------------------------------------------------------------
320 
321 void TwoDSlidingFitSplittingAndSplicingAlgorithm::SplitBranchCluster(const Cluster *const pCluster, const CartesianVector &splitPosition,
322  const CartesianVector &splitDirection, CaloHitList &principalCaloHitList, CaloHitList &branchCaloHitList) const
323 {
324  // Distribute hits in branch cluster between new principal and residual clusters
325  CaloHitList caloHitsToDistribute;
326  pCluster->GetOrderedCaloHitList().FillCaloHitList(caloHitsToDistribute);
327 
328  for (CaloHitList::const_iterator iter = caloHitsToDistribute.begin(), iterEnd = caloHitsToDistribute.end(); iter != iterEnd; ++iter)
329  {
330  const CaloHit *const pCaloHit = *iter;
331 
332  if (splitDirection.GetDotProduct((pCaloHit->GetPositionVector() - splitPosition)) > 0.f)
333  {
334  branchCaloHitList.push_back(pCaloHit);
335  }
336  else
337  {
338  principalCaloHitList.push_back(pCaloHit);
339  }
340  }
341 
342  if (branchCaloHitList.empty())
343  throw StatusCodeException(STATUS_CODE_NOT_ALLOWED);
344 }
345 
346 //------------------------------------------------------------------------------------------------------------------------------------------
347 
349  const ClusterExtensionList &splitList, TwoDSlidingFitResultMap &branchResultMap, TwoDSlidingFitResultMap &replacementResultMap) const
350 {
351  bool foundSplit(false);
352 
353  for (ClusterExtensionList::const_iterator iter = splitList.begin(), iterEnd = splitList.end(); iter != iterEnd; ++iter)
354  {
355  const ClusterExtension &thisSplit = *iter;
356 
357  const Cluster *const pBranchCluster = thisSplit.GetBranchCluster();
358  const Cluster *const pReplacementCluster = thisSplit.GetReplacementCluster();
359  const CartesianVector &branchSplitPosition = thisSplit.GetBranchVertex();
360  const CartesianVector &branchSplitDirection = thisSplit.GetBranchDirection();
361 
362  TwoDSlidingFitResultMap::iterator iterBranch1 = branchResultMap.find(pBranchCluster);
363  TwoDSlidingFitResultMap::iterator iterBranch2 = branchResultMap.find(pReplacementCluster);
364 
365  TwoDSlidingFitResultMap::iterator iterReplacement1 = replacementResultMap.find(pBranchCluster);
366  TwoDSlidingFitResultMap::iterator iterReplacement2 = replacementResultMap.find(pReplacementCluster);
367 
368  if (branchResultMap.end() == iterBranch1 || branchResultMap.end() == iterBranch2 ||
369  replacementResultMap.end() == iterReplacement1 || replacementResultMap.end() == iterReplacement2)
370  continue;
371 
372  PANDORA_RETURN_RESULT_IF(
373  STATUS_CODE_SUCCESS, !=, this->ReplaceBranch(pBranchCluster, pReplacementCluster, branchSplitPosition, branchSplitDirection));
374  branchResultMap.erase(iterBranch1);
375  branchResultMap.erase(iterBranch2);
376 
377  replacementResultMap.erase(iterReplacement1);
378  replacementResultMap.erase(iterReplacement2);
379 
380  foundSplit = true;
381  }
382 
383  if (foundSplit)
384  return STATUS_CODE_SUCCESS;
385 
386  return STATUS_CODE_NOT_FOUND;
387 }
388 
389 //------------------------------------------------------------------------------------------------------------------------------------------
390 
391 StatusCode TwoDSlidingFitSplittingAndSplicingAlgorithm::ReplaceBranch(const Cluster *const pBranchCluster,
392  const Cluster *const pReplacementCluster, const CartesianVector &branchSplitPosition, const CartesianVector &branchSplitDirection) const
393 {
394  ClusterList clusterList;
395  clusterList.push_back(pBranchCluster);
396  clusterList.push_back(pReplacementCluster);
397 
398  std::string clusterListToSaveName, clusterListToDeleteName;
399  PANDORA_RETURN_RESULT_IF(STATUS_CODE_SUCCESS, !=,
400  PandoraContentApi::InitializeFragmentation(*this, clusterList, clusterListToDeleteName, clusterListToSaveName));
401 
402  // Entire replacement cluster goes into new principal cluster
403  PandoraContentApi::Cluster::Parameters principalParameters;
404  pReplacementCluster->GetOrderedCaloHitList().FillCaloHitList(principalParameters.m_caloHitList);
405 
406  // Distribute hits in branch cluster between new principal and residual clusters
407  PandoraContentApi::Cluster::Parameters residualParameters;
408  this->SplitBranchCluster(
409  pBranchCluster, branchSplitPosition, branchSplitDirection, principalParameters.m_caloHitList, residualParameters.m_caloHitList);
410 
411  const Cluster *pPrincipalCluster(NULL), *pResidualCluster(NULL);
412  PANDORA_RETURN_RESULT_IF(STATUS_CODE_SUCCESS, !=, PandoraContentApi::Cluster::Create(*this, principalParameters, pPrincipalCluster));
413  PANDORA_RETURN_RESULT_IF(STATUS_CODE_SUCCESS, !=, PandoraContentApi::Cluster::Create(*this, residualParameters, pResidualCluster));
414  PANDORA_RETURN_RESULT_IF(STATUS_CODE_SUCCESS, !=, PandoraContentApi::EndFragmentation(*this, clusterListToSaveName, clusterListToDeleteName));
415 
416  return STATUS_CODE_SUCCESS;
417 }
418 
419 //------------------------------------------------------------------------------------------------------------------------------------------
420 
421 StatusCode TwoDSlidingFitSplittingAndSplicingAlgorithm::ReadSettings(const TiXmlHandle xmlHandle)
422 {
423  PANDORA_RETURN_RESULT_IF_AND_IF(
424  STATUS_CODE_SUCCESS, STATUS_CODE_NOT_FOUND, !=, XmlHelper::ReadValue(xmlHandle, "ShortHalfWindow", m_shortHalfWindowLayers));
425 
426  PANDORA_RETURN_RESULT_IF_AND_IF(
427  STATUS_CODE_SUCCESS, STATUS_CODE_NOT_FOUND, !=, XmlHelper::ReadValue(xmlHandle, "LongHalfWindow", m_longHalfWindowLayers));
428 
429  PANDORA_RETURN_RESULT_IF_AND_IF(
430  STATUS_CODE_SUCCESS, STATUS_CODE_NOT_FOUND, !=, XmlHelper::ReadValue(xmlHandle, "MinClusterLength", m_minClusterLength));
431 
432  PANDORA_RETURN_RESULT_IF_AND_IF(
433  STATUS_CODE_SUCCESS, STATUS_CODE_NOT_FOUND, !=, XmlHelper::ReadValue(xmlHandle, "VetoDisplacement", m_vetoDisplacement));
434 
435  PANDORA_RETURN_RESULT_IF_AND_IF(STATUS_CODE_SUCCESS, STATUS_CODE_NOT_FOUND, !=, XmlHelper::ReadValue(xmlHandle, "CosmicMode", m_runCosmicMode));
436 
437  return STATUS_CODE_SUCCESS;
438 }
439 
440 } // namespace lar_content
intermediate_table::iterator iterator
static bool SortByNHits(const pandora::Cluster *const pLhs, const pandora::Cluster *const pRhs)
Sort clusters by number of hits, then layer span, then inner layer, then position, then pulse-height.
const pandora::Cluster * GetReplacementCluster() const
return the address of the replacement Cluster
virtual void FindBestSplitPosition(const TwoDSlidingFitResult &branchSlidingFit, const TwoDSlidingFitResult &replacementSlidingFit, pandora::CartesianVector &replacementStartPosition, pandora::CartesianVector &branchSplitPosition, pandora::CartesianVector &branchSplitDirection) const =0
Output the best split positions in branch and replacement clusters.
void BuildSlidingFitResultMap(const pandora::ClusterVector &clusterVector, const unsigned int halfWindowLayers, TwoDSlidingFitResultMap &slidingFitResultMap) const
Build the map of sliding fit results.
void BuildClusterExtensionList(const pandora::ClusterVector &clusterVector, const TwoDSlidingFitResultMap &branchResultMap, const TwoDSlidingFitResultMap &replacementResultMap, ClusterExtensionList &clusterExtensionList) const
Build a list of candidate splits.
intermediate_table::const_iterator const_iterator
const pandora::Cluster * GetBranchCluster() const
return the address of the branch Cluster
static pandora::HitType GetClusterHitType(const pandora::Cluster *const pCluster)
Get the hit type associated with a two dimensional cluster.
const pandora::CartesianVector & GetBranchVertex() const
return the split position of the branch cluster
const pandora::CartesianVector & GetBranchDirection() const
return the split direction of the branch cluster
TFile f
Definition: plotHisto.C:6
Header file for the geometry helper class.
void GetListOfCleanClusters(const pandora::ClusterList *const pClusterList, pandora::ClusterVector &clusterVector) const
Populate cluster vector with subset of cluster list, containing clusters judged to be clean...
pandora::StatusCode RunSplitAndExtension(const ClusterExtensionList &splitList, TwoDSlidingFitResultMap &branchResultMap, TwoDSlidingFitResultMap &replacementResultMap) const
Run the machinary that performs the cluster splitting and extending.
virtual pandora::StatusCode ReadSettings(const pandora::TiXmlHandle xmlHandle)
pandora::StatusCode ReplaceBranch(const pandora::Cluster *const pBranchCluster, const pandora::Cluster *const pReplacementCluster, const pandora::CartesianVector &branchSplitPosition, const pandora::CartesianVector &branchSplitDirection) const
Remove a branch from a cluster and replace it with a second cluster.
static float GetWirePitch(const pandora::Pandora &pandora, const pandora::HitType view, const float maxWirePitchDiscrepancy=0.01)
Return the wire pitch.
std::unordered_map< const pandora::Cluster *, TwoDSlidingFitResult > TwoDSlidingFitResultMap
Header file for the two dimensional sliding fit splitting and splicing algorithm class.
static float GetLengthSquared(const pandora::Cluster *const pCluster)
Get length squared of cluster.
void SplitBranchCluster(const pandora::Cluster *const pCluster, const pandora::CartesianVector &splitPosition, const pandora::CartesianVector &splitDirection, pandora::CaloHitList &principalCaloHitList, pandora::CaloHitList &branchCaloHitList) const
Separate cluster into the branch hits to be split from the primary cluster.
float CalculateBranchChi2(const pandora::Cluster *const pCluster, const pandora::CartesianVector &splitPosition, const pandora::CartesianVector &splitDirection) const
Calculate RMS deviation of branch hits relative to the split direction.
std::vector< art::Ptr< recob::Cluster > > ClusterVector
void PruneClusterExtensionList(const ClusterExtensionList &inputList, const TwoDSlidingFitResultMap &branchResultMap, const TwoDSlidingFitResultMap &replacementResultMap, ClusterExtensionList &outputList) const
Finalize the list of candidate splits.