Skip to content

Commit d238d41

Browse files
authored
TPCFastTransformation: Resolve recursion at compile time with templates. (#14462)
* TPCFastTransformation: Resolve recursion at compile time with templates. * TPCFastTransformation: Fix runtime parameters on CPU for polynoms. * Fix failing unittest.
1 parent bbb5bb8 commit d238d41

File tree

6 files changed

+157
-45
lines changed

6 files changed

+157
-45
lines changed

GPU/Common/GPUCommonDefAPI.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
#define GPUdDefault()
8080
#define GPUhdDefault()
8181
#define GPUdi() inline
82-
#define GPUdii() inline
82+
#define GPUdii() __attribute__((always_inline)) inline
8383
#define GPUdni()
8484
#define GPUdnii()
8585
#define GPUh() INVALID_TRIGGER_ERROR_NO_HOST_CODE

GPU/GPUTracking/DataTypes/CalibdEdxTrackTopologyPol.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ class CalibdEdxTrackTopologyPol : public o2::gpu::FlatObject
6262
/// \param region region of the TPC
6363
/// \param charge correction for maximum or total charge
6464
/// \param x coordinates where the correction is evaluated
65-
GPUd() float getCorrection(const int32_t region, const ChargeType charge, float x[/*inpXdim*/]) const { return (charge == ChargeType::Tot) ? mCalibPolsqTot[region].eval(x) : mCalibPolsqMax[region].eval(x); }
65+
GPUd() float getCorrection(const int32_t region, const ChargeType charge, float x[/*inpXdim*/]) const
66+
{
67+
return (charge == ChargeType::Tot) ? mCalibPolsqTot[region].eval(x) : mCalibPolsqMax[region].eval(x);
68+
}
6669

6770
/// \return returns the track topology correction
6871
/// \param region region of the TPC

GPU/TPCFastTransformation/MultivariatePolynomial.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class MultivariatePolynomial : public FlatObject, public MultivariatePolynomialH
5656

5757
/// constructor for compile time evaluation of polynomial formula
5858
template <bool IsEnabled = true, typename std::enable_if<(IsEnabled && (Dim != 0 && Degree != 0)), int32_t>::type = 0>
59-
MultivariatePolynomial() : mNParams{this->getNParameters(Degree, Dim, InteractionOnly)}
59+
MultivariatePolynomial() : mNParams{this->template getNParameters<Degree, Dim, InteractionOnly>()}
6060
{
6161
construct();
6262
}

GPU/TPCFastTransformation/MultivariatePolynomialHelper.h

Lines changed: 131 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,63 @@ struct MultivariatePolynomialContainer {
5757
class MultivariatePolynomialParametersHelper
5858
{
5959
public:
60+
/// \returns number of parameters for given dimension and degree of polynomials at compile time
61+
/// calculates the number of parameters for a multivariate polynomial for given degree: nParameters = (n+d-1 d) -> binomial coefficient
62+
/// see: https://mathoverflow.net/questions/225953/number-of-polynomial-terms-for-certain-degree-and-certain-number-of-variables
63+
template <uint32_t Degree, uint32_t Dim>
64+
GPUd() static constexpr uint32_t getNParametersAllTerms()
65+
{
66+
if constexpr (Degree == 0) {
67+
return binomialCoeff<Dim - 1, 0>();
68+
} else {
69+
return binomialCoeff<Dim - 1 + Degree, Degree>() + getNParametersAllTerms<Degree - 1, Dim>();
70+
}
71+
}
72+
6073
/// \returns number of parameters for given dimension and degree of polynomials
6174
/// calculates the number of parameters for a multivariate polynomial for given degree: nParameters = (n+d-1 d) -> binomial coefficient
6275
/// see: https://mathoverflow.net/questions/225953/number-of-polynomial-terms-for-certain-degree-and-certain-number-of-variables
63-
GPUd() static constexpr uint32_t getNParametersAllTerms(const uint32_t degree, const uint32_t dim) { return (degree == 0) ? binomialCoeff(dim - 1, 0) : binomialCoeff(dim - 1 + degree, degree) + getNParametersAllTerms(degree - 1, dim); }
76+
GPUd() static constexpr uint32_t getNParametersAllTerms(uint32_t degree, uint32_t dim)
77+
{
78+
if (degree == 0) {
79+
return binomialCoeff(dim - 1, 0);
80+
} else {
81+
return binomialCoeff(dim - 1 + degree, degree) + getNParametersAllTerms(degree - 1, dim);
82+
}
83+
}
84+
85+
/// \returns the number of parameters at compile time for interaction terms only (see: https://en.wikipedia.org/wiki/Combination)
86+
template <uint32_t Degree, uint32_t Dim>
87+
GPUd() static constexpr uint32_t getNParametersInteractionOnly()
88+
{
89+
if constexpr (Degree == 0) {
90+
return binomialCoeff<Dim - 1, 0>();
91+
} else {
92+
return binomialCoeff<Dim, Degree>() + getNParametersInteractionOnly<Degree - 1, Dim>();
93+
}
94+
}
6495

6596
/// \returns the number of parameters for interaction terms only (see: https://en.wikipedia.org/wiki/Combination)
66-
GPUd() static constexpr uint32_t getNParametersInteractionOnly(const uint32_t degree, const uint32_t dim) { return (degree == 0) ? binomialCoeff(dim - 1, 0) : binomialCoeff(dim, degree) + getNParametersInteractionOnly(degree - 1, dim); }
97+
GPUd() static constexpr uint32_t getNParametersInteractionOnly(uint32_t degree, uint32_t dim)
98+
{
99+
if (degree == 0) {
100+
return binomialCoeff(dim - 1, 0);
101+
} else {
102+
return binomialCoeff(dim, degree) + getNParametersInteractionOnly(degree - 1, dim);
103+
}
104+
}
105+
106+
template <uint32_t Degree, uint32_t Dim, bool InteractionOnly>
107+
GPUd() static constexpr uint32_t getNParameters()
108+
{
109+
if constexpr (InteractionOnly) {
110+
return getNParametersInteractionOnly<Degree, Dim>();
111+
} else {
112+
return getNParametersAllTerms<Degree, Dim>();
113+
}
114+
}
67115

68-
GPUd() static constexpr uint32_t getNParameters(const uint32_t degree, const uint32_t dim, const bool interactionOnly)
116+
GPUd() static constexpr uint32_t getNParameters(uint32_t degree, uint32_t dim, bool interactionOnly)
69117
{
70118
if (interactionOnly) {
71119
return getNParametersInteractionOnly(degree, dim);
@@ -75,13 +123,36 @@ class MultivariatePolynomialParametersHelper
75123
}
76124

77125
private:
126+
/// calculate factorial of n at compile time
127+
/// \return returns n!
128+
template <uint32_t N>
129+
GPUd() static constexpr uint32_t factorial()
130+
{
131+
if constexpr (N == 0 || N == 1) {
132+
return 1;
133+
} else {
134+
return N * factorial<N - 1>();
135+
}
136+
}
137+
78138
/// calculate factorial of n
79139
/// \return returns n!
80-
GPUd() static constexpr uint32_t factorial(const uint32_t n) { return (n == 0) || (n == 1) ? 1 : n * factorial(n - 1); }
140+
GPUd() static constexpr uint32_t factorial(uint32_t n) { return n == 0 || n == 1 ? 1 : n * factorial(n - 1); }
141+
142+
/// calculates binomial coefficient at compile time
143+
/// \return returns (n k)
144+
template <uint32_t N, uint32_t K>
145+
GPUd() static constexpr uint32_t binomialCoeff()
146+
{
147+
return factorial<N>() / (factorial<K>() * factorial<N - K>());
148+
}
81149

82150
/// calculates binomial coefficient
83151
/// \return returns (n k)
84-
GPUd() static constexpr uint32_t binomialCoeff(const uint32_t n, const uint32_t k) { return factorial(n) / (factorial(k) * factorial(n - k)); }
152+
GPUd() static constexpr uint32_t binomialCoeff(uint32_t n, uint32_t k)
153+
{
154+
return factorial(n) / (factorial(k) * factorial(n - k));
155+
}
85156
};
86157

87158
/// Helper struct for evaluating a multidimensional polynomial using compile time evaluated formula
@@ -103,7 +174,10 @@ class MultivariatePolynomialHelper : public MultivariatePolynomialParametersHelp
103174
/// evaluates the polynomial for given parameters and coordinates
104175
/// \param par parameters of the polynomials
105176
/// \param x input coordinates
106-
GPUd() static constexpr float evalPol(GPUgeneric() const float par[/*number of parameters*/], const float x[/*number of dimensions*/]) { return par[0] + loopDegrees<1>(par, x); }
177+
GPUd() static constexpr float evalPol(GPUgeneric() const float par[/*number of parameters*/], const float x[/*number of dimensions*/])
178+
{
179+
return par[0] + loopDegrees<1>(par, x);
180+
}
107181

108182
/// \return returns number of dimensions of the polynomials
109183
GPUd() static constexpr uint32_t getDim() { return Dim; }
@@ -118,19 +192,36 @@ class MultivariatePolynomialHelper : public MultivariatePolynomialParametersHelp
118192
/// computes power of 10
119193
GPUd() static constexpr uint32_t pow10(const uint32_t n) { return n == 0 ? 1 : 10 * pow10(n - 1); }
120194

195+
template <uint32_t N>
196+
GPUd() static constexpr uint32_t pow10()
197+
{
198+
if constexpr (N == 0) {
199+
return 1;
200+
} else {
201+
return 10 * pow10<N - 1>();
202+
}
203+
}
204+
121205
/// helper for modulo to extract the digit in an integer a at position b (can be obtained with pow10(digitposition)): e.g. a=1234 b=pow10(2)=100 -> returns 2
122206
GPUd() static constexpr uint32_t mod10(const uint32_t a, const uint32_t b) { return (a / b) % 10; }
123207

208+
template <uint32_t A, uint32_t B>
209+
GPUd() static constexpr uint32_t mod10()
210+
{
211+
return (A / B) % 10;
212+
}
213+
124214
/// resetting digits of pos for given position to refDigit
125215
GPUd() static constexpr uint32_t resetIndices(const uint32_t degreePol, const uint32_t pos, const uint32_t leftDigit, const uint32_t iter, const uint32_t refDigit);
126216

127-
GPUd() static constexpr uint32_t getNewPos(const uint32_t degreePol, const uint32_t pos, const uint32_t digitPos);
217+
template <uint32_t DegreePol, uint32_t Pos, uint32_t DigitPos>
218+
GPUd() static constexpr uint32_t getNewPos();
128219

129220
/// calculates term e.g. x^3*y
130221
/// \tparam DegreePol max degree of the polynomials
131222
/// \pos decoded information about the current term e.g. 1233 -> x[1]*x[2]*x[3]*x[3] (otherwise an array could be used)
132-
template <uint32_t DegreePol>
133-
GPUd() static constexpr float prodTerm(const float x[], const uint32_t pos);
223+
template <uint32_t DegreePol, uint32_t Pos>
224+
GPUd() static constexpr float prodTerm(const float x[]);
134225

135226
/// helper function for checking for interaction terms
136227
template <uint32_t DegreePol, uint32_t posNew>
@@ -203,7 +294,10 @@ class MultivariatePolynomialHelper<0, 0, false> : public MultivariatePolynomialP
203294
/// evaluating the polynomial
204295
/// \param par coefficients of the polynomial
205296
/// \param x input coordinates
206-
float evalPol(const float par[/*number of parameters*/], const float x[/*number of dimensions*/]) const { return evalPol(par, x, mDegree, mDim, mInteractionOnly); }
297+
float evalPol(const float par[/*number of parameters*/], const float x[/*number of dimensions*/]) const
298+
{
299+
return evalPol(par, x, mDegree, mDim, mInteractionOnly);
300+
}
207301

208302
/// evalutes the polynomial
209303
float evalPol(const float par[], const float x[], const uint32_t degree, const uint32_t dim, const bool interactionOnly) const;
@@ -248,35 +342,39 @@ GPUd() constexpr uint32_t MultivariatePolynomialHelper<Dim, Degree, InteractionO
248342
}
249343

250344
template <uint32_t Dim, uint32_t Degree, bool InteractionOnly>
251-
GPUd() constexpr uint32_t MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::getNewPos(const uint32_t degreePol, const uint32_t pos, const uint32_t digitPos)
345+
template <uint32_t DegreePol, uint32_t Pos, uint32_t DigitPos>
346+
GPUd() constexpr uint32_t MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::getNewPos()
252347
{
253-
if (degreePol > digitPos) {
348+
if constexpr (DegreePol > DigitPos) {
254349
// check if digit of current position is at is max position
255-
if (mod10(pos, pow10(digitPos)) == Dim) {
350+
if constexpr (mod10<Pos, pow10<DigitPos>()>() == Dim) {
256351
// increase digit of left position
257-
const uint32_t leftDigit = digitPos + 1;
258-
const uint32_t posTmp = pos + pow10(leftDigit);
259-
const uint32_t refDigit = mod10(posTmp, pow10(digitPos + 1));
352+
constexpr uint32_t LeftDigit = DigitPos + 1;
353+
constexpr uint32_t PowLeftDigit = pow10<LeftDigit>();
354+
constexpr uint32_t PosTmp = Pos + PowLeftDigit;
355+
constexpr uint32_t RefDigit = mod10<PosTmp, PowLeftDigit>();
260356

261357
// resetting digits to the right if digit exceeds number of dimensions
262-
const uint32_t posReset = resetIndices(degreePol, posTmp, leftDigit - 1, degreePol - digitPos, refDigit);
358+
constexpr uint32_t PosReset = resetIndices(DegreePol, PosTmp, LeftDigit - 1, DegreePol - DigitPos, RefDigit);
263359

264360
// check next digit
265-
return getNewPos(degreePol, posReset, digitPos + 1);
361+
return getNewPos<DegreePol, PosReset, DigitPos + 1>();
362+
} else {
363+
return getNewPos<DegreePol, Pos, DigitPos + 1>();
266364
}
267-
return getNewPos(degreePol, pos, digitPos + 1);
365+
} else {
366+
return Pos;
268367
}
269-
return pos;
270368
}
271369

272370
template <uint32_t Dim, uint32_t Degree, bool InteractionOnly>
273-
template <uint32_t DegreePol>
274-
GPUd() constexpr float MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::prodTerm(const float x[], const uint32_t pos)
371+
template <uint32_t DegreePol, uint32_t Pos>
372+
GPUd() constexpr float MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::prodTerm(const float x[])
275373
{
276374
if constexpr (DegreePol > 0) {
277375
// extract index of the dimension which is decoded in the digit
278-
const uint32_t index = mod10(pos, pow10(DegreePol - 1));
279-
return x[index] * prodTerm<DegreePol - 1>(x, pos);
376+
const uint32_t index = mod10<Pos, pow10<DegreePol - 1>()>();
377+
return x[index] * prodTerm<DegreePol - 1, Pos>(x);
280378
}
281379
return 1;
282380
}
@@ -286,7 +384,7 @@ template <uint32_t DegreePol, uint32_t posNew>
286384
constexpr bool MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::checkInteraction()
287385
{
288386
if constexpr (DegreePol > 1) {
289-
constexpr bool isInteraction = mod10(posNew, pow10(DegreePol - 1)) == mod10(posNew, pow10(DegreePol - 2));
387+
constexpr bool isInteraction = mod10<posNew, pow10<DegreePol - 1>()>() == mod10<posNew, pow10<DegreePol - 2>()>();
290388
if constexpr (isInteraction) {
291389
return true;
292390
}
@@ -300,16 +398,16 @@ template <uint32_t DegreePol, uint32_t Pos, uint32_t Index>
300398
GPUd() constexpr float MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::sumTerms(GPUgeneric() const float par[], const float x[])
301399
{
302400
// checking if the current position is reasonable e.g. if the max dimension is x[4]: for Pos=15 -> x[1]*x[5] the position is set to 22 -> x[2]*x[2]
303-
constexpr uint32_t posNew = getNewPos(DegreePol, Pos, 0);
304-
if constexpr (mod10(posNew, pow10(DegreePol)) != 1) {
401+
constexpr uint32_t PosNew = getNewPos<DegreePol, Pos, 0>();
402+
if constexpr (mod10<PosNew, pow10<DegreePol>()>() != 1) {
305403

306404
// check if all digits in posNew are unequal: For interaction_only terms with x[Dim]*x[Dim]... etc. can be skipped
307-
if constexpr (InteractionOnly && checkInteraction<DegreePol, posNew>()) {
308-
return sumTerms<DegreePol, posNew + 1, Index>(par, x);
405+
if constexpr (InteractionOnly && checkInteraction<DegreePol, PosNew>()) {
406+
return sumTerms<DegreePol, PosNew + 1, Index>(par, x);
407+
} else {
408+
// sum up the term for corrent term and set posotion for next combination
409+
return par[Index] * prodTerm<DegreePol, PosNew>(x) + sumTerms<DegreePol, PosNew + 1, Index + 1>(par, x);
309410
}
310-
311-
// sum up the term for corrent term and set posotion for next combination
312-
return par[Index] * prodTerm<DegreePol>(x, posNew) + sumTerms<DegreePol, posNew + 1, Index + 1>(par, x);
313411
}
314412
return 0;
315413
}
@@ -319,7 +417,7 @@ template <uint32_t DegreePol>
319417
GPUd() constexpr float MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::loopDegrees(GPUgeneric() const float par[], const float x[])
320418
{
321419
if constexpr (DegreePol <= Degree) {
322-
constexpr uint32_t index{getNParameters(DegreePol - 1, Dim, InteractionOnly)}; // offset of the index for accessing the parameters
420+
constexpr uint32_t index{getNParameters<DegreePol - 1, Dim, InteractionOnly>()}; // offset of the index for accessing the parameters
323421
return sumTerms<DegreePol, 0, index>(par, x) + loopDegrees<DegreePol + 1>(par, x);
324422
}
325423
return 0;

GPU/TPCFastTransformation/NDPiecewisePolynomials.h

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ class NDPiecewisePolynomials : public FlatObject
141141
/// evaluate specific polynomial at given index for given coordinate
142142
/// \param x coordinates where to interpolate
143143
/// \param index index of the polynomial
144-
GPUd() float evalPol(const float x[/* Dim */], const int32_t index[/* Dim */]) const { return MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::evalPol(getParameters(index), x); }
144+
GPUd() float evalPol(const float x[/* Dim */], const int32_t index[/* Dim */]) const
145+
{
146+
return MultivariatePolynomialHelper<Dim, Degree, InteractionOnly>::evalPol(getParameters(index), x);
147+
}
145148

146149
/// \return returns min range for given dimension
147150
GPUd() float getXMin(const uint32_t dim) const { return mMin[dim]; }
@@ -215,7 +218,7 @@ class NDPiecewisePolynomials : public FlatObject
215218
#endif // !defined(GPUCA_GPUCODE) && !defined(GPUCA_STANDALONE)
216219

217220
/// \return returns the total number of stored parameters
218-
uint32_t getNParameters() const { return getNPolynomials() * MultivariatePolynomialParametersHelper::getNParameters(Degree, Dim, InteractionOnly); }
221+
uint32_t getNParameters() const { return getNPolynomials() * MultivariatePolynomialParametersHelper::getNParameters<Degree, Dim, InteractionOnly>(); }
219222

220223
/// \return returns number of dimensions of the polynomials
221224
GPUd() static constexpr uint32_t getDim() { return Dim; }
@@ -241,11 +244,19 @@ class NDPiecewisePolynomials : public FlatObject
241244

242245
/// returns terms which are needed to calculate the index for the grid for given dimension
243246
/// \param dim dimension
244-
GPUd() uint32_t getTerms(const uint32_t dim) const { return (dim == 0) ? 1 : (mN[dim - 1] - 1) * getTerms(dim - 1); }
247+
template <uint32_t TermDim>
248+
GPUd() uint32_t getTerms() const
249+
{
250+
if constexpr (TermDim == 0) {
251+
return 1;
252+
} else {
253+
return (mN[TermDim - 1] - 1) * getTerms<TermDim - 1>();
254+
}
255+
}
245256

246257
/// returns index for accessing the parameter on the grid
247258
/// \param ix index per dimension
248-
GPUd() uint32_t getDataIndex(const int32_t ix[/* Dim */]) const { return getDataIndex<Dim - 1>(ix) * MultivariatePolynomialParametersHelper::getNParameters(Degree, Dim, InteractionOnly); }
259+
GPUd() uint32_t getDataIndex(const int32_t ix[/* Dim */]) const { return getDataIndex<Dim - 1>(ix) * MultivariatePolynomialParametersHelper::getNParameters<Degree, Dim, InteractionOnly>(); }
249260

250261
/// helper function to get the index
251262
template <uint32_t DimTmp>
@@ -325,7 +336,7 @@ void NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::setFromContainer(cons
325336
template <uint32_t Dim, uint32_t Degree, bool InteractionOnly>
326337
void NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::setDefault()
327338
{
328-
const auto nParamsPerPol = MultivariatePolynomialParametersHelper::getNParameters(Degree, Dim, InteractionOnly);
339+
const auto nParamsPerPol = MultivariatePolynomialParametersHelper::getNParameters<Degree, Dim, InteractionOnly>();
329340
const auto nPols = getNPolynomials();
330341
std::vector<float> params(nParamsPerPol);
331342
params.front() = 1;
@@ -429,10 +440,10 @@ void NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::setFutureBufferAddres
429440

430441
template <uint32_t Dim, uint32_t Degree, bool InteractionOnly>
431442
template <uint32_t DimTmp>
432-
GPUdi() uint32_t NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::getDataIndex(const int32_t ix[/* Dim */]) const
443+
GPUd() uint32_t NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::getDataIndex(const int32_t ix[/* Dim */]) const
433444
{
434445
if constexpr (DimTmp > 0) {
435-
return ix[DimTmp] * getTerms(DimTmp) + getDataIndex<DimTmp - 1>(ix);
446+
return ix[DimTmp] * getTerms<DimTmp>() + getDataIndex<DimTmp - 1>(ix);
436447
}
437448
return ix[DimTmp];
438449
}

GPU/TPCFastTransformation/NDPiecewisePolynomials.inc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ void NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::performFits(const std
165165
// check if data points are in the grid
166166
if (index == indexClamped) {
167167
// index of the polyniomial
168-
const uint32_t idx = getDataIndex(index.data()) / MultivariatePolynomialParametersHelper::getNParameters(Degree, Dim, InteractionOnly);
168+
const uint32_t idx = getDataIndex(index.data()) / MultivariatePolynomialParametersHelper::getNParameters<Degree, Dim, InteractionOnly>();
169169

170170
// store index to data point
171171
dataPointsIndices[idx].emplace_back(i);
@@ -216,7 +216,7 @@ void NDPiecewisePolynomials<Dim, Degree, InteractionOnly>::performFits(const std
216216
const auto params = MultivariatePolynomialHelper<0, 0, false>::fit(fitter, xCords, response, error, true);
217217

218218
// store parameters
219-
std::copy(params.begin(), params.end(), &mParams[i * MultivariatePolynomialParametersHelper::getNParameters(Degree, Dim, InteractionOnly)]);
219+
std::copy(params.begin(), params.end(), &mParams[i * MultivariatePolynomialParametersHelper::getNParameters<Degree, Dim, InteractionOnly>()]);
220220
}
221221
}
222222

0 commit comments

Comments
 (0)