Skip to content

Commit b3df3d3

Browse files
abhatfnalHaiwangYu
authored andcommitted
clus, iface, root: extend SCECorrection to transverse (y, z) displacements
Adds the transverse components to the SCECorrection chain introduced in this PR. ISCEField gains optional displacement_y / displacement_z [default no-op, so maps without transverse components are unaffected]. SCEFieldTH3 soft-loads the TrueBkwd_Displacement_{Y,Z}_{E,W} histograms and routes all three components through a single shared interp() helper. SCECorrection now outputs the fully-corrected triple. Behavior change [intentional]: SCECorrection output_scope goes from {x_sce, y, z} to {x_sce, y_sce, z_sce}; stored_array_names likewise. Downstream configs that cluster on the SCE scope therefore cluster in fully-corrected 3D. Validated end-to-end on 50 SBND crossing-muon DETSIM events: per-point Dx residual vs the TH3 map is unchanged from the prior baseline [1.83 / 2.23 um E/W], and the new Dy / Dz residuals close at the same interpolation precision -- Dy 2.63 / 1.65 um, Dz 3.35 / 2.99 um; max <= 9.91 um across all three components. Evidence in the companion wcp-porting-validation PR (sbnd-sce-036-stage3a-yz branch).
1 parent 80ca0d1 commit b3df3d3

4 files changed

Lines changed: 145 additions & 56 deletions

File tree

clus/inc/WireCellClus/SCECorrection.h

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
// (typically WireCell::Root::SCEFieldTH3) lives in the root/ subpackage and
77
// is looked up by jsonnet TypeName.
88
//
9-
// Applies:
9+
// Applies (all in WCT mm):
1010
// (1) T0: x_t0 = x_raw - dirx * cluster_t0 * drift_speed
11-
// (2) SCE: x_sce = x_t0 + field->displacement_x(apa, x_t0, y, z)
11+
// (2) SCE: x_sce = x_t0 + field->displacement_x(apa, x_t0, y, z)
12+
// y_sce = y + field->displacement_y(apa, x_t0, y, z)
13+
// z_sce = z + field->displacement_z(apa, x_t0, y, z)
1214
//
1315
// East/West (apa==0/1) routing is handled inside the ISCEField implementation.
14-
// If no field is provided (nullptr), the SCE step is a no-op (T0 still applied).
16+
// If no field is provided (nullptr), the SCE step is a no-op (T0 still applied
17+
// to x; y, z pass through).
1518
//
1619
// Author: Avinay Bhat (UChicago), for SBND
1720
// Modeled on T0Correction by Haiwang Yu.
@@ -53,24 +56,30 @@ namespace WireCell::Clus {
5356
double cluster_t0, int face, int apa) const override;
5457

5558
virtual PointCloud::Tree::Scope output_scope() const override {
56-
return {"3d", {"x_sce", "y", "z"}};
59+
return {"3d", {"x_sce", "y_sce", "z_sce"}};
5760
}
5861
virtual std::vector<std::string> stored_array_names() const override {
59-
return {"x_sce"};
62+
return {"x_sce", "y_sce", "z_sce"};
6063
}
6164

6265
private:
6366
IDetectorVolumes::pointer m_dv;
6467
WireCell::ISCEField::pointer m_field;
6568
std::map<int, std::map<int, double>> m_drift_speeds;
6669

67-
// Combined T0 + SCE for one X coordinate. All quantities in WCT mm.
68-
inline double corrected_x(double x_raw_mm, double y_mm, double z_mm,
69-
double cluster_t0, int face, int apa) const {
70+
// Combined T0 + full 3D SCE for one point. All quantities in WCT mm.
71+
inline void corrected_xyz(double x_raw_mm, double y_mm, double z_mm,
72+
double cluster_t0, int face, int apa,
73+
double& xs, double& ys, double& zs) const {
7074
const double dirx = m_dv->face_dirx(WirePlaneId(kAllLayers, face, apa));
7175
const double x_t0_mm = x_raw_mm - dirx * cluster_t0 * m_drift_speeds.at(apa).at(face);
72-
const double dx_mm = m_field ? m_field->displacement_x(apa, x_t0_mm, y_mm, z_mm) : 0.0;
73-
return x_t0_mm + dx_mm;
76+
if (m_field) {
77+
xs = x_t0_mm + m_field->displacement_x(apa, x_t0_mm, y_mm, z_mm);
78+
ys = y_mm + m_field->displacement_y(apa, x_t0_mm, y_mm, z_mm);
79+
zs = z_mm + m_field->displacement_z(apa, x_t0_mm, y_mm, z_mm);
80+
} else {
81+
xs = x_t0_mm; ys = y_mm; zs = z_mm;
82+
}
7483
}
7584
};
7685

@@ -83,21 +92,23 @@ namespace WireCell::Clus {
8392
const auto md = m_dv->metadata(wpid);
8493
m_drift_speeds[wpid.apa()][wpid.face()] = md["drift_speed"].asDouble();
8594
}
86-
if (m_field) spdlog::info("SCECorrection: ISCEField wired in");
95+
if (m_field) spdlog::info("SCECorrection: ISCEField wired in (x,y,z)");
8796
else spdlog::info("SCECorrection: no ISCEField -- SCE disabled (T0 still applied)");
8897
}
8998

9099
inline Point SCECorrection::forward(const Point& pos_raw, double t0, int face, int apa) const {
91100
Point pc(pos_raw);
92-
pc[0] = corrected_x(pos_raw[0], pos_raw[1], pos_raw[2], t0, face, apa);
101+
corrected_xyz(pos_raw[0], pos_raw[1], pos_raw[2], t0, face, apa, pc[0], pc[1], pc[2]);
93102
return pc;
94103
}
95104

96105
inline Point SCECorrection::backward(const Point& pos_cor, double t0, int face, int apa) const {
97106
Point pr(pos_cor);
98107
if (m_field) {
99-
// Approximate: evaluate field at x_sce instead of x_t0 (small displacement OK).
108+
// Approximate: evaluate field at the corrected position.
100109
pr[0] -= m_field->displacement_x(apa, pos_cor[0], pos_cor[1], pos_cor[2]);
110+
pr[1] -= m_field->displacement_y(apa, pos_cor[0], pos_cor[1], pos_cor[2]);
111+
pr[2] -= m_field->displacement_z(apa, pos_cor[0], pos_cor[1], pos_cor[2]);
101112
}
102113
const double dirx = m_dv->face_dirx(WirePlaneId(kAllLayers, face, apa));
103114
pr[0] += dirx * t0 * m_drift_speeds.at(apa).at(face);
@@ -117,14 +128,15 @@ namespace WireCell::Clus {
117128
const auto& ax = pc_raw.get(arr_raw_names[0])->elements<double>();
118129
const auto& ay = pc_raw.get(arr_raw_names[1])->elements<double>();
119130
const auto& az = pc_raw.get(arr_raw_names[2])->elements<double>();
120-
std::vector<double> ax_c(ax.size());
131+
std::vector<double> ax_c(ax.size()), ay_c(ax.size()), az_c(ax.size());
121132
for (size_t i = 0; i < ax.size(); ++i) {
122-
ax_c[i] = corrected_x(ax[i], ay[i], az[i], t0, face, apa);
133+
corrected_xyz(ax[i], ay[i], az[i], t0, face, apa,
134+
ax_c[i], ay_c[i], az_c[i]);
123135
}
124136
Dataset ds;
125-
ds.add(arr_cor_names[0], Array(ax_c));
126-
ds.add(arr_cor_names[1], Array(ay));
127-
ds.add(arr_cor_names[2], Array(az));
137+
ds.add(arr_cor_names[0], Array(ax_c)); // x_sce
138+
ds.add(arr_cor_names[1], Array(ay_c)); // y_sce
139+
ds.add(arr_cor_names[2], Array(az_c)); // z_sce
128140
return ds;
129141
}
130142

@@ -138,17 +150,21 @@ namespace WireCell::Clus {
138150
const auto& az = pc_cor.get(arr_cor_names[2])->elements<double>();
139151
const double dirx = m_dv->face_dirx(WirePlaneId(kAllLayers, face, apa));
140152
const double v = m_drift_speeds.at(apa).at(face);
141-
std::vector<double> ax_r(ax.size());
153+
std::vector<double> ax_r(ax.size()), ay_r(ax.size()), az_r(ax.size());
142154
for (size_t i = 0; i < ax.size(); ++i) {
143-
double x = ax[i];
144-
if (m_field) x -= m_field->displacement_x(apa, ax[i], ay[i], az[i]);
155+
double x = ax[i], y = ay[i], z = az[i];
156+
if (m_field) {
157+
x -= m_field->displacement_x(apa, ax[i], ay[i], az[i]);
158+
y -= m_field->displacement_y(apa, ax[i], ay[i], az[i]);
159+
z -= m_field->displacement_z(apa, ax[i], ay[i], az[i]);
160+
}
145161
x += dirx * t0 * v;
146-
ax_r[i] = x;
162+
ax_r[i] = x; ay_r[i] = y; az_r[i] = z;
147163
}
148164
Dataset ds;
149165
ds.add(arr_raw_names[0], Array(ax_r));
150-
ds.add(arr_raw_names[1], Array(ay));
151-
ds.add(arr_raw_names[2], Array(az));
166+
ds.add(arr_raw_names[1], Array(ay_r));
167+
ds.add(arr_raw_names[2], Array(az_r));
152168
return ds;
153169
}
154170

iface/inc/WireCellIface/ISCEField.h

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ namespace WireCell {
88
/** Per-TPC backward (reco -> true) Space-Charge-Effect displacement field.
99
*
1010
* Implementations expose, for a given APA/TPC index and 3D position,
11-
* the SIGNED backward X displacement to be added directly to a t0-corrected
12-
* X coordinate to recover the true X. Out-of-domain queries are clamped
13-
* internally by the implementation. Implementations that carry no map
14-
* should return 0 (no-op safe).
11+
* the SIGNED backward displacement to be added directly to a t0-corrected
12+
* coordinate to recover the true coordinate. Out-of-domain queries are
13+
* clamped internally by the implementation. Implementations that carry no
14+
* map should return 0 (no-op safe).
1515
*
1616
* Units: WCT internal (mm) in, (mm) out. Implementations handle any
1717
* conversion required by their underlying map.
1818
*
19-
* Future revisions may add displacement_y / displacement_z if the
20-
* underlying map provides those components.
19+
* displacement_x is mandatory. displacement_y / displacement_z are
20+
* optional: implementations whose underlying map lacks transverse
21+
* components inherit the default no-op (return 0).
2122
*/
2223
class ISCEField : public IComponent<ISCEField> {
2324
public:
@@ -26,6 +27,12 @@ namespace WireCell {
2627
/// Backward X-displacement at (x,y,z) for APA index `apa`.
2728
/// SBND convention: apa=0 East, apa=1 West. Units: mm.
2829
virtual double displacement_x(int apa, double x, double y, double z) const = 0;
30+
31+
/// Backward Y-displacement at (x,y,z). Units: mm. Optional (default no-op).
32+
virtual double displacement_y(int /*apa*/, double /*x*/, double /*y*/, double /*z*/) const { return 0.0; }
33+
34+
/// Backward Z-displacement at (x,y,z). Units: mm. Optional (default no-op).
35+
virtual double displacement_z(int /*apa*/, double /*x*/, double /*y*/, double /*z*/) const { return 0.0; }
2936
};
3037

3138
} // namespace WireCell

root/inc/WireCellRoot/SCEFieldTH3.h

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/** ISCEField implementation backed by per-TPC TH3F histograms in a
2-
* ROOT file (SBND dualmap layout: TrueBkwd_Displacement_X_E,
3-
* TrueBkwd_Displacement_X_W). IConfigurable so jsonnet wiring works
4-
* the usual way; the actual SCE-correction transform in clus/
5-
* receives an ISCEField::pointer and never sees ROOT.
2+
* ROOT file (SBND dualmap layout: TrueBkwd_Displacement_{X,Y,Z}_{E,W}).
3+
* IConfigurable so jsonnet wiring works the usual way; the actual
4+
* SCE-correction transform in clus/ receives an ISCEField::pointer and
5+
* never sees ROOT.
66
*/
77

88
#ifndef WIRECELLROOT_SCEFIELDTH3
@@ -30,6 +30,8 @@ namespace WireCell {
3030

3131
// ISCEField
3232
virtual double displacement_x(int apa, double x, double y, double z) const override;
33+
virtual double displacement_y(int apa, double x, double y, double z) const override;
34+
virtual double displacement_z(int apa, double x, double y, double z) const override;
3335

3436
// IConfigurable
3537
virtual void configure(const WireCell::Configuration& cfg) override;
@@ -38,19 +40,37 @@ namespace WireCell {
3840
private:
3941
// configuration
4042
std::string m_sce_map_file;
41-
std::string m_th3_name_E;
43+
std::string m_th3_name_E; // X (mandatory)
4244
std::string m_th3_name_W;
45+
std::string m_th3_name_E_y; // Y (optional transverse)
46+
std::string m_th3_name_W_y;
47+
std::string m_th3_name_E_z; // Z (optional transverse)
48+
std::string m_th3_name_W_z;
4349
bool m_axis_unit_mm{false};
4450
double m_sign{-1.0};
4551

46-
// loaded state
52+
// loaded state (all owned by m_file)
4753
std::unique_ptr<TFile> m_file;
48-
TH3F* m_hE{nullptr}; // owned by m_file
54+
TH3F* m_hE{nullptr};
4955
TH3F* m_hW{nullptr};
56+
TH3F* m_hE_y{nullptr};
57+
TH3F* m_hW_y{nullptr};
58+
TH3F* m_hE_z{nullptr};
59+
TH3F* m_hW_z{nullptr};
5060

5161
Log::logptr_t l;
5262

63+
// Shared: pick E/W histo by apa, clamp, convert units, apply sign.
64+
// Returns 0 if the requested histo is absent (no-op component).
65+
double interp(TH3F* hE, TH3F* hW, int apa,
66+
double x, double y, double z) const;
67+
5368
static double clamp_axis(const TAxis* a, double v);
69+
70+
// Load one (E,W) histo pair; hard=true throws on missing,
71+
// hard=false warns and leaves pointers null.
72+
void load_pair(const std::string& nameE, const std::string& nameW,
73+
TH3F*& hE, TH3F*& hW, bool hard);
5474
};
5575

5676
} // namespace Root

root/src/SCEFieldTH3.cxx

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ using namespace WireCell::Root;
1616
SCEFieldTH3::SCEFieldTH3()
1717
: m_th3_name_E("TrueBkwd_Displacement_X_E"),
1818
m_th3_name_W("TrueBkwd_Displacement_X_W"),
19+
m_th3_name_E_y("TrueBkwd_Displacement_Y_E"),
20+
m_th3_name_W_y("TrueBkwd_Displacement_Y_W"),
21+
m_th3_name_E_z("TrueBkwd_Displacement_Z_E"),
22+
m_th3_name_W_z("TrueBkwd_Displacement_Z_W"),
1923
l(Log::logger("wct"))
2024
{
2125
}
@@ -25,19 +29,46 @@ SCEFieldTH3::~SCEFieldTH3() = default;
2529
Configuration SCEFieldTH3::default_configuration() const
2630
{
2731
Configuration cfg;
28-
cfg["sce_map_file"] = m_sce_map_file;
29-
cfg["th3_name_E"] = m_th3_name_E;
30-
cfg["th3_name_W"] = m_th3_name_W;
31-
cfg["axis_unit_mm"] = m_axis_unit_mm;
32-
cfg["sign"] = m_sign;
32+
cfg["sce_map_file"] = m_sce_map_file;
33+
cfg["th3_name_E"] = m_th3_name_E;
34+
cfg["th3_name_W"] = m_th3_name_W;
35+
cfg["th3_name_E_y"] = m_th3_name_E_y;
36+
cfg["th3_name_W_y"] = m_th3_name_W_y;
37+
cfg["th3_name_E_z"] = m_th3_name_E_z;
38+
cfg["th3_name_W_z"] = m_th3_name_W_z;
39+
cfg["axis_unit_mm"] = m_axis_unit_mm;
40+
cfg["sign"] = m_sign;
3341
return cfg;
3442
}
3543

44+
void SCEFieldTH3::load_pair(const std::string& nameE, const std::string& nameW,
45+
TH3F*& hE, TH3F*& hW, bool hard)
46+
{
47+
hE = dynamic_cast<TH3F*>(m_file->Get(nameE.c_str()));
48+
hW = dynamic_cast<TH3F*>(m_file->Get(nameW.c_str()));
49+
if (!hE || !hW) {
50+
if (hard) {
51+
throw std::runtime_error("SCEFieldTH3: missing TH3F '" + nameE
52+
+ "' or '" + nameW + "' in " + m_sce_map_file);
53+
}
54+
l->warn("SCEFieldTH3: optional TH3 '{}' / '{}' absent -- that component is a no-op",
55+
nameE, nameW);
56+
hE = nullptr; hW = nullptr;
57+
return;
58+
}
59+
hE->SetDirectory(nullptr);
60+
hW->SetDirectory(nullptr);
61+
}
62+
3663
void SCEFieldTH3::configure(const Configuration& cfg)
3764
{
3865
m_sce_map_file = get<std::string>(cfg, "sce_map_file", m_sce_map_file);
3966
m_th3_name_E = get<std::string>(cfg, "th3_name_E", m_th3_name_E);
4067
m_th3_name_W = get<std::string>(cfg, "th3_name_W", m_th3_name_W);
68+
m_th3_name_E_y = get<std::string>(cfg, "th3_name_E_y", m_th3_name_E_y);
69+
m_th3_name_W_y = get<std::string>(cfg, "th3_name_W_y", m_th3_name_W_y);
70+
m_th3_name_E_z = get<std::string>(cfg, "th3_name_E_z", m_th3_name_E_z);
71+
m_th3_name_W_z = get<std::string>(cfg, "th3_name_W_z", m_th3_name_W_z);
4172
m_axis_unit_mm = get<bool> (cfg, "axis_unit_mm", m_axis_unit_mm);
4273
m_sign = get<double> (cfg, "sign", m_sign);
4374

@@ -51,16 +82,15 @@ void SCEFieldTH3::configure(const Configuration& cfg)
5182
if (!m_file || m_file->IsZombie()) {
5283
throw std::runtime_error("SCEFieldTH3: cannot open " + m_sce_map_file);
5384
}
54-
m_hE = dynamic_cast<TH3F*>(m_file->Get(m_th3_name_E.c_str()));
55-
m_hW = dynamic_cast<TH3F*>(m_file->Get(m_th3_name_W.c_str()));
56-
if (!m_hE || !m_hW) {
57-
throw std::runtime_error("SCEFieldTH3: missing TH3F '" + m_th3_name_E
58-
+ "' or '" + m_th3_name_W + "' in " + m_sce_map_file);
59-
}
60-
m_hE->SetDirectory(nullptr);
61-
m_hW->SetDirectory(nullptr);
62-
l->info("SCEFieldTH3: loaded TH3 E='{}' W='{}' axis_mm={} sign={}",
63-
m_th3_name_E, m_th3_name_W, m_axis_unit_mm, m_sign);
85+
86+
load_pair(m_th3_name_E, m_th3_name_W, m_hE, m_hW, /*hard=*/true);
87+
load_pair(m_th3_name_E_y, m_th3_name_W_y, m_hE_y, m_hW_y, /*hard=*/false);
88+
load_pair(m_th3_name_E_z, m_th3_name_W_z, m_hE_z, m_hW_z, /*hard=*/false);
89+
90+
l->info("SCEFieldTH3: loaded X[E='{}' W='{}'] Y[{}] Z[{}] axis_mm={} sign={}",
91+
m_th3_name_E, m_th3_name_W,
92+
(m_hE_y ? "ok" : "absent"), (m_hE_z ? "ok" : "absent"),
93+
m_axis_unit_mm, m_sign);
6494
}
6595

6696
double SCEFieldTH3::clamp_axis(const TAxis* a, double v)
@@ -73,9 +103,10 @@ double SCEFieldTH3::clamp_axis(const TAxis* a, double v)
73103
return v;
74104
}
75105

76-
double SCEFieldTH3::displacement_x(int apa, double x, double y, double z) const
106+
double SCEFieldTH3::interp(TH3F* hE, TH3F* hW, int apa,
107+
double x, double y, double z) const
77108
{
78-
TH3F* h = (apa == 0) ? m_hE : (apa == 1) ? m_hW : nullptr;
109+
TH3F* h = (apa == 0) ? hE : (apa == 1) ? hW : nullptr;
79110
if (!h) return 0.0;
80111

81112
// WCT input is mm; map axes are cm by default (mm if m_axis_unit_mm).
@@ -85,7 +116,22 @@ double SCEFieldTH3::displacement_x(int apa, double x, double y, double z) const
85116
const double xq = clamp_axis(h->GetXaxis(), x * s_in);
86117
const double yq = clamp_axis(h->GetYaxis(), y * s_in);
87118
const double zq = clamp_axis(h->GetZaxis(), z * s_in);
88-
const double dx_map = h->Interpolate(xq, yq, zq);
119+
const double d_map = h->Interpolate(xq, yq, zq);
89120

90-
return m_sign * dx_map * s_out; // signed displacement in mm
121+
return m_sign * d_map * s_out; // signed displacement in mm
122+
}
123+
124+
double SCEFieldTH3::displacement_x(int apa, double x, double y, double z) const
125+
{
126+
return interp(m_hE, m_hW, apa, x, y, z);
127+
}
128+
129+
double SCEFieldTH3::displacement_y(int apa, double x, double y, double z) const
130+
{
131+
return interp(m_hE_y, m_hW_y, apa, x, y, z);
132+
}
133+
134+
double SCEFieldTH3::displacement_z(int apa, double x, double y, double z) const
135+
{
136+
return interp(m_hE_z, m_hW_z, apa, x, y, z);
91137
}

0 commit comments

Comments
 (0)