From f3903892f4b3395e9798c011e04aeb96f112523e Mon Sep 17 00:00:00 2001 From: Matthias Kleiner Date: Wed, 21 Oct 2026 15:31:12 +0200 Subject: [PATCH 1/2] TPC: fix the weight scaling of the mixed spline derivative in addCorrections TPCFastSpaceChargeCorrectionHelper::addCorrections scaled the 4th parameter of each spline knot (the mixed derivative d2S/dudv) with the square of the weight: secMainScale * secMainScale for the main correction, ls * ls * ... for the added ones (ls is the weight times the z fade-out factor, 1 at the knots). A spline is linear in its parameters, so scaling the correction by w scales all of them by w. The correction was therefore right at the knots but wrong between them whenever a weight != 1 was used, e.g. the derivative map added with weight lumiScale in TPCScalerSpec::buildMap. The error is proportional to w - w^2. Measured with a production map and the derivative map of run 568041 (random points, vs the weighted point-by-point sum of getCorrectionLocal and of the inverse getters), for map + 0.13 x derivative: rms ~20 um in dx and dz, ~10 um without the first 10 pad rows; deviations above 100 um are rare (0.4 % of the points) and lie mostly in the first pad rows. At a weight of -0.5 the rms is ~150 um (~60 um without the first 10 pad rows). With the fix the sum is exact to float precision (< 0.1 um) for weights from -1 to 1.5, inverse corrections included; w = 1 is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx b/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx index 2ec64765f243a..283d109bbe544 100644 --- a/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx +++ b/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx @@ -1059,7 +1059,8 @@ void TPCFastSpaceChargeCorrectionHelper::addCorrections( constexpr int nKnotPar3d = nKnotPar1d * 3; { // scale the main correction - double parscale[4] = {secMainScale, secMainScale, secMainScale, secMainScale * secMainScale}; + // a spline is linear in its parameters: scaling the function scales all of them, the mixed derivative included + double parscale[4] = {secMainScale, secMainScale, secMainScale, secMainScale}; for (int iknot = 0, ind = 0; iknot < spline.getNumberOfKnots(); iknot++) { for (int ipar = 0; ipar < nKnotPar1d; ++ipar) { for (int idim = 0; idim < 3; idim++, ind++) { @@ -1116,7 +1117,7 @@ void TPCFastSpaceChargeCorrectionHelper::addCorrections( float lu, lv, ls; corr.convLocalToGrid(sector, row, y, z, lu, lv, ls); ls *= scale; - double parscale[4] = {ls, ls * scaleU, ls * scaleV, ls * ls * scaleU * scaleV}; + double parscale[4] = {ls, ls * scaleU, ls * scaleV, ls * scaleU * scaleV}; const auto& spl = corr.getSplineForRow(row); spl.interpolateParametersAtU(corr.getCorrectionData(sector, row), lu, lv, P); for (int ipar = 0, ind = 0; ipar < nKnotPar1d; ++ipar) { @@ -1132,7 +1133,7 @@ void TPCFastSpaceChargeCorrectionHelper::addCorrections( float lu, lv, ls; corr.convRealLocalToGrid(sector, row, y, z, lu, lv, ls); ls *= scale; - double parscale[4] = {ls, ls * scaleRealU, ls * scaleRealV, ls * ls * scaleRealU * scaleRealV}; + double parscale[4] = {ls, ls * scaleRealU, ls * scaleRealV, ls * scaleRealU * scaleRealV}; { // inverse X correction corr.getSplineInvXforRow(row).interpolateParametersAtU(corr.getCorrectionDataInvX(sector, row), lu, lv, P); From cdbce61878493d06b91d3d4640223bf31c636d36 Mon Sep 17 00:00:00 2001 From: Matthias Kleiner Date: Wed, 21 Oct 2026 16:26:35 +0200 Subject: [PATCH 2/2] TPC: fix the chain-rule grid factors in addCorrections A correction added on a different spline grid is sampled at the main grid's knots, and its derivatives must be converted with the chain rule: with u = (y - y0) * yScale, d/du_main = (yScale_corr / yScale_main) d/du_corr, the same for v. addCorrections used the inverse ratios, and applied the u factor to the dS/dv parameter and the v factor to dS/du (knot parameters {S, dS/dv, dS/du, d2S/dudv}). For identical grids (e.g. the derivative map) the factors are 1 and nothing changes. The M-shape map is created on a 4 x 4 grid in TPCScalerSpec, with factors of 35-98 with respect to a production map: adding the M-shape map of run 544797 (up to 6 mm) to a map of run 568041 gave errors of up to 89 cm in dx and up to the kMaxCorrection limit of 100 cm in dy (rms 4 cm and 7 cm), vs the point-by-point sum. With the fix: rms 1-2 um, max 60 um (resampling of non-nested grids) inside the main map's grid. Outside of it (|z| > ~1.37 x for production voxel maps) the merged map follows the edge and z fade-out of the main grid, as before. Co-Authored-By: Claude Opus 5.5 --- .../src/TPCFastSpaceChargeCorrectionHelper.cxx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx b/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx index 283d109bbe544..6b484519ddbcf 100644 --- a/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx +++ b/Detectors/TPC/calibration/src/TPCFastSpaceChargeCorrectionHelper.cxx @@ -1098,10 +1098,11 @@ void TPCFastSpaceChargeCorrectionHelper::addCorrections( auto& linfo = corr.getRowInfo(row); - double scaleU = rowInfo.gridMeasured.getYscale() / linfo.gridMeasured.getYscale(); - double scaleV = rowInfo.gridMeasured.getZscale() / linfo.gridMeasured.getZscale(); - double scaleRealU = rowInfo.gridReal.getYscale() / linfo.gridReal.getYscale(); - double scaleRealV = rowInfo.gridReal.getZscale() / linfo.gridReal.getZscale(); + // chain rule for the derivatives: u = (y - y0) * yScale, v = (z - z0) * zScale => d/du_main = (du_corr / du_main) d/du_corr + double scaleU = linfo.gridMeasured.getYscale() / rowInfo.gridMeasured.getYscale(); + double scaleV = linfo.gridMeasured.getZscale() / rowInfo.gridMeasured.getZscale(); + double scaleRealU = linfo.gridReal.getYscale() / rowInfo.gridReal.getYscale(); + double scaleRealV = linfo.gridReal.getZscale() / rowInfo.gridReal.getZscale(); for (int iu = 0; iu < gridU.getNumberOfKnots(); iu++) { double u = gridU.getKnot(iu).u; @@ -1117,7 +1118,8 @@ void TPCFastSpaceChargeCorrectionHelper::addCorrections( float lu, lv, ls; corr.convLocalToGrid(sector, row, y, z, lu, lv, ls); ls *= scale; - double parscale[4] = {ls, ls * scaleU, ls * scaleV, ls * scaleU * scaleV}; + // knot parameters: {S, dS/dv, dS/du, d2S/dudv} + double parscale[4] = {ls, ls * scaleV, ls * scaleU, ls * scaleU * scaleV}; const auto& spl = corr.getSplineForRow(row); spl.interpolateParametersAtU(corr.getCorrectionData(sector, row), lu, lv, P); for (int ipar = 0, ind = 0; ipar < nKnotPar1d; ++ipar) { @@ -1133,7 +1135,7 @@ void TPCFastSpaceChargeCorrectionHelper::addCorrections( float lu, lv, ls; corr.convRealLocalToGrid(sector, row, y, z, lu, lv, ls); ls *= scale; - double parscale[4] = {ls, ls * scaleRealU, ls * scaleRealV, ls * scaleRealU * scaleRealV}; + double parscale[4] = {ls, ls * scaleRealV, ls * scaleRealU, ls * scaleRealU * scaleRealV}; { // inverse X correction corr.getSplineInvXforRow(row).interpolateParametersAtU(corr.getCorrectionDataInvX(sector, row), lu, lv, P);