ATLAS Offline Software
Loading...
Searching...
No Matches
StripClusteringTool.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
3*/
5
12
13#include <algorithm>
14#include <cmath>
15#include <stdexcept>
16
17
18namespace ActsTrk {
19constexpr double ONE_TWELFTH = 1./12.;
20constexpr float oneStripSF = 1.1025;
21constexpr float twoStripSF = 0.0729;
22
24 const std::string& type, const std::string& name, const IInterface* parent)
25 : base_class(type,name,parent)
26{
27}
28
30{
31 ATH_MSG_DEBUG("Initializing " << name() << "...");
32
39
40 ATH_CHECK(m_lorentzAngleTool.retrieve());
42
43 ATH_CHECK(m_stripDetElStatus.initialize(not m_stripDetElStatus.empty()));
44 ATH_CHECK(m_stripDetEleCollKey.initialize());
45
46 ATH_CHECK( detStore()->retrieve(m_stripID, "SCT_ID") );
47
48 return StatusCode::SUCCESS;
49}
50
51std::pair<unsigned int, unsigned int>
52StripClusteringTool::countCells(const RDOContainer& rdoContainer,
53 const std::vector<IdentifierHash> &listOfIds,
54 const InDetDD::SiDetectorElementCollection &detector_elements) const {
55 auto getNHits =[](const InDetRawDataCollection<SCT_RDORawData> &RDOs,
56 [[maybe_unused]] const InDetDD::SiDetectorElementCollection &detector_elements )
57 -> unsigned int
58 {
59 unsigned int n_hits = 0u;
60 for (const SCT_RDORawData* rdo : RDOs) {
61 n_hits += rdo->getGroupSize();
62 }
63 return n_hits;
64 };
65 unsigned int n_hits=0;
66 if (listOfIds.empty()) {
67 for (const InDetRawDataCollection<SCT_RDORawData> *RDOs : rdoContainer) {
68 assert( RDOs);
69 n_hits += getNHits(*RDOs, detector_elements);
70 }
71 }
72 else {
73 for (const IdentifierHash& id : listOfIds) {
74 if (not id.is_valid()) continue;
75 const InDetRawDataCollection<SCT_RDORawData> *RDOs = rdoContainer.indexFindPtr(id);
76 if (RDOs) { // necessary ?
77 n_hits += getNHits(*RDOs, detector_elements);
78 }
79 }
80 }
81 return {n_hits,n_hits};
82}
83
85 StripAuxDataCache(SG::AuxVectorData& cont, unsigned int n_cluster_rdos)
86 : xAOD::VariableStruct(cont),
87 rdoList(cont, xAOD::StripCluster::rdoListAcc(), n_cluster_rdos)
88 {}
90};
91
93 std::size_t nClusterRDOs) const
94{
95 return std::any (StripAuxDataCache (cont, nClusterRDOs));
96}
97
99{
100 for (size_t i = 0; i < m_timeBinStr.size(); i++) {
101 if (i >= 3) {
102 ATH_MSG_WARNING("Time bin string has excess characters");
103 break;
104 }
105 switch (std::toupper(m_timeBinStr[i])) {
106 case 'X': m_timeBinBits[i] = -1; break;
107 case '0': m_timeBinBits[i] = 0; break;
108 case '1': m_timeBinBits[i] = 1; break;
109 default:
110 ATH_MSG_FATAL("Invalid time bin string: " << m_timeBinStr);
111 return StatusCode::FAILURE;
112 }
113 }
114 return StatusCode::SUCCESS;
115}
116
117StatusCode
118StripClusteringTool::clusterize([[maybe_unused]] const EventContext& ctx,
119 const RawDataCollection& RDOs,
120 const InDet::SiDetectorElementStatus& stripDetElStatus,
121 const InDetDD::SiDetectorElement& element,
122 IStripClusteringTool::CellContainer &cellContainer) const
123{
124 IdentifierHash idHash = RDOs.identifyHash();
126
127 // At least an empty cluster collection needs to be always added because the
128 // assumption is that there is one element per element RawDataCollection.
129 bool goodModule = true;
130 if (m_checkBadModules.value()) {
131 goodModule = stripDetElStatus.isGood(idHash);
132 }
133
134 // If more than a certain number of RDOs set module to bad
135 // in this case we skip clusterization
136 if (!goodModule && m_maxFiredStrips != 0u) {
137 unsigned int nFiredStrips = 0u;
138 for (const SCT_RDORawData* rdo : RDOs) {
139 nFiredStrips += rdo->getGroupSize();
140 }
141 goodModule |= (nFiredStrips <m_maxFiredStrips);
142 }
143
144 if (goodModule) {
145 std::span<IStripClusteringTool::CellContainer::Cell>
146 cellRange = unpackRDOs(RDOs, stripDetElStatus, element, cellContainer);
147
148 static constexpr unsigned int SORT_BY_LOCAL_X=0u;
149 namespace CL=Acts::InPlaceClusterization;
150 CL::clusterize<SORT_BY_LOCAL_X, std::uint16_t>(cellRange,
151 CL::defaultConnectionHelper<CL::EConnectionType::CommonEdgeOrCorner>(cellRange));
152
153 // set the cell range per cluster
155 [&cellContainer](std::span<IStripClusteringTool::CellContainer::Cell> &,
156 unsigned int idx_begin,
157 unsigned int idx_end) {
158 cellContainer.registerNewCluster(idx_begin,idx_end);
159 });
160 }
161 // must add a range for every call otherwise the cell container and
162 // the list of processed modules get out of sync.
163 cellContainer.registerClustersForNewModule(rangeGuard.range());
164 return StatusCode::SUCCESS;
165}
166
167
168StatusCode
169StripClusteringTool::makeClusters(const EventContext& ctx,
170 [[maybe_unused]] const RDOContainer &rdoContainer,
171 const IStripClusteringTool::CellContainer& cellContainer,
172 unsigned int imodule,
173 const InDetDD::SiDetectorElement& element,
174 unsigned int icluster,
176 std::any& cache) const
177{
178 const IdentifierHash idHash = element.identifyHash();
179 double lorentzShift = m_lorentzAngleTool->getLorentzShift(idHash, ctx);
180
181 const InDetDD::SiDetectorDesign& design = element.design();
182 // get the pitch, this will be the local covariance for the cluster
183
184 assert((not m_isITk) || element.isBarrel() || dynamic_cast<const InDetDD::StripStereoAnnulusDesign*>(&element.design()) !=nullptr);
185 float pitch = (element.isBarrel() or (not m_isITk))
186 ? design.phiPitch()
187 : static_cast<const InDetDD::StripStereoAnnulusDesign&>(element.design()).phiPitchPhi();
188 Eigen::Matrix<float,1,1> localCov(pitch * pitch * ONE_TWELFTH);
189
190 StripAuxDataCache* auxDataCache = std::any_cast<StripAuxDataCache> (&cache);
191 if (!auxDataCache) throw std::bad_any_cast();
192
196 CellContainerProxy cellContainerProxy(&cellContainer);
197 ModuleProxy moduleProxy(cellContainerProxy[imodule]);
198
199 for (ClusterProxy clusterProxy: moduleProxy) {
200 assert( icluster< cont.size() && cont[icluster]);
201 xAOD::StripCluster &stripCluster= *cont[icluster];
202 ATH_CHECK(makeCluster(icluster++,
203 stripCluster,
204 clusterProxy,
205 element,
206 design,
207 lorentzShift,
208 localCov,
209 *auxDataCache));
210 }
211 return StatusCode::SUCCESS;
212}
213
214static
215std::pair<
216 Eigen::Matrix<float,1,1>,
217 Eigen::Matrix<float,3,1>>
219 const InDetDD::SiDetectorElement& element,
220 const InDetDD::SiDetectorDesign& design,
221 double lorentzShift,
222 bool isITk )
223{
224
225
226 InDetDD::SiCellId frontId = cluster.front().coordinates()[0];
227 InDetDD::SiLocalPosition pos = design.localPositionOfCell(frontId);
228 if (cluster.size()>1) {
229 InDetDD::SiCellId backId = cluster.back().coordinates()[0];
231 design.localPositionOfCell(backId);
232 pos = 0.5 * (pos + backPos);
233 }
234
235 // update the xPhi position
236 pos.xPhi( pos.xPhi() + lorentzShift );
237 // @TODO use local to global of Acts surface instead
238 Eigen::Matrix<float,3,1> posG(element.surface().localToGlobal(pos).cast<float>());
239
240 if ((not element.isBarrel()) and isITk) {
241 assert(dynamic_cast<const InDetDD::StripStereoAnnulusDesign*>(&design) != nullptr);
242 const InDetDD::StripStereoAnnulusDesign& annulusDesign =
243 static_cast<const InDetDD::StripStereoAnnulusDesign&>
244 (design);
245 pos = annulusDesign.localPositionOfCellPC(element.cellIdOfPosition(pos));
246 }
247
248 return std::make_pair(Eigen::Matrix<float,1,1>(pos.xPhi()),
249 std::move(posG));
250}
251
252static
255 const InDetDD::SiDetectorDesign& design)
256{ // For Inner Detector SCT, compute the local position of the cluster center using the strip positions.
257 InDetDD::SiCellId frontId = cluster.front().coordinates()[0];
258 InDetDD::SiLocalPosition pos = design.localPositionOfCell(frontId);
259 if (cluster.size() > 1) {
260 InDetDD::SiCellId backId = cluster.back().coordinates()[0];
261 InDetDD::SiLocalPosition backPos = design.localPositionOfCell(backId);
262 pos = 0.5 * (pos + backPos);
263 }
264 return pos;
265}
266
267static
269 float localPos,
270 float localCov,
271 const InDetDD::SiDetectorElement& element,
272 const InDetDD::SiDetectorDesign& design)
273{ // For Inner Detector SCT, in the case of endcap modules, rotate the local covariance to account for the stereo angle.
274 const bool rotate = (design.shape() == InDetDD::Trapezoid || design.shape() == InDetDD::Annulus);
275 if (!rotate) {
276 return localCov;
277 }
278
279 const auto* sctDesign = dynamic_cast<const InDetDD::SCT_ModuleSideDesign*>(&design);
280 if (sctDesign == nullptr) {
281 return localCov;
282 }
283
284 const InDetDD::SiLocalPosition centrePos = computeCentrePosition(cluster, design);
285 const auto ends = sctDesign->endsOfStrip(centrePos);
286 const double stripL = std::abs(ends.first.xEta() - ends.second.xEta());
287 const double iphipitch = 1. / element.phiPitch();
288 const Amg::Vector2D localPos2D{localPos, 0.0};
289 const double w = element.phiPitch(localPos2D) * iphipitch;
290
291 const double sn = element.sinStereoLocal(localPos2D);
292 const double sn2 = sn * sn;
293 const double cs2 = 1. - sn2;
294 const double v0 = localCov * w * w;
295 const double v1 = stripL * stripL * ONE_TWELFTH;
296
297 const float rotatedCov = cs2 * v0 + sn2 * v1;
298 // copied from InDet::SCT_ClusteringTool, but in ACTS-based tracking, SCT cov is 1-dimensional, just keep the rotatedCov(0,0) for the moment
299 // rotatedCov(0,1) = rotatedCov = sn * sqrt(cs2) * (v0 - v1);
300 // rotatedCov(1,1) = sn2 * v0 + cs2 * v1;
301 return rotatedCov;
302}
303
304StatusCode
307 const StripClusteringTool::ClusterProxy &clusterProxy,
308 const InDetDD::SiDetectorElement& element,
309 const InDetDD::SiDetectorDesign& design,
310 const double lorentzShift,
311 Eigen::Matrix<float,1,1>& localCov,
312 StripAuxDataCache &auxDataCache) const
313{
314 auto [localPos, globalPos]
315 = computePosition(clusterProxy, element, design, lorentzShift, m_isITk);
316
317 // For Strip Clusters the identifier is taken from the front rod list object
318 // This is the same strategy used in Athena:
319 // Since clusterId is arbitary (it only needs to be unique) just use ID of first strip
320 // For strip Cluster it has been found that "identifierOfPosition" does not produces unique values
321
322
323 // If requiring broad errors, use the cluster size as error -
324 // TODO use SiWidth to get the right cluster size as I'm assuming equal strip pitch
325 std::size_t size = clusterProxy.size();
326 if (m_errorStrategy == 1) {// use width
327
328 localCov *= size*size;
329
330 } else if (m_errorStrategy == 2) { //use tuned error as function of size
331
332 if (size == 1)
333 localCov *= oneStripSF;
334 else if (size == 2)
335 localCov *= twoStripSF*size*size;
336 else
337 localCov *= size*size;
338
339 }
340
341 if (not m_isITk) {
342 localCov(0,0) = computeRotatedLocalCov(clusterProxy, localPos(0,0), localCov(0,0), element, design);
343 }
344
345 cl.setMeasurement<1>(element.identifyHash(), localPos, localCov);
346 Identifier frontRDOId = m_stripID->strip_id(element.identify(), clusterProxy.front().coordinates()[0]);
347 cl.setIdentifier( frontRDOId.get_compact() );
348
349 // Do I really need the global position in fast tracking?
350 cl.globalPosition() = globalPos;
351
352 cl.setChannelsInPhi(size);
353
354 unsigned int n_rdos = auxDataCache.rdoList.getBeginIndex(icluster);
355 Identifier waferId=element.identify();
357 for (CellProxy cellProxy : clusterProxy) {
358 Identifier rdoId = m_stripID->strip_id(waferId, cellProxy.coordinates()[0]);
359 auxDataCache.rdoList.setValue(n_rdos,rdoId.get_compact());
360 ++n_rdos;
361 }
362 auxDataCache.rdoList.updateEndIndex(icluster,n_rdos);
363
364 return StatusCode::SUCCESS;
365}
366
367
368bool StripClusteringTool::passTiming(const std::bitset<3>& timePattern) const {
369 // Convert the given timebin to a bit set and test each bit
370 // if bit is -1 (i.e. X) it always passes, other wise require exact match of 0/1
371 // N.B bitset has opposite order to the bit pattern we define
372 if (m_timeBinBits[0] != -1 and timePattern.test(2) != static_cast<bool>(m_timeBinBits[0])) return false;
373 if (m_timeBinBits[1] != -1 and timePattern.test(1) != static_cast<bool>(m_timeBinBits[1])) return false;
374 if (m_timeBinBits[2] != -1 and timePattern.test(0) != static_cast<bool>(m_timeBinBits[2])) return false;
375 return true;
376}
377
378
380 IdentifierHash waferHash,
381 std::int16_t strip)
382{
383 if (stripDetElStatus) {
384 return not stripDetElStatus->isCellGood(waferHash.value(), strip) ;
385 }
386 return false;
387}
388
389
390std::span<IStripClusteringTool::CellContainer::Cell>
391StripClusteringTool::unpackRDOs(const RawDataCollection& RDOs,
392 const InDet::SiDetectorElementStatus& stripDetElStatus,
393 const InDetDD::SiDetectorElement& element,
394 IStripClusteringTool::CellContainer &cellContainer) const
395{
396 const InDetDD::SiDetectorDesign& design = element.design();
397
398 //Check type in debug build otherwise assume it is correct
399 assert(dynamic_cast<const InDetDD::SCT_ModuleSideDesign*>(&design)!=nullptr);
400 std::size_t ncells = static_cast<size_t>(static_cast<const InDetDD::SCT_ModuleSideDesign&>(design).cells());
401
402 IStripClusteringTool::CellContainer::ModuleRangeGuard rangeGuard(cellContainer, RDOs.identifyHash() );
403 // Simple single-entry cache
404 IdentifierHash waferHash=RDOs.identifyHash();
405
406 for (unsigned int rdo_i=0; rdo_i<RDOs.size(); ++rdo_i) {
407 const StripRDORawData *raw=RDOs[rdo_i];
408
409 //Check type in debug build otherwise assume it is correct
410 assert(dynamic_cast<const SCT3_RawData*>(raw)!=nullptr);
411 const SCT3_RawData* raw3 = static_cast<const SCT3_RawData*>(raw);
412
413 std::bitset<3> timePattern(raw3->getTimeBin());
414 if (!passTiming(timePattern)) {
415 ATH_MSG_DEBUG("Strip failed timing check");
416 continue;
417 }
418
419 Identifier firstStripId = raw->identify();
420 Identifier waferId = m_stripID->wafer_id(firstStripId);
421
422 assert( waferHash == m_stripID->wafer_hash(waferId) );
423
424 std::int16_t iFirstStrip = static_cast<std::int16_t>(m_stripID->strip(firstStripId));
425 assert(m_stripID->strip(firstStripId)==iFirstStrip);
426
427 assert( ncells == static_cast<std::uint16_t>(ncells) );
428 std::int16_t iMaxStrip = std::min(
429 static_cast<std::int16_t>(iFirstStrip + raw->getGroupSize()),
430 static_cast<std::int16_t>(ncells));
431
432 for (std::int16_t strip_i = iFirstStrip; strip_i < iMaxStrip; strip_i++) {
433 if (isBadStrip(&stripDetElStatus, waferHash, strip_i)) {
434 // Bad strip, throw it out to minimize useless work.
435 ATH_MSG_DEBUG("Bad strip encountered on module " << waferHash << " : "
436 << m_stripID->strip_id(waferId, strip_i)
437 << ", wafer is: " << waferId << " strip is "<< strip_i
438 << " hash " << m_stripID->wafer_hash(waferId));
439 } else {
440 // Good strip!
441
442 std::array<std::int16_t,1> coordinates{strip_i};
443 cellContainer.emplace_back_cell(coordinates, rdo_i);
444 }
445 }
446 }
447
448 return rangeGuard.moduleCellSpan();
449}
450
451} // namespace ActsTrk
#define ATH_CHECK
Evaluate an expression and check for errors.
#define ATH_MSG_FATAL(x)
#define ATH_MSG_WARNING(x)
#define ATH_MSG_DEBUG(x)
size_t size() const
Number of registered mappings.
void rotate(double angler, GeoTrf::Vector2D &vector)
Define structures for fast filling of xAOD objects.
Gaudi::Property< bool > m_isITk
static bool isBadStrip(const InDet::SiDetectorElementStatus *sctDetElStatus, IdentifierHash waferHash, std::int16_t strip)
Gaudi::Property< unsigned int > m_maxFiredStrips
Gaudi::Property< bool > m_checkBadModules
bool passTiming(const std::bitset< 3 > &timePattern) const
virtual std::pair< unsigned int, unsigned int > countCells(const RDOContainer &rdo_collection, const std::vector< IdentifierHash > &listOfIds, const InDetDD::SiDetectorElementCollection &detector_elements) const override
virtual std::any createEventDataCache(xAOD::StripClusterContainer &cont, std::size_t nClusterRDOs) const override
virtual StatusCode initialize() override
virtual StatusCode makeClusters(const EventContext &ctx, const RDOContainer &rdo_container, const IStripClusteringTool::CellContainer &cellContainer, unsigned int module_i, const InDetDD::SiDetectorElement &element, unsigned int icluster, xAOD::StripClusterContainer &cont, std::any &vars) const override
StripClusteringTool(const std::string &type, const std::string &name, const IInterface *parent)
ToolHandle< ISiLorentzAngleTool > m_lorentzAngleTool
virtual StatusCode clusterize(const EventContext &ctx, const RawDataCollection &RDOs, const InDet::SiDetectorElementStatus &stripDetElStatus, const InDetDD::SiDetectorElement &element, IStripClusteringTool::CellContainer &cellContainer) const override
StatusCode makeCluster(size_t icluster, xAOD::StripCluster &cl, const ClusterProxy &cluster_proxy, const InDetDD::SiDetectorElement &element, const InDetDD::SiDetectorDesign &design, const double lorentzShift, Eigen::Matrix< float, 1, 1 > &localCov, StripAuxDataCache &auxDataCache) const
SG::ReadCondHandleKey< InDetDD::SiDetectorElementCollection > m_stripDetEleCollKey
Gaudi::Property< unsigned int > m_errorStrategy
SG::ReadHandleKey< InDet::SiDetectorElementStatus > m_stripDetElStatus
InPlaceClusterization::ClusterProxy< const IStripClusteringTool::CellContainer > ClusterProxy
std::span< IStripClusteringTool::CellContainer::Cell > unpackRDOs(const RawDataCollection &RDOs, const InDet::SiDetectorElementStatus &stripDetElStatus, const InDetDD::SiDetectorElement &element, IStripClusteringTool::CellContainer &cellContainer) const
size_type size() const noexcept
Returns the number of elements in the collection.
This is a "hash" representation of an Identifier.
constexpr value_type value() const
value_type get_compact() const
Get the compact id.
virtual DetectorShape shape() const
Shape of element.
virtual SiLocalPosition localPositionOfCell(const SiCellId &cellId) const =0
readout or diode id -> position.
virtual double phiPitch() const =0
Pitch in phi direction.
Base class for the SCT module side design, extended by the Forward and Barrel module design.
int cells() const
number of readout stips within module side:
Identifier for the strip or pixel cell.
Definition SiCellId.h:29
Base class for the detector design classes for Pixel and SCT.
Class to hold the SiDetectorElement objects to be put in the detector store.
Class to hold geometrical description of a silicon detector element.
virtual const SiDetectorDesign & design() const override final
access to the local description (inline):
double phiPitch() const
Pitch (inline methods).
double sinStereoLocal(const Amg::Vector2D &localPos) const
Angle of strip in local frame with respect to the etaAxis.
Class to represent a position in the natural frame of a silicon sensor, for Pixel and SCT For Pixel: ...
SiCellId cellIdOfPosition(const Amg::Vector2D &localPos) const
As in previous method but returns SiCellId.
virtual IdentifierHash identifyHash() const override final
identifier hash (inline)
virtual Identifier identify() const override final
identifier of this detector element (inline)
Trk::Surface & surface()
Element Surface.
double phiPitchPhi(const SiLocalPosition &localPosition) const
SiLocalPosition localPositionOfCellPC(const SiCellId &cellId) const
This is for debugging only.
virtual Identifier identify() const override final
bool isCellGood(IdentifierHash hash, unsigned short cell_i) const
bool isGood(IdentifierHash hash) const
int getTimeBin() const
virtual int getGroupSize() const =0
Manage lookup of vectors of auxiliary data.
virtual void localToGlobal(const Amg::Vector2D &locp, const Amg::Vector3D &mom, Amg::Vector3D &glob) const =0
Specified by each surface type: LocalToGlobal method without dynamic memory allocation.
Define structures for fast filling of xAOD objects.
VariableStruct(SG::AuxVectorData &cont)
void updateEndIndex(unsigned int obj_i, unsigned int elm_i)
unsigned int getBeginIndex(unsigned int obj_i) const
The AlignStoreProviderAlg loads the rigid alignment corrections and pipes them through the readout ge...
constexpr float oneStripSF
static float computeRotatedLocalCov(const InPlaceClusterization::ClusterProxy< const IStripClusteringTool::CellContainer > &cluster, float localPos, float localCov, const InDetDD::SiDetectorElement &element, const InDetDD::SiDetectorDesign &design)
static InDetDD::SiLocalPosition computeCentrePosition(const InPlaceClusterization::ClusterProxy< const IStripClusteringTool::CellContainer > &cluster, const InDetDD::SiDetectorDesign &design)
constexpr float twoStripSF
constexpr double ONE_TWELFTH
static std::pair< Eigen::Matrix< float, 1, 1 >, Eigen::Matrix< float, 3, 1 > > computePosition(const InPlaceClusterization::ClusterProxy< const IStripClusteringTool::CellContainer > &cluster, const InDetDD::SiDetectorElement &element, const InDetDD::SiDetectorDesign &design, double lorentzShift, bool isITk)
void for_each_cluster(cell_collection_t &cells, func_t func)
call the given function for each cluster of a label sorted cell collection.
Eigen::Matrix< double, 2, 1 > Vector2D
ICaloAffectedTool is abstract interface for tools checking if 4 mom is in calo affected region.
StripCluster_v1 StripCluster
Define the version of the strip cluster class.
StripClusterContainer_v1 StripClusterContainer
Define the version of the strip cluster container.
void registerNewCluster(index_t cell_begin_idx, index_t cell_end_idx)
void emplace_back_cell(const std::array< coordinates_t, NDIM > &the_coordinates, index_t src_index)
void registerClustersForNewModule(const ClusterRange &a_range)
ModuleRangeGuard startNewModule(unsigned int id_hash)
StripAuxDataCache(SG::AuxVectorData &cont, unsigned int n_cluster_rdos)
xAOD::xAODInDetMeasurement::Utilities::JaggedVecEltCache< Identifier::value_type > rdoList
std::size_t size() const
Default implementation to compute the number of elements this proxy container contains/refers to The ...
auto front() const
Get a proxy for the first child element (read-only) The operation is undefined if there are no child ...
auto back() const
Get a proxy for the last child element (read-only).