Skip to content

Commit

Permalink
Merge 319d2cf into sapling-pr-archive-ktf
Browse files Browse the repository at this point in the history
  • Loading branch information
ktf authored Dec 12, 2024
2 parents bf46d59 + 319d2cf commit 77d637e
Show file tree
Hide file tree
Showing 40 changed files with 2,416 additions and 914 deletions.
19 changes: 17 additions & 2 deletions Common/DCAFitter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
\page refDetectorsVertexing DCAFitter
/doxy -->

## DCAFitterN
# DCAFitterN

Templated class to fit the Point of Closest Approach (PCA) of secondary vertex with N prongs. Allows minimization of either absolute or weighted Distances of Closest Approach (DCA) of N tracks to their common PCA.

Expand Down Expand Up @@ -74,7 +74,22 @@ Extra method `setWeightedFinalPCA(bool)` is provided for the "mixed" mode: if `s
but the final V0 position will be calculated using weighted average. One can also recalculate the V0 position by the weighted average method by calling explicitly
`ft.recalculatePCAWithErrors(int icand=0)`, w/o prior call of `setWeightedFinalPCA(true)`: this will update the position returned by the `getPCACandidate(int cand = 0)`.

The covariance matrix of the V0 position is calculated as an inversed sum of tracks inversed covariances at respective `X_dca` points.
The covariance matrix of the V0 position is calculated as an inverted sum of tracks inversed covariances at respective `X_dca` points.

See ``O2/Common/DCAFitter/test/testDCAFitterN.cxx`` for more extended example.
Currently only 2 and 3 prongs permitted, thought this can be changed by modifying ``DCAFitterN::NMax`` constant.

## Error handling

It may happen that the track propagation to the the proximity of the PCA fails at the various stage of the fit. In this case the fit is abandoned and the failure flag is set, it can be checked using
isPropagationFailure(int cand = 0)` method.

Also, due to the linearization errors the covariance matrix of the track propagated to some point may become non-positive defined.
In this case the relevant correlation coefficient of the cov.matrix is redefined to cure the position part of the cov.matrix and further program flow depends on the user settings for `DCAFitterN::setBadCovPolicy(v)`:

`DCAFitterN::setBadCovPolicy(DCAFitterN::Discard);` : abandon fit (default)

`DCAFitterN::setBadCovPolicy(DCAFitterN::Override);` : continue fit with overridden cov.matrix

`DCAFitterN::setBadCovPolicy(DCAFitterN::OverrideAnFlag);` continue fit with overridden cov.matrix but set the propagation failure flag (can be checked using the same `isPropagationFailure(int cand = 0)` method).

132 changes: 105 additions & 27 deletions Common/DCAFitter/include/DCAFitter/DCAFitterN.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,32 @@ namespace o2
{
namespace vertexing
{

///__________________________________________________________________________________
///< Inverse cov matrix (augmented by a dummy X error) of the point defined by the track
struct TrackCovI {
float sxx, syy, syz, szz;

GPUd() TrackCovI(const o2::track::TrackParCov& trc, float xerrFactor = 1.) { set(trc, xerrFactor); }

GPUdDefault() TrackCovI() = default;

GPUd() void set(const o2::track::TrackParCov& trc, float xerrFactor = 1)
GPUd() bool set(const o2::track::TrackParCov& trc, float xerrFactor = 1.f)
{
// we assign Y error to X for DCA calculation
// (otherwise for quazi-collinear tracks the X will not be constrained)
float cyy = trc.getSigmaY2(), czz = trc.getSigmaZ2(), cyz = trc.getSigmaZY(), cxx = cyy * xerrFactor;
float detYZ = cyy * czz - cyz * cyz;
if (detYZ > 0.) {
auto detYZI = 1. / detYZ;
sxx = 1. / cxx;
syy = czz * detYZI;
syz = -cyz * detYZI;
szz = cyy * detYZI;
} else {
#ifndef GPUCA_GPUCODE
throw std::runtime_error("invalid track covariance");
#else
printf("invalid track covariance\n");
#endif
bool res = true;
if (detYZ <= 0.) {
cyz = o2::gpu::GPUCommonMath::Sqrt(cyy * czz) * (cyz > 0 ? 0.98f : -0.98f);
detYZ = cyy * czz - cyz * cyz;
res = false;
}
auto detYZI = 1. / detYZ;
sxx = 1. / cxx;
syy = czz * detYZI;
syz = -cyz * detYZI;
szz = cyy * detYZI;
return res;
}
};

Expand All @@ -73,6 +71,27 @@ struct TrackDeriv {
}
};

///__________________________________________________________________________
///< Log log-throttling helper
struct LogLogThrottler {
size_t evCount{0};
size_t evCountPrev{0};
size_t logCount{0};

GPUdi() bool needToLog()
{
if (size_t(o2::gpu::GPUCommonMath::Log(++evCount)) + 1 > logCount) {
logCount++;
return true;
}
return false;
}

GPUdi() size_t getNMuted() const { return evCount - evCountPrev - 1; }

GPUdi() void clear() { evCount = evCountPrev = logCount = 0; }
};

template <int N, typename... Args>
class DCAFitterN
{
Expand All @@ -99,6 +118,12 @@ class DCAFitterN
using ArrTrPos = o2::gpu::gpustd::array<Vec3D, N>; // container of Track positions

public:
enum BadCovPolicy { // if encountering non-positive defined cov. matrix, the choice is:
Discard = 0, // stop evaluation
Override = 1, // override correlation coef. to have cov.matrix pos.def and continue
OverrideAndFlag = 2 // override correlation coef. to have cov.matrix pos.def, set mPropFailed flag of corresponding candidate to true and continue (up to the user to check the flag)
};

static constexpr int getNProngs() { return N; }

DCAFitterN() = default;
Expand Down Expand Up @@ -299,6 +324,9 @@ class DCAFitterN
pnt[2] = tr.getZ();
}

void setBadCovPolicy(BadCovPolicy v) { mBadCovPolicy = v; }
BadCovPolicy getBadCovPolicy() const { return mBadCovPolicy; }

private:
// vectors of 1st derivatives of track local residuals over X parameters
o2::gpu::gpustd::array<o2::gpu::gpustd::array<Vec3D, N>, N> mDResidDx;
Expand All @@ -324,11 +352,15 @@ class DCAFitterN
o2::gpu::gpustd::array<int, MAXHYP> mNIters; // number of iterations for each seed
o2::gpu::gpustd::array<bool, MAXHYP> mTrPropDone{}; // Flag that the tracks are fully propagated to PCA
o2::gpu::gpustd::array<bool, MAXHYP> mPropFailed{}; // Flag that some propagation failed for this PCA candidate
LogLogThrottler mLoggerBadCov{};
LogLogThrottler mLoggerBadInv{};
LogLogThrottler mLoggerBadProp{};
MatSym3D mWeightInv; // inverse weight of single track, [sum{M^T E M}]^-1 in EQ.T
o2::gpu::gpustd::array<int, MAXHYP> mOrder{0};
int mCurHyp = 0;
int mCrossIDCur = 0;
int mCrossIDAlt = -1;
BadCovPolicy mBadCovPolicy{BadCovPolicy::Discard}; // what to do in case of non-pos-def. cov. matrix, see BadCovPolicy enum
bool mAllowAltPreference = true; // if the fit converges to alternative PCA seed, abandon the current one
bool mUseAbsDCA = false; // use abs. distance minimization rather than chi2
bool mWeightedFinalPCA = false; // recalculate PCA as a cov-matrix weighted mean, even if absDCA method was used
Expand Down Expand Up @@ -677,7 +709,23 @@ GPUd() bool DCAFitterN<N, Args...>::recalculatePCAWithErrors(int cand)
mCurHyp = mOrder[cand];
if (mUseAbsDCA) {
for (int i = N; i--;) {
mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i], XerrFactor); // prepare inverse cov.matrices at starting point
if (!mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i], XerrFactor)) { // prepare inverse cov.matrices at starting point
if (mLoggerBadCov.needToLog()) {
#ifndef GPUCA_GPUCODE
printf("fitter %d: error (%ld muted): overrode invalid track covariance from %s\n",
mFitterID, mLoggerBadCov.getNMuted(), mCandTr[mCurHyp][i].asString().c_str());
#else
printf("fitter %d: error (%ld muted): overrode invalid track covariance cyy:%e czz:%e cyz:%e\n",
mFitterID, mLoggerBadCov.getNMuted(), mCandTr[mCurHyp][i].getSigmaY2(), mCandTr[mCurHyp][i].getSigmaZ2(), mCandTr[mCurHyp][i].getSigmaZY());
#endif
mLoggerBadCov.evCountPrev = mLoggerBadCov.evCount;
}
if (mBadCovPolicy == Discard) {
return false;
} else if (mBadCovPolicy == OverrideAndFlag) {
mPropFailed[mCurHyp] = true;
} // otherwise, just use overridden errors w/o flagging
}
}
if (!calcPCACoefs()) {
mCurHyp = saveCurHyp;
Expand Down Expand Up @@ -884,7 +932,23 @@ GPUd() bool DCAFitterN<N, Args...>::minimizeChi2()
return false;
}
setTrackPos(mTrPos[mCurHyp][i], mCandTr[mCurHyp][i]); // prepare positions
mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i], XerrFactor); // prepare inverse cov.matrices at starting point
if (!mTrcEInv[mCurHyp][i].set(mCandTr[mCurHyp][i], XerrFactor)) { // prepare inverse cov.matrices at starting point
if (mLoggerBadCov.needToLog()) {
#ifndef GPUCA_GPUCODE
printf("fitter %d: error (%ld muted): overrode invalid track covariance from %s\n",
mFitterID, mLoggerBadCov.getNMuted(), mCandTr[mCurHyp][i].asString().c_str());
#else
printf("fitter %d: error (%ld muted): overrode invalid track covariance cyy:%e czz:%e cyz:%e\n",
mFitterID, mLoggerBadCov.getNMuted(), mCandTr[mCurHyp][i].getSigmaY2(), mCandTr[mCurHyp][i].getSigmaZ2(), mCandTr[mCurHyp][i].getSigmaZY());
#endif
mLoggerBadCov.evCountPrev = mLoggerBadCov.evCount;
}
if (mBadCovPolicy == Discard) {
return false;
} else if (mBadCovPolicy == OverrideAndFlag) {
mPropFailed[mCurHyp] = true;
} // otherwise, just use overridden errors w/o flagging
}
}

if (mMaxDZIni > 0 && !roughDZCut()) { // apply rough cut on tracks Z difference
Expand All @@ -904,11 +968,10 @@ GPUd() bool DCAFitterN<N, Args...>::minimizeChi2()

// do Newton-Rapson iteration with corrections = - dchi2/d{x0..xN} * [ d^2chi2/d{x0..xN}^2 ]^-1
if (!mD2Chi2Dx2.Invert()) {
#ifndef GPUCA_GPUCODE_DEVICE
LOG(error) << "InversionFailed";
#else
printf("InversionFailed\n");
#endif
if (mLoggerBadInv.needToLog()) {
printf("fitter %d: error (%ld muted): Inversion failed\n", mFitterID, mLoggerBadCov.getNMuted());
mLoggerBadInv.evCountPrev = mLoggerBadInv.evCount;
}
return false;
}
VecND dx = mD2Chi2Dx2 * mDChi2Dx;
Expand Down Expand Up @@ -961,11 +1024,10 @@ GPUd() bool DCAFitterN<N, Args...>::minimizeChi2NoErr()

// do Newton-Rapson iteration with corrections = - dchi2/d{x0..xN} * [ d^2chi2/d{x0..xN}^2 ]^-1
if (!mD2Chi2Dx2.Invert()) {
#ifndef GPUCA_GPUCODE_DEVICE
LOG(error) << "InversionFailed";
#else
printf("InversionFailed\n");
#endif
if (mLoggerBadInv.needToLog()) {
printf("itter %d: error (%ld muted): Inversion failed\n", mFitterID, mLoggerBadCov.getNMuted());
mLoggerBadInv.evCountPrev = mLoggerBadInv.evCount;
}
return false;
}
VecND dx = mD2Chi2Dx2 * mDChi2Dx;
Expand Down Expand Up @@ -1108,6 +1170,14 @@ GPUdi() bool DCAFitterN<N, Args...>::propagateParamToX(o2::track::TrackPar& t, f
}
if (!res) {
mPropFailed[mCurHyp] = true;
if (mLoggerBadProp.needToLog()) {
#ifndef GPUCA_GPUCODE
printf("fitter %d: error (%ld muted): propagation failed for %s\n", mFitterID, mLoggerBadProp.getNMuted(), t.asString().c_str());
#else
printf("fitter %d: error (%ld muted): propagation failed\n", mFitterID, mLoggerBadProp.getNMuted());
#endif
mLoggerBadProp.evCountPrev = mLoggerBadProp.evCount;
}
}
return res;
}
Expand All @@ -1126,6 +1196,14 @@ GPUdi() bool DCAFitterN<N, Args...>::propagateToX(o2::track::TrackParCov& t, flo
}
if (!res) {
mPropFailed[mCurHyp] = true;
if (mLoggerBadProp.needToLog()) {
#ifndef GPUCA_GPUCODE
printf("fitter %d: error (%ld muted): propagation failed for %s\n", mFitterID, mLoggerBadProp.getNMuted(), t.asString().c_str());
#else
printf("fitter %d: error (%ld muted): propagation failed\n", mFitterID, mLoggerBadProp.getNMuted());
#endif
mLoggerBadProp.evCountPrev = mLoggerBadProp.evCount;
}
}
return res;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ class AODProducerWorkflowDPL : public Task
std::unordered_set<GIndex> mGIDUsedBySVtx;
std::unordered_set<GIndex> mGIDUsedByStr;

AODProducerStreamerMask mStreamerMask;
AODProducerStreamerMask mStreamerMask{0};
std::shared_ptr<o2::utils::TreeStreamRedirector> mStreamer;

int mNThreads = 1;
Expand Down Expand Up @@ -433,6 +433,8 @@ class AODProducerWorkflowDPL : public Task
int8_t dRefGloSnp{std::numeric_limits<int8_t>::min()};
int8_t dRefGloTgl{std::numeric_limits<int8_t>::min()};
int8_t dRefGloQ2Pt{std::numeric_limits<int8_t>::min()};
int8_t dTofdX{std::numeric_limits<int8_t>::min()};
int8_t dTofdZ{std::numeric_limits<int8_t>::min()};
};

// helper struct for addToFwdTracksTable()
Expand Down
33 changes: 24 additions & 9 deletions Detectors/AOD/src/AODProducerWorkflowSpec.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ void AODProducerWorkflowDPL::addToTracksExtraTable(TracksExtraCursorType& tracks
extraInfoHolder.itsClusterSizes,
extraInfoHolder.tpcNClsFindable,
extraInfoHolder.tpcNClsFindableMinusFound,
// extraInfoHolder.tpcNClsFindableMinusPID,
extraInfoHolder.tpcNClsFindableMinusPID,
extraInfoHolder.tpcNClsFindableMinusCrossedRows,
extraInfoHolder.tpcNClsShared,
extraInfoHolder.trdPattern,
Expand Down Expand Up @@ -381,7 +381,9 @@ void AODProducerWorkflowDPL::addToTracksQATable(TracksQACursorType& tracksQACurs
trackQAInfoHolder.dRefGloZ,
trackQAInfoHolder.dRefGloSnp,
trackQAInfoHolder.dRefGloTgl,
trackQAInfoHolder.dRefGloQ2Pt);
trackQAInfoHolder.dRefGloQ2Pt,
trackQAInfoHolder.dTofdX,
trackQAInfoHolder.dTofdZ);
}

template <typename mftTracksCursorType, typename AmbigMFTTracksCursorType>
Expand Down Expand Up @@ -2567,6 +2569,12 @@ AODProducerWorkflowDPL::TrackQA AODProducerWorkflowDPL::processBarrelTrackQA(int
trackQAHolder.tpcdcaR = 100. * dcaInfo[0] / sqrt(1. + trackPar.getQ2Pt() * trackPar.getQ2Pt());
trackQAHolder.tpcdcaZ = 100. * dcaInfo[1] / sqrt(1. + trackPar.getQ2Pt() * trackPar.getQ2Pt());
}
// This allows to safely clamp any float to one byte, using the
// minmal/maximum values as under-/overflow borders and rounding to the nearest integer
auto safeInt8Clamp = [](auto value) -> int8_t {
using ValType = decltype(value);
return static_cast<int8_t>(TMath::Nint(std::clamp(value, static_cast<ValType>(std::numeric_limits<int8_t>::min()), static_cast<ValType>(std::numeric_limits<int8_t>::max()))));
};
/// get tracklet byteMask
uint8_t clusterCounters[8] = {0};
{
Expand Down Expand Up @@ -2597,6 +2605,16 @@ AODProducerWorkflowDPL::TrackQA AODProducerWorkflowDPL::processBarrelTrackQA(int
trackQAHolder.tpcdEdxTot1R = uint8_t(tpcOrig.getdEdx().dEdxTotOROC1 * dEdxNorm);
trackQAHolder.tpcdEdxTot2R = uint8_t(tpcOrig.getdEdx().dEdxTotOROC2 * dEdxNorm);
trackQAHolder.tpcdEdxTot3R = uint8_t(tpcOrig.getdEdx().dEdxTotOROC3 * dEdxNorm);
///
float scaleTOF{0};
auto contributorsGIDA = data.getSingleDetectorRefs(trackIndex);
if (contributorsGIDA[GIndex::Source::TOF].isIndexSet()) { // ITS-TPC-TRD-TOF, ITS-TPC-TOF, TPC-TRD-TOF, TPC-TOF
const auto& tofMatch = data.getTOFMatch(trackIndex);
const float qpt = trackPar.getQ2Pt();
scaleTOF = std::sqrt(o2::aod::track::trackQAScaledTOF[0] * o2::aod::track::trackQAScaledTOF[0] + qpt * qpt * o2::aod::track::trackQAScaledTOF[1] * o2::aod::track::trackQAScaledTOF[1]) / (2. * o2::aod::track::trackQAScaleBins);
trackQAHolder.dTofdX = safeInt8Clamp(tofMatch.getDXatTOF() / scaleTOF);
trackQAHolder.dTofdZ = safeInt8Clamp(tofMatch.getDZatTOF() / scaleTOF);
}

// Add matching information at a reference point (defined by
// o2::aod::track::trackQARefRadius) in the same frame as the global track
Expand All @@ -2622,13 +2640,6 @@ AODProducerWorkflowDPL::TrackQA AODProducerWorkflowDPL::processBarrelTrackQA(int
return o2::aod::track::trackQAScaleBins / std::sqrt(o2::aod::track::trackQAScaleGloP0[i] * o2::aod::track::trackQAScaleGloP0[i] + (o2::aod::track::trackQAScaleGloP1[i] * x) * (o2::aod::track::trackQAScaleGloP1[i] * x));
};

// This allows to safely clamp any float to one byte, using the
// minmal/maximum values as under-/overflow borders and rounding to the nearest integer
auto safeInt8Clamp = [](auto value) -> int8_t {
using ValType = decltype(value);
return static_cast<int8_t>(TMath::Nint(std::clamp(value, static_cast<ValType>(std::numeric_limits<int8_t>::min()), static_cast<ValType>(std::numeric_limits<int8_t>::max()))));
};

// Calculate deltas for contributors
trackQAHolder.dRefContY = safeInt8Clamp((itsCopy.getY() - tpcCopy.getY()) * scaleCont(0));
trackQAHolder.dRefContZ = safeInt8Clamp((itsCopy.getZ() - tpcCopy.getZ()) * scaleCont(1));
Expand All @@ -2641,6 +2652,7 @@ AODProducerWorkflowDPL::TrackQA AODProducerWorkflowDPL::processBarrelTrackQA(int
trackQAHolder.dRefGloSnp = safeInt8Clamp(((itsCopy.getSnp() + tpcCopy.getSnp()) * 0.5f - gloCopy.getSnp()) * scaleGlo(2));
trackQAHolder.dRefGloTgl = safeInt8Clamp(((itsCopy.getTgl() + tpcCopy.getTgl()) * 0.5f - gloCopy.getTgl()) * scaleGlo(3));
trackQAHolder.dRefGloQ2Pt = safeInt8Clamp(((itsCopy.getQ2Pt() + tpcCopy.getQ2Pt()) * 0.5f - gloCopy.getQ2Pt()) * scaleGlo(4));
//

if (O2_ENUM_TEST_BIT(mStreamerMask, AODProducerStreamerMask::TrackQA)) {
(*mStreamer) << "trackQA"
Expand Down Expand Up @@ -2684,6 +2696,9 @@ AODProducerWorkflowDPL::TrackQA AODProducerWorkflowDPL::processBarrelTrackQA(int
<< "trackQAHolder.dRefGloSnp=" << trackQAHolder.dRefGloSnp
<< "trackQAHolder.dRefGloTgl=" << trackQAHolder.dRefGloTgl
<< "trackQAHolder.dRefGloQ2Pt=" << trackQAHolder.dRefGloQ2Pt
<< "trackQAHolder.dTofdX=" << trackQAHolder.dTofdX
<< "trackQAHolder.dTofdZ=" << trackQAHolder.dTofdZ
<< "scaleTOF=" << scaleTOF
<< "\n";
}
}
Expand Down
10 changes: 5 additions & 5 deletions Detectors/FOCAL/simulation/data/simcuts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
FOC 0 5.e-5 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
* Si sensor
FOC 1 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 -1. -1 -1 -1 -1 1 -1 1 -1 -1 -1 -1 -1
* Si pixel
FOC 2 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 1.e-5 -1. -1 -1 -1 -1 1 -1 1 -1 -1 -1 -1 -1
* G10 plate
FOC 2 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
FOC 3 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
* Alloy
FOC 5 5.e-5 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
FOC 6 5.e-5 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
* Aluminium
FOC 10 5.e-5 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
* G10 plate
FOC 12 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
FOC 11 5.e-5 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 1.e-4 -1. -1 -1 -1 -1 1 -1 3 -1 -1 -1 -1 -1
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ COMMAND_INSERT_PIX_AT_L9
GLOBAL_TOWER_TOL 0. Air
GLOBAL_TOWER_TOLX 0.02 Air
GLOBAL_TOWER_TOLY 0.8 Al
GLOBAL_FOCAL_Z 764.47
GLOBAL_FOCAL_Z 763.5
GLOBAL_Tower_NX 2
GLOBAL_Tower_NY 11
GLOBAL_MIDDLE_TOWER_OFFSET 5
Expand Down
Loading

0 comments on commit 77d637e

Please sign in to comment.