From 7ed4f99484c6f731cb21941433c93416360fd3f8 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Thu, 17 Nov 2022 16:54:09 -0800 Subject: [PATCH 01/17] Major update of comments; add uniformB option for tracking; added debugging code to KalmanToGBLDriver --- .../recon/tracking/gbl/KalmanToGBLDriver.java | 209 +++++-- .../hps/recon/tracking/kalman/FieldMap.java | 24 +- .../tracking/kalman/HelixPlaneIntersect.java | 60 +- .../hps/recon/tracking/kalman/HelixState.java | 278 +++++++-- .../hps/recon/tracking/kalman/HelixTest3.java | 37 +- .../org/hps/recon/tracking/kalman/KalHit.java | 29 +- .../hps/recon/tracking/kalman/KalTrack.java | 378 ++++++++++-- .../tracking/kalman/KalmanDriverHPS.java | 10 +- .../tracking/kalman/KalmanInterface.java | 562 +++++++++++++----- .../recon/tracking/kalman/KalmanKinkFit.java | 7 +- .../tracking/kalman/KalmanKinkFitDriver.java | 6 +- .../recon/tracking/kalman/KalmanParams.java | 22 +- .../tracking/kalman/KalmanPatRecDriver.java | 117 +++- .../tracking/kalman/KalmanPatRecHPS.java | 100 +++- .../tracking/kalman/KalmanPatRecPlots.java | 53 +- .../tracking/kalman/KalmanTrackFit2.java | 13 +- .../recon/tracking/kalman/Measurement.java | 25 + .../tracking/kalman/MeasurementSite.java | 155 ++++- .../hps/recon/tracking/kalman/PatRecTest.java | 2 +- .../org/hps/recon/tracking/kalman/Plane.java | 68 ++- .../tracking/kalman/PropagatedTrackState.java | 40 +- .../hps/recon/tracking/kalman/RotMatrix.java | 95 ++- .../recon/tracking/kalman/RungeKutta4.java | 24 +- .../hps/recon/tracking/kalman/SeedTrack.java | 243 ++++++-- .../hps/recon/tracking/kalman/SiModule.java | 90 ++- .../recon/tracking/kalman/StateVector.java | 228 ++++--- .../recon/tracking/kalman/TrackCandidate.java | 60 +- 27 files changed, 2323 insertions(+), 612 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/gbl/KalmanToGBLDriver.java b/tracking/src/main/java/org/hps/recon/tracking/gbl/KalmanToGBLDriver.java index 9b571ba588..fca6bc3c7f 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/gbl/KalmanToGBLDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/gbl/KalmanToGBLDriver.java @@ -1,8 +1,11 @@ package org.hps.recon.tracking.gbl; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.Collections; import java.util.Comparator; @@ -11,21 +14,28 @@ //import org.lcsim.event.TrackState; import org.lcsim.event.Track; - +import org.lcsim.event.TrackState; import org.lcsim.event.EventHeader; import org.lcsim.util.Driver; -import org.lcsim.geometry.Detector; +import org.lcsim.util.aida.AIDA; +import hep.aida.IHistogram1D; +import hep.aida.IHistogramFactory; + +import org.lcsim.geometry.Detector; import org.lcsim.event.LCRelation; import org.lcsim.event.RelationalTable; import org.lcsim.event.base.BaseRelationalTable; - +import org.lcsim.fit.helicaltrack.HelicalTrackFit; import org.lcsim.constants.Constants; +import org.apache.commons.math3.util.Pair; import org.hps.recon.tracking.TrackResidualsData; +import org.hps.recon.tracking.TrackStateUtils; import org.lcsim.event.base.BaseLCRelation; //import org.lcsim.fit.helicaltrack.HelicalTrackFit; import org.hps.recon.tracking.TrackUtils; +import org.hps.recon.tracking.TrackingReconstructionPlots; /** * A Driver which refits Kalman Tracks using GBL @@ -42,19 +52,46 @@ public class KalmanToGBLDriver extends Driver { private String kinkDataCollectionName = GBLKinkData.DATA_COLLECTION; private String kinkDataRelationsName = GBLKinkData.DATA_RELATION_COLLECTION; private Boolean _debug = false; + private Boolean _analysis = false; private Boolean constrainedBSFit = false; private double bfield; private Boolean computeGBLResiduals = true; + public AIDA aida; + private IHistogram1D hGBLurt, hGBLurb, hKFurt, hKFurb, hGBLrt, hGBLrb; + private IHistogram1D hDelPhi0, hDelTanL, hDelOmega, hDelD0, hDelZ0; + private IHistogramFactory ahf; - void setDebug(boolean val) { + public void setDebug(boolean val) { _debug = val; } + public void setAnalysis(boolean val) { + _analysis = val; + } + + void definePlots() { + if (aida == null) aida = AIDA.defaultInstance(); + aida.tree().cd("/"); + ahf = aida.histogramFactory(); + hGBLurt = aida.histogram1D("GBL unbiased residuals top detector", 100, -0.25, 0.25); + hGBLurb = aida.histogram1D("GBL unbiased residuals bottom detector", 100, -0.25, 0.25); + hGBLrt = aida.histogram1D("GBL biased residuals top detector", 100, -1., 1.); + hGBLrb = aida.histogram1D("GBL biased residuals bottom detector", 100, -1., 1.); + hKFurt = aida.histogram1D("KF unbiased residuals top detector", 100, -1., 1.); + hKFurb = aida.histogram1D("KF unbiased residuals bottom detector", 100, -1., 1.); + hDelPhi0 = aida.histogram1D("KF minus GBL phi0", 100, -0.02, 0.02); + hDelD0 = aida.histogram1D("KF minus GBL D0", 100, -5., 5.); + hDelOmega = aida.histogram1D("KF minus GBL Omega", 100, -3.E-4, 3.E-4); + hDelTanL = aida.histogram1D("KF minus GBL tanLamba", 100, -0.01, 0.01); + hDelZ0 = aida.histogram1D("KF minus GBL Z0", 100, -0.1, 0.1); + } + @Override protected void detectorChanged(Detector detector) { bfield = Math.abs(TrackUtils.getBField(detector).magnitude()); + if (_analysis) definePlots(); } @Override @@ -69,19 +106,66 @@ protected void process(EventHeader event) { List kinkDataCollection = new ArrayList(); List kinkDataRelations = new ArrayList(); + List GBLtracks = null; + if (_analysis) { + if (event.hasCollection(Track.class, "GBLTracks")) { + GBLtracks = event.get(Track.class, "GBLTracks"); + if (_debug) { + System.out.format("KalmanToGBLDriver: number of input GBL tracks = %d\n", GBLtracks.size()); + } + } + } + //Get the track collection from the event - if (!event.hasCollection(Track.class, inputCollectionName)) + if (!event.hasCollection(Track.class, inputCollectionName)) { + if (_debug) System.out.format("KalmanToGBLDriver: collection %s not found\n", inputCollectionName); return; + } List tracks = event.get(Track.class,inputCollectionName); - if (_debug) - System.out.println("Found tracks: " + inputCollectionName+" " + tracks.size()); - + String residualsKFcollectionName = "KFUnbiasRes"; + List residualsKF = null; + if (event.hasCollection(TrackResidualsData.class, residualsKFcollectionName)) { + if (_debug) System.out.format("KalmanToGBLDriver: collection %s found\n", residualsKFcollectionName); + residualsKF = event.get(TrackResidualsData.class, residualsKFcollectionName); + } + String residualsRelationsKFcollectionName = "KFUnbiasResRelations"; + List residualsRelKF = null; + RelationalTable kfResidsRT = null; + if (event.hasCollection(LCRelation.class, residualsRelationsKFcollectionName)) { + if (_debug) System.out.format("KalmanToGBLDriver: collection %s found\n", residualsRelationsKFcollectionName); + residualsRelKF = event.get(LCRelation.class, residualsRelationsKFcollectionName); + kfResidsRT = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); + for (LCRelation relation : residualsRelKF) { + if (relation != null && relation.getFrom() != null && relation.getTo() != null) { + kfResidsRT.add(relation.getFrom(), relation.getTo()); + } + } + } + + if (_debug) System.out.println("KalmanToGBLDriver, found tracks: " + inputCollectionName+" " + tracks.size()); - //Loop on Kalman Tracks + RelationalTable kfSCDsRT = null; + List kfSCDRelation = new ArrayList(); + if (event.hasCollection(LCRelation.class,"KFGBLStripClusterDataRelations")) { + kfSCDsRT = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); + kfSCDRelation = event.get(LCRelation.class,"KFGBLStripClusterDataRelations"); + for (LCRelation relation : kfSCDRelation) { + if (relation != null && relation.getFrom() != null && relation.getTo() != null) { + kfSCDsRT.add(relation.getFrom(), relation.getTo()); + } + } + } else { + System.out.println("null KFGBLStripCluster Data Relations."); + return; + } + + List newGBLtks = null; + if (_analysis) newGBLtks = new ArrayList(tracks.size()); + //Loop on Kalman Tracks for (Track trk : tracks ) { //Remove tracks with less than 10 hits @@ -93,34 +177,12 @@ protected void process(EventHeader event) { //Remove tracks where there is high mis-tracking rate //TrackState trackState = trk.getTrackStates().get(0); //if (Math.abs(trackState.getTanLambda()) < 0.02) - // continue; - - - //Get the GBLStripClusterData - RelationalTable kfSCDsRT = null; - List kfSCDRelation = new ArrayList(); - - - if (event.hasCollection(LCRelation.class,"KFGBLStripClusterDataRelations")) { - kfSCDsRT = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); - kfSCDRelation = event.get(LCRelation.class,"KFGBLStripClusterDataRelations"); - for (LCRelation relation : kfSCDRelation) { - if (relation != null && relation.getFrom() != null && relation.getTo() != null) { - kfSCDsRT.add(relation.getFrom(), relation.getTo()); - } - } - } else { - System.out.println("null KFGBLStripCluster Data Relations."); - return; - } - + // continue; + //Get the strip cluster data Set kfSCDs = kfSCDsRT.allFrom(trk); - - if (_debug) - System.out.println("Kalman Strip Cluster data size: " + kfSCDs.size()); - + if (_debug) System.out.println("Kalman Strip Cluster data size: " + kfSCDs.size()); //Convert the set to a list for sorting it @@ -142,6 +204,7 @@ protected void process(EventHeader event) { FittedGblTrajectory fitGbl_traj = HpsGblRefitter.fit(list_kfSCDs, bfac, false); GblTrajectory gbl_fit_trajectory = fitGbl_traj.get_traj(); + if (_debug) System.out.format("KalmanToGBLDriver: track %d fit with %d points\n", tracks.indexOf(trk), gbl_fit_trajectory.getNumPoints()); // Compute the residuals @@ -160,8 +223,7 @@ protected void process(EventHeader event) { }//computeGBLResiduals - - + // Get the derivatives /* @@ -184,6 +246,15 @@ protected void process(EventHeader event) { } */ + // Create a GBL track from the fitted trajectory + if (_analysis) { + int trackType = 57; // randomly chosen track type + TrackState atIP = TrackStateUtils.getTrackStateAtIP(trk); + HelicalTrackFit htkft = TrackUtils.getHTF(atIP); + Pair GBLtkrPair = MakeGblTracks.makeCorrectedTrack(fitGbl_traj, htkft, trk.getTrackerHits(), trackType, bfield, true); + newGBLtks.add(GBLtkrPair.getFirst()); + } + }// track loop if (computeGBLResiduals) { @@ -193,6 +264,63 @@ protected void process(EventHeader event) { event.put(kinkDataRelationsName, kinkDataRelations, LCRelation.class, 0); } + + // Analysis added by R. Johnson for debugging etc + if (_debug) { + if (residualsKF == null) System.out.format("KalmanToGBLDriver: residualsKF is null\n"); + if (residualsRelKF == null) System.out.format("KalmanToGBLDriver: residualsRelKF is null\n"); + } + if (_analysis && residualsKF != null && residualsRelKF != null) { + + RelationalTable gblResidsRT = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); + for (LCRelation relation : trackResidualsRelations) { + if (relation != null && relation.getFrom() != null && relation.getTo() != null) { + gblResidsRT.add(relation.getFrom(), relation.getTo()); + } + } + for (Track trk : tracks ) { + Track trkGBL = newGBLtks.get(tracks.indexOf(trk)); + Set gblResids = gblResidsRT.allTo(trk); + TrackState atIP_GBL = TrackStateUtils.getTrackStateAtIP(trkGBL); + TrackState atIP_KF = TrackStateUtils.getTrackStateAtIP(trk); + hDelPhi0.fill(atIP_KF.getPhi() - atIP_GBL.getPhi()); + hDelOmega.fill(atIP_KF.getOmega() - atIP_GBL.getOmega()); + hDelTanL.fill(atIP_KF.getTanLambda() - atIP_GBL.getTanLambda()); + hDelD0.fill(atIP_KF.getD0() - atIP_GBL.getD0()); + hDelZ0.fill(atIP_KF.getZ0() - atIP_GBL.getZ0()); + if (_debug) { + System.out.println("GBL Track state: " + atIP_GBL.toString()); + System.out.println("KF Track state: " + atIP_KF.toString()); + System.out.format("Track %d has %d set of GBL residuals:\n", tracks.indexOf(trk), gblResids.size()); + } + for (TrackResidualsData resData : gblResids) { + int vol = resData.getIntVal(resData.getNInt()-1); + if (_debug) System.out.format(" GBL residuals for tracker volume %d\n", vol); + for (int i=0; i kfResids = kfResidsRT.allTo(trk); + if (_debug) System.out.format("Kalman track %d has %d set of residuals:\n", tracks.indexOf(trk), kfResids.size()); + for (TrackResidualsData resData : kfResids) { + int vol = resData.getIntVal(resData.getNInt()-1); + if (_debug) System.out.format(" KF residuals for tracker volume %d\n", vol); + for (int i=0; i arcLComparator = new Comparator() { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/FieldMap.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/FieldMap.java index 88de5d113d..19a21cddda 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/FieldMap.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/FieldMap.java @@ -133,7 +133,12 @@ public FieldMap(String FileName, String type, boolean uniform, double xOffset, d offsets.print("field map offsets"); } - double [] getField(Vec r) { // Interpolate the 3D field map + /** + * Interpolate the 3D field map + * @param r 3-vector position + * @return array of 3 field components + */ + double [] getField(Vec r) { Vec rHPS; if (uniform) { rHPS = new Vec(0., 0., 505.57); @@ -192,6 +197,17 @@ public FieldMap(String FileName, String type, boolean uniform, double xOffset, d return Bout; } + /** + * Trilinear interpolation + * @param i x index into array + * @param j y index into array + * @param k z index into array + * @param xd x value + * @param yd y value + * @param zd z value + * @param f 3D array to interpolate + * @return interpolated value of the array f at the given coordinates + */ private double triLinear(int i, int j, int k, double xd, double yd, double zd, double[][][] f) { // System.out.format(" triLinear: xd=%10.7f, yd=%10.7f, zd=%10.7f\n", xd,yd,zd); // System.out.format(" 000=%12.4e, 100=%12.4e\n", f[i][j][k],f[i+1][j][k]); @@ -210,7 +226,11 @@ private double triLinear(int i, int j, int k, double xd, double yd, double zd, d return c; } - void writeBinaryFile(String fName) { // Make a binary field map file that can be read much more quickly + /** + * Make a binary field map file that can be read much more quickly + * @param fName Name of output file + */ + void writeBinaryFile(String fName) { FileOutputStream ofile; try { ofile = new FileOutputStream(fName); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixPlaneIntersect.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixPlaneIntersect.java index 47a2f3b9e8..9ef189333e 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixPlaneIntersect.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixPlaneIntersect.java @@ -30,17 +30,19 @@ class HelixPlaneIntersect { double arcLength() { // Return the arc length for the last call to rkIntersect return deltaS; } - // Runge Kutta integration extrapolation to a plane through a non-uniform field - // When close to the plane, then a helix is used to find the exact intersection + /** + * Runge Kutta integration extrapolation to a plane through a non-uniform field + * When close to the plane, then a helix is used to find the exact intersection + * + * @param P definition of the plane to which to extrapolate + * @param X0 3-D starting point for the extrapolation + * @param P0in 3-momentum at the start of the extrapolation + * @param Qin sign of the particle charge (+1 or -1) + * @param fM HPS field map + * @param pInt return value for the momentum at the intersection + * @return 3-D intersection point + */ Vec rkIntersect(Plane P, Vec X0, Vec P0in, double Qin, org.lcsim.geometry.FieldMap fM, Vec pInt) { - // P definition of the plane to which to extrapolate - // X0 3-D starting point for the extrapolation - // P0 3-momentum at the start of the extrapolation - // Q sign of the particle charge (+1 or -1) - // fM HPS field map - // pInt return value for the momentum at the intersection - // the function return value is the 3-D intersection point - if (debug) { System.out.format("Entering HelixPlaneIntersect.rkIntersect for plane %s\n", P.toString()); X0.print("rkIntersect start location, global coords"); @@ -144,10 +146,16 @@ Vec rkIntersect(Plane P, Vec X0, Vec P0in, double Qin, org.lcsim.geometry.FieldM return xIntGlb; } - // Given the momentum and charge at a location, return the parameters of the helix, - // assuming a reference frame in which the magnetic field is in the z direction! - // The new pivot point is the location provided, so rho0 and z0 will always be - // zero. + /** + * Given the momentum and charge at a location, return the parameters of the helix, + * assuming a reference frame in which the magnetic field is in the z direction! + * The new pivot point is the location provided, so rho0 and z0 will always be + * zero. + * @param x point on the track + * @param p 3-momentum at that point + * @param Q charge + * @return 5-vector of helix parameters + */ static Vec pToHelix(Vec x, Vec p, double Q) { double E = p.mag(); Vec t = p.unitVec(E); @@ -158,12 +166,15 @@ static Vec pToHelix(Vec x, Vec p, double Q) { return new Vec(0., phi0, K, 0., tanl); } - // Find the intersection of a helix with a plane. + /** + * Find the intersection of a helix with a plane. + * @param a 5-vector of helix parameters + * @param pivot helix pivot point + * @param alpha 10^12/c/B to convert curvature to momentum + * @param p point and direction cosines of the plane + * @return angle through which the helix turns going from the pivot point to the plane + */ double planeIntersect(Vec a, Vec pivot, double alpha, Plane p) { - // p: Plane assumed to be defined in the local helix reference frame - // a: vector of 5 helix parameters - // alpha: 10^12/c/B - // Take as a starting guess the solution for the case that the plane orientation is exactly y-hat. // System.out.format("HelixPlaneIntersection:planeIntersect, alpha=%f10.5\n", alpha); this.alpha = alpha; @@ -183,9 +194,16 @@ static Vec pToHelix(Vec x, Vec p, double Q) { return phi; } - // Safe Newton-Raphson zero finding from Numerical Recipes in C + /** + * Safe Newton-Raphson zero finding from Numerical Recipes in C + * @param xGuess starting guess for the phi angle of the helix intersection + * @param x1 minimum of search range + * @param x2 maximum of search range + * @param xacc specification of the required accuracy + * @return a zero point between x1 and x2 + */ private double rtSafe(double xGuess, double x1, double x2, double xacc) { - // Here xGuess is a starting guess for the phi angle of the helix intersection + // Here xGuess is a // x1 and x2 give a range for the value of the solution // xacc specifies the accuracy needed // The output is an accurate result for the phi of the intersection diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java index f26370865c..cf7e6a6c1f 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java @@ -42,6 +42,15 @@ public Object clone() { } } + /** + * Constructor with all helix information provided by the call + * @param a 5 helix parameters + * @param X0 pivot point + * @param origin coordinate system origin + * @param C covariance matrix of helix parameters + * @param B magnetic field magnitude + * @param tB magnetic field direction + */ HelixState(Vec a, Vec X0, Vec origin, DMatrixRMaj C, double B, Vec tB) { logger = Logger.getLogger(HelixState.class.getName()); this.a = a; @@ -59,6 +68,12 @@ public Object clone() { Rot = new RotMatrix(u, v, tB); } + /** + * Constructor without helix supplied + * @param B magnetic field magnitude + * @param tB magnetic field direction + * @param origin origin of coordinate system + */ HelixState(double B, Vec tB, Vec origin) { logger = Logger.getLogger(HelixState.class.getName()); this.origin = origin; @@ -73,20 +88,36 @@ public Object clone() { Rot = new RotMatrix(u, v, tB); } + /** + * Empty constructor + */ HelixState() { logger = Logger.getLogger(HelixState.class.getName()); hpi = new HelixPlaneIntersect(); c = 2.99793e8; // Speed of light in m/s } + /** + * Deep copy + * @return new helix state + */ HelixState copy() { return new HelixState(a.copy(), X0.copy(), origin.copy(), C.copy(), B, tB.copy()); } + /** + * Debug printout of the helix state + * @param s A string used to identify the printout + */ void print(String s) { System.out.format("%s", this.toString(s)); } + /** + * Debug printout to a string + * @param s A short string to identify the printout + * @return A long string that can be printed + */ String toString(String s) { String str; str = String.format("HelixState %s: helix parameters=%s, pivot=%s\n", s, a.toString(), X0.toString()); @@ -97,18 +128,34 @@ String toString(String s) { return str; } + /** + * Check whether the helix parameter covariance matrix is mathematically good + * @return true or false + */ boolean goodCov() { if (!MatrixFeatures_DDRM.isDiagonalPositive(C)) return false; if (MatrixFeatures_DDRM.hasNaN(C)) return false; return true; } - // Returns a point on the helix at the angle phi + /** + * Returns a point on the helix at the angle phi + * @param phi The angle + * @return 3-vector point on the helix + */ // Warning: the point returned is in the B-Field reference frame Vec atPhi(double phi) { return atPhi(X0, a, phi, alpha); } + /** + * Returns a point on a provided helix at the angle phi + * @param X0 pivot point of the helix + * @param a 5 helix parameters + * @param phi the angle + * @param alpha parameter to transform between curvature and momentum + * @return 3-vector point on the helix + */ static Vec atPhi(Vec X0, Vec a, double phi, double alpha) { double x = X0.v[0] + (a.v[0] + (alpha / a.v[2])) * FastMath.cos(a.v[1]) - (alpha / a.v[2]) * FastMath.cos(a.v[1] + phi); double y = X0.v[1] + (a.v[0] + (alpha / a.v[2])) * FastMath.sin(a.v[1]) - (alpha / a.v[2]) * FastMath.sin(a.v[1] + phi); @@ -116,24 +163,39 @@ static Vec atPhi(Vec X0, Vec a, double phi, double alpha) { return new Vec(x, y, z); } - // Calculate the phi angle to propagate on helix to the intersection with a - // measurement plane + /** + * Calculate the phi angle to propagate on helix to the intersection with a measurement plane + * @param pIn point and direction cosines of the plane + * @return turning angle from the pivot point to the plane + */ double planeIntersect(Plane pIn) { // pIn is assumed to be defined in the global reference frame Plane p = pIn.toLocal(Rot, origin); // Transform the plane into the B-field local reference frame return hpi.planeIntersect(a, X0, alpha, p); } - // Return errors on the helix parameters at the present pivot point + /** + * Return errors on the helix parameters at the present pivot point + * @return 5-vector of helix parameter errors + */ Vec helixErrors() { return new Vec(FastMath.sqrt(C.unsafe_get(0,0)), FastMath.sqrt(C.unsafe_get(1,1)), FastMath.sqrt(C.unsafe_get(2,2)), FastMath.sqrt(C.unsafe_get(3,3)), FastMath.sqrt(C.unsafe_get(4,4))); } - // Derivative matrix for the pivot transform (without energy loss or field rotations) + /** + * Create derivative matrix for the pivot transform (without energy loss or field rotations) + * @param aP Input transformed helix parameters + * @param F Returned derivative matrix + */ void makeF(Vec aP, DMatrixRMaj F) { makeF(aP, F, a, alpha); } - + + /** + * Create derivative matrix for the pivot transform (without energy loss or field rotations) + * @param aP Helix parameters + * @param F Returned derivative matrix + */ static void makeF(Vec aP, DMatrixRMaj F, Vec a, double alpha) { F.unsafe_set(0, 0, FastMath.cos(aP.v[1] - a.v[1])); F.unsafe_set(0, 1, (a.v[0] + alpha / a.v[2]) * FastMath.sin(aP.v[1] - a.v[1])); @@ -153,12 +215,23 @@ static void makeF(Vec aP, DMatrixRMaj F, Vec a, double alpha) { // All other values are always zero } - // Returns the particle momentum at the helix angle phi - // Warning! This is returned in the B-Field coordinate system. + /** + * Returns the particle momentum at the helix angle phi. + * Warning! This is returned in the B-Field coordinate system. + * @param phi turning angle from the pivot to the helix point of interest + * @return 3-momentum at the given angle + */ Vec getMom(double phi) { return getMom(phi, a); } - + + /** + * Returns the particle momentum at the helix angle phi. + * Warning! This is returned in the B-Field coordinate system. + * @param phi turning angle from the pivot to the helix point of interest + * @param a 5-vector of helix parameters + * @return 3-momentum at the given angle + */ static Vec getMom(double phi, Vec a) { double px = -FastMath.sin(a.v[1] + phi) / Math.abs(a.v[2]); double py = FastMath.cos(a.v[1] + phi) / Math.abs(a.v[2]); @@ -166,7 +239,11 @@ static Vec getMom(double phi, Vec a) { return new Vec(px, py, pz); } - // Momentum at the start of the given helix (point closest to the pivot) + /** + * Momentum at the start of the given helix (point closest to the pivot) + * @param a Helix parameters + * @return 3-momentum at pivot + */ static Vec aTOp(Vec a) { double px = -FastMath.sin(a.v[1]) / Math.abs(a.v[2]); double py = FastMath.cos(a.v[1]) / Math.abs(a.v[2]); @@ -174,7 +251,14 @@ static Vec aTOp(Vec a) { return new Vec(px, py, pz); } - // Transform from momentum at helix starting point back to the helix parameters + /** + * Transform from momentum at helix starting point back to the helix parameters + * @param p 3-momentum at pivot + * @param drho d-rho helix parameter (not transformed) + * @param dz dz helix parameter (not transformed) + * @param Q charge + * @return 4-vector of helix parameters + */ // drho and dz are not modified static Vec pTOa(Vec p, double drho, double dz, double Q) { double phi0 = FastMath.atan2(-p.v[0], p.v[1]); @@ -184,24 +268,39 @@ static Vec pTOa(Vec p, double drho, double dz, double Q) { return new Vec(drho, phi0, K, dz, tanl); } - // To transform a space point from global to local field coordinates, first subtract - // and then rotate by . + /** + * To transform a space point from global to local field coordinates, first subtract + * and then rotate by . + * @param xGlobal global space point + * @return local space point + */ Vec toLocal(Vec xGlobal) { Vec xLocal = Rot.rotate(xGlobal.dif(origin)); return xLocal; } - // To transform a space point from local field coordinates to global coordinates, first rotate by - // the inverse of and then add the . + /** + * To transform a space point from local field coordinates to global coordinates, first rotate by + * the inverse of and then add the . + * @param xLocal local space point + * @return global space point + */ Vec toGlobal(Vec xLocal) { Vec xGlobal = Rot.inverseRotate(xLocal).sum(origin); return xGlobal; } - // Transformation of helix parameters from one B-field frame to another, by rotation R - // Warning: the pivot point has to be transformed too! Here we assume that the new pivot point - // will be on the helix at phi=0, so drho and dz will always be returned as zero. Therefore, before - // calling this routine, make sure that the current pivot point is on the helix (drho=dz=0) + /** + * Transformation of helix parameters from one B-field frame to another, by rotation R + * Warning: the pivot point has to be transformed too! Here we assume that the new pivot point + * will be on the helix at phi=0, so drho and dz will always be returned as zero. Therefore, before + * calling this routine, make sure that the current pivot point is on the helix (drho=dz=0). + * This routine is not used if the B-Field is assumed uniform. + * @param a 5-vector of helix parameters + * @param R rotation matrix + * @param fRot returned derivative matrix + * @return 5 transformed helix parameters + */ static Vec rotateHelix(Vec a, RotMatrix R, DMatrixRMaj fRot) { // The rotation is easily applied to the momentum vector, so first we transform from helix parameters // to momentum, apply the rotation, and then transform back to helix parameters. @@ -281,23 +380,43 @@ static Vec rotateHelix(Vec a, RotMatrix R, DMatrixRMaj fRot) { return aNew; } - // Transform the helix to a pivot back at the global origin + /** + * Transform the helix to a pivot back at the global origin + * @return new helix parameters + */ Vec pivotTransform() { Vec pivot = origin.scale(-1.0); return pivotTransform(pivot); } - // Pivot transform of the state vector, from the current pivot to the pivot in - // the argument (specified in local coordinates) + /** + * Pivot transform of the state vector, from the current pivot to the pivot provided + * @param pivot the new pivot point (specified in local coordinates) + * @return the new 5-vector of helix parameters + */ Vec pivotTransform(Vec pivot) { return pivotTransform(pivot, a, X0, alpha, 0.); } - // Pivot transform including energy loss just before + /** + * Pivot transform including energy loss just before (energy loss formalism is still not tested and used) + * @param pivot + * @param deltaEoE + * @return + */ Vec pivotTransform(Vec pivot, double deltaEoE) { return pivotTransform(pivot, a, X0, alpha, deltaEoE); } + /** + * Pivot transform of the state vector, from the current pivot to the pivot provided + * @param pivot new pivot + * @param a old helix parameters + * @param X0 old pivot + * @param alpha parameter to transform between curvature and momentum + * @param deltaEoE energy loss + * @return new helix parameters + */ static Vec pivotTransform(Vec pivot, Vec a, Vec X0, double alpha, double deltaEoE) { double K = a.v[2] * (1.0 - deltaEoE); // Lose energy before propagating double xC = X0.v[0] + (a.v[0] + alpha / K) * FastMath.cos(a.v[1]); // Center of the helix circle @@ -323,17 +442,28 @@ static Vec pivotTransform(Vec pivot, Vec a, Vec X0, double alpha, double deltaEo return new Vec(5, aP); } - // Propagate a helix by Runge-Kutta integration to an arbitrary plane + /** + * Propagate a helix by Runge-Kutta integration to an arbitrary plane + * @param pln plane to where the extrapolation is taking place in global coordinates. + * The origin of pln will be the new helix pivot point in global coordinates and the origin of the B-field system. + * @param yScat input array of y values where scattering in silicon will take place. Only those between the start and finish points + * will be used, so including extras will just waste a bit of CPU time. Silicon is assumed to lie in a plane + * perpendicular to the beam axis at each of these yScat values. + * @param XL input array of y values where scattering in silicon will take place. Only those between the start and finish points + * will be used, so including extras will just waste a bit of CPU time. Silicon is assumed to lie in a plane + * perpendicular to the beam axis at each of these yScat values. + * @param fM HPS field map + * @param arcLength return the arc length to the plane + * @return helix state at the new pivot. These helix parameters are valid in the B-field coordinate system with + * origin at the pivot point and z axis in the direction of the B-field at the pivot. + */ HelixState propagateRungeKutta(Plane pln, ArrayList yScat, ArrayList XL, org.lcsim.geometry.FieldMap fM, double [] arcLength) { - // pln = plane to where the extrapolation is taking place in global coordinates. - // The origin of pln will be the new helix pivot point in global coordinates and the origin of the B-field system. - // yScat = input array of y values where scattering in silicon will take place. Only those between the start and finish points - // will be used, so including extras will just waste a bit of CPU time. Silicon is assumed to lie in a plane - // perpendicular to the beam axis at each of these yScat values. + // pln = + // + // yScat = // XL = silicon thickness in radiation lengths at each of the scattering planes - // fM = HPS field map - // return value = helix state at the new pivot. These helix parameters are valid in the B-field coordinate system with - // origin at the pivot point and z axis in the direction of the B-field at the pivot. + // fM = + // return value = final boolean debug = false; @@ -418,30 +548,53 @@ HelixState propagateRungeKutta(Plane pln, ArrayList yScat, ArrayList XL, org.lcsim.geometry.FieldMap fM, double [] arcLength) { ArrayList yScat = new ArrayList(); return propagateRungeKutta(pln, yScat, XL, fM, arcLength); } + /** + * Get the intersection point with the plane, as calculated by propagateRungeKutta + * @return 3-vector intersection point + */ Vec getRKintersection() { return xPlaneRK; } + /** + * Calculated the expected rms projected multiple scattering angle. + * @param p momentum magnitude + * @param XL radiation lengths of the scattering material + * @return rms projected angle + */ static double projMSangle(double p, double XL) { if (XL <= 0.) return 0.; return (0.0136 / Math.abs(p)) * FastMath.sqrt(XL) * (1.0 + 0.038 * FastMath.log(XL)); } - boolean helixStepper(double maxStep, ArrayList yScat, ArrayList XL, DMatrixRMaj Covariance, Vec finalHelix, Vec newOrigin, org.lcsim.geometry.FieldMap fM) { - // The old and new origin points are in global coordinates. The old helix and old pivot are defined - // in a coordinate system aligned with the field and centered at the old origin. The returned - // helix will be in a coordinate system aligned with the local field at the new origin, and the - // new pivot point will be at the new origin, in global coordinates, although to use it one should transform to the field frame, which - // is defined with its origin at newOrigin and aligned there with the local field. - // All scattering layers are assumed to be of the same thickness XL, in radiation lengths - // We assume that the starting StateVector is at a layer with a hit, in which case the Kalman filter has already accounted for - // multiple scattering at that layer. + /** + * Transform a helix in discrete steps through a non-uniform B field. Each step assumes locally uniform field. + * The old and new origin points are in global coordinates. The old helix and old pivot are defined + * in a coordinate system aligned with the field and centered at the old origin. The returned + * helix will be in a coordinate system aligned with the local field at the new origin, and the + * new pivot point will be at the new origin, in global coordinates, although to use it one should transform to the field frame, which + * is defined with its origin at newOrigin and aligned there with the local field. + * All scattering layers are assumed to be of the same thickness XL, in radiation lengths + * We assume that the starting StateVector is at a layer with a hit, in which case the Kalman filter has already accounted for + * multiple scattering at that layer. + * @param maxStep Maximum step size, in mm + * @param yScat Locations of scattering planes + * @param XL radiation lengths of scattering planes + * @param Covariance output the covariance matrix of the transformed helix + * @param finalHelix output transformed helix + * @param newOrigin new origin 3-vector point, in global coordinates + * @param fM HPS field map + * @return successful conclusion if true + */ + boolean helixStepper(double maxStep, ArrayList yScat, ArrayList XL, DMatrixRMaj Covariance, Vec finalHelix, Vec newOrigin, org.lcsim.geometry.FieldMap fM) { final boolean debug = false; @@ -626,17 +779,10 @@ boolean helixStepper(double maxStep, ArrayList yScat, ArrayList return true; } - // Transform the HelixState into a standard HPS TrackState (which loses a lot of information) - // In the returned TrackState the reference point gets set to the point on the helix closest to the - // original pivot point (e.g. the helix intersection with the plane of silicon). - // The pivot of the returned TrackState is always the origin (0,0,0) - TrackState toTrackState(double alphaCenter, Plane pln, int loc) { - // See TrackState for the different choices for loc (e.g. TrackState.atOther) - return KalmanInterface.toTrackState(this, pln, alphaCenter, loc); - } - - // Transform a helix from one pivot to another through a non-uniform B field in several steps - // Deprecated original version without scattering planes + /** + * Transform a helix from one pivot to another through a non-uniform B field in several steps + * Deprecated original version without scattering planes + */ Vec helixStepper(int nSteps, DMatrixRMaj Covariance, Vec newOrigin, org.lcsim.geometry.FieldMap fM) { double maxStep = Math.abs(newOrigin.v[1]-origin.v[1])/(double)nSteps; ArrayList yScats = new ArrayList(); @@ -645,8 +791,25 @@ Vec helixStepper(int nSteps, DMatrixRMaj Covariance, Vec newOrigin, org.lcsim.ge helixStepper(maxStep, yScats, XL, Covariance, transHelix, newOrigin, fM); return transHelix; } + + /** + * Transform the HelixState into a standard HPS TrackState (which loses a lot of information) + * In the returned TrackState the reference point gets set to the point on the helix closest to the + * original pivot point (e.g. the helix intersection with the plane of silicon). + * The pivot of the returned TrackState is always the origin (0,0,0) + * @param alphaCenter parameter to transform between curvature and momentum at the tracker center + * @param pln the position and direction cosines of the plane + * @param loc location descriptor (from LCSIM) + * @return + */ + TrackState toTrackState(double alphaCenter, Plane pln, int loc) { + // See TrackState for the different choices for loc (e.g. TrackState.atOther) + return KalmanInterface.toTrackState(this, pln, alphaCenter, loc); + } - // Comparator function for sorting pairs in helixStepper by y + /** + * Comparator function for sorting pairs in helixStepper by y + */ static Comparator> pairComparator = new Comparator>() { public int compare(Pair p1, Pair p2) { if (p1.getFirstElement() < p2.getFirstElement()) { @@ -657,8 +820,11 @@ public int compare(Pair p1, Pair p2) { } }; - // Multiple scattering matrix; assume a single thin scattering layer at the - // beginning of the helix propagation + /** + * Multiple scattering matrix; assume a single thin scattering layer at the beginning of the helix propagation + * @param sigmaMS rms projected multiple scattering angle + * @param Q returned multiple scattering matrix + */ void getQ(double sigmaMS, DMatrixRMaj Q) { double V = sigmaMS * sigmaMS; Q.unsafe_set(1, 1, V * (1.0 + a.v[4] * a.v[4])); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index 5b0df88a89..62ceb4bbbf 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -37,7 +37,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code // Control parameters // Units are Tesla, GeV, mm - int nTrials = 100; // The number of test events to generate for fitting + int nTrials = 10000; // The number of test events to generate for fitting int startLayer = 10; // Where to start the Kalman filtering int nIteration = 2; // Number of filter iterations int nAxial = 3; // Number of axial layers needed by the linear fit @@ -57,7 +57,17 @@ class HelixTest3 { // Program for testing the Kalman fitting code DMatrixRMaj tempM1 = new DMatrixRMaj(5,5); DMatrixRMaj tempM2 = new DMatrixRMaj(5,5); DMatrixRMaj fRot = new DMatrixRMaj(5,5); + for (int i=0; i<5; ++i) { + for (int j=0; j<5; ++j) { + if (i==j) { + fRot.unsafe_set(i, j, 1.); + } else { + fRot.unsafe_set(i, j, 0); + } + } + } KalmanParams kPar = new KalmanParams(); + kPar.print(); // Seed the random number generator long rndSeed = -3263009337738135404L; @@ -435,6 +445,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hPropx1s = new Histogram(100,-5.,0.1,"projected track-state x error 1 step","sigmas","track"); Histogram hPropz1 = new Histogram(100,-5.,0.1,"projected track-state z error 1 step","mm","track"); Histogram hPropz1s = new Histogram(100,-5.,0.1,"projected track-state z error 1 step","sigmas","track"); + Instant timestamp = Instant.now(); System.out.format("Beginning time = %s\n", timestamp.toString()); LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); @@ -449,8 +460,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code if (verbose) System.out.format("Oops! No intersection found with initial plane"); return; } + Vec lyr1Int = TkInitial.atPhi(phi1); Vec p1 = new Vec(3); + // Find intersection with 1st plane by RK integration Vec pivotBegin = TkRKinitial.planeIntersect(si1.p, p1); + Vec errOfB = pivotBegin.dif(lyr1Int); + errOfB.print("error in extrap to lyr 1 without RK"); Helix helixBegin = new Helix(Q, pivotBegin, p1, pivotBegin, fMg, rnd); RKhelix helixBeginRK = new RKhelix(pivotBegin, p1, Q, fMg, rnd); @@ -460,7 +475,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code TkInitial.print("TkInitial: initial helix at the origin"); helixBegin.print("helixBegin: starting helix at layer 1"); RKhelix TkEnd = helixBeginRK; - KalmanInterface KI = new KalmanInterface(false, kPar, fM); + KalmanInterface KI = new KalmanInterface(kPar, fM); for (int iTrial = 0; iTrial < nTrials; iTrial++) { RKhelix Tk = helixBeginRK.copy(); if (verbose) { Tk.print("copied initial helix"); } @@ -610,7 +625,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code continue; } - SeedTrack seed = new SeedTrack(SiModules, location[frstLyr], hitList, verbose); + SeedTrack seed = new SeedTrack(SiModules, location[frstLyr], hitList, verbose, kPar); if (!seed.success) { System.out.format("Failed to make a seed track\n"); continue; @@ -978,14 +993,16 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } Vec newPivot = kF.fittedStateBegin().helix.toLocal(helixBegin.origin.sum(helixBegin.X0)); Vec aF = kF.fittedStateBegin().helix.pivotTransform(newPivot); - // now rotate to the original field frame - RotMatrix Rcombo = helixBegin.R.multiply(kF.fittedStateBegin().helix.Rot.invert()); - aF = HelixState.rotateHelix(aF, Rcombo, fRot); - if (verbose) { - Rcombo.print("Rcombo, into the frame of the true helix"); - aF.print("final smoothed helix parameters at the track beginning"); - newPivot.print("final smoothed helix pivot in local coordinates"); + // now rotate to the original field frame + if (!kPar.uniformB) { + RotMatrix Rcombo = helixBegin.R.multiply(kF.fittedStateBegin().helix.Rot.invert()); + aF = HelixState.rotateHelix(aF, Rcombo, fRot); + if (verbose) { + Rcombo.print("Rcombo, into the frame of the true helix"); + aF.print("final smoothed helix parameters at the track beginning"); + newPivot.print("final smoothed helix pivot in local coordinates"); + } } Vec aFe = new Vec(5); DMatrixRMaj aFC = kF.fittedStateBegin().covariancePivotTransform(aF); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalHit.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalHit.java index fdec0a48b5..46166ad28a 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalHit.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalHit.java @@ -8,23 +8,42 @@ * Relationships between hits, silicon modules, and track candidates */ class KalHit { - SiModule module; - Measurement hit; - Set tkrCandidates; + SiModule module; // The silicon module in which the hit is found + Measurement hit; // The measurement object corresponding to the hit + Set tkrCandidates; // List of candidate tracks that incorporate this hit + /** + * Kalman hit constructor + * @param module silicon module object + * @param hit measurement hit object + */ KalHit(SiModule module, Measurement hit) { this.module = module; this.hit = hit; tkrCandidates = new HashSet(); } + /** + * Is the hit on a stereo layer? + * @return true or false + */ boolean isStereo() { return module.isStereo; } + + /** + * Debug printout of a given Kalman hit + * @param s Arbitrary string to identify the printout + */ void print(String s) { System.out.format("%s", this.toString(s)); } + /** + * Debug printout to a string for a given Kalman hit + * @param s Arbitrary string, to identify the printout + * @return The full string, in printable format. + */ String toString(String s) { int ntks = hit.tracks.size(); String str; @@ -45,7 +64,9 @@ String toString(String s) { return str; } - // Comparator function for sorting hits on a track candidate + /** + * Comparator function for sorting hits on a track candidate + */ static Comparator HitComparator = new Comparator() { public int compare(KalHit h1, KalHit h2) { if (h1 == h2) return 0; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index a7c671b06f..2625b05ec8 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -38,7 +38,7 @@ public class KalTrack { public boolean bad; HelixState helixAtOrigin; private boolean propagated; - private RotMatrix Rot; + private RotMatrix Rot; // Rotation matrix between global and field coordinates at the beam spot private Vec originPoint; private Vec originMomentum; private ArrayList yScat; @@ -60,8 +60,18 @@ public class KalTrack { private static boolean initialized; private double [] arcLength; private static LinearSolverDense solver; + private static final boolean uniformBatOrigin = false; static int [] nBadCov = {0, 0}; + /** + * Track constructor + * @param evtNumb event number + * @param tkID integer ID for the track + * @param SiteList list of measurement sites + * @param yScat array of scattering planes to propagate through + * @param XLscat scattering radiation lengths at each plane + * @param kPar KalmanParams instance + */ KalTrack(int evtNumb, int tkID, ArrayList SiteList, ArrayList yScat, ArrayList XLscat, KalmanParams kPar) { // System.out.format("KalTrack constructor chi2=%10.6f\n", chi2); eventNumber = evtNumb; @@ -111,7 +121,12 @@ public class KalTrack { helixAtOrigin = null; propagated = false; MeasurementSite site0 = this.SiteList.get(0); - Vec B = KalmanInterface.getField(new Vec(3,kPar.beamSpot), site0.m.Bfield); + Vec B = null; + if (kPar.uniformB) { + B = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), site0.m.Bfield); + } else { + B = KalmanInterface.getField(new Vec(3,kPar.beamSpot), site0.m.Bfield); + } Bmag = B.mag(); tB = B.unitVec(Bmag); Vec yhat = new Vec(0., 1.0, 0.); @@ -160,10 +175,17 @@ public class KalTrack { } } + /** + * Get the time of the track + * @return time in ns + */ public double getTime() { return time; } + /** + * Make a map between measurement sites and 3-vector intercepts of the track at the SSD planes of those sites + */ public Map interceptVects() { if (interceptVects == null) { interceptVects = new HashMap(nHits); @@ -179,6 +201,9 @@ public Map interceptVects() { return interceptVects; } + /** + * Make a map between measurement sites and 3-vector momentum at the intercept of the track and SSD plane + */ public Map interceptMomVects() { if (interceptMomVects == null) { interceptMomVects = new HashMap(); @@ -194,9 +219,14 @@ public Map interceptMomVects() { return interceptMomVects; } - // Calculate and return the intersection point of the Kaltrack with an SiModule. + /** + * Calculate and return the intersection point of the Kaltrack with an SiModule. // Local sensor coordinates (u,v) are returned. // The global intersection can be returned via rGbl if an array of length 3 is passed. + * @param mod module + * @param rGbl returned global intersection point + * @return intersection point + */ public double [] moduleIntercept(SiModule mod, double [] rGbl) { HelixState hx = null; for (MeasurementSite site : SiteList) { @@ -226,6 +256,9 @@ public Map interceptMomVects() { return rtnArray; } + /** + * Make a map between the track measurement sites and the tracker layers + */ private void makeLyrMap() { lyrMap = new HashMap(nHits); for (MeasurementSite site : SiteList) { @@ -233,6 +266,9 @@ private void makeLyrMap() { } } + /** + * Make a map between the measurement sites and the corresponding Millipede IDs + */ private void makeMillipedeMap() { millipedeMap = new HashMap(nHits); for (MeasurementSite site : SiteList) { @@ -240,7 +276,11 @@ private void makeMillipedeMap() { } } - // Find the change in smoothed helix angle in XY between one layer and the next + /** + * Find the change in smoothed helix angle in XY between one layer and the next + * @param layer layer from 0 to 13 + * @return difference angle in XY + */ public double scatX(int layer) { if (lyrMap == null) makeLyrMap(); if (!lyrMap.containsKey(layer)) return -999.; @@ -262,7 +302,11 @@ public double scatX(int layer) { return t1 - t2; } - // Find the change in smoothed helix angle in ZY between one layer and the next + /** + * Find the change in smoothed helix angle in ZY between one layer and the next + * @param layer layer from 0 to 13 + * @return difference angle in ZY + */ public double scatZ(int layer) { if (lyrMap == null) makeLyrMap(); if (!lyrMap.containsKey(layer)) return -999.; @@ -283,7 +327,11 @@ public double scatZ(int layer) { return t1 - t2; } - public double chi2prime() { // Alternative calculation of the fit chi^2, considering only residuals divided by the hit sigma + /** + * Alternative calculation of the fit chi^2, considering only residuals divided by the hit sigma + * @return chi-squared + */ + public double chi2prime() { double c2 = 0.; for (MeasurementSite S : SiteList) { if (S.aS == null) continue; @@ -299,6 +347,11 @@ public double chi2prime() { // Alternative calculation of the fit chi^2, conside return c2; } + /** + * Get track unbiased residuals by Millipede ID + * @param millipedeID + * @return pair of unbiased residual and its error estimate + */ public Pair unbiasedResidualMillipede(int millipedeID) { if (millipedeMap == null) makeMillipedeMap(); if (millipedeMap.containsKey(millipedeID)) { @@ -308,6 +361,11 @@ public Pair unbiasedResidualMillipede(int millipedeID) { } } + /** + * Get track unbiased residual by layer number + * @param layer + * @return pair of unbiased residual and its error estimate + */ public Pair unbiasedResidual(int layer) { if (lyrMap == null) makeLyrMap(); if (lyrMap.containsKey(layer)) { @@ -317,7 +375,11 @@ public Pair unbiasedResidual(int layer) { } } - // Returns the unbiased residual for the track at a given layer, together with the variance on that residual + /** + * Returns the unbiased residual for the track at a given site, together with the variance on that residual + * @param site measurement site + * @return residual and error + */ public Pair unbiasedResidual(MeasurementSite site) { double resid = -999.; double varResid = -999.; @@ -342,6 +404,11 @@ public Pair unbiasedResidual(MeasurementSite site) { return new Pair(resid, varResid); } + /** + * Returns the biased residual for the track at a given layer, together with the variance on that residual + * @param layer + * @return biased residual and error + */ public Pair biasedResidual(int layer) { if (lyrMap == null) makeLyrMap(); if (lyrMap.containsKey(layer)) { @@ -350,7 +417,12 @@ public Pair biasedResidual(int layer) { return new Pair(-999., -999.); } } - + + /** + * Get track biased residuals by Millipede ID + * @param millipedeID + * @return pair of biased residual and its error estimate + */ public Pair biasedResidualMillipede(int millipedeID) { if (millipedeMap == null) makeMillipedeMap(); if (millipedeMap.containsKey(millipedeID)) { @@ -359,7 +431,12 @@ public Pair biasedResidualMillipede(int millipedeID) { return new Pair(-999., -999.); } } - + + /** + * Get track biased residuals by measurement site + * @param site + * @return pair of biased residual and its error estimate + */ public Pair biasedResidual(MeasurementSite site) { double resid = -999.; double varResid = -999.; @@ -370,9 +447,14 @@ public Pair biasedResidual(MeasurementSite site) { return new Pair(resid, varResid); } + /** + * Relatively short debug printout + * @param s Arbitrary string for the user's reference + */ public void print(String s) { System.out.format("\nKalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); if (propagated) System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); + System.out.format(" Magnetic field magnitude = %10.5f and direction = %s\n", Bmag, tB.toString()); MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { System.out.format(" Helix at layer %d: %s, pivot=%s\n", site0.m.Layer, site0.aS.helix.a.toString(), site0.aS.helix.X0.toString()); @@ -395,11 +477,19 @@ public void print(String s) { } } } - + + /** + * Long detailed debug printout + * @param s Arbitrary string for the user's reference + */ public void printLong(String s) { System.out.format("%s", this.toString(s)); } - + + /** + * Long detailed debug printout to a string + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str = String.format("\n KalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); if (propagated) { @@ -448,8 +538,11 @@ String toString(String s) { return str; } - // Method to make simple yz plots of the track and the residuals. Note that in the yz plot of the track the hits are - // placed at the strip center, so they generally will not appear to be right on the track. Use gnuplot to display. + /** + * Method to make simple yz plots of the track and the residuals. Note that in the yz plot of the track the hits are + * placed at the strip center, so they generally will not appear to be right on the track. Use gnuplot to display. + * @param path where to put the output + */ public void plot(String path) { File file = new File(String.format("%s/Track%d_%d.gp", path, ID, eventNumber)); file.getParentFile().mkdirs(); @@ -513,14 +606,20 @@ public void plot(String path) { pW.close(); } - // Arc length along track from the origin to the first measurement + /** + * Arc length along track from the origin to the first measurement + * @return arc length in mm + */ public double originArcLength() { if (!propagated || arcLength == null) originHelix(); if (arcLength == null) return 0.; return arcLength[0]; } - // Runge Kutta propagation of the helix to the origin + /** + * Runge Kutta propagation of the helix to the origin + * @return helix state at the origin + */ public boolean originHelix() { if (propagated) return true; @@ -548,24 +647,34 @@ public boolean originHelix() { // The StateVector method propagateRungeKutta transforms the origin plane into the origin B-field frame Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); arcLength = new double[1]; - helixAtOrigin = innerSite.aS.helix.propagateRungeKutta(originPlane, yScat, XLscat, innerSite.m.Bfield, arcLength); - if (debug) System.out.format("KalTrack::originHelix: arc length to the first measurement = %9.4f\n", arcLength[0]); - if (covNaN()) return false; - if (!solver.setA(helixAtOrigin.C.copy())) { - logger.fine("KalTrack:originHelix, cannot invert the covariance matrix"); - for (int i=0; i<5; ++i) { // Fill the matrix and inverse with something not too crazy and continue . . . - for (int j=0; j<5; ++j) { - if (i == j) { - Cinv.unsafe_set(i,j,1.0/Math.abs(helixAtOrigin.C.unsafe_get(i, j))); - helixAtOrigin.C.unsafe_set(i, j, Math.abs(helixAtOrigin.C.unsafe_get(i, j))); - } else { - Cinv.unsafe_set(i, j, 0.); - helixAtOrigin.C.unsafe_set(i, j, 0.); - } - } - } + if (uniformBatOrigin) { // Just a simple pivot transform is needed if the field is assumed uniform + Vec origin = new Vec(0.,0.,0.); + Vec newHelixParams = innerSite.aS.helix.pivotTransform(); + DMatrixRMaj newCovariance = innerSite.aS.covariancePivotTransform(newHelixParams); + helixAtOrigin = new HelixState(newHelixParams, origin, origin, newCovariance, Bmag, tB); + //innerSite.aS.helix.print("at innerSite"); + //helixAtOrigin.print("uniformB"); } else { - solver.invert(Cinv); + helixAtOrigin = innerSite.aS.helix.propagateRungeKutta(originPlane, yScat, XLscat, innerSite.m.Bfield, arcLength); + //helixAtOrigin.print("nonuniformB"); + if (debug) System.out.format("KalTrack::originHelix: arc length to the first measurement = %9.4f\n", arcLength[0]); + if (covNaN()) return false; + if (!solver.setA(helixAtOrigin.C.copy())) { + logger.fine("KalTrack:originHelix, cannot invert the covariance matrix"); + for (int i=0; i<5; ++i) { // Fill the matrix and inverse with something not too crazy and continue . . . + for (int j=0; j<5; ++j) { + if (i == j) { + Cinv.unsafe_set(i,j,1.0/Math.abs(helixAtOrigin.C.unsafe_get(i, j))); + helixAtOrigin.C.unsafe_set(i, j, Math.abs(helixAtOrigin.C.unsafe_get(i, j))); + } else { + Cinv.unsafe_set(i, j, 0.); + helixAtOrigin.C.unsafe_set(i, j, 0.); + } + } + } + } else { + solver.invert(Cinv); + } } // Find the position and momentum of the particle near the origin, including covariance @@ -597,31 +706,55 @@ public boolean originHelix() { return true; } + /** + * Get the extrapolate point in the plane of the origin + * @return 3-vector array of coordinates + */ public double[] originX() { if (!propagated) originHelix(); if (!propagated) return new double [] {0.,0.,0.}; return originPoint.v.clone(); } + /** + * Get the covariance of the track point in the origin plane + * @return 2D array, 3 by 3 + */ public double[][] originXcov() { return Cx.clone(); } + /** + * Get the track momentum at the origin + * @return 3-vector momentum + */ + public double[] originP() { + if (!propagated) originHelix(); + if (!propagated) return new double [] {0.,0.,0.}; + return originMomentum.v.clone(); + } + + /** + * Get the covariance of the momentum at the origin + * @return 3 by 3 array + */ public double[][] originPcov() { return Cp.clone(); } + /** + * Get the helix pivot point near the origin + * @return 3-vector array + */ public double[] originPivot() { if (propagated) return helixAtOrigin.X0.v.clone(); else return null; } - public double[] originP() { - if (!propagated) originHelix(); - if (!propagated) return new double [] {0.,0.,0.}; - return originMomentum.v.clone(); - } - + /** + * Get the covariance of helix parameters at the origin + * @return 5 by 5 array + */ public double[][] originCovariance() { if (!propagated) originHelix(); double [][] M = new double[5][5]; @@ -634,17 +767,30 @@ public double[][] originCovariance() { return M; } + /** + * Check whether all elements of the covariance are real numbers + * @return false if the covariance is rotten + */ public boolean covNaN() { if (helixAtOrigin.C == null) return true; return MatrixFeatures_DDRM.hasNaN(helixAtOrigin.C); } + /** + * Return the helix parameters of the track at the origin + * @return array of 5 doubles + */ public double[] originHelixParms() { if (propagated) return helixAtOrigin.a.v.clone(); else return null; } - //Update the helix parameters at the "origin" by using the target position or vertex as a constraint + /** + * Update the helix parameters at the "origin" by using the target position or vertex as a constraint + * @param vtx vertex location + * @param vtxCov vertex error matrix + * @return constrained helix state + */ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { if (!propagated) originHelix(); if (!propagated) return null; @@ -799,7 +945,14 @@ private static void matrixPrint(String s, double [][] A, int M, int N) { } } - //Find the helix turning alpha phi for the point on the helix 'a' closest to the point 'v' + /** + * Find the helix turning alpha phi for the point on the helix 'a' closest to the point 'v' + * @param a helix parameters + * @param v point v + * @param X0 pivot point + * @param alpha magnetic field constant + * @return turning angle + */ private static double phiDOCA(Vec a, Vec v, Vec X0, double alpha) { Plane p = new Plane(v, new Vec(0.,1.,0.)); @@ -815,12 +968,19 @@ private static double phiDOCA(Vec a, Vec v, Vec X0, double alpha) { return phiDoca; } - // Safe Newton-Raphson zero finding from Numerical Recipes in C, used here to solve for the point of closest approach. + /** + * Safe Newton-Raphson zero finding from Numerical Recipes in C, used here to solve for the point of closest approach. + * @param xGuess initial guess at the solution + * @param x1 minimum phi of solution + * @param x2 maximum phi of solution + * @param xacc accuracy + * @param a helix parameters + * @param v point of interest + * @param X0 pivot point of helix + * @param alpha magnetic field parameters + * @return phi of the DOCA point + */ private static double rtSafe(double xGuess, double x1, double x2, double xacc, Vec a, Vec v, Vec X0, double alpha) { - // Here xGuess is a starting guess for the phi angle of the helix DOCA point - // x1 and x2 give a range for the value of the solution - // xacc specifies the accuracy needed - // The output is an accurate result for the phi of the DOCA point double df, dx, dxold, f, fh, fl; double temp, xh, xl, rts; int MAXIT = 100; @@ -889,14 +1049,30 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V return rts; } - // Function that is zero when the helix turning angle phi is at the point of closest approach to v + /** + * Function that is zero when the helix turning angle phi is at the point of closest approach to v + * @param phi turning angle + * @param a helix parameters + * @param v point of interest + * @param X0 helix pivot point + * @param alpha magnetic field constant + * @return deviation from zero + */ private static double fDOCA(double phi, Vec a, Vec v, Vec X0, double alpha) { Vec t = tangentVec(a, phi, alpha); Vec x = HelixState.atPhi(X0, a, phi, alpha); return (v.dif(x)).dot(t); } - // derivative of the fDOCA function with respect to phi, for the zero-finding algorithm + /** + * derivative of the fDOCA function with respect to phi, for the zero-finding algorithm + * @param phi turning angle + * @param a helix parameters + * @param v point of interest + * @param X0 helix pivot point + * @param alpha magnetic field constant + * @return derivative + */ private static double dfDOCAdPhi(double phi, Vec a, Vec v, Vec X0, double alpha) { Vec x = HelixState.atPhi(X0, a, phi, alpha); Vec dxdphi = dXdPhi(a, phi, alpha); @@ -907,13 +1083,25 @@ private static double dfDOCAdPhi(double phi, Vec a, Vec v, Vec X0, double alpha) return dfdphi; } - // Derivatives of position along a helix with respect to the turning angle phi + /** + * Derivatives of position along a helix with respect to the turning angle phi + * @param a helix parameters + * @param phi turning angle + * @param alpha magnetic field parameter + * @return derivative + */ private static Vec dXdPhi(Vec a, double phi, double alpha) { return new Vec((alpha / a.v[2]) * FastMath.sin(a.v[1] + phi), -(alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), -(alpha / a.v[2]) * a.v[4]); } - // A vector tangent to the helix 'a' at the alpha phi + /** + * A vector tangent to the helix 'a' at the alpha phi + * @param a helix parameters + * @param phi turning angle + * @param alpha magnetic field parameter + * @return tangent vector + */ private static Vec tangentVec(Vec a, double phi, double alpha) { return new Vec((alpha/a.v[2])*FastMath.sin(a.v[1]+phi), -(alpha/a.v[2])*FastMath.cos(a.v[1]+phi), -(alpha/a.v[2])*a.v[4]); } @@ -922,14 +1110,16 @@ private static Vec dTangentVecDphi(Vec a, double phi, double alpha) { return new Vec((alpha/a.v[2])*FastMath.cos(a.v[1]+phi), (alpha/a.v[2])*FastMath.sin(a.v[2]+phi), 0.); } - //Derivative matrix for the helix 'a' point of closet approach to point 'v' + /** + * /Derivative matrix for the helix 'a' point of closet approach to point 'v' + * @param a helix parameters + * @param v 3D point for which we are finding the DOCA (the "measurement" point) + * @param X0 pivot point of helix + * @param phi angle along helix to the point of closet approach + * @param alpha constant to convert from curvature to 1/pt + * @return derivative matrix + */ private static double [][] buildH(Vec a, Vec v, Vec X0, double phi, double alpha) { - // a = helix parameters - // X0 = pivot point of helix - // phi = angle along helix to the point of closet approach - // v = 3D point for which we are finding the DOCA (the "measurement" point) - // alpha = constant to convert from curvature to 1/pt - Vec x = HelixState.atPhi(X0, a, phi, alpha); Vec dxdphi = dXdPhi(a, phi, alpha); Vec t = tangentVec(a, phi, alpha); @@ -974,21 +1164,40 @@ private static Vec dTangentVecDphi(Vec a, double phi, double alpha) { return H; } + /** + * Return the error on one of the helix parameters at the origin + * @param i 0 to 4 to select which parameter + * @return the error estimate + */ public double helixErr(int i) { return FastMath.sqrt(helixAtOrigin.C.unsafe_get(i, i)); } + /** + * Rotate a 3-vector from local field coordinates to global coordinates + * @param x input 3-vector + * @return output 3-vector + */ public double[] rotateToGlobal(double[] x) { Vec xIn = new Vec(x[0], x[1], x[2]); return Rot.inverseRotate(xIn).v.clone(); } + /** + * Rotate a 3-vector from global coordinates to local field coordinates + * @param x input 3-vector + * @return output 3-vector + */ public double[] rotateToLocal(double[] x) { Vec xIn = new Vec(x[0], x[1], x[2]); return Rot.rotate(xIn).v.clone(); } - // Figure out which measurement site on this track points to a given detector module + /** + * Figure out which measurement site on this track points to a given detector module + * @param module silicon module + * @return measurement site + */ public int whichSite(SiModule module) { if (lyrMap != null) { return SiteList.indexOf(lyrMap.get(module.Layer)); @@ -1001,11 +1210,22 @@ public int whichSite(SiModule module) { return -1; } + /** + * Sort the measurement sites in order of SVT layer number + * @param ascending true for ascending order, false for descending + */ public void sortSites(boolean ascending) { if (ascending) Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); else Collections.sort(SiteList, MeasurementSite.SiteComparatorDn); } + /** + * Remove a selected hit from a KalTrack object. Try to add a different hit. + * @param site The measurement site of the hit to be removed + * @param mxChi2Inc Maximum chi^2 increment to add another hit in the same layer + * @param mxTdif Maximum time difference of all hits to add another hit in the same layer + * @return + */ public boolean removeHit(MeasurementSite site, double mxChi2Inc, double mxTdif) { boolean exchange = false; if (debug) System.out.format("Event %d track %d remove hit %d on layer %d detector %d\n", @@ -1043,7 +1263,15 @@ public boolean removeHit(MeasurementSite site, double mxChi2Inc, double mxTdif) return exchange; } - // Try to add missing hits to the track + /** + * Try to add missing hits to the track + * @param data All the SVT data + * @param mxResid Maximum residual + * @param mxChi2inc Maximum chi^2 increase + * @param mxTdif Maximum time difference of all hits, including the new one + * @param verbose true to spew lots of printout + * @return number of hits added + */ public int addHits(ArrayList data, double mxResid, double mxChi2inc, double mxTdif, boolean verbose) { int numAdded = 0; int numLayers = 14; @@ -1142,9 +1370,12 @@ public int addHits(ArrayList data, double mxResid, double mxChi2inc, d return numAdded; } - // re-fit the track + /** + * Re-fit the track + * @param keep true if there might be another recursion after dropping more hits + * @return true if the refit was successful + */ public boolean fit(boolean keep) { - // keep = true if there might be another recursion after dropping more hits double chi2s = 0.; if (debug) System.out.format("Entering KalTrack.fit for event %d, track %d\n", eventNumber, ID); StateVector sH = SiteList.get(0).aS.copy(); @@ -1247,7 +1478,11 @@ public boolean fit(boolean keep) { return true; } - // Derivative matrix for propagating the covariance of the helix parameters to a covariance of momentum + /** + * Derivative matrix for propagating the covariance of the helix parameters to a covariance of momentum + * @param a helix parameters + * @return 2D array of derivatives + */ static double[][] DpTOa(Vec a) { double[][] M = new double[3][5]; double K = Math.abs(a.v[2]); @@ -1261,8 +1496,11 @@ static double[][] DpTOa(Vec a) { return M; } - // Derivative matrix for propagating the covariance of the helix parameter to a - // covariance of the point of closest approach to the origin (i.e. at phi=0) + /** + * Derivative matrix for propagating the covariance of the helix parameter to a + * covariance of the point of closest approach to the origin (i.e. at phi=0) + * @param a helix parameters + */ static double[][] DxTOa(Vec a) { double[][] M = new double[3][5]; M[0][0] = FastMath.cos(a.v[1]); @@ -1273,7 +1511,9 @@ static double[][] DxTOa(Vec a) { return M; } - // Comparator function for sorting tracks by quality + /** + * Comparator function for sorting tracks by quality + */ static Comparator TkrComparator = new Comparator() { public int compare(KalTrack t1, KalTrack t2) { double penalty1 = 1.0; @@ -1287,6 +1527,11 @@ public int compare(KalTrack t1, KalTrack t2) { } }; + /** + * Transform a DMatrixRMaj object into a Kalman SquareMatrix object + * @param M DMatrixRMaj object + * @return the SquareMatrix equivalent + */ static SquareMatrix mToS(DMatrixRMaj M) { SquareMatrix S = new SquareMatrix(M.numRows); if (M.numCols != M.numRows) return null; @@ -1298,6 +1543,11 @@ static SquareMatrix mToS(DMatrixRMaj M) { return S; } + /** + * Check whether two tracks are redundant. + * @param other the other track object besides self + * @return true if they are equivalent + */ @Override public boolean equals(Object other) { // Consider two tracks to be equal if they have the same hits if (this == other) return true; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java index 691ce0f912..07260d4c26 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java @@ -37,8 +37,11 @@ import org.lcsim.util.Driver; import org.lcsim.util.aida.AIDA; import org.lcsim.event.MCParticle; - -// $ java -jar ./distribution/target/hps-distribution-4.0-SNAPSHOT-bin.jar -b -DoutputFile=output -d HPS-EngRun2015-Nominal-v4-4-fieldmap -i tracking/tst_4-1.slcio -n 1 -R 5772 steering-files/src/main/resources/org/hps/steering/recon/KalmanTest.lcsim +/** + * + * This driver was used originally to test Kalman-Filter track fitting using hits on tracks found by SeedTracker and fit by GBL. + * + */ public class KalmanDriverHPS extends Driver { private ArrayList detPlanes; @@ -170,7 +173,8 @@ public void detectorChanged(Detector det) { sensors = det.getSubdetector("Tracker").getDetectorElement().findDescendants(HpsSiSensor.class); KalmanParams kPar = new KalmanParams(); - KI = new KalmanInterface(this.uniformB, kPar, fm); + kPar.setUniformB(uniformB); + KI = new KalmanInterface(kPar, fm); KI.createSiModules(detPlanes); System.out.format("KalmanDriver: the B field is assumed uniform? %b\n", uniformB); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 9f21a50efb..2c154ce45e 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -50,7 +50,8 @@ /** * This class provides an interface between hps-java and the Kalman Filter fitting and pattern recognition code. * It can be used to refit the hits on an existing hps track, or it can be used to drive the pattern recognition. - * However, both cannot be done at the same time. The interface must be reset between doing one and the other. + * However, both cannot be done at the same time. The interface must be reset between doing one and the other and + * should also be reset after each event, before going to the next event. */ public class KalmanInterface { private Detector det; @@ -62,6 +63,7 @@ public class KalmanInterface { private ArrayList SiMlist; private List SeedTrackLayers = null; private int _siHitsLimit = -1; + private static boolean uniformB; double alphaCenter; private List detPlanes; double svtAngle; @@ -73,7 +75,6 @@ public class KalmanInterface { public static RotMatrix HpsSvtToKalman; public static RotMatrix KalmanToHpsSvt; public static BasicHep3Matrix HpsSvtToKalmanMatrix; - private static boolean uniformB; private static DMatrixRMaj tempM; private static DMatrixRMaj Ft; private int maxHits; @@ -81,23 +82,42 @@ public class KalmanInterface { private int eventNumber; private static final boolean debug = false; - private static final double SVTcenter = 505.57; private static final double c = 2.99793e8; // Speed of light in m/s + /** + * Set the limit on the maximum number of hits in an event to analyze (larger events will be skipped) + * + * @param limit desired hit limit + */ public void setSiHitsLimit(int limit) { _siHitsLimit = limit; } + /** + * Get the current setting on the maximum number of hits to analyze + * + * @return hit limit + */ public int getSiHitsLimit() { return _siHitsLimit; } - // Get the HPS tracker hit corresponding to a Kalman hit + /** + * Get the HPS tracker hit corresponding to a Kalman hit + * + * @param km Kalman Measurement object + * @return identity of the HPS hit + */ public TrackerHit getHpsHit(Measurement km) { return hitMap.get(km); } - // Get the HPS sensor that corresponds to a Kalman SiModule + /** + * Get the HPS sensor that corresponds to a Kalman SiModule + * + * @param kalmanSiMod Kalman SiModule object (geometry of one sensor) + * @return HpsSiSensor object + */ public HpsSiSensor getHpsSensor(SiModule kalmanSiMod) { if (moduleMap == null) return null; else { @@ -107,54 +127,80 @@ public HpsSiSensor getHpsSensor(SiModule kalmanSiMod) { } } - // Return the entire map relating HPS sensors to Kalman SiModule objects + /** + * Get the entire map relating HPS sensors to Kalman SiModule objects + * + * @return Map from HPS sensors to Kalman SiModule objects + */ public Map getModuleMap() { return moduleMap; } - // The HPS field map is in the HPS global coordinate system. This routine includes the transformations - // to return the field in the Kalman global coordinate system given a coordinate in the same system. + /** + * Get the HPS B field at a provided location. The HPS field map is in the HPS global coordinate system. + * This routine includes the transformations to return the field in the Kalman global coordinate system + * given a coordinate in the same system. Don't confuse this with org.lcsim.geometry.FieldMap.getField, + * which works with double arrays in HPS coordinates. + * + * @param kalPos Kalman 3-vector object specifying the position + * @param hpsFm HPS field map + * @return Kalman 3-vector object specifying the magnetic field at the given location + */ static Vec getField(Vec kalPos, org.lcsim.geometry.FieldMap hpsFm) { return new Vec(3, getFielD(kalPos, hpsFm)); } - + + /** + * Get the HPS B field at a provided location for stand-alone running of the Kalman code. + * For hps-java use KalmanInterface.getField to return a 3-vector Vec object. + * + * @param kalPos Kalman 3-vector object specifying the position + * @param hpsFm HPS field map + * @return Kalman 3-vector object specifying the magnetic field at the given location + */ static double [] getFielD(Vec kalPos, org.lcsim.geometry.FieldMap hpsFm) { // Field map for stand-alone running if (FieldMap.class.isInstance(hpsFm)) return ((FieldMap) (hpsFm)).getField(kalPos); - // Standard field map for running in hps-java + // Interface to the standard field map for running in hps-java, including conversions for Kalman coordinates //System.out.format("Accessing HPS field map for position %8.3f %8.3f %8.3f\n", kalPos.v[0], kalPos.v[1], kalPos.v[2]); double[] hpsPos = { kalPos.v[0], -1.0 * kalPos.v[2], kalPos.v[1] }; - if (uniformB) { - hpsPos[0] = 0.; - hpsPos[1] = 0.; - hpsPos[2] = SVTcenter; - } else { - if (hpsPos[1] > 70.0) hpsPos[1] = 70.0; // To avoid getting a field returned that is identically equal to zero - if (hpsPos[1] < -70.0) hpsPos[1] = -70.0; - if (hpsPos[0] < -225.) hpsPos[0] = -225.; - if (hpsPos[0] > 270.) hpsPos[0] = 270.; - } + //hpsPos[0] = 0.; + //hpsPos[1] = 0.; + //hpsPos[2] = 505.57; + + // To avoid going off the map and getting a field returned that is identically equal to zero + if (hpsPos[1] > 70.0) hpsPos[1] = 70.0; + if (hpsPos[1] < -70.0) hpsPos[1] = -70.0; + if (hpsPos[0] < -225.) hpsPos[0] = -225.; + if (hpsPos[0] > 270.) hpsPos[0] = 270.; double[] hpsField = hpsFm.getField(hpsPos); - if (uniformB) { - double [] kalField = {0., 0., -1.0 * hpsField[1]}; - return kalField; - } double [] kalField = {hpsField[0], hpsField[2], -1.0 * hpsField[1]}; return kalField; } - // Set the layers to be used for finding seed tracks (not used by Kalman pattern recognition) + /** + * Set the layers to be used for finding seed tracks (not used by Kalman pattern recognition) + * @param input List of layers + */ public void setSeedTrackLayers(List input) { SeedTrackLayers = input; } - // Constructor with no uniformB argument defaults to non-uniform field - public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { - this(false, kPar, fM); - } - + /** + * alternate constructor for backward compatibility + */ public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { + this(kPar, fM); + } + + /** + * Constructor for the interface between the Kalman-filter tracking code and other HPS-Java code + * + * @param kPar Object containing all the control parameters for the Kalman code + * @param fM The HPS field map + */ + public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { this.det = det; this.sensors = sensors; @@ -168,7 +214,6 @@ public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.F tempM = new DMatrixRMaj(5,5); Ft = new DMatrixRMaj(5,5); - KalmanInterface.uniformB = uniformB; hitMap = new HashMap(); simHitMap = new HashMap(); moduleMap = new HashMap(); @@ -180,9 +225,10 @@ public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.F SeedTrackLayers.add(4); SeedTrackLayers.add(5); - if (uniformB) { + if (kPar.uniformB) { logger.log(Level.WARNING, "KalmanInterface WARNING: the magnetic field is set to a uniform value."); } + uniformB = kPar.uniformB; // Transformation from HPS SVT tracking coordinates to Kalman global coordinates double[][] HpsSvtToKalmanVals = { { 0, 1, 0 }, { 1, 0, 0 }, { 0, 0, -1 } }; @@ -211,31 +257,53 @@ public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.F kPat = new KalmanPatRecHPS(kPar); - Vec centerB = KalmanInterface.getField(new Vec(0., SVTcenter, 0.), fM); + Vec centerB = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), fM); + logger.log(Level.INFO, String.format("The magnetic field at the detector center (%10.5f mm) is %10.5f\n", kPar.SVTcenter, centerB.mag())); double conFac = 1.0e12 / c; alphaCenter = conFac/ centerB.mag(); } + /** + * End-of-job formatted printout of some counters. + */ public void summary() { - System.out.format("KalmanInterface::summary: number of events with > 200 hits=%d.\n", nBigEvents); + System.out.format("KalmanInterface::summary: number of events with > 200 hits = %d.\n", nBigEvents); System.out.format(" Maximum event size = %d strip hits.\n", maxHits); - System.out.format(" Events with > %d hits were not processed.\n", _siHitsLimit); - System.out.format(" Number of tracks with bad covariance in filterTrack= %d %d\n", KalmanPatRecHPS.nBadCov[0], KalmanPatRecHPS.nBadCov[1]); - System.out.format(" Number of tracks with bad covariance in KalTrack.fit=%d %d\n", KalTrack.nBadCov[0], KalTrack.nBadCov[1]); + if (_siHitsLimit > 0) { + System.out.format(" Events with > %d hits were not processed.\n", _siHitsLimit); + } + System.out.format(" Number of tracks with bad covariance in filterTrack= %d is %d\n", KalmanPatRecHPS.nBadCov[0], KalmanPatRecHPS.nBadCov[1]); + System.out.format(" Number of tracks with bad covariance in KalTrack.fit=%d is %d\n", KalTrack.nBadCov[0], KalTrack.nBadCov[1]); } - // Return the reference to the parameter setting code for the driver to use + /** + * Return the reference to the parameter setting object for the driver to use + * + * @return reference to the parameter setting object KalParams.java + */ public KalmanParams getKalmanParams() { return kPar; } - // Transformation from HPS global coordinates to Kalman global coordinates + /** + * Transformation from HPS global coordinates to Kalman global coordinates + * + * @param HPSvec 3-vector array of HPS global coordinates + * @return 3-vector object of Kalman global coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. + */ public static Vec vectorGlbToKalman(double[] HPSvec) { Vec kalVec = new Vec(HPSvec[0], HPSvec[2], -HPSvec[1]); return kalVec; } - // Transformation from Kalman global coordinates to HPS global coordinates + /** + * Transformation from Kalman global coordinates to HPS global coordinates + * + * @param KalVec 3-vector object of Kalman global coordinates + * @return 3-vector array of HPS global coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. + */ public static double[] vectorKalmanToGlb(Vec KalVec) { double[] HPSvec = new double[3]; HPSvec[0] = KalVec.v[0]; @@ -244,7 +312,13 @@ public static double[] vectorKalmanToGlb(Vec KalVec) { return HPSvec; } - // Transformation from Kalman global coordinates to HPS tracking coordinates + /** + * Transformation from Kalman global coordinates to HPS tracking coordinates + * + * @param KalVec Kalman 3-vector object in Kalman global coordinates + * @return HPS tracking coordinates a 3-vector array + * @see Definitions of coordinate systems in the Kalman pdf documentation. + */ public static double[] vectorKalmanToTrk(Vec KalVec) { double[] HPSvec = new double[3]; HPSvec[0] = KalVec.v[1]; @@ -253,19 +327,37 @@ public static double[] vectorKalmanToTrk(Vec KalVec) { return HPSvec; } - // Transformation from HPS tracking coordinates to Kalman global coordinates + /** + * Transformation from HPS tracking coordinates to Kalman global coordinates + * + * @param HPSvec HPS tracking coordinates a 3-vector array + * @return Kalman 3-vector object in Kalman global coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. + */ public static Vec vectorTrkToKalman(double[] HPSvec) { Vec kalVec = new Vec(HPSvec[1], HPSvec[0], -HPSvec[2]); return kalVec; } - // Transformation from HPS sensor coordinates to Kalman sensor coordinates + /** + * Transformation from HPS sensor coordinates to Kalman sensor coordinates + * + * @param HPSvec HPS 3-vector sensor coordinate double-precision array + * @return Kalman 3-vector object in Kalman sensor coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. + */ public static Vec localHpsToKal(double[] HPSvec) { Vec kalVec = new Vec(-HPSvec[1], HPSvec[0], HPSvec[2]); return kalVec; } - // Transformation from Kalman sensor coordinates to HPS sensor coordinates + /** + * Transformation from Kalman sensor coordinates to HPS sensor coordinates + * + * @param KalVec 3-vector object in Kalman coordinates + * @return 3-vector double-precision array in HPS coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. + */ public static double[] localKalToHps(Vec KalVec) { double[] HPSvec = new double[3]; HPSvec[0] = KalVec.v[1]; @@ -274,12 +366,19 @@ public static double[] localKalToHps(Vec KalVec) { return HPSvec; } - // Return the entire list of Kalman SiModule + /** + * Return the entire list of Kalman SiModule (geometry description for the Kalman filter code) + * + * @return List of all SiModule objects + */ public ArrayList getSiModuleList() { return SiMlist; } - // Return a list of all measurements for all SiModule + /** + * Return a list of all measurements for all SiModule + * @return List of all Measurement objects in the detector + */ public ArrayList getMeasurements() { ArrayList measList = new ArrayList();; for (SiModule SiM : SiMlist) { @@ -290,7 +389,9 @@ public ArrayList getMeasurements() { return measList; } - // Clear the event hit and track information without deleting the SiModule geometry information + /** + * Clear the Kalman interface event hit and track information without deleting the SiModule geometry information + */ public void clearInterface() { logger.fine("Clearing the Kalman interface"); hitMap.clear(); @@ -300,83 +401,85 @@ public void clearInterface() { SiM.hits.clear(); } } - - // Create an HPS TrackState from a Kalman HelixState at the location of a particular SiModule - public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoothed) { - // Note that the helix parameters that get stored in the TrackState assume a B-field exactly oriented in the - // z direction and a pivot point at the origin (0,0,0). The referencePoint of the TrackState is set to the - // intersection point with the detector plane. - StateVector sv = null; - if (useSmoothed) { - if (!ms.smoothed) return null; - sv = ms.aS; - } else { // using the filtered state is really not recommended - if (!ms.filtered) return null; - sv = ms.aF; - } - - return sv.helix.toTrackState(alphaCenter, ms.m.p, loc); - } - - // Transform a Kalman helix to an HPS helix rotated to the global frame and with the pivot at the origin - // Provide covHPS with 15 elements to get the covariance as well - // Provide 3-vector position to get the location in HPS global coordinates of the original helix pivot + + /** + * Transform a Kalman helix to an HPS helix rotated to the global frame and with the pivot at the origin + * The calling routine must provide empty covHPS[15] and position[3] arrays. + * No rotations are needed if the field is assumed to be uniform. + * + * @param helixState description of the Kalman helix + * @param pln Kalman plane object (origin point and t,u,v axis directions) + * @param alphaCenter alpha value to use to convert between momentum and curvature + * @param covHPS return array of LCSIM helix parameter covariance (15 values for the symmetric matrix) + * @param position return 3-vector array of the location in HPS global coordinates of the original helix pivot + * @return array of 5 LCSIM helix parameters + */ static double [] toHPShelix(HelixState helixState, Plane pln, double alphaCenter, double [] covHPS, double[] position) { final boolean debug = false; + Vec finalHelixParams = null; + Vec pivotGlobal = null; + DMatrixRMaj F = new DMatrixRMaj(5,5); + DMatrixRMaj covRotated = new DMatrixRMaj(5,5); double phiInt = helixState.planeIntersect(pln); if (Double.isNaN(phiInt)) { Logger logger = Logger.getLogger(KalmanInterface.class.getName()); logger.fine(String.format("toHPShelix: no intersection with the plane at %s",pln.toString())); phiInt = 0.; } - // Transforms helix to a pivot point on the helix itself (so rho0 and z0 become zero) Vec newPivot = helixState.atPhi(phiInt); - Vec helixParamsPivoted = helixState.pivotTransform(newPivot); - DMatrixRMaj F = new DMatrixRMaj(5,5); - helixState.makeF(helixParamsPivoted, F); - if (debug) { - System.out.format("Entering KalmanInterface.toHPShelix"); - helixState.print("provided"); - pln.print("provided"); - newPivot.print("new pivot"); - helixParamsPivoted.print("pivoted helix params"); - System.out.format("turning angle to the plane containing the helixState origin=%10.6f\n", phiInt); - Vec intGlb = helixState.toGlobal(newPivot); - intGlb.print("global intersection with plane"); - } - - // Then rotate the helix to the global system. This isn't quite kosher, since the B-field will not - // be aligned with the global system in general, but we have to do it to fit back into the HPS TrackState - // coordinate convention, for which the field is assumed to be uniform and aligned. - DMatrixRMaj fRot = new DMatrixRMaj(5,5); - Vec helixParamsRotated = HelixState.rotateHelix(helixParamsPivoted, helixState.Rot.invert(), fRot); - CommonOps_DDRM.mult(fRot, F, Ft); - - CommonOps_DDRM.multTransB(helixState.C, Ft, tempM); - DMatrixRMaj covRotated = new DMatrixRMaj(5,5); - CommonOps_DDRM.mult(Ft, tempM, covRotated); - if (debug) helixParamsRotated.print("rotated helix params"); - - // Transform the pivot to the global system. - Vec pivotGlobal = helixState.toGlobal(newPivot); - if (debug) pivotGlobal.print("pivot in global system"); - - // Pivot transform to the final pivot at the origin Vec finalPivot = new Vec(0.,0.,0.); - Vec finalHelixParams = HelixState.pivotTransform(finalPivot, helixParamsRotated, pivotGlobal, alphaCenter, 0.); - HelixState.makeF(finalHelixParams, F, helixParamsRotated, alphaCenter); - CommonOps_DDRM.multTransB(covRotated, F, tempM); - CommonOps_DDRM.mult(F, tempM, covRotated); - if (debug) { - finalPivot.print("final pivot point"); - finalHelixParams.print("final helix parameters"); - HelixPlaneIntersect hpi = new HelixPlaneIntersect(); - phiInt = hpi.planeIntersect(finalHelixParams, finalPivot, alphaCenter, pln); - if (!Double.isNaN(phiInt)) { - Vec rInt = HelixState.atPhi(finalPivot, finalHelixParams, phiInt, alphaCenter); - rInt.print("final helix intersection with given plane"); + if (!uniformB) { + // Transform helix to a pivot point on the helix itself (so rho0 and z0 become zero) + Vec helixParamsPivoted = helixState.pivotTransform(newPivot); + helixState.makeF(helixParamsPivoted, F); + if (debug) { + System.out.format("Entering KalmanInterface.toHPShelix"); + helixState.print("provided"); + pln.print("provided"); + newPivot.print("new pivot"); + helixParamsPivoted.print("pivoted helix params"); + System.out.format("turning angle to the plane containing the helixState origin=%10.6f\n", phiInt); + Vec intGlb = helixState.toGlobal(newPivot); + intGlb.print("global intersection with plane"); + } + + // Then rotate the helix to the global system. This isn't quite kosher, since the B-field will not + // be aligned with the global system in general, but we have to do it to fit back into the HPS TrackState + // coordinate convention, for which the field is assumed to be uniform and aligned. + DMatrixRMaj fRot = new DMatrixRMaj(5,5); + Vec helixParamsRotated = HelixState.rotateHelix(helixParamsPivoted, helixState.Rot.invert(), fRot); + CommonOps_DDRM.mult(fRot, F, Ft); + + CommonOps_DDRM.multTransB(helixState.C, Ft, tempM); + CommonOps_DDRM.mult(Ft, tempM, covRotated); + if (debug) helixParamsRotated.print("rotated helix params"); + + // Transform the pivot to the global system. + pivotGlobal = helixState.toGlobal(newPivot); + if (debug) pivotGlobal.print("pivot in global system"); + + // Pivot transform to the final pivot at the origin + finalHelixParams = HelixState.pivotTransform(finalPivot, helixParamsRotated, pivotGlobal, alphaCenter, 0.); + HelixState.makeF(finalHelixParams, F, helixParamsRotated, alphaCenter); + CommonOps_DDRM.multTransB(covRotated, F, tempM); + CommonOps_DDRM.mult(F, tempM, covRotated); + if (debug) { + finalPivot.print("final pivot point"); + finalHelixParams.print("final helix parameters"); + HelixPlaneIntersect hpi = new HelixPlaneIntersect(); + phiInt = hpi.planeIntersect(finalHelixParams, finalPivot, alphaCenter, pln); + if (!Double.isNaN(phiInt)) { + Vec rInt = HelixState.atPhi(finalPivot, finalHelixParams, phiInt, alphaCenter); + rInt.print("final helix intersection with given plane"); + } + System.out.format("Exiting KalmanInterface.toHPShelix\n"); } - System.out.format("Exiting KalmanInterface.toHPShelix\n"); + } else { // For a uniform field, all we have to do is a pivot transform to the origin + pivotGlobal = newPivot; // Intersection with the provided plane + finalHelixParams = helixState.pivotTransform(finalPivot); + helixState.makeF(finalHelixParams, F); + CommonOps_DDRM.multTransB(helixState.C, F, tempM); + CommonOps_DDRM.mult(F, tempM, covRotated); } if (covHPS != null) { double [] temp = KalmanInterface.getLCSimCov(covRotated, alphaCenter).asPackedArray(true); @@ -389,6 +492,15 @@ public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoot return KalmanInterface.getLCSimParams(finalHelixParams.v, alphaCenter); } + /** + * Transform a Kalman HelixState to an HPS TrackState. + * + * @param helixState The Kalman HelixState object. + * @param pln The Kalman Plane object (origin point and t,u,v axis directions) + * @param alphaCenter The alpha value used to calculate momentum from curvature. + * @param loc Integer representation of the TrackState location (AtIP, AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) + * @return The HPS TrackState + */ static TrackState toTrackState(HelixState helixState, Plane pln, double alphaCenter, int loc) { double [] covHPS = new double[15]; double [] position = new double[3]; @@ -397,6 +509,34 @@ static TrackState toTrackState(HelixState helixState, Plane pln, double alphaCen return new BaseTrackState( helixHPS, covHPS, position, loc); } + /** + * Create an HPS TrackState from a Kalman HelixState at the location of a particular SiModule + * + * @param ms Kalman MeasurementSite object + * @param loc Integer representation of the TrackState location (AtIP, AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) + * @param useSmoothed Choose between the smoothed vs filtered Kalman helix + * @return The HPS TrackState + */ + public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoothed) { + // Note that the helix parameters that get stored in the TrackState assume a B-field exactly oriented in the + // z direction and a pivot point at the origin (0,0,0). The referencePoint of the TrackState is set to the + // intersection point with the detector plane. + StateVector sv = null; + if (useSmoothed) { + if (!ms.smoothed) return null; + sv = ms.aS; + } else { // using the filtered state is really not recommended + if (!ms.filtered) return null; + sv = ms.aF; + } + return sv.helix.toTrackState(alphaCenter, ms.m.p, loc); + } + + /** + * Formatted debug printout of a single GBLStripClusterData object. + * + * @param clstr The GBLStripClusterData object to be printed + */ public void printGBLStripClusterData(GBLStripClusterData clstr) { System.out.format("\nKalmanInterface.printGBLStripClusterData: cluster ID=%d, scatterOnly=%d\n", clstr.getId(), clstr.getScatterOnly()); Pair IDdecode = TrackUtils.getLayerSide(clstr.getVolume(), clstr.getId()); @@ -412,12 +552,16 @@ public void printGBLStripClusterData(GBLStripClusterData clstr) { System.out.format(" RMS projected scattering angle=%10.6f\n", clstr.getScatterAngle()); } - // Make a GBLStripClusterData object for each MeasurementSite of a Kalman track + /** + * Make a GBLStripClusterData object for each MeasurementSite of a Kalman track. This can be used to refit the track using + * the GBL program. + * + * @param kT The Kalman track + * @return List of strip-cluster data for this Kalman track + */ public List createGBLStripClusterData(KalTrack kT) { List rtnList = new ArrayList(kT.SiteList.size()); - - //Arc length from origin to first plane - + double phi_org = kT.originHelixParms()[1]; double phi_1state = kT.SiteList.get(0).aS.helix.a.v[1]; double alpha = kT.helixAtOrigin.alpha; @@ -508,14 +652,29 @@ public List createGBLStripClusterData(KalTrack kT) { return rtnList; } - // Propagate a TrackState "stateHPS" to a plane given by "location" and "direction". - // The PropagatedTrackState object created includes the new propagated TrackState plus information on - // the intersection point of the track with the plane. + /** + * Propagate a TrackState "stateHPS" to a plane given by "location" and "direction". + * The PropagatedTrackState object created includes the new propagated TrackState plus information on + * the intersection point of the track with the plane. This propagations is done using the full field map, + * even if kPar.uniformB is true. + * + * @param stateHPS HPS trackstate at the plane in question + * @param location HPS location description (AtIP, AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) + * @param direction Direction cosines of the plane + * @return Kalman PropagatedTrackState object + */ + public PropagatedTrackState propagateTrackState(TrackState stateHPS, double [] location, double [] direction) { - return new PropagatedTrackState(stateHPS, location, direction, detPlanes, fM); + return new PropagatedTrackState(stateHPS, location, direction, detPlanes, kPar.SVTcenter, fM); } - // Create an HPS track from a Kalman track + /** + * Create an HPS track from a Kalman track + * + * @param kT Kalman track object + * @param storeTrackStates true to store in addition all of the track states + * @return HPS track + */ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { if (kT.SiteList == null) { logger.log(Level.WARNING, "KalmanInterface.createTrack: Kalman track is incomplete."); @@ -537,7 +696,7 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { // center of the magnet), so we need to use the magnetic field there to convert from 1/pt to curvature. // Field at the origin => For 2016 this is ~ 0.430612 T // In the center of SVT => For 2016 this is ~ 0.52340 T - Vec Bfield = KalmanInterface.getField(new Vec(0., SVTcenter ,0.), kT.SiteList.get(0).m.Bfield); + Vec Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter ,0.), kT.SiteList.get(0).m.Bfield); double B = Bfield.mag(); double[] newParams = getLCSimParams(globalParams, alphaCenter); double[] newCov = getLCSimCov(globalCov, alphaCenter).asPackedArray(true); @@ -602,7 +761,13 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { return newTrack; } - // Convert helix parameters from Kalman to LCSim + /** + * Convert helix parameters from Kalman to LCSim + * + * @param oldParams Array of 5 Kalman helix parameters + * @param alpha Parameter to use to convert between momentum and curvature + * @return Array of 5 LCSim helix parameters + */ static double[] getLCSimParams(double[] oldParams, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. @@ -616,7 +781,13 @@ static double[] getLCSimParams(double[] oldParams, double alpha) { return params; } - // Convert helix parameters from LCSim to Kalman + /** + * Convert helix parameters from LCSim to Kalman + * + * @param oldParams Array of 5 LCSim helix parameters + * @param alpha Parameter to use to convert between momentum and curvature + * @return Array of 5 Kalman helix parameters + */ static double[] unGetLCSimParams(double[] oldParams, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. @@ -629,7 +800,13 @@ static double[] unGetLCSimParams(double[] oldParams, double alpha) { return params; } - // Convert helix parameter covariance matrix from Kalman to LCSim + /** + * Convert helix parameter covariance matrix from Kalman to LCSim + * + * @param oldCov Kalman covariance matrix + * @param alpha Parameter to use to convert between momentum and curvature + * @return Covariance matrix of LCSim helix parameters + */ static SymmetricMatrix getLCSimCov(DMatrixRMaj oldCov, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. @@ -649,7 +826,13 @@ static SymmetricMatrix getLCSimCov(DMatrixRMaj oldCov, double alpha) { return cov; } - // Convert helix parameter covariance matrix from LCSim to Kalman + /** + * Convert helix parameter covariance matrix from LCSim to Kalman + * + * @param oldCov LCSIM symmetric covariance matrix as a linear array of 15 elements + * @param alpha Parameter to use to convert between momentum and curvature + * @return 5 by 5 covariance matrix for Kalman helix parameters + */ static double[][] ungetLCSimCov(double[] oldCov, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. @@ -678,7 +861,12 @@ static double[][] ungetLCSimCov(double[] oldCov, double alpha) { return cov; } - // Used only for Kalman tracks made by fitting hits on a GBL track + /** + * Add hits to an existing track + * Used only for Kalman tracks made by fitting hits on a GBL track + * + * @param newTrack Track to which hits will be added + */ private void addHitsToTrack(BaseTrack newTrack) { List measList = getMeasurements(); @@ -691,7 +879,12 @@ private void addHitsToTrack(BaseTrack newTrack) { newTrack.setNDF(newTrack.getTrackerHits().size()); } - // Create an HPS track from a Kalman seed + /** + * Create an HPS track from a Kalman seed + * + * @param trk Seed + * @return HPS track + */ public BaseTrack createTrack(SeedTrack trk) { double[] newPivot = { 0., 0., 0. }; double[] params = getLCSimParams(trk.pivotTransform(newPivot), alphaCenter); @@ -706,7 +899,11 @@ public BaseTrack createTrack(SeedTrack trk) { return newTrack; } - // Method to create one Kalman SiModule object for each silicon-strip detector + /** + * Method to create one Kalman SiModule geometry object for each silicon-strip detector + * + * @param inputPlanes List of all the silicon-strip detector planes + */ public void createSiModules(List inputPlanes) { if (debug) { System.out.format("Entering KalmanInterface.creasteSiModules\n"); @@ -792,8 +989,14 @@ public void createSiModules(List inputPlanes) { } } - // Method to feed simulated hits into the pattern recognition, for testing - private boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { + /** + * Method to feed simulated hits into the pattern recognition, for testing + * + * @param event Event header + * @param decoder Decoder for unpacking hit information + * @return true if it executed successfully + */ + boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { boolean success = false; eventNumber = event.getEventNumber(); @@ -872,7 +1075,12 @@ private boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { return success; } - // Method to fill all Si hits into the SiModule objects, to feed to the pattern recognition. + /** + * Method to fill all Si hits into the SiModule objects, to feed to the pattern recognition. + * + * @param event Event header + * @return true if it executed successfully + */ private boolean fillAllMeasurements(EventHeader event) { boolean success = false; eventNumber = event.getEventNumber(); @@ -882,8 +1090,8 @@ private boolean fillAllMeasurements(EventHeader event) { List striphits = event.get(TrackerHit.class, stripHitInputCollectionName); if (striphits.size() > maxHits) maxHits = striphits.size(); - if (striphits.size() > 200) nBigEvents++; if (_siHitsLimit > 0 && striphits.size() > _siHitsLimit) { + nBigEvents++; System.out.format("KalmanInterface::Skip event %d with %s %d hits > %d\n", event.getEventNumber(), stripHitInputCollectionName, striphits.size(), _siHitsLimit); return false; @@ -1048,7 +1256,13 @@ private boolean fillAllMeasurements(EventHeader event) { return success; } - // Method to fill the Si hits into the SiModule objects for a given track, in order to refit the track + /** + * Method to fill the Si hits into the SiModule objects for a given GBL track, in order to refit the track using Kalman filter + * + * @param hits1D 1-dimensional tracker hits + * @param addMode 0 to skip layers not already having hits; 1 to skip layers already having hits + * @return + */ private double fillMeasurements(List hits1D, int addMode) { double firstZ = 10000; Map> hitsMap = new HashMap>(); @@ -1124,15 +1338,32 @@ private double fillMeasurements(List hits1D, int addMode) { return firstZ; } - // Make a linear fit to a set of hits to be used to initialize the Kalman Filter + /** + * Make a linear fit to a set of hits to be used to initialize the Kalman Filter + * + * @param track GBL track + * @param hitToStrips Relation table for GBL hits to strips + * @param hitToRotated Relation table for GBL hits to rotated hits + * @return New track seed + */ public SeedTrack createKalmanSeedTrack(Track track, RelationalTable hitToStrips, RelationalTable hitToRotated) { List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); double firstHitZ = fillMeasurements(hitsOnTrack, 0); if (debug) System.out.printf("firstHitZ %f \n", firstHitZ); - return new SeedTrack(trackHitsKalman, firstHitZ, 0.); + return new SeedTrack(trackHitsKalman, firstHitZ, 0., kPar); } - // Method to refit an existing track's hits, using the Kalman seed-track to initialize the Kalman Filter. + /** + * Method to refit an existing track's hits, using the Kalman seed-track to initialize the Kalman Filter. + * + * @param evtNumb Event number + * @param seed Kalman track seed + * @param track GBL track + * @param hitToStrips Relation table for GBL hits to strips + * @param hitToRotated Relation table for GBL hits to rotated hits + * @param nIt Number of iterations + * @return Kalman track fit object + */ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated, int nIt) { double firstHitZ = 10000.; @@ -1155,7 +1386,6 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track t if (SeedTrackLayers.contains((SiM.Layer + 1) / 2) && (i > startIndex)) { startIndex = i; } if (debug) { SiM.print(String.format("SiMoccupied%d", i)); } } - // startIndex++; if (debug) { System.out.printf("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); } @@ -1165,7 +1395,19 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track t return new KalmanTrackFit2(evtNumb, SiMoccupied, null, startIndex, nIt, new Vec(0., seed.yOrigin, 0.), seed.helixParams(), cov, kPar, fM); } - // Method to refit an existing track, using the track's helix parameters and covariance to initialize the Kalman Filter. + /** + * Method to refit an existing GBL track, using the track's helix parameters and covariance to initialize the Kalman Filter. + * + * @param evtNumb Event number + * @param helixParams 5-vector of starting-guess Kalman helix parameters + * @param pivot Pivot point for the helix parameters + * @param cov Starting-guess Helix parameters covariance + * @param track GBL track + * @param hitToStrips Relation table for GBL hits to strips + * @param hitToRotated Relation table for GBL hits to rotated hits + * @param nIt Number of Kalman fit iterations + * @return Kalman track-fit object + */ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pivot, DMatrixRMaj cov, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated, int nIt) { List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); @@ -1190,10 +1432,10 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pi return new KalmanTrackFit2(evtNumb, SiMoccupied, null, startIndex, nIt, pivot, helixParams, cov, kPar, fM); } - // public KalTrack createKalmanTrack(KalmanTrackFit2 ktf, int trackID) { - // return new KalTrack(trackID, ktf.sites.size(), ktf.sites, ktf.chi2s); - // } - + /** + * Sort the Kalman-filter geometry objects according to tracker layer + * + */ class SortByLayer implements Comparator { @Override public int compare(SiModule o1, SiModule o2) { @@ -1201,7 +1443,13 @@ public int compare(SiModule o1, SiModule o2) { } } - // Method to drive the Kalman-Filter based pattern recognition + /** + * Method to drive the Kalman-Filter based pattern recognition + * + * @param event Event header + * @param decoder Decoder for hit information + * @return Two lists of Kalman KalTrack objects, for top and bottom detectors + */ public ArrayList[] KalmanPatRec(EventHeader event, IDDecoder decoder) { if (debug) System.out.format("KalmanInterface: entering KalmanPatRec for event %d\n", event.getEventNumber()); ArrayList[] outList = new ArrayList[2]; @@ -1236,11 +1484,19 @@ public ArrayList[] KalmanPatRec(EventHeader event, IDDecoder decoder) System.out.format("KalmanInterface.KalmanPatRec event %d: calling KalmanPatRecHPS for topBottom=%d\n", event.getEventNumber(), topBottom); } outList[topBottom] = kPat.kalmanPatRec(event, hitMap, SiMoccupied, topBottom); + + //for (KalTrack tkr : outList[topBottom]) tkr.printLong("Kalman Track"); } return outList; } - // The following method is a debugging aid for comparing SeedTracker/GBL tracks to the Kalman counterparts. + /** + * The following method is a debugging aid for comparing SeedTracker/GBL tracks to the Kalman counterparts. + * + * @param trackCollectionName Tracks to be compared to Kalman + * @param event Event header + * @param kPatList Lists of all Kalman tracks top and bottom + */ public void compareAllTracks(String trackCollectionName, EventHeader event, ArrayList[] kPatList) { if (!event.hasCollection(Track.class, trackCollectionName)) { System.out.format("\nKalmanInterface.compareAllTracks: the track collection %s is missing. Abort.\n",trackCollectionName); @@ -1427,6 +1683,12 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra } } + /** + * Plots GBL tracks by creating an ASCII file to be displayed by GNUplot + * + * @param path Path to the folder where the output file should be written + * @param event Event header + */ public void plotGBLtracks(String path, EventHeader event) { String trackCollectionName = "GBLTracks"; @@ -1523,7 +1785,13 @@ public void plotGBLtracks(String path, EventHeader event) { printWriter3.format("\n"); printWriter3.close(); } - // This method makes a Gnuplot file to display the Kalman tracks and hits in 3D. + /** + * This method makes a Gnuplot file to display the Kalman tracks and hits in 3D. + * + * @param path Path to the folder where the output text file will be writtng + * @param event Event header + * @param patRecList Array of dimension 2 (up vs down) of lists of tracks to be plotted + */ public void plotKalmanEvent(String path, EventHeader event, ArrayList[] patRecList) { boolean debug = false; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFit.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFit.java index e5c3aea834..0621db8abe 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFit.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFit.java @@ -17,9 +17,10 @@ import java.util.Set; import org.lcsim.event.GenericObject; - -// Break a Kalman track into two halves and fit them separately. -// Find the kink angle between the two halves +/** + * Break a Kalman track into two halves and fit them separately. + * Find the kink angle between the two halves + */ public class KalmanKinkFit { private KalmanTrackFit2 innerTrack, outerTrack; private Vec innerP, outerP; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java index 877d82aedb..64ec754976 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java @@ -19,6 +19,10 @@ import hep.aida.IHistogram1D; +/** + * Driver to study kinks in Kalman tracks, measured by breaking the track into two halves + * + */ public class KalmanKinkFitDriver extends Driver { private ArrayList detPlanes; @@ -65,7 +69,7 @@ public void detectorChanged(Detector det) { KalmanParams kPar = new KalmanParams(); kPar.print(); - KI = new KalmanInterface(false, kPar, fm); + KI = new KalmanInterface(kPar, fm); KI.createSiModules(detPlanes); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index eb0bb209c1..8ee2cde58a 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -38,16 +38,22 @@ public class KalmanParams { double [] minSeedE; double edgeTolerance; static final int numLayers = 14; + boolean uniformB; private int[] Swap = {1,0, 3,2, 5,4, 7,6, 9,8, 11,10, 13,12}; private String [] tb; private Logger logger; int maxListIter1; + double SVTcenter = 505.57; // Location to evaluate the field in case it is assumed uniform + /** + * Print all the Kalman Tracking parameters values (good idea to call it at the beginning of the run) + */ public void print() { System.out.format("\nKalmanParams: dump of the Kalman pattern recognition cuts and parameters\n"); System.out.println(" (In the case of two values, they refer to the two iterations.)"); System.out.format(" There are %d layers in the tracker.\n", numLayers); + if (uniformB) System.out.format(" The magnetic field is assumed to be uniform!\n"); System.out.format(" Cluster energy cuts for seeds, by layer: "); for (int lyr=0; lyr 1.) { logger.warning(String.format("low pulse-height threshold %10.4f is not valid and is ignored.", cut)); @@ -456,7 +471,12 @@ private boolean addStrategy(String strategy) { return addStrategy(strategy,"top") && addStrategy(strategy,"bottom"); } - // Add a seed search strategy for the bottom or top tracker + /** + * Add a seed search strategy for the bottom or top tracker + * @param strategy string specifying the strategy + * @param topBottom string specifying "top" or "bottom" + * @return true if successful + */ public boolean addStrategy(String strategy, String topBottom) { if (!(topBottom == "top" || topBottom == "bottom")) { logger.config("The argument topBottom must be 'top' or 'bottom'. This seed strategy is ignored."); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java index f19518872c..d197795868 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java @@ -44,7 +44,8 @@ public class KalmanPatRecDriver extends Driver { private org.lcsim.geometry.FieldMap fm; private KalmanInterface KI; private boolean verbose = false; - private boolean uniformB = false; + private boolean uniformB = false; // If true, fit tracks assuming a uniform B field + private boolean addResiduals = false; // If true add the hit-on-track residuals to the LCIO event private String outputFullTrackCollectionName = "KalmanFullTracks"; public AIDA aida; private int nTracks; @@ -95,42 +96,75 @@ public class KalmanPatRecDriver extends Driver { private boolean useBeamPositionConditions; // True to use beam position from database private boolean useFixedVertexZPosition; // True to override the database just for the z beam position private Level logLevel = Level.WARNING; // Set log level from steering - private boolean addResiduals; // If true add the hit-on-track residuals to the LCIO event private List sensors = null; // List of tracker sensors - - + public String getOutputFullTrackCollectionName() { return outputFullTrackCollectionName; } + /** + * Used to change the track collection name from the default "KalmanFullTracks" + * + * @param input Desired new name for the track collection + */ public void setOutputFullTrackCollectionName(String input) { outputFullTrackCollectionName = input; } - public void setVerbose(boolean input) { - verbose = input; + /** + * Optional call to force a change to the amount of debug printing + * + * @param input true to turn on lots of debug printing + */ + public void setVerbose(boolean verbose) { + this.verbose = verbose; + if (verbose) System.out.println("KalmanPatRecDriver: verbose print output is turned on!"); } - public void setUniformB(boolean input) { - uniformB = input; - logger.config("KalmanPatRecDriver: the B field will be assumed uniform.\n"); + /** + * Option to treat the B field as uniform. This normally is used only for development and debugging work. + * + * @param input Set true to assume a uniform B field. + */ + public void setUniformB(boolean uniformB) { + this.uniformB = uniformB; + System.out.format("KalmanPatRecDriver: a uniform field will be used in track fitting: %b\n", uniformB); } public void setMaterialManager(MaterialSupervisor mm) { _materialManager = mm; } + /** + * Determine whether residuals will be added to the event output + * + * @param input set true to force residuals to be output with the event + */ + public void setAddResiduals(boolean addResiduals) { + this.addResiduals = addResiduals; + System.out.format("KalmanPatRecDriver: residuals will be added to the event output: %b\n", addResiduals); + } + + /** + * Do-nothing method. See instead setOutputFullTrackCollectionName. + * + * @param input ignored---the call will do nothing. + */ public void setTrackCollectionName(String input) { } + /** + * Set the maximum number of SiClusters in one event allowed for KF pattern recognition (protection against monster events) + * + * @param input Maximum number of hits to use + */ public void setSiHitsLimit(int input) { siHitsLimit = input; } - public void setAddResiduals(boolean input) { - addResiduals = input; - } - + /** + * Set up the geometry and parameters for the Kalman-filter track finding and fitting. + */ @Override public void detectorChanged(Detector det) { logger = Logger.getLogger(KalmanPatRecDriver.class.getName()); @@ -149,31 +183,30 @@ public void detectorChanged(Detector det) { _materialManager.buildModel(det); fm = det.getFieldMap(); - /* - System.out.format("B field map vs y:\n"); + + System.out.format("B field map vs y in Kalman coordinates:\n"); double eCalLoc = 1394.; for (double y=0.; y<1500.; y+=5.) { double z1=-50.; double z2= 50.; - Vec B1 = new Vec(3, KalmanInterface.getFielD(new Vec(0., y, z1), fm)); - Vec B2 = new Vec(3, KalmanInterface.getFielD(new Vec(0., y, 0.), fm)); - Vec B3 = new Vec(3, KalmanInterface.getFielD(new Vec(0., y, z2), fm)); + Vec B1 = KalmanInterface.getField(new Vec(0., y, z1), fm); + Vec B2 = KalmanInterface.getField(new Vec(0., y, 0.), fm); + Vec B3 = KalmanInterface.getField(new Vec(0., y, z2), fm); System.out.format("y=%6.1f z=%6.1f: %s z=0: %s z=%6.1f: %s\n", y, z1, B1.toString(), B2.toString(), z2, B3.toString()); } - System.out.format("B field map vs z at ECAL:\n"); + System.out.format("B field map vs z at ECAL, in Kalman coordinates:\n"); for (double z=-200.; z<200.; z+=5.) { double y=eCalLoc; - Vec B = new Vec(3, KalmanInterface.getFielD(new Vec(0., y, z), fm)); + Vec B = KalmanInterface.getField(new Vec(0., y, z), fm); System.out.format("x=0 y=%6.1f z=%6.1f: %s\n", y, z, B.toString()); } - System.out.format("B field map vs x at ECAL:\n"); + System.out.format("B field map vs x at ECAL, in Kalman coordinates:\n"); for (double x=-200.; x<200.; x+=5.) { double y=eCalLoc; double z=20.; - Vec B = new Vec(3, KalmanInterface.getFielD(new Vec(x, y, z), fm)); + Vec B = KalmanInterface.getField(new Vec(x, y, z), fm); System.out.format("x=%6.1f y=%6.1f z=%6.1f: %s\n", x, y, z, B.toString()); - } - */ + } detPlanes = new ArrayList(); List materialVols = ((MaterialSupervisor) (_materialManager)).getMaterialVolumes(); @@ -214,6 +247,7 @@ public void detectorChanged(Detector det) { if (beamSigmaY != 0.0) kPar.setBeamSizeZ(beamSigmaY); if (mxChi2Vtx != 0.0) kPar.setMaxChi2Vtx(mxChi2Vtx); if (minSeedEnergy >= 0.) kPar.setMinSeedEnergy(minSeedEnergy); + kPar.setUniformB(uniformB); // Here we set the seed strategies for the pattern recognition if (strategies != null || (strategiesTop != null && strategiesBot != null)) { @@ -267,7 +301,7 @@ public void detectorChanged(Detector det) { logger.config("KalmanPatRecDriver: done with configuration changes."); kPar.print(); - KI = new KalmanInterface(uniformB, kPar, fm); + KI = new KalmanInterface(kPar, fm); KI.setSiHitsLimit(siHitsLimit); KI.createSiModules(detPlanes); decoder = det.getSubdetector("Tracker").getIDDecoder(); @@ -276,6 +310,11 @@ public void detectorChanged(Detector det) { } } + /** + * Top level method called for each event, to do the Kalman-filter tracking finding and fitting + * + * @param event input the header for this event + */ @Override public void process(EventHeader event) { @@ -336,6 +375,20 @@ public int compare(TrackerHit o1, TrackerHit o2) { } } + /** + * Execute the Kalman pattern recognition. All but the first argument are outputs, but the calling routine has to + * supply the empty data structures. + * + * @param event input the header for this event + * @param outputFullTracks output the list of HPS tracks + * @param trackDataCollection output list of data that go with the tracks + * @param trackDataRelations output the relations between tracks and the data + * @param allClstrs output all the clusters needed for refitting the Kalman tracks by GBL + * @param gblStripClusterDataRelations output relations for the clusters + * @param trackResiduals output the residuals for hits on Kalman tracks + * @param trackResidualsRelations output relations for the residuals + * @return Two lists of native Kalman tracks, one for each of top and bottom detectors + */ private ArrayList[] prepareTrackCollections(EventHeader event, List outputFullTracks, List trackDataCollection, List trackDataRelations, List allClstrs, List gblStripClusterDataRelations, List trackResiduals, List trackResidualsRelations) { int evtNumb = event.getEventNumber(); @@ -395,6 +448,8 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List clstrs = KI.createGBLStripClusterData(kTk); if (verbose) { for (GBLStripClusterData clstr : clstrs) { @@ -402,10 +457,6 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List[] prepareTrackCollections(EventHeader event, List layers = new ArrayList(); List residuals = new ArrayList(); List sigmas = new ArrayList(); @@ -447,7 +497,7 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List[] prepareTrackCollections(EventHeader event, List candidateList; + /** + * Constructor to call each event for each detector (top/bottom). + * @param kPar Object containing all the cuts and parameters + */ KalmanPatRecHPS(KalmanParams kPar) { startTime = (long)0.; this.kPar = kPar; @@ -92,14 +96,25 @@ class KalmanPatRecHPS { eventNumber = 0; } - // Override the default vertex location and size + /** + * Override the default vertex location and size + * @param vtx 3-vector vertex position + * @param vtxCov 3 by 3 covariance matrix for the vertex position + */ void patRecSetVtx(double [] vtx, double [][] vtxCov) { this.vtx = vtx; this.vtxCov = vtxCov; } + /** + * Execute the Kalman-Filter pattern recognition for a given detector in a given event. + * @param event Event header + * @param hitMapHPS Map between tracker hits and measurement objects + * @param data Array of SiModule objects containing all the hit data + * @param topBottom 0 for the bottom tracker (z>0), 1 for the top tracker (z<0) + * @return + */ ArrayList kalmanPatRec(EventHeader event, Map hitMapHPS, ArrayList data, int topBottom) { - // topBottom = 0 for the bottom tracker (z>0); 1 for the top tracker (z<0) if (event != null) eventNumber = event.getEventNumber(); else eventNumber++; @@ -174,6 +189,14 @@ ArrayList kalmanPatRec(EventHeader event, Map } System.out.format("\n"); } + + Vec tB = null; // Magnetic field direction + double Bmag = 0.; // Magnetic field magnitude + if (kPar.uniformB) { + Vec Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), data.get(0).Bfield); + tB = new Vec(0., 0., 1.); + Bmag = Bfield.v[2]; + } // Loop over seed strategies, each with 2 non-stereo layers and 3 stereo layers // For each strategy generate a seed track for every hit combination @@ -310,7 +333,7 @@ ArrayList kalmanPatRec(EventHeader event, Map if (redundantSeed) continue; // Fit the seed to extract helix parameters - SeedTrack seed = new SeedTrack(hitList, yOrigin, kPar.beamSpot[1]); + SeedTrack seed = new SeedTrack(hitList, yOrigin, kPar.beamSpot[1], kPar); if (!seed.success) { if (debug) { System.out.format("Seed %d %d %d %d %d failed fit\n",idx[0], idx[1], idx[2], idx[3], idx[4]); @@ -378,9 +401,11 @@ ArrayList kalmanPatRec(EventHeader event, Map } // Kalman filter the sorted seeds - Vec Bfield = KalmanInterface.getField(pivot, m0.Bfield); - double Bmag = Bfield.mag(); - Vec tB = Bfield.unitVec(Bmag); + if (!kPar.uniformB) { + Vec Bfield = KalmanInterface.getField(pivot, m0.Bfield); + Bmag = Bfield.mag(); + tB = Bfield.unitVec(Bmag); + } seedLoop: for (SeedTrack seed : seedList) { if (debug) { System.out.format("\n\nStart the filter step for seed"); @@ -419,7 +444,7 @@ ArrayList kalmanPatRec(EventHeader event, Map setInitCov(CovGuess, seed.helixParams(), true); // Create an state vector from the input seed to initialize the Kalman filter - StateVector sI = new StateVector(-1, seed.helixParams(), CovGuess, new Vec(0., 0., 0.), Bmag, tB, pivot); + StateVector sI = new StateVector(-1, seed.helixParams(), CovGuess, new Vec(0., 0., 0.), Bmag, tB, pivot, kPar.uniformB); TrackCandidate candidateTrack = new TrackCandidate(candID, seed.hits, kPar, hitMap, eventNumber); candID++; filterTrack(candidateTrack, list[0], KalmanParams.numLayers - 1, sI, trial, true, true); @@ -1361,6 +1386,9 @@ ArrayList kalmanPatRec(EventHeader event, Map return TkrList; } + /** + * Print a debug summary of all the track candidates + */ private void printCandidateList() { System.out.format("KalmanPatRecHPS: list of track candidates in event %d\n", eventNumber); for (TrackCandidate tkr : candidateList) { @@ -1374,7 +1402,12 @@ private void printCandidateList() { } } - // Remove the worst hit from lousy track candidates + /** + * Remove the worst hit from lousy track candidates + * @param tkr The track candidate + * @param trial Trial (0 or 1, to select what cuts are used in kPar) + * @return true for success + */ private boolean removeBadHits(TrackCandidate tkr, int trial) { if (tkr.chi2s/(double) tkr.hits.size() < kPar.chi2mx1[trial]) return false; @@ -1411,7 +1444,10 @@ private boolean removeBadHits(TrackCandidate tkr, int trial) { return false; } - // Method to smooth an already filtered track candidate + /** + * Method to smooth an already filtered track candidate + * @param filteredTkr Track candidate to be smoothed + */ private void smoothTrack(TrackCandidate filteredTkr) { MeasurementSite nextSite = null; boolean badCov = false; @@ -1436,11 +1472,20 @@ private void smoothTrack(TrackCandidate filteredTkr) { filteredTkr.smoothed = true; } - // Execute the Kalman prediction and filter steps over a range of SVT layers + /** + * Execute the Kalman prediction and filter steps over a range of SVT layers + * @param tkrCandidate Track candidate to be worked on + * @param lyrBegin Beginning layer + * @param lyrEnd Ending layer + * @param sI State vector to use for initialization + * @param trial Trial level (0 or 1) to select what cuts and parameters to use + * @param startNew Start the fit over from the beginning, if true + * @param pickUp true to allow picking up new hits + */ private void filterTrack(TrackCandidate tkrCandidate, int lyrBegin, // layer on which to start the filtering - int lyrEnd, // layer on which to end the filtering - StateVector sI, // initialization state vector - int trial, // trial level, for selecting cuts + int lyrEnd, // layer on which to end the filtering + StateVector sI, // initialization state vector + int trial, // trial level, for selecting cuts boolean startNew, // Start the fit over boolean pickUp // true to allow picking up new hits ) { @@ -1636,6 +1681,12 @@ private void filterTrack(TrackCandidate tkrCandidate, int lyrBegin, // layer on return; } + /** + * Store a track candidate as a KalTrack object + * @param tkID Integer ID for the track + * @param tkrCand The candidate + * @return + */ boolean storeTrack(int tkID, TrackCandidate tkrCand) { if (debug) System.out.format("entering storeTrack for track %d, debug=%b\n", tkID, debug); @@ -1704,6 +1755,11 @@ public int compare(Pair p1, Pair p2) { } }; + /** + * Check for negative covariance in a simple way, by looking at just the diagonal elements + * @param C Covariance matrix to check + * @return true if it is not positive definite + */ static boolean negativeCov(DMatrixRMaj C) { boolean badCov = false; for (int i=0; i<5; ++i) { @@ -1715,6 +1771,12 @@ static boolean negativeCov(DMatrixRMaj C) { return badCov; } + /** + * Make an initial-guess helix covariance matrix + * @param C The covariance + * @param helix The helix parameters + * @param newM true if it is a new matrix (i.e. all elements are zero) + */ static void setInitCov(DMatrixRMaj C, Vec helix, boolean newM) { C.unsafe_set(0, 0, 0.5); C.unsafe_set(1, 1, .00005); @@ -1730,6 +1792,11 @@ static void setInitCov(DMatrixRMaj C, Vec helix, boolean newM) { } } + /** + * Try to fix the helix covariance matrix + * @param C The bad covariance matrix + * @param helix The helix parameters + */ static void fixCov(DMatrixRMaj C, Vec helix) { for (int i=0; i<5; ++i) { if (C.unsafe_get(i, i) < 0.) { @@ -1761,7 +1828,12 @@ static void fixCov(DMatrixRMaj C, Vec helix) { } } - // Quick check on where the seed track is heading, using only the two axial layers in the seed + /** + * Quick check on where the seed track is heading, using only the two axial layers in the seed + * @param j The axial layer where we are interested + * @param iter Iteration (0,1) for choosing the kPar cuts and parameters + * @return true if the direction is no good for a track seed + */ private boolean seedNoGood(int j, int iter) { // j must point to an axial layer in the seed. // Find the previous axial layer, if there is one. . . diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java index a8446d7740..08a8d8aafb 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java @@ -102,7 +102,7 @@ class KalmanPatRecPlots { aida.histogram1D("GBL >=12-hit track chi^2", 100, 0., 200.); aida.histogram1D("Kalman Track Number Hits", 20, 0., 20.); aida.histogram1D("GBL number tracks", 10, 0., 10.); - aida.histogram1D("Kalman missed hit residual", 100, -1.0, 1.0); + aida.histogram1D("Kalman missed hit residual", 100, -2.0, 2.0); aida.histogram1D("Kalman track hit residual, sigmas", 100, -5., 5.); aida.histogram1D("Kalman track hit residual >= 10 hits, sigmas", 100, -5., 5.); aida.histogram1D("Kalman track hit residual", 100, -0.1, 0.1); @@ -157,7 +157,7 @@ class KalmanPatRecPlots { aida.histogram1D("dz error estimate",50,0.,0.2); aida.histogram1D("tanl error estimate",50,0.,.005); for (int lyr=0; lyr<14; ++lyr) { - aida.histogram1D(String.format("Layers/Kalman missed hit residual in layer %d",lyr), 100, -1.0, 1.0); + aida.histogram1D(String.format("Layers/Kalman missed hit residual in layer %d",lyr), 100, -2.0, 2.0); aida.histogram1D(String.format("Layers/Kalman track hit residual in layer %d",lyr), 100, -0.1, 0.1); aida.histogram1D(String.format("Layers/Kalman track hit residual in layer %d, sigmas",lyr), 100, -5., 5.); aida.histogram1D(String.format("Layers/Kalman track unbiased hit residual in layer %d",lyr), 100, -0.1, 0.1); @@ -193,6 +193,16 @@ class KalmanPatRecPlots { aida.histogram1D("seed slope",100,0.,0.3); aida.histogram1D("seed z intercept",100,0.,10.); aida.histogram1D("seed y intercept",100,-100.,100.); + for (int topBottom=0; topBottom<2; ++topBottom) { + aida.histogram1D(String.format("Missed hit residual/ det=%d, stereo=%b",topBottom,true), 200, -10.0, 10.0); + aida.histogram1D(String.format("Missed hit residual/ det=%d, stereo=%b",topBottom,false), 200, -10.0, 10.0); + for (int lyr=8; lyr<14; ++lyr) { + boolean isStereo; + if (topBottom == 0) isStereo = lyr%2 == 0; + else isStereo = lyr%2 != 0; + aida.histogram1D(String.format("Missed hit residual/ det=%d, lyr=%d, stereo=%b",topBottom,lyr,isStereo), 100, -10.0, 10.0); + } + } } void process(EventHeader event, List sensors, ArrayList[] kPatList, @@ -488,6 +498,41 @@ void process(EventHeader event, List sensors, ArrayList[] aida.histogram1D("tanl error estimate").fill(Math.sqrt(thisCov.unsafe_get(4, 4))); } + // Analyze high momentum tracks for missing hits in the outer layers + if (pMag > 2.0) { + //System.out.format("pMag=%12.6f\n", pMag); + int firstLayer = kTk.SiteList.get(0).m.Layer; + if (firstLayer < 2) { + int lastSite = 999; + for (int idx = kTk.SiteList.size()-1; idx >= 0; --idx) { + lastSite = idx; + if (kTk.SiteList.get(idx).hitID >= 0) break; + } + int lastLayer = kTk.SiteList.get(lastSite).m.Layer; + //System.out.format("firstLayer=%d lastLayer=%d\n", firstLayer, lastLayer); + if (lastLayer >= 7 && lastLayer <= 9 && kTk.nHits >= 8) { + for (MeasurementSite site: kTk.SiteList) { + if (site.hitID >= 0) continue; + SiModule mod = site.m; + if (mod == null) continue; + if (mod.Layer < 8) continue; + //System.out.format(" Layer %d\n", mod.Layer); + double [] rGbl = null; + double hitV = kTk.moduleIntercept(mod, rGbl)[1]; + double minResid = 9.9e9; + for (Measurement m : mod.hits) { + double resid = m.v - hitV; + if (Math.abs(resid) < Math.abs(minResid)) minResid = resid; + } + if (Math.abs(minResid) < 10.) { + aida.histogram1D(String.format("Missed hit residual/ det=%d, stereo=%b",topBottom,mod.isStereo)).fill(minResid); + aida.histogram1D(String.format("Missed hit residual/ det=%d, lyr=%d, stereo=%b",topBottom,mod.Layer,mod.isStereo)).fill(minResid); + } + } + } + } + } + // Histogram residuals of hits in layers with no hits on the track and with hits ArrayList mcParts = new ArrayList(); ArrayList mcCnt= new ArrayList(); @@ -503,9 +548,9 @@ void process(EventHeader event, List sensors, ArrayList[] double minResid = 9.9e9; for (Measurement m : mod.hits) { double resid = m.v - hitV; - if (resid < minResid) minResid = resid; + if (Math.abs(resid) < Math.abs(minResid)) minResid = resid; } - if (kTk.nHits >= 10 && Math.abs(minResid) < 1.0) { + if (kTk.nHits >= 10 && Math.abs(minResid) < 2.0) { aida.histogram1D("Kalman missed hit residual").fill(minResid); aida.histogram1D(String.format("Layers/Kalman missed hit residual in layer %d",mod.Layer)).fill(minResid); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanTrackFit2.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanTrackFit2.java index 756033bbb2..ed39882644 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanTrackFit2.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanTrackFit2.java @@ -47,10 +47,15 @@ class KalmanTrackFit2 { tkr = null; // Create an state vector from the input seed to initialize the Kalman filter - Vec Bfield = KalmanInterface.getField(pivot, fM); + Vec Bfield = null; + if (kPar.uniformB) { + Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), fM); + } else { + Bfield = KalmanInterface.getField(pivot, fM); + } double B = Bfield.mag(); Vec t = Bfield.unitVec(B); - StateVector sI = new StateVector(-1, helixParams, C, new Vec(0., 0., 0.), B, t, pivot); + StateVector sI = new StateVector(-1, helixParams, C, new Vec(0., 0., 0.), B, t, pivot, kPar.uniformB); if (verbose) { System.out.format("KalmanTrackFit2: begin Kalman fit, start=%d, number iterations=%d\n", start, nIterations); @@ -58,8 +63,6 @@ class KalmanTrackFit2 { sI.print("initial state for KalmanTrackFit"); } - double mxResid = 9999.; - sites = new ArrayList(); initialSite = 0; finalSite = 0; @@ -135,7 +138,6 @@ class KalmanTrackFit2 { startSite = newSite; } - int nHits = 0; // Restart the fit at the first layer and iterate the fit if requested for (int iteration = 0; iteration < nIterations; iteration++) { StateVector sH = null; @@ -246,7 +248,6 @@ class KalmanTrackFit2 { } if (currentSite.hitID >= 0) { chi2s += currentSite.chi2inc; - if (iteration == nIterations - 1) nHits++; } if (verbose) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java index be87e0bd3f..4dd869c19a 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java @@ -16,6 +16,9 @@ class Measurement { // ArrayList tracks; // Tracks that this hit lies on ArrayList tksMC; // MC tracks that contributed to this hit + /** + * Constructor with no MC truth info stored + */ Measurement(double value, double xStrip, double resolution, double t, double E) { v = value; x = xStrip; @@ -28,6 +31,16 @@ class Measurement { // tksMC = null; } + /** + * Full constructor, including MC truth + * @param value value of the measured coordinate on the detector coordinate system + * @param xStrip x value of the center of the strip in the detector coordinate system + * @param resolution uncertainty in v + * @param t measured time + * @param E measured energy deposit + * @param rGlobal global MC truth hit position + * @param vTrue MC truth measurement value + */ Measurement(double value, double xStrip, double resolution, double t, double E, Vec rGlobal, double vTrue) { v = value; x = xStrip; @@ -40,10 +53,18 @@ class Measurement { // tksMC = new ArrayList(); } + /** + * Add a MC truth track to the list + * @param idx index of the MC truth track + */ void addMC(int idx) { tksMC.add(idx); } + /** + * Debug printout of the measurement instance + * @param s Arbitrary string for the user's reference + */ void print(String s) { System.out.format("Measurement %s: Measurement value=%10.5f+-%8.6f; xStrip=%7.2f, MC truth=%10.5f; t=%8.3f; E=%8.3f", s, v, sigma, x, vTrue, time, energy); if (tracks.size() == 0) { @@ -56,6 +77,10 @@ void print(String s) { if (rGlobal != null) rGlobal.print("global location from MC truth"); } + /** + * Debug printout to a string of the measurement instance + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str = String.format("Measurement %s: Measurement value=%10.5f+-%8.6f; xStrip=%7.2f, MC truth=%10.5f; t=%8.3f; E=%8.3f", s, v, sigma, x, vTrue, time, energy); if (tracks.size() == 0) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java index 895def7ee9..85e49346ee 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java @@ -34,14 +34,19 @@ class MeasurementSite { private static Logger logger; private static DMatrixRMaj tempV; private static boolean initialized; - - // Note: I can remove the concept of a dummy layer and make all layers equivalent, except that the non-physical ones - // will never have a hit and thus will be handled the same as physical layers that lack hits + /** + * Debug printout of a MeasurementSite instance + * @param s Arbitrary string for the user's reference + */ void print(String s) { System.out.format("%s", this.toString(s)); } + /** + * Debug printout to a string of a MeasurementSite instance + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str; if (m.Layer < 0) { @@ -58,8 +63,14 @@ String toString(String s) { } str=str+String.format(" Hit ID=%d, maximum allowed residual=%10.5f\n", hitID, kPar.mxResid[1]); str = str + m.toString("for this site"); - double B = KalmanInterface.getField(m.p.X(), m.Bfield).mag(); - Vec tB = KalmanInterface.getField(m.p.X(), m.Bfield).unitVec(); + Vec Bfield = null; + if (kPar.uniformB) { + Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), m.Bfield); + } else { + Bfield = KalmanInterface.getField(m.p.X(), m.Bfield); + } + double B = Bfield.mag(); + Vec tB = Bfield.unitVec(); str=str+String.format(" Magnetic field strength=%10.6f; alpha=%10.6f\n", B, alpha); str = str + tB.toString("magnetic field direction") + "\n"; str=str+String.format(" chi^2 increment=%12.4e\n", chi2inc); @@ -77,6 +88,12 @@ String toString(String s) { return str; } + /** + * Constructor + * @param thisSite integer index + * @param data SiModule holding the data for this site + * @param kPar instance of Kalman run parameters + */ MeasurementSite(int thisSite, SiModule data, KalmanParams kPar) { this.thisSite = thisSite; this.kPar = kPar; @@ -84,7 +101,12 @@ String toString(String s) { hitID = -1; double c = 2.99793e8; // Speed of light in m/s conFac = 1.0e12 / c; - Vec Bfield = KalmanInterface.getField(m.p.X(), m.Bfield); + Vec Bfield = null; + if (kPar.uniformB) { + Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), m.Bfield); + } else { + Bfield = KalmanInterface.getField(m.p.X(), m.Bfield); + } B = Bfield.mag(); alpha = conFac / B; // Convert from 1/pt in 1/GeV to curvature in mm predicted = false; @@ -103,7 +125,11 @@ String toString(String s) { } } - double scatX() { // scattering angle in the x,y plane for the filtered state vector + /** + * Scattering angle in the x,y plane for the filtered state vector + * @return the scattering angle in radians + */ + double scatX() { if (aP == null || aF == null) return -999.; Vec p1 = aP.helix.getMom(0.); double t1 = FastMath.atan2(p1.v[0], p1.v[1]); @@ -112,7 +138,11 @@ String toString(String s) { return t1 - t2; } - double scatZ() { // scattering angle in the z,y plane for the filtered state vector + /** + * Scattering angle in the z,y plane for the filtered state vector + * @return the scattering angle in radians + */ + double scatZ() { if (aP == null || aF == null) return -999.; Vec p1 = aP.helix.getMom(0.); double t1 = FastMath.atan2(p1.v[2], p1.v[1]); @@ -121,31 +151,56 @@ String toString(String s) { return t1 - t2; } + /** + * Create predicted state vector by propagating from previous site to the new site + */ int makePrediction(StateVector pS, int hitNumber, boolean sharingOK, boolean pickup) { SiModule mPs = null; return makePrediction(pS, mPs, hitNumber, sharingOK, pickup, false); } + /** + * Create predicted state vector by propagating from previous site to the new site + */ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingOK, boolean pickup) { return makePrediction(pS, mPs, hitNumber, sharingOK, false, false); } + /** + * Create predicted state vector by propagating from previous site to the new site + */ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingOK, boolean pickup, boolean checkBounds) { double [] dT = {-1000., 1000.}; return makePrediction(pS, mPs, hitNumber, sharingOK, pickup, checkBounds, dT, 0); } - + + /** + * Create predicted state vector by propagating from previous site to the new site + */ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingOK, boolean pickup, boolean checkBounds, double [] tRange, int trial) { return makePrediction(pS, mPs, hitNumber, sharingOK, pickup, checkBounds, tRange, trial, false); } - int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingOK, boolean pickup, boolean checkBounds, double [] tRange, int trial, boolean verbose2) { // Create predicted state vector by propagating from previous site - // pS = state vector that we are predicting from - // mPS = Si module that we are predicting from, if any - // tRange = allowed time range [tmin,tmax] for picking up a hit + /** + * Create predicted state vector by propagating from previous site to the new site + * @param pS Previous site state vector that we are predicting from + * @param mPs Si module that we are predicting from, if any + * @param hitNumber hit number to assume if we are not doing pattern recognition + * @param sharingOK true to allow sharing of a hit between multiple tracks + * @param pickup true if we are doing pattern recognition here and need to pick up hits to add to the track + * @param checkBounds true to check that the track is within bounds of the silicon, if doing pattern recognition + * @param tRange Allowed time range [tmin,tmax] for picking up a hit (from KalParams) + * @param trial 0 or 1 for which trial is being executed, to index into KalParams + * @param verbose2 True to get lots of extra debug printout spew + * @return Flag: -2 for extrapolation not within detector, -1 for error, 1 for a hit was used, 0 no hit used + */ + int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingOK, boolean pickup, boolean checkBounds, double [] tRange, int trial, boolean verbose2) { + // pS = + // mPS = + // tRange = // minE = minimum hit energy for it to be picked up, unless no other hit exists - // sharingOK = whether to allow sharing of a hit between multiple tracks - // pickup = whether we are doing pattern recognition here and need to pick up hits to add to the track + // sharingOK = + // pickup = int returnFlag = 0; double phi = pS.helix.planeIntersect(m.p); if (debug) verbose2 = true; @@ -172,7 +227,12 @@ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingO double deltaE = 0.; // dEdx*thickness/ct; Vec origin = m.p.X(); - Vec Bfield = KalmanInterface.getField(pS.helix.toGlobal(X0), m.Bfield); + Vec Bfield = null; + if (kPar.uniformB) { + Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), m.Bfield); + } else { + Bfield = KalmanInterface.getField(pS.helix.toGlobal(X0), m.Bfield); + } double B = Bfield.mag(); Vec tB = Bfield.unitVec(B); if (debug) { @@ -416,7 +476,11 @@ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingO return returnFlag; // -2 for extrapolation not within detector, -1 for error, 1 for a hit was used, 0 no hit used } - boolean filter() { // Produce the filtered state vector for this site + /** + * Produce the filtered state vector for this site + * @return true for success, false for bad news + */ + boolean filter() { if (!predicted) { System.out.format("******MeasurementSite.filter: Warning, this site is not in the correct state!\n"); } @@ -484,7 +548,11 @@ boolean filter() { // Produce the filtered state vector for this site return true; } - // Inverse Kalman filter: remove this site from the smoothed track fit + /** + * Inverse Kalman filter: remove this site from the smoothed track fit + * This didn't really work, so it is stripped down to simply deleting the hit. + * @return true for success, false if there was no hit to remove + */ boolean removeHit() { if (hitID < 0) return false; hitID = -1; @@ -494,6 +562,14 @@ boolean removeHit() { return true; } + /** + * Try to add a hit to a given track + * @param tkr KalTrack instance to which we want to add a hit + * @param cut Maximum chi^2 increment allowed for the new hit + * @param mxTdif Maximum different in time for the track when the new hit is included + * @param oldID ID of a hit that was just removed + * @return The measurement instance added to the track, or null if none + */ Measurement addHit(KalTrack tkr, double cut, double mxTdif, int oldID) { if (aP == null) { logger.log(Level.WARNING, "******MeasurementSite.addHit: Warning, this site is not in the correct state!"); @@ -557,7 +633,12 @@ Measurement addHit(KalTrack tkr, double cut, double mxTdif, int oldID) { return null; } - // Produce the smoothed state vector for this site + + /** + * Produce the smoothed state vector for this site + * @param nS the previous site that was smoothed + * @return true for success + */ boolean smooth(MeasurementSite nS) { // nS is the next site in the filtering chain (i.e. the previous site that was smoothed) if (!filtered) { @@ -603,7 +684,13 @@ boolean smooth(MeasurementSite nS) { return true; } - double h(StateVector pS, SiModule siM) {// Predict the measurement for a helix passing through this plane + /** + * Predict the measurement for a helix passing through this plane + * @param pS The StateVector + * @param siM The silicon module + * @return The predicted measurement value + */ + double h(StateVector pS, SiModule siM) { double phi = pS.helix.planeIntersect(siM.p); if (Double.isNaN(phi)) { logger.log(Level.FINE, "MeasurementSite.h: warning, no intersection of helix with the plane exists."); @@ -612,7 +699,15 @@ boolean smooth(MeasurementSite nS) { return h(pS, siM, phi); } - double h(StateVector pS, SiModule siM, double phi) { // Shortcut call in case phi is already known + /** + * Predict the measurement for a helix passing through this plane. + * Shortcut call in case phi is already known + * @param pS The StateVector + * @param siM The silicon module + * @param phi Turning angle to the intersection with the plane, in radians + * @return The predicted measurement value + */ + double h(StateVector pS, SiModule siM, double phi) { Vec rGlobal = pS.helix.toGlobal(pS.helix.atPhi(phi)); Vec rLocal = siM.toLocal(rGlobal); // Rotate into the detector coordinate system if (debug) { @@ -624,7 +719,11 @@ boolean smooth(MeasurementSite nS) { return rLocal.v[1]; } - // Create the derivative matrix for prediction of the measurement from the helix + /** + * Create the derivative matrix for prediction of the measurement from the helix + * @param S The StateVector + * @param H The derivative matrix that is filled in by this method + */ private void buildH(StateVector S, DMatrixRMaj H) { double phi = S.helix.planeIntersect(m.p); @@ -634,9 +733,7 @@ private void buildH(StateVector S, DMatrixRMaj H) { } if (debug) { System.out.format("MeasurementSite.buildH: phi=%10.7f\n", phi); - // S.print("given to buildH"); - // R.print("in buildH"); - // p.print("in buildH"); + S.print("given to buildH"); } Vec dxdphi = new Vec((alpha / S.helix.a.v[2]) * FastMath.sin(S.helix.a.v[1] + phi), -(alpha / S.helix.a.v[2]) * FastMath.cos(S.helix.a.v[1] + phi), -(alpha / S.helix.a.v[2]) * S.helix.a.v[4]); @@ -717,7 +814,9 @@ private void buildH(StateVector S, DMatrixRMaj H) { } } - // Comparator functions for sorting measurement sites by layer number + /** + * Comparator function for sorting measurement sites by increasing layer number + */ static Comparator SiteComparatorUp = new Comparator() { public int compare(MeasurementSite s1, MeasurementSite s2) { int lyr1 = s1.m.Layer; @@ -725,6 +824,10 @@ public int compare(MeasurementSite s1, MeasurementSite s2) { return lyr1 - lyr2; } }; + + /** + * Comparator function for sorting measurement sites by decreasing layer number + */ static Comparator SiteComparatorDn = new Comparator() { public int compare(MeasurementSite s1, MeasurementSite s2) { int lyr1 = s1.m.Layer; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java index c93bd26c57..f5d5ba1633 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java @@ -26,7 +26,7 @@ class PatRecTest { PatRecTest(String path) { // Units are Tesla, GeV, mm - int nTrials = 1000; // The number of test eventNumbers to generate for pattern recognition and fitting + int nTrials = 3; // The number of test eventNumbers to generate for pattern recognition and fitting int mxPlot = 10; // Maximum number of single event plots int [] eventToPrint = {1,2,3,4,5,6,7,8,9,10}; boolean perfect = false; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/Plane.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/Plane.java index 6c4bce4c2b..2669a79758 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/Plane.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/Plane.java @@ -10,6 +10,11 @@ class Plane { private Vec u; // Local axis always lies in the world system x,y plane private Vec v; // Other local axis, t,u,v forms a RH system + /** + * Constructor + * @param point 3-vector point on the plane + * @param direction 3-vector direction cosines of the plane + */ Plane(Vec point, Vec direction) { x = point.copy(); t = direction.copy(); @@ -18,10 +23,21 @@ class Plane { v = t.cross(u); } + /** + * Copy method + * @return A new deep copy of the plane + */ Plane copy() { return new Plane(x, t); } + /** + * Alternative constructor + * @param newX 3-vector point on the plane + * @param newT t unit vector perpendicular to the plane + * @param newU u unit vector in the plane and in the x,y plane + * @param newV v unit vector perpendicular to t and u (t,u,v is a RH system) + */ Plane(Vec newX, Vec newT, Vec newU, Vec newV) { x = newX; t = newT; @@ -29,6 +45,12 @@ Plane copy() { v = newV; } + /** + * Alternative constuctor + * @param X 3-vector point on the plane + * @param T t unit vector perpendicular to the plane + * @param U u unit vector in the plane and in the x,y plane + */ Plane(Vec X, Vec T, Vec U) { x = X; t = T; @@ -36,7 +58,12 @@ Plane copy() { v = t.cross(u); } - // strange constructor for backward compatibility of SiModule.java + /** + * strange constructor for backward compatibility of SiModule.java + * @param X 3-vector point on the plane + * @param T t unit vector perpendicular to the plane + * @param theta angle used to rotate into the t,u,v system + */ Plane(Vec X, Vec T, double theta) { x = X; Vec zhat = new Vec(0., 0., 1.); @@ -53,10 +80,18 @@ Plane copy() { } } + /** + * Debug printout of a plane instance + * @param s Arbitrary string for the user's reference + */ void print(String s) { System.out.format("%s",this.toString(s)); } + /** + * Debug printout to a string of a plane instance + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str = String.format("Printout of plane %s\n", s); str=str+" point="+x.toString(); @@ -66,28 +101,51 @@ String toString(String s) { return str; } + /** + * Short string describing the plane + */ public String toString() { return String.format("pnt %s dir %s", x.toString(), t.toString()); } - - Vec U() { // unit vector in the plane perpendicular to t and the world z axis + /** + * Get u + * @return The unit vector in the plane perpendicular to t and the world z axis + */ + Vec U() { return u; } - Vec V() { // another orthogonal unit vector in the plane perp to t and u + /** + * Get v + * @return Another orthogonal unit vector in the plane perp to t and u + */ + Vec V() { return v; } + /** + * Get t + * @return The unit vector perpendicular to the plane + */ Vec T() { return t; } + /** + * Get x + * @return The point in the plane + */ Vec X() { return x; } - // Convert the plane to a local coordinate system + /** + * Convert the plane to a local coordinate system + * @param R Rotation matrix from global to local + * @param origin Origin of the local system + * @return The plane in the local system + */ Plane toLocal(RotMatrix R, Vec origin) { return new Plane(R.rotate(x.dif(origin)), R.rotate(t), R.rotate(u), R.rotate(v)); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java index 52d100fd18..abf0efd51d 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java @@ -15,7 +15,6 @@ */ public class PropagatedTrackState { private static final double c = 2.99793e8; - private static final double SVTcenter = 505.57; private static final boolean debug = false; private double conFac; private List detPlanes; @@ -33,7 +32,7 @@ public class PropagatedTrackState { private static DMatrixRMaj Ft; private static boolean initialized; - PropagatedTrackState (TrackState stateHPS, double [] location, double [] direction, List detPlanes, org.lcsim.geometry.FieldMap fM) { + PropagatedTrackState (TrackState stateHPS, double [] location, double [] direction, List detPlanes, double SVTcenter, org.lcsim.geometry.FieldMap fM) { logger = Logger.getLogger(PropagatedTrackState.class.getName()); // stateHPS = HPS track state to be propagated // location = 3-D point on the plane to which the track is to be propagated @@ -213,10 +212,18 @@ public class PropagatedTrackState { if (debug) printTrackState(trackState,"final"); } + /** + * Get the track state + * @return TrackState corresponding to the propagated helix + */ public TrackState getTrackState() { return trackState; } + /** + * Get the predicted intersection with the destination plane, in global coordinates + * @return 3-vector array of doubles + */ public double [] getIntersection() { double phi = newHelixState.planeIntersect(destinationPlane); if (Double.isNaN(phi)) { @@ -228,6 +235,10 @@ public TrackState getTrackState() { return KalmanInterface.vectorKalmanToGlb(xPlaneGlb); } + /** + * Get the covariance matrix of the predicted intersection point + * @return 3 by 3 array of doubles + */ public double [][] getIntersectionCov() { Vec helixAtInt = getIntersectionHelix(); DMatrixRMaj F = new DMatrixRMaj(5,5); @@ -253,6 +264,10 @@ public TrackState getTrackState() { return Chps.M; } + /** + * Debug printout + * @param s Arbitrary string for the user's reference + */ public void print(String s) { System.out.format("Dump of PropagatedTrackState %s at plane %s\n", s, destinationPlane.toString()); printTrackState(trackState,"internal"); @@ -264,6 +279,11 @@ public void print(String s) { System.out.format("End of PropagatedTrackState dump\n"); } + /** + * Debug printout of a track state + * @param trackState The instance to print + * @param s Arbitrary string for the user's reference + */ public static void printTrackState(TrackState trackState, String s) { double [] r = trackState.getReferencePoint(); System.out.format("Dump of TrackState %s at location %d ref. pnt %10.6f %10.6f %10.6f\n", @@ -279,18 +299,34 @@ public static void printTrackState(TrackState trackState, String s) { System.out.format(" %12.4e %12.4e %12.4e %12.4e %12.4e\n", C[10], C[11], C[12], C[13], C[14]); } + /** + * Get the 3-D point on the plane to which the track is to be propagated + * @return 3-vector of doubles + */ public double [] getLocation() { return location; } + /** + * Get the direction cosines of the plane to which the track is to be propagated + * @return 3-vector of doubles + */ public double [] getDirection() { return direction; } + /** + * Get the list of scattering planes through which the track is propagated + * @return the list of silicon-strip plane objects + */ public List getDetPlanes() { return detPlanes; } + /** + * Get the helix parameters at the intersection point (in the local B-field coordinate system, of course) + * @return 5-vector of helix parameters + */ private Vec getIntersectionHelix() { if (xPlane == null) getIntersection(); Vec helixAtInt = newHelixState.pivotTransform(xPlane); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/RotMatrix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/RotMatrix.java index 4d9f2142cf..a61f9a3fe5 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/RotMatrix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/RotMatrix.java @@ -6,16 +6,30 @@ class RotMatrix { double[][] M = null; - RotMatrix() { // Create a blank matrix + /** + * Constructor of a blank matrix + */ + RotMatrix() { M = new double[3][3]; } - // Careful here: a new array is not made + /** + * Constructor from a provided array. + * Careful here: a new array is not made. + * @param Min 3 by 3 array of doubles (should be orthogonal, but not checked!) + */ RotMatrix(double[][] Min) { M = Min; } - RotMatrix(Vec u, Vec v, Vec t) { // Transforms from global coordinates to frame with 3-D unit vectors u, v, t + /** + * Constructor from a RH u,v,t coordinate system + * Transforms from global coordinates to frame with 3-D unit vectors u, v, t + * @param u 3-vector direction cosines of u + * @param v 3-vector direction cosines of v + * @param t 3-vector direction cosines of t + */ + RotMatrix(Vec u, Vec v, Vec t) { M = new double[3][3]; for (int i = 0; i < 3; i++) { // Simply place the vectors u, v, and t along the successive rows of the matrix M[0][i] = u.v[i]; @@ -24,7 +38,16 @@ class RotMatrix { } } - RotMatrix(Vec u1, Vec v1, Vec t1, Vec u2, Vec v2, Vec t2) { // Rotation from frame 1 to frame 2 + /** + * Construct the matrix for a rotation from frame 1 to frame 2 + * @param u1 u axis in frame 1 + * @param v1 v axis in frame 1 + * @param t1 t axis in frame 1 + * @param u2 u axis in frame 2 + * @param v2 v axis in frame 2 + * @param t2 t axis in frame 2 + */ + RotMatrix(Vec u1, Vec v1, Vec t1, Vec u2, Vec v2, Vec t2) { M = new double[3][3]; M[0][0] = u2.dot(u1); M[0][1] = u2.dot(v1); @@ -37,7 +60,13 @@ class RotMatrix { M[2][2] = t2.dot(t1); } - RotMatrix(double phi, double theta, double psi) {// Create the rotation matrix from Euler angles + /** + * Construct the rotation matrix from Euler angles + * @param phi First Euler angle (rotation angle about z axis) + * @param theta Second Euler angle (rotation angle about x' axis) + * @param psi Third Euler angle (rotation angle about z'' axis) + */ + RotMatrix(double phi, double theta, double psi) {// double c1 = Math.cos(phi); double c2 = Math.cos(theta); double c3 = Math.cos(psi); @@ -56,7 +85,11 @@ class RotMatrix { M[2][2] = c2; } - RotMatrix(double phi) { // Simple rotation only about z + /** + * Constructor for a simple rotation only about z + * @param phi angle of rotation about z, in radians + */ + RotMatrix(double phi) { double c1 = Math.cos(phi); double s1 = Math.sin(phi); M = new double[3][3]; @@ -67,9 +100,12 @@ class RotMatrix { M[2][2] = 1.0; } - RotMatrix(Vec k, double theta) { // Rodrigues' rotation formula - // k is a unit vector defining the axis of rotation - // theta is the angle of rotation counterclockwise about that axis + /** + * Constructor using Rodrigues' rotation formula + * @param k unit vector defining the axis of rotation + * @param theta angle of rotation counterclockwise about the axis + */ + RotMatrix(Vec k, double theta) { // double[][] K = new double[3][3]; K[0][0] = 0.; K[0][1] = -k.v[2]; @@ -94,6 +130,10 @@ class RotMatrix { } } + /** + * Deep copy + * @return the copy + */ RotMatrix copy() { RotMatrix Rnew = new RotMatrix(); for (int i = 0; i < 3; i++) { @@ -104,6 +144,10 @@ RotMatrix copy() { return Rnew; } + /** + * Invert the rotation matrix (i.e. the transpose) + * @return the inverted matrix + */ RotMatrix invert() { RotMatrix mInv = new RotMatrix(); for (int i = 0; i < 3; i++) { @@ -114,6 +158,11 @@ RotMatrix invert() { return mInv; } + /** + * Matrix multiplication + * @param R2 Second matrix in the product + * @return The product matrix + */ RotMatrix multiply(RotMatrix R2) { // Multiply one rotation matrix by another RotMatrix R3 = new RotMatrix(); for (int i = 0; i < 3; i++) { @@ -126,10 +175,18 @@ RotMatrix multiply(RotMatrix R2) { // Multiply one rotation matrix by another return R3; } + /** + * Debug printout + * @param s Arbitrary string for the user's reference + */ void print(String s) { System.out.format("%s",this.toString(s)); } + /** + * Debug printout to a string + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str = String.format("The 3 by 3 rotation matrix %s:\n", s); for (int i = 0; i < 3; i++) { @@ -141,6 +198,11 @@ String toString(String s) { return str; } + /** + * Rotate a vector + * @param V The vector to rotate + * @return The rotated vector + */ Vec rotate(Vec V) { // Use the matrix to rotate a 3-D vector Vec Vp = new Vec(0., 0., 0.); for (int i = 0; i < 3; i++) { @@ -151,6 +213,11 @@ Vec rotate(Vec V) { // Use the matrix to rotate a 3-D vector return Vp; } + /** + * Rotate a 3 by 3 matrix (similarity transformation) + * @param S The square matrix to rotate + * @return Rotated square matrix + */ SquareMatrix rotate(SquareMatrix S) { // Use the matrix to rotate a 3 by 3 matrix SquareMatrix Q = new SquareMatrix(3); if (S.N != 3) System.out.format("RotMatrix rotate: the input matrix must be 3 by 3.\n"); @@ -166,6 +233,11 @@ SquareMatrix rotate(SquareMatrix S) { // Use the matrix to rotate a 3 by 3 matri return Q; } + /** + * Inverse rotation of a vector + * @param V Vector to rotate + * @return Rotated vector + */ Vec inverseRotate(Vec V) { // Use the matrix to rotate a 3-D vector in the opposite sense, using the // inverse (i.e. transpose) of the rotation matrix Vec Vp = new Vec(0., 0., 0.); @@ -177,6 +249,11 @@ Vec inverseRotate(Vec V) { // Use the matrix to rotate a 3-D vector in the oppos return Vp; } + /** + * Inverse rotate a 3 by 3 matrix (similarity transformation) + * @param S The square matrix to rotate + * @return Rotated square matrix + */ SquareMatrix inverseRotate(SquareMatrix S) { // Use the matrix to rotate a 3 by 3 matrix in the opposite sense SquareMatrix Q = new SquareMatrix(3); if (S.N != 3) System.out.format("RotMatrix rotate: the input matrix must be 3 by 3.\n"); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/RungeKutta4.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/RungeKutta4.java index 9218dab067..6a29893ca1 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/RungeKutta4.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/RungeKutta4.java @@ -14,6 +14,12 @@ public class RungeKutta4 { private double alpha; private org.lcsim.geometry.FieldMap fM; + /** + * Constructor + * @param Q Particle charge (+ or - one) + * @param dx Step size in mm + * @param fM Field map + */ public RungeKutta4(double Q, double dx, org.lcsim.geometry.FieldMap fM) { alpha = Q * 2.99792458e-4; // Q is the charge in units of the proton charge h = dx; // Step size in mm @@ -21,10 +27,14 @@ public RungeKutta4(double Q, double dx, org.lcsim.geometry.FieldMap fM) { this.fM = fM; // Magnetic field map } + /** + * Execute the integration + * @param r0 The initial point 3-vector in mm + * @param p0 The initial 3-vector momentum in GeV/c + * @param s Distance to propagate, along the trajectory + * @return Final position 3-vector of doubles + */ double[] integrate(Vec r0, Vec p0, double s) { - // r0 is the initial point in mm - // p0 is the initial momentum in GeV/c - // s is the distance to propagate (approximate to distance dx) double[] r = { r0.v[0], r0.v[1], r0.v[2], p0.v[0], p0.v[1], p0.v[2] }; double[] k1 = new double[6]; double[] k2 = new double[6]; @@ -49,7 +59,13 @@ public RungeKutta4(double Q, double dx, org.lcsim.geometry.FieldMap fM) { return r; } - private double[] f(Vec x, double[] p) { // Return all the derivatives + /** + * Return all the derivatives needed to make an integration step + * @param x 3-vector position in mm + * @param p 3-vector momentum in GeV/c + * @return 6 derivatives + */ + private double[] f(Vec x, double[] p) { double [] B = KalmanInterface.getFielD(x, fM); // This field routine assumes the Kalman-Filter coordinate system. double[] d = new double[6]; double pmag = FastMath.sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/SeedTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/SeedTrack.java index eab25e7c37..b14fff033f 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/SeedTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/SeedTrack.java @@ -51,14 +51,25 @@ class SeedTrack { private static final boolean debug = false; // Set true to generate lots of debug printout //private static int nCalls; + /** + * Get the field conversion factor + */ double getAlpha() { return alpha; } + /** + * Debug print of the seed + * @param s Arbitrary string for the user's reference + */ void print(String s) { System.out.format("%s", this.toString(s)); } + /** + * Debug print of the seed to a string + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str; if (success) { @@ -84,11 +95,19 @@ String toString(String s) { return str; } - // Older interface - SeedTrack(ArrayList data, // List of Si modules with data - double yOrigin, // New origin along beam to use for the fit - ArrayList hitList, // Element 0= index of Si module; Element 1= hit number - boolean verbose // Set true for lots of debug printout + /** + * Old constructor + * @param data List of Si modules with data + * @param yOrigin New origin along beam to use for the fit + * @param hitList List of hits to fit to, element 0= index of Si module; Element 1= hit number + * @param verbose No longer used + * @param kPar KalmanParams instance + */ + SeedTrack(ArrayList data, + double yOrigin, + ArrayList hitList, + boolean verbose, + KalmanParams kPar ) { ArrayList theHits = new ArrayList(hitList.size()); for (int i = 0; i < hitList.size(); i++) { @@ -97,16 +116,28 @@ String toString(String s) { theHits.add(tmpHit); } double yTarget = 0.; - SeedTracker(theHits, yOrigin, yTarget); + SeedTracker(theHits, yOrigin, yTarget, kPar); } - // Newer interface - SeedTrack(ArrayList hitList, double yOrigin, double yTarget) { - SeedTracker(hitList, yOrigin, yTarget); + /** + * New constructor + * @param hitList List of Si modules with data + * @param yOrigin New origin along beam to use for the fit + * @param yTarget Location along the beam of the target itself + * @param kPar KalmanParams instance + */ + SeedTrack(ArrayList hitList, double yOrigin, double yTarget, KalmanParams kPar) { + SeedTracker(hitList, yOrigin, yTarget, kPar); } - - private void SeedTracker(ArrayList hitList, double yOrigin, double yTarget) { + /** + * Actual code for the constructor + * @param hitList List of Si modules with data + * @param yOrigin New origin along beam to use for the fit + * @param yTarget Location along the beam of the target itself + * @param kPar KalmanParams instance + */ + private void SeedTracker(ArrayList hitList, double yOrigin, double yTarget, KalmanParams kPar) { // yOrigin is the location along the beam about which we fit the seed helix // yTarget is the location along the beam of the target itself //if (nCalls < 3) debug = true; @@ -153,14 +184,22 @@ private void SeedTracker(ArrayList hitList, double yOrigin, double yTarg Nnonbending = 0; // First find the average field - Vec Bvec = new Vec(0., 0., 0.); - for (KalHit pnt : hitList) { + + Vec Bvec = null; + if (kPar.uniformB) { + KalHit pnt = hitList.get(0); SiModule thisSi = pnt.module; - Vec thisB = KalmanInterface.getField(thisSi.toGlobal(new Vec(0., 0., 0.)), thisSi.Bfield); - Bvec = Bvec.sum(thisB); + Bvec = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), thisSi.Bfield); + } else { + Bvec = new Vec(0., 0., 0.); + for (KalHit pnt : hitList) { + SiModule thisSi = pnt.module; + Vec thisB = KalmanInterface.getField(thisSi.toGlobal(new Vec(0., 0., 0.)), thisSi.Bfield); + Bvec = Bvec.sum(thisB); + } + double sF = 1.0 / ((double) nHits); + Bvec = Bvec.scale(sF); } - double sF = 1.0 / ((double) nHits); - Bvec = Bvec.scale(sF); Bavg = Bvec.mag(); if (debug) { System.out.format("*** SeedTrack: nHits=%d, Bavg=%10.5e\n", nHits, Bavg); } double c = 2.99793e8; // Speed of light in m/s @@ -287,51 +326,80 @@ private void SeedTracker(ArrayList hitList, double yOrigin, double yTarg // will give trouble here! // Rotate the result into the frame of the B field at the specified origin - KalHit itr = hitList.get(0); - SiModule firstSi = itr.module; - Vec firstB = KalmanInterface.getField(new Vec(0., yOrigin, 0.), firstSi.Bfield); - if (Math.abs(firstB.v[1] / firstB.v[2]) > 0.002) { - Vec zhat = firstB.unitVec(); - Vec yhat = new Vec(0., 1., 0.); - Vec xhat = (yhat.cross(zhat)).unitVec(); - yhat = zhat.cross(xhat); - RotMatrix Rot = new RotMatrix(xhat, yhat, zhat); - - hParm = rotateHelix(new Vec(drho, phi0, K, dz, tanl), Rot); - if (debug) { - firstB.print("Seedtrack, B field"); - Rot.print("Seedtrack, rotation matrix"); - hParm.print("Seedtrack, rotated helix"); + if (!kPar.uniformB) { + KalHit itr = hitList.get(0); + SiModule firstSi = itr.module; + Vec firstB = KalmanInterface.getField(new Vec(0., yOrigin, 0.), firstSi.Bfield); + if (Math.abs(firstB.v[1] / firstB.v[2]) > 0.002) { + Vec zhat = firstB.unitVec(); + Vec yhat = new Vec(0., 1., 0.); + Vec xhat = (yhat.cross(zhat)).unitVec(); + yhat = zhat.cross(xhat); + RotMatrix Rot = new RotMatrix(xhat, yhat, zhat); + + hParm = rotateHelix(new Vec(drho, phi0, K, dz, tanl), Rot); + if (debug) { + firstB.print("Seedtrack, B field"); + Rot.print("Seedtrack, rotation matrix"); + hParm.print("Seedtrack, rotated helix"); + } + } else { + hParm = new Vec(drho, phi0, K, dz, tanl); + if (debug) { hParm.print("Seedtrack, rotated helix"); } } } else { hParm = new Vec(drho, phi0, K, dz, tanl); - if (debug) { hParm.print("Seedtrack, rotated helix"); } + if (debug) hParm.print("Seedtrack, rotated helix"); } - success = true; } + /** + * Square a double + * @param x number to square + * @return the square + */ private double square(double x) { return x * x; } - Vec helixParams() { // Return the fitted helix parameters + /** + * Return the fitted helix parameters + * @return 5-vector of helix parameters + */ + Vec helixParams() { // return hParm; } - DMatrixRMaj covariance() { // Return covariance matrix of the fitted helix parameters + /** + * Return covariance matrix of the fitted helix parameters + * @return Covariance matrix + */ + DMatrixRMaj covariance() { return C; } - Vec solution() { // Return the 5 polynomial coefficients + /** + * Return the solution to the linear fit + * @return 5-vector of polynomial coefficients + */ + Vec solution() { return new Vec(a.unsafe_get(0, 0),a.unsafe_get(1, 0),a.unsafe_get(2, 0),a.unsafe_get(3, 0),a.unsafe_get(4, 0)); } - double B() { // Return the average field + /** + * Return the average field + * @return B field + */ + double B() { return Bavg; } - SquareMatrix solutionCovariance() { // Return covariance of the polynomial coefficients + /** + * Return covariance of the polynomial coefficients + * @return 5 by 5 square matrix + */ + SquareMatrix solutionCovariance() { // SquareMatrix CM = new SquareMatrix(5); for (int i=0; i<5; ++i) { for (int j=0; j<5; ++j) { @@ -341,19 +409,31 @@ SquareMatrix solutionCovariance() { // Return covariance of the polynomial coeff return CM; } - Vec solutionErrors() { // Return errors on the polynomial coefficients (for testing) + /** + * Return errors on the polynomial coefficients (for testing) + * @return 5-vector of errors + */ + Vec solutionErrors() { return new Vec(FastMath.sqrt(CovA.unsafe_get(0,0)), FastMath.sqrt(CovA.unsafe_get(1,1)), FastMath.sqrt(CovA.unsafe_get(2,2)), FastMath.sqrt(CovA.unsafe_get(3,3)), FastMath.sqrt(CovA.unsafe_get(4,4))); } - Vec errors() { // Return errors on the helix parameters + /** + * Return errors on the helix parameters + * @return 5-vector of errors + */ + Vec errors() { return new Vec(FastMath.sqrt(C.unsafe_get(0,0)), FastMath.sqrt(C.unsafe_get(1,1)), FastMath.sqrt(C.unsafe_get(2,2)), FastMath.sqrt(C.unsafe_get(3,3)), FastMath.sqrt(C.unsafe_get(4,4))); } - private double[] parabolaToCircle(double sgn, Vec coef) { // Utility to convert from parabola coefficients to circle - // (i.e. helix) - // parameters drho, phi0, and K + /** + * Utility to convert from parabola coefficients to circle (i.e. helix) + * @param sgn charge sign + * @param coef parameters drho, phi0, and K + * @return + */ + private double[] parabolaToCircle(double sgn, Vec coef) { R = -sgn / (2.0 * coef.v[2]); yc = sgn * R * coef.v[1]; xc = coef.v[0] - sgn * R * (1.0 - 0.5 * coef.v[1] * coef.v[1]); @@ -373,7 +453,12 @@ private double[] parabolaToCircle(double sgn, Vec coef) { // Utility to convert return r; } - // Transformation of a helix from one B-field frame to another, by rotation R + /** + * Transformation of a helix from one B-field frame to another, by rotation R + * @param a Helix parameters + * @param R Rotation matrix + * @return New helix parameters + */ private Vec rotateHelix(Vec a, RotMatrix R) { // a = 5 helix parameters // R = 3 by 3 rotation matrix @@ -386,7 +471,12 @@ private Vec rotateHelix(Vec a, RotMatrix R) { return new Vec(a.v[0], a_prime.v[0], a_prime.v[1], a.v[3], a_prime.v[2]); } - // Transform from momentum at helix starting point back to the helix parameters + /** + * Transform from momentum at helix starting point back to the helix parameters + * @param p Momentum vector + * @param Q Charge + * @return Helix parameters + */ private static Vec pTOa(Vec p, Double Q) { double phi0 = FastMath.atan2(-p.v[0], p.v[1]); double K = Q / FastMath.sqrt(p.v[0] * p.v[0] + p.v[1] * p.v[1]); @@ -397,7 +487,9 @@ private static Vec pTOa(Vec p, Double Q) { return new Vec(phi0, K, tanl); } - // Comparator function for sorting seed tracks by curvature + /** + * Comparator function for sorting seed tracks by curvature + */ static Comparator curvatureComparator = new Comparator() { public int compare(SeedTrack t1, SeedTrack t2) { double K1 = Math.abs(t1.helixParams().v[2]); @@ -410,7 +502,9 @@ public int compare(SeedTrack t1, SeedTrack t2) { } }; - // Comparator function for sorting seeds by distance from origin in x,z plane at the target + /** + * Comparator function for sorting seeds by distance from origin in x,z plane at the target + */ static Comparator dRhoComparator = new Comparator() { public int compare(SeedTrack t1, SeedTrack t2) { Vec pInt1 = t1.planeIntersection(t1.p0); @@ -423,7 +517,12 @@ public int compare(SeedTrack t1, SeedTrack t2) { } }; - //Check if two seeds are compatible given a certain relative threshold + /** + * Check if two seeds are compatible given a certain relative threshold + * @param st Other seed + * @param rel_eps Tolerance + * @return True if they are the same + */ boolean isCompatibleTo(SeedTrack st, double rel_eps) { Vec st_hp = st.helixParams(); boolean compatible = true; @@ -436,19 +535,34 @@ boolean isCompatibleTo(SeedTrack st, double rel_eps) { return compatible; } + /** + * Intersection of the helix with a plane perpendicular to the y axis + * @param p instance of a plane + * @return 3-vector intersection point + */ Vec planeIntersection(Plane p) { double arg = (K / alpha) * ((drho + (alpha / K)) * FastMath.sin(phi0) - (p.X().v[1] - yOrigin)); double phiInt = -phi0 + FastMath.asin(arg); return atPhi(phiInt); } - private Vec atPhi(double phi) { // point on the helix at the angle phi + /** + * Point on the helix at the angle phi + * @param phi radians + * @return 3-vector + */ + private Vec atPhi(double phi) { double x = (drho + (alpha / K)) * FastMath.cos(phi0) - (alpha / K) * FastMath.cos(phi0 + phi); double y = yOrigin + (drho + (alpha / K)) * FastMath.sin(phi0) - (alpha / K) * FastMath.sin(phi0 + phi); double z = dz - (alpha / K) * phi * tanl; return new Vec(x, y, z); } + /** + * Helix pivot transform + * @param pivot New pivot point + * @return Transformed helix parameters + */ double[] pivotTransform(double[] pivot) { Vec X0 = new Vec(0., yOrigin, 0.); double xC = X0.v[0] + (drho + alpha / K) * FastMath.cos(phi0); // Center of the helix circle @@ -469,14 +583,16 @@ private Vec atPhi(double phi) { // point on the helix at the angle phi return aP; } + /** + * Linear fit of a set of points to an approximate helix (parabola plus line) + * @param N Number of measurement points (detector layers) to fit. This must be no less than 5, with at least 2 axial and 2 stereo + * @param y nominal location of each measurement plane along the beam axis + * @param v array of measurement values in the detector coordinate system, perpendicular to the strips + * @param s one sigma error estimate on each measurement + * @param delta offset of the detector coordinate system from the global system (minus the nominal y value along the beam axis) + * @param R2 2nd row of the general rotation from the global system to the local detector system + */ private void LinearHelixFit(int N, double[] y, double[] v, double[] s, double[][] delta, double[][] R2) { - // N = number of measurement points (detector layers) to fit. This must be no less than 5, with at least 2 axial and 2 stereo - // y = nominal location of each measurement plane along the beam axis - // v = measurement value in the detector coordinate system, perpendicular to the strips - // s = one sigma error estimate on each measurement - // delta = offset of the detector coordinate system from the global system (minus the nominal y value along the beam axis) - // R2 = 2nd row of the general rotation from the global system to the local detector system - A[0][0] = 0.; A[0][1] = 0.; A[0][2] = 0.; @@ -561,14 +677,27 @@ private void LinearHelixFit(int N, double[] y, double[] v, double[] s, double[][ } } + /** + * Find a point on the line given y + * @param y y value + * @return z value + */ private double evaluateLine(double y) { return a.unsafe_get(0,0) + a.unsafe_get(1,0) * y; } + /** + * Find a point on the parabola given y + * @param y y value (along beam) + * @return x value + */ private double evaluateParabola(double y) { return a.unsafe_get(2,0) + (a.unsafe_get(3,0) + a.unsafe_get(4,0) * y) * y; } + /** + * Debug print the fit results + */ private void printFit(int N, double[] x, double[] y, double[] z, double[] v, double[] s) { System.out.format("LinearHelixFit: parabola a=%10.7f b=%10.7f c=%10.7f\n", a.get(2,0), a.get(3,0), a.get(4,0)); System.out.format("LinearHelixFit: line a=%10.7f b=%10.7f\n", a.get(0,0), a.get(1,0)); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java index 2367a7aed9..972c7dd6fc 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java @@ -2,14 +2,12 @@ import java.util.ArrayList; import java.util.Iterator; -import java.util.logging.Level; -import java.util.logging.Logger; import org.apache.commons.math3.util.Pair; import org.hps.recon.tracking.TrackUtils; /** - * Description of a single silicon-strip module, and a container for its hits + * Description of a single silicon-strip module for the Kalman code, and a container for its hits. */ class SiModule { int Layer; // Tracker layer number, or a negative integer for a dummy layer added just for stepping in a @@ -25,33 +23,41 @@ class SiModule { double[] yExtent; // Plus and minus limits on the detector active area in the y direction // (perpendicular to the strips) boolean split; // True if the strips are split into two channels at the middle - RotMatrix R; // Rotation from the detector coordinates to global coordinates (not field coordinates) - RotMatrix Rinv; // Rotation from global (not field) coordinates to detector coordinates (transpose of R) - // The local coordinate system is u, v, t where t is more-or-less the beam direction (y-global) - // and v is the measurement direction. + RotMatrix R; // Rotation from the detector coordinates to global coordinates (not field coordinates) + RotMatrix Rinv; // Rotation from global (not field) coordinates to detector coordinates (transpose of R) + // The local coordinate system is u, v, t where t is more-or-less the beam direction (y-global) + // and v is the measurement direction. double thickness; // Silicon thickness in mm (should be 0 for a dummy layer!) int topBottom; // 0 for bottom tracker, 1 for top tracker org.lcsim.geometry.FieldMap Bfield; boolean isStereo; - private boolean verbose; - private Logger logger; + static final private boolean debug = false; + /** + * Constructor for backwards-compatibility with old stand-alone development code: assume axial layers have stereo angle=0 + */ SiModule(int Layer, Plane p, double stereo, double width, double height, boolean split, double thickness, org.lcsim.geometry.FieldMap Bfield) { - // for backwards-compatibility with old stand-alone development code: assume axial - // layers have stereo angle=0 + // this(Layer, p, stereo != 0.0, width, height, split, thickness, Bfield, 0, 0, 0); topBottom = 1; if (p.X().v[2]>0) topBottom = 0; } + /** + * Constructor for backward compatibility with old test code + */ SiModule(int Layer, Plane p, boolean isStereo, double width, double height, boolean split, double thickness, org.lcsim.geometry.FieldMap Bfield) { this(Layer, p, isStereo, width, height, split, thickness, Bfield, 0, 0, 0); topBottom = 1; if (p.X().v[2]>0) topBottom = 0; } - + + /** + * Another constructor for backward compatibility. + * No millipedeID and figures out top or bottom from the location coordinate. + */ SiModule(int Layer, Plane p, boolean isStereo, double width, double height, boolean split, double thickness, org.lcsim.geometry.FieldMap Bfield, int detector) { this(Layer, p, isStereo, width, height, split, thickness, Bfield, detector, 0, 0); @@ -59,19 +65,29 @@ class SiModule { if (p.X().v[2]>0) topBottom = 0; } + /** + * Full constructor + * @param Layer Layer number from 0 to 13 for 2019/2021 data + * @param p Plane describing the sensor location and orientation + * @param isStereo True for stereo layers, false for axial + * @param width Silicon wafer width + * @param height Silicon wafer height + * @param split True if the strips are split into two channels at the middle (thin sensors) + * @param thickness Silicon thickness + * @param Bfield Field map + * @param detector 0 or 1 (only zero for planes with a single sensor) + * @param millipedeID ID used for alignment studies + * @param topBottom Which detector, top (1) or bottom (0)? + */ SiModule(int Layer, Plane p, boolean isStereo, double width, double height, boolean split, double thickness, org.lcsim.geometry.FieldMap Bfield, int detector, int millipedeID, int topBottom) { this.topBottom = topBottom; - logger = Logger.getLogger(SiModule.class.getName()); - verbose = (logger.getLevel()==Level.FINE); this.millipedeID = millipedeID; - if (verbose) { + if (debug) { System.out.format("SiModule constructor called with layer = %d, detector module = %d, y=%8.2f\n", Layer, detector, p.X().v[1]); p.print("of SiModule"); - } - Vec BOnAxis = KalmanInterface.getField(new Vec(0.,p.X().v[1],0.), Bfield); - Vec BatCenter = KalmanInterface.getField(p.X(), Bfield); - if (verbose) { + Vec BOnAxis = KalmanInterface.getField(new Vec(0.,p.X().v[1],0.), Bfield); + Vec BatCenter = KalmanInterface.getField(p.X(), Bfield); BOnAxis.print("B field on axis"); BatCenter.print("B at detector center"); } @@ -94,10 +110,18 @@ class SiModule { hits = new ArrayList(); } + /** + * Debug printout of all details of an instance. + * @param s Arbitrary string for the user's reference. + */ void print(String s) { System.out.format("%s",this.toString(s)); } - + + /** + * Debug printout to a string of all details of an instance. + * @param s Arbitrary string for the user's reference. + */ String toString(String s) { boolean top = topBottom == 1; String str = String.format( @@ -125,6 +149,10 @@ String toString(String s) { } return str; } + + /** + * Abbreviated debug printout to a string. + */ public String toString() { boolean top = topBottom == 1; String str = String.format("Si Module: Lyr=%2d Det=%2d Top=%b Mpd=%d split=%b stereo=%b pnt=%8.3f %8.3f %8.3f t=%7.3f %7.3f %7.3f\n", @@ -134,11 +162,17 @@ public String toString() { return str; } - // Delete all the existing hits + /** + * Delete all the existing hits + */ void reset() { hits = new ArrayList(); } + /** + * Add a silicon-strip measurement to the list for this module + * @param m The measurement object + */ void addMeasurement(Measurement m) { if (hits.size() == 0) hits.add(m); else { @@ -154,11 +188,21 @@ void addMeasurement(Measurement m) { } } - Vec toGlobal(Vec vLocal) { // Convert a position vector from local detector coordinates to global + /** + * Convert a position vector from local detector coordinates to global + * @param vLocal 3-vector of the point in local coordinates + * @return 3-vector of the point in global coordinates + */ + Vec toGlobal(Vec vLocal) { return p.X().sum(R.rotate(vLocal)); } - Vec toLocal(Vec vGlobal) { // Convert a position vector from global coordinates to local detector + /** + * Convert a position vector from global coordinates to local detector + * @param vGlobal 3-vector of the point in global coordinates + * @return 3-vector of the point in local coordinates + */ + Vec toLocal(Vec vGlobal) { return Rinv.rotate(vGlobal.dif(p.X())); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java index 5ad7205c46..2464d4a9da 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java @@ -33,14 +33,26 @@ class StateVector { private static DMatrixRMaj U; // Unit matrix private static LinearSolverDense solver; private static boolean initialized; - - // Constructor for the initial state vector used to start the Kalman filter. - StateVector(int site, Vec helixParams, DMatrixRMaj Cov, Vec pivot, double B, Vec tB, Vec origin) { + private boolean uniformB; + + /** + * Constructor for the initial state vector used to start the Kalman filter. + * @param site integer index of the site + * @param helixParams 5-vector of helix parameters + * @param Cov covariance matrix of the helix parameters + * @param pivot 3-vector pivot point of the helix + * @param B magnetic field magnitude + * @param tB magnetic field direction cosines + * @param origin 3-vector origin of the coordinate system + * @param uniformB true to treat the magnetic field as uniform + */ + StateVector(int site, Vec helixParams, DMatrixRMaj Cov, Vec pivot, double B, Vec tB, Vec origin, boolean uniformB) { // Here tB is the B field direction, while B is the magnitude if (debug) System.out.format("StateVector: constructing an initial state vector\n"); helix = new HelixState(helixParams, pivot, origin, Cov, B, tB); kLow = site; kUp = kLow; + this.uniformB = uniformB; if (!initialized) { // Initialize the static working arrays on the first call logger = Logger.getLogger(StateVector.class.getName()); tempV = new DMatrixRMaj(5,1); @@ -55,10 +67,13 @@ class StateVector { } } - // Constructor for a new blank state vector with a new B field - StateVector(int site, double B, Vec tB, Vec origin) { + /** + * Constructor for a new blank state vector with a new B field + */ + StateVector(int site, double B, Vec tB, Vec origin, boolean uniformB) { helix = new HelixState(B, tB, origin); kLow = site; + this.uniformB = uniformB; if (!initialized) { // Initialize the static working arrays on the first call logger = Logger.getLogger(StateVector.class.getName()); tempV = new DMatrixRMaj(5,1); @@ -73,9 +88,13 @@ class StateVector { } } - // Constructor for a new completely blank state vector - StateVector(int site) { + /** + * Constructor for a new completely blank state vector + * @param site + */ + StateVector(int site, boolean uniformB) { kLow = site; + this.uniformB = uniformB; if (!initialized) { // Initialize the static working arrays on the first call logger = Logger.getLogger(StateVector.class.getName()); tempV = new DMatrixRMaj(5,1); @@ -90,8 +109,12 @@ class StateVector { } } + /** + * Deep copy of the state vector + * @return copy + */ StateVector copy() { - StateVector q = new StateVector(kLow); + StateVector q = new StateVector(kLow, uniformB); q.helix = (HelixState)helix.copy(); // Deep copy q.kUp = kUp; q.F = F; // Don't deep copy the F matrix @@ -102,11 +125,18 @@ StateVector copy() { return q; } - // Debug printout of the state vector + /** + * Debug printout of the state vector + * @param s Arbitrary string for the user's reference + */ void print(String s) { System.out.format("%s", this.toString(s)); } - + + /** + * Debug printout to a string of the state vector + * @param s Arbitrary string for the user's reference + */ String toString(String s) { String str = String.format(">>>Dump of state vector %s %d %d\n", s, kUp, kLow); str = str + helix.toString(" "); @@ -123,17 +153,21 @@ String toString(String s) { return str; } - // Create a predicted state vector by propagating a given helix to a measurement site + /** + * Create a predicted state vector by propagating a given helix to a measurement site + * @param newSite index of the new site + * @param pivot pivot point of the new site in the local coordinates of this state vector (i.e. coordinates of the old site) + * @param B magnitude of the magnetic field at the pivot point, in global coordinates + * @param t direction of the magnetic field at the pivot point, in global coordinates + * @param originPrime origin of the detector coordinates at the new site in global coordinates + * @param XL thickness of the scattering material + * @param deltaE energy loss in the scattering material + * @return predicted state vector + */ StateVector predict(int newSite, Vec pivot, double B, Vec t, Vec originPrime, double XL, double deltaE) { - // newSite = index of the new site - // pivot = pivot point of the new site in the local coordinates of this state vector (i.e. coordinates of the old site) - // B and t = magnitude and direction of the magnetic field at the pivot point, in global coordinates - // XL = thickness of the scattering material - // deltaE = energy loss in the scattering material - // originPrime = origin of the detector coordinates at the new site in global coordinates // This constructs a new blank state vector with pivot and helix parameters undefined as yet - StateVector aPrime = new StateVector(newSite, B, t, originPrime); + StateVector aPrime = new StateVector(newSite, B, t, originPrime, uniformB); aPrime.kUp = kUp; aPrime.helix.X0 = pivot; // pivot before helix rotation, in coordinate system of the previous site @@ -164,53 +198,55 @@ StateVector predict(int newSite, Vec pivot, double B, Vec t, Vec originPrime, do // Transform to the coordinate system of the field at the new site // First, transform the pivot point to the new system aPrime.helix.X0 = aPrime.helix.toLocal(this.helix.toGlobal(aPrime.helix.X0)); - - // Calculate the matrix for the net rotation from the old site coordinates to the new site coordinates - RotMatrix Rt = aPrime.helix.Rot.multiply(this.helix.Rot.invert()); - if (debug) { - aPrime.helix.Rot.print("aPrime rotation matrix"); - this.helix.Rot.print("this rotation matrix"); - Rt.print("rotation from old local frame to new local frame"); - aPrime.helix.a.print("StateVector:predict helix before rotation"); - } - - // Rotate the helix parameters here. - // dz and drho will remain unchanged at zero - // phi0 and tanl(lambda) change, as does kappa (1/pt). However, |p| should be unchanged by the rotation. - // This call to rotateHelix also calculates the derivative matrix fRot - aPrime.helix.a = HelixState.rotateHelix(aPrime.helix.a, Rt, tempM); - if (debug) { - aPrime.helix.a.print("StateVector:predict helix after rotation"); - System.out.println("fRot from StateVector:predict"); - tempM.print(); - } - CommonOps_DDRM.mult(tempM, F, tempA); - - // Test the derivatives - /* - if (debug) { - double daRel[] = { 0.01, 0.03, -0.02, 0.05, -0.01 }; - StateVector aPda = this.Copy(); - for (int i = 0; i < 5; i++) { - aPda.a.v[i] = a.v[i] * (1.0 + daRel[i]); + if (!uniformB) { + + // Calculate the matrix for the net rotation from the old site coordinates to the new site coordinates + RotMatrix Rt = aPrime.helix.Rot.multiply(this.helix.Rot.invert()); + if (debug) { + aPrime.helix.Rot.print("aPrime rotation matrix"); + this.helix.Rot.print("this rotation matrix"); + Rt.print("rotation from old local frame to new local frame"); + aPrime.helix.a.print("StateVector:predict helix before rotation"); } - Vec da = aPda.a.dif(a); - StateVector aPrimeNew = this.Copy(); - aPrimeNew.a = aPda.pivotTransform(pivot); - RotMatrix RtTmp = Rot.invert().multiply(aPrime.Rot); - SquareMatrix fRotTmp = new SquareMatrix(5); - aPrimeNew.a = rotateHelix(aPrimeNew.a, RtTmp, fRotTmp); - for (int i = 0; i < 5; i++) { - double deltaExact = aPrimeNew.a.v[i] - aPrime.a.v[i]; - double delta = 0.; - for (int j = 0; j < 5; j++) { - delta += F.M[i][j] * da.v[j]; + + // Rotate the helix parameters here. + // dz and drho will remain unchanged at zero + // phi0 and tanl(lambda) change, as does kappa (1/pt). However, |p| should be unchanged by the rotation. + // This call to rotateHelix also calculates the derivative matrix fRot = tempM + aPrime.helix.a = HelixState.rotateHelix(aPrime.helix.a, Rt, tempM); + if (debug) { + aPrime.helix.a.print("StateVector:predict helix after rotation"); + System.out.println("fRot from StateVector:predict"); + tempM.print(); + } + CommonOps_DDRM.mult(tempM, F, tempA); + + // Test the derivatives + /* + if (debug) { + double daRel[] = { 0.01, 0.03, -0.02, 0.05, -0.01 }; + StateVector aPda = this.Copy(); + for (int i = 0; i < 5; i++) { + aPda.a.v[i] = a.v[i] * (1.0 + daRel[i]); + } + Vec da = aPda.a.dif(a); + StateVector aPrimeNew = this.Copy(); + aPrimeNew.a = aPda.pivotTransform(pivot); + RotMatrix RtTmp = Rot.invert().multiply(aPrime.Rot); + SquareMatrix fRotTmp = new SquareMatrix(5); + aPrimeNew.a = rotateHelix(aPrimeNew.a, RtTmp, fRotTmp); + for (int i = 0; i < 5; i++) { + double deltaExact = aPrimeNew.a.v[i] - aPrime.a.v[i]; + double delta = 0.; + for (int j = 0; j < 5; j++) { + delta += F.M[i][j] * da.v[j]; + } + System.out.format("Test of F: Helix parameter %d, deltaExact=%10.8f, delta=%10.8f\n", i, deltaExact, + delta); } - System.out.format("Test of F: Helix parameter %d, deltaExact=%10.8f, delta=%10.8f\n", i, deltaExact, - delta); } + */ } - */ aPrime.kLow = newSite; aPrime.kUp = kUp; @@ -229,18 +265,25 @@ StateVector predict(int newSite, Vec pivot, double B, Vec t, Vec originPrime, do } // Now propagate the multiple scattering matrix and covariance matrix to the new site - CommonOps_DDRM.multTransB(Cinv, tempA, tempM); aPrime.helix.C = new DMatrixRMaj(5,5); - CommonOps_DDRM.mult(tempA, tempM, aPrime.helix.C); + if (uniformB) { + CommonOps_DDRM.multTransB(Cinv, F, tempM); + CommonOps_DDRM.mult(F, tempM, aPrime.helix.C); + } else { + CommonOps_DDRM.multTransB(Cinv, tempA, tempM); + CommonOps_DDRM.mult(tempA, tempM, aPrime.helix.C); + } return aPrime; } - // Create a filtered state vector from a predicted state vector + /** + * Create a filtered state vector from a predicted state vector + * @param H prediction matrix (5-vector) + * @param V hit variance (1/sigma^2) + * @return filtered state vector + */ StateVector filter(DMatrixRMaj H, double V) { - // H = prediction matrix (5-vector) - // V = hit variance (1/sigma^2) - StateVector aPrime = this.copy(); aPrime.kUp = kLow; @@ -315,7 +358,13 @@ StateVector filter(DMatrixRMaj H, double V) { return aPrime; } - // Modify the state vector by removing the hit information + /** + * Modify the state vector by removing the hit information + * @param H + * @param V + * @param Cnew + * @return + */ Vec inverseFilter(DMatrixRMaj H, double V, DMatrixRMaj Cnew) { CommonOps_DDRM.mult(helix.C, H, tempV); double denom = -V + CommonOps_DDRM.dot(H, tempV); @@ -337,7 +386,12 @@ Vec inverseFilter(DMatrixRMaj H, double V, DMatrixRMaj Cnew) { return aNew; } - // Create a smoothed state vector from the filtered state vector + /** + * Create a smoothed state vector from the filtered state vector + * @param snS + * @param snP + * @return Smoothed state vector + */ StateVector smooth(StateVector snS, StateVector snP) { if (debug) System.out.format("StateVector.smooth of filtered state %d %d, using smoothed state %d %d and predicted state %d %d\n", kLow, kUp, snS.kLow, snS.kUp, snP.kLow, snP.kUp); @@ -414,16 +468,23 @@ StateVector smooth(StateVector snS, StateVector snP) { return sS; } - // Return errors on the helix parameters at the global origin + /** + * Return errors on the helix parameters at the global origin + * @param aPrime helix parameters for a pivot at the global origin, assumed already to be calculated by pivotTransform() + * @return 5-vector of errors + */ Vec helixErrors(Vec aPrime) { - // aPrime are the helix parameters for a pivot at the global origin, assumed - // already to be calculated by pivotTransform() + DMatrixRMaj tC = covariancePivotTransform(aPrime); return new Vec(Math.sqrt(tC.unsafe_get(0,0)), Math.sqrt(tC.unsafe_get(1,1)), Math.sqrt(tC.unsafe_get(2,2)), Math.sqrt(tC.unsafe_get(3,3)), Math.sqrt(tC.unsafe_get(4,4))); } - // Transform the helix covariance to new pivot point (specified in local coordinates) + /** + * Transform the helix covariance to new pivot point (specified in local coordinates) + * @param aP 5-vector of helix parameters + * @return transformed helix parameters + */ DMatrixRMaj covariancePivotTransform(Vec aP) { // aP are the helix parameters for the new pivot point, assumed already to be // calculated by pivotTransform() @@ -434,16 +495,33 @@ DMatrixRMaj covariancePivotTransform(Vec aP) { CommonOps_DDRM.mult(mF, tempM, tempA); return tempA; } - // Go to and from 1D EJML matrix for a vector Vec + + /** + * Go to and from 1D EJML matrix for a vector Vec + * @param a vector + * @param m EJML matrix out + */ static void vecToM(Vec a, DMatrixRMaj m) { for (int i=0; i hitMap) { + if (event == null) return -1; String trackCollectionName = "GBLTracks"; if (!event.hasCollection(Track.class, trackCollectionName)) { System.out.format("\nKalmanInterface.compareAllTracks: the track collection %s is missing. Abort.\n",trackCollectionName); @@ -155,14 +164,27 @@ int compareGBL(EventHeader event, Map hitMap) { return -1; } + /** + * See if the track candidate contains all of the given list of hits + * @param hitList list of hits to check + * @return true for a match + */ boolean contains(ArrayList hitList) { return hits.containsAll(hitList); } + /** + * Return the number of hits on a track candidate + * @return number of hits + */ int numHits() { return hits.size(); } + /** + * Return the number of stereo hits on the track candidate + * @return number of stereo hits + */ int numStereo() { int nStereo = 0; for (KalHit ht : hits) { @@ -171,6 +193,10 @@ int numStereo() { return nStereo; } + /** + * Return the helix parameters at the origin + * @return 5-vector of helix parameters + */ Vec originHelix() { MeasurementSite site0 = null; for (MeasurementSite site: sites) { // sites are assumed to be sorted @@ -182,6 +208,11 @@ Vec originHelix() { return site0.aS.helix.pivotTransform(); } + /** + * Remove a hit from a track candidate + * @param hit the hit to remove from the fit + * @param deleteFromList true to delete it from the list of hits + */ void removeHit(KalHit hit, boolean deleteFromList) { MeasurementSite siteR = null; SiModule mod = hit.module; @@ -218,6 +249,10 @@ void removeHit(KalHit hit, boolean deleteFromList) { if (nstr < 3 || nax < 2) good = false; } + /** + * Refit a track candidate + * @return true for success + */ boolean reFit() { final boolean verbose = false; if (verbose) System.out.format("TrackCandidate.reFit: starting filtering for event %d.\n",eventNumber); @@ -364,10 +399,20 @@ boolean reFit() { return true; } + /** + * Debug printout of a track candidate + * @param s Arbitrary string for the user's reference + * @param shrt true to give an abreviated printout + */ void print(String s, boolean shrt) { System.out.format("%s", this.toString(s, shrt)); } - + + /** + * Debug printout to a string of a track candidate + * @param s Arbitrary string for the user's reference + * @param shrt true to give an abreviated printout + */ String toString(String s, boolean shrt) { String str; if (good) { @@ -454,7 +499,9 @@ String toString(String s, boolean shrt) { return str; } - // Comparator function for sorting track candidates by quality + /** + * Comparator function for sorting track candidates by quality + */ static Comparator CandidateComparator = new Comparator() { public int compare(TrackCandidate t1, TrackCandidate t2) { double p1 = 1.; @@ -469,6 +516,9 @@ public int compare(TrackCandidate t1, TrackCandidate t2) { } }; + /** + * Check whether two candidates are identical + */ @Override public boolean equals(Object other) { if (this == other) return true; From 51d59ea4623eeb3275a6b884448aeb1915d86d4f Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Wed, 23 Nov 2022 16:23:41 -0800 Subject: [PATCH 02/17] New code for ECAL constrained track --- .../hps/recon/tracking/kalman/HelixState.java | 105 ++++++++++++++++++ .../hps/recon/tracking/kalman/HelixTest3.java | 49 +++++++- .../hps/recon/tracking/kalman/KalTrack.java | 21 ++++ 3 files changed, 170 insertions(+), 5 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java index cf7e6a6c1f..ffe29c0a2a 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java @@ -11,7 +11,9 @@ import org.ejml.data.DMatrixRMaj; import org.ejml.dense.row.CommonOps_DDRM; import org.ejml.dense.row.MatrixFeatures_DDRM; +import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; import org.ejml.dense.fixed.CommonOps_DDF3; +import org.ejml.interfaces.linsol.LinearSolverDense; import org.hps.util.Pair; import org.lcsim.event.TrackState; @@ -31,6 +33,11 @@ class HelixState implements Cloneable { private Logger logger; private HelixPlaneIntersect hpi; private Vec xPlaneRK; + private static LinearSolverDense solver; + private static boolean initialized; + private static DMatrixRMaj Cinv; // Temporary working area + private static DMatrixRMaj X, V, H; + final static private boolean debug = false; public Object clone() { try { @@ -105,6 +112,104 @@ HelixState copy() { return new HelixState(a.copy(), X0.copy(), origin.copy(), C.copy(), B, tB.copy()); } + /** + * Invert the covariance matrix of this HelixState and store the result in a static array Cinv + */ + private void invertC() { + if (!HelixState.initialized) { + HelixState.Cinv = new DMatrixRMaj(5,5); + HelixState.X = new DMatrixRMaj(5,1); + HelixState.V = new DMatrixRMaj(5,1); + HelixState.H = new DMatrixRMaj(5,1); + solver = LinearSolverFactory_DDRM.symmPosDef(5); + HelixState.initialized = true; + } + if (!solver.setA(C.copy())) { + SquareMatrix invrs = KalTrack.mToS(C).fastInvert(); + if (invrs == null) { + logger.warning("StateVector:smooth, inversion of the covariance matrix failed"); + C.print(); + for (int i=0; i<5; ++i) { // Fill the inverse with something not too crazy and continue . . . + for (int j=0; j<5; ++j) { + if (i == j) { + HelixState.Cinv.unsafe_set(i,j,1.0/C.unsafe_get(i,j)); + } else { + HelixState.Cinv.unsafe_set(i, j, 0.); + C.unsafe_set(i, j, 0.); + } + } + } + } else { + for (int i=0; i<5; ++i) { + for (int j=0; j<5; ++j) { + HelixState.Cinv.unsafe_set(i, j, invrs.M[i][j]); + } + } + } + } else { + HelixState.solver.invert(HelixState.Cinv); + } + } + + /** + * Make a new HelixState with an energy constraint (from the ECAL). This uses the Kalman weighted-means formalism to "filter" + * the helix state vector. + * @param E The energy from the ECAL cluster + * @param sigmaE The energy uncertainty for the ECAL cluster + * @return A new HelixState with the energy constraint incorporated + */ + HelixState energyConstrained(double E, double sigmaE) { + double tanL = a.v[4]; + double K = a.v[2]; + double m = (1.0/E)*Math.signum(K); + double sigma_m = sigmaE/(E*E); + double G = 1.0/(sigma_m*sigma_m); + double dmdK = 1.0/FastMath.sqrt(1.0+tanL*tanL); + double dmdtanL = -K*tanL/FastMath.pow(1.0 + tanL*tanL, 1.5); + HelixState hsNew = this.copy(); + + invertC(); + HelixState.H.unsafe_set(2, 0, dmdK); + HelixState.H.unsafe_set(4, 0, dmdtanL); + for (int i=0; i<5; ++i) { + X.unsafe_set(i, 0, a.v[i]); + } + CommonOps_DDRM.mult(HelixState.Cinv, HelixState.X, HelixState.V); + CommonOps_DDRM.scale(G*m, HelixState.H); + CommonOps_DDRM.addEquals(HelixState.V, HelixState.H); + + for (int i=0; i<5; ++i) { + for (int j=0; j<5; ++j) { + hsNew.C.unsafe_set(i, j, HelixState.Cinv.unsafe_get(i, j)); + } + } + hsNew.C.unsafe_set(2, 2, HelixState.Cinv.unsafe_get(2, 2) + (dmdK*dmdK)*G); + hsNew.C.unsafe_set(4, 4, HelixState.Cinv.unsafe_get(4, 4) + (dmdtanL*dmdtanL)*G); + hsNew.C.unsafe_set(2, 4, HelixState.Cinv.unsafe_get(2, 4) + (dmdK*dmdtanL)*G); + hsNew.C.unsafe_set(4, 2, HelixState.Cinv.unsafe_get(4, 2) + (dmdtanL*dmdK)*G); + hsNew.invertC(); + for (int i=0; i<5; ++i) { + for (int j=0; j<5; ++j) { + hsNew.C.unsafe_set(i, j, HelixState.Cinv.unsafe_get(i, j)); + } + } + CommonOps_DDRM.mult(HelixState.Cinv, HelixState.V, HelixState.X); + for (int i=0; i<5; ++i) { + hsNew.a.v[i] = HelixState.X.unsafe_get(i, 0); + } + if (debug) { + System.out.println("Original helix params X:"); X.print(); + System.out.println("Derivative vector H:"); H.print(); + hsNew.a.print("New helix params"); + System.out.println("New covariance:"); hsNew.C.print(); + double varK = C.unsafe_get(2, 2); + double A = 1.0/FastMath.sqrt(1.0+tanL*tanL); + double Kprime = (K/varK + A*m*G)/(1.0/varK + G/(A*A)); + System.out.format(" Simple weighted mean update of kappa = %10.6f\n", Kprime); + } + return hsNew; + } + /** * Debug printout of the helix state * @param s A string used to identify the printout diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index 62ceb4bbbf..c8d770f6b0 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -12,6 +12,7 @@ import java.util.Random; import org.hps.recon.tracking.gbl.matrix.Matrix; +import org.apache.commons.math.util.FastMath; import org.ejml.data.DMatrixRMaj; import org.ejml.dense.row.CommonOps_DDRM; import org.ejml.dense.row.MatrixFeatures_DDRM; @@ -79,7 +80,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code // Read in the magnetic field map String mapType = "binary"; - String mapFile = "C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v3.bin"; + String mapFile = "C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"; + //String mapFile = "C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v3.bin"; FieldMap fM = null; FieldMap fMg = null; try { @@ -90,7 +92,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code return; } if (mapType != "binary") { - fM.writeBinaryFile("C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v2.bin"); + //fM.writeBinaryFile("C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v2.bin"); + fM.writeBinaryFile("C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"); } System.out.format("B field map vs y:\n"); for (double y=0.; y<1500.; y+=5.) { @@ -253,7 +256,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code double resolution = 0.006; // SSD point resolution, in mm double Q = -1.0; - double p = 1.3; + double p = 2.5; + double Etrue = p; double hitEfficiency = 1.0; Vec helixOrigin = new Vec(0., 0., 0.); // Pivot point of initial helix @@ -386,6 +390,13 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEkO = new Histogram(100, -10., 0.2, "Origin helix parameter K error", "sigmas", "track"); Histogram hEdzO = new Histogram(100, -10., 0.2, "Origin helix parameter dz error", "sigmas", "track"); Histogram hEtanlO = new Histogram(100, -10., 0.2, "Origin helix parameter tanl error", "sigmas", "track"); + + Histogram hEdrhoCon = new Histogram(100, -10., 0.2, "Origin helix parameter drho error with ECAL constraint", "sigmas", "track"); + Histogram hEphi0Con = new Histogram(100, -10., 0.2, "Origin helix parameter phi0 error with ECAL constraint", "sigmas", "track"); + Histogram hEkCon = new Histogram(100, -10., 0.2, "Origin helix parameter K error with ECAL constraint", "sigmas", "track"); + Histogram hEdzCon = new Histogram(100, -10., 0.2, "Origin helix parameter dz error with ECAL constraint", "sigmas", "track"); + Histogram hEtanlCon = new Histogram(100, -10., 0.2, "Origin helix parameter tanl error with ECAL constraint", "sigmas", "track"); + Histogram hEdrhoSigO = new Histogram(100, -10., 0.2, "Origin helix parameter drho constrained error", "sigmas", "track"); Histogram hEphi0SigO = new Histogram(100, -10., 0.2, "Origin helix parameter phi0 constrained error", "sigmas", "track"); Histogram hEkSigO = new Histogram(100, -10., 0.2, "Origin helix parameter K constrained error", "sigmas", "track"); @@ -393,9 +404,11 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEtanlSigO = new Histogram(100, -10., 0.2, "Origin helix parameter tanl constrained error", "sigmas", "track"); Histogram hEadrhoO = new Histogram(100, -1., 0.02, "Origin helix parameter drho error", "mm", "track"); Histogram hEaphi0O = new Histogram(100, -0.02, 0.0004, "Origin helix parameter phi0 error", "radians", "track"); - Histogram hEakO = new Histogram(100, -0.5, 0.01, "Origin helix parameter K error", "1/GeV", "track"); + Histogram hEakO = new Histogram(100, -0.2, 0.004, "Origin helix parameter K error", "1/GeV", "track"); + Histogram hEakcon = new Histogram(100, -0.2, 0.004, "Origin helix parameter K error with ECAL constraint", "1/GeV", "track"); Histogram hEadzO = new Histogram(100, -0.4, 0.008, "Origin helix parameter dz error", "mm", "track"); Histogram hEatanlO = new Histogram(100, -0.005, 0.0001, "Origin helix parameter tanl error", " ", "track"); + Histogram hEatanlcon = new Histogram(100, -0.005, 0.0001, "Origin helix parameter tanl error with ECAL constraint", " ", "track"); Histogram hEcdrhoO = new Histogram(100, -1., 0.02, "Origin helix parameter drho constrained error", "mm", "track"); Histogram hEcphi0O = new Histogram(100, -0.02, 0.0004, "Origin helix parameter phi0 constrained error", "radians", "track"); Histogram hEckO = new Histogram(100, -0.5, 0.01, "Origin helix parameter K constrained error", "1/GeV", "track"); @@ -1123,6 +1136,25 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { KalmanTrack.helixErr(i), hErr[i], e); } } + // Study the effect of an energy constraint + double sigmaE = 0.05*FastMath.sqrt(Etrue); + double E = Etrue + rnd.nextGaussian()*sigmaE; + KalmanTrack.energyConstraint(E, sigmaE); + if (verbose) { + System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n",Etrue,E,sigmaE); + KalmanTrack.helixAtOrigin.a.print("helix at origin"); + TkInitial.p.print("true helix"); + KalmanTrack.energyConstrainedHelix.a.print("energy constrained helix"); + KalmanTrack.printLong("after adding energy constraint"); + } + for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.energyConstrainedHelix.a.v[i] - TkInitial.p.v[i]); + hEatanlcon.entry(hErr[4]); + hEakcon.entry(hErr[2]); + hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(0,0))); + hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(1,1))); + hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(2,2))); + hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(3,3))); + hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(4,4))); } } } @@ -1177,6 +1209,11 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hEkO.plot(path + "kErrorO.gp", true, "gaus", " "); hEdzO.plot(path + "dzErrorO.gp", true, "gaus", " "); hEtanlO.plot(path + "tanlErrorO.gp", true, "gaus", " "); + hEdrhoCon.plot(path + "drhoErrorConSig.gp", true, "gaus", " "); + hEphi0Con.plot(path + "phi0ErrorConSig.gp", true, "gaus", " "); + hEkCon.plot(path + "kErrorConSig.gp", true, "gaus", " "); + hEdzCon.plot(path + "dzErrorConSig.gp", true, "gaus", " "); + hEtanlCon.plot(path + "tanlErrorConSig.gp", true, "gaus", " "); hEdrhoSigO.plot(path + "drhoErrorSigO.gp", true, "gaus", " "); hEphi0SigO.plot(path + "phi0ErrorSigO.gp", true, "gaus", " "); hEkSigO.plot(path + "kErrorSigO.gp", true, "gaus", " "); @@ -1185,8 +1222,10 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hEadrhoO.plot(path + "drhoErrorOa.gp", true, "gaus", " "); hEaphi0O.plot(path + "phi0ErrorOa.gp", true, "gaus", " "); hEakO.plot(path + "kErrorOa.gp", true, "gaus", " "); + hEakcon.plot(path + "kErrorCon.gp", true, "gaus", " "); hEadzO.plot(path + "dzErrorOa.gp", true, "gaus", " "); - hEatanlO.plot(path + "tanlErrorOa.gp", true, "gaus", " "); + hEatanlO.plot(path + "tanlErrorOa.gp", true, " ", " "); + hEatanlcon.plot(path + "tanlErrorCon.gp", true, " ", " "); hEcdrhoO.plot(path + "drhoErrorOc.gp", true, "gaus", " "); hEcphi0O.plot(path + "phi0ErrorOc.gp", true, "gaus", " "); hEckO.plot(path + "kErrorOc.gp", true, "gaus", " "); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 2625b05ec8..ddbaad365c 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -37,6 +37,7 @@ public class KalTrack { public int eventNumber; public boolean bad; HelixState helixAtOrigin; + HelixState energyConstrainedHelix; private boolean propagated; private RotMatrix Rot; // Rotation matrix between global and field coordinates at the beam spot private Vec originPoint; @@ -119,6 +120,7 @@ public class KalTrack { } helixAtOrigin = null; + energyConstrainedHelix = null; propagated = false; MeasurementSite site0 = this.SiteList.get(0); Vec B = null; @@ -502,7 +504,18 @@ String toString(String s) { str=str+originMomentum.toString("momentum of the particle at closest approach to the origin\n"); SquareMatrix C2 = new SquareMatrix(3, Cp); str=str+C2.toString("covariance matrix for the momentum"); + double tanL = helixAtOrigin.a.v[4]; + double K = helixAtOrigin.a.v[2]; + double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); + str=str+String.format(" Energy unconstrained = %10.6f\n", energy); } + if (energyConstrainedHelix != null) { + double tanL = energyConstrainedHelix.a.v[4]; + double K = energyConstrainedHelix.a.v[2]; + double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); + str=str+String.format(" Energy with constraint = %10.6f\n", energy); + str=str+energyConstrainedHelix.toString("helix state with energy constraint"); + } MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { str=str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); @@ -1478,6 +1491,14 @@ public boolean fit(boolean keep) { return true; } + void energyConstraint(double E, double sigmaE) { + if (!propagated) { + originHelix(); + propagated = true; + } + energyConstrainedHelix = helixAtOrigin.energyConstrained(E, sigmaE); + } + /** * Derivative matrix for propagating the covariance of the helix parameters to a covariance of momentum * @param a helix parameters From db1c369dd284c0a71ed93a591cb5c76695d192c3 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Wed, 23 Nov 2022 16:34:42 -0800 Subject: [PATCH 03/17] documentation update for ECAL constrained tracks --- .../hps/recon/tracking/kalman/KalmanDoc.pdf | Bin 269343 -> 284031 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDoc.pdf b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDoc.pdf index b6af9f40fa113a249846a450e84047f7871f62bc..1e5e2be2366c80557c45b657848f7e0e4304ff92 100644 GIT binary patch delta 159090 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

Jj7x~8=25w6 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

    axd$GZ!+C2C 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 delta 144771 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τl(ew~hWyo)61XN^~LF}u(+$onc8MWKk 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

    $~OCKy4% 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 Date: Tue, 20 Dec 2022 13:28:45 -0800 Subject: [PATCH 04/17] Create ReconstructedParticleRefitter.java A Driver used to refit ReconstructedParticles by including the energy of the associated ECal cluster in the track fit. A new ReconstructedParticle collection is added to the event. --- .../ReconstructedParticleRefitter.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java new file mode 100644 index 0000000000..3d23b2c271 --- /dev/null +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -0,0 +1,218 @@ +package org.hps.recon.particle; + +import hep.physics.vec.Hep3Vector; +import static java.lang.Math.abs; +import java.util.ArrayList; +import java.util.List; +import org.lcsim.detector.DetectorElementStore; +import org.lcsim.detector.IDetectorElement; +import org.lcsim.detector.identifier.IExpandedIdentifier; +import org.lcsim.detector.identifier.IIdentifier; +import org.lcsim.detector.identifier.IIdentifierDictionary; +import org.lcsim.detector.tracker.silicon.SiSensor; +import org.lcsim.event.Cluster; +import org.lcsim.event.EventHeader; +import org.lcsim.event.RawTrackerHit; +import org.lcsim.event.ReconstructedParticle; +import org.lcsim.event.Track; +import org.lcsim.event.TrackerHit; +import org.lcsim.event.base.BaseReconstructedParticle; +import org.lcsim.util.Driver; +import org.lcsim.util.aida.AIDA; + +/** + * This Driver is used to improve the measurement of ReconstructedParticles + * composed of a Track and an associated Ecal Cluster by including the energy of + * the cluster in a global refit. + * + * A new collection of ReconstructedParticles is added to the event. + * + * @author Norman A. Graf + * + */ +public class ReconstructedParticleRefitter extends Driver { + + /** + * The histogram handler + */ + private AIDA aida = AIDA.defaultInstance(); + + /** + * The name of the input ReconstructedParticle collection to process + */ + private String _finalStateParticleCollectionName = "FinalStateParticles_KF"; + + /** + * The name of the output ReconstructedParticle collection to add to the + * event + */ + private String _refitParticleCollectionName = "FinalStateParticles_KF_refit"; + + /** + * The tolerance on E/p for tracks and clusters to make sure we are fitting + * to a showering electron or positron and not a MIP trace or poorly matched + * combination + */ + private double _eOverpCut = 0.1; + + /** + * The action + * + * @param event + */ + public void process(EventHeader event) { + if (event.hasCollection(ReconstructedParticle.class, _finalStateParticleCollectionName)) { + // setup the hit-to-sensor associations + // should not need to do this if not reading in events from disk + setupSensors(event); + // instantiate the output collection of new ReconstructedParticles + List refitReconstructedParticles = new ArrayList<>(); + //fetch the input list of ReconstructedParticles to process + List rpList = event.get(ReconstructedParticle.class, _finalStateParticleCollectionName); + for (ReconstructedParticle rp : rpList) { + // skip particles without a track, i.e. photons + if (rp.getParticleIDUsed().getPDG() != 22) { + // skip particles without an associated cluster + if (!rp.getClusters().isEmpty()) { + // quick check on E/p so we don't try to fit to the energy of MIP tracks + double eOverP = rp.getEnergy() / rp.getMomentum().magnitude(); + aida.histogram1D("e over p", 100, 0., 2.).fill(eOverP); + if (abs(eOverP - 1) < _eOverpCut) { + // create a new ReconstructedParticle here... + refitReconstructedParticles.add(makeNewReconstructedParticle(rp)); + } else { + refitReconstructedParticles.add(rp); + } + } else { + refitReconstructedParticles.add(rp); + } + } else { + refitReconstructedParticles.add(rp); + } + } + // add the new collection to the event + event.put(_refitParticleCollectionName, refitReconstructedParticles, ReconstructedParticle.class, 0); + } + } + + /** + * + * The method to create a new ReconstructedParticle by refitting the track + * along with the cluster energy + * + * @param rp the ReconstructedParticle to refit + * @return + */ + private ReconstructedParticle makeNewReconstructedParticle(ReconstructedParticle rp) { + // Create a reconstructed particle to represent the track. + ReconstructedParticle particle = new BaseReconstructedParticle(); + Cluster cluster = rp.getClusters().get(0); + // refit the track with the cluster energy + Track newTrack = refitTrack(rp); + // Store the track in the particle. + particle.addTrack(newTrack); + + // Set the type of the particle. This is used to identify + // the tracking strategy used in finding the track associated with + // this particle. + // Modify this to flag that we have refit with the energy + // for now, just add 1000 + ((BaseReconstructedParticle) particle).setType(1000 + newTrack.getType()); + ((BaseReconstructedParticle) particle).setParticleIdUsed(new SimpleParticleID(rp.getParticleIDUsed().getPDG(), 0, 0, 0)); + // add cluster to the particle: + particle.addCluster(cluster); + // will need to set the RP fourVector, charge, etc. + // for now, leave as zero. + // TODO discuss what the best measurement of this is + + return particle; + } + + /** + * Method stub for refitting a track + * + * @param rp The input ReconstructedParticle with track and matching cluster + * @return the newly refit track including the cluster energy + */ + private Track refitTrack(ReconstructedParticle rp) { + Track track = rp.getTracks().get(0); + Cluster cluster = rp.getClusters().get(0); + //the energy of the associated cluster + double energy = cluster.getEnergy(); + //the list of tracker hits + List hits = track.getTrackerHits(); + // initial guess for the track parameters + Hep3Vector momentum = rp.getMomentum(); + // TODO fit a new track with this list of hits and the cluster energy. + // for now simply return the existing track + return track; + } + + /** + * Method to associate SVT hits to sensors + * + * @param event + */ + private void setupSensors(EventHeader event) { + List rawTrackerHits = event.get(RawTrackerHit.class, "SVTRawTrackerHits"); + EventHeader.LCMetaData meta = event.getMetaData(rawTrackerHits); + // Get the ID dictionary and field information. + IIdentifierDictionary dict = meta.getIDDecoder().getSubdetector().getDetectorElement().getIdentifierHelper().getIdentifierDictionary(); + int fieldIdx = dict.getFieldIndex("side"); + int sideIdx = dict.getFieldIndex("strip"); + for (RawTrackerHit hit : rawTrackerHits) { + // The "side" and "strip" fields needs to be stripped from the ID for sensor lookup. + IExpandedIdentifier expId = dict.unpack(hit.getIdentifier()); + expId.setValue(fieldIdx, 0); + expId.setValue(sideIdx, 0); + IIdentifier strippedId = dict.pack(expId); + // Find the sensor DetectorElement. + List des = DetectorElementStore.getInstance().find(strippedId); + if (des == null || des.size() == 0) { + throw new RuntimeException("Failed to find any DetectorElements with stripped ID <0x" + Long.toHexString(strippedId.getValue()) + ">."); + } else if (des.size() == 1) { + hit.setDetectorElement((SiSensor) des.get(0)); + } else { + // Use first sensor found, which should work unless there are sensors with duplicate IDs. + for (IDetectorElement de : des) { + if (de instanceof SiSensor) { + hit.setDetectorElement((SiSensor) de); + break; + } + } + } + // No sensor was found. + if (hit.getDetectorElement() == null) { + throw new RuntimeException("No sensor was found for hit with stripped ID <0x" + Long.toHexString(strippedId.getValue()) + ">."); + } + } + } + + /** + * Convenience method to set the name of the input collection of + * ReconstructedParticles + * + * @param s + */ + public void setFinalStateParticleCollectionName(String s) { + _finalStateParticleCollectionName = s; + } + + /** + * Convenience method to set the name of the output collection of + * ReconstructedParticles + * + * @param s + */ + public void setRefitParticleCollectionName(String s) { + _refitParticleCollectionName = s; + } + + /** + * + * @param d + */ + public void set_eOverpCut(double d) { + _eOverpCut = d; + } +} From 156e361e9c739c4b3ca705659bee5f2e06fba4f9 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Wed, 21 Dec 2022 09:06:40 -0800 Subject: [PATCH 05/17] implement smoothing of tracks that have ECAL energy constraint --- .../org/hps/recon/tracking/kalman/Helix.java | 41 ++--- .../hps/recon/tracking/kalman/HelixState.java | 27 +-- .../hps/recon/tracking/kalman/HelixTest3.java | 107 +++++++----- .../hps/recon/tracking/kalman/KalTrack.java | 155 ++++++++++++++---- .../tracking/kalman/KalmanInterface.java | 6 +- .../recon/tracking/kalman/KalmanParams.java | 8 + .../tracking/kalman/KalmanPatRecDriver.java | 11 +- .../tracking/kalman/KalmanPatRecPlots.java | 2 +- .../tracking/kalman/MeasurementSite.java | 23 +-- .../hps/recon/tracking/kalman/PatRecTest.java | 6 +- .../tracking/kalman/PropagatedTrackState.java | 4 +- .../hps/recon/tracking/kalman/RKhelix.java | 16 +- .../recon/tracking/kalman/StateVector.java | 35 ++-- .../hps/recon/tracking/kalman/TestMain.java | 2 +- 14 files changed, 288 insertions(+), 155 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java index 7b290b84fa..1c1861be13 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java @@ -21,17 +21,19 @@ class Helix { Vec origin; // Origin of the B-field reference frame in global coordinates private FieldMap fM; private Random rndm; + private boolean doeLoss; // Construct a helix starting from a momentum vector - Helix(double Q, Vec Xinit, Vec Pinit, Vec origin, FieldMap fM, Random rndm) { + Helix(double Q, Vec Xinit, Vec Pinit, Vec origin, FieldMap fM, Random rndm, boolean doeLoss) { this.Q = Q; this.origin = origin.copy(); + this.doeLoss = doeLoss; this.fM = fM; Vec Bf = new Vec(3,fM.getField(Xinit)); B = Bf.mag(); double c = 2.99793e8; alpha = 1000.0 * 1.0E9 / (c * B); // Units are Tesla, mm, GeV - rho = 2.329; // Density of silicon in g/cm^2 + rho = 2.329; // Density of silicon in g/cm^3 tB = Bf.unitVec(B); Vec yhat = new Vec(0., 1.0, 0.); uB = yhat.cross(tB).unitVec(); @@ -53,9 +55,10 @@ class Helix { } // Construct a helix from given helix parameters (given in B field frame) - Helix(Vec HelixParams, Vec pivotGlobal, Vec origin, FieldMap fM, Random rndm) { + Helix(Vec HelixParams, Vec pivotGlobal, Vec origin, FieldMap fM, Random rndm, boolean doeLoss) { this.origin = origin.copy(); this.fM = fM; + this.doeLoss = doeLoss; p = HelixParams.copy(); Vec Bf = new Vec(3,fM.getField(pivotGlobal)); B = Bf.mag(); @@ -85,7 +88,7 @@ class Helix { } Helix copy() { - return new Helix(p, R.inverseRotate(X0).sum(origin), origin, fM, rndm); + return new Helix(p, R.inverseRotate(X0).sum(origin), origin, fM, rndm, doeLoss); } void print(String s) { @@ -216,8 +219,12 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc tnew = RB.rotate(tnew); double E = pmom.mag(); // Everything is assumed electron - double sp = 0.0; // 0.002; // Estar collision stopping power for electrons in silicon at about a - // GeV, in GeV cm2/g + double sp = 0.; + if (doeLoss) { + sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g + sp = sp*20; // ToDo temporary!!! + } + sp = sp*20; // ToDo temporary!!! double dEdx = 0.1 * sp * rho; // in GeV/mm double eLoss = dEdx * X / ct; // System.out.format("randomScat: energy=%10.7f, energy loss=%10.7f\n", E, eLoss); @@ -229,26 +236,6 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc Vec H = new Vec(0., phi0, K, 0., tanl); // Pivot point is on the helix, at the plane intersection point, so drho and dz are zero // H.print("scattered helix parameters"); - return new Helix(H, r, P.X(), fM, rndm); // Create the new helix with new origin and pivot point - } - - Vec pivotTransform(Vec pivot) { - double xC = X0.v[0] + (p.v[0] + alpha / p.v[2]) * Math.cos(p.v[1]); // Center of the helix circle - double yC = X0.v[1] + (p.v[0] + alpha / p.v[2]) * Math.sin(p.v[1]); - // System.out.format("pivotTransform center=%10.6f, %10.6f\n", xC, yC); - - // Predicted state vector - double[] aP = new double[5]; - aP[2] = p.v[2]; - aP[4] = p.v[4]; - if (p.v[2] > 0) { - aP[1] = Math.atan2(yC - pivot.v[1], xC - pivot.v[0]); - } else { - aP[1] = Math.atan2(pivot.v[1] - yC, pivot.v[0] - xC); - } - aP[0] = (xC - pivot.v[0]) * Math.cos(aP[1]) + (yC - pivot.v[1]) * Math.sin(aP[1]) - alpha / p.v[2]; - aP[3] = X0.v[2] - pivot.v[2] + p.v[3] - (alpha / p.v[2]) * (aP[1] - p.v[1]) * p.v[4]; - - return new Vec(5, aP); + return new Helix(H, r, P.X(), fM, rndm, doeLoss); // Create the new helix with new origin and pivot point } } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java index ffe29c0a2a..66a5684a54 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixState.java @@ -292,26 +292,27 @@ Vec helixErrors() { * @param aP Input transformed helix parameters * @param F Returned derivative matrix */ - void makeF(Vec aP, DMatrixRMaj F) { - makeF(aP, F, a, alpha); + void makeF(Vec aP, DMatrixRMaj F, double eFactor) { + makeF(aP, F, a, alpha, eFactor); } /** - * Create derivative matrix for the pivot transform (without energy loss or field rotations) + * Create derivative matrix for the pivot transform (without field rotations) * @param aP Helix parameters * @param F Returned derivative matrix + * @param eFactor 1.0-deltaE/E factor for energy loss */ - static void makeF(Vec aP, DMatrixRMaj F, Vec a, double alpha) { + static void makeF(Vec aP, DMatrixRMaj F, Vec a, double alpha, double eFactor) { F.unsafe_set(0, 0, FastMath.cos(aP.v[1] - a.v[1])); F.unsafe_set(0, 1, (a.v[0] + alpha / a.v[2]) * FastMath.sin(aP.v[1] - a.v[1])); - F.unsafe_set(0, 2, (alpha / (a.v[2] * a.v[2])) * (1.0 - FastMath.cos(aP.v[1] - a.v[1]))); + F.unsafe_set(0, 2, eFactor*(alpha / (a.v[2] * a.v[2])) * (1.0 - FastMath.cos(aP.v[1] - a.v[1]))); F.unsafe_set(1, 0, -FastMath.sin(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2])); F.unsafe_set(1, 1, (a.v[0] + alpha / a.v[2]) * FastMath.cos(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2])); - F.unsafe_set(1, 2, (alpha / (a.v[2] * a.v[2])) * FastMath.sin(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2])); - F.unsafe_set(2, 2, 1.0); + F.unsafe_set(1, 2, eFactor*(alpha / (a.v[2] * a.v[2])) * FastMath.sin(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2])); + F.unsafe_set(2, 2, eFactor); F.unsafe_set(3, 0, (alpha / a.v[2]) * a.v[4] * FastMath.sin(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2])); F.unsafe_set(3, 1, (alpha / a.v[2]) * a.v[4] * (1.0 - (a.v[0] + alpha / a.v[2]) * FastMath.cos(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2]))); - F.unsafe_set(3, 2, (alpha / (a.v[2] * a.v[2])) * a.v[4] + F.unsafe_set(3, 2, eFactor*(alpha / (a.v[2] * a.v[2])) * a.v[4] * (aP.v[1] - a.v[1] - (alpha / a.v[2]) * FastMath.sin(aP.v[1] - a.v[1]) / (aP.v[0] + alpha / a.v[2]))); F.unsafe_set(3, 3, 1.0); F.unsafe_set(3, 4, -(alpha / a.v[2]) * (aP.v[1] - a.v[1])); @@ -526,8 +527,7 @@ static Vec pivotTransform(Vec pivot, Vec a, Vec X0, double alpha, double deltaEo double K = a.v[2] * (1.0 - deltaEoE); // Lose energy before propagating double xC = X0.v[0] + (a.v[0] + alpha / K) * FastMath.cos(a.v[1]); // Center of the helix circle double yC = X0.v[1] + (a.v[0] + alpha / K) * FastMath.sin(a.v[1]); - // if (verbose) System.out.format("pivotTransform center=%13.10f, %13.10f\n", - // xC, yC); + // if (verbose) System.out.format("pivotTransform center=%13.10f, %13.10f\n", xC, yC); // Predicted state vector double[] aP = new double[5]; @@ -677,7 +677,7 @@ Vec getRKintersection() { */ static double projMSangle(double p, double XL) { if (XL <= 0.) return 0.; - return (0.0136 / Math.abs(p)) * FastMath.sqrt(XL) * (1.0 + 0.038 * FastMath.log(XL)); + return (0.0136 / p) * FastMath.sqrt(XL) * (1.0 + 0.038 * FastMath.log(XL)); } /** @@ -690,6 +690,7 @@ static double projMSangle(double p, double XL) { * All scattering layers are assumed to be of the same thickness XL, in radiation lengths * We assume that the starting StateVector is at a layer with a hit, in which case the Kalman filter has already accounted for * multiple scattering at that layer. + * Note: energy loss is not included. * @param maxStep Maximum step size, in mm * @param yScat Locations of scattering planes * @param XL radiation lengths of scattering planes @@ -819,7 +820,7 @@ boolean helixStepper(double maxStep, ArrayList yScat, ArrayList Vec newPoint = atPhi(newPivot, newHelixPivoted, dphi, localAlpha); newPoint.print("new point of intersection, should be same as the old"); } - makeF(newHelixPivoted, F, newHelix, localAlpha); + makeF(newHelixPivoted, F, newHelix, localAlpha, 1.0); newHelix = newHelixPivoted; // Rotate the helix into the field system at the new origin @@ -868,7 +869,7 @@ boolean helixStepper(double maxStep, ArrayList yScat, ArrayList Vec newOriginLocal = new Vec(0.,0.,0.); Vec oldPivot = RM.rotate(Origin.dif(newOrigin)); Vec finalHx = pivotTransform(newOriginLocal, newHelix, oldPivot, localAlpha, 0.); - makeF(finalHx, F, newHelix, localAlpha); + makeF(finalHx, F, newHelix, localAlpha, 1.0); CommonOps_DDRM.multTransB(Cov, F, cIntermediate); CommonOps_DDRM.mult(F,cIntermediate,Cov); if (debug) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index c8d770f6b0..233b60fb78 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -39,6 +39,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code // Units are Tesla, GeV, mm int nTrials = 10000; // The number of test events to generate for fitting + boolean residualsEconstrained = false; // Use constrained or unconstrained tracks for residuals histograms int startLayer = 10; // Where to start the Kalman filtering int nIteration = 2; // Number of filter iterations int nAxial = 3; // Number of axial layers needed by the linear fit @@ -68,6 +69,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code } } KalmanParams kPar = new KalmanParams(); + kPar.setEloss(true); kPar.print(); // Seed the random number generator @@ -80,8 +82,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code // Read in the magnetic field map String mapType = "binary"; - String mapFile = "C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"; - //String mapFile = "C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v3.bin"; + String mapFile = "C:\\Users\\rjohn\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"; + //String mapFile = "C:\\Users\\rjohn\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v3.bin"; FieldMap fM = null; FieldMap fMg = null; try { @@ -92,8 +94,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code return; } if (mapType != "binary") { - //fM.writeBinaryFile("C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v2.bin"); - fM.writeBinaryFile("C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"); + //fM.writeBinaryFile("C:\\Users\\rjohn\\Documents\\GitHub\\hps-java\\fieldmap\\125acm2_3kg_corrected_unfolded_scaled_0.7992_v2.bin"); + fM.writeBinaryFile("C:\\Users\\rjohn\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"); } System.out.format("B field map vs y:\n"); for (double y=0.; y<1500.; y+=5.) { @@ -256,7 +258,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code double resolution = 0.006; // SSD point resolution, in mm double Q = -1.0; - double p = 2.5; + double p = 3.5; double Etrue = p; double hitEfficiency = 1.0; @@ -287,8 +289,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code Vec helixMCtrue = null; Vec momentum = new Vec(p * initialDirection.v[0], p * initialDirection.v[1], p * initialDirection.v[2]); momentum.print("initial helix momentum"); - Helix TkInitial = new Helix(Q, helixOrigin, momentum, helixOrigin, fMg, rnd); - RKhelix TkRKinitial = new RKhelix(helixOrigin, momentum, Q, fMg, rnd); + Helix TkInitial = new Helix(Q, helixOrigin, momentum, helixOrigin, fMg, rnd, kPar.eLoss); + RKhelix TkRKinitial = new RKhelix(helixOrigin, momentum, Q, fMg, rnd, kPar.eLoss); drho = TkInitial.p.v[0]; phi0 = TkInitial.p.v[1]; K = TkInitial.p.v[2]; @@ -370,6 +372,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hXscat = new Histogram(100, -1., 0.02, "X scattering angles", "degrees", "Si planes"); Histogram hZscat = new Histogram(100, -0.5, 0.01, "Z scattering angles", "degrees", "Si planes"); Histogram hChi2HelixS = new Histogram(80, 0., 0.4, "smoothed chi^2 of helix parameters", "chi^2", "tracks"); + Histogram hChi2HelixE = new Histogram(80, 0., 0.4, "Helix parameters chi^2 at the origin, energy constrained", "chi^2", "tracks"); Histogram hChi2Helix = new Histogram(80, 0., 0.4, "filtered chi^2 of helix parameters", "chi^2", "tracks"); Histogram hChi2Guess = new Histogram(80, 0., 2.0, "chi^2 of guess helix parameters", "chi^2", "tracks"); Histogram hChi2Origin = new Histogram(80, 0., .4, "Helix parameters chi^2 at the origin", "chi^2", "tracks"); @@ -390,7 +393,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEkO = new Histogram(100, -10., 0.2, "Origin helix parameter K error", "sigmas", "track"); Histogram hEdzO = new Histogram(100, -10., 0.2, "Origin helix parameter dz error", "sigmas", "track"); Histogram hEtanlO = new Histogram(100, -10., 0.2, "Origin helix parameter tanl error", "sigmas", "track"); - + Histogram hChi2E = new Histogram(80, 0., 1.0, "Helix fit chi^2 after energy constraint", "chi^2", "tracks"); Histogram hEdrhoCon = new Histogram(100, -10., 0.2, "Origin helix parameter drho error with ECAL constraint", "sigmas", "track"); Histogram hEphi0Con = new Histogram(100, -10., 0.2, "Origin helix parameter phi0 error with ECAL constraint", "sigmas", "track"); Histogram hEkCon = new Histogram(100, -10., 0.2, "Origin helix parameter K error with ECAL constraint", "sigmas", "track"); @@ -404,12 +407,15 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEtanlSigO = new Histogram(100, -10., 0.2, "Origin helix parameter tanl constrained error", "sigmas", "track"); Histogram hEadrhoO = new Histogram(100, -1., 0.02, "Origin helix parameter drho error", "mm", "track"); Histogram hEaphi0O = new Histogram(100, -0.02, 0.0004, "Origin helix parameter phi0 error", "radians", "track"); + Histogram hEPhi0con = new Histogram(100, -0.02, 0.0004, "Origin helix parameter phi0 error with ECAL constraint", "radians", "track"); Histogram hEakO = new Histogram(100, -0.2, 0.004, "Origin helix parameter K error", "1/GeV", "track"); Histogram hEakcon = new Histogram(100, -0.2, 0.004, "Origin helix parameter K error with ECAL constraint", "1/GeV", "track"); Histogram hEadzO = new Histogram(100, -0.4, 0.008, "Origin helix parameter dz error", "mm", "track"); + Histogram hEZ0con = new Histogram(100, -0.4, 0.008, "Origin helix parameter dz error with ECAL constraint", "mm", "track"); Histogram hEatanlO = new Histogram(100, -0.005, 0.0001, "Origin helix parameter tanl error", " ", "track"); Histogram hEatanlcon = new Histogram(100, -0.005, 0.0001, "Origin helix parameter tanl error with ECAL constraint", " ", "track"); Histogram hEcdrhoO = new Histogram(100, -1., 0.02, "Origin helix parameter drho constrained error", "mm", "track"); + Histogram hERhocon = new Histogram(100, -1., 0.02, "Origin helix parameter drho constrained error with ECAL constrain", "mm", "track"); Histogram hEcphi0O = new Histogram(100, -0.02, 0.0004, "Origin helix parameter phi0 constrained error", "radians", "track"); Histogram hEckO = new Histogram(100, -0.5, 0.01, "Origin helix parameter K constrained error", "1/GeV", "track"); Histogram hEcdzO = new Histogram(100, -0.4, 0.008, "Origin helix parameter dz constrained error", "mm", "track"); @@ -435,14 +441,16 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram[] hResidZ = new Histogram[nLayers]; Histogram[] hUnbias = new Histogram[nLayers]; Histogram[] hUnbiasSig = new Histogram[nLayers]; + String preamb = ""; + if (residualsEconstrained) preamb = "E constrained "; for (int i = 0; i < nLayers; i++) { - hResidS0[i] = new Histogram(100, -10., 0.2, String.format("Smoothed fit residual for plane %d", i), "sigmas", "hits"); - hResidS2[i] = new Histogram(100, -0.02, 0.0004, String.format("Smoothed fit residual for plane %d", i), "mm", "hits"); - hResidS4[i] = new Histogram(100, -0.1, 0.002, String.format("Smoothed true residual for plane %d", i), "mm", "hits"); - hResidX[i] = new Histogram(100, -0.8, 0.016, String.format("True residual in global X for plane %d", i), "mm", "hits"); - hResidZ[i] = new Histogram(100, -0.1, 0.002, String.format("True residual in global Z for plane %d", i), "mm", "hits"); - hUnbias[i] = new Histogram(100, -0.2, 0.004, String.format("Unbiased residual for plane %d", i), "mm", "hits"); - hUnbiasSig[i] = new Histogram(100, -10., 0.2, String.format("Unbiased residuals for layer %d", i), "sigmas", "hits"); + hResidS0[i] = new Histogram(100, -10., 0.2, String.format(preamb+"Smoothed fit residual for plane %d", i), "sigmas", "hits"); + hResidS2[i] = new Histogram(100, -0.02, 0.0004, String.format(preamb+"Smoothed fit residual for plane %d", i), "mm", "hits"); + hResidS4[i] = new Histogram(100, -0.1, 0.002, String.format(preamb+"Smoothed true residual for plane %d", i), "mm", "hits"); + hResidX[i] = new Histogram(100, -0.8, 0.016, String.format(preamb+"True residual in global X for plane %d", i), "mm", "hits"); + hResidZ[i] = new Histogram(100, -0.1, 0.002, String.format(preamb+"True residual in global Z for plane %d", i), "mm", "hits"); + hUnbias[i] = new Histogram(100, -0.2, 0.004, String.format(preamb+"Unbiased residual for plane %d", i), "mm", "hits"); + hUnbiasSig[i] = new Histogram(100, -10., 0.2, String.format(preamb+"Unbiased residuals for layer %d", i), "sigmas", "hits"); } Histogram hpropx = new Histogram(100,-5.,0.1,"projected track-state x error","mm","track"); Histogram hpropxs = new Histogram(100,-5.,0.1,"projected track-state x error","sigmas","track"); @@ -479,8 +487,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code Vec pivotBegin = TkRKinitial.planeIntersect(si1.p, p1); Vec errOfB = pivotBegin.dif(lyr1Int); errOfB.print("error in extrap to lyr 1 without RK"); - Helix helixBegin = new Helix(Q, pivotBegin, p1, pivotBegin, fMg, rnd); - RKhelix helixBeginRK = new RKhelix(pivotBegin, p1, Q, fMg, rnd); + Helix helixBegin = new Helix(Q, pivotBegin, p1, pivotBegin, fMg, rnd, kPar.eLoss); + Helix helixEnd = null; + RKhelix helixBeginRK = new RKhelix(pivotBegin, p1, Q, fMg, rnd, kPar.eLoss); Vec[] helixSaved = new Vec[SiModules.size()]; Vec[] pivotSaved = new Vec[SiModules.size()]; @@ -524,7 +533,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code if (verbose) { System.out.format(" Intersection point is outside of the detector %d in layer %d\n", det, pln); } continue; } - Tk = new RKhelix(rscat, pInt, Q, fMg, rnd); + Tk = new RKhelix(rscat, pInt, Q, fMg, rnd, kPar.eLoss); if (thisSi.Layer == startLayer || rnd.nextDouble() < hitEfficiency) { // Apply some hit inefficiency double[] gran = new double[2]; if (perfect) { @@ -764,6 +773,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code KalmanTrack.originHelix(); if (verbose) KalmanTrack.print("KalmanTrack"); + // Apply ECAL energy constraint + if (kPar.eLoss) Etrue = TkEnd.p.mag(); + double sigmaE = 0.03*FastMath.sqrt(Etrue); + double E = Etrue + rnd.nextGaussian()*sigmaE; + KalmanTrack.energyConstraint(E, sigmaE); + // Check on the covariance matrix Matrix C = new Matrix(KalmanTrack.originCovariance()); EigenvalueDecomposition eED= new EigenvalueDecomposition(C); @@ -852,7 +867,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { intcpt = lastSite.aS.helix.toGlobal(intcpt); if (debug) intcpt.print("intercept global"); DMatrixRMaj F = new DMatrixRMaj(5,5); - lastSite.aS.helix.makeF(helixAtIntcpt, F); + lastSite.aS.helix.makeF(helixAtIntcpt, F, 1.0); if (debug) F.print("tranform matrix F"); Vec pMom = HelixState.getMom(0.,helixAtIntcpt); double pMag = pMom.mag(); @@ -943,17 +958,20 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { else hResid1.entry(site.aF.r / Math.sqrt(site.aF.R)); } if (site.smoothed) { - if (site.m.Layer == 4) hReducedErr.entry(Math.sqrt(site.aS.R)); - chi2s += Math.pow(site.aS.mPred - site.m.hits.get(site.hitID).vTrue, 2) / site.aS.R; - hResidS0[siM.Layer].entry(site.aS.r / Math.sqrt(site.aS.R)); - hResidS2[siM.Layer].entry(site.aS.r); - if (site.hitID >= 0) { hResidS4[siM.Layer].entry(site.m.hits.get(site.hitID).vTrue - site.aS.mPred); } + StateVector aA = null; + if (residualsEconstrained) aA = site.aES; + else aA = site.aS; + if (site.m.Layer == 4) hReducedErr.entry(Math.sqrt(aA.R)); + chi2s += Math.pow(aA.mPred - site.m.hits.get(site.hitID).vTrue, 2) / aA.R; + hResidS0[siM.Layer].entry(aA.r / Math.sqrt(aA.R)); + hResidS2[siM.Layer].entry(aA.r); + if (site.hitID >= 0) { hResidS4[siM.Layer].entry(site.m.hits.get(site.hitID).vTrue - aA.mPred); } } } } } for (int layer=0; layer < nLayers; ++layer) { - Pair resid = KalmanTrack.unbiasedResidual(layer); + Pair resid = KalmanTrack.unbiasedResidual(layer, residualsEconstrained); if (resid.getSecondElement() > -999.) { double variance = resid.getSecondElement(); double sigma = Math.sqrt(variance); @@ -1018,7 +1036,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } } Vec aFe = new Vec(5); - DMatrixRMaj aFC = kF.fittedStateBegin().covariancePivotTransform(aF); + DMatrixRMaj aFC = kF.fittedStateBegin().covariancePivotTransform(aF, 1.0); CommonOps_DDRM.multTransB(aFC, fRot, tempM1); CommonOps_DDRM.mult(fRot, tempM1, aFC); for (int i = 0; i < 5; i++) aFe.v[i] = Math.sqrt(Math.max(0., aFC.unsafe_get(i,i))); @@ -1037,9 +1055,12 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hEtanlS.entry(trueErr.v[4] / aFe.v[4]); double helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(aFC).invert())); hChi2HelixS.entry(helixChi2); + Vec pivotEnd = TkEnd.x; + helixEnd = new Helix(Q, pivotEnd, TkEnd.p, pivotEnd, fMg, rnd, kPar.eLoss); if (verbose) { System.out.format("Full chi^2 of the smoothed helix parameters = %12.4e\n", helixChi2); TkEnd.print("MC true helix at the last detector plane"); + helixEnd.print("MC true helix params at the last detector plane"); kF.fittedStateEnd().print("fitted state at the last detector plane"); } newPivot = kF.fittedStateEnd().helix.toLocal(TkEnd.R(kF.fittedStateEnd().helix.origin) @@ -1049,7 +1070,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { eF.print("final smoothed helix parameters at the track end"); newPivot.print("new pivot at the track end"); } - DMatrixRMaj eFc = kF.fittedStateEnd().covariancePivotTransform(eF); + DMatrixRMaj eFc = kF.fittedStateEnd().covariancePivotTransform(eF, 1.0); Vec eFe = new Vec(5); for (int i = 0; i < 5; i++) eFe.v[i] = Math.sqrt(Math.max(0., eFc.unsafe_get(i,i))); Vec pivotF = new Vec(3); @@ -1137,24 +1158,31 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } } // Study the effect of an energy constraint - double sigmaE = 0.05*FastMath.sqrt(Etrue); - double E = Etrue + rnd.nextGaussian()*sigmaE; - KalmanTrack.energyConstraint(E, sigmaE); + MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size()-1); if (verbose) { + lastSite.print("last site on track"); + helixEnd.print("true helix at last site on track"); System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n",Etrue,E,sigmaE); KalmanTrack.helixAtOrigin.a.print("helix at origin"); TkInitial.p.print("true helix"); - KalmanTrack.energyConstrainedHelix.a.print("energy constrained helix"); + KalmanTrack.helixAtOriginEconstraint.a.print("energy constrained helix"); KalmanTrack.printLong("after adding energy constraint"); } - for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.energyConstrainedHelix.a.v[i] - TkInitial.p.v[i]); + hChi2E.entry(KalmanTrack.chi2_Econstraint); + for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.helixAtOriginEconstraint.a.v[i] - TkInitial.p.v[i]); hEatanlcon.entry(hErr[4]); hEakcon.entry(hErr[2]); - hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(0,0))); - hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(1,1))); - hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(2,2))); - hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(3,3))); - hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(4,4))); + hERhocon.entry(hErr[0]); + hEPhi0con.entry(hErr[1]); + hEZ0con.entry(hErr[3]); + hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(0,0))); + hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(1,1))); + hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(2,2))); + hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(3,3))); + hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(4,4))); + trueErr = new Vec(5,hErr); + helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(KalmanTrack.helixAtOriginEconstraint.C).invert())); + hChi2HelixE.entry(helixChi2); } } } @@ -1188,7 +1216,9 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hChi2Alt.plot(path + "chi2sAlt.gp", true, " ", " "); hChi2p.plot(path + "chi2prm.gp", true, " ", " "); hChi2f.plot(path + "chi2f.gp", true, " ", " "); + hChi2E.plot(path + "chi2E.gp", true, " ", " "); hChi2HelixS.plot(path + "chi2helixS.gp", true, " ", " "); + hChi2HelixE.plot(path + "chi2helixE.gp", true, " ", " "); hChi2Origin.plot(path + "chi2helixO.gp", true, " ", " "); hChi2OriginC.plot(path + "chi2helixOc.gp", true, " ", " "); hChi2Helix.plot(path + "chi2helixF.gp", true, " ", " "); @@ -1206,6 +1236,9 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hEtanlS.plot(path + "tanlErrorS.gp", true, "gaus", " "); hEdrhoO.plot(path + "drhoErrorO.gp", true, "gaus", " "); hEphi0O.plot(path + "phi0ErrorO.gp", true, "gaus", " "); + hERhocon.plot(path + "drhoErrorE.gp", true, "gaus", " "); + hEPhi0con.plot(path + "phi0ErrorE.gp", true, "gaus", " "); + hEZ0con.plot(path + "Z0ErrorE.gp", true, "gaus", " "); hEkO.plot(path + "kErrorO.gp", true, "gaus", " "); hEdzO.plot(path + "dzErrorO.gp", true, "gaus", " "); hEtanlO.plot(path + "tanlErrorO.gp", true, "gaus", " "); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index ddbaad365c..dbef9c983a 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -26,6 +26,7 @@ public class KalTrack { public int ID; public int nHits; public double chi2; + public double chi2_Econstraint; private double reducedChi2; ArrayList SiteList; @@ -37,8 +38,8 @@ public class KalTrack { public int eventNumber; public boolean bad; HelixState helixAtOrigin; - HelixState energyConstrainedHelix; - private boolean propagated; + HelixState helixAtOriginEconstraint; + private boolean propagated, propagatedE; private RotMatrix Rot; // Rotation matrix between global and field coordinates at the beam spot private Vec originPoint; private Vec originMomentum; @@ -59,7 +60,7 @@ public class KalTrack { private static DMatrixRMaj Cinv; private static Logger logger; private static boolean initialized; - private double [] arcLength; + private double [] arcLength, arcLengthE; private static LinearSolverDense solver; private static final boolean uniformBatOrigin = false; static int [] nBadCov = {0, 0}; @@ -86,7 +87,7 @@ public class KalTrack { if (!initialized) { logger = Logger.getLogger(KalTrack.class.getName()); - tempV = new DMatrixRMaj(5,1); + if (tempV == null) tempV = new DMatrixRMaj(5,1); Cinv = new DMatrixRMaj(5,5); initialized = true; solver = LinearSolverFactory_DDRM.symmPosDef(5); @@ -120,8 +121,9 @@ public class KalTrack { } helixAtOrigin = null; - energyConstrainedHelix = null; + helixAtOriginEconstraint = null; propagated = false; + propagatedE = false; MeasurementSite site0 = this.SiteList.get(0); Vec B = null; if (kPar.uniformB) { @@ -354,10 +356,10 @@ public double chi2prime() { * @param millipedeID * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidualMillipede(int millipedeID) { + public Pair unbiasedResidualMillipede(int millipedeID, boolean eConstrain) { if (millipedeMap == null) makeMillipedeMap(); if (millipedeMap.containsKey(millipedeID)) { - return unbiasedResidual(millipedeMap.get(millipedeID)); + return unbiasedResidual(millipedeMap.get(millipedeID), eConstrain); } else { return new Pair(-999., -999.); } @@ -368,10 +370,10 @@ public Pair unbiasedResidualMillipede(int millipedeID) { * @param layer * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidual(int layer) { + public Pair unbiasedResidual(int layer, boolean eConstrain) { if (lyrMap == null) makeLyrMap(); if (lyrMap.containsKey(layer)) { - return unbiasedResidual(lyrMap.get(layer)); + return unbiasedResidual(lyrMap.get(layer), eConstrain); } else { return new Pair(-999., -999.); } @@ -382,20 +384,22 @@ public Pair unbiasedResidual(int layer) { * @param site measurement site * @return residual and error */ - public Pair unbiasedResidual(MeasurementSite site) { + public Pair unbiasedResidual(MeasurementSite site, boolean eConstrain) { double resid = -999.; double varResid = -999.; Vec aStar = null; if (site.hitID >= 0) { + StateVector aA = site.aS; + if (eConstrain) aA = site.aES; double sigma = site.m.hits.get(site.hitID).sigma; DMatrixRMaj Cstar = new DMatrixRMaj(5,5); - aStar = site.aS.inverseFilter(site.H, sigma*sigma, Cstar); + aStar = aA.inverseFilter(site.H, sigma*sigma, Cstar); HelixPlaneIntersect hpi = new HelixPlaneIntersect(); - Plane pTrans = site.m.p.toLocal(site.aS.helix.Rot, site.aS.helix.origin); - double phiInt = hpi.planeIntersect(aStar, site.aS.helix.X0, site.aS.helix.alpha, pTrans); + Plane pTrans = site.m.p.toLocal(aA.helix.Rot, aA.helix.origin); + double phiInt = hpi.planeIntersect(aStar, aA.helix.X0, aA.helix.alpha, pTrans); if (!Double.isNaN(phiInt)) { - Vec intPnt = HelixState.atPhi(site.aS.helix.X0, aStar, phiInt, site.aS.helix.alpha); - Vec globalInt = site.aS.helix.toGlobal(intPnt); + Vec intPnt = HelixState.atPhi(aA.helix.X0, aStar, phiInt, aA.helix.alpha); + Vec globalInt = aA.helix.toGlobal(intPnt); Vec localInt = site.m.toLocal(globalInt); resid = site.m.hits.get(site.hitID).v - localInt.v[1]; @@ -456,6 +460,7 @@ public Pair biasedResidual(MeasurementSite site) { public void print(String s) { System.out.format("\nKalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); if (propagated) System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); + if (propagatedE) System.out.format(" Helix parameters at origin energy constrainted = %s\n", helixAtOriginEconstraint.a.toString()); System.out.format(" Magnetic field magnitude = %10.5f and direction = %s\n", Bmag, tB.toString()); MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { @@ -470,10 +475,10 @@ public void print(String s) { if (hitID>=0) { System.out.format(" t=%5.1f ", site.m.hits.get(site.hitID).time); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site); + Pair unBiasedResid = unbiasedResidual(site, false); double [] lclint = moduleIntercept(m, null); - System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, - residual, lclint[0], m.xExtent[1]); + System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, + residual, unBiasedResid.getFirstElement(), lclint[0], m.xExtent[1]); } else { System.out.format("\n"); } @@ -509,16 +514,25 @@ String toString(String s) { double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); str=str+String.format(" Energy unconstrained = %10.6f\n", energy); } - if (energyConstrainedHelix != null) { - double tanL = energyConstrainedHelix.a.v[4]; - double K = energyConstrainedHelix.a.v[2]; + if (propagatedE) { + str=str+String.format("Chi-squared of energy constrained fit = %10.5f\n", chi2_Econstraint); + str=str+helixAtOriginEconstraint.toString("E-constrained helix state for a pivot at the origin")+"\n"; + //str=str+originPoint.toString("point on the helix closest to the origin")+"\n"; + //str=str+String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); + //SquareMatrix C1 = new SquareMatrix(3, Cx); + //str=str+C1.toString("covariance matrix for the point"); + //str=str+originMomentum.toString("momentum of the particle at closest approach to the origin\n"); + //SquareMatrix C2 = new SquareMatrix(3, Cp); + //str=str+C2.toString("covariance matrix for the momentum"); + double tanL = helixAtOriginEconstraint.a.v[4]; + double K = helixAtOriginEconstraint.a.v[2]; double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); - str=str+String.format(" Energy with constraint = %10.6f\n", energy); - str=str+energyConstrainedHelix.toString("helix state with energy constraint"); - } + str=str+String.format(" Energy constrained = %10.6f\n", energy); + } MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { str=str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); + str=str + String.format(" Energy constrained helix at layer %d: %s\n", site0.m.Layer, site0.aES.helix.a.toString()); } for (int i = 0; i < SiteList.size(); i++) { MeasurementSite site = SiteList.get(i); @@ -542,7 +556,7 @@ String toString(String s) { Vec interceptVec = interceptVects().get(site); Vec interceptMomVec = interceptMomVects().get(site); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site); + Pair unBiasedResid = unbiasedResidual(site, false); str=str+String.format(" Intercept=%s, p=%s, measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f+-%9.5f, error=%9.5f \n", interceptVec.toString(), interceptMomVec.toString(), site.m.hits.get(hitID).v, site.aS.mPred, residual, unBiasedResid.getFirstElement(), unBiasedResid.getSecondElement(), FastMath.sqrt(site.aS.R)); } @@ -663,7 +677,7 @@ public boolean originHelix() { if (uniformBatOrigin) { // Just a simple pivot transform is needed if the field is assumed uniform Vec origin = new Vec(0.,0.,0.); Vec newHelixParams = innerSite.aS.helix.pivotTransform(); - DMatrixRMaj newCovariance = innerSite.aS.covariancePivotTransform(newHelixParams); + DMatrixRMaj newCovariance = innerSite.aS.covariancePivotTransform(newHelixParams, 1.0); helixAtOrigin = new HelixState(newHelixParams, origin, origin, newCovariance, Bmag, tB); //innerSite.aS.helix.print("at innerSite"); //helixAtOrigin.print("uniformB"); @@ -1491,12 +1505,85 @@ public boolean fit(boolean keep) { return true; } + /** + * Starting from the most downstream tracker hit, run a filter to include + * the ECAL energy information (roughly a weighted mean with the SVT + * momentum measurement). Then smooth back to the first layer and propagate + * to the origin. + * @param E ECAL energy + * @param sigmaE ECAL energy uncertainty + */ void energyConstraint(double E, double sigmaE) { - if (!propagated) { - originHelix(); - propagated = true; + // The prediction step from the last tracker site to the ECAL has F=1, + // since we don't alter an helix parameters in the prediction. + // The HelixState.energyConstrained method then uses the Kalman weighted + // means formalism for the filter step to update the helix parameters. + // The smoothing gain matrix for the step to the ECAL is simply unity, + // so the smoothed state vector with energy constraint at the last tracker + // site is exactly what is returned by the energyConstrained method. The + // smoothing back to the first tracker site can then proceed as usual. + int idxLast = SiteList.size()-1; + MeasurementSite lastSite = SiteList.get(idxLast); + HelixState energyConstrainedHelix = lastSite.aS.helix.energyConstrained(E, sigmaE); + //System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aS.helix.a.v[2],energyConstrainedHelix.a.v[2]); + lastSite.energyConstrained = true; + lastSite.aES = new StateVector(lastSite.aS.kLow, lastSite.aS.uniformB); + lastSite.aES.helix = energyConstrainedHelix; + lastSite.aES.kUp = lastSite.aS.kUp; + lastSite.aES.F = lastSite.aS.F; // Don't deep copy the F matrix + lastSite.aES.mPred = lastSite.aS.mPred; + lastSite.aES.R = lastSite.aS.R; + lastSite.aES.r = lastSite.aS.r; + lastSite.aES.K = lastSite.aS.K; + lastSite.chi2incE = (lastSite.aES.r * lastSite.aES.r) / lastSite.aES.R; + // Get the residual of the prediction at the ECAL + double kappa = energyConstrainedHelix.a.v[2]; + double tanl = energyConstrainedHelix.a.v[4]; + double ePredict = FastMath.sqrt(1.0+tanl*tanl)/Math.abs(kappa); + double chi = (E - ePredict)/sigmaE; + //System.out.format("KalTrack:energyConstraint E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n",E,ePredict,sigmaE,chi); + this.chi2_Econstraint = lastSite.chi2incE + chi*chi; + MeasurementSite nS = lastSite; + for (int idx=idxLast-1; idx>=0; --idx) { + MeasurementSite thisSite = SiteList.get(idx); + thisSite.aES = thisSite.aF.smooth(nS.aES, nS.aP); + if (thisSite.hitID < 0) { + thisSite.energyConstrained = true; + continue; + } + Measurement hit = thisSite.m.hits.get(thisSite.hitID); + double V = hit.sigma * hit.sigma; + double phiS = thisSite.aES.helix.planeIntersect(thisSite.m.p); + + if (Double.isNaN(phiS)) { // This should almost never happen! + logger.log(Level.FINE, "KalTrack.energyConstraint: no intersection of helix with the plane exists."); + continue; + } + thisSite.aES.mPred = thisSite.h(thisSite.aES, thisSite.m, phiS); + thisSite.aES.r = hit.v - thisSite.aES.mPred; + if (tempV == null) tempV = new DMatrixRMaj(5,1); + CommonOps_DDRM.mult(thisSite.aES.helix.C, thisSite.H, tempV); + thisSite.aES.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); + if (thisSite.aES.R < 0) { + if (debug) System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aES.R); + //aS.print("the smoothed state"); + //nS.print("the next site in the chain"); + thisSite.aES.R = 0.25*V; // A negative covariance makes no sense, hence this fudge + } + + thisSite.chi2incE = (thisSite.aES.r * thisSite.aES.r) / thisSite.aES.R; + this.chi2_Econstraint += thisSite.chi2incE; + thisSite.energyConstrained = true; + nS = thisSite; } - energyConstrainedHelix = helixAtOrigin.energyConstrained(E, sigmaE); + Vec beamSpot = new Vec(3, kPar.beamSpot); + + // This propagated helix will have its pivot at the origin but is in the origin B-field frame + // The StateVector method propagateRungeKutta transforms the origin plane into the origin B-field frame + Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); + arcLengthE = new double[1]; + helixAtOriginEconstraint = SiteList.get(0).aES.helix.propagateRungeKutta(originPlane, yScat, XLscat, SiteList.get(0).m.Bfield, arcLengthE); + propagatedE = true; } /** @@ -1542,12 +1629,16 @@ public int compare(KalTrack t1, KalTrack t2) { if (!t1.SiteList.get(0).aS.helix.goodCov()) penalty1 = 9.9e3; if (!t2.SiteList.get(0).aS.helix.goodCov()) penalty2 = 9.9e3; - Double chi1 = new Double((penalty1*t1.chi2) / t1.nHits + 300.0*(1.0 - (double)t1.nHits/14.)); - Double chi2 = new Double((penalty2*t2.chi2) / t2.nHits + 300.0*(1.0 - (double)t2.nHits/14.)); + Double chi1 = Double.valueOf((penalty1*t1.chi2) / t1.nHits + 300.0*(1.0 - (double)t1.nHits/14.)); + Double chi2 = Double.valueOf((penalty2*t2.chi2) / t2.nHits + 300.0*(1.0 - (double)t2.nHits/14.)); return chi1.compareTo(chi2); } }; + static double sigmaECAL(double E) { + return FastMath.sqrt(2.62 + (8.24 + 6.25*E)*E)/100.; + } + /** * Transform a DMatrixRMaj object into a Kalman SquareMatrix object * @param M DMatrixRMaj object diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 2c154ce45e..23522f2bcb 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -431,7 +431,7 @@ public void clearInterface() { if (!uniformB) { // Transform helix to a pivot point on the helix itself (so rho0 and z0 become zero) Vec helixParamsPivoted = helixState.pivotTransform(newPivot); - helixState.makeF(helixParamsPivoted, F); + helixState.makeF(helixParamsPivoted, F, 1.0); if (debug) { System.out.format("Entering KalmanInterface.toHPShelix"); helixState.print("provided"); @@ -460,7 +460,7 @@ public void clearInterface() { // Pivot transform to the final pivot at the origin finalHelixParams = HelixState.pivotTransform(finalPivot, helixParamsRotated, pivotGlobal, alphaCenter, 0.); - HelixState.makeF(finalHelixParams, F, helixParamsRotated, alphaCenter); + HelixState.makeF(finalHelixParams, F, helixParamsRotated, alphaCenter, 1.0); CommonOps_DDRM.multTransB(covRotated, F, tempM); CommonOps_DDRM.mult(F, tempM, covRotated); if (debug) { @@ -477,7 +477,7 @@ public void clearInterface() { } else { // For a uniform field, all we have to do is a pivot transform to the origin pivotGlobal = newPivot; // Intersection with the provided plane finalHelixParams = helixState.pivotTransform(finalPivot); - helixState.makeF(finalHelixParams, F); + helixState.makeF(finalHelixParams, F, 1.0); CommonOps_DDRM.multTransB(helixState.C, F, tempM); CommonOps_DDRM.mult(F, tempM, covRotated); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index 8ee2cde58a..86fba49536 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -39,6 +39,7 @@ public class KalmanParams { double edgeTolerance; static final int numLayers = 14; boolean uniformB; + boolean eLoss; private int[] Swap = {1,0, 3,2, 5,4, 7,6, 9,8, 11,10, 13,12}; private String [] tb; @@ -83,6 +84,7 @@ public void print() { System.out.format(" Maximum time difference among the hits on a track: %8.2f ns\n", mxTdif); System.out.format(" Threshold to remove redundant seeds (-1 to disable): %8.2f\n", seedCompThr); System.out.format(" Maximum chi^2 for 5-hit tracks with a vertex constraint: %8.2f\n", mxChi2Vtx); + System.out.format(" Include ionization energy loss in fit = %b\n", eLoss); System.out.format(" Default origin to use for vertex constraints:\n"); for (int i=0; i<3; ++i) { System.out.format(" %d: %8.3f +- %8.3f\n", i, beamSpot[i], vtxSize[i]); @@ -131,6 +133,7 @@ public KalmanParams() { // The index is the iteration number. // The second iteration generally will have looser cuts. uniformB = false; + eLoss = false; nTrials = 2; // Number of global iterations of the pattern recognition nIterations = 1; // Number of Kalman filter iterations per track in the final fit edgeTolerance = 1.; // Tolerance on checking if a track is within the detector bounds @@ -227,6 +230,11 @@ public void setUniformB(boolean input) { uniformB = input; } + public void setEloss(boolean eLoss) { + logger.config(String.format("Setting the energy loss to %b", eLoss)); + this.eLoss = eLoss; + } + public void setLowPhThreshold(double cut) { if (cut <0. || cut > 1.) { logger.warning(String.format("low pulse-height threshold %10.4f is not valid and is ignored.", cut)); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java index d197795868..bc8cc6a37f 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java @@ -44,6 +44,7 @@ public class KalmanPatRecDriver extends Driver { private org.lcsim.geometry.FieldMap fm; private KalmanInterface KI; private boolean verbose = false; + private boolean eLoss = false; private boolean uniformB = false; // If true, fit tracks assuming a uniform B field private boolean addResiduals = false; // If true add the hit-on-track residuals to the LCIO event private String outputFullTrackCollectionName = "KalmanFullTracks"; @@ -128,9 +129,14 @@ public void setVerbose(boolean verbose) { */ public void setUniformB(boolean uniformB) { this.uniformB = uniformB; - System.out.format("KalmanPatRecDriver: a uniform field will be used in track fitting: %b\n", uniformB); + if (uniformB) System.out.println("KalmanPatRecDriver: a uniform field will be used in track fitting: %b"); } + public void setELoss(boolean eLoss) { + this.eLoss = eLoss; + if (eLoss) System.out.println("KalmanPatRecDriver: energy loss will be included in fits."); + } + public void setMaterialManager(MaterialSupervisor mm) { _materialManager = mm; } @@ -248,6 +254,7 @@ public void detectorChanged(Detector det) { if (mxChi2Vtx != 0.0) kPar.setMaxChi2Vtx(mxChi2Vtx); if (minSeedEnergy >= 0.) kPar.setMinSeedEnergy(minSeedEnergy); kPar.setUniformB(uniformB); + kPar.setEloss(eLoss); // Here we set the seed strategies for the pattern recognition if (strategies != null || (strategiesTop != null && strategiesBot != null)) { @@ -491,7 +498,7 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List sigmas = new ArrayList(); for (int ilay = 0; ilay<14; ilay++) { - Pair res_and_sigma = kTk.unbiasedResidual(ilay); + Pair res_and_sigma = kTk.unbiasedResidual(ilay, false); if (res_and_sigma.getSecondElement() > -1.) { layers.add(ilay); residuals.add(res_and_sigma.getFirstElement()); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java index 08a8d8aafb..9a81930328 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java @@ -577,7 +577,7 @@ void process(EventHeader event, List sensors, ArrayList[] aida.histogram1D(String.format("Layers/Kalman kink in xy, layer %d", mod.Layer)).fill(kTk.scatX(mod.Layer)); aida.histogram1D(String.format("Layers/Kalman kink in zy, layer %d", mod.Layer)).fill(kTk.scatZ(mod.Layer)); } - Pair residPr = kTk.unbiasedResidual(site.m.Layer); + Pair residPr = kTk.unbiasedResidual(site.m.Layer, false); if (residPr.getSecondElement() > -999. && kTk.nHits >= 10) { double variance = residPr.getSecondElement(); if (variance <= 0.) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java index 85e49346ee..7e1187b7ec 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java @@ -21,7 +21,10 @@ class MeasurementSite { boolean filtered; // True if the filtered state vector has been built StateVector aS; // Smoothed state vector boolean smoothed; // True if the smoothed state vector has been built + StateVector aES; // Energy-constrained smoothed state vector + boolean energyConstrained; // True if the energy constrained smoothed state vector has been built double chi2inc; // chi^2 increment for this site + double chi2incE; // chi^2 increment for the energy-constrainted track DMatrixRMaj H; // Derivatives of the transformation from state vector to measurement double arcLength; // Arc length from the previous measurement private double conFac; // Conversion from B to alpha @@ -61,6 +64,7 @@ String toString(String s) { } else if (predicted) { str=str+" This site has been predicted\n"; } + if (energyConstrained) str=str+" This site has been energy constrained\n"; str=str+String.format(" Hit ID=%d, maximum allowed residual=%10.5f\n", hitID, kPar.mxResid[1]); str = str + m.toString("for this site"); Vec Bfield = null; @@ -73,11 +77,12 @@ String toString(String s) { Vec tB = Bfield.unitVec(); str=str+String.format(" Magnetic field strength=%10.6f; alpha=%10.6f\n", B, alpha); str = str + tB.toString("magnetic field direction") + "\n"; - str=str+String.format(" chi^2 increment=%12.4e\n", chi2inc); + str=str+String.format(" chi^2 increment=%12.4e; E constrained=%12.4E\n", chi2inc, chi2incE); str=str+String.format(" x scattering angle=%10.8f, y scattering angle=%10.8f\n", scatX(), scatZ()); if (predicted) str = str + aP.toString("predicted"); if (filtered) str = str + aF.toString("filtered"); if (smoothed) str = str + aS.toString("smoothed"); + if (energyConstrained) str = str + aES.toString("energy-constrained"); if (H != null) str = str + "matrix of the transformation from state vector to measurement:" + H.toString(); str=str+String.format(" Assumed electron dE/dx in GeV/mm = %10.6f; Detector thickness=%10.6f\n", dEdx, m.thickness); if (m.Layer < 0) { @@ -112,11 +117,13 @@ String toString(String s) { predicted = false; filtered = false; smoothed = false; + energyConstrained = false; double rho = 2.329; // Density of silicon in g/cm^3 radLen = (21.82 / rho) * 10.0; // Radiation length of silicon in millimeters double sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g dEdx = -0.1 * sp * rho; // in GeV/mm chi2inc = 0.; + chi2incE = 0.; H = new DMatrixRMaj(5,1); if (!initialized) { tempV = new DMatrixRMaj(5,1); @@ -195,12 +202,6 @@ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingO * @return Flag: -2 for extrapolation not within detector, -1 for error, 1 for a hit was used, 0 no hit used */ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingOK, boolean pickup, boolean checkBounds, double [] tRange, int trial, boolean verbose2) { - // pS = - // mPS = - // tRange = - // minE = minimum hit energy for it to be picked up, unless no other hit exists - // sharingOK = - // pickup = int returnFlag = 0; double phi = pS.helix.planeIntersect(m.p); if (debug) verbose2 = true; @@ -224,8 +225,6 @@ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingO check); } - double deltaE = 0.; // dEdx*thickness/ct; - Vec origin = m.p.X(); Vec Bfield = null; if (kPar.uniformB) { @@ -244,13 +243,16 @@ int makePrediction(StateVector pS, SiModule mPs, int hitNumber, boolean sharingO // First we need the momentum direction to calculate how much silicon we pass through Vec pMom = pS.helix.Rot.inverseRotate(pS.helix.getMom(0.)); double XL; + double deltaE = 0.; if (mPs == null) { XL = 0.; arcLength = 0.; } else { double ct = pMom.unitVec().dot(mPs.p.T()); // cos(theta) at the **previous** site + double dL = mPs.thickness/FastMath.abs(ct); + if (kPar.eLoss) deltaE = dEdx*dL; double radius = alpha/pS.helix.a.v[2]; - XL = mPs.thickness / radLen / Math.abs(ct); // Si scattering thickness at previous site + XL = dL / radLen; // Si scattering thickness at previous site arcLength = -radius*phi*FastMath.sqrt(1.0 + pS.helix.a.v[4] * pS.helix.a.v[4]); if (debug) { double dx = m.p.X().v[0]-mPs.p.X().v[0]; @@ -557,6 +559,7 @@ boolean removeHit() { if (hitID < 0) return false; hitID = -1; chi2inc = 0.; + chi2incE = 0.; smoothed = false; filtered = false; return true; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java index f5d5ba1633..2e408223d4 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java @@ -273,7 +273,7 @@ class PatRecTest { for (int i = 0; i < nHelices; i++) { Vec momentum = new Vec(p[i] * initialDirection[i].v[0], p[i] * initialDirection[i].v[1], p[i] * initialDirection[i].v[2]); if (verbose) momentum.print("initial helix momentum"); - TkInitial[i] = new Helix(Q[i], helixOrigin, momentum, helixOrigin, fM, rnd); + TkInitial[i] = new Helix(Q[i], helixOrigin, momentum, helixOrigin, fM, rnd, kPar.eLoss); drho[i] = TkInitial[i].p.v[0]; phi0[i] = TkInitial[i].p.v[1]; K[i] = TkInitial[i].p.v[2]; @@ -302,7 +302,7 @@ class PatRecTest { Vec p1 = new Vec(3); HelixPlaneIntersect hpi1 = new HelixPlaneIntersect(); Vec pivotBegin = hpi1.rkIntersect(si1.p, TkInitial[i].atPhiGlobal(0.), TkInitial[i].getMomGlobal(0.), Q[i], fM, p1); - helixBegin[i] = new Helix(Q[i], pivotBegin, p1, pivotBegin, fM, rnd); + helixBegin[i] = new Helix(Q[i], pivotBegin, p1, pivotBegin, fM, rnd, kPar.eLoss); if (verbose) helixBegin[i].print("helixBegin"); } PrintWriter printWriter2 = null; @@ -642,7 +642,7 @@ class PatRecTest { } } for (int layer=2; layer < nLayers; ++layer) { - Pair resid = tkr.unbiasedResidual(layer); + Pair resid = tkr.unbiasedResidual(layer, false); if (resid.getSecondElement() > -999.) { double unbResid = resid.getFirstElement(); double variance = resid.getSecondElement(); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java index abf0efd51d..3b3221657f 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/PropagatedTrackState.java @@ -120,7 +120,7 @@ public class PropagatedTrackState { pMom.print(String.format("initial momentum from pivoted helix, p=%10.6f",pMom.mag())); } DMatrixRMaj F = new DMatrixRMaj(5,5); - HelixState.makeF(helixParamsPivoted, F, helixParams, alpha); + HelixState.makeF(helixParamsPivoted, F, helixParams, alpha, 1.0); // Then rotate the helix into the B-field reference frame DMatrixRMaj fRot = new DMatrixRMaj(5,5); @@ -242,7 +242,7 @@ public TrackState getTrackState() { public double [][] getIntersectionCov() { Vec helixAtInt = getIntersectionHelix(); DMatrixRMaj F = new DMatrixRMaj(5,5); - newHelixState.makeF(helixAtInt, F); + newHelixState.makeF(helixAtInt, F, 1.0); CommonOps_DDRM.multTransB(newHelixState.C, F, tempM); DMatrixRMaj covAtInt = new DMatrixRMaj(5,5); CommonOps_DDRM.mult(F, tempM, covAtInt); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java index b544ba4628..7e1d1b2ac9 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java @@ -19,13 +19,15 @@ class RKhelix { private HelixPlaneIntersect hpi; private double rho; private double radLen; + private boolean doeLoss; - RKhelix(Vec x, Vec p, double Q, org.lcsim.geometry.FieldMap fM, Random rndm) { + RKhelix(Vec x, Vec p, double Q, org.lcsim.geometry.FieldMap fM, Random rndm, boolean doeLoss) { this.rndm = rndm; this.fM = fM; this.x = x.copy(); this.p = p.copy(); this.Q = Q; + this.doeLoss = doeLoss; hpi = new HelixPlaneIntersect(); rho = 2.329; // Density of silicon in g/cm^2 radLen = (21.82 / rho) * 10.0; // Radiation length of silicon in millimeters @@ -34,7 +36,7 @@ class RKhelix { RKhelix propagateRK(Plane pln) { Vec newP = new Vec(3); Vec newX = planeIntersect(pln, newP); - return new RKhelix(newX, newP, Q, fM, rndm); + return new RKhelix(newX, newP, Q, fM, rndm, doeLoss); } Vec planeIntersect(Plane pln, Vec pInt) { // phi value where the helix intersects the plane P (given in global coordinates) @@ -65,7 +67,7 @@ Vec helixParameters(Vec pivot, Vec pivotF) { } RKhelix copy() { - return new RKhelix(x.copy(),p.copy(),Q,fM,rndm); + return new RKhelix(x.copy(),p.copy(),Q,fM,rndm, doeLoss); } RotMatrix R(Vec position) { @@ -98,13 +100,17 @@ RKhelix randomScat(Plane P, double X) { // Produce a new helix scattered randoml Vec tnew = Rp.inverseRotate(tLoc); double E = p.mag(); // Everything is assumed electron - double sp = 0.0; // 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g + double sp = 0.0; + if (doeLoss) { + sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g + sp = sp*20; // ToDo temporary!!! + } double dEdx = 0.1 * sp * rho; // in GeV/mm double eLoss = dEdx * X / ct; E = E - eLoss; Vec pNew = tnew.scale(E); - return new RKhelix(x, pNew, Q, fM, rndm); // Create the new helix with new origin and pivot point + return new RKhelix(x, pNew, Q, fM, rndm, doeLoss); // Create the new helix with new origin and pivot point } } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java index 2464d4a9da..7a13d8eebf 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/StateVector.java @@ -6,6 +6,7 @@ import org.ejml.dense.row.CommonOps_DDRM; import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; import org.ejml.interfaces.linsol.LinearSolverDense; +import org.apache.commons.math.util.FastMath; /** * Helix state vector (projected, filtered, or smoothed) for the Kalman filter @@ -21,7 +22,7 @@ class StateVector { final static private boolean debug = false; DMatrixRMaj F; // Propagator matrix to propagate from this site to the next site private static Logger logger; - private DMatrixRMaj K; // Kalman gain matrix + DMatrixRMaj K; // Kalman gain matrix // Working arrays for efficiency, to avoid creating temporary working space over and over private static DMatrixRMaj tempV; @@ -33,7 +34,7 @@ class StateVector { private static DMatrixRMaj U; // Unit matrix private static LinearSolverDense solver; private static boolean initialized; - private boolean uniformB; + boolean uniformB; /** * Constructor for the initial state vector used to start the Kalman filter. @@ -143,7 +144,7 @@ String toString(String s) { if (F != null) str = str + "Propagator matrix: " + F.toString(); double sigmas; if (R > 0.) { - sigmas = r / Math.sqrt(R); + sigmas = r / FastMath.sqrt(R); } else { sigmas = 0.; } @@ -171,8 +172,8 @@ StateVector predict(int newSite, Vec pivot, double B, Vec t, Vec originPrime, do aPrime.kUp = kUp; aPrime.helix.X0 = pivot; // pivot before helix rotation, in coordinate system of the previous site - double E = helix.a.v[2] * Math.sqrt(1.0 + helix.a.v[4] * helix.a.v[4]); - double deltaEoE = deltaE / E; + double momentum = FastMath.sqrt(1.0 + helix.a.v[4] * helix.a.v[4])/FastMath.abs(helix.a.v[2]); + double deltaEoE = deltaE / momentum; // Transform helix in old coordinate system to new pivot point lying on the next detector plane if (deltaE == 0.) { @@ -188,12 +189,9 @@ StateVector predict(int newSite, Vec pivot, double B, Vec t, Vec originPrime, do helix.X0.print("old pivot"); } + double eFactor = 1.0 - deltaEoE; F = new DMatrixRMaj(5,5); - this.helix.makeF(aPrime.helix.a, F); // Calculate derivatives of the pivot transform - if (deltaE != 0.) { - double factor = 1.0 - deltaEoE; - for (int i = 0; i < 5; i++) F.unsafe_set(i, 2, F.unsafe_get(i,2)*factor); - } + this.helix.makeF(aPrime.helix.a, F, eFactor); // Calculate derivatives of the pivot transform // Transform to the coordinate system of the field at the new site // First, transform the pivot point to the new system @@ -257,7 +255,6 @@ StateVector predict(int newSite, Vec pivot, double B, Vec t, Vec originPrime, do Cinv.set(this.helix.C); if (debug) System.out.format("StateVector.predict: XL=%9.6f\n", XL); } else { - double momentum = (1.0 / helix.a.v[2]) * Math.sqrt(1.0 + helix.a.v[4] * helix.a.v[4]); double sigmaMS = HelixState.projMSangle(momentum, XL); if (debug) System.out.format("StateVector.predict: momentum=%12.5e, XL=%9.6f sigmaMS=%12.5e\n", momentum, XL, sigmaMS); this.helix.getQ(sigmaMS, Q); @@ -388,9 +385,9 @@ Vec inverseFilter(DMatrixRMaj H, double V, DMatrixRMaj Cnew) { /** * Create a smoothed state vector from the filtered state vector - * @param snS - * @param snP - * @return Smoothed state vector + * @param snS Smoothed state vector of the next layer out + * @param snP Predicted state vector of the next layer out + * @return Smoothed state vector corresponding to this layer's filtered state vector */ StateVector smooth(StateVector snS, StateVector snP) { if (debug) System.out.format("StateVector.smooth of filtered state %d %d, using smoothed state %d %d and predicted state %d %d\n", kLow, kUp, @@ -475,9 +472,9 @@ StateVector smooth(StateVector snS, StateVector snP) { */ Vec helixErrors(Vec aPrime) { - DMatrixRMaj tC = covariancePivotTransform(aPrime); - return new Vec(Math.sqrt(tC.unsafe_get(0,0)), Math.sqrt(tC.unsafe_get(1,1)), Math.sqrt(tC.unsafe_get(2,2)), - Math.sqrt(tC.unsafe_get(3,3)), Math.sqrt(tC.unsafe_get(4,4))); + DMatrixRMaj tC = covariancePivotTransform(aPrime, 1.0); + return new Vec(FastMath.sqrt(tC.unsafe_get(0,0)), FastMath.sqrt(tC.unsafe_get(1,1)), FastMath.sqrt(tC.unsafe_get(2,2)), + FastMath.sqrt(tC.unsafe_get(3,3)), FastMath.sqrt(tC.unsafe_get(4,4))); } /** @@ -485,12 +482,12 @@ Vec helixErrors(Vec aPrime) { * @param aP 5-vector of helix parameters * @return transformed helix parameters */ - DMatrixRMaj covariancePivotTransform(Vec aP) { + DMatrixRMaj covariancePivotTransform(Vec aP, double eFactor) { // aP are the helix parameters for the new pivot point, assumed already to be // calculated by pivotTransform() // Note that no field rotation is assumed or accounted for here DMatrixRMaj mF = new DMatrixRMaj(5,5); - helix.makeF(aP, mF); + helix.makeF(aP, mF, eFactor); CommonOps_DDRM.multTransB(helix.C, mF, tempM); CommonOps_DDRM.mult(mF, tempM, tempA); return tempA; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/TestMain.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/TestMain.java index 962ba72da9..44e8680dd2 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/TestMain.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/TestMain.java @@ -12,7 +12,7 @@ public class TestMain { public static void main(String[] args) { - String defaultPath = "C:\\Users\\Robert\\Desktop\\Kalman\\"; + String defaultPath = "C:\\Users\\rjohn\\Desktop\\Kalman\\"; String path; // Path to where the output histograms should be written if (args.length == 0) { path = defaultPath; From cf29750f36d99fcccb677a9962cc6c6a70ce6031 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Tue, 3 Jan 2023 15:28:34 -0800 Subject: [PATCH 06/17] added hpsHelixIntersect method --- .../tracking/kalman/KalmanInterface.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 23522f2bcb..85e1eb52fc 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -402,6 +402,54 @@ public void clearInterface() { } } + /** + * Find the intersection of a track with a detector plane. All of the input and output are assumed to + * be in HPS coordinates (not Kalman coordinates). Memory must be provided for xLoc and pAtPlane, the + * charge must be -1 or +1, and the axis unit vectors uGlb, vGlb, tGlb must be normalized and right-handed. + * @param x 3-vector coordinate on the track + * @param p 3-vector momentum at the point x + * @param Q particle charge (-1.0 or +1.0) + * @param pointOnPlane 3-vector point on the detector plane + * @param uGlb 3-vector direction cosines of the u-axis of the detector plane (perp to strips) + * @param vGlb 3-vector direction cosines of the v-axis of the detector plane (parallel to strips) + * @param tGlb 3-vector direction cosines of the t-axis of the detector plane (perp to plane) + * @param fM HPS field map + * @param xLocal return 3-vector intersection in detector coordinates (u, v, t) + * @param pAtPlane return 3-vector momentum at the intersection, in global coordinates + * @return 3-vector point of intersection on the detector plane in global coordinates + */ + static double [] hpsHelixIntersect(double [] x, double [] p, double Q, double [] pointOnPlane, double [] uGlb, double [] vGlb, double [] tGlb, + org.lcsim.geometry.FieldMap fM, double [] xLocal, double [] pAtPlane) { + + // Transform the input information into Kalman coordinates + Vec uK = (vectorGlbToKalman(vGlb).scale(-1.0)); // u and v are reversed in hps compared to kalman + Vec vK = vectorGlbToKalman(uGlb); + Vec tK = vectorGlbToKalman(tGlb); + Vec pointOnPlaneTransformed = vectorGlbToKalman(pointOnPlane); + Vec xK = vectorGlbToKalman(x); + Vec pK = vectorGlbToKalman(p); + + // Define the detector plane (in Kalman coordinates) + Plane detPln = new Plane(pointOnPlaneTransformed, tK, uK, vK); + + // Integrate to find the intersection with the detector plane + HelixPlaneIntersect hpi = new HelixPlaneIntersect(); + Vec pInt = new Vec(3); + Vec intercept = hpi.rkIntersect(detPln, xK, pK, Q, fM, pInt); + + // Convert the results back to HPS coordinates + double [] interceptPos = vectorKalmanToGlb(intercept); + pAtPlane = vectorKalmanToGlb(pInt); + + // Transform the intersection to local coordinates in the Kalman sensor system + // and transform the result to HPS local coordinates. + RotMatrix R = new RotMatrix(detPln.U(), detPln.V(), detPln.T()); + Vec xLocK = R.rotate(intercept.dif(detPln.X())); + xLocal = localKalToHps(xLocK); + + return interceptPos; + } + /** * Transform a Kalman helix to an HPS helix rotated to the global frame and with the pivot at the origin * The calling routine must provide empty covHPS[15] and position[3] arrays. From 6df865960f82a9924ff48fc030775d59cf10e937 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Tue, 3 Jan 2023 15:39:29 -0800 Subject: [PATCH 07/17] added public declaration to the new method hpsHelixIntersect --- .../java/org/hps/recon/tracking/kalman/KalmanInterface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 85e1eb52fc..2bbd52f0f3 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -418,7 +418,7 @@ public void clearInterface() { * @param pAtPlane return 3-vector momentum at the intersection, in global coordinates * @return 3-vector point of intersection on the detector plane in global coordinates */ - static double [] hpsHelixIntersect(double [] x, double [] p, double Q, double [] pointOnPlane, double [] uGlb, double [] vGlb, double [] tGlb, + public static double [] hpsHelixIntersect(double [] x, double [] p, double Q, double [] pointOnPlane, double [] uGlb, double [] vGlb, double [] tGlb, org.lcsim.geometry.FieldMap fM, double [] xLocal, double [] pAtPlane) { // Transform the input information into Kalman coordinates From 30a8fbf8fa15fea2f8a48ce7f3f75ca7dd778739 Mon Sep 17 00:00:00 2001 From: Graf Date: Tue, 17 Jan 2023 16:40:35 -0800 Subject: [PATCH 08/17] Fixed style violations --- .../org/hps/recon/tracking/kalman/Helix.java | 29 +- .../hps/recon/tracking/kalman/HelixTest3.java | 402 +++--- .../hps/recon/tracking/kalman/KalTrack.java | 1212 ++++++++++------- .../recon/tracking/kalman/KalmanParams.java | 333 ++--- .../tracking/kalman/KalmanPatRecDriver.java | 426 +++--- .../hps/recon/tracking/kalman/RKhelix.java | 36 +- 6 files changed, 1455 insertions(+), 983 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java index 7b290b84fa..515f315a44 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java @@ -1,11 +1,14 @@ package org.hps.recon.tracking.kalman; import java.util.Random; + /** - * This is for stand-alone testing only and is not part of the Kalman fitting code. - * Create a simple helix oriented along the B field axis for testing the Kalman fit. + * This is for stand-alone testing only and is not part of the Kalman fitting + * code. Create a simple helix oriented along the B field axis for testing the + * Kalman fit. */ -class Helix { +class Helix { + Vec p; // Helix parameters drho, phi0, K, dz, tanl Vec X0; // Pivot point in the B field reference frame private double alpha; @@ -27,7 +30,7 @@ class Helix { this.Q = Q; this.origin = origin.copy(); this.fM = fM; - Vec Bf = new Vec(3,fM.getField(Xinit)); + Vec Bf = new Vec(3, fM.getField(Xinit)); B = Bf.mag(); double c = 2.99793e8; alpha = 1000.0 * 1.0E9 / (c * B); // Units are Tesla, mm, GeV @@ -57,7 +60,7 @@ class Helix { this.origin = origin.copy(); this.fM = fM; p = HelixParams.copy(); - Vec Bf = new Vec(3,fM.getField(pivotGlobal)); + Vec Bf = new Vec(3, fM.getField(pivotGlobal)); B = Bf.mag(); double c = 2.99793e8; alpha = 1000.0 * 1.0E9 / (c * B); // Units are Tesla, mm, GeV @@ -98,7 +101,7 @@ void print(String s) { System.out.format(" Pivot in B-field frame=%10.5f, %10.5f, %10.5f\n", X0.v[0], X0.v[1], X0.v[2]); Vec pivotGlobal = R.inverseRotate(X0).sum(origin); System.out.format(" Pivot in global frame=%10.5f, %10.5f, %10.5f\n", pivotGlobal.v[0], pivotGlobal.v[1], pivotGlobal.v[2]); - Vec Bf = new Vec(3,fM.getField(pivotGlobal)); + Vec Bf = new Vec(3, fM.getField(pivotGlobal)); Bf.print("B field in global frame at the pivot point"); Vec Bflocal = R.rotate(Bf); Bflocal.print("B field in its local frame; should be in +z direction"); @@ -142,7 +145,7 @@ Vec atPhiGlobal(double phi) { // return the global coordinates on the helix at a } double planeIntersect(Plane Pin) { // phi value where the helix intersects the plane P (given in global - // coordinates) + // coordinates) Plane P = Pin.toLocal(R, origin); double phi = hpi.planeIntersect(p, X0, alpha, P); // System.out.format("Helix:planeIntersect: phi = %12.10f\n", phi); @@ -172,7 +175,6 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc // Vec r = this.atPhiGlobal(phi); //double tst = r.dif(P.X()).dot(P.T()); // System.out.format("randomScat: test dot product %12.6e should be zero\n", tst); - // r.print("randomScat: r global"); // Vec pmom = getMomGlobal(phi); // pmom.print("randomScat: p global"); @@ -187,8 +189,11 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc double ct = Math.abs(P.T().dot(t)); double theta0; // Get the scattering angle - if (X == 0.) theta0 = 0.; - else theta0 = Math.sqrt((X / radLen) / ct) * (0.0136 / pmom.mag()) * (1.0 + 0.038 * Math.log((X / radLen) / ct)); + if (X == 0.) { + theta0 = 0.; + } else { + theta0 = Math.sqrt((X / radLen) / ct) * (0.0136 / pmom.mag()) * (1.0 + 0.038 * Math.log((X / radLen) / ct)); + } double thetaX = rndm.nextGaussian() * theta0; double thetaY = rndm.nextGaussian() * theta0; // System.out.format("Helix.randomScat: X=%12.5e, ct=%12.5e, theta0=%12.5e, thetaX=%12.5e, @@ -205,7 +210,7 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc // System.out.format("recalculated scattered angle=%10.7f\n", check); // Rotate the direction into the frame of the new field (evaluated at the new pivot) - Vec Bf = new Vec(3,fM.getField(r)); + Vec Bf = new Vec(3, fM.getField(r)); double Bnew = Bf.mag(); Vec tBnew = Bf.unitVec(Bnew); Vec yhat = new Vec(0., 1., 0.); @@ -217,7 +222,7 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc double E = pmom.mag(); // Everything is assumed electron double sp = 0.0; // 0.002; // Estar collision stopping power for electrons in silicon at about a - // GeV, in GeV cm2/g + // GeV, in GeV cm2/g double dEdx = 0.1 * sp * rho; // in GeV/mm double eLoss = dEdx * X / ct; // System.out.format("randomScat: energy=%10.7f, energy loss=%10.7f\n", E, eLoss); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index c8d770f6b0..bb172cb7f9 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -22,7 +22,8 @@ import org.lcsim.event.TrackState; /** - * This is for stand-alone testing of the Kalman fit only and is not part of the HPS Kalman fitting code package + * This is for stand-alone testing of the Kalman fit only and is not part of the + * HPS Kalman fitting code package */ class HelixTest3 { // Program for testing the Kalman fitting code @@ -30,14 +31,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code // z is the B field direction, downward in lab coordinates // y is the beam direction // x is y cross z - Random rnd; HelixTest3(String path) { // Control parameters // Units are Tesla, GeV, mm - int nTrials = 10000; // The number of test events to generate for fitting int startLayer = 10; // Where to start the Kalman filtering int nIteration = 2; // Number of filter iterations @@ -45,22 +44,22 @@ class HelixTest3 { // Program for testing the Kalman fitting code int nStereo = 4; // Number of stereo layers needed by the linear fit boolean cheat = false; // true to use the true helix parameters (smeared) for the starting guess boolean perfect = false; - double [] vtxRes = {0.1, 0.5, 0.05}; + double[] vtxRes = {0.1, 0.5, 0.05}; boolean verbose = nTrials < 2; double executionTime = 0.; int nBadCov = 0; - + double eCalLoc = 1394.; - + DMatrixRMaj testCov = null; Vec testHelix = null; - DMatrixRMaj tempM1 = new DMatrixRMaj(5,5); - DMatrixRMaj tempM2 = new DMatrixRMaj(5,5); - DMatrixRMaj fRot = new DMatrixRMaj(5,5); - for (int i=0; i<5; ++i) { - for (int j=0; j<5; ++j) { - if (i==j) { + DMatrixRMaj tempM1 = new DMatrixRMaj(5, 5); + DMatrixRMaj tempM2 = new DMatrixRMaj(5, 5); + DMatrixRMaj fRot = new DMatrixRMaj(5, 5); + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + if (i == j) { fRot.unsafe_set(i, j, 1.); } else { fRot.unsafe_set(i, j, 0); @@ -69,14 +68,16 @@ class HelixTest3 { // Program for testing the Kalman fitting code } KalmanParams kPar = new KalmanParams(); kPar.print(); - + // Seed the random number generator long rndSeed = -3263009337738135404L; rnd = new Random(); rnd.setSeed(rndSeed); - + Histogram hGaus = new Histogram(100, -4., 0.08, "Normal Distribution 1", "x", "y"); - for (int i = 0; i < 1000000; i++) { hGaus.entry(rnd.nextGaussian()); } + for (int i = 0; i < 1000000; i++) { + hGaus.entry(rnd.nextGaussian()); + } // Read in the magnetic field map String mapType = "binary"; @@ -96,32 +97,33 @@ class HelixTest3 { // Program for testing the Kalman fitting code fM.writeBinaryFile("C:\\Users\\Robert\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"); } System.out.format("B field map vs y:\n"); - for (double y=0.; y<1500.; y+=5.) { - double z1=-50.; - double z2= 50.; + for (double y = 0.; y < 1500.; y += 5.) { + double z1 = -50.; + double z2 = 50.; Vec B1 = new Vec(3, fM.getField(new Vec(0., y, z1))); Vec B2 = new Vec(3, fM.getField(new Vec(0., y, 0.))); Vec B3 = new Vec(3, fM.getField(new Vec(0., y, z2))); System.out.format("y=%6.1f z=%6.1f: %s z=0: %s z=%6.1f: %s\n", y, z1, B1.toString(), B2.toString(), z2, B3.toString()); } System.out.format("B field map vs z at ECAL:\n"); - for (double z=-200.; z<200.; z+=5.) { - double y=eCalLoc + 10.; + for (double z = -200.; z < 200.; z += 5.) { + double y = eCalLoc + 10.; Vec B = new Vec(3, fM.getField(new Vec(0., y, z))); System.out.format("x=0 y=%6.1f z=%6.1f: %s\n", y, z, B.toString()); } System.out.format("B field map vs x at center:\n"); - for (double x=-300.; x<300.; x+=5.) { - double y=505.; - double z=20.; + for (double x = -300.; x < 300.; x += 5.) { + double y = 505.; + double z = 20.; Vec B = new Vec(3, fM.getField(new Vec(x, y, z))); System.out.format("x=%6.1f y=%6.1f z=%6.1f: %s\n", x, y, z, B.toString()); } // Tracking instrument description - double thickness = 0.3; // Silicon thickness in mm - if (perfect) { thickness = 0.0000000000001; } + if (perfect) { + thickness = 0.0000000000001; + } ArrayList SiModules = new ArrayList(); Plane plnInt; SiModule newModule; @@ -200,7 +202,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code plnInt = new Plane(new Vec(0., 870.0, 30.0), new Vec(0., 1.0, 0.)); newModule = new SiModule(-5, plnInt, false, 0., 900., 900., 0., fM, 0); SiModules.add(newModule); - */ + */ plnInt = new Plane(new Vec(-22.879, 905.35, 35.309), new Vec(-0.029214, -0.99957, 0.0019280), -0.049801); newModule = new SiModule(11, plnInt, true, 100., 40.34, false, thickness, fM, 0); SiModules.add(newModule); @@ -227,12 +229,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code for (SiModule mod : SiModules) { if (mod.Layer == layer) { yScat.add(mod.p.X().v[1]); - XLscat.add(mod.thickness/radLen); + XLscat.add(mod.thickness / radLen); break; } } } - + double[] location = new double[nLayers]; double[] xdet = new double[SiModules.size()]; double[] ydet = new double[SiModules.size()]; @@ -240,12 +242,14 @@ class HelixTest3 { // Program for testing the Kalman fitting code int[] lyr = new int[SiModules.size()]; for (int i = 0; i < SiModules.size(); i++) { SiModule si = SiModules.get(i); - - si.p.T().v[0]=0.; - si.p.T().v[1]=1. * Math.signum(si.p.T().v[1]); // !!!!!!!!!!!!!!!!!!!!! All modules aligned - si.p.T().v[2]=0.; - - if (si.Layer >= 0) { location[si.Layer] = si.p.X().v[1]; } + + si.p.T().v[0] = 0.; + si.p.T().v[1] = 1. * Math.signum(si.p.T().v[1]); // !!!!!!!!!!!!!!!!!!!!! All modules aligned + si.p.T().v[2] = 0.; + + if (si.Layer >= 0) { + location[si.Layer] = si.p.X().v[1]; + } lyr[i] = si.Layer; xdet[i] = si.p.X().v[0]; ydet[i] = si.p.X().v[1]; @@ -261,12 +265,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code double hitEfficiency = 1.0; Vec helixOrigin = new Vec(0., 0., 0.); // Pivot point of initial helix - Vec Bpivot = new Vec(3,fM.getField(helixOrigin)); + Vec Bpivot = new Vec(3, fM.getField(helixOrigin)); Bpivot.print("magnetic field at the initial origin"); for (int pln = 0; pln < SiModules.size(); pln++) { - Vec bf = new Vec(3,fM.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); + Vec bf = new Vec(3, fM.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); System.out.format("Kalman fitting B field at module %d = %10.7f, %10.7f, %10.7f\n", pln, bf.v[0], bf.v[1], bf.v[2]); - bf = new Vec(3,fMg.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); + bf = new Vec(3, fMg.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); System.out.format("MC generator B field at module %d = %10.7f, %10.7f, %10.7f\n", pln, bf.v[0], bf.v[1], bf.v[2]); } double Phi = 91.5 * Math.PI / 180.; @@ -349,7 +353,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code printWriter.format("splot "); printWriter.format("$runga u 1:2:3 with lines lw 3, $helix u 1:2:3 with lines lw 3\n"); printWriter.close(); - + Vec zhat = null; Vec uhat = null; Vec vhat = null; @@ -390,13 +394,13 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEkO = new Histogram(100, -10., 0.2, "Origin helix parameter K error", "sigmas", "track"); Histogram hEdzO = new Histogram(100, -10., 0.2, "Origin helix parameter dz error", "sigmas", "track"); Histogram hEtanlO = new Histogram(100, -10., 0.2, "Origin helix parameter tanl error", "sigmas", "track"); - + Histogram hEdrhoCon = new Histogram(100, -10., 0.2, "Origin helix parameter drho error with ECAL constraint", "sigmas", "track"); Histogram hEphi0Con = new Histogram(100, -10., 0.2, "Origin helix parameter phi0 error with ECAL constraint", "sigmas", "track"); Histogram hEkCon = new Histogram(100, -10., 0.2, "Origin helix parameter K error with ECAL constraint", "sigmas", "track"); Histogram hEdzCon = new Histogram(100, -10., 0.2, "Origin helix parameter dz error with ECAL constraint", "sigmas", "track"); Histogram hEtanlCon = new Histogram(100, -10., 0.2, "Origin helix parameter tanl error with ECAL constraint", "sigmas", "track"); - + Histogram hEdrhoSigO = new Histogram(100, -10., 0.2, "Origin helix parameter drho constrained error", "sigmas", "track"); Histogram hEphi0SigO = new Histogram(100, -10., 0.2, "Origin helix parameter phi0 constrained error", "sigmas", "track"); Histogram hEkSigO = new Histogram(100, -10., 0.2, "Origin helix parameter K constrained error", "sigmas", "track"); @@ -414,8 +418,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEckO = new Histogram(100, -0.5, 0.01, "Origin helix parameter K constrained error", "1/GeV", "track"); Histogram hEcdzO = new Histogram(100, -0.4, 0.008, "Origin helix parameter dz constrained error", "mm", "track"); Histogram hEctanlO = new Histogram(100, -0.005, 0.0001, "Origin helix parameter tanl constrained error", " ", "track"); - Histogram hchi2inc = new Histogram(100, 0., 1., "Origin helix constrained chi^2 increment","chi2","track"); - Histogram hchi2c = new Histogram(100, 0., 1., "Origin helix constrained chi^2","chi2","track"); + Histogram hchi2inc = new Histogram(100, 0., 1., "Origin helix constrained chi^2 increment", "chi2", "track"); + Histogram hchi2c = new Histogram(100, 0., 1., "Origin helix constrained chi^2", "chi2", "track"); Histogram hResid0 = new Histogram(100, -10., 0.2, "Filtered residual for axial planes", "sigmas", "hits"); Histogram hResid1 = new Histogram(100, -10., 0.2, "Filtered residual for stereo planes", "sigmas", "hits"); Histogram hEdrhoG = new Histogram(100, -40., 0.8, "Helix guess drho error", "sigmas", "track"); @@ -444,33 +448,35 @@ class HelixTest3 { // Program for testing the Kalman fitting code hUnbias[i] = new Histogram(100, -0.2, 0.004, String.format("Unbiased residual for plane %d", i), "mm", "hits"); hUnbiasSig[i] = new Histogram(100, -10., 0.2, String.format("Unbiased residuals for layer %d", i), "sigmas", "hits"); } - Histogram hpropx = new Histogram(100,-5.,0.1,"projected track-state x error","mm","track"); - Histogram hpropxs = new Histogram(100,-5.,0.1,"projected track-state x error","sigmas","track"); - Histogram hpropy = new Histogram(100,-5.,0.1,"projected track-state y error","mm","track"); - Histogram hpropys = new Histogram(100,-5.,0.1,"projected track-state y error","sigmas","track"); - Histogram hpropxu = new Histogram(100,0.,0.05,"projected track-state x uncertainty","mm","track"); - Histogram hpropyu = new Histogram(100,0.,0.05,"projected track-state y uncertainty","mm","track"); - Histogram hPropxHS = new Histogram(100,-5.,0.1,"projected HelixState x error","mm","track"); - Histogram hPropzHS = new Histogram(100,-5.,0.1,"projected HelixState z error","mm","track"); - Histogram hPropxsHS = new Histogram(100,-5.,0.1,"projected HelixState x error","sigmas","track"); - Histogram hPropzsHS = new Histogram(100,-5.,0.1,"projected HelixState z error","sigmas","track"); - Histogram hPropx1 = new Histogram(100,-5.,0.1,"projected track-state x error 1 step","mm","track"); - Histogram hPropx1s = new Histogram(100,-5.,0.1,"projected track-state x error 1 step","sigmas","track"); - Histogram hPropz1 = new Histogram(100,-5.,0.1,"projected track-state z error 1 step","mm","track"); - Histogram hPropz1s = new Histogram(100,-5.,0.1,"projected track-state z error 1 step","sigmas","track"); - + Histogram hpropx = new Histogram(100, -5., 0.1, "projected track-state x error", "mm", "track"); + Histogram hpropxs = new Histogram(100, -5., 0.1, "projected track-state x error", "sigmas", "track"); + Histogram hpropy = new Histogram(100, -5., 0.1, "projected track-state y error", "mm", "track"); + Histogram hpropys = new Histogram(100, -5., 0.1, "projected track-state y error", "sigmas", "track"); + Histogram hpropxu = new Histogram(100, 0., 0.05, "projected track-state x uncertainty", "mm", "track"); + Histogram hpropyu = new Histogram(100, 0., 0.05, "projected track-state y uncertainty", "mm", "track"); + Histogram hPropxHS = new Histogram(100, -5., 0.1, "projected HelixState x error", "mm", "track"); + Histogram hPropzHS = new Histogram(100, -5., 0.1, "projected HelixState z error", "mm", "track"); + Histogram hPropxsHS = new Histogram(100, -5., 0.1, "projected HelixState x error", "sigmas", "track"); + Histogram hPropzsHS = new Histogram(100, -5., 0.1, "projected HelixState z error", "sigmas", "track"); + Histogram hPropx1 = new Histogram(100, -5., 0.1, "projected track-state x error 1 step", "mm", "track"); + Histogram hPropx1s = new Histogram(100, -5., 0.1, "projected track-state x error 1 step", "sigmas", "track"); + Histogram hPropz1 = new Histogram(100, -5., 0.1, "projected track-state z error 1 step", "mm", "track"); + Histogram hPropz1s = new Histogram(100, -5., 0.1, "projected track-state z error 1 step", "sigmas", "track"); + Instant timestamp = Instant.now(); System.out.format("Beginning time = %s\n", timestamp.toString()); LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); System.out.format("%s %d %d at %d:%d %d.%d seconds\n", ldt.getMonth(), ldt.getDayOfMonth(), ldt.getYear(), ldt.getHour(), ldt.getMinute(), ldt.getSecond(), ldt.getNano()); - double startTime = (double)(ldt.getMinute())*60. + (double)ldt.getSecond() + (double)(ldt.getNano())/1e9; + double startTime = (double) (ldt.getMinute()) * 60. + (double) ldt.getSecond() + (double) (ldt.getNano()) / 1e9; // Extrapolate the helix from the origin to the first detector layer SiModule si1 = SiModules.get(0); double phi1 = TkInitial.planeIntersect(si1.p); if (Double.isNaN(phi1)) { - if (verbose) System.out.format("Oops! No intersection found with initial plane"); + if (verbose) { + System.out.format("Oops! No intersection found with initial plane"); + } return; } Vec lyr1Int = TkInitial.atPhi(phi1); @@ -491,13 +497,17 @@ class HelixTest3 { // Program for testing the Kalman fitting code KalmanInterface KI = new KalmanInterface(kPar, fM); for (int iTrial = 0; iTrial < nTrials; iTrial++) { RKhelix Tk = helixBeginRK.copy(); - if (verbose) { Tk.print("copied initial helix"); } + if (verbose) { + Tk.print("copied initial helix"); + } // Populate the Si detector planes with hits from the helix scattered at each // plane for (int icm = 0; icm < SiModules.size(); icm++) { SiModule thisSi = SiModules.get(icm); - if (thisSi.Layer < 0) { continue; } + if (thisSi.Layer < 0) { + continue; + } thisSi.reset(); int pln = thisSi.Layer; int det = thisSi.detector; @@ -521,7 +531,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code // Check whether the intersection is within the bounds of the detector if (rDet.v[0] > thisSi.xExtent[1] || rDet.v[0] < thisSi.xExtent[0] || rDet.v[1] > thisSi.yExtent[1] || rDet.v[1] < thisSi.yExtent[0]) { - if (verbose) { System.out.format(" Intersection point is outside of the detector %d in layer %d\n", det, pln); } + if (verbose) { + System.out.format(" Intersection point is outside of the detector %d in layer %d\n", det, pln); + } continue; } Tk = new RKhelix(rscat, pInt, Q, fMg, rnd); @@ -537,7 +549,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code double smear = resolution * gran[0]; double m1 = rDet.v[1] + smear; hRes.entry(smear); - if (verbose) { System.out.format(" Measurement 1= %10.7f, Truth=%10.7f\n", m1, rDet.v[1]); } + if (verbose) { + System.out.format(" Measurement 1= %10.7f, Truth=%10.7f\n", m1, rDet.v[1]); + } Measurement thisM1 = new Measurement(m1, 0., resolution, 0., 10., rscat, rDet.v[1]); thisSi.addMeasurement(thisM1); } @@ -582,19 +596,23 @@ class HelixTest3 { // Program for testing the Kalman fitting code } } else { TkEnd = Tk; - if (verbose) { TkEnd.print("TkEnd"); } + if (verbose) { + TkEnd.print("TkEnd"); + } } } - + // Extrapolate further to the ECAL location. The helix parameters are in a coordinate system with origin // on the helix at the current location x and aligned with the local B field. Since we provide the pivot // point to be x (in global coordinates), the pivotECAL in the field system is (0,0,0). - Plane pEcal = new Plane(new Vec(0.,eCalLoc,0.), new Vec(0.,1.,0.)); + Plane pEcal = new Plane(new Vec(0., eCalLoc, 0.), new Vec(0., 1., 0.)); RKhelix TkEcal = Tk.propagateRK(pEcal); int nHits = 0; for (SiModule siM : SiModules) { - if (siM.hits.size() > 0) nHits++; + if (siM.hits.size() > 0) { + nHits++; + } } hnHit.entry(nHits); @@ -606,8 +624,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code //for (int i=0; i= 0; i--) { SiModule si = SiModules.get(i); - if (si.Layer > startLayer) continue; - if (si.hits.isEmpty()) continue; + if (si.Layer > startLayer) { + continue; + } + if (si.hits.isEmpty()) { + continue; + } if (nA < nAxial) { if (!si.isStereo) { int[] ht = new int[2]; @@ -618,7 +640,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code frstLyr = si.Layer; } } else { - if (nS >= nStereo) break; + if (nS >= nStereo) { + break; + } } if (nS < nStereo) { if (si.isStereo) { @@ -630,7 +654,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code frstLyr = si.Layer; } } else { - if (nA >= nStereo) break; + if (nA >= nStereo) { + break; + } } } if (nS < nStereo || nA < nAxial) { @@ -657,10 +683,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code // For comparison, get the true helix in the B field frame at the first layer of the linear fit // Then transform it to the same pivot used by the linear fit. Here we have to ignore details about // coordinate system rotation, because the linear fit is only done in the global frame assuming constant field - helixMCtrue = helixSaved[frstLyr]; Vec pivotOnAxis = new Vec(0., location[frstLyr], 0.); - Vec Bpiv = new Vec(3,fMg.getField(pivotOnAxis)); + Vec Bpiv = new Vec(3, fMg.getField(pivotOnAxis)); double alpha = 1.0e12 / (2.99793e8 * Bpiv.mag()); Vec helixTrueTrans = HelixState.pivotTransform(pivotOnAxis, helixMCtrue, pivotSaved[frstLyr], alpha, 0.); Vec gErrVec = initialHelixGuess.dif(helixTrueTrans); @@ -668,7 +693,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code // initialHelixGuess.v[2] - K, // initialHelixGuess.v[3] - dz, initialHelixGuess.v[4] - tanl); double[] gErr = new double[5]; - for (int i = 0; i < 5; i++) { gErr[i] = gErrVec.v[i] / GuessErrors.v[i]; } + for (int i = 0; i < 5; i++) { + gErr[i] = gErrVec.v[i] / GuessErrors.v[i]; + } if (verbose) { // helixMCtrue.print("MC true helix at this layer"); // pivotSaved[frstLyr].print("old pivot"); @@ -701,9 +728,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code // double Bstart = seed.B(); // Vec tBstart = new Vec(0., 0., 1.); - // Cheating initial "guess" for the helix - double[] rn = new double[2]; if (perfect) { rn[0] = 0.; @@ -736,7 +761,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code initialCovariance.unsafe_set(3, 3, dzSigma * dzSigma); initialCovariance.unsafe_set(4, 4, tanlSigma * tanlSigma); - Vec Bf0 = new Vec(3,fMg.getField(helixOrigin)); + Vec Bf0 = new Vec(3, fMg.getField(helixOrigin)); if (verbose) { initialHelixGuess.print("initial helix guess"); System.out.format("True helix: %10.6f %10.6f %10.6f %10.6f %10.6f\n", drho, phi0, K, dz, tanl); @@ -756,41 +781,49 @@ class HelixTest3 { // Program for testing the Kalman fitting code KalmanTrackFit2 kF = new KalmanTrackFit2(iTrial, SiModules, null, startLayer, nIteration, new Vec(0., location[frstLyr], 0.), initialHelixGuess, initialCovariance, kPar, fM); long endTimeF = System.nanoTime(); - double runTime = (double)(endTimeF - startTimeF)/1000000.; + double runTime = (double) (endTimeF - startTimeF) / 1000000.; executionTime += runTime; - if (!kF.success) continue; + if (!kF.success) { + continue; + } KalTrack KalmanTrack = kF.tkr; - if (KalmanTrack == null) continue; + if (KalmanTrack == null) { + continue; + } KalmanTrack.originHelix(); - if (verbose) KalmanTrack.print("KalmanTrack"); - + if (verbose) { + KalmanTrack.print("KalmanTrack"); + } + // Check on the covariance matrix Matrix C = new Matrix(KalmanTrack.originCovariance()); - EigenvalueDecomposition eED= new EigenvalueDecomposition(C); - double [] ev = eED.getRealEigenvalues(); + EigenvalueDecomposition eED = new EigenvalueDecomposition(C); + double[] ev = eED.getRealEigenvalues(); boolean badCov = false; - for (int i=0; i<5; ++i) { + for (int i = 0; i < 5; ++i) { if (ev[i] < 0.) { System.out.format("Event %d, eigenvalue %d of covariance is negative!", iTrial, i); badCov = true; } } - if (badCov) nBadCov++; + if (badCov) { + nBadCov++; + } if (iTrial < 10) { - Vec evV = new Vec(5,ev); + Vec evV = new Vec(5, ev); evV.print("Eigenvalues of covariance"); } - + // Test the helix propagation code - List states = new ArrayList(); for (MeasurementSite site : kF.sites) { int loc = TrackState.AtOther; - if (kF.sites.indexOf(site) == 0) loc = TrackState.AtFirstHit; - else if (kF.sites.indexOf(site) == kF.sites.size()-1) { + if (kF.sites.indexOf(site) == 0) { + loc = TrackState.AtFirstHit; + } else if (kF.sites.indexOf(site) == kF.sites.size() - 1) { loc = TrackState.AtLastHit; } - TrackState ts = KI.createTrackState(site, loc, true); + TrackState ts = KI.createTrackState(site, loc, true); states.add(ts); } TrackState lastState = null; @@ -801,20 +834,22 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } } if (lastState != null) { - // test propagation of a track state from one end of the track to the ECAL region - final boolean debug = false; + // test propagation of a track state from one end of the track to the ECAL region + final boolean debug = false; if (KalmanTrack.nHits >= 12) { - if (KalmanTrack.chi2/KalmanTrack.nHits < 2.) { - MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size()-1); + if (KalmanTrack.chi2 / KalmanTrack.nHits < 2.) { + MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size() - 1); Vec eCalPos = TkEcal.x; - Plane plnAtEcal = new Plane(eCalPos, new Vec(0.,1.,0.)); + Plane plnAtEcal = new Plane(eCalPos, new Vec(0., 1., 0.)); if (debug) { eCalPos.print("ECAL cluster position"); lastSite.aS.helix.print("helix at last layer"); } - double [] arcLength = new double[1]; + double[] arcLength = new double[1]; HelixState helixAtEcal = lastSite.aS.helix.propagateRungeKutta(plnAtEcal, yScat, XLscat, fM, arcLength); - if (MatrixFeatures_DDRM.hasNaN(helixAtEcal.C)) continue; + if (MatrixFeatures_DDRM.hasNaN(helixAtEcal.C)) { + continue; + } Vec intPnt = helixAtEcal.getRKintersection(); if (debug) { helixAtEcal.print("helix at ECAL cluster"); @@ -823,21 +858,21 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hPropxHS.entry(intPnt.v[0] - eCalPos.v[0]); hPropzHS.entry(intPnt.v[2] - eCalPos.v[2]); DMatrixRMaj covAtEcal = helixAtEcal.C; - double [][] dadx = KalTrack.DxTOa(helixAtEcal.a); - double [][] Cx = new double[3][3]; + double[][] dadx = KalTrack.DxTOa(helixAtEcal.a); + double[][] Cx = new double[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Cx[i][j] = 0.; for (int k = 0; k < 5; k++) { for (int l = 0; l < 5; l++) { - Cx[i][j] += dadx[i][k] * covAtEcal.unsafe_get(k,l) * dadx[j][l]; + Cx[i][j] += dadx[i][k] * covAtEcal.unsafe_get(k, l) * dadx[j][l]; } } } } - hPropxsHS.entry((intPnt.v[0] - eCalPos.v[0])/Math.sqrt(Cx[0][0])); - hPropzsHS.entry((intPnt.v[2] - eCalPos.v[2])/Math.sqrt(Cx[2][2])); - + hPropxsHS.entry((intPnt.v[0] - eCalPos.v[0]) / Math.sqrt(Cx[0][0])); + hPropzsHS.entry((intPnt.v[2] - eCalPos.v[2]) / Math.sqrt(Cx[2][2])); + // Try in a single step---this worked perfect in uniform field, as long as last scatter is included double phiIntEcal = lastSite.aS.helix.planeIntersect(plnAtEcal); if (!Double.isNaN(phiIntEcal)) { @@ -848,22 +883,28 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } Vec intcpt = lastSite.aS.helix.atPhi(phiIntEcal); Vec helixAtIntcpt = lastSite.aS.helix.pivotTransform(intcpt); - if (debug) intcpt.print("intercept local"); + if (debug) { + intcpt.print("intercept local"); + } intcpt = lastSite.aS.helix.toGlobal(intcpt); - if (debug) intcpt.print("intercept global"); - DMatrixRMaj F = new DMatrixRMaj(5,5); + if (debug) { + intcpt.print("intercept global"); + } + DMatrixRMaj F = new DMatrixRMaj(5, 5); lastSite.aS.helix.makeF(helixAtIntcpt, F); - if (debug) F.print("tranform matrix F"); - Vec pMom = HelixState.getMom(0.,helixAtIntcpt); + if (debug) { + F.print("tranform matrix F"); + } + Vec pMom = HelixState.getMom(0., helixAtIntcpt); double pMag = pMom.mag(); - double ct = pMom.v[1]/pMag; - double sigmaMS = HelixState.projMSangle(pMag, XLscat.get(XLscat.size()-1)/ct); - DMatrixRMaj Qmcs = new DMatrixRMaj(5,5); + double ct = pMom.v[1] / pMag; + double sigmaMS = HelixState.projMSangle(pMag, XLscat.get(XLscat.size() - 1) / ct); + DMatrixRMaj Qmcs = new DMatrixRMaj(5, 5); lastSite.aS.helix.getQ(sigmaMS, Qmcs); CommonOps_DDRM.add(lastSite.aS.helix.C, Qmcs, tempM1); CommonOps_DDRM.multTransB(tempM1, F, tempM2); - DMatrixRMaj covAtIntcpt = new DMatrixRMaj(5,5); - CommonOps_DDRM.mult(F, tempM2, covAtIntcpt); + DMatrixRMaj covAtIntcpt = new DMatrixRMaj(5, 5); + CommonOps_DDRM.mult(F, tempM2, covAtIntcpt); dadx = KalTrack.DxTOa(helixAtIntcpt); Cx = new double[3][3]; for (int i = 0; i < 3; i++) { @@ -871,21 +912,21 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { Cx[i][j] = 0.; for (int k = 0; k < 5; k++) { for (int l = 0; l < 5; l++) { - Cx[i][j] += dadx[i][k] * covAtIntcpt.unsafe_get(k,l) * dadx[j][l]; + Cx[i][j] += dadx[i][k] * covAtIntcpt.unsafe_get(k, l) * dadx[j][l]; } } } } if (debug) { covAtIntcpt.print("covAtIntcpt"); - new SquareMatrix(3,Cx).print("Cx"); + new SquareMatrix(3, Cx).print("Cx"); } hPropx1.entry(intcpt.v[0] - eCalPos.v[0]); - hPropz1.entry(intcpt.v[2] - eCalPos.v[2]); - hPropx1s.entry((intcpt.v[0] - eCalPos.v[0])/Math.sqrt(Cx[0][0])); - hPropz1s.entry((intcpt.v[2] - eCalPos.v[2])/Math.sqrt(Cx[2][2])); + hPropz1.entry(intcpt.v[2] - eCalPos.v[2]); + hPropx1s.entry((intcpt.v[0] - eCalPos.v[0]) / Math.sqrt(Cx[0][0])); + hPropz1s.entry((intcpt.v[2] - eCalPos.v[2]) / Math.sqrt(Cx[2][2])); } - } + } } /* for (TrackState tkState : states) { @@ -914,19 +955,19 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { break; } } - */ + */ } // end of test of helix propagation - + // Test the vertex constraint Vec vtx = helixOrigin.copy(); - for (int i=0; i<3; ++i) { + for (int i = 0; i < 3; ++i) { vtx.v[i] += rnd.nextGaussian() * vtxRes[i]; } SquareMatrix vtxCov = new SquareMatrix(3); - vtxCov.M[0][0] = vtxRes[0]*vtxRes[0]; - vtxCov.M[1][1] = vtxRes[1]*vtxRes[1]; - vtxCov.M[2][2] = vtxRes[2]*vtxRes[2]; + vtxCov.M[0][0] = vtxRes[0] * vtxRes[0]; + vtxCov.M[1][1] = vtxRes[1] * vtxRes[1]; + vtxCov.M[2][2] = vtxRes[2] * vtxRes[2]; HelixState constrainedHelix = KalmanTrack.originConstraint(vtx.v, vtxCov.M); hchi2inc.entry(KalmanTrack.chi2incOrigin()); hchi2c.entry(KalmanTrack.chi2incOrigin() + KalmanTrack.chi2); @@ -939,32 +980,41 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { SiModule siM = site.m; if (site.m.Layer >= 0) { if (site.filtered) { - if (siM.isStereo) hResid0.entry(site.aF.r / Math.sqrt(site.aF.R)); - else hResid1.entry(site.aF.r / Math.sqrt(site.aF.R)); + if (siM.isStereo) { + hResid0.entry(site.aF.r / Math.sqrt(site.aF.R)); + } else { + hResid1.entry(site.aF.r / Math.sqrt(site.aF.R)); + } } if (site.smoothed) { - if (site.m.Layer == 4) hReducedErr.entry(Math.sqrt(site.aS.R)); + if (site.m.Layer == 4) { + hReducedErr.entry(Math.sqrt(site.aS.R)); + } chi2s += Math.pow(site.aS.mPred - site.m.hits.get(site.hitID).vTrue, 2) / site.aS.R; hResidS0[siM.Layer].entry(site.aS.r / Math.sqrt(site.aS.R)); hResidS2[siM.Layer].entry(site.aS.r); - if (site.hitID >= 0) { hResidS4[siM.Layer].entry(site.m.hits.get(site.hitID).vTrue - site.aS.mPred); } + if (site.hitID >= 0) { + hResidS4[siM.Layer].entry(site.m.hits.get(site.hitID).vTrue - site.aS.mPred); + } } } } } - for (int layer=0; layer < nLayers; ++layer) { + for (int layer = 0; layer < nLayers; ++layer) { Pair resid = KalmanTrack.unbiasedResidual(layer); if (resid.getSecondElement() > -999.) { double variance = resid.getSecondElement(); double sigma = Math.sqrt(variance); double unbResid = resid.getFirstElement(); hUnbias[layer].entry(unbResid); - hUnbiasSig[layer].entry(unbResid/sigma); - if (variance < resolution*resolution) { - System.out.format("Event %d layer %d, unbiased residual variance too small: %10.5f, chi2=%9.2f, hits=%d, resid=%9.6f, lyrs:", - iTrial, layer, variance, KalmanTrack.chi2, KalmanTrack.nHits, unbResid); + hUnbiasSig[layer].entry(unbResid / sigma); + if (variance < resolution * resolution) { + System.out.format("Event %d layer %d, unbiased residual variance too small: %10.5f, chi2=%9.2f, hits=%d, resid=%9.6f, lyrs:", + iTrial, layer, variance, KalmanTrack.chi2, KalmanTrack.nHits, unbResid); for (MeasurementSite site : KalmanTrack.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } System.out.format(" %d ", site.m.Layer); } System.out.format("\n"); @@ -974,8 +1024,12 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { for (MeasurementSite site : KalmanTrack.interceptVects().keySet()) { Vec loc = KalmanTrack.interceptVects().get(site); SiModule siM = site.m; - if (siM.Layer < 0) continue; - if (site.hitID < 0) { System.out.format("Missing hit ID on site with layer=%d", siM.Layer); } + if (siM.Layer < 0) { + continue; + } + if (site.hitID < 0) { + System.out.format("Missing hit ID on site with layer=%d", siM.Layer); + } Vec locMC = site.m.hits.get(site.hitID).rGlobal; hResidX[siM.Layer].entry(loc.v[0] - locMC.v[0]); hResidZ[siM.Layer].entry(loc.v[2] - locMC.v[2]); @@ -1006,7 +1060,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } Vec newPivot = kF.fittedStateBegin().helix.toLocal(helixBegin.origin.sum(helixBegin.X0)); Vec aF = kF.fittedStateBegin().helix.pivotTransform(newPivot); - + // now rotate to the original field frame if (!kPar.uniformB) { RotMatrix Rcombo = helixBegin.R.multiply(kF.fittedStateBegin().helix.Rot.invert()); @@ -1021,8 +1075,12 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { DMatrixRMaj aFC = kF.fittedStateBegin().covariancePivotTransform(aF); CommonOps_DDRM.multTransB(aFC, fRot, tempM1); CommonOps_DDRM.mult(fRot, tempM1, aFC); - for (int i = 0; i < 5; i++) aFe.v[i] = Math.sqrt(Math.max(0., aFC.unsafe_get(i,i))); - if (verbose) { aFe.print("error estimates on the smoothed helix parameters"); } + for (int i = 0; i < 5; i++) { + aFe.v[i] = Math.sqrt(Math.max(0., aFC.unsafe_get(i, i))); + } + if (verbose) { + aFe.print("error estimates on the smoothed helix parameters"); + } Vec trueErr = aF.dif(helixBegin.p); if (verbose) { for (int i = 0; i < 5; i++) { @@ -1051,7 +1109,9 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } DMatrixRMaj eFc = kF.fittedStateEnd().covariancePivotTransform(eF); Vec eFe = new Vec(5); - for (int i = 0; i < 5; i++) eFe.v[i] = Math.sqrt(Math.max(0., eFc.unsafe_get(i,i))); + for (int i = 0; i < 5; i++) { + eFe.v[i] = Math.sqrt(Math.max(0., eFc.unsafe_get(i, i))); + } Vec pivotF = new Vec(3); Vec fH = TkEnd.helixParameters(TkEnd.x, pivotF); trueErr = eF.dif(fH); @@ -1070,27 +1130,33 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hEtanl.entry(trueErr.v[4] / eFe.v[4]); trueErr = eF.dif(fH); helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(eFc).invert())); - if (verbose) { System.out.format("Full chi^2 of the filtered helix parameters = %12.4e\n", helixChi2); } + if (verbose) { + System.out.format("Full chi^2 of the filtered helix parameters = %12.4e\n", helixChi2); + } hChi2Helix.entry(helixChi2); // Study the fitted helix extrapolated back to the origin double[] hP = KalmanTrack.originHelixParms(); - testHelix = new Vec(5,hP); + testHelix = new Vec(5, hP); testCov = new DMatrixRMaj(KalmanTrack.originCovariance()); double[] hErr = new double[5]; - for (int i = 0; i < 5; ++i) { hErr[i] = (hP[i] - TkInitial.p.v[i]); } + for (int i = 0; i < 5; ++i) { + hErr[i] = (hP[i] - TkInitial.p.v[i]); + } double[] hErrC = new double[5]; - for (int i = 0; i < 5; ++i) { hErrC[i] = (constrainedHelix.a.v[i] - TkInitial.p.v[i]); } + for (int i = 0; i < 5; ++i) { + hErrC[i] = (constrainedHelix.a.v[i] - TkInitial.p.v[i]); + } hEdrhoO.entry(hErr[0] / KalmanTrack.helixErr(0)); hEphi0O.entry(hErr[1] / KalmanTrack.helixErr(1)); hEkO.entry(hErr[2] / KalmanTrack.helixErr(2)); hEdzO.entry(hErr[3] / KalmanTrack.helixErr(3)); hEtanlO.entry(hErr[4] / KalmanTrack.helixErr(4)); - hEdrhoSigO.entry(hErrC[0] / Math.sqrt(constrainedHelix.C.unsafe_get(0,0))); - hEphi0SigO.entry(hErrC[1] / Math.sqrt(constrainedHelix.C.unsafe_get(1,1))); - hEkSigO.entry(hErrC[2] / Math.sqrt(constrainedHelix.C.unsafe_get(2,2))); - hEdzSigO.entry(hErrC[3] / Math.sqrt(constrainedHelix.C.unsafe_get(3,3))); - hEtanlSigO.entry(hErrC[4] / Math.sqrt(constrainedHelix.C.unsafe_get(4,4))); + hEdrhoSigO.entry(hErrC[0] / Math.sqrt(constrainedHelix.C.unsafe_get(0, 0))); + hEphi0SigO.entry(hErrC[1] / Math.sqrt(constrainedHelix.C.unsafe_get(1, 1))); + hEkSigO.entry(hErrC[2] / Math.sqrt(constrainedHelix.C.unsafe_get(2, 2))); + hEdzSigO.entry(hErrC[3] / Math.sqrt(constrainedHelix.C.unsafe_get(3, 3))); + hEtanlSigO.entry(hErrC[4] / Math.sqrt(constrainedHelix.C.unsafe_get(4, 4))); hEadrhoO.entry(hErr[0]); hEaphi0O.entry(hErr[1]); hEakO.entry(hErr[2]); @@ -1137,24 +1203,26 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } } // Study the effect of an energy constraint - double sigmaE = 0.05*FastMath.sqrt(Etrue); - double E = Etrue + rnd.nextGaussian()*sigmaE; + double sigmaE = 0.05 * FastMath.sqrt(Etrue); + double E = Etrue + rnd.nextGaussian() * sigmaE; KalmanTrack.energyConstraint(E, sigmaE); if (verbose) { - System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n",Etrue,E,sigmaE); + System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n", Etrue, E, sigmaE); KalmanTrack.helixAtOrigin.a.print("helix at origin"); TkInitial.p.print("true helix"); KalmanTrack.energyConstrainedHelix.a.print("energy constrained helix"); KalmanTrack.printLong("after adding energy constraint"); } - for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.energyConstrainedHelix.a.v[i] - TkInitial.p.v[i]); + for (int i = 0; i < 5; ++i) { + hErr[i] = (KalmanTrack.energyConstrainedHelix.a.v[i] - TkInitial.p.v[i]); + } hEatanlcon.entry(hErr[4]); hEakcon.entry(hErr[2]); - hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(0,0))); - hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(1,1))); - hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(2,2))); - hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(3,3))); - hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(4,4))); + hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(0, 0))); + hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(1, 1))); + hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(2, 2))); + hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(3, 3))); + hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.energyConstrainedHelix.C.unsafe_get(4, 4))); } } } @@ -1167,7 +1235,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); System.out.format("%s %d %d at %d:%d %d.%d seconds\n", ldt.getMonth(), ldt.getDayOfMonth(), ldt.getYear(), ldt.getHour(), ldt.getMinute(), ldt.getSecond(), ldt.getNano()); - double endTime = (double)(ldt.getMinute())*60. + (double)(ldt.getSecond()) + (double)(ldt.getNano())/1e9; + double endTime = (double) (ldt.getMinute()) * 60. + (double) (ldt.getSecond()) + (double) (ldt.getNano()) / 1e9; double elapsedTime = endTime - startTime; System.out.format("Total elapsed time = %10.5f\n", elapsedTime); System.out.format("Elapsed time for Kalman Filter = %10.4f ms\n", executionTime); @@ -1268,8 +1336,8 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hPropz1.plot(path + "propz1.gp", true, "gaus", " "); hPropx1s.plot(path + "propx1s.gp", true, "gaus", " "); hPropz1s.plot(path + "propz1s.gp", true, "gaus", " "); - - /* + + /* // Test of helix covariance extrapolation if (testCov != null && testHelix != null) { Vec X0initial = new Vec(0.,0.,0.); @@ -1471,9 +1539,9 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hTanLpF.plot(path + "TestTanlpF.gp", true, "gaus", " "); System.out.println("All Done!"); } - */ + */ } - + /* double[] gausRan() { // Return two gaussian random numbers @@ -1492,5 +1560,5 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { return gran; } - */ + */ } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index ddbaad365c..bf159dac65 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -23,6 +23,7 @@ * Track followed and fitted by the Kalman filter */ public class KalTrack { + public int ID; public int nHits; public double chi2; @@ -30,7 +31,7 @@ public class KalTrack { ArrayList SiteList; // call the corresponding functions to create and access the following two maps - private Map interceptVects; + private Map interceptVects; private Map interceptMomVects; Map millipedeMap; Map lyrMap; @@ -59,66 +60,71 @@ public class KalTrack { private static DMatrixRMaj Cinv; private static Logger logger; private static boolean initialized; - private double [] arcLength; + private double[] arcLength; private static LinearSolverDense solver; private static final boolean uniformBatOrigin = false; - static int [] nBadCov = {0, 0}; + static int[] nBadCov = {0, 0}; /** * Track constructor - * @param evtNumb event number - * @param tkID integer ID for the track - * @param SiteList list of measurement sites - * @param yScat array of scattering planes to propagate through - * @param XLscat scattering radiation lengths at each plane - * @param kPar KalmanParams instance + * + * @param evtNumb event number + * @param tkID integer ID for the track + * @param SiteList list of measurement sites + * @param yScat array of scattering planes to propagate through + * @param XLscat scattering radiation lengths at each plane + * @param kPar KalmanParams instance */ KalTrack(int evtNumb, int tkID, ArrayList SiteList, ArrayList yScat, ArrayList XLscat, KalmanParams kPar) { // System.out.format("KalTrack constructor chi2=%10.6f\n", chi2); eventNumber = evtNumb; bad = false; this.yScat = yScat; - this.XLscat = XLscat; + this.XLscat = XLscat; this.kPar = kPar; ID = tkID; arcLength = null; //debug = (evtNumb == 217481); - + if (!initialized) { logger = Logger.getLogger(KalTrack.class.getName()); - tempV = new DMatrixRMaj(5,1); - Cinv = new DMatrixRMaj(5,5); + tempV = new DMatrixRMaj(5, 1); + Cinv = new DMatrixRMaj(5, 5); initialized = true; solver = LinearSolverFactory_DDRM.symmPosDef(5); } - + // Trim empty sites from the track ends Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); int firstSite = -1; - for (int idx=0; idx= 0) break; + if (SiteList.get(idx).hitID >= 0) { + break; + } } int lastSite = 999; - for (int idx = SiteList.size()-1; idx >= 0; --idx) { + for (int idx = SiteList.size() - 1; idx >= 0; --idx) { lastSite = idx; - if (SiteList.get(idx).hitID >= 0) break; + if (SiteList.get(idx).hitID >= 0) { + break; + } } - + // Make a new list of sites, without empty sites at beginning or end this.SiteList = new ArrayList(SiteList.size()); - for (int idx=firstSite; idx<=lastSite; ++idx) { + for (int idx = firstSite; idx <= lastSite; ++idx) { MeasurementSite site = SiteList.get(idx); if (site.aS == null) { // This should never happen - logger.log(Level.SEVERE, String.format("Event %d: site of track %d is missing smoothed state vector for layer %d detector %d", + logger.log(Level.SEVERE, String.format("Event %d: site of track %d is missing smoothed state vector for layer %d detector %d", eventNumber, ID, site.m.Layer, site.m.detector)); logger.log(Level.WARNING, site.toString("bad site")); bad = true; continue; } - this.SiteList.add(site); + this.SiteList.add(site); } - + helixAtOrigin = null; energyConstrainedHelix = null; propagated = false; @@ -127,7 +133,7 @@ public class KalTrack { if (kPar.uniformB) { B = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), site0.m.Bfield); } else { - B = KalmanInterface.getField(new Vec(3,kPar.beamSpot), site0.m.Bfield); + B = KalmanInterface.getField(new Vec(3, kPar.beamSpot), site0.m.Bfield); } Bmag = B.mag(); tB = B.unitVec(Bmag); @@ -140,31 +146,37 @@ public class KalTrack { double c = 2.99793e8; // Speed of light in m/s alpha = 1.0e12 / (c * Bmag); // Convert from pt in GeV to curvature in mm Cx = null; - Cp = null; + Cp = null; // Fill the maps time = 0.; tMin = 9.9e9; tMax = -9.9e9; this.chi2 = 0.; this.nHits = 0; - if (debug) System.out.format("KalTrack: event %d, creating track %d\n", evtNumb, ID); + if (debug) { + System.out.format("KalTrack: event %d, creating track %d\n", evtNumb, ID); + } for (MeasurementSite site : this.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } nHits++; time += site.m.hits.get(site.hitID).time; tMin = Math.min(tMin, site.m.hits.get(site.hitID).time); - tMax = Math.max(tMax, site.m.hits.get(site.hitID).time); + tMax = Math.max(tMax, site.m.hits.get(site.hitID).time); this.chi2 += site.chi2inc; - if (debug) System.out.format(" Layer %d, chi^2 increment=%10.5f, a=%s\n", site.m.Layer, site.chi2inc, site.aS.helix.a.toString()); + if (debug) { + System.out.format(" Layer %d, chi^2 increment=%10.5f, a=%s\n", site.m.Layer, site.chi2inc, site.aS.helix.a.toString()); + } } - time = time/(double)nHits; - reducedChi2 = chi2/(double)nHits; + time = time / (double) nHits; + reducedChi2 = chi2 / (double) nHits; lyrMap = null; millipedeMap = null; interceptVects = null; interceptMomVects = null; if (nHits < 5) { // This should never happen - logger.log(Level.WARNING, "KalTrack error: not enough hits ("+nHits+") on the candidate track (ID::"+ID+") for event "+eventNumber); + logger.log(Level.WARNING, "KalTrack error: not enough hits (" + nHits + ") on the candidate track (ID::" + ID + ") for event " + eventNumber); bad = true; //for (MeasurementSite site : SiteList) logger.log(Level.FINE, site.toString("in KalTrack input list")); //logger.log(Level.FINE, String.format("KalTrack error in event %d: not enough hits on track %d: ",evtNumb,tkID)); @@ -179,74 +191,96 @@ public class KalTrack { /** * Get the time of the track - * @return time in ns + * + * @return time in ns */ public double getTime() { return time; } - + /** - * Make a map between measurement sites and 3-vector intercepts of the track at the SSD planes of those sites + * Make a map between measurement sites and 3-vector intercepts of the track + * at the SSD planes of those sites */ public Map interceptVects() { if (interceptVects == null) { interceptVects = new HashMap(nHits); for (MeasurementSite site : this.SiteList) { StateVector sV = null; - if (site.smoothed) sV = site.aS; - else sV = site.aP; + if (site.smoothed) { + sV = site.aS; + } else { + sV = site.aP; + } double phiS = sV.helix.planeIntersect(site.m.p); - if (Double.isNaN(phiS)) phiS = 0.; - interceptVects.put(site, sV.helix.toGlobal(sV.helix.atPhi(phiS))); + if (Double.isNaN(phiS)) { + phiS = 0.; + } + interceptVects.put(site, sV.helix.toGlobal(sV.helix.atPhi(phiS))); } } return interceptVects; } - + /** - * Make a map between measurement sites and 3-vector momentum at the intercept of the track and SSD plane + * Make a map between measurement sites and 3-vector momentum at the + * intercept of the track and SSD plane */ public Map interceptMomVects() { if (interceptMomVects == null) { interceptMomVects = new HashMap(); for (MeasurementSite site : this.SiteList) { StateVector sV = null; - if (site.smoothed) sV = site.aS; - else sV = site.aP; + if (site.smoothed) { + sV = site.aS; + } else { + sV = site.aP; + } double phiS = sV.helix.planeIntersect(site.m.p); - if (Double.isNaN(phiS)) phiS = 0.; + if (Double.isNaN(phiS)) { + phiS = 0.; + } interceptMomVects.put(site, sV.helix.Rot.inverseRotate(sV.helix.getMom(phiS))); } } return interceptMomVects; } - + /** - * Calculate and return the intersection point of the Kaltrack with an SiModule. - // Local sensor coordinates (u,v) are returned. - // The global intersection can be returned via rGbl if an array of length 3 is passed. - * @param mod module - * @param rGbl returned global intersection point - * @return intersection point + * Calculate and return the intersection point of the Kaltrack with an + * SiModule. // Local sensor coordinates (u,v) are returned. // The global + * intersection can be returned via rGbl if an array of length 3 is passed. + * + * @param mod module + * @param rGbl returned global intersection point + * @return intersection point */ - public double [] moduleIntercept(SiModule mod, double [] rGbl) { + public double[] moduleIntercept(SiModule mod, double[] rGbl) { HelixState hx = null; for (MeasurementSite site : SiteList) { - if (site.m == mod) hx = site.aS.helix; + if (site.m == mod) { + hx = site.aS.helix; + } } if (hx == null) { int mxLayer = -1; for (MeasurementSite site : SiteList) { - if (site.m.Layer > mod.Layer) continue; + if (site.m.Layer > mod.Layer) { + continue; + } if (site.m.Layer > mxLayer) { mxLayer = site.m.Layer; hx = site.aS.helix; } } } - if (hx == null) hx = SiteList.get(0).aS.helix; + if (hx == null) { + hx = SiteList.get(0).aS.helix; + } double phiS = hx.planeIntersect(mod.p); - if (Double.isNaN(phiS)) phiS = 0.; + if (Double.isNaN(phiS)) { + phiS = 0.; + } Vec intGlb = hx.toGlobal(hx.atPhi(phiS)); if (rGbl != null) { rGbl[0] = intGlb.v[0]; @@ -254,10 +288,10 @@ public Map interceptMomVects() { rGbl[2] = intGlb.v[2]; } Vec intLcl = mod.toLocal(intGlb); - double [] rtnArray = {intLcl.v[0], intLcl.v[1]}; + double[] rtnArray = {intLcl.v[0], intLcl.v[1]}; return rtnArray; } - + /** * Make a map between the track measurement sites and the tracker layers */ @@ -267,9 +301,10 @@ private void makeLyrMap() { lyrMap.put(site.m.Layer, site); } } - + /** - * Make a map between the measurement sites and the corresponding Millipede IDs + * Make a map between the measurement sites and the corresponding Millipede + * IDs */ private void makeMillipedeMap() { millipedeMap = new HashMap(nHits); @@ -277,72 +312,112 @@ private void makeMillipedeMap() { millipedeMap.put(site.m.millipedeID, site); } } - + /** - * Find the change in smoothed helix angle in XY between one layer and the next - * @param layer layer from 0 to 13 - * @return difference angle in XY + * Find the change in smoothed helix angle in XY between one layer and the + * next + * + * @param layer layer from 0 to 13 + * @return difference angle in XY */ public double scatX(int layer) { - if (lyrMap == null) makeLyrMap(); - if (!lyrMap.containsKey(layer)) return -999.; + if (lyrMap == null) { + makeLyrMap(); + } + if (!lyrMap.containsKey(layer)) { + return -999.; + } int lyrNxt = layer + 1; - while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) lyrNxt++; - if (lyrNxt > 13) return -999.; + while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) { + lyrNxt++; + } + if (lyrNxt > 13) { + return -999.; + } MeasurementSite s1 = lyrMap.get(layer); MeasurementSite s2 = lyrMap.get(lyrNxt); - if (s1.aS == null || s2.aS == null) return -999.; + if (s1.aS == null || s2.aS == null) { + return -999.; + } double phiS1 = s1.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS1)) return -999.; + if (Double.isNaN(phiS1)) { + return -999.; + } Vec p1 = s1.aS.helix.getMom(phiS1); double t1 = FastMath.atan2(p1.v[0], p1.v[1]); double phiS2 = s2.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS2)) return -999.; + if (Double.isNaN(phiS2)) { + return -999.; + } Vec p2 = s2.aS.helix.getMom(phiS2); double t2 = FastMath.atan2(p2.v[0], p2.v[1]); return t1 - t2; } /** - * Find the change in smoothed helix angle in ZY between one layer and the next - * @param layer layer from 0 to 13 - * @return difference angle in ZY + * Find the change in smoothed helix angle in ZY between one layer and the + * next + * + * @param layer layer from 0 to 13 + * @return difference angle in ZY */ public double scatZ(int layer) { - if (lyrMap == null) makeLyrMap(); - if (!lyrMap.containsKey(layer)) return -999.; + if (lyrMap == null) { + makeLyrMap(); + } + if (!lyrMap.containsKey(layer)) { + return -999.; + } int lyrNxt = layer + 1; - while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) lyrNxt++; - if (lyrNxt > 13) return -999.; + while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) { + lyrNxt++; + } + if (lyrNxt > 13) { + return -999.; + } MeasurementSite s1 = lyrMap.get(layer); MeasurementSite s2 = lyrMap.get(lyrNxt); - if (s1.aS == null || s2.aS == null) return -999.; + if (s1.aS == null || s2.aS == null) { + return -999.; + } double phiS1 = s1.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS1)) return -999.; + if (Double.isNaN(phiS1)) { + return -999.; + } Vec p1 = s1.aS.helix.Rot.inverseRotate(s1.aS.helix.getMom(phiS1)); double t1 = FastMath.atan2(p1.v[2], p1.v[1]); double phiS2 = s2.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS2)) return -999.; + if (Double.isNaN(phiS2)) { + return -999.; + } Vec p2 = s2.aS.helix.Rot.inverseRotate(s2.aS.helix.getMom(phiS2)); double t2 = FastMath.atan2(p2.v[2], p2.v[1]); return t1 - t2; } /** - * Alternative calculation of the fit chi^2, considering only residuals divided by the hit sigma - * @return chi-squared + * Alternative calculation of the fit chi^2, considering only residuals + * divided by the hit sigma + * + * @return chi-squared */ - public double chi2prime() { + public double chi2prime() { double c2 = 0.; for (MeasurementSite S : SiteList) { - if (S.aS == null) continue; + if (S.aS == null) { + continue; + } double phiS = S.aS.helix.planeIntersect(S.m.p); - if (Double.isNaN(phiS)) { phiS = 0.; } + if (Double.isNaN(phiS)) { + phiS = 0.; + } double vpred = S.h(S.aS, S.m, phiS); for (Measurement hit : S.m.hits) { - for (KalTrack tkr : hit.tracks) { - if (tkr.equals(this)) c2 += FastMath.pow((vpred - hit.v) / hit.sigma, 2); + for (KalTrack tkr : hit.tracks) { + if (tkr.equals(this)) { + c2 += FastMath.pow((vpred - hit.v) / hit.sigma, 2); + } } } } @@ -351,45 +426,53 @@ public double chi2prime() { /** * Get track unbiased residuals by Millipede ID + * * @param millipedeID * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidualMillipede(int millipedeID) { - if (millipedeMap == null) makeMillipedeMap(); + public Pair unbiasedResidualMillipede(int millipedeID) { + if (millipedeMap == null) { + makeMillipedeMap(); + } if (millipedeMap.containsKey(millipedeID)) { return unbiasedResidual(millipedeMap.get(millipedeID)); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } } - + /** * Get track unbiased residual by layer number + * * @param layer * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidual(int layer) { - if (lyrMap == null) makeLyrMap(); + public Pair unbiasedResidual(int layer) { + if (lyrMap == null) { + makeLyrMap(); + } if (lyrMap.containsKey(layer)) { return unbiasedResidual(lyrMap.get(layer)); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } - } - + } + /** - * Returns the unbiased residual for the track at a given site, together with the variance on that residual - * @param site measurement site - * @return residual and error + * Returns the unbiased residual for the track at a given site, together + * with the variance on that residual + * + * @param site measurement site + * @return residual and error */ - public Pair unbiasedResidual(MeasurementSite site) { + public Pair unbiasedResidual(MeasurementSite site) { double resid = -999.; - double varResid = -999.; - Vec aStar = null; + double varResid = -999.; + Vec aStar = null; if (site.hitID >= 0) { double sigma = site.m.hits.get(site.hitID).sigma; - DMatrixRMaj Cstar = new DMatrixRMaj(5,5); - aStar = site.aS.inverseFilter(site.H, sigma*sigma, Cstar); + DMatrixRMaj Cstar = new DMatrixRMaj(5, 5); + aStar = site.aS.inverseFilter(site.H, sigma * sigma, Cstar); HelixPlaneIntersect hpi = new HelixPlaneIntersect(); Plane pTrans = site.m.p.toLocal(site.aS.helix.Rot, site.aS.helix.origin); double phiInt = hpi.planeIntersect(aStar, site.aS.helix.X0, site.aS.helix.alpha, pTrans); @@ -398,64 +481,75 @@ public Pair unbiasedResidual(MeasurementSite site) { Vec globalInt = site.aS.helix.toGlobal(intPnt); Vec localInt = site.m.toLocal(globalInt); resid = site.m.hits.get(site.hitID).v - localInt.v[1]; - - CommonOps_DDRM.mult(Cstar, site.H, tempV); - varResid = sigma*sigma + CommonOps_DDRM.dot(site.H, tempV); + + CommonOps_DDRM.mult(Cstar, site.H, tempV); + varResid = sigma * sigma + CommonOps_DDRM.dot(site.H, tempV); } - } - return new Pair(resid, varResid); + } + return new Pair(resid, varResid); } /** - * Returns the biased residual for the track at a given layer, together with the variance on that residual - * @param layer - * @return biased residual and error + * Returns the biased residual for the track at a given layer, together with + * the variance on that residual + * + * @param layer + * @return biased residual and error */ - public Pair biasedResidual(int layer) { - if (lyrMap == null) makeLyrMap(); + public Pair biasedResidual(int layer) { + if (lyrMap == null) { + makeLyrMap(); + } if (lyrMap.containsKey(layer)) { return biasedResidual(lyrMap.get(layer)); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } } /** * Get track biased residuals by Millipede ID + * * @param millipedeID * @return pair of biased residual and its error estimate */ - public Pair biasedResidualMillipede(int millipedeID) { - if (millipedeMap == null) makeMillipedeMap(); + public Pair biasedResidualMillipede(int millipedeID) { + if (millipedeMap == null) { + makeMillipedeMap(); + } if (millipedeMap.containsKey(millipedeID)) { return biasedResidual(millipedeMap.get(millipedeID)); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } } /** * Get track biased residuals by measurement site + * * @param site * @return pair of biased residual and its error estimate */ - public Pair biasedResidual(MeasurementSite site) { + public Pair biasedResidual(MeasurementSite site) { double resid = -999.; - double varResid = -999.; + double varResid = -999.; if (site.aS != null) { resid = site.aS.r; varResid = site.aS.R; } - return new Pair(resid, varResid); + return new Pair(resid, varResid); } - + /** * Relatively short debug printout - * @param s Arbitrary string for the user's reference + * + * @param s Arbitrary string for the user's reference */ public void print(String s) { System.out.format("\nKalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); - if (propagated) System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); + if (propagated) { + System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); + } System.out.format(" Magnetic field magnitude = %10.5f and direction = %s\n", Bmag, tB.toString()); MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { @@ -467,12 +561,12 @@ public void print(String s) { int hitID = site.hitID; System.out.format(" Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f ", m.Layer, m.detector, m.isStereo, site.chi2inc); - if (hitID>=0) { + if (hitID >= 0) { System.out.format(" t=%5.1f ", site.m.hits.get(site.hitID).time); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site); - double [] lclint = moduleIntercept(m, null); - System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, + Pair unBiasedResid = unbiasedResidual(site); + double[] lclint = moduleIntercept(m, null); + System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, residual, lclint[0], m.xExtent[1]); } else { System.out.format("\n"); @@ -482,79 +576,84 @@ public void print(String s) { /** * Long detailed debug printout - * @param s Arbitrary string for the user's reference + * + * @param s Arbitrary string for the user's reference */ public void printLong(String s) { - System.out.format("%s", this.toString(s)); + System.out.format("%s", this.toString(s)); } /** * Long detailed debug printout to a string - * @param s Arbitrary string for the user's reference + * + * @param s Arbitrary string for the user's reference */ String toString(String s) { String str = String.format("\n KalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); if (propagated) { - str=str+String.format(" B-field at the origin=%10.6f, direction=%8.6f %8.6f %8.6f\n", Bmag, tB.v[0], tB.v[1], tB.v[2]); - str=str+helixAtOrigin.toString("helix state for a pivot at the origin")+"\n"; - str=str+originPoint.toString("point on the helix closest to the origin")+"\n"; - str=str+String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); + str = str + String.format(" B-field at the origin=%10.6f, direction=%8.6f %8.6f %8.6f\n", Bmag, tB.v[0], tB.v[1], tB.v[2]); + str = str + helixAtOrigin.toString("helix state for a pivot at the origin") + "\n"; + str = str + originPoint.toString("point on the helix closest to the origin") + "\n"; + str = str + String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); SquareMatrix C1 = new SquareMatrix(3, Cx); - str=str+C1.toString("covariance matrix for the point"); - str=str+originMomentum.toString("momentum of the particle at closest approach to the origin\n"); + str = str + C1.toString("covariance matrix for the point"); + str = str + originMomentum.toString("momentum of the particle at closest approach to the origin\n"); SquareMatrix C2 = new SquareMatrix(3, Cp); - str=str+C2.toString("covariance matrix for the momentum"); + str = str + C2.toString("covariance matrix for the momentum"); double tanL = helixAtOrigin.a.v[4]; double K = helixAtOrigin.a.v[2]; - double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); - str=str+String.format(" Energy unconstrained = %10.6f\n", energy); - } + double energy = FastMath.sqrt(1.0 + tanL * tanL) / Math.abs(K); + str = str + String.format(" Energy unconstrained = %10.6f\n", energy); + } if (energyConstrainedHelix != null) { double tanL = energyConstrainedHelix.a.v[4]; double K = energyConstrainedHelix.a.v[2]; - double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); - str=str+String.format(" Energy with constraint = %10.6f\n", energy); - str=str+energyConstrainedHelix.toString("helix state with energy constraint"); + double energy = FastMath.sqrt(1.0 + tanL * tanL) / Math.abs(K); + str = str + String.format(" Energy with constraint = %10.6f\n", energy); + str = str + energyConstrainedHelix.toString("helix state with energy constraint"); } MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { - str=str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); + str = str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); } for (int i = 0; i < SiteList.size(); i++) { MeasurementSite site = SiteList.get(i); SiModule m = site.m; int hitID = site.hitID; - str=str+String.format("Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f, Xscat=%10.8f Zscat=%10.8f, arc=%10.5f, hit=%d", m.Layer, m.detector, m.isStereo, + str = str + String.format("Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f, Xscat=%10.8f Zscat=%10.8f, arc=%10.5f, hit=%d", m.Layer, m.detector, m.isStereo, site.chi2inc, site.scatX(), site.scatZ(), site.arcLength, hitID); if (hitID < 0) { - str=str+"\n"; + str = str + "\n"; continue; } - str=str+String.format(", t=%5.1f", site.m.hits.get(site.hitID).time); + str = str + String.format(", t=%5.1f", site.m.hits.get(site.hitID).time); if (m.hits.get(hitID).tksMC != null) { - str=str+String.format(" MC tracks: "); + str = str + String.format(" MC tracks: "); for (int iMC : m.hits.get(hitID).tksMC) { - str=str+String.format(" %d ", iMC); + str = str + String.format(" %d ", iMC); } - str=str+"\n"; + str = str + "\n"; } if (interceptVects().containsKey(site)) { Vec interceptVec = interceptVects().get(site); Vec interceptMomVec = interceptMomVects().get(site); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site); - str=str+String.format(" Intercept=%s, p=%s, measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f+-%9.5f, error=%9.5f \n", interceptVec.toString(), + Pair unBiasedResid = unbiasedResidual(site); + str = str + String.format(" Intercept=%s, p=%s, measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f+-%9.5f, error=%9.5f \n", interceptVec.toString(), interceptMomVec.toString(), site.m.hits.get(hitID).v, site.aS.mPred, residual, unBiasedResid.getFirstElement(), unBiasedResid.getSecondElement(), FastMath.sqrt(site.aS.R)); } } - str=str+String.format("End of printing for KalTrack %s ID %d in event %d\n\n", s, ID, eventNumber); + str = str + String.format("End of printing for KalTrack %s ID %d in event %d\n\n", s, ID, eventNumber); return str; } /** - * Method to make simple yz plots of the track and the residuals. Note that in the yz plot of the track the hits are - * placed at the strip center, so they generally will not appear to be right on the track. Use gnuplot to display. - * @param path where to put the output + * Method to make simple yz plots of the track and the residuals. Note that + * in the yz plot of the track the hits are placed at the strip center, so + * they generally will not appear to be right on the track. Use gnuplot to + * display. + * + * @param path where to put the output */ public void plot(String path) { File file = new File(String.format("%s/Track%d_%d.gp", path, ID, eventNumber)); @@ -605,13 +704,19 @@ public void plot(String path) { pW.format("set yrange[-0.025 : 0.025]\n"); pW.format("$resids << EOD\n"); for (MeasurementSite site : SiteList) { - if (site.m.Layer < 0) continue; + if (site.m.Layer < 0) { + continue; + } double phiS = site.aS.helix.planeIntersect(site.m.p); - if (Double.isNaN(phiS)) { continue; } + if (Double.isNaN(phiS)) { + continue; + } Vec rHelixG = site.aS.helix.toGlobal(site.aS.helix.atPhi(phiS)); Vec rHelixL = site.m.toLocal(rHelixG); double residual = -999.; - if (site.hitID >= 0) residual = site.m.hits.get(site.hitID).v - rHelixL.v[1]; + if (site.hitID >= 0) { + residual = site.m.hits.get(site.hitID).v - rHelixL.v[1]; + } pW.format(" %10.5f %10.6f # %10.6f\n", rHelixG.v[1], residual, site.aS.r); } pW.format("EOD\n"); @@ -621,20 +726,28 @@ public void plot(String path) { /** * Arc length along track from the origin to the first measurement - * @return arc length in mm + * + * @return arc length in mm */ public double originArcLength() { - if (!propagated || arcLength == null) originHelix(); - if (arcLength == null) return 0.; + if (!propagated || arcLength == null) { + originHelix(); + } + if (arcLength == null) { + return 0.; + } return arcLength[0]; } - + /** * Runge Kutta propagation of the helix to the origin - * @return helix state at the origin + * + * @return helix state at the origin */ public boolean originHelix() { - if (propagated) return true; + if (propagated) { + return true; + } // Find the measurement site closest to the origin (target) MeasurementSite innerSite = null; @@ -655,13 +768,13 @@ public boolean originHelix() { return false; } Vec beamSpot = new Vec(3, kPar.beamSpot); - + // This propagated helix will have its pivot at the origin but is in the origin B-field frame // The StateVector method propagateRungeKutta transforms the origin plane into the origin B-field frame - Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); + Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); arcLength = new double[1]; if (uniformBatOrigin) { // Just a simple pivot transform is needed if the field is assumed uniform - Vec origin = new Vec(0.,0.,0.); + Vec origin = new Vec(0., 0., 0.); Vec newHelixParams = innerSite.aS.helix.pivotTransform(); DMatrixRMaj newCovariance = innerSite.aS.covariancePivotTransform(newHelixParams); helixAtOrigin = new HelixState(newHelixParams, origin, origin, newCovariance, Bmag, tB); @@ -670,21 +783,25 @@ public boolean originHelix() { } else { helixAtOrigin = innerSite.aS.helix.propagateRungeKutta(originPlane, yScat, XLscat, innerSite.m.Bfield, arcLength); //helixAtOrigin.print("nonuniformB"); - if (debug) System.out.format("KalTrack::originHelix: arc length to the first measurement = %9.4f\n", arcLength[0]); - if (covNaN()) return false; + if (debug) { + System.out.format("KalTrack::originHelix: arc length to the first measurement = %9.4f\n", arcLength[0]); + } + if (covNaN()) { + return false; + } if (!solver.setA(helixAtOrigin.C.copy())) { logger.fine("KalTrack:originHelix, cannot invert the covariance matrix"); - for (int i=0; i<5; ++i) { // Fill the matrix and inverse with something not too crazy and continue . . . - for (int j=0; j<5; ++j) { + for (int i = 0; i < 5; ++i) { // Fill the matrix and inverse with something not too crazy and continue . . . + for (int j = 0; j < 5; ++j) { if (i == j) { - Cinv.unsafe_set(i,j,1.0/Math.abs(helixAtOrigin.C.unsafe_get(i, j))); + Cinv.unsafe_set(i, j, 1.0 / Math.abs(helixAtOrigin.C.unsafe_get(i, j))); helixAtOrigin.C.unsafe_set(i, j, Math.abs(helixAtOrigin.C.unsafe_get(i, j))); } else { Cinv.unsafe_set(i, j, 0.); helixAtOrigin.C.unsafe_set(i, j, 0.); } } - } + } } else { solver.invert(Cinv); } @@ -721,17 +838,23 @@ public boolean originHelix() { /** * Get the extrapolate point in the plane of the origin - * @return 3-vector array of coordinates + * + * @return 3-vector array of coordinates */ public double[] originX() { - if (!propagated) originHelix(); - if (!propagated) return new double [] {0.,0.,0.}; + if (!propagated) { + originHelix(); + } + if (!propagated) { + return new double[]{0., 0., 0.}; + } return originPoint.v.clone(); } /** * Get the covariance of the track point in the origin plane - * @return 2D array, 3 by 3 + * + * @return 2D array, 3 by 3 */ public double[][] originXcov() { return Cx.clone(); @@ -739,17 +862,23 @@ public double[][] originXcov() { /** * Get the track momentum at the origin - * @return 3-vector momentum + * + * @return 3-vector momentum */ public double[] originP() { - if (!propagated) originHelix(); - if (!propagated) return new double [] {0.,0.,0.}; + if (!propagated) { + originHelix(); + } + if (!propagated) { + return new double[]{0., 0., 0.}; + } return originMomentum.v.clone(); } /** * Get the covariance of the momentum at the origin - * @return 3 by 3 array + * + * @return 3 by 3 array */ public double[][] originPcov() { return Cp.clone(); @@ -757,24 +886,34 @@ public double[][] originPcov() { /** * Get the helix pivot point near the origin - * @return 3-vector array + * + * @return 3-vector array */ public double[] originPivot() { - if (propagated) return helixAtOrigin.X0.v.clone(); - else return null; + if (propagated) { + return helixAtOrigin.X0.v.clone(); + } else { + return null; + } } - + /** * Get the covariance of helix parameters at the origin - * @return 5 by 5 array + * + * @return 5 by 5 array */ public double[][] originCovariance() { - if (!propagated) originHelix(); - double [][] M = new double[5][5]; - for (int i=0; i<5; ++i) { - for (int j=0; j<5; ++j) { - if (propagated) M[i][j] = helixAtOrigin.C.unsafe_get(i, j); - else M[i][j] = SiteList.get(0).aS.helix.C.unsafe_get(i, j); + if (!propagated) { + originHelix(); + } + double[][] M = new double[5][5]; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + if (propagated) { + M[i][j] = helixAtOrigin.C.unsafe_get(i, j); + } else { + M[i][j] = SiteList.get(0).aS.helix.C.unsafe_get(i, j); + } } } return M; @@ -782,38 +921,51 @@ public double[][] originCovariance() { /** * Check whether all elements of the covariance are real numbers - * @return false if the covariance is rotten + * + * @return false if the covariance is rotten */ - public boolean covNaN() { - if (helixAtOrigin.C == null) return true; + public boolean covNaN() { + if (helixAtOrigin.C == null) { + return true; + } return MatrixFeatures_DDRM.hasNaN(helixAtOrigin.C); } - + /** * Return the helix parameters of the track at the origin - * @return array of 5 doubles + * + * @return array of 5 doubles */ public double[] originHelixParms() { - if (propagated) return helixAtOrigin.a.v.clone(); - else return null; + if (propagated) { + return helixAtOrigin.a.v.clone(); + } else { + return null; + } } - + /** - * Update the helix parameters at the "origin" by using the target position or vertex as a constraint - * @param vtx vertex location - * @param vtxCov vertex error matrix - * @return constrained helix state + * Update the helix parameters at the "origin" by using the target position + * or vertex as a constraint + * + * @param vtx vertex location + * @param vtxCov vertex error matrix + * @return constrained helix state */ - public HelixState originConstraint(double [] vtx, double [][] vtxCov) { - if (!propagated) originHelix(); - if (!propagated) return null; - + public HelixState originConstraint(double[] vtx, double[][] vtxCov) { + if (!propagated) { + originHelix(); + } + if (!propagated) { + return null; + } + // Transform the inputs in the the helix field-oriented coordinate system - Vec v = helixAtOrigin.toLocal(new Vec(3,vtx)); - SquareMatrix Cov = helixAtOrigin.Rot.rotate(new SquareMatrix(3,vtxCov)); + Vec v = helixAtOrigin.toLocal(new Vec(3, vtx)); + SquareMatrix Cov = helixAtOrigin.Rot.rotate(new SquareMatrix(3, vtxCov)); Vec X0 = helixAtOrigin.X0; double phi = phiDOCA(helixAtOrigin.a, v, X0, alpha); -/* if (debug) { // Test the DOCA algorithm + /* if (debug) { // Test the DOCA algorithm Vec rDoca = HelixState.atPhi(X0, helixAtOrigin.a, phi, alpha); System.out.format("originConstraint: phi of DOCA=%10.5e\n", phi); rDoca.print(" DOCA point"); @@ -832,54 +984,58 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { double df2 = deriv * delPhi; System.out.format("Test of fDOCA derivative: df exact = %11.7f; df from derivative = %11.7f\n", df1, df2); }*/ - double [][] H = buildH(helixAtOrigin.a, v, X0, phi, alpha); + double[][] H = buildH(helixAtOrigin.a, v, X0, phi, alpha); Vec pntDOCA = HelixState.atPhi(X0, helixAtOrigin.a, phi, alpha); if (debug) { matrixPrint("H", H, 3, 5); - + // Derivative test HelixState hx = helixAtOrigin.copy(); - double daRel[] = { -0.04, 0.03, -0.16, -0.02, -0.015 }; - for (int i=0; i<5; ++i) daRel[i] = daRel[i]/100.; - for (int i = 0; i < 5; i++) { hx.a.v[i] = hx.a.v[i] * (1.0 + daRel[i]); } + double daRel[] = {-0.04, 0.03, -0.16, -0.02, -0.015}; + for (int i = 0; i < 5; ++i) { + daRel[i] = daRel[i] / 100.; + } + for (int i = 0; i < 5; i++) { + hx.a.v[i] = hx.a.v[i] * (1.0 + daRel[i]); + } Vec da = new Vec(hx.a.v[0] * daRel[0], hx.a.v[1] * daRel[1], hx.a.v[2] * daRel[2], hx.a.v[3] * daRel[3], hx.a.v[4] * daRel[4]); double phi2 = phiDOCA(hx.a, v, X0, alpha); Vec newX = HelixState.atPhi(X0, hx.a, phi2, alpha); Vec dxTrue = newX.dif(pntDOCA); dxTrue.print("originConstraint derivative test actual difference"); - + Vec dx = new Vec(3); - for (int i=0; i<3; ++i) { - for (int j=0; j<5; ++j) { - dx.v[i] += H[i][j]*da.v[j]; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 5; ++j) { + dx.v[i] += H[i][j] * da.v[j]; } } dx.print("difference from H derivative matrix"); - for (int i=0; i<3; ++i) { - double err = 100.*(dxTrue.v[i] - dx.v[i])/dxTrue.v[i]; + for (int i = 0; i < 3; ++i) { + double err = 100. * (dxTrue.v[i] - dx.v[i]) / dxTrue.v[i]; System.out.format(" Coordiante %d: percent difference = %10.6f\n", i, err); } System.out.println("helix covariance:"); helixAtOrigin.C.print(); } SquareMatrix Ginv = new SquareMatrix(3); - for (int i=0; i<3; ++i) { - for (int j=0; j<3; ++j) { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { Ginv.M[i][j] = Cov.M[i][j]; - for (int k=0; k<5; ++k) { - for (int l=0; l<5; ++l) { + for (int k = 0; k < 5; ++k) { + for (int l = 0; l < 5; ++l) { Ginv.M[i][j] += H[i][k] * helixAtOrigin.C.unsafe_get(k, l) * H[j][l]; } } } } SquareMatrix G = Ginv.fastInvert(); - double [][] K = new double[5][3]; // Kalman gain matrix - for (int i=0; i<5; ++i) { - for (int j=0; j<3; ++j) { - for (int k=0; k<5; ++k) { - for (int l=0; l<3; ++l) { + double[][] K = new double[5][3]; // Kalman gain matrix + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 3; ++j) { + for (int k = 0; k < 5; ++k) { + for (int l = 0; l < 3; ++l) { K[i][j] += helixAtOrigin.C.unsafe_get(i, k) * H[l][k] * G.M[l][j]; } } @@ -889,24 +1045,24 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { G.print("G"); matrixPrint("K", K, 5, 3); } - double [] newHelixParms = new double[5]; - double [][] newHelixCov = new double[5][5]; - for (int i=0; i<5; ++i) { + double[] newHelixParms = new double[5]; + double[][] newHelixCov = new double[5][5]; + for (int i = 0; i < 5; ++i) { newHelixParms[i] = helixAtOrigin.a.v[i]; - for (int j=0; j<3; ++j) { + for (int j = 0; j < 3; ++j) { newHelixParms[i] += K[i][j] * (vtx[j] - pntDOCA.v[j]); } - for (int j=0; j<5; ++j) { + for (int j = 0; j < 5; ++j) { newHelixCov[i][j] = helixAtOrigin.C.unsafe_get(i, j); - for (int k=0; k<3; ++k) { - for (int l=0; l<5; ++l) { + for (int k = 0; k < 3; ++k) { + for (int l = 0; l < 5; ++l) { newHelixCov[i][j] -= K[i][k] * H[k][l] * helixAtOrigin.C.unsafe_get(l, j); } } } } // Calculate the chi-squared contribution - Vec newHelix = new Vec(5,newHelixParms); + Vec newHelix = new Vec(5, newHelixParms); phi = phiDOCA(newHelix, v, X0, alpha); SquareMatrix CovInv = Cov.invert(); pntDOCA = HelixState.atPhi(X0, newHelix, phi, alpha); @@ -918,10 +1074,10 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { solver.setA(helixAtOrigin.C); solver.invert(Cinv); SquareMatrix CinvS = mToS(Cinv); - for (int i=0; i<5; ++i) { - for (int j=0; j<5; ++j) { - for (int k=0; k<3; ++k) { - for (int l=0; l<3; ++l) { + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + for (int k = 0; k < 3; ++k) { + for (int l = 0; l < 3; ++l) { CinvS.M[i][j] += H[k][i] * Vinv.M[k][l] * H[l][j]; } } @@ -929,11 +1085,11 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { } SquareMatrix Calt = CinvS.invert(); Calt.print("Alternative filtered covariance"); - double [][] Kp = new double[5][3]; - for (int i=0; i<5; ++i) { - for (int j=0; j<3; ++j) { - for (int k=0; k<5; ++k) { - for (int l=0; l<3; ++l) { + double[][] Kp = new double[5][3]; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 3; ++j) { + for (int k = 0; k < 5; ++k) { + for (int l = 0; l < 3; ++l) { Kp[i][j] += Calt.M[i][k] * H[l][k] * Vinv.M[l][j]; } } @@ -943,55 +1099,59 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { } return new HelixState(newHelix, X0, helixAtOrigin.origin, new DMatrixRMaj(newHelixCov), helixAtOrigin.B, helixAtOrigin.tB); } - + public double chi2incOrigin() { return chi2incVtx; } - - private static void matrixPrint(String s, double [][] A, int M, int N) { + + private static void matrixPrint(String s, double[][] A, int M, int N) { System.out.format("Dump of %d by %d matrix %s:\n", M, N, s); - for (int i=0; i= x2) { - Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING,"rtsafe: initial guess needs to be bracketed."); + Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING, "rtsafe: initial guess needs to be bracketed."); return xGuess; } fl = fDOCA(x1, a, v, X0, alpha); fh = fDOCA(x2, a, v, X0, alpha); int nTry = 0; - while (fl*fh > 0.0) { + while (fl * fh > 0.0) { if (nTry == 5) { - Logger.getLogger(KalTrack.class.getName()).log(Level.FINE,String.format("Root is not bracketed in zero finding, fl=%12.5e, fh=%12.5e, alpha=%10.6f, x1=%12.5f x2=%12.5f xGuess=%12.5f", + Logger.getLogger(KalTrack.class.getName()).log(Level.FINE, String.format("Root is not bracketed in zero finding, fl=%12.5e, fh=%12.5e, alpha=%10.6f, x1=%12.5f x2=%12.5f xGuess=%12.5f", fl, fh, alpha, x1, x2, xGuess)); return xGuess; } @@ -1018,8 +1178,12 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V nTry++; } //if (nTry > 0) System.out.format("KalTrack.rtsafe: %d tries needed to bracket solution.\n", nTry); - if (fl == 0.) return x1; - if (fh == 0.) return x2; + if (fl == 0.) { + return x1; + } + if (fh == 0.) { + return x2; + } if (fl < 0.0) { xl = x1; xh = x2; @@ -1031,19 +1195,23 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V dxold = Math.abs(x2 - x1); dx = dxold; f = fDOCA(rts, a, v, X0, alpha); - df = dfDOCAdPhi(rts,a, v, X0, alpha); + df = dfDOCAdPhi(rts, a, v, X0, alpha); for (int j = 1; j <= MAXIT; j++) { if ((((rts - xh) * df - f) * ((rts - xl) * df - f) > 0.0) || (Math.abs(2.0 * f) > Math.abs(dxold * df))) { dxold = dx; dx = 0.5 * (xh - xl); // Use bisection if the Newton-Raphson method is going bonkers rts = xl + dx; - if (xl == rts) return rts; + if (xl == rts) { + return rts; + } } else { dxold = dx; dx = f / df; // Newton-Raphson method temp = rts; rts -= dx; - if (temp == rts) return rts; + if (temp == rts) { + return rts; + } } if (Math.abs(dx) < xacc) { // System.out.format("KalTrack.rtSafe: solution converged in %d iterations.\n", @@ -1051,40 +1219,44 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V return rts; } f = fDOCA(rts, a, v, X0, alpha); - df = dfDOCAdPhi(rts,a, v, X0, alpha); + df = dfDOCAdPhi(rts, a, v, X0, alpha); if (f < 0.0) { xl = rts; } else { xh = rts; } } - Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING,"rtsafe: maximum number of iterations exceeded."); + Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING, "rtsafe: maximum number of iterations exceeded."); return rts; } - + /** - * Function that is zero when the helix turning angle phi is at the point of closest approach to v - * @param phi turning angle - * @param a helix parameters - * @param v point of interest - * @param X0 helix pivot point - * @param alpha magnetic field constant - * @return deviation from zero + * Function that is zero when the helix turning angle phi is at the point of + * closest approach to v + * + * @param phi turning angle + * @param a helix parameters + * @param v point of interest + * @param X0 helix pivot point + * @param alpha magnetic field constant + * @return deviation from zero */ private static double fDOCA(double phi, Vec a, Vec v, Vec X0, double alpha) { Vec t = tangentVec(a, phi, alpha); Vec x = HelixState.atPhi(X0, a, phi, alpha); return (v.dif(x)).dot(t); } - + /** - * derivative of the fDOCA function with respect to phi, for the zero-finding algorithm - * @param phi turning angle - * @param a helix parameters - * @param v point of interest - * @param X0 helix pivot point - * @param alpha magnetic field constant - * @return derivative + * derivative of the fDOCA function with respect to phi, for the + * zero-finding algorithm + * + * @param phi turning angle + * @param a helix parameters + * @param v point of interest + * @param X0 helix pivot point + * @param alpha magnetic field constant + * @return derivative */ private static double dfDOCAdPhi(double phi, Vec a, Vec v, Vec X0, double alpha) { Vec x = HelixState.atPhi(X0, a, phi, alpha); @@ -1095,59 +1267,65 @@ private static double dfDOCAdPhi(double phi, Vec a, Vec v, Vec X0, double alpha) double dfdphi = -t.dot(dxdphi) + dfdt.dot(dtdphi); return dfdphi; } - + /** - * Derivatives of position along a helix with respect to the turning angle phi - * @param a helix parameters - * @param phi turning angle - * @param alpha magnetic field parameter - * @return derivative + * Derivatives of position along a helix with respect to the turning angle + * phi + * + * @param a helix parameters + * @param phi turning angle + * @param alpha magnetic field parameter + * @return derivative */ private static Vec dXdPhi(Vec a, double phi, double alpha) { return new Vec((alpha / a.v[2]) * FastMath.sin(a.v[1] + phi), -(alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), -(alpha / a.v[2]) * a.v[4]); } - + /** * A vector tangent to the helix 'a' at the alpha phi - * @param a helix parameters - * @param phi turning angle - * @param alpha magnetic field parameter - * @return tangent vector + * + * @param a helix parameters + * @param phi turning angle + * @param alpha magnetic field parameter + * @return tangent vector */ private static Vec tangentVec(Vec a, double phi, double alpha) { - return new Vec((alpha/a.v[2])*FastMath.sin(a.v[1]+phi), -(alpha/a.v[2])*FastMath.cos(a.v[1]+phi), -(alpha/a.v[2])*a.v[4]); + return new Vec((alpha / a.v[2]) * FastMath.sin(a.v[1] + phi), -(alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), -(alpha / a.v[2]) * a.v[4]); } - + private static Vec dTangentVecDphi(Vec a, double phi, double alpha) { - return new Vec((alpha/a.v[2])*FastMath.cos(a.v[1]+phi), (alpha/a.v[2])*FastMath.sin(a.v[2]+phi), 0.); + return new Vec((alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), (alpha / a.v[2]) * FastMath.sin(a.v[2] + phi), 0.); } - + /** - * /Derivative matrix for the helix 'a' point of closet approach to point 'v' - * @param a helix parameters - * @param v 3D point for which we are finding the DOCA (the "measurement" point) - * @param X0 pivot point of helix - * @param phi angle along helix to the point of closet approach - * @param alpha constant to convert from curvature to 1/pt - * @return derivative matrix + * /Derivative matrix for the helix 'a' point of closet approach to point + * 'v' + * + * @param a helix parameters + * @param v 3D point for which we are finding the DOCA (the "measurement" + * point) + * @param X0 pivot point of helix + * @param phi angle along helix to the point of closet approach + * @param alpha constant to convert from curvature to 1/pt + * @return derivative matrix */ - private static double [][] buildH(Vec a, Vec v, Vec X0, double phi, double alpha) { + private static double[][] buildH(Vec a, Vec v, Vec X0, double phi, double alpha) { Vec x = HelixState.atPhi(X0, a, phi, alpha); Vec dxdphi = dXdPhi(a, phi, alpha); Vec t = tangentVec(a, phi, alpha); Vec dtdphi = dTangentVecDphi(a, phi, alpha); Vec dfdt = v.dif(x); double dfdphi = -t.dot(dxdphi) + dfdt.dot(dtdphi); - double [][] dtda = new double[3][5]; - dtda[0][1] = (alpha/a.v[2])*FastMath.cos(a.v[1]+phi); - dtda[0][2] = (-alpha/(a.v[2]*a.v[2]))*FastMath.sin(a.v[1]+phi); - dtda[1][1] = (alpha/a.v[2])*FastMath.sin(a.v[1]+phi); - dtda[1][2] = (alpha/(a.v[2]*a.v[2]))*FastMath.cos(a.v[1]+phi); - dtda[2][2] = (alpha/(a.v[2]*a.v[2]))*a.v[4]; - dtda[2][4] = -alpha/a.v[2]; - - double [][] dxda = new double[3][5]; + double[][] dtda = new double[3][5]; + dtda[0][1] = (alpha / a.v[2]) * FastMath.cos(a.v[1] + phi); + dtda[0][2] = (-alpha / (a.v[2] * a.v[2])) * FastMath.sin(a.v[1] + phi); + dtda[1][1] = (alpha / a.v[2]) * FastMath.sin(a.v[1] + phi); + dtda[1][2] = (alpha / (a.v[2] * a.v[2])) * FastMath.cos(a.v[1] + phi); + dtda[2][2] = (alpha / (a.v[2] * a.v[2])) * a.v[4]; + dtda[2][4] = -alpha / a.v[2]; + + double[][] dxda = new double[3][5]; dxda[0][0] = FastMath.cos(a.v[1]); dxda[1][0] = FastMath.sin(a.v[1]); dxda[0][1] = -(a.v[0] + alpha / a.v[2]) * FastMath.sin(a.v[1]) + (alpha / a.v[2]) * FastMath.sin(a.v[1] + phi); @@ -1157,30 +1335,31 @@ private static Vec dTangentVecDphi(Vec a, double phi, double alpha) { dxda[2][2] = (alpha / (a.v[2] * a.v[2])) * a.v[4] * phi; dxda[2][3] = 1.0; dxda[2][4] = -(alpha / a.v[2]) * phi; - + Vec dfda = new Vec(5); Vec dphida = new Vec(5); - for (int i=0; i<5; ++i) { - for (int j=0; j<3; ++j) { - dfda.v[i] += -t.v[j]*dxda[j][i] + dfdt.v[j]*dtda[j][i]; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 3; ++j) { + dfda.v[i] += -t.v[j] * dxda[j][i] + dfdt.v[j] * dtda[j][i]; } - dphida.v[i] = -dfda.v[i]/dfdphi; + dphida.v[i] = -dfda.v[i] / dfdphi; } - - double [][] H = new double[3][5]; - for (int i=0; i<3; ++i) { - for (int j=0; j<5; ++j) { - H[i][j] = dxdphi.v[i]*dphida.v[j] + dxda[i][j]; - } + + double[][] H = new double[3][5]; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 5; ++j) { + H[i][j] = dxdphi.v[i] * dphida.v[j] + dxda[i][j]; + } } - + return H; } /** * Return the error on one of the helix parameters at the origin - * @param i 0 to 4 to select which parameter - * @return the error estimate + * + * @param i 0 to 4 to select which parameter + * @return the error estimate */ public double helixErr(int i) { return FastMath.sqrt(helixAtOrigin.C.unsafe_get(i, i)); @@ -1188,8 +1367,9 @@ public double helixErr(int i) { /** * Rotate a 3-vector from local field coordinates to global coordinates - * @param x input 3-vector - * @return output 3-vector + * + * @param x input 3-vector + * @return output 3-vector */ public double[] rotateToGlobal(double[] x) { Vec xIn = new Vec(x[0], x[1], x[2]); @@ -1197,9 +1377,10 @@ public double[] rotateToGlobal(double[] x) { } /** - * Rotate a 3-vector from global coordinates to local field coordinates - * @param x input 3-vector - * @return output 3-vector + * Rotate a 3-vector from global coordinates to local field coordinates + * + * @param x input 3-vector + * @return output 3-vector */ public double[] rotateToLocal(double[] x) { Vec xIn = new Vec(x[0], x[1], x[2]); @@ -1207,9 +1388,11 @@ public double[] rotateToLocal(double[] x) { } /** - * Figure out which measurement site on this track points to a given detector module - * @param module silicon module - * @return measurement site + * Figure out which measurement site on this track points to a given + * detector module + * + * @param module silicon module + * @return measurement site */ public int whichSite(SiModule module) { if (lyrMap != null) { @@ -1225,38 +1408,47 @@ public int whichSite(SiModule module) { /** * Sort the measurement sites in order of SVT layer number - * @param ascending true for ascending order, false for descending + * + * @param ascending true for ascending order, false for descending */ public void sortSites(boolean ascending) { - if (ascending) Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); - else Collections.sort(SiteList, MeasurementSite.SiteComparatorDn); + if (ascending) { + Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); + } else { + Collections.sort(SiteList, MeasurementSite.SiteComparatorDn); + } } /** - * Remove a selected hit from a KalTrack object. Try to add a different hit. - * @param site The measurement site of the hit to be removed - * @param mxChi2Inc Maximum chi^2 increment to add another hit in the same layer - * @param mxTdif Maximum time difference of all hits to add another hit in the same layer + * Remove a selected hit from a KalTrack object. Try to add a different hit. + * + * @param site The measurement site of the hit to be removed + * @param mxChi2Inc Maximum chi^2 increment to add another hit in the same + * layer + * @param mxTdif Maximum time difference of all hits to add another hit in + * the same layer * @return */ public boolean removeHit(MeasurementSite site, double mxChi2Inc, double mxTdif) { boolean exchange = false; - if (debug) System.out.format("Event %d track %d remove hit %d on layer %d detector %d\n", - eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + if (debug) { + System.out.format("Event %d track %d remove hit %d on layer %d detector %d\n", + eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + } if (site.hitID < 0) { // This should never happen - logger.log(Level.WARNING, String.format("Event %d track %d, trying to remove nonexistent hit on layer %d detector %d", + logger.log(Level.WARNING, String.format("Event %d track %d, trying to remove nonexistent hit on layer %d detector %d", eventNumber, ID, site.m.Layer, site.m.detector)); return exchange; } if (site.m.hits.get(site.hitID).tracks.contains(this)) { site.m.hits.get(site.hitID).tracks.remove(this); } else { // This should never happen - logger.log(Level.WARNING, String.format("track %d is missing on hit %d track list in layer %d detector %d", + logger.log(Level.WARNING, String.format("track %d is missing on hit %d track list in layer %d detector %d", ID, site.hitID, site.m.Layer, site.m.detector)); } chi2 -= site.chi2inc; nHits--; - reducedChi2 = chi2/(double)nHits; + reducedChi2 = chi2 / (double) nHits; int oldID = site.hitID; site.removeHit(); // Check whether there might be another hit available @@ -1268,91 +1460,117 @@ public boolean removeHit(MeasurementSite site, double mxChi2Inc, double mxTdif) tMax = Math.max(tMax, newHit.time); exchange = true; nHits++; - if (debug) System.out.format("Event %d track %d added hit %d on layer %d detector %d\n", - eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + if (debug) { + System.out.format("Event %d track %d added hit %d on layer %d detector %d\n", + eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + } } else { SiteList.remove(site); } return exchange; } - + /** * Try to add missing hits to the track - * @param data All the SVT data - * @param mxResid Maximum residual - * @param mxChi2inc Maximum chi^2 increase - * @param mxTdif Maximum time difference of all hits, including the new one - * @param verbose true to spew lots of printout - * @return number of hits added + * + * @param data All the SVT data + * @param mxResid Maximum residual + * @param mxChi2inc Maximum chi^2 increase + * @param mxTdif Maximum time difference of all hits, including the new one + * @param verbose true to spew lots of printout + * @return number of hits added */ public int addHits(ArrayList data, double mxResid, double mxChi2inc, double mxTdif, boolean verbose) { - int numAdded = 0; + int numAdded = 0; int numLayers = 14; - if (nHits == numLayers) return numAdded; - - if (verbose) logger.setLevel(Level.FINER); - if (debug) System.out.format("addHits, event %d: trying to add hits to track %d\n", eventNumber, ID); - + if (nHits == numLayers) { + return numAdded; + } + + if (verbose) { + logger.setLevel(Level.FINER); + } + if (debug) { + System.out.format("addHits, event %d: trying to add hits to track %d\n", eventNumber, ID); + } + sortSites(true); if (debug) { String str = String.format("KalTrac.addHits: initial list of sites: "); for (MeasurementSite site : SiteList) { - str = str + String.format("(%d, %d, %d) ",site.m.Layer, site.m.detector, site.hitID); + str = str + String.format("(%d, %d, %d) ", site.m.Layer, site.m.detector, site.hitID); } System.out.format("%s\n", str); } - + ArrayList> moduleList = new ArrayList>(numLayers); for (int lyr = 0; lyr < numLayers; lyr++) { ArrayList modules = new ArrayList(); moduleList.add(modules); } for (SiModule thisSi : data) { - if (thisSi.hits.size() > 0) moduleList.get(thisSi.Layer).add(thisSi); + if (thisSi.hits.size() > 0) { + moduleList.get(thisSi.Layer).add(thisSi); + } } - + ArrayList newSites = new ArrayList(); - for (int idx = 0; idx < SiteList.size()-1; idx++) { - MeasurementSite site = SiteList.get(idx); - if (site.hitID < 0) continue; + for (int idx = 0; idx < SiteList.size() - 1; idx++) { + MeasurementSite site = SiteList.get(idx); + if (site.hitID < 0) { + continue; + } int nxtIdx = -1; - for (int jdx = idx+1; jdx < SiteList.size(); jdx++) { + for (int jdx = idx + 1; jdx < SiteList.size(); jdx++) { if (SiteList.get(jdx).hitID >= 0) { nxtIdx = jdx; break; } } - if (nxtIdx < 0) break; + if (nxtIdx < 0) { + break; + } MeasurementSite nxtSite = SiteList.get(nxtIdx); MeasurementSite siteFrom = site; - for (int lyr=site.m.Layer+1; lyr tMax) tMax = hitTime; - else if (hitTime < tMin) tMin = hitTime; + if (hitTime > tMax) { + tMax = hitTime; + } else if (hitTime < tMin) { + tMin = hitTime; + } break; } } } } - } + } } } if (numAdded > 0) { @@ -1364,37 +1582,49 @@ public int addHits(ArrayList data, double mxResid, double mxChi2inc, d break; } } - if (debug) System.out.format("KalTrack.addHits event %d: added hit %d on layer %d detector %d\n", eventNumber, site.hitID, site.m.Layer, site.m.detector); - if (siteToDelete != null) SiteList.remove(siteToDelete); + if (debug) { + System.out.format("KalTrack.addHits event %d: added hit %d on layer %d detector %d\n", eventNumber, site.hitID, site.m.Layer, site.m.detector); + } + if (siteToDelete != null) { + SiteList.remove(siteToDelete); + } SiteList.add(site); } sortSites(true); if (debug) { String str = String.format("KalTrack.addHits: final list of sites: "); for (MeasurementSite site : SiteList) { - str = str + String.format("(%d, %d, %d) ",site.m.Layer, site.m.detector, site.hitID); + str = str + String.format("(%d, %d, %d) ", site.m.Layer, site.m.detector, site.hitID); } System.out.format("%s\n", str); } } else { - if (debug) System.out.format("KalTrack.addHits: no hits added in event %d to track %d\n", eventNumber, ID); + if (debug) { + System.out.format("KalTrack.addHits: no hits added in event %d to track %d\n", eventNumber, ID); + } } return numAdded; } - + /** - * Re-fit the track - * @param keep true if there might be another recursion after dropping more hits - * @return true if the refit was successful + * Re-fit the track + * + * @param keep true if there might be another recursion after dropping more + * hits + * @return true if the refit was successful */ public boolean fit(boolean keep) { double chi2s = 0.; - if (debug) System.out.format("Entering KalTrack.fit for event %d, track %d\n", eventNumber, ID); + if (debug) { + System.out.format("Entering KalTrack.fit for event %d, track %d\n", eventNumber, ID); + } StateVector sH = SiteList.get(0).aS.copy(); boolean badC = KalmanPatRecHPS.negativeCov(sH.helix.C); if (badC) { - if (debug) System.out.format("KalTrack.fit: negative starting covariance, event %d track %d\n", eventNumber, ID); + if (debug) { + System.out.format("KalTrack.fit: negative starting covariance, event %d track %d\n", eventNumber, ID); + } KalmanPatRecHPS.setInitCov(sH.helix.C, sH.helix.a, false); } else { CommonOps_DDRM.scale(100., sH.helix.C); // Blow up the initial covariance matrix to avoid double counting measurements @@ -1406,42 +1636,56 @@ public boolean fit(boolean keep) { for (int idx = 0; idx < SiteList.size(); idx++) { // Redo all the filter steps MeasurementSite currentSite = SiteList.get(idx); MeasurementSite newSite = new MeasurementSite(currentSite.m.Layer, currentSite.m, kPar); - + boolean allowSharing = false; boolean pickupHits = false; boolean checkBounds = false; - double [] tRange = {-999., 999.}; + double[] tRange = {-999., 999.}; if (newSite.makePrediction(sH, prevMod, currentSite.hitID, allowSharing, pickupHits, checkBounds, tRange, 0) < 0) { - if (debug) System.out.format("KalTrack.fit: event %d, track %d failed to make prediction at layer %d!\n", eventNumber, ID, newSite.m.Layer); + if (debug) { + System.out.format("KalTrack.fit: event %d, track %d failed to make prediction at layer %d!\n", eventNumber, ID, newSite.m.Layer); + } return false; } if (!newSite.filter()) { - if (debug) System.out.format("KalTrack.fit: event %d, track %d failed to filter!\n", eventNumber, ID); + if (debug) { + System.out.format("KalTrack.fit: event %d, track %d failed to filter!\n", eventNumber, ID); + } return false; } if (KalmanPatRecHPS.negativeCov(currentSite.aF.helix.C)) { - if (debug) System.out.format("KalTrack: event %d, ID %d, negative covariance after filtering at layer %d\n", - eventNumber,ID,currentSite.m.Layer); + if (debug) { + System.out.format("KalTrack: event %d, ID %d, negative covariance after filtering at layer %d\n", + eventNumber, ID, currentSite.m.Layer); + } badCov = true; KalmanPatRecHPS.fixCov(currentSite.aF.helix.C, currentSite.aF.helix.a); } if (debug) { - if (newSite.hitID >= 0) chi2f += Math.max(currentSite.chi2inc,0.); + if (newSite.hitID >= 0) { + chi2f += Math.max(currentSite.chi2inc, 0.); + } } newSite.hitID = currentSite.hitID; sH = newSite.aF; - if (debug) System.out.format(" Layer %d hit %d filter, chi^2 increment=%10.5f, a=%s\n", - newSite.m.Layer, newSite.hitID, newSite.chi2inc, newSite.aF.helix.a.toString()); + if (debug) { + System.out.format(" Layer %d hit %d filter, chi^2 increment=%10.5f, a=%s\n", + newSite.m.Layer, newSite.hitID, newSite.chi2inc, newSite.aF.helix.a.toString()); + } prevMod = newSite.m; - if (keep) currentSite.chi2inc = newSite.chi2inc; // Residuals to cut out hits in next recursion + if (keep) { + currentSite.chi2inc = newSite.chi2inc; // Residuals to cut out hits in next recursion + } newSiteList.add(newSite); } if (badCov) { nBadCov[0]++; bad = true; - } - if (debug) System.out.format("KalTrack.fit: Track %d, Fit chi^2 after filtering = %12.4e\n", ID, chi2f); - + } + if (debug) { + System.out.format("KalTrack.fit: Track %d, Fit chi^2 after filtering = %12.4e\n", ID, chi2f); + } + chi2s = 0.; int nNewHits = 0; badCov = false; @@ -1455,18 +1699,20 @@ public boolean fit(boolean keep) { currentSite.smooth(nextSite); } if (currentSite.hitID >= 0) { - chi2s += Math.max(currentSite.chi2inc,0.); + chi2s += Math.max(currentSite.chi2inc, 0.); nNewHits++; } if (KalmanPatRecHPS.negativeCov(currentSite.aS.helix.C)) { - if (debug) System.out.format("KalTrack: event %d, ID %d, negative covariance after smoothing at layer %d\n", - eventNumber,ID,currentSite.m.Layer); + if (debug) { + System.out.format("KalTrack: event %d, ID %d, negative covariance after smoothing at layer %d\n", + eventNumber, ID, currentSite.m.Layer); + } badCov = true; KalmanPatRecHPS.fixCov(currentSite.aS.helix.C, currentSite.aS.helix.a); } if (debug) { - System.out.format(" Layer %d hit %d, smooth, chi^2 increment=%10.5f, a=%s\n", - currentSite.m.Layer, currentSite.hitID, currentSite.chi2inc, currentSite.aS.helix.a.toString()); + System.out.format(" Layer %d hit %d, smooth, chi^2 increment=%10.5f, a=%s\n", + currentSite.m.Layer, currentSite.hitID, currentSite.chi2inc, currentSite.aS.helix.a.toString()); } nextSite = currentSite; } @@ -1474,20 +1720,26 @@ public boolean fit(boolean keep) { nBadCov[1]++; bad = true; } - if (debug) System.out.format("KalTrack.fit: Track %d, Fit chi^2 after smoothing = %12.4e\n", ID, chi2s); + if (debug) { + System.out.format("KalTrack.fit: Track %d, Fit chi^2 after smoothing = %12.4e\n", ID, chi2s); + } if (!keep) { - if (chi2s/(double)nNewHits > reducedChi2*1.2) { - if (debug) System.out.format("KalTrack.fit event %d track %d: fit chisquared=%10.5f is not an improvement. Discard new fit.\n", - eventNumber, ID, chi2s); + if (chi2s / (double) nNewHits > reducedChi2 * 1.2) { + if (debug) { + System.out.format("KalTrack.fit event %d track %d: fit chisquared=%10.5f is not an improvement. Discard new fit.\n", + eventNumber, ID, chi2s); + } return false; } } SiteList = newSiteList; this.chi2 = chi2s; this.nHits = nNewHits; - this.reducedChi2 = chi2s/(double)nNewHits; + this.reducedChi2 = chi2s / (double) nNewHits; propagated = false; - if (debug) System.out.format("Exiting KalTrack.fit for event %d, track %d\n", eventNumber, ID); + if (debug) { + System.out.format("Exiting KalTrack.fit for event %d, track %d\n", eventNumber, ID); + } return true; } @@ -1498,11 +1750,13 @@ void energyConstraint(double E, double sigmaE) { } energyConstrainedHelix = helixAtOrigin.energyConstrained(E, sigmaE); } - + /** - * Derivative matrix for propagating the covariance of the helix parameters to a covariance of momentum - * @param a helix parameters - * @return 2D array of derivatives + * Derivative matrix for propagating the covariance of the helix parameters + * to a covariance of momentum + * + * @param a helix parameters + * @return 2D array of derivatives */ static double[][] DpTOa(Vec a) { double[][] M = new double[3][5]; @@ -1518,9 +1772,11 @@ static double[][] DpTOa(Vec a) { } /** - * Derivative matrix for propagating the covariance of the helix parameter to a - * covariance of the point of closest approach to the origin (i.e. at phi=0) - * @param a helix parameters + * Derivative matrix for propagating the covariance of the helix parameter + * to a covariance of the point of closest approach to the origin (i.e. at + * phi=0) + * + * @param a helix parameters */ static double[][] DxTOa(Vec a) { double[][] M = new double[3][5]; @@ -1533,57 +1789,79 @@ static double[][] DxTOa(Vec a) { } /** - * Comparator function for sorting tracks by quality + * Comparator function for sorting tracks by quality */ static Comparator TkrComparator = new Comparator() { public int compare(KalTrack t1, KalTrack t2) { double penalty1 = 1.0; double penalty2 = 1.0; - if (!t1.SiteList.get(0).aS.helix.goodCov()) penalty1 = 9.9e3; - if (!t2.SiteList.get(0).aS.helix.goodCov()) penalty2 = 9.9e3; + if (!t1.SiteList.get(0).aS.helix.goodCov()) { + penalty1 = 9.9e3; + } + if (!t2.SiteList.get(0).aS.helix.goodCov()) { + penalty2 = 9.9e3; + } - Double chi1 = new Double((penalty1*t1.chi2) / t1.nHits + 300.0*(1.0 - (double)t1.nHits/14.)); - Double chi2 = new Double((penalty2*t2.chi2) / t2.nHits + 300.0*(1.0 - (double)t2.nHits/14.)); + Double chi1 = new Double((penalty1 * t1.chi2) / t1.nHits + 300.0 * (1.0 - (double) t1.nHits / 14.)); + Double chi2 = new Double((penalty2 * t2.chi2) / t2.nHits + 300.0 * (1.0 - (double) t2.nHits / 14.)); return chi1.compareTo(chi2); } }; /** * Transform a DMatrixRMaj object into a Kalman SquareMatrix object - * @param M DMatrixRMaj object - * @return the SquareMatrix equivalent + * + * @param M DMatrixRMaj object + * @return the SquareMatrix equivalent */ static SquareMatrix mToS(DMatrixRMaj M) { SquareMatrix S = new SquareMatrix(M.numRows); - if (M.numCols != M.numRows) return null; - for (int i=0; i [] lyrList; - double [] beamSpot; - double [] vtxSize; - double [] minSeedE; + ArrayList[] lyrList; + double[] beamSpot; + double[] vtxSize; + double[] minSeedE; double edgeTolerance; static final int numLayers = 14; boolean uniformB; - - private int[] Swap = {1,0, 3,2, 5,4, 7,6, 9,8, 11,10, 13,12}; - private String [] tb; + + private int[] Swap = {1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10, 13, 12}; + private String[] tb; private Logger logger; int maxListIter1; double SVTcenter = 505.57; // Location to evaluate the field in case it is assumed uniform - - /** - * Print all the Kalman Tracking parameters values (good idea to call it at the beginning of the run) + + /** + * Print all the Kalman Tracking parameters values (good idea to call it at + * the beginning of the run) */ public void print() { System.out.format("\nKalmanParams: dump of the Kalman pattern recognition cuts and parameters\n"); System.out.println(" (In the case of two values, they refer to the two iterations.)"); System.out.format(" There are %d layers in the tracker.\n", numLayers); - if (uniformB) System.out.format(" The magnetic field is assumed to be uniform!\n"); + if (uniformB) { + System.out.format(" The magnetic field is assumed to be uniform!\n"); + } System.out.format(" Cluster energy cuts for seeds, by layer: "); - for (int lyr=0; lyr(); - } + } // 0 1 2 3 4 5 6 // A S A S A S A S A S A S A S top // 0,1,2,3,4,5,6,7,8,9,10,11,12,13 @@ -195,7 +199,7 @@ public KalmanParams() { addStrategy("SBB0000"); addStrategy("SABS000"); maxListIter1 = 16; // The maximum index for lyrList for the first iteration - + beamSpot = new double[3]; beamSpot[0] = 0.; beamSpot[1] = 0.; @@ -221,261 +225,264 @@ public KalmanParams() { // beamSpot[2] = beamPosKal.v[2]; // } } - + public void setUniformB(boolean input) { logger.config(String.format("Setting the field to be uniform? %b", input)); uniformB = input; } - + public void setLowPhThreshold(double cut) { - if (cut <0. || cut > 1.) { + if (cut < 0. || cut > 1.) { logger.warning(String.format("low pulse-height threshold %10.4f is not valid and is ignored.", cut)); return; } logger.config(String.format("Setting the low-pulse-height threshold to %10.4f", cut)); lowPhThresh = cut; } - + public void setMinSeedEnergy(double minE) { logger.config("Setting the minimum seed energy to " + Double.toString(minE)); - for (int lyr=0; lyr mxTrials) { - logger.log(Level.WARNING,String.format("Number of global iterations %d is not valid and is ignored.", nTrials)); + logger.log(Level.WARNING, String.format("Number of global iterations %d is not valid and is ignored.", nTrials)); return; } logger.log(Level.CONFIG, String.format("Setting the number of global patrec iterations to %d", nTrials)); this.nTrials = nTrials; } - + public void setFirstLayer(int firstLayer) { if (firstLayer != 0 && firstLayer != 2) { - logger.log(Level.WARNING,String.format("First layer of %d is not valid and is ignored.", firstLayer)); + logger.log(Level.WARNING, String.format("First layer of %d is not valid and is ignored.", firstLayer)); return; } logger.log(Level.CONFIG, String.format("Setting the first tracking layer to %d", firstLayer)); this.firstLayer = firstLayer; } - + public void setIterations(int N) { if (N < 1) { - logger.log(Level.WARNING,String.format("%d iterations not allowed.", N)); + logger.log(Level.WARNING, String.format("%d iterations not allowed.", N)); return; } - logger.log(Level.CONFIG,String.format("Setting the number of Kalman Filter iterations to %d.", N)); + logger.log(Level.CONFIG, String.format("Setting the number of Kalman Filter iterations to %d.", N)); nIterations = N; } - + public void setMxChi2Inc(double mxC) { if (mxC <= 1.) { - logger.log(Level.WARNING,String.format("Maximum chi^2 increment must be at least unity. %8.2f not valid.", mxC)); + logger.log(Level.WARNING, String.format("Maximum chi^2 increment must be at least unity. %8.2f not valid.", mxC)); return; } - logger.log(Level.CONFIG,String.format("Maximum chi^2 increment to add a hit to a track to %8.2f.", mxC)); + logger.log(Level.CONFIG, String.format("Maximum chi^2 increment to add a hit to a track to %8.2f.", mxC)); mxChi2Inc = mxC; } - + public void setMxChi2double(double mxDb) { if (mxDb <= 0.) { - logger.log(Level.WARNING,String.format("Maximum chi^2 increment of shared hit of %8.2f not allowed.", mxDb)); + logger.log(Level.WARNING, String.format("Maximum chi^2 increment of shared hit of %8.2f not allowed.", mxDb)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum chi^2 increment of shared hit to %8.2f sigma.", mxDb)); - mxChi2double = mxDb; + logger.log(Level.CONFIG, String.format("Setting the maximum chi^2 increment of shared hit to %8.2f sigma.", mxDb)); + mxChi2double = mxDb; } - + public void setMinChi2IncBad(double mnB) { if (mnB <= 3.0) { - logger.log(Level.WARNING,String.format("Minimum chi^2 increment to remove a bad hit must be at least 3. %8.2f not valid.", mnB)); + logger.log(Level.WARNING, String.format("Minimum chi^2 increment to remove a bad hit must be at least 3. %8.2f not valid.", mnB)); return; } - logger.log(Level.CONFIG,String.format("Setting the minimum chi^2 increment to remove a bad hit to %8.2f.", mnB)); - minChi2IncBad = mnB; + logger.log(Level.CONFIG, String.format("Setting the minimum chi^2 increment to remove a bad hit to %8.2f.", mnB)); + minChi2IncBad = mnB; } - + public void setMxResidShare(double mxSh) { if (mxSh <= 0.) { - logger.log(Level.WARNING,String.format("Maximum residual of shared hit of %8.2f not allowed.", mxSh)); + logger.log(Level.WARNING, String.format("Maximum residual of shared hit of %8.2f not allowed.", mxSh)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum residual for a shared hit to %8.2f sigma.", mxSh)); - mxResidShare = mxSh; + logger.log(Level.CONFIG, String.format("Setting the maximum residual for a shared hit to %8.2f sigma.", mxSh)); + mxResidShare = mxSh; } - + public void setMaxK(double kMx) { if (kMx <= 0.) { - logger.log(Level.WARNING,String.format("Max 1/pt of %8.2f not allowed.", kMx)); + logger.log(Level.WARNING, String.format("Max 1/pt of %8.2f not allowed.", kMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum 1/pt to %8.2f.", kMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum 1/pt to %8.2f.", kMx)); kMax[1] = kMx; - kMax[0] = Math.min(kMax[0], 0.5*kMx); + kMax[0] = Math.min(kMax[0], 0.5 * kMx); } - + void setMinK(double kMn) { if (kMn < 0.) { - logger.log(Level.WARNING,String.format("Min 1/pt of %8.2f not allowed.", kMn)); + logger.log(Level.WARNING, String.format("Min 1/pt of %8.2f not allowed.", kMn)); return; } kMin = kMn; } - + public void setMxResid(double mxR) { if (mxR <= 1.) { - logger.log(Level.WARNING,String.format("Max resid of %8.2f not allowed.", mxR)); + logger.log(Level.WARNING, String.format("Max resid of %8.2f not allowed.", mxR)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum residual to pick up hits to %8.2f sigma.", mxR)); + logger.log(Level.CONFIG, String.format("Setting the maximum residual to pick up hits to %8.2f sigma.", mxR)); mxResid[1] = mxR; - mxResid[0] = Math.min(mxResid[0], 0.5*mxR); + mxResid[0] = Math.min(mxResid[0], 0.5 * mxR); } - + public void setMaxTanL(double tlMx) { if (tlMx <= 0.) { - logger.log(Level.WARNING,String.format("Max seed tan(lambda) of %8.2f not allowed.", tlMx)); + logger.log(Level.WARNING, String.format("Max seed tan(lambda) of %8.2f not allowed.", tlMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum seed tan(lambda) to %8.2f.", tlMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum seed tan(lambda) to %8.2f.", tlMx)); tanlMax[1] = tlMx; - tanlMax[0] = Math.min(tanlMax[0], 0.8*tlMx); + tanlMax[0] = Math.min(tanlMax[0], 0.8 * tlMx); } - + public void setMaxdRho(double dMx) { if (dMx <= 0.0) { - logger.log(Level.WARNING,String.format("Max dRho of %8.2f not allowed.", dMx)); + logger.log(Level.WARNING, String.format("Max dRho of %8.2f not allowed.", dMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum dRho to %8.2f mm.", dMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum dRho to %8.2f mm.", dMx)); dRhoMax[1] = dMx; - dRhoMax[0] = Math.min(dRhoMax[0], 0.6*dMx); + dRhoMax[0] = Math.min(dRhoMax[0], 0.6 * dMx); } - + public void setMaxdZ(double zMx) { if (zMx <= 0.0) { - logger.log(Level.WARNING,String.format("Max dZ of %8.2f not allowed.", zMx)); + logger.log(Level.WARNING, String.format("Max dZ of %8.2f not allowed.", zMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum dz to %8.2f mm.", zMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum dz to %8.2f mm.", zMx)); dzMax[1] = zMx; - dzMax[0] = Math.min(dzMax[0], 0.4*zMx); + dzMax[0] = Math.min(dzMax[0], 0.4 * zMx); } - + public void setMaxChi2(double xMx) { if (xMx <= 0.) { - logger.log(Level.WARNING,String.format("Max chi2 of %8.2f not allowed.", xMx)); + logger.log(Level.WARNING, String.format("Max chi2 of %8.2f not allowed.", xMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum chi^2/hit to %8.2f.", xMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum chi^2/hit to %8.2f.", xMx)); chi2mx1[1] = xMx; - chi2mx1[0] = Math.min(chi2mx1[0], 0.5*xMx); + chi2mx1[0] = Math.min(chi2mx1[0], 0.5 * xMx); } - + public void setMaxChi2Vtx(double xMx) { if (xMx <= 0.) { - logger.log(Level.WARNING,String.format("Max chi2 of %8.2f not allowed.", xMx)); + logger.log(Level.WARNING, String.format("Max chi2 of %8.2f not allowed.", xMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum chi^2 for 5-hit tracks with vertex constraint to %8.2f.", xMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum chi^2 for 5-hit tracks with vertex constraint to %8.2f.", xMx)); mxChi2Vtx = xMx; } - + public void setMinHits(int minH) { if (minH < 5) { - logger.log(Level.WARNING,String.format("Minimum number of hits = %d not allowed.", minH)); + logger.log(Level.WARNING, String.format("Minimum number of hits = %d not allowed.", minH)); return; } - logger.log(Level.CONFIG,String.format("Setting the minimum number of hits to %d.", minH)); + logger.log(Level.CONFIG, String.format("Setting the minimum number of hits to %d.", minH)); minHits1[1] = minH; - minHits1[0] = Math.max(minHits1[0], minH+1); + minHits1[0] = Math.max(minHits1[0], minH + 1); } - + public void setMinStereo(int minS) { if (minS < 3) { - logger.log(Level.WARNING,String.format("Minimum number of stereo hits = %d not allowed.", minS)); + logger.log(Level.WARNING, String.format("Minimum number of stereo hits = %d not allowed.", minS)); return; } - logger.log(Level.CONFIG,String.format("Setting the minimum number of stereo hits to %d.", minS)); + logger.log(Level.CONFIG, String.format("Setting the minimum number of stereo hits to %d.", minS)); minStereo[1] = minS; - minStereo[0] = Math.max(minStereo[0], minS+1); + minStereo[0] = Math.max(minStereo[0], minS + 1); } - + public void setMaxShared(int mxSh) { - logger.log(Level.CONFIG,String.format("Setting the maximum number of shared hits to %d.", mxSh)); + logger.log(Level.CONFIG, String.format("Setting the maximum number of shared hits to %d.", mxSh)); mxShared = mxSh; } - + public void setMaxTimeRange(double mxT) { - logger.log(Level.CONFIG,String.format("Setting the maximum time range for hits on a track to %8.2f ns.", mxT)); + logger.log(Level.CONFIG, String.format("Setting the maximum time range for hits on a track to %8.2f ns.", mxT)); mxTdif = mxT; } - public void setSeedCompThr(double seedComp_Thr) { + public void setSeedCompThr(double seedComp_Thr) { if (seedComp_Thr < 0.) { logger.log(Level.CONFIG, "SeedTracks duplicate removal is turned off."); return; } - logger.log(Level.CONFIG, String.format("Setting the SeedTracks duplicate removal threshold to %f percent.",seedComp_Thr*100.)); + logger.log(Level.CONFIG, String.format("Setting the SeedTracks duplicate removal threshold to %f percent.", seedComp_Thr * 100.)); seedCompThr = seedComp_Thr; } public void setBeamSpotY(double spot) { beamSpot[1] = spot; - logger.log(Level.CONFIG, String.format("Setting the Y beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); + logger.log(Level.CONFIG, String.format("Setting the Y beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); } - + public void setBeamSizeY(double size) { vtxSize[1] = size; - logger.log(Level.CONFIG, String.format("Setting the Y beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); + logger.log(Level.CONFIG, String.format("Setting the Y beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); } + public void setBeamSpotX(double spot) { beamSpot[0] = spot; - logger.log(Level.CONFIG, String.format("Setting the X beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); + logger.log(Level.CONFIG, String.format("Setting the X beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); } - + public void setBeamSizeX(double size) { vtxSize[0] = size; - logger.log(Level.CONFIG, String.format("Setting the X beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); + logger.log(Level.CONFIG, String.format("Setting the X beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); } + public void setBeamSpotZ(double spot) { beamSpot[2] = spot; - logger.log(Level.CONFIG, String.format("Setting the Z beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); + logger.log(Level.CONFIG, String.format("Setting the Z beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); } - + public void setBeamSizeZ(double size) { vtxSize[2] = size; - logger.log(Level.CONFIG, String.format("Setting the Z beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); + logger.log(Level.CONFIG, String.format("Setting the Z beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); } - + public void clrStrategies() { - logger.log(Level.CONFIG,String.format("Clearing all lists of search strategies..")); + logger.log(Level.CONFIG, String.format("Clearing all lists of search strategies..")); lyrList[0].clear(); lyrList[1].clear(); } - + public void setNumSeedIter1(int num) { int n = num; - for (int topBottom=0; topBottom<2; ++topBottom) { + for (int topBottom = 0; topBottom < 2; ++topBottom) { if (n > lyrList[topBottom].size()) { n = lyrList[topBottom].size(); } } logger.config(String.format("The number of seeds used in iteration 1 is set to %d", n)); - maxListIter1 = n-1; + maxListIter1 = n - 1; } - + private boolean addStrategy(String strategy) { - return addStrategy(strategy,"top") && addStrategy(strategy,"bottom"); + return addStrategy(strategy, "top") && addStrategy(strategy, "bottom"); } - + /** - * Add a seed search strategy for the bottom or top tracker - * @param strategy string specifying the strategy - * @param topBottom string specifying "top" or "bottom" - * @return true if successful + * Add a seed search strategy for the bottom or top tracker + * + * @param strategy string specifying the strategy + * @param topBottom string specifying "top" or "bottom" + * @return true if successful */ public boolean addStrategy(String strategy, String topBottom) { if (!(topBottom == "top" || topBottom == "bottom")) { @@ -487,42 +494,44 @@ public boolean addStrategy(String strategy, String topBottom) { return false; } int iTB; - if (topBottom=="top") { + if (topBottom == "top") { iTB = 1; } else { iTB = 0; } int nAxial = 0; int nStereo = 0; - int n=0; - int [] newList = new int[5]; + int n = 0; + int[] newList = new int[5]; String goodChars = "0AaSsBb"; - for (int lyr=0; lyr<7; ++lyr) { + for (int lyr = 0; lyr < 7; ++lyr) { if (goodChars.indexOf(strategy.charAt(lyr)) < 0) { - logger.warning(String.format("Character %c for layer %d in strategy %s is not recognized. Should be 0, A, S, B, a, s, or b", + logger.warning(String.format("Character %c for layer %d in strategy %s is not recognized. Should be 0, A, S, B, a, s, or b", strategy.charAt(lyr), lyr, strategy)); continue; } - if (strategy.charAt(lyr) == '0') continue; + if (strategy.charAt(lyr) == '0') { + continue; + } int nA = n; int nS = n; - if (strategy.charAt(lyr)=='B' || strategy.charAt(lyr)=='b') { - if (topBottom=="top") { - nS=n+1; + if (strategy.charAt(lyr) == 'B' || strategy.charAt(lyr) == 'b') { + if (topBottom == "top") { + nS = n + 1; } else { - nA=n+1; + nA = n + 1; } n += 2; - } else if (strategy.charAt(lyr)=='A' || strategy.charAt(lyr)=='a' || strategy.charAt(lyr)=='S' || strategy.charAt(lyr)=='s') { + } else if (strategy.charAt(lyr) == 'A' || strategy.charAt(lyr) == 'a' || strategy.charAt(lyr) == 'S' || strategy.charAt(lyr) == 's') { n++; } //System.out.format("addStrategy %s: lyr=%d, nA=%d, nS=%d, strategy=%s\n",topBottom,lyr,nA,nS,strategy); - if (strategy.charAt(lyr)=='A' || strategy.charAt(lyr)=='B' || strategy.charAt(lyr)=='a' || strategy.charAt(lyr)=='b') { + if (strategy.charAt(lyr) == 'A' || strategy.charAt(lyr) == 'B' || strategy.charAt(lyr) == 'a' || strategy.charAt(lyr) == 'b') { if (topBottom == "top") { if (nA > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nA] = 2*lyr; // The top tracker begins with an axial layer + newList[nA] = 2 * lyr; // The top tracker begins with an axial layer //System.out.format("addStrategy %s: adding axial element %d, lyr=%d\n", topBottom, nA, 2*lyr); nAxial++; } @@ -530,18 +539,18 @@ public boolean addStrategy(String strategy, String topBottom) { if (nA > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nA] = 2*lyr + 1; // The bottom tracker begins with a stereo layer + newList[nA] = 2 * lyr + 1; // The bottom tracker begins with a stereo layer //System.out.format("addStrategy %s: adding axial element %d, lyr=%d\n", topBottom, nA, 2*lyr+1); nAxial++; } } } - if (strategy.charAt(lyr)=='S' || strategy.charAt(lyr)=='B' || strategy.charAt(lyr)=='s' || strategy.charAt(lyr)=='b') { + if (strategy.charAt(lyr) == 'S' || strategy.charAt(lyr) == 'B' || strategy.charAt(lyr) == 's' || strategy.charAt(lyr) == 'b') { if (topBottom == "top") { if (nS > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nS] = 2*lyr + 1; // The top tracker begins with an axial layer + newList[nS] = 2 * lyr + 1; // The top tracker begins with an axial layer //System.out.format("addStrategy %s: adding stereo element %d, lyr=%d\n", topBottom, nS, 2*lyr+1); nStereo++; } @@ -549,7 +558,7 @@ public boolean addStrategy(String strategy, String topBottom) { if (nS > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nS] = 2*lyr; // The bottom tracker begins with a stereo layer + newList[nS] = 2 * lyr; // The bottom tracker begins with a stereo layer //System.out.format("addStrategy %s: adding stereo element %d, lyr=%d\n", topBottom, nS, 2*lyr); nStereo++; } @@ -557,22 +566,24 @@ public boolean addStrategy(String strategy, String topBottom) { } } if (nAxial != 2 || nStereo != 3) { - logger.log(Level.WARNING,String.format("addStrategy: Invalid search strategy " + strategy + " for topBottom=%s: %d %d %d %d %d", - topBottom, newList[0],newList[1],newList[2],newList[3],newList[4])); + logger.log(Level.WARNING, String.format("addStrategy: Invalid search strategy " + strategy + " for topBottom=%s: %d %d %d %d %d", + topBottom, newList[0], newList[1], newList[2], newList[3], newList[4])); return false; } - for (int [] oldList : lyrList[iTB]) { + for (int[] oldList : lyrList[iTB]) { int nMatch = 0; - for (int i=0; i<5; ++i) { - if (oldList[i] == newList[i]) nMatch++; + for (int i = 0; i < 5; ++i) { + if (oldList[i] == newList[i]) { + nMatch++; + } } if (nMatch == 5) { - logger.log(Level.WARNING,String.format("addStrategy: strategy %s is already in the list", strategy)); + logger.log(Level.WARNING, String.format("addStrategy: strategy %s is already in the list", strategy)); return false; } } - logger.log(Level.CONFIG,String.format("addStrategy: adding search strategy %d %d %d %d %d for %s", - newList[0],newList[1],newList[2],newList[3],newList[4],topBottom)); + logger.log(Level.CONFIG, String.format("addStrategy: adding search strategy %d %d %d %d %d for %s", + newList[0], newList[1], newList[2], newList[3], newList[4], topBottom)); lyrList[iTB].add(newList); return true; } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java index d197795868..893d9d6412 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java @@ -34,7 +34,8 @@ import org.lcsim.geometry.IDDecoder; /** - * Driver for pattern recognition and fitting of HPS tracks using the Kalman Filter + * Driver for pattern recognition and fitting of HPS tracks using the Kalman + * Filter */ public class KalmanPatRecDriver extends Driver { @@ -57,7 +58,7 @@ public class KalmanPatRecDriver extends Driver { private KalmanParams kPar; private KalmanPatRecPlots kPlot; private static Logger logger; - + // Parameters for the Kalman pattern recognition that can be set by the user in the steering file: private ArrayList strategies; // List of seed strategies for both top and bottom trackers, from steering private ArrayList strategiesTop; // List of all the top tracker seed strategies from the steering file @@ -82,7 +83,7 @@ public class KalmanPatRecDriver extends Driver { private int numEvtPlots; // Number of event displays to plot (gnuplot files) private boolean doDebugPlots; // Whether to make all the debugging histograms private int siHitsLimit; // Maximum number of SiClusters in one event allowed for KF pattern reco - // (protection against monster events) + // (protection against monster events) private double seedCompThr; // Threshold for seedTrack helix parameters compatibility private int numStrategyIter1; // Number of seed strategies to use in the first iteration of pattern recognition private double beamPositionZ; // Beam spot location along the beam axis @@ -92,19 +93,20 @@ public class KalmanPatRecDriver extends Driver { private double beamPositionY; private double beamSigmaY; private double lowPhThresh; // Threshold in residual ratio to prefer a low-ph hit over a high-ph hit - private double minSeedEnergy=-1.; // Minimum energy of a hit for it to be used in a pattern recognition seed + private double minSeedEnergy = -1.; // Minimum energy of a hit for it to be used in a pattern recognition seed private boolean useBeamPositionConditions; // True to use beam position from database private boolean useFixedVertexZPosition; // True to override the database just for the z beam position private Level logLevel = Level.WARNING; // Set log level from steering private List sensors = null; // List of tracker sensors - + public String getOutputFullTrackCollectionName() { return outputFullTrackCollectionName; } /** - * Used to change the track collection name from the default "KalmanFullTracks" - * + * Used to change the track collection name from the default + * "KalmanFullTracks" + * * @param input Desired new name for the track collection */ public void setOutputFullTrackCollectionName(String input) { @@ -113,17 +115,20 @@ public void setOutputFullTrackCollectionName(String input) { /** * Optional call to force a change to the amount of debug printing - * - * @param input true to turn on lots of debug printing + * + * @param input true to turn on lots of debug printing */ public void setVerbose(boolean verbose) { this.verbose = verbose; - if (verbose) System.out.println("KalmanPatRecDriver: verbose print output is turned on!"); + if (verbose) { + System.out.println("KalmanPatRecDriver: verbose print output is turned on!"); + } } /** - * Option to treat the B field as uniform. This normally is used only for development and debugging work. - * + * Option to treat the B field as uniform. This normally is used only for + * development and debugging work. + * * @param input Set true to assume a uniform B field. */ public void setUniformB(boolean uniformB) { @@ -137,33 +142,35 @@ public void setMaterialManager(MaterialSupervisor mm) { /** * Determine whether residuals will be added to the event output - * - * @param input set true to force residuals to be output with the event + * + * @param input set true to force residuals to be output with the event */ public void setAddResiduals(boolean addResiduals) { this.addResiduals = addResiduals; System.out.format("KalmanPatRecDriver: residuals will be added to the event output: %b\n", addResiduals); } - + /** - * Do-nothing method. See instead setOutputFullTrackCollectionName. - * - * @param input ignored---the call will do nothing. + * Do-nothing method. See instead setOutputFullTrackCollectionName. + * + * @param input ignored---the call will do nothing. */ public void setTrackCollectionName(String input) { } - + /** - * Set the maximum number of SiClusters in one event allowed for KF pattern recognition (protection against monster events) - * + * Set the maximum number of SiClusters in one event allowed for KF pattern + * recognition (protection against monster events) + * * @param input Maximum number of hits to use */ public void setSiHitsLimit(int input) { siHitsLimit = input; - } + } /** - * Set up the geometry and parameters for the Kalman-filter track finding and fitting. + * Set up the geometry and parameters for the Kalman-filter track finding + * and fitting. */ @Override public void detectorChanged(Detector det) { @@ -172,83 +179,133 @@ public void detectorChanged(Detector det) { logger.setLevel(logLevel); //LogManager.getLogManager().getLogger(Logger.GLOBAL_LOGGER_NAME).setLevel(logLevel); } - verbose = (logger.getLevel()==Level.FINE); + verbose = (logger.getLevel() == Level.FINE); System.out.format("KalmanPatRecDriver: entering detectorChanged, logger level = %s\n", logger.getLevel().getName()); executionTime = 0.; interfaceTime = 0.; plottingTime = 0.; maxTime = 0.; - + _materialManager = new MaterialSupervisor(); _materialManager.buildModel(det); fm = det.getFieldMap(); - + System.out.format("B field map vs y in Kalman coordinates:\n"); double eCalLoc = 1394.; - for (double y=0.; y<1500.; y+=5.) { - double z1=-50.; - double z2= 50.; + for (double y = 0.; y < 1500.; y += 5.) { + double z1 = -50.; + double z2 = 50.; Vec B1 = KalmanInterface.getField(new Vec(0., y, z1), fm); Vec B2 = KalmanInterface.getField(new Vec(0., y, 0.), fm); Vec B3 = KalmanInterface.getField(new Vec(0., y, z2), fm); System.out.format("y=%6.1f z=%6.1f: %s z=0: %s z=%6.1f: %s\n", y, z1, B1.toString(), B2.toString(), z2, B3.toString()); } System.out.format("B field map vs z at ECAL, in Kalman coordinates:\n"); - for (double z=-200.; z<200.; z+=5.) { - double y=eCalLoc; + for (double z = -200.; z < 200.; z += 5.) { + double y = eCalLoc; Vec B = KalmanInterface.getField(new Vec(0., y, z), fm); System.out.format("x=0 y=%6.1f z=%6.1f: %s\n", y, z, B.toString()); } System.out.format("B field map vs x at ECAL, in Kalman coordinates:\n"); - for (double x=-200.; x<200.; x+=5.) { - double y=eCalLoc; - double z=20.; + for (double x = -200.; x < 200.; x += 5.) { + double y = eCalLoc; + double z = 20.; Vec B = KalmanInterface.getField(new Vec(x, y, z), fm); System.out.format("x=%6.1f y=%6.1f z=%6.1f: %s\n", x, y, z, B.toString()); - } - + } + detPlanes = new ArrayList(); List materialVols = ((MaterialSupervisor) (_materialManager)).getMaterialVolumes(); for (ScatteringDetectorVolume vol : materialVols) { detPlanes.add((SiStripPlane) (vol)); } - + sensors = det.getSubdetector("Tracker").getDetectorElement().findDescendants(HpsSiSensor.class); // Instantiate the interface to the Kalman-Filter code and set up the geometry KalmanParams kPar = new KalmanParams(); - + // Change Kalman parameters per settings supplied by the steering file // We assume that if not set by the steering file, then the parameters will have the Java default values for the primitives // Note that all of the parameters have defaults hard coded in KalmanParams.java - if (numPatRecIteration != 0) kPar.setGlbIterations(numPatRecIteration); - if (numKalmanIteration != 0) kPar.setIterations(numKalmanIteration); - if (maxPtInverse != 0.0) kPar.setMaxK(maxPtInverse); - if (maxD0 != 0.0) kPar.setMaxdRho(maxD0); - if (maxZ0 != 0.0) kPar.setMaxdZ(maxZ0); - if (maxChi2 != 0.0) kPar.setMaxChi2(maxChi2); - if (minHits != 0) kPar.setMinHits(minHits); - if (minStereo != 0) kPar.setMinStereo(minStereo); - if (maxSharedHits != 0) kPar.setMaxShared(maxSharedHits); - if (maxTimeRange != 0.0) kPar.setMaxTimeRange(maxTimeRange); - if (maxTanLambda != 0.0) kPar.setMaxTanL(maxTanLambda); - if (maxResidual != 0.0) kPar.setMxResid(maxResidual); - if (maxChi2Inc != 0.0) kPar.setMxChi2Inc(maxChi2Inc); - if (minChi2IncBad != 0.0) kPar.setMinChi2IncBad(minChi2IncBad); - if (maxResidShare != 0.0) kPar.setMxResidShare(maxResidShare); - if (maxChi2IncShare != 0.0) kPar.setMxChi2double(maxChi2IncShare); - if (seedCompThr != 0.0) kPar.setSeedCompThr(seedCompThr); - if (beamPositionZ != 0.0) kPar.setBeamSpotY(beamPositionZ); - if (beamSigmaZ != 0.0) kPar.setBeamSizeY(beamSigmaZ); - if (beamPositionX != 0.0) kPar.setBeamSpotX(beamPositionX); - if (beamSigmaX != 0.0) kPar.setBeamSizeX(beamSigmaX); - if (beamPositionY != 0.0) kPar.setBeamSpotZ(-beamPositionY); - if (beamSigmaY != 0.0) kPar.setBeamSizeZ(beamSigmaY); - if (mxChi2Vtx != 0.0) kPar.setMaxChi2Vtx(mxChi2Vtx); - if (minSeedEnergy >= 0.) kPar.setMinSeedEnergy(minSeedEnergy); + if (numPatRecIteration != 0) { + kPar.setGlbIterations(numPatRecIteration); + } + if (numKalmanIteration != 0) { + kPar.setIterations(numKalmanIteration); + } + if (maxPtInverse != 0.0) { + kPar.setMaxK(maxPtInverse); + } + if (maxD0 != 0.0) { + kPar.setMaxdRho(maxD0); + } + if (maxZ0 != 0.0) { + kPar.setMaxdZ(maxZ0); + } + if (maxChi2 != 0.0) { + kPar.setMaxChi2(maxChi2); + } + if (minHits != 0) { + kPar.setMinHits(minHits); + } + if (minStereo != 0) { + kPar.setMinStereo(minStereo); + } + if (maxSharedHits != 0) { + kPar.setMaxShared(maxSharedHits); + } + if (maxTimeRange != 0.0) { + kPar.setMaxTimeRange(maxTimeRange); + } + if (maxTanLambda != 0.0) { + kPar.setMaxTanL(maxTanLambda); + } + if (maxResidual != 0.0) { + kPar.setMxResid(maxResidual); + } + if (maxChi2Inc != 0.0) { + kPar.setMxChi2Inc(maxChi2Inc); + } + if (minChi2IncBad != 0.0) { + kPar.setMinChi2IncBad(minChi2IncBad); + } + if (maxResidShare != 0.0) { + kPar.setMxResidShare(maxResidShare); + } + if (maxChi2IncShare != 0.0) { + kPar.setMxChi2double(maxChi2IncShare); + } + if (seedCompThr != 0.0) { + kPar.setSeedCompThr(seedCompThr); + } + if (beamPositionZ != 0.0) { + kPar.setBeamSpotY(beamPositionZ); + } + if (beamSigmaZ != 0.0) { + kPar.setBeamSizeY(beamSigmaZ); + } + if (beamPositionX != 0.0) { + kPar.setBeamSpotX(beamPositionX); + } + if (beamSigmaX != 0.0) { + kPar.setBeamSizeX(beamSigmaX); + } + if (beamPositionY != 0.0) { + kPar.setBeamSpotZ(-beamPositionY); + } + if (beamSigmaY != 0.0) { + kPar.setBeamSizeZ(beamSigmaY); + } + if (mxChi2Vtx != 0.0) { + kPar.setMaxChi2Vtx(mxChi2Vtx); + } + if (minSeedEnergy >= 0.) { + kPar.setMinSeedEnergy(minSeedEnergy); + } kPar.setUniformB(uniformB); - + // Here we set the seed strategies for the pattern recognition if (strategies != null || (strategiesTop != null && strategiesBot != null)) { logger.config("The Kalman pattern recognition seed strategies are being set from the steering file"); @@ -278,29 +335,34 @@ public void detectorChanged(Detector det) { kPar.setNumSeedIter1(nA + nT); kPar.setNumSeedIter1(nA + nB); } - if (numStrategyIter1 != 0) kPar.setNumSeedIter1(numStrategyIter1); - + if (numStrategyIter1 != 0) { + kPar.setNumSeedIter1(numStrategyIter1); + } + // Setup optional usage of beam positions from database. final DatabaseConditionsManager mgr = DatabaseConditionsManager.getInstance(); if (useBeamPositionConditions && mgr.hasConditionsRecord("beam_positions")) { logger.config("Using Kalman beam position from the conditions database"); - BeamPositionCollection beamPositions = - mgr.getCachedConditions(BeamPositionCollection.class, "beam_positions").getCachedData(); - BeamPosition beamPositionCond = beamPositions.get(0); - if (!useFixedVertexZPosition) kPar.setBeamSpotY(beamPositionCond.getPositionZ()); - else logger.config("Using fixed Kalman beam Z position: " + kPar.beamSpot[1]); + BeamPositionCollection beamPositions + = mgr.getCachedConditions(BeamPositionCollection.class, "beam_positions").getCachedData(); + BeamPosition beamPositionCond = beamPositions.get(0); + if (!useFixedVertexZPosition) { + kPar.setBeamSpotY(beamPositionCond.getPositionZ()); + } else { + logger.config("Using fixed Kalman beam Z position: " + kPar.beamSpot[1]); + } kPar.setBeamSpotX(beamPositionCond.getPositionX()); // Includes a transformation to Kalman coordinates kPar.setBeamSpotZ(-beamPositionCond.getPositionY()); } else { logger.config("Using Kalman beam position from the steering file or default"); } logger.config("Using Kalman beam position [ Z, X, Y ]: " + String.format("[ %f, %f, %f ]", - kPar.beamSpot[0], -kPar.beamSpot[2], kPar.beamSpot[1]) + " in HPS coordinates."); - + kPar.beamSpot[0], -kPar.beamSpot[2], kPar.beamSpot[1]) + " in HPS coordinates."); + logger.config(String.format("KalmanPatRecDriver: the B field is assumed uniform? %b\n", uniformB)); logger.config("KalmanPatRecDriver: done with configuration changes."); kPar.print(); - + KI = new KalmanInterface(kPar, fm); KI.setSiHitsLimit(siHitsLimit); KI.createSiModules(detPlanes); @@ -311,54 +373,55 @@ public void detectorChanged(Detector det) { } /** - * Top level method called for each event, to do the Kalman-filter tracking finding and fitting - * - * @param event input the header for this event + * Top level method called for each event, to do the Kalman-filter tracking + * finding and fitting + * + * @param event input the header for this event */ @Override public void process(EventHeader event) { - + List outputFullTracks = new ArrayList(); - + //For additional track information List trackDataCollection = new ArrayList(); List trackDataRelations = new ArrayList(); - + //For GBL Refitting List allClstrs = new ArrayList(); - List gblStripClusterDataRelations = new ArrayList(); - + List gblStripClusterDataRelations = new ArrayList(); + //For hit-on-track residuals information List trackResiduals = new ArrayList(); List trackResidualsRelations = new ArrayList(); - + ArrayList[] kPatList = prepareTrackCollections(event, outputFullTracks, trackDataCollection, trackDataRelations, allClstrs, gblStripClusterDataRelations, trackResiduals, trackResidualsRelations); - + int flag = 1 << LCIOConstants.TRBIT_HITS; event.put(outputFullTrackCollectionName, outputFullTracks, Track.class, flag); event.put("KFGBLStripClusterData", allClstrs, GBLStripClusterData.class, flag); event.put("KFGBLStripClusterDataRelations", gblStripClusterDataRelations, LCRelation.class, flag); - event.put("KFTrackData",trackDataCollection, TrackData.class,0); - event.put("KFTrackDataRelations",trackDataRelations,LCRelation.class,0); - + event.put("KFTrackData", trackDataCollection, TrackData.class, 0); + event.put("KFTrackDataRelations", trackDataRelations, LCRelation.class, 0); + if (addResiduals) { - event.put("KFUnbiasRes", trackResiduals, TrackResidualsData.class,0); - event.put("KFUnbiasResRelations",trackResidualsRelations, LCRelation.class,0); + event.put("KFUnbiasRes", trackResiduals, TrackResidualsData.class, 0); + event.put("KFUnbiasResRelations", trackResidualsRelations, LCRelation.class, 0); } if (kPlot != null) { long startTime = System.nanoTime(); - + kPlot.process(event, sensors, kPatList, outputFullTracks); long endPlottingTime = System.nanoTime(); - double runTime = (double)(endPlottingTime - startTime)/1000000.; - plottingTime += runTime; + double runTime = (double) (endPlottingTime - startTime) / 1000000.; + plottingTime += runTime; } - + KI.clearInterface(); logger.log(Level.FINE, String.format("\n KalmanPatRecDriver.process: Done with event %d", event.getEventNumber())); } - + class SortByZ implements Comparator> { @Override @@ -366,42 +429,45 @@ public int compare(Pair o1, Pair o2) { return (int) (o1.getSecondElement()[2] - o2.getSecondElement()[2]); } } - + class SortByZ2 implements Comparator { - + @Override - public int compare(TrackerHit o1, TrackerHit o2) { + public int compare(TrackerHit o1, TrackerHit o2) { return (int) (o1.getPosition()[2] - o2.getPosition()[2]); } } /** - * Execute the Kalman pattern recognition. All but the first argument are outputs, but the calling routine has to - * supply the empty data structures. - * - * @param event input the header for this event - * @param outputFullTracks output the list of HPS tracks - * @param trackDataCollection output list of data that go with the tracks - * @param trackDataRelations output the relations between tracks and the data - * @param allClstrs output all the clusters needed for refitting the Kalman tracks by GBL - * @param gblStripClusterDataRelations output relations for the clusters - * @param trackResiduals output the residuals for hits on Kalman tracks - * @param trackResidualsRelations output relations for the residuals - * @return Two lists of native Kalman tracks, one for each of top and bottom detectors + * Execute the Kalman pattern recognition. All but the first argument are + * outputs, but the calling routine has to supply the empty data structures. + * + * @param event input the header for this event + * @param outputFullTracks output the list of HPS tracks + * @param trackDataCollection output list of data that go with the tracks + * @param trackDataRelations output the relations between tracks and the + * data + * @param allClstrs output all the clusters needed for refitting the Kalman + * tracks by GBL + * @param gblStripClusterDataRelations output relations for the clusters + * @param trackResiduals output the residuals for hits on Kalman tracks + * @param trackResidualsRelations output relations for the residuals + * @return Two lists of native Kalman tracks, one for each of top and bottom + * detectors */ private ArrayList[] prepareTrackCollections(EventHeader event, List outputFullTracks, List trackDataCollection, List trackDataRelations, List allClstrs, List gblStripClusterDataRelations, List trackResiduals, List trackResidualsRelations) { - + int evtNumb = event.getEventNumber(); String stripHitInputCollectionName = "StripClusterer_SiTrackerHitStrip1D"; if (!event.hasCollection(TrackerHit.class, stripHitInputCollectionName)) { System.out.format("KalmanPatRecDriver.process:" + stripHitInputCollectionName + " does not exist; skipping event %d\n", evtNumb); return null; } - + long startTime = System.nanoTime(); ArrayList[] kPatList = KI.KalmanPatRec(event, decoder); long endTime = System.nanoTime(); - double runTime = (double)(endTime - startTime)/1000000.; + double runTime = (double) (endTime - startTime) / 1000000.; executionTime += runTime; if (verbose) { if (runTime > 200.) { @@ -410,45 +476,49 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List maxTime) maxTime = runTime; + if (runTime > maxTime) { + maxTime = runTime; + } nEvents++; - logger.log(Level.FINE,"KalmanPatRecDriver.process: run time for pattern recognition at event "+evtNumb+" is "+runTime+" milliseconds"); - + logger.log(Level.FINE, "KalmanPatRecDriver.process: run time for pattern recognition at event " + evtNumb + " is " + runTime + " milliseconds"); + //List rawhits = event.get(RawTrackerHit.class, "SVTRawTrackerHits"); //if (rawhits == null) { // logger.log(Level.FINE, String.format("KalmanPatRecDriver.process: the raw hits collection is missing")); // return null; //} - int nKalTracks = 0; - for (int topBottom=0; topBottom<2; ++topBottom) { + for (int topBottom = 0; topBottom < 2; ++topBottom) { ArrayList kPat = kPatList[topBottom]; if (kPat.size() == 0) { logger.log(Level.FINE, String.format("KalmanPatRecDriver.process: pattern recognition failed to find tracks in tracker %d for event %d.", topBottom, evtNumb)); } for (KalTrack kTk : kPat) { - if (verbose) kTk.print(String.format(" PatRec for topBot=%d ",topBottom)); - double [][] covar = kTk.originCovariance(); - for (int ix=0; ix<5; ++ix) { - for (int iy=0; iy<5; ++iy) { + if (verbose) { + kTk.print(String.format(" PatRec for topBot=%d ", topBottom)); + } + double[][] covar = kTk.originCovariance(); + for (int ix = 0; ix < 5; ++ix) { + for (int iy = 0; iy < 5; ++iy) { if (Double.isNaN(covar[ix][iy])) { - logger.log(Level.FINE, String.format("KalmanPatRecDriver.process event %d: NaN at %d %d in covariance for track %d",evtNumb,ix,iy,kTk.ID)); + logger.log(Level.FINE, String.format("KalmanPatRecDriver.process event %d: NaN at %d %d in covariance for track %d", evtNumb, ix, iy, kTk.ID)); } } - } + } nKalTracks++; - + //Here is where the tracks to be persisted are formed Track KalmanTrackHPS = KI.createTrack(kTk, true); - if (KalmanTrackHPS == null) continue; - + if (KalmanTrackHPS == null) { + continue; + } + //pT cut //double [] hParams_check = kTk.originHelixParms(); //double ptInv_check = hParams_check[2]; //double pt = Math.abs(1./ptInv_check); - outputFullTracks.add(KalmanTrackHPS); - + // Create clusters that can be used to refit the Kalman track using GBL List clstrs = KI.createGBLStripClusterData(kTk); if (verbose) { @@ -456,68 +526,70 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List layers = new ArrayList(); - List residuals = new ArrayList(); - List sigmas = new ArrayList(); - - for (int ilay = 0; ilay<14; ilay++) { - Pair res_and_sigma = kTk.unbiasedResidual(ilay); - if (res_and_sigma.getSecondElement() > -1.) { + List layers = new ArrayList(); + List residuals = new ArrayList(); + List sigmas = new ArrayList(); + + for (int ilay = 0; ilay < 14; ilay++) { + Pair res_and_sigma = kTk.unbiasedResidual(ilay); + if (res_and_sigma.getSecondElement() > -1.) { layers.add(ilay); residuals.add(res_and_sigma.getFirstElement()); sigmas.add(res_and_sigma.getSecondElement().floatValue()); } } // End loop on layers - - TrackResidualsData resData = new TrackResidualsData(trackerVolume,layers,residuals,sigmas); + + TrackResidualsData resData = new TrackResidualsData(trackerVolume, layers, residuals, sigmas); trackResiduals.add(resData); - trackResidualsRelations.add(new BaseLCRelation(resData,KalmanTrackHPS)); + trackResidualsRelations.add(new BaseLCRelation(resData, KalmanTrackHPS)); /* if (KalmanTrackHPS.getTrackerHits().size() != residuals.size()) { System.out.println("KalmanPatRecDriver::Residuals consistency check failed."); System.out.printf("Track has %d hits while I have %d residuals \n", KalmanTrackHPS.getTrackerHits().size(), residuals.size()); } - */ - + */ + } // end of loop on tracks } // end of loop on trackers - + nTracks += nKalTracks; - + long endInterfaceTime = System.nanoTime(); - runTime = (double)(endInterfaceTime - endTime)/1000000.; + runTime = (double) (endInterfaceTime - endTime) / 1000000.; interfaceTime += runTime; - + return kPatList; } @@ -526,118 +598,149 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List(); @@ -645,6 +748,7 @@ public void setSeedStrategy(String seedStrategy) { strategies.add(seedStrategy); System.out.format("KalmanPatRecDriver: top and bottom strategy %s specified by steering.\n", seedStrategy); } + public void setSeedStrategyTop(String seedStrategy) { if (strategiesTop == null) { strategiesTop = new ArrayList(); @@ -652,6 +756,7 @@ public void setSeedStrategyTop(String seedStrategy) { strategiesTop.add(seedStrategy); System.out.format("KalmanPatRecDriver: top strategy %s specified by steering.\n", seedStrategy); } + public void setSeedStrategyBottom(String seedStrategy) { if (strategiesBot == null) { strategiesBot = new ArrayList(); @@ -659,9 +764,10 @@ public void setSeedStrategyBottom(String seedStrategy) { strategiesBot.add(seedStrategy); System.out.format("KalmanPatRecDriver: bottom strategy %s specified by steering.\n", seedStrategy); } + public void setLogLevel(String logLevel) { System.out.format("KalmanPatRecDriver: setting the logger level to %s\n", logLevel); this.logLevel = Level.parse(logLevel); - System.out.format(" logger level = %s\n",this.logLevel.getName()); + System.out.format(" logger level = %s\n", this.logLevel.getName()); } } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java index b544ba4628..039614e640 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java @@ -5,21 +5,22 @@ import org.apache.commons.math.util.FastMath; /** - * Runge-Kutta propagation through the detector, including Gaussian MCS at silicon planes. - * This code is only to help with internal testing of the Kalman package and is not part of the fitting or pattern recognition. + * Runge-Kutta propagation through the detector, including Gaussian MCS at + * silicon planes. This code is only to help with internal testing of the Kalman + * package and is not part of the fitting or pattern recognition. */ class RKhelix { Vec x; // point on the track Vec p; // momentum of the track at point x double Q; // charge of the particle - + private org.lcsim.geometry.FieldMap fM; private Random rndm; private HelixPlaneIntersect hpi; private double rho; private double radLen; - + RKhelix(Vec x, Vec p, double Q, org.lcsim.geometry.FieldMap fM, Random rndm) { this.rndm = rndm; this.fM = fM; @@ -30,13 +31,13 @@ class RKhelix { rho = 2.329; // Density of silicon in g/cm^2 radLen = (21.82 / rho) * 10.0; // Radiation length of silicon in millimeters } - + RKhelix propagateRK(Plane pln) { Vec newP = new Vec(3); Vec newX = planeIntersect(pln, newP); return new RKhelix(newX, newP, Q, fM, rndm); } - + Vec planeIntersect(Plane pln, Vec pInt) { // phi value where the helix intersects the plane P (given in global coordinates) return hpi.rkIntersect(pln, x, p, Q, fM, pInt); } @@ -44,7 +45,7 @@ Vec planeIntersect(Plane pln, Vec pInt) { // phi value where the helix intersect void print(String s) { System.out.format("RKhelix %s: x=%s, p=%s, Q=%3.1f\n", s, x.toString(), p.toString(), Q); } - + // Get parameters for the helix passing through the point x. // pivotF is the pivot point in the helix field reference system. // Input "pivot", the desired pivot point in global coordinates. This will be the origin of the field reference system. @@ -58,16 +59,16 @@ Vec helixParameters(Vec pivot, Vec pivotF) { Vec helixAtX = HelixState.pTOa(pF, 0., 0., Q); // Helix with pivot at x in field frame // Transform the desired pivot point into the field frame Vec pivotTrans = Rot.rotate(pivot.dif(x)); - for (int i=0; i<3; ++i) { + for (int i = 0; i < 3; ++i) { pivotF.v[i] = pivotTrans.v[i]; } return HelixState.pivotTransform(pivotF, helixAtX, new Vec(0., 0., 0.), alpha, 0.); } - - RKhelix copy() { - return new RKhelix(x.copy(),p.copy(),Q,fM,rndm); + + RKhelix copy() { + return new RKhelix(x.copy(), p.copy(), Q, fM, rndm); } - + RotMatrix R(Vec position) { Vec B = KalmanInterface.getField(position, fM); double Bmag = B.mag(); @@ -77,7 +78,7 @@ RotMatrix R(Vec position) { Vec v = t.cross(u); return new RotMatrix(u, v, t); } - + RKhelix randomScat(Plane P, double X) { // Produce a new helix scattered randomly in a given plane P Vec t = p.unitVec(); @@ -87,9 +88,12 @@ RKhelix randomScat(Plane P, double X) { // Produce a new helix scattered randoml RotMatrix Rp = new RotMatrix(uhat, vhat, t); double ct = Math.abs(P.T().dot(t)); double theta0; - - if (X == 0.) theta0 = 0.; // Get the scattering angle - else theta0 = FastMath.sqrt((X / radLen) / ct) * (0.0136 / p.mag()) * (1.0 + 0.038 * FastMath.log((X / radLen) / ct)); + + if (X == 0.) { + theta0 = 0.; // Get the scattering angle + } else { + theta0 = FastMath.sqrt((X / radLen) / ct) * (0.0136 / p.mag()) * (1.0 + 0.038 * FastMath.log((X / radLen) / ct)); + } double thetaX = rndm.nextGaussian() * theta0; double thetaY = rndm.nextGaussian() * theta0; double tx = FastMath.sin(thetaX); From 4391b8b2152db39b3cbf9b89ae28dc7929272e9d Mon Sep 17 00:00:00 2001 From: Graf Date: Tue, 17 Jan 2023 16:47:40 -0800 Subject: [PATCH 09/17] Fix style check violations --- .../org/hps/recon/tracking/kalman/Helix.java | 33 +- .../hps/recon/tracking/kalman/HelixTest3.java | 441 +++--- .../hps/recon/tracking/kalman/KalTrack.java | 1299 ++++++++++------- .../tracking/kalman/KalmanInterface.java | 1267 +++++++++------- .../recon/tracking/kalman/KalmanParams.java | 339 ++--- .../tracking/kalman/KalmanPatRecDriver.java | 438 +++--- .../hps/recon/tracking/kalman/RKhelix.java | 40 +- 7 files changed, 2282 insertions(+), 1575 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java index 1c1861be13..e3da763a34 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/Helix.java @@ -1,11 +1,14 @@ package org.hps.recon.tracking.kalman; import java.util.Random; + /** - * This is for stand-alone testing only and is not part of the Kalman fitting code. - * Create a simple helix oriented along the B field axis for testing the Kalman fit. + * This is for stand-alone testing only and is not part of the Kalman fitting + * code. Create a simple helix oriented along the B field axis for testing the + * Kalman fit. */ -class Helix { +class Helix { + Vec p; // Helix parameters drho, phi0, K, dz, tanl Vec X0; // Pivot point in the B field reference frame private double alpha; @@ -29,7 +32,7 @@ class Helix { this.origin = origin.copy(); this.doeLoss = doeLoss; this.fM = fM; - Vec Bf = new Vec(3,fM.getField(Xinit)); + Vec Bf = new Vec(3, fM.getField(Xinit)); B = Bf.mag(); double c = 2.99793e8; alpha = 1000.0 * 1.0E9 / (c * B); // Units are Tesla, mm, GeV @@ -60,7 +63,7 @@ class Helix { this.fM = fM; this.doeLoss = doeLoss; p = HelixParams.copy(); - Vec Bf = new Vec(3,fM.getField(pivotGlobal)); + Vec Bf = new Vec(3, fM.getField(pivotGlobal)); B = Bf.mag(); double c = 2.99793e8; alpha = 1000.0 * 1.0E9 / (c * B); // Units are Tesla, mm, GeV @@ -101,7 +104,7 @@ void print(String s) { System.out.format(" Pivot in B-field frame=%10.5f, %10.5f, %10.5f\n", X0.v[0], X0.v[1], X0.v[2]); Vec pivotGlobal = R.inverseRotate(X0).sum(origin); System.out.format(" Pivot in global frame=%10.5f, %10.5f, %10.5f\n", pivotGlobal.v[0], pivotGlobal.v[1], pivotGlobal.v[2]); - Vec Bf = new Vec(3,fM.getField(pivotGlobal)); + Vec Bf = new Vec(3, fM.getField(pivotGlobal)); Bf.print("B field in global frame at the pivot point"); Vec Bflocal = R.rotate(Bf); Bflocal.print("B field in its local frame; should be in +z direction"); @@ -145,7 +148,7 @@ Vec atPhiGlobal(double phi) { // return the global coordinates on the helix at a } double planeIntersect(Plane Pin) { // phi value where the helix intersects the plane P (given in global - // coordinates) + // coordinates) Plane P = Pin.toLocal(R, origin); double phi = hpi.planeIntersect(p, X0, alpha, P); // System.out.format("Helix:planeIntersect: phi = %12.10f\n", phi); @@ -175,7 +178,6 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc // Vec r = this.atPhiGlobal(phi); //double tst = r.dif(P.X()).dot(P.T()); // System.out.format("randomScat: test dot product %12.6e should be zero\n", tst); - // r.print("randomScat: r global"); // Vec pmom = getMomGlobal(phi); // pmom.print("randomScat: p global"); @@ -190,8 +192,11 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc double ct = Math.abs(P.T().dot(t)); double theta0; // Get the scattering angle - if (X == 0.) theta0 = 0.; - else theta0 = Math.sqrt((X / radLen) / ct) * (0.0136 / pmom.mag()) * (1.0 + 0.038 * Math.log((X / radLen) / ct)); + if (X == 0.) { + theta0 = 0.; + } else { + theta0 = Math.sqrt((X / radLen) / ct) * (0.0136 / pmom.mag()) * (1.0 + 0.038 * Math.log((X / radLen) / ct)); + } double thetaX = rndm.nextGaussian() * theta0; double thetaY = rndm.nextGaussian() * theta0; // System.out.format("Helix.randomScat: X=%12.5e, ct=%12.5e, theta0=%12.5e, thetaX=%12.5e, @@ -208,7 +213,7 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc // System.out.format("recalculated scattered angle=%10.7f\n", check); // Rotate the direction into the frame of the new field (evaluated at the new pivot) - Vec Bf = new Vec(3,fM.getField(r)); + Vec Bf = new Vec(3, fM.getField(r)); double Bnew = Bf.mag(); Vec tBnew = Bf.unitVec(Bnew); Vec yhat = new Vec(0., 1., 0.); @@ -221,10 +226,10 @@ Helix randomScat(Plane P, Vec r, Vec pmom, double X) { // Produce a new helix sc double E = pmom.mag(); // Everything is assumed electron double sp = 0.; if (doeLoss) { - sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g - sp = sp*20; // ToDo temporary!!! + sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g + sp = sp * 20; // ToDo temporary!!! } - sp = sp*20; // ToDo temporary!!! + sp = sp * 20; // ToDo temporary!!! double dEdx = 0.1 * sp * rho; // in GeV/mm double eLoss = dEdx * X / ct; // System.out.format("randomScat: energy=%10.7f, energy loss=%10.7f\n", E, eLoss); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index 233b60fb78..792cb4c99c 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -22,7 +22,8 @@ import org.lcsim.event.TrackState; /** - * This is for stand-alone testing of the Kalman fit only and is not part of the HPS Kalman fitting code package + * This is for stand-alone testing of the Kalman fit only and is not part of the + * HPS Kalman fitting code package */ class HelixTest3 { // Program for testing the Kalman fitting code @@ -30,14 +31,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code // z is the B field direction, downward in lab coordinates // y is the beam direction // x is y cross z - Random rnd; HelixTest3(String path) { // Control parameters // Units are Tesla, GeV, mm - int nTrials = 10000; // The number of test events to generate for fitting boolean residualsEconstrained = false; // Use constrained or unconstrained tracks for residuals histograms int startLayer = 10; // Where to start the Kalman filtering @@ -46,22 +45,22 @@ class HelixTest3 { // Program for testing the Kalman fitting code int nStereo = 4; // Number of stereo layers needed by the linear fit boolean cheat = false; // true to use the true helix parameters (smeared) for the starting guess boolean perfect = false; - double [] vtxRes = {0.1, 0.5, 0.05}; + double[] vtxRes = {0.1, 0.5, 0.05}; boolean verbose = nTrials < 2; double executionTime = 0.; int nBadCov = 0; - + double eCalLoc = 1394.; - + DMatrixRMaj testCov = null; Vec testHelix = null; - DMatrixRMaj tempM1 = new DMatrixRMaj(5,5); - DMatrixRMaj tempM2 = new DMatrixRMaj(5,5); - DMatrixRMaj fRot = new DMatrixRMaj(5,5); - for (int i=0; i<5; ++i) { - for (int j=0; j<5; ++j) { - if (i==j) { + DMatrixRMaj tempM1 = new DMatrixRMaj(5, 5); + DMatrixRMaj tempM2 = new DMatrixRMaj(5, 5); + DMatrixRMaj fRot = new DMatrixRMaj(5, 5); + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + if (i == j) { fRot.unsafe_set(i, j, 1.); } else { fRot.unsafe_set(i, j, 0); @@ -71,14 +70,16 @@ class HelixTest3 { // Program for testing the Kalman fitting code KalmanParams kPar = new KalmanParams(); kPar.setEloss(true); kPar.print(); - + // Seed the random number generator long rndSeed = -3263009337738135404L; rnd = new Random(); rnd.setSeed(rndSeed); - + Histogram hGaus = new Histogram(100, -4., 0.08, "Normal Distribution 1", "x", "y"); - for (int i = 0; i < 1000000; i++) { hGaus.entry(rnd.nextGaussian()); } + for (int i = 0; i < 1000000; i++) { + hGaus.entry(rnd.nextGaussian()); + } // Read in the magnetic field map String mapType = "binary"; @@ -98,32 +99,33 @@ class HelixTest3 { // Program for testing the Kalman fitting code fM.writeBinaryFile("C:\\Users\\rjohn\\Documents\\GitHub\\hps-java\\fieldmap\\418acm2_10kg_corrected_unfolded_scaled_1.0319.bin"); } System.out.format("B field map vs y:\n"); - for (double y=0.; y<1500.; y+=5.) { - double z1=-50.; - double z2= 50.; + for (double y = 0.; y < 1500.; y += 5.) { + double z1 = -50.; + double z2 = 50.; Vec B1 = new Vec(3, fM.getField(new Vec(0., y, z1))); Vec B2 = new Vec(3, fM.getField(new Vec(0., y, 0.))); Vec B3 = new Vec(3, fM.getField(new Vec(0., y, z2))); System.out.format("y=%6.1f z=%6.1f: %s z=0: %s z=%6.1f: %s\n", y, z1, B1.toString(), B2.toString(), z2, B3.toString()); } System.out.format("B field map vs z at ECAL:\n"); - for (double z=-200.; z<200.; z+=5.) { - double y=eCalLoc + 10.; + for (double z = -200.; z < 200.; z += 5.) { + double y = eCalLoc + 10.; Vec B = new Vec(3, fM.getField(new Vec(0., y, z))); System.out.format("x=0 y=%6.1f z=%6.1f: %s\n", y, z, B.toString()); } System.out.format("B field map vs x at center:\n"); - for (double x=-300.; x<300.; x+=5.) { - double y=505.; - double z=20.; + for (double x = -300.; x < 300.; x += 5.) { + double y = 505.; + double z = 20.; Vec B = new Vec(3, fM.getField(new Vec(x, y, z))); System.out.format("x=%6.1f y=%6.1f z=%6.1f: %s\n", x, y, z, B.toString()); } // Tracking instrument description - double thickness = 0.3; // Silicon thickness in mm - if (perfect) { thickness = 0.0000000000001; } + if (perfect) { + thickness = 0.0000000000001; + } ArrayList SiModules = new ArrayList(); Plane plnInt; SiModule newModule; @@ -202,7 +204,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code plnInt = new Plane(new Vec(0., 870.0, 30.0), new Vec(0., 1.0, 0.)); newModule = new SiModule(-5, plnInt, false, 0., 900., 900., 0., fM, 0); SiModules.add(newModule); - */ + */ plnInt = new Plane(new Vec(-22.879, 905.35, 35.309), new Vec(-0.029214, -0.99957, 0.0019280), -0.049801); newModule = new SiModule(11, plnInt, true, 100., 40.34, false, thickness, fM, 0); SiModules.add(newModule); @@ -229,12 +231,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code for (SiModule mod : SiModules) { if (mod.Layer == layer) { yScat.add(mod.p.X().v[1]); - XLscat.add(mod.thickness/radLen); + XLscat.add(mod.thickness / radLen); break; } } } - + double[] location = new double[nLayers]; double[] xdet = new double[SiModules.size()]; double[] ydet = new double[SiModules.size()]; @@ -242,12 +244,14 @@ class HelixTest3 { // Program for testing the Kalman fitting code int[] lyr = new int[SiModules.size()]; for (int i = 0; i < SiModules.size(); i++) { SiModule si = SiModules.get(i); - - si.p.T().v[0]=0.; - si.p.T().v[1]=1. * Math.signum(si.p.T().v[1]); // !!!!!!!!!!!!!!!!!!!!! All modules aligned - si.p.T().v[2]=0.; - - if (si.Layer >= 0) { location[si.Layer] = si.p.X().v[1]; } + + si.p.T().v[0] = 0.; + si.p.T().v[1] = 1. * Math.signum(si.p.T().v[1]); // !!!!!!!!!!!!!!!!!!!!! All modules aligned + si.p.T().v[2] = 0.; + + if (si.Layer >= 0) { + location[si.Layer] = si.p.X().v[1]; + } lyr[i] = si.Layer; xdet[i] = si.p.X().v[0]; ydet[i] = si.p.X().v[1]; @@ -263,12 +267,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code double hitEfficiency = 1.0; Vec helixOrigin = new Vec(0., 0., 0.); // Pivot point of initial helix - Vec Bpivot = new Vec(3,fM.getField(helixOrigin)); + Vec Bpivot = new Vec(3, fM.getField(helixOrigin)); Bpivot.print("magnetic field at the initial origin"); for (int pln = 0; pln < SiModules.size(); pln++) { - Vec bf = new Vec(3,fM.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); + Vec bf = new Vec(3, fM.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); System.out.format("Kalman fitting B field at module %d = %10.7f, %10.7f, %10.7f\n", pln, bf.v[0], bf.v[1], bf.v[2]); - bf = new Vec(3,fMg.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); + bf = new Vec(3, fMg.getField(new Vec(xdet[pln], ydet[pln], zdet[pln]))); System.out.format("MC generator B field at module %d = %10.7f, %10.7f, %10.7f\n", pln, bf.v[0], bf.v[1], bf.v[2]); } double Phi = 91.5 * Math.PI / 180.; @@ -351,7 +355,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code printWriter.format("splot "); printWriter.format("$runga u 1:2:3 with lines lw 3, $helix u 1:2:3 with lines lw 3\n"); printWriter.close(); - + Vec zhat = null; Vec uhat = null; Vec vhat = null; @@ -399,7 +403,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEkCon = new Histogram(100, -10., 0.2, "Origin helix parameter K error with ECAL constraint", "sigmas", "track"); Histogram hEdzCon = new Histogram(100, -10., 0.2, "Origin helix parameter dz error with ECAL constraint", "sigmas", "track"); Histogram hEtanlCon = new Histogram(100, -10., 0.2, "Origin helix parameter tanl error with ECAL constraint", "sigmas", "track"); - + Histogram hEdrhoSigO = new Histogram(100, -10., 0.2, "Origin helix parameter drho constrained error", "sigmas", "track"); Histogram hEphi0SigO = new Histogram(100, -10., 0.2, "Origin helix parameter phi0 constrained error", "sigmas", "track"); Histogram hEkSigO = new Histogram(100, -10., 0.2, "Origin helix parameter K constrained error", "sigmas", "track"); @@ -420,8 +424,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram hEckO = new Histogram(100, -0.5, 0.01, "Origin helix parameter K constrained error", "1/GeV", "track"); Histogram hEcdzO = new Histogram(100, -0.4, 0.008, "Origin helix parameter dz constrained error", "mm", "track"); Histogram hEctanlO = new Histogram(100, -0.005, 0.0001, "Origin helix parameter tanl constrained error", " ", "track"); - Histogram hchi2inc = new Histogram(100, 0., 1., "Origin helix constrained chi^2 increment","chi2","track"); - Histogram hchi2c = new Histogram(100, 0., 1., "Origin helix constrained chi^2","chi2","track"); + Histogram hchi2inc = new Histogram(100, 0., 1., "Origin helix constrained chi^2 increment", "chi2", "track"); + Histogram hchi2c = new Histogram(100, 0., 1., "Origin helix constrained chi^2", "chi2", "track"); Histogram hResid0 = new Histogram(100, -10., 0.2, "Filtered residual for axial planes", "sigmas", "hits"); Histogram hResid1 = new Histogram(100, -10., 0.2, "Filtered residual for stereo planes", "sigmas", "hits"); Histogram hEdrhoG = new Histogram(100, -40., 0.8, "Helix guess drho error", "sigmas", "track"); @@ -442,43 +446,47 @@ class HelixTest3 { // Program for testing the Kalman fitting code Histogram[] hUnbias = new Histogram[nLayers]; Histogram[] hUnbiasSig = new Histogram[nLayers]; String preamb = ""; - if (residualsEconstrained) preamb = "E constrained "; + if (residualsEconstrained) { + preamb = "E constrained "; + } for (int i = 0; i < nLayers; i++) { - hResidS0[i] = new Histogram(100, -10., 0.2, String.format(preamb+"Smoothed fit residual for plane %d", i), "sigmas", "hits"); - hResidS2[i] = new Histogram(100, -0.02, 0.0004, String.format(preamb+"Smoothed fit residual for plane %d", i), "mm", "hits"); - hResidS4[i] = new Histogram(100, -0.1, 0.002, String.format(preamb+"Smoothed true residual for plane %d", i), "mm", "hits"); - hResidX[i] = new Histogram(100, -0.8, 0.016, String.format(preamb+"True residual in global X for plane %d", i), "mm", "hits"); - hResidZ[i] = new Histogram(100, -0.1, 0.002, String.format(preamb+"True residual in global Z for plane %d", i), "mm", "hits"); - hUnbias[i] = new Histogram(100, -0.2, 0.004, String.format(preamb+"Unbiased residual for plane %d", i), "mm", "hits"); - hUnbiasSig[i] = new Histogram(100, -10., 0.2, String.format(preamb+"Unbiased residuals for layer %d", i), "sigmas", "hits"); + hResidS0[i] = new Histogram(100, -10., 0.2, String.format(preamb + "Smoothed fit residual for plane %d", i), "sigmas", "hits"); + hResidS2[i] = new Histogram(100, -0.02, 0.0004, String.format(preamb + "Smoothed fit residual for plane %d", i), "mm", "hits"); + hResidS4[i] = new Histogram(100, -0.1, 0.002, String.format(preamb + "Smoothed true residual for plane %d", i), "mm", "hits"); + hResidX[i] = new Histogram(100, -0.8, 0.016, String.format(preamb + "True residual in global X for plane %d", i), "mm", "hits"); + hResidZ[i] = new Histogram(100, -0.1, 0.002, String.format(preamb + "True residual in global Z for plane %d", i), "mm", "hits"); + hUnbias[i] = new Histogram(100, -0.2, 0.004, String.format(preamb + "Unbiased residual for plane %d", i), "mm", "hits"); + hUnbiasSig[i] = new Histogram(100, -10., 0.2, String.format(preamb + "Unbiased residuals for layer %d", i), "sigmas", "hits"); } - Histogram hpropx = new Histogram(100,-5.,0.1,"projected track-state x error","mm","track"); - Histogram hpropxs = new Histogram(100,-5.,0.1,"projected track-state x error","sigmas","track"); - Histogram hpropy = new Histogram(100,-5.,0.1,"projected track-state y error","mm","track"); - Histogram hpropys = new Histogram(100,-5.,0.1,"projected track-state y error","sigmas","track"); - Histogram hpropxu = new Histogram(100,0.,0.05,"projected track-state x uncertainty","mm","track"); - Histogram hpropyu = new Histogram(100,0.,0.05,"projected track-state y uncertainty","mm","track"); - Histogram hPropxHS = new Histogram(100,-5.,0.1,"projected HelixState x error","mm","track"); - Histogram hPropzHS = new Histogram(100,-5.,0.1,"projected HelixState z error","mm","track"); - Histogram hPropxsHS = new Histogram(100,-5.,0.1,"projected HelixState x error","sigmas","track"); - Histogram hPropzsHS = new Histogram(100,-5.,0.1,"projected HelixState z error","sigmas","track"); - Histogram hPropx1 = new Histogram(100,-5.,0.1,"projected track-state x error 1 step","mm","track"); - Histogram hPropx1s = new Histogram(100,-5.,0.1,"projected track-state x error 1 step","sigmas","track"); - Histogram hPropz1 = new Histogram(100,-5.,0.1,"projected track-state z error 1 step","mm","track"); - Histogram hPropz1s = new Histogram(100,-5.,0.1,"projected track-state z error 1 step","sigmas","track"); - + Histogram hpropx = new Histogram(100, -5., 0.1, "projected track-state x error", "mm", "track"); + Histogram hpropxs = new Histogram(100, -5., 0.1, "projected track-state x error", "sigmas", "track"); + Histogram hpropy = new Histogram(100, -5., 0.1, "projected track-state y error", "mm", "track"); + Histogram hpropys = new Histogram(100, -5., 0.1, "projected track-state y error", "sigmas", "track"); + Histogram hpropxu = new Histogram(100, 0., 0.05, "projected track-state x uncertainty", "mm", "track"); + Histogram hpropyu = new Histogram(100, 0., 0.05, "projected track-state y uncertainty", "mm", "track"); + Histogram hPropxHS = new Histogram(100, -5., 0.1, "projected HelixState x error", "mm", "track"); + Histogram hPropzHS = new Histogram(100, -5., 0.1, "projected HelixState z error", "mm", "track"); + Histogram hPropxsHS = new Histogram(100, -5., 0.1, "projected HelixState x error", "sigmas", "track"); + Histogram hPropzsHS = new Histogram(100, -5., 0.1, "projected HelixState z error", "sigmas", "track"); + Histogram hPropx1 = new Histogram(100, -5., 0.1, "projected track-state x error 1 step", "mm", "track"); + Histogram hPropx1s = new Histogram(100, -5., 0.1, "projected track-state x error 1 step", "sigmas", "track"); + Histogram hPropz1 = new Histogram(100, -5., 0.1, "projected track-state z error 1 step", "mm", "track"); + Histogram hPropz1s = new Histogram(100, -5., 0.1, "projected track-state z error 1 step", "sigmas", "track"); + Instant timestamp = Instant.now(); System.out.format("Beginning time = %s\n", timestamp.toString()); LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); System.out.format("%s %d %d at %d:%d %d.%d seconds\n", ldt.getMonth(), ldt.getDayOfMonth(), ldt.getYear(), ldt.getHour(), ldt.getMinute(), ldt.getSecond(), ldt.getNano()); - double startTime = (double)(ldt.getMinute())*60. + (double)ldt.getSecond() + (double)(ldt.getNano())/1e9; + double startTime = (double) (ldt.getMinute()) * 60. + (double) ldt.getSecond() + (double) (ldt.getNano()) / 1e9; // Extrapolate the helix from the origin to the first detector layer SiModule si1 = SiModules.get(0); double phi1 = TkInitial.planeIntersect(si1.p); if (Double.isNaN(phi1)) { - if (verbose) System.out.format("Oops! No intersection found with initial plane"); + if (verbose) { + System.out.format("Oops! No intersection found with initial plane"); + } return; } Vec lyr1Int = TkInitial.atPhi(phi1); @@ -500,13 +508,17 @@ class HelixTest3 { // Program for testing the Kalman fitting code KalmanInterface KI = new KalmanInterface(kPar, fM); for (int iTrial = 0; iTrial < nTrials; iTrial++) { RKhelix Tk = helixBeginRK.copy(); - if (verbose) { Tk.print("copied initial helix"); } + if (verbose) { + Tk.print("copied initial helix"); + } // Populate the Si detector planes with hits from the helix scattered at each // plane for (int icm = 0; icm < SiModules.size(); icm++) { SiModule thisSi = SiModules.get(icm); - if (thisSi.Layer < 0) { continue; } + if (thisSi.Layer < 0) { + continue; + } thisSi.reset(); int pln = thisSi.Layer; int det = thisSi.detector; @@ -530,7 +542,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code // Check whether the intersection is within the bounds of the detector if (rDet.v[0] > thisSi.xExtent[1] || rDet.v[0] < thisSi.xExtent[0] || rDet.v[1] > thisSi.yExtent[1] || rDet.v[1] < thisSi.yExtent[0]) { - if (verbose) { System.out.format(" Intersection point is outside of the detector %d in layer %d\n", det, pln); } + if (verbose) { + System.out.format(" Intersection point is outside of the detector %d in layer %d\n", det, pln); + } continue; } Tk = new RKhelix(rscat, pInt, Q, fMg, rnd, kPar.eLoss); @@ -546,7 +560,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code double smear = resolution * gran[0]; double m1 = rDet.v[1] + smear; hRes.entry(smear); - if (verbose) { System.out.format(" Measurement 1= %10.7f, Truth=%10.7f\n", m1, rDet.v[1]); } + if (verbose) { + System.out.format(" Measurement 1= %10.7f, Truth=%10.7f\n", m1, rDet.v[1]); + } Measurement thisM1 = new Measurement(m1, 0., resolution, 0., 10., rscat, rDet.v[1]); thisSi.addMeasurement(thisM1); } @@ -591,19 +607,23 @@ class HelixTest3 { // Program for testing the Kalman fitting code } } else { TkEnd = Tk; - if (verbose) { TkEnd.print("TkEnd"); } + if (verbose) { + TkEnd.print("TkEnd"); + } } } - + // Extrapolate further to the ECAL location. The helix parameters are in a coordinate system with origin // on the helix at the current location x and aligned with the local B field. Since we provide the pivot // point to be x (in global coordinates), the pivotECAL in the field system is (0,0,0). - Plane pEcal = new Plane(new Vec(0.,eCalLoc,0.), new Vec(0.,1.,0.)); + Plane pEcal = new Plane(new Vec(0., eCalLoc, 0.), new Vec(0., 1., 0.)); RKhelix TkEcal = Tk.propagateRK(pEcal); int nHits = 0; for (SiModule siM : SiModules) { - if (siM.hits.size() > 0) nHits++; + if (siM.hits.size() > 0) { + nHits++; + } } hnHit.entry(nHits); @@ -615,8 +635,12 @@ class HelixTest3 { // Program for testing the Kalman fitting code //for (int i=0; i= 0; i--) { SiModule si = SiModules.get(i); - if (si.Layer > startLayer) continue; - if (si.hits.isEmpty()) continue; + if (si.Layer > startLayer) { + continue; + } + if (si.hits.isEmpty()) { + continue; + } if (nA < nAxial) { if (!si.isStereo) { int[] ht = new int[2]; @@ -627,7 +651,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code frstLyr = si.Layer; } } else { - if (nS >= nStereo) break; + if (nS >= nStereo) { + break; + } } if (nS < nStereo) { if (si.isStereo) { @@ -639,7 +665,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code frstLyr = si.Layer; } } else { - if (nA >= nStereo) break; + if (nA >= nStereo) { + break; + } } } if (nS < nStereo || nA < nAxial) { @@ -666,10 +694,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code // For comparison, get the true helix in the B field frame at the first layer of the linear fit // Then transform it to the same pivot used by the linear fit. Here we have to ignore details about // coordinate system rotation, because the linear fit is only done in the global frame assuming constant field - helixMCtrue = helixSaved[frstLyr]; Vec pivotOnAxis = new Vec(0., location[frstLyr], 0.); - Vec Bpiv = new Vec(3,fMg.getField(pivotOnAxis)); + Vec Bpiv = new Vec(3, fMg.getField(pivotOnAxis)); double alpha = 1.0e12 / (2.99793e8 * Bpiv.mag()); Vec helixTrueTrans = HelixState.pivotTransform(pivotOnAxis, helixMCtrue, pivotSaved[frstLyr], alpha, 0.); Vec gErrVec = initialHelixGuess.dif(helixTrueTrans); @@ -677,7 +704,9 @@ class HelixTest3 { // Program for testing the Kalman fitting code // initialHelixGuess.v[2] - K, // initialHelixGuess.v[3] - dz, initialHelixGuess.v[4] - tanl); double[] gErr = new double[5]; - for (int i = 0; i < 5; i++) { gErr[i] = gErrVec.v[i] / GuessErrors.v[i]; } + for (int i = 0; i < 5; i++) { + gErr[i] = gErrVec.v[i] / GuessErrors.v[i]; + } if (verbose) { // helixMCtrue.print("MC true helix at this layer"); // pivotSaved[frstLyr].print("old pivot"); @@ -710,9 +739,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code // double Bstart = seed.B(); // Vec tBstart = new Vec(0., 0., 1.); - // Cheating initial "guess" for the helix - double[] rn = new double[2]; if (perfect) { rn[0] = 0.; @@ -745,7 +772,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code initialCovariance.unsafe_set(3, 3, dzSigma * dzSigma); initialCovariance.unsafe_set(4, 4, tanlSigma * tanlSigma); - Vec Bf0 = new Vec(3,fMg.getField(helixOrigin)); + Vec Bf0 = new Vec(3, fMg.getField(helixOrigin)); if (verbose) { initialHelixGuess.print("initial helix guess"); System.out.format("True helix: %10.6f %10.6f %10.6f %10.6f %10.6f\n", drho, phi0, K, dz, tanl); @@ -765,47 +792,57 @@ class HelixTest3 { // Program for testing the Kalman fitting code KalmanTrackFit2 kF = new KalmanTrackFit2(iTrial, SiModules, null, startLayer, nIteration, new Vec(0., location[frstLyr], 0.), initialHelixGuess, initialCovariance, kPar, fM); long endTimeF = System.nanoTime(); - double runTime = (double)(endTimeF - startTimeF)/1000000.; + double runTime = (double) (endTimeF - startTimeF) / 1000000.; executionTime += runTime; - if (!kF.success) continue; + if (!kF.success) { + continue; + } KalTrack KalmanTrack = kF.tkr; - if (KalmanTrack == null) continue; + if (KalmanTrack == null) { + continue; + } KalmanTrack.originHelix(); - if (verbose) KalmanTrack.print("KalmanTrack"); - + if (verbose) { + KalmanTrack.print("KalmanTrack"); + } + // Apply ECAL energy constraint - if (kPar.eLoss) Etrue = TkEnd.p.mag(); - double sigmaE = 0.03*FastMath.sqrt(Etrue); - double E = Etrue + rnd.nextGaussian()*sigmaE; - KalmanTrack.energyConstraint(E, sigmaE); - + if (kPar.eLoss) { + Etrue = TkEnd.p.mag(); + } + double sigmaE = 0.03 * FastMath.sqrt(Etrue); + double E = Etrue + rnd.nextGaussian() * sigmaE; + KalmanTrack.energyConstraint(E, sigmaE); + // Check on the covariance matrix Matrix C = new Matrix(KalmanTrack.originCovariance()); - EigenvalueDecomposition eED= new EigenvalueDecomposition(C); - double [] ev = eED.getRealEigenvalues(); + EigenvalueDecomposition eED = new EigenvalueDecomposition(C); + double[] ev = eED.getRealEigenvalues(); boolean badCov = false; - for (int i=0; i<5; ++i) { + for (int i = 0; i < 5; ++i) { if (ev[i] < 0.) { System.out.format("Event %d, eigenvalue %d of covariance is negative!", iTrial, i); badCov = true; } } - if (badCov) nBadCov++; + if (badCov) { + nBadCov++; + } if (iTrial < 10) { - Vec evV = new Vec(5,ev); + Vec evV = new Vec(5, ev); evV.print("Eigenvalues of covariance"); } - + // Test the helix propagation code - List states = new ArrayList(); for (MeasurementSite site : kF.sites) { int loc = TrackState.AtOther; - if (kF.sites.indexOf(site) == 0) loc = TrackState.AtFirstHit; - else if (kF.sites.indexOf(site) == kF.sites.size()-1) { + if (kF.sites.indexOf(site) == 0) { + loc = TrackState.AtFirstHit; + } else if (kF.sites.indexOf(site) == kF.sites.size() - 1) { loc = TrackState.AtLastHit; } - TrackState ts = KI.createTrackState(site, loc, true); + TrackState ts = KI.createTrackState(site, loc, true); states.add(ts); } TrackState lastState = null; @@ -816,20 +853,22 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } } if (lastState != null) { - // test propagation of a track state from one end of the track to the ECAL region - final boolean debug = false; + // test propagation of a track state from one end of the track to the ECAL region + final boolean debug = false; if (KalmanTrack.nHits >= 12) { - if (KalmanTrack.chi2/KalmanTrack.nHits < 2.) { - MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size()-1); + if (KalmanTrack.chi2 / KalmanTrack.nHits < 2.) { + MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size() - 1); Vec eCalPos = TkEcal.x; - Plane plnAtEcal = new Plane(eCalPos, new Vec(0.,1.,0.)); + Plane plnAtEcal = new Plane(eCalPos, new Vec(0., 1., 0.)); if (debug) { eCalPos.print("ECAL cluster position"); lastSite.aS.helix.print("helix at last layer"); } - double [] arcLength = new double[1]; + double[] arcLength = new double[1]; HelixState helixAtEcal = lastSite.aS.helix.propagateRungeKutta(plnAtEcal, yScat, XLscat, fM, arcLength); - if (MatrixFeatures_DDRM.hasNaN(helixAtEcal.C)) continue; + if (MatrixFeatures_DDRM.hasNaN(helixAtEcal.C)) { + continue; + } Vec intPnt = helixAtEcal.getRKintersection(); if (debug) { helixAtEcal.print("helix at ECAL cluster"); @@ -838,21 +877,21 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hPropxHS.entry(intPnt.v[0] - eCalPos.v[0]); hPropzHS.entry(intPnt.v[2] - eCalPos.v[2]); DMatrixRMaj covAtEcal = helixAtEcal.C; - double [][] dadx = KalTrack.DxTOa(helixAtEcal.a); - double [][] Cx = new double[3][3]; + double[][] dadx = KalTrack.DxTOa(helixAtEcal.a); + double[][] Cx = new double[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Cx[i][j] = 0.; for (int k = 0; k < 5; k++) { for (int l = 0; l < 5; l++) { - Cx[i][j] += dadx[i][k] * covAtEcal.unsafe_get(k,l) * dadx[j][l]; + Cx[i][j] += dadx[i][k] * covAtEcal.unsafe_get(k, l) * dadx[j][l]; } } } } - hPropxsHS.entry((intPnt.v[0] - eCalPos.v[0])/Math.sqrt(Cx[0][0])); - hPropzsHS.entry((intPnt.v[2] - eCalPos.v[2])/Math.sqrt(Cx[2][2])); - + hPropxsHS.entry((intPnt.v[0] - eCalPos.v[0]) / Math.sqrt(Cx[0][0])); + hPropzsHS.entry((intPnt.v[2] - eCalPos.v[2]) / Math.sqrt(Cx[2][2])); + // Try in a single step---this worked perfect in uniform field, as long as last scatter is included double phiIntEcal = lastSite.aS.helix.planeIntersect(plnAtEcal); if (!Double.isNaN(phiIntEcal)) { @@ -863,22 +902,28 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } Vec intcpt = lastSite.aS.helix.atPhi(phiIntEcal); Vec helixAtIntcpt = lastSite.aS.helix.pivotTransform(intcpt); - if (debug) intcpt.print("intercept local"); + if (debug) { + intcpt.print("intercept local"); + } intcpt = lastSite.aS.helix.toGlobal(intcpt); - if (debug) intcpt.print("intercept global"); - DMatrixRMaj F = new DMatrixRMaj(5,5); + if (debug) { + intcpt.print("intercept global"); + } + DMatrixRMaj F = new DMatrixRMaj(5, 5); lastSite.aS.helix.makeF(helixAtIntcpt, F, 1.0); - if (debug) F.print("tranform matrix F"); - Vec pMom = HelixState.getMom(0.,helixAtIntcpt); + if (debug) { + F.print("tranform matrix F"); + } + Vec pMom = HelixState.getMom(0., helixAtIntcpt); double pMag = pMom.mag(); - double ct = pMom.v[1]/pMag; - double sigmaMS = HelixState.projMSangle(pMag, XLscat.get(XLscat.size()-1)/ct); - DMatrixRMaj Qmcs = new DMatrixRMaj(5,5); + double ct = pMom.v[1] / pMag; + double sigmaMS = HelixState.projMSangle(pMag, XLscat.get(XLscat.size() - 1) / ct); + DMatrixRMaj Qmcs = new DMatrixRMaj(5, 5); lastSite.aS.helix.getQ(sigmaMS, Qmcs); CommonOps_DDRM.add(lastSite.aS.helix.C, Qmcs, tempM1); CommonOps_DDRM.multTransB(tempM1, F, tempM2); - DMatrixRMaj covAtIntcpt = new DMatrixRMaj(5,5); - CommonOps_DDRM.mult(F, tempM2, covAtIntcpt); + DMatrixRMaj covAtIntcpt = new DMatrixRMaj(5, 5); + CommonOps_DDRM.mult(F, tempM2, covAtIntcpt); dadx = KalTrack.DxTOa(helixAtIntcpt); Cx = new double[3][3]; for (int i = 0; i < 3; i++) { @@ -886,21 +931,21 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { Cx[i][j] = 0.; for (int k = 0; k < 5; k++) { for (int l = 0; l < 5; l++) { - Cx[i][j] += dadx[i][k] * covAtIntcpt.unsafe_get(k,l) * dadx[j][l]; + Cx[i][j] += dadx[i][k] * covAtIntcpt.unsafe_get(k, l) * dadx[j][l]; } } } } if (debug) { covAtIntcpt.print("covAtIntcpt"); - new SquareMatrix(3,Cx).print("Cx"); + new SquareMatrix(3, Cx).print("Cx"); } hPropx1.entry(intcpt.v[0] - eCalPos.v[0]); - hPropz1.entry(intcpt.v[2] - eCalPos.v[2]); - hPropx1s.entry((intcpt.v[0] - eCalPos.v[0])/Math.sqrt(Cx[0][0])); - hPropz1s.entry((intcpt.v[2] - eCalPos.v[2])/Math.sqrt(Cx[2][2])); + hPropz1.entry(intcpt.v[2] - eCalPos.v[2]); + hPropx1s.entry((intcpt.v[0] - eCalPos.v[0]) / Math.sqrt(Cx[0][0])); + hPropz1s.entry((intcpt.v[2] - eCalPos.v[2]) / Math.sqrt(Cx[2][2])); } - } + } } /* for (TrackState tkState : states) { @@ -929,19 +974,19 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { break; } } - */ + */ } // end of test of helix propagation - + // Test the vertex constraint Vec vtx = helixOrigin.copy(); - for (int i=0; i<3; ++i) { + for (int i = 0; i < 3; ++i) { vtx.v[i] += rnd.nextGaussian() * vtxRes[i]; } SquareMatrix vtxCov = new SquareMatrix(3); - vtxCov.M[0][0] = vtxRes[0]*vtxRes[0]; - vtxCov.M[1][1] = vtxRes[1]*vtxRes[1]; - vtxCov.M[2][2] = vtxRes[2]*vtxRes[2]; + vtxCov.M[0][0] = vtxRes[0] * vtxRes[0]; + vtxCov.M[1][1] = vtxRes[1] * vtxRes[1]; + vtxCov.M[2][2] = vtxRes[2] * vtxRes[2]; HelixState constrainedHelix = KalmanTrack.originConstraint(vtx.v, vtxCov.M); hchi2inc.entry(KalmanTrack.chi2incOrigin()); hchi2c.entry(KalmanTrack.chi2incOrigin() + KalmanTrack.chi2); @@ -954,35 +999,47 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { SiModule siM = site.m; if (site.m.Layer >= 0) { if (site.filtered) { - if (siM.isStereo) hResid0.entry(site.aF.r / Math.sqrt(site.aF.R)); - else hResid1.entry(site.aF.r / Math.sqrt(site.aF.R)); + if (siM.isStereo) { + hResid0.entry(site.aF.r / Math.sqrt(site.aF.R)); + } else { + hResid1.entry(site.aF.r / Math.sqrt(site.aF.R)); + } } if (site.smoothed) { StateVector aA = null; - if (residualsEconstrained) aA = site.aES; - else aA = site.aS; - if (site.m.Layer == 4) hReducedErr.entry(Math.sqrt(aA.R)); + if (residualsEconstrained) { + aA = site.aES; + } else { + aA = site.aS; + } + if (site.m.Layer == 4) { + hReducedErr.entry(Math.sqrt(aA.R)); + } chi2s += Math.pow(aA.mPred - site.m.hits.get(site.hitID).vTrue, 2) / aA.R; hResidS0[siM.Layer].entry(aA.r / Math.sqrt(aA.R)); hResidS2[siM.Layer].entry(aA.r); - if (site.hitID >= 0) { hResidS4[siM.Layer].entry(site.m.hits.get(site.hitID).vTrue - aA.mPred); } + if (site.hitID >= 0) { + hResidS4[siM.Layer].entry(site.m.hits.get(site.hitID).vTrue - aA.mPred); + } } } } } - for (int layer=0; layer < nLayers; ++layer) { + for (int layer = 0; layer < nLayers; ++layer) { Pair resid = KalmanTrack.unbiasedResidual(layer, residualsEconstrained); if (resid.getSecondElement() > -999.) { double variance = resid.getSecondElement(); double sigma = Math.sqrt(variance); double unbResid = resid.getFirstElement(); hUnbias[layer].entry(unbResid); - hUnbiasSig[layer].entry(unbResid/sigma); - if (variance < resolution*resolution) { - System.out.format("Event %d layer %d, unbiased residual variance too small: %10.5f, chi2=%9.2f, hits=%d, resid=%9.6f, lyrs:", - iTrial, layer, variance, KalmanTrack.chi2, KalmanTrack.nHits, unbResid); + hUnbiasSig[layer].entry(unbResid / sigma); + if (variance < resolution * resolution) { + System.out.format("Event %d layer %d, unbiased residual variance too small: %10.5f, chi2=%9.2f, hits=%d, resid=%9.6f, lyrs:", + iTrial, layer, variance, KalmanTrack.chi2, KalmanTrack.nHits, unbResid); for (MeasurementSite site : KalmanTrack.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } System.out.format(" %d ", site.m.Layer); } System.out.format("\n"); @@ -992,8 +1049,12 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { for (MeasurementSite site : KalmanTrack.interceptVects().keySet()) { Vec loc = KalmanTrack.interceptVects().get(site); SiModule siM = site.m; - if (siM.Layer < 0) continue; - if (site.hitID < 0) { System.out.format("Missing hit ID on site with layer=%d", siM.Layer); } + if (siM.Layer < 0) { + continue; + } + if (site.hitID < 0) { + System.out.format("Missing hit ID on site with layer=%d", siM.Layer); + } Vec locMC = site.m.hits.get(site.hitID).rGlobal; hResidX[siM.Layer].entry(loc.v[0] - locMC.v[0]); hResidZ[siM.Layer].entry(loc.v[2] - locMC.v[2]); @@ -1024,7 +1085,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } Vec newPivot = kF.fittedStateBegin().helix.toLocal(helixBegin.origin.sum(helixBegin.X0)); Vec aF = kF.fittedStateBegin().helix.pivotTransform(newPivot); - + // now rotate to the original field frame if (!kPar.uniformB) { RotMatrix Rcombo = helixBegin.R.multiply(kF.fittedStateBegin().helix.Rot.invert()); @@ -1039,8 +1100,12 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { DMatrixRMaj aFC = kF.fittedStateBegin().covariancePivotTransform(aF, 1.0); CommonOps_DDRM.multTransB(aFC, fRot, tempM1); CommonOps_DDRM.mult(fRot, tempM1, aFC); - for (int i = 0; i < 5; i++) aFe.v[i] = Math.sqrt(Math.max(0., aFC.unsafe_get(i,i))); - if (verbose) { aFe.print("error estimates on the smoothed helix parameters"); } + for (int i = 0; i < 5; i++) { + aFe.v[i] = Math.sqrt(Math.max(0., aFC.unsafe_get(i, i))); + } + if (verbose) { + aFe.print("error estimates on the smoothed helix parameters"); + } Vec trueErr = aF.dif(helixBegin.p); if (verbose) { for (int i = 0; i < 5; i++) { @@ -1072,7 +1137,9 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } DMatrixRMaj eFc = kF.fittedStateEnd().covariancePivotTransform(eF, 1.0); Vec eFe = new Vec(5); - for (int i = 0; i < 5; i++) eFe.v[i] = Math.sqrt(Math.max(0., eFc.unsafe_get(i,i))); + for (int i = 0; i < 5; i++) { + eFe.v[i] = Math.sqrt(Math.max(0., eFc.unsafe_get(i, i))); + } Vec pivotF = new Vec(3); Vec fH = TkEnd.helixParameters(TkEnd.x, pivotF); trueErr = eF.dif(fH); @@ -1091,27 +1158,33 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hEtanl.entry(trueErr.v[4] / eFe.v[4]); trueErr = eF.dif(fH); helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(eFc).invert())); - if (verbose) { System.out.format("Full chi^2 of the filtered helix parameters = %12.4e\n", helixChi2); } + if (verbose) { + System.out.format("Full chi^2 of the filtered helix parameters = %12.4e\n", helixChi2); + } hChi2Helix.entry(helixChi2); // Study the fitted helix extrapolated back to the origin double[] hP = KalmanTrack.originHelixParms(); - testHelix = new Vec(5,hP); + testHelix = new Vec(5, hP); testCov = new DMatrixRMaj(KalmanTrack.originCovariance()); double[] hErr = new double[5]; - for (int i = 0; i < 5; ++i) { hErr[i] = (hP[i] - TkInitial.p.v[i]); } + for (int i = 0; i < 5; ++i) { + hErr[i] = (hP[i] - TkInitial.p.v[i]); + } double[] hErrC = new double[5]; - for (int i = 0; i < 5; ++i) { hErrC[i] = (constrainedHelix.a.v[i] - TkInitial.p.v[i]); } + for (int i = 0; i < 5; ++i) { + hErrC[i] = (constrainedHelix.a.v[i] - TkInitial.p.v[i]); + } hEdrhoO.entry(hErr[0] / KalmanTrack.helixErr(0)); hEphi0O.entry(hErr[1] / KalmanTrack.helixErr(1)); hEkO.entry(hErr[2] / KalmanTrack.helixErr(2)); hEdzO.entry(hErr[3] / KalmanTrack.helixErr(3)); hEtanlO.entry(hErr[4] / KalmanTrack.helixErr(4)); - hEdrhoSigO.entry(hErrC[0] / Math.sqrt(constrainedHelix.C.unsafe_get(0,0))); - hEphi0SigO.entry(hErrC[1] / Math.sqrt(constrainedHelix.C.unsafe_get(1,1))); - hEkSigO.entry(hErrC[2] / Math.sqrt(constrainedHelix.C.unsafe_get(2,2))); - hEdzSigO.entry(hErrC[3] / Math.sqrt(constrainedHelix.C.unsafe_get(3,3))); - hEtanlSigO.entry(hErrC[4] / Math.sqrt(constrainedHelix.C.unsafe_get(4,4))); + hEdrhoSigO.entry(hErrC[0] / Math.sqrt(constrainedHelix.C.unsafe_get(0, 0))); + hEphi0SigO.entry(hErrC[1] / Math.sqrt(constrainedHelix.C.unsafe_get(1, 1))); + hEkSigO.entry(hErrC[2] / Math.sqrt(constrainedHelix.C.unsafe_get(2, 2))); + hEdzSigO.entry(hErrC[3] / Math.sqrt(constrainedHelix.C.unsafe_get(3, 3))); + hEtanlSigO.entry(hErrC[4] / Math.sqrt(constrainedHelix.C.unsafe_get(4, 4))); hEadrhoO.entry(hErr[0]); hEaphi0O.entry(hErr[1]); hEakO.entry(hErr[2]); @@ -1158,29 +1231,31 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { } } // Study the effect of an energy constraint - MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size()-1); + MeasurementSite lastSite = KalmanTrack.SiteList.get(KalmanTrack.SiteList.size() - 1); if (verbose) { - lastSite.print("last site on track"); - helixEnd.print("true helix at last site on track"); - System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n",Etrue,E,sigmaE); + lastSite.print("last site on track"); + helixEnd.print("true helix at last site on track"); + System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n", Etrue, E, sigmaE); KalmanTrack.helixAtOrigin.a.print("helix at origin"); TkInitial.p.print("true helix"); KalmanTrack.helixAtOriginEconstraint.a.print("energy constrained helix"); KalmanTrack.printLong("after adding energy constraint"); } hChi2E.entry(KalmanTrack.chi2_Econstraint); - for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.helixAtOriginEconstraint.a.v[i] - TkInitial.p.v[i]); + for (int i = 0; i < 5; ++i) { + hErr[i] = (KalmanTrack.helixAtOriginEconstraint.a.v[i] - TkInitial.p.v[i]); + } hEatanlcon.entry(hErr[4]); hEakcon.entry(hErr[2]); hERhocon.entry(hErr[0]); hEPhi0con.entry(hErr[1]); hEZ0con.entry(hErr[3]); - hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(0,0))); - hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(1,1))); - hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(2,2))); - hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(3,3))); - hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(4,4))); - trueErr = new Vec(5,hErr); + hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(0, 0))); + hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(1, 1))); + hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(2, 2))); + hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(3, 3))); + hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(4, 4))); + trueErr = new Vec(5, hErr); helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(KalmanTrack.helixAtOriginEconstraint.C).invert())); hChi2HelixE.entry(helixChi2); } @@ -1195,7 +1270,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()); System.out.format("%s %d %d at %d:%d %d.%d seconds\n", ldt.getMonth(), ldt.getDayOfMonth(), ldt.getYear(), ldt.getHour(), ldt.getMinute(), ldt.getSecond(), ldt.getNano()); - double endTime = (double)(ldt.getMinute())*60. + (double)(ldt.getSecond()) + (double)(ldt.getNano())/1e9; + double endTime = (double) (ldt.getMinute()) * 60. + (double) (ldt.getSecond()) + (double) (ldt.getNano()) / 1e9; double elapsedTime = endTime - startTime; System.out.format("Total elapsed time = %10.5f\n", elapsedTime); System.out.format("Elapsed time for Kalman Filter = %10.4f ms\n", executionTime); @@ -1301,8 +1376,8 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hPropz1.plot(path + "propz1.gp", true, "gaus", " "); hPropx1s.plot(path + "propx1s.gp", true, "gaus", " "); hPropz1s.plot(path + "propz1s.gp", true, "gaus", " "); - - /* + + /* // Test of helix covariance extrapolation if (testCov != null && testHelix != null) { Vec X0initial = new Vec(0.,0.,0.); @@ -1504,9 +1579,9 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { hTanLpF.plot(path + "TestTanlpF.gp", true, "gaus", " "); System.out.println("All Done!"); } - */ + */ } - + /* double[] gausRan() { // Return two gaussian random numbers @@ -1525,5 +1600,5 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { return gran; } - */ + */ } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index dbef9c983a..ef8d99e6c1 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -23,6 +23,7 @@ * Track followed and fitted by the Kalman filter */ public class KalTrack { + public int ID; public int nHits; public double chi2; @@ -31,7 +32,7 @@ public class KalTrack { ArrayList SiteList; // call the corresponding functions to create and access the following two maps - private Map interceptVects; + private Map interceptVects; private Map interceptMomVects; Map millipedeMap; Map lyrMap; @@ -60,66 +61,73 @@ public class KalTrack { private static DMatrixRMaj Cinv; private static Logger logger; private static boolean initialized; - private double [] arcLength, arcLengthE; + private double[] arcLength, arcLengthE; private static LinearSolverDense solver; private static final boolean uniformBatOrigin = false; - static int [] nBadCov = {0, 0}; + static int[] nBadCov = {0, 0}; /** * Track constructor - * @param evtNumb event number - * @param tkID integer ID for the track - * @param SiteList list of measurement sites - * @param yScat array of scattering planes to propagate through - * @param XLscat scattering radiation lengths at each plane - * @param kPar KalmanParams instance + * + * @param evtNumb event number + * @param tkID integer ID for the track + * @param SiteList list of measurement sites + * @param yScat array of scattering planes to propagate through + * @param XLscat scattering radiation lengths at each plane + * @param kPar KalmanParams instance */ KalTrack(int evtNumb, int tkID, ArrayList SiteList, ArrayList yScat, ArrayList XLscat, KalmanParams kPar) { // System.out.format("KalTrack constructor chi2=%10.6f\n", chi2); eventNumber = evtNumb; bad = false; this.yScat = yScat; - this.XLscat = XLscat; + this.XLscat = XLscat; this.kPar = kPar; ID = tkID; arcLength = null; //debug = (evtNumb == 217481); - + if (!initialized) { logger = Logger.getLogger(KalTrack.class.getName()); - if (tempV == null) tempV = new DMatrixRMaj(5,1); - Cinv = new DMatrixRMaj(5,5); + if (tempV == null) { + tempV = new DMatrixRMaj(5, 1); + } + Cinv = new DMatrixRMaj(5, 5); initialized = true; solver = LinearSolverFactory_DDRM.symmPosDef(5); } - + // Trim empty sites from the track ends Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); int firstSite = -1; - for (int idx=0; idx= 0) break; + if (SiteList.get(idx).hitID >= 0) { + break; + } } int lastSite = 999; - for (int idx = SiteList.size()-1; idx >= 0; --idx) { + for (int idx = SiteList.size() - 1; idx >= 0; --idx) { lastSite = idx; - if (SiteList.get(idx).hitID >= 0) break; + if (SiteList.get(idx).hitID >= 0) { + break; + } } - + // Make a new list of sites, without empty sites at beginning or end this.SiteList = new ArrayList(SiteList.size()); - for (int idx=firstSite; idx<=lastSite; ++idx) { + for (int idx = firstSite; idx <= lastSite; ++idx) { MeasurementSite site = SiteList.get(idx); if (site.aS == null) { // This should never happen - logger.log(Level.SEVERE, String.format("Event %d: site of track %d is missing smoothed state vector for layer %d detector %d", + logger.log(Level.SEVERE, String.format("Event %d: site of track %d is missing smoothed state vector for layer %d detector %d", eventNumber, ID, site.m.Layer, site.m.detector)); logger.log(Level.WARNING, site.toString("bad site")); bad = true; continue; } - this.SiteList.add(site); + this.SiteList.add(site); } - + helixAtOrigin = null; helixAtOriginEconstraint = null; propagated = false; @@ -129,7 +137,7 @@ public class KalTrack { if (kPar.uniformB) { B = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), site0.m.Bfield); } else { - B = KalmanInterface.getField(new Vec(3,kPar.beamSpot), site0.m.Bfield); + B = KalmanInterface.getField(new Vec(3, kPar.beamSpot), site0.m.Bfield); } Bmag = B.mag(); tB = B.unitVec(Bmag); @@ -142,31 +150,37 @@ public class KalTrack { double c = 2.99793e8; // Speed of light in m/s alpha = 1.0e12 / (c * Bmag); // Convert from pt in GeV to curvature in mm Cx = null; - Cp = null; + Cp = null; // Fill the maps time = 0.; tMin = 9.9e9; tMax = -9.9e9; this.chi2 = 0.; this.nHits = 0; - if (debug) System.out.format("KalTrack: event %d, creating track %d\n", evtNumb, ID); + if (debug) { + System.out.format("KalTrack: event %d, creating track %d\n", evtNumb, ID); + } for (MeasurementSite site : this.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } nHits++; time += site.m.hits.get(site.hitID).time; tMin = Math.min(tMin, site.m.hits.get(site.hitID).time); - tMax = Math.max(tMax, site.m.hits.get(site.hitID).time); + tMax = Math.max(tMax, site.m.hits.get(site.hitID).time); this.chi2 += site.chi2inc; - if (debug) System.out.format(" Layer %d, chi^2 increment=%10.5f, a=%s\n", site.m.Layer, site.chi2inc, site.aS.helix.a.toString()); + if (debug) { + System.out.format(" Layer %d, chi^2 increment=%10.5f, a=%s\n", site.m.Layer, site.chi2inc, site.aS.helix.a.toString()); + } } - time = time/(double)nHits; - reducedChi2 = chi2/(double)nHits; + time = time / (double) nHits; + reducedChi2 = chi2 / (double) nHits; lyrMap = null; millipedeMap = null; interceptVects = null; interceptMomVects = null; if (nHits < 5) { // This should never happen - logger.log(Level.WARNING, "KalTrack error: not enough hits ("+nHits+") on the candidate track (ID::"+ID+") for event "+eventNumber); + logger.log(Level.WARNING, "KalTrack error: not enough hits (" + nHits + ") on the candidate track (ID::" + ID + ") for event " + eventNumber); bad = true; //for (MeasurementSite site : SiteList) logger.log(Level.FINE, site.toString("in KalTrack input list")); //logger.log(Level.FINE, String.format("KalTrack error in event %d: not enough hits on track %d: ",evtNumb,tkID)); @@ -181,74 +195,96 @@ public class KalTrack { /** * Get the time of the track - * @return time in ns + * + * @return time in ns */ public double getTime() { return time; } - + /** - * Make a map between measurement sites and 3-vector intercepts of the track at the SSD planes of those sites + * Make a map between measurement sites and 3-vector intercepts of the track + * at the SSD planes of those sites */ public Map interceptVects() { if (interceptVects == null) { interceptVects = new HashMap(nHits); for (MeasurementSite site : this.SiteList) { StateVector sV = null; - if (site.smoothed) sV = site.aS; - else sV = site.aP; + if (site.smoothed) { + sV = site.aS; + } else { + sV = site.aP; + } double phiS = sV.helix.planeIntersect(site.m.p); - if (Double.isNaN(phiS)) phiS = 0.; - interceptVects.put(site, sV.helix.toGlobal(sV.helix.atPhi(phiS))); + if (Double.isNaN(phiS)) { + phiS = 0.; + } + interceptVects.put(site, sV.helix.toGlobal(sV.helix.atPhi(phiS))); } } return interceptVects; } - + /** - * Make a map between measurement sites and 3-vector momentum at the intercept of the track and SSD plane + * Make a map between measurement sites and 3-vector momentum at the + * intercept of the track and SSD plane */ public Map interceptMomVects() { if (interceptMomVects == null) { interceptMomVects = new HashMap(); for (MeasurementSite site : this.SiteList) { StateVector sV = null; - if (site.smoothed) sV = site.aS; - else sV = site.aP; + if (site.smoothed) { + sV = site.aS; + } else { + sV = site.aP; + } double phiS = sV.helix.planeIntersect(site.m.p); - if (Double.isNaN(phiS)) phiS = 0.; + if (Double.isNaN(phiS)) { + phiS = 0.; + } interceptMomVects.put(site, sV.helix.Rot.inverseRotate(sV.helix.getMom(phiS))); } } return interceptMomVects; } - + /** - * Calculate and return the intersection point of the Kaltrack with an SiModule. - // Local sensor coordinates (u,v) are returned. - // The global intersection can be returned via rGbl if an array of length 3 is passed. - * @param mod module - * @param rGbl returned global intersection point - * @return intersection point + * Calculate and return the intersection point of the Kaltrack with an + * SiModule. // Local sensor coordinates (u,v) are returned. // The global + * intersection can be returned via rGbl if an array of length 3 is passed. + * + * @param mod module + * @param rGbl returned global intersection point + * @return intersection point */ - public double [] moduleIntercept(SiModule mod, double [] rGbl) { + public double[] moduleIntercept(SiModule mod, double[] rGbl) { HelixState hx = null; for (MeasurementSite site : SiteList) { - if (site.m == mod) hx = site.aS.helix; + if (site.m == mod) { + hx = site.aS.helix; + } } if (hx == null) { int mxLayer = -1; for (MeasurementSite site : SiteList) { - if (site.m.Layer > mod.Layer) continue; + if (site.m.Layer > mod.Layer) { + continue; + } if (site.m.Layer > mxLayer) { mxLayer = site.m.Layer; hx = site.aS.helix; } } } - if (hx == null) hx = SiteList.get(0).aS.helix; + if (hx == null) { + hx = SiteList.get(0).aS.helix; + } double phiS = hx.planeIntersect(mod.p); - if (Double.isNaN(phiS)) phiS = 0.; + if (Double.isNaN(phiS)) { + phiS = 0.; + } Vec intGlb = hx.toGlobal(hx.atPhi(phiS)); if (rGbl != null) { rGbl[0] = intGlb.v[0]; @@ -256,10 +292,10 @@ public Map interceptMomVects() { rGbl[2] = intGlb.v[2]; } Vec intLcl = mod.toLocal(intGlb); - double [] rtnArray = {intLcl.v[0], intLcl.v[1]}; + double[] rtnArray = {intLcl.v[0], intLcl.v[1]}; return rtnArray; } - + /** * Make a map between the track measurement sites and the tracker layers */ @@ -269,9 +305,10 @@ private void makeLyrMap() { lyrMap.put(site.m.Layer, site); } } - + /** - * Make a map between the measurement sites and the corresponding Millipede IDs + * Make a map between the measurement sites and the corresponding Millipede + * IDs */ private void makeMillipedeMap() { millipedeMap = new HashMap(nHits); @@ -279,72 +316,112 @@ private void makeMillipedeMap() { millipedeMap.put(site.m.millipedeID, site); } } - + /** - * Find the change in smoothed helix angle in XY between one layer and the next - * @param layer layer from 0 to 13 - * @return difference angle in XY + * Find the change in smoothed helix angle in XY between one layer and the + * next + * + * @param layer layer from 0 to 13 + * @return difference angle in XY */ public double scatX(int layer) { - if (lyrMap == null) makeLyrMap(); - if (!lyrMap.containsKey(layer)) return -999.; + if (lyrMap == null) { + makeLyrMap(); + } + if (!lyrMap.containsKey(layer)) { + return -999.; + } int lyrNxt = layer + 1; - while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) lyrNxt++; - if (lyrNxt > 13) return -999.; + while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) { + lyrNxt++; + } + if (lyrNxt > 13) { + return -999.; + } MeasurementSite s1 = lyrMap.get(layer); MeasurementSite s2 = lyrMap.get(lyrNxt); - if (s1.aS == null || s2.aS == null) return -999.; + if (s1.aS == null || s2.aS == null) { + return -999.; + } double phiS1 = s1.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS1)) return -999.; + if (Double.isNaN(phiS1)) { + return -999.; + } Vec p1 = s1.aS.helix.getMom(phiS1); double t1 = FastMath.atan2(p1.v[0], p1.v[1]); double phiS2 = s2.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS2)) return -999.; + if (Double.isNaN(phiS2)) { + return -999.; + } Vec p2 = s2.aS.helix.getMom(phiS2); double t2 = FastMath.atan2(p2.v[0], p2.v[1]); return t1 - t2; } /** - * Find the change in smoothed helix angle in ZY between one layer and the next - * @param layer layer from 0 to 13 - * @return difference angle in ZY + * Find the change in smoothed helix angle in ZY between one layer and the + * next + * + * @param layer layer from 0 to 13 + * @return difference angle in ZY */ public double scatZ(int layer) { - if (lyrMap == null) makeLyrMap(); - if (!lyrMap.containsKey(layer)) return -999.; + if (lyrMap == null) { + makeLyrMap(); + } + if (!lyrMap.containsKey(layer)) { + return -999.; + } int lyrNxt = layer + 1; - while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) lyrNxt++; - if (lyrNxt > 13) return -999.; + while (lyrNxt <= 13 && !lyrMap.containsKey(lyrNxt)) { + lyrNxt++; + } + if (lyrNxt > 13) { + return -999.; + } MeasurementSite s1 = lyrMap.get(layer); MeasurementSite s2 = lyrMap.get(lyrNxt); - if (s1.aS == null || s2.aS == null) return -999.; + if (s1.aS == null || s2.aS == null) { + return -999.; + } double phiS1 = s1.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS1)) return -999.; + if (Double.isNaN(phiS1)) { + return -999.; + } Vec p1 = s1.aS.helix.Rot.inverseRotate(s1.aS.helix.getMom(phiS1)); double t1 = FastMath.atan2(p1.v[2], p1.v[1]); double phiS2 = s2.aS.helix.planeIntersect(s2.m.p); - if (Double.isNaN(phiS2)) return -999.; + if (Double.isNaN(phiS2)) { + return -999.; + } Vec p2 = s2.aS.helix.Rot.inverseRotate(s2.aS.helix.getMom(phiS2)); double t2 = FastMath.atan2(p2.v[2], p2.v[1]); return t1 - t2; } /** - * Alternative calculation of the fit chi^2, considering only residuals divided by the hit sigma - * @return chi-squared + * Alternative calculation of the fit chi^2, considering only residuals + * divided by the hit sigma + * + * @return chi-squared */ - public double chi2prime() { + public double chi2prime() { double c2 = 0.; for (MeasurementSite S : SiteList) { - if (S.aS == null) continue; + if (S.aS == null) { + continue; + } double phiS = S.aS.helix.planeIntersect(S.m.p); - if (Double.isNaN(phiS)) { phiS = 0.; } + if (Double.isNaN(phiS)) { + phiS = 0.; + } double vpred = S.h(S.aS, S.m, phiS); for (Measurement hit : S.m.hits) { - for (KalTrack tkr : hit.tracks) { - if (tkr.equals(this)) c2 += FastMath.pow((vpred - hit.v) / hit.sigma, 2); + for (KalTrack tkr : hit.tracks) { + if (tkr.equals(this)) { + c2 += FastMath.pow((vpred - hit.v) / hit.sigma, 2); + } } } } @@ -353,47 +430,57 @@ public double chi2prime() { /** * Get track unbiased residuals by Millipede ID + * * @param millipedeID * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidualMillipede(int millipedeID, boolean eConstrain) { - if (millipedeMap == null) makeMillipedeMap(); + public Pair unbiasedResidualMillipede(int millipedeID, boolean eConstrain) { + if (millipedeMap == null) { + makeMillipedeMap(); + } if (millipedeMap.containsKey(millipedeID)) { return unbiasedResidual(millipedeMap.get(millipedeID), eConstrain); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } } - + /** * Get track unbiased residual by layer number + * * @param layer * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidual(int layer, boolean eConstrain) { - if (lyrMap == null) makeLyrMap(); + public Pair unbiasedResidual(int layer, boolean eConstrain) { + if (lyrMap == null) { + makeLyrMap(); + } if (lyrMap.containsKey(layer)) { return unbiasedResidual(lyrMap.get(layer), eConstrain); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } - } - + } + /** - * Returns the unbiased residual for the track at a given site, together with the variance on that residual - * @param site measurement site - * @return residual and error + * Returns the unbiased residual for the track at a given site, together + * with the variance on that residual + * + * @param site measurement site + * @return residual and error */ - public Pair unbiasedResidual(MeasurementSite site, boolean eConstrain) { + public Pair unbiasedResidual(MeasurementSite site, boolean eConstrain) { double resid = -999.; - double varResid = -999.; - Vec aStar = null; + double varResid = -999.; + Vec aStar = null; if (site.hitID >= 0) { - StateVector aA = site.aS; - if (eConstrain) aA = site.aES; + StateVector aA = site.aS; + if (eConstrain) { + aA = site.aES; + } double sigma = site.m.hits.get(site.hitID).sigma; - DMatrixRMaj Cstar = new DMatrixRMaj(5,5); - aStar = aA.inverseFilter(site.H, sigma*sigma, Cstar); + DMatrixRMaj Cstar = new DMatrixRMaj(5, 5); + aStar = aA.inverseFilter(site.H, sigma * sigma, Cstar); HelixPlaneIntersect hpi = new HelixPlaneIntersect(); Plane pTrans = site.m.p.toLocal(aA.helix.Rot, aA.helix.origin); double phiInt = hpi.planeIntersect(aStar, aA.helix.X0, aA.helix.alpha, pTrans); @@ -402,65 +489,78 @@ public Pair unbiasedResidual(MeasurementSite site, boolean eConst Vec globalInt = aA.helix.toGlobal(intPnt); Vec localInt = site.m.toLocal(globalInt); resid = site.m.hits.get(site.hitID).v - localInt.v[1]; - - CommonOps_DDRM.mult(Cstar, site.H, tempV); - varResid = sigma*sigma + CommonOps_DDRM.dot(site.H, tempV); + + CommonOps_DDRM.mult(Cstar, site.H, tempV); + varResid = sigma * sigma + CommonOps_DDRM.dot(site.H, tempV); } - } - return new Pair(resid, varResid); + } + return new Pair(resid, varResid); } /** - * Returns the biased residual for the track at a given layer, together with the variance on that residual - * @param layer - * @return biased residual and error + * Returns the biased residual for the track at a given layer, together with + * the variance on that residual + * + * @param layer + * @return biased residual and error */ - public Pair biasedResidual(int layer) { - if (lyrMap == null) makeLyrMap(); + public Pair biasedResidual(int layer) { + if (lyrMap == null) { + makeLyrMap(); + } if (lyrMap.containsKey(layer)) { return biasedResidual(lyrMap.get(layer)); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } } /** * Get track biased residuals by Millipede ID + * * @param millipedeID * @return pair of biased residual and its error estimate */ - public Pair biasedResidualMillipede(int millipedeID) { - if (millipedeMap == null) makeMillipedeMap(); + public Pair biasedResidualMillipede(int millipedeID) { + if (millipedeMap == null) { + makeMillipedeMap(); + } if (millipedeMap.containsKey(millipedeID)) { return biasedResidual(millipedeMap.get(millipedeID)); - } else { - return new Pair(-999., -999.); + } else { + return new Pair(-999., -999.); } } /** * Get track biased residuals by measurement site + * * @param site * @return pair of biased residual and its error estimate */ - public Pair biasedResidual(MeasurementSite site) { + public Pair biasedResidual(MeasurementSite site) { double resid = -999.; - double varResid = -999.; + double varResid = -999.; if (site.aS != null) { resid = site.aS.r; varResid = site.aS.R; } - return new Pair(resid, varResid); + return new Pair(resid, varResid); } - + /** * Relatively short debug printout - * @param s Arbitrary string for the user's reference + * + * @param s Arbitrary string for the user's reference */ public void print(String s) { System.out.format("\nKalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); - if (propagated) System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); - if (propagatedE) System.out.format(" Helix parameters at origin energy constrainted = %s\n", helixAtOriginEconstraint.a.toString()); + if (propagated) { + System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); + } + if (propagatedE) { + System.out.format(" Helix parameters at origin energy constrainted = %s\n", helixAtOriginEconstraint.a.toString()); + } System.out.format(" Magnetic field magnitude = %10.5f and direction = %s\n", Bmag, tB.toString()); MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { @@ -472,12 +572,12 @@ public void print(String s) { int hitID = site.hitID; System.out.format(" Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f ", m.Layer, m.detector, m.isStereo, site.chi2inc); - if (hitID>=0) { + if (hitID >= 0) { System.out.format(" t=%5.1f ", site.m.hits.get(site.hitID).time); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site, false); - double [] lclint = moduleIntercept(m, null); - System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, + Pair unBiasedResid = unbiasedResidual(site, false); + double[] lclint = moduleIntercept(m, null); + System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, residual, unBiasedResid.getFirstElement(), lclint[0], m.xExtent[1]); } else { System.out.format("\n"); @@ -487,36 +587,38 @@ public void print(String s) { /** * Long detailed debug printout - * @param s Arbitrary string for the user's reference + * + * @param s Arbitrary string for the user's reference */ public void printLong(String s) { - System.out.format("%s", this.toString(s)); + System.out.format("%s", this.toString(s)); } /** * Long detailed debug printout to a string - * @param s Arbitrary string for the user's reference + * + * @param s Arbitrary string for the user's reference */ String toString(String s) { String str = String.format("\n KalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); if (propagated) { - str=str+String.format(" B-field at the origin=%10.6f, direction=%8.6f %8.6f %8.6f\n", Bmag, tB.v[0], tB.v[1], tB.v[2]); - str=str+helixAtOrigin.toString("helix state for a pivot at the origin")+"\n"; - str=str+originPoint.toString("point on the helix closest to the origin")+"\n"; - str=str+String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); + str = str + String.format(" B-field at the origin=%10.6f, direction=%8.6f %8.6f %8.6f\n", Bmag, tB.v[0], tB.v[1], tB.v[2]); + str = str + helixAtOrigin.toString("helix state for a pivot at the origin") + "\n"; + str = str + originPoint.toString("point on the helix closest to the origin") + "\n"; + str = str + String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); SquareMatrix C1 = new SquareMatrix(3, Cx); - str=str+C1.toString("covariance matrix for the point"); - str=str+originMomentum.toString("momentum of the particle at closest approach to the origin\n"); + str = str + C1.toString("covariance matrix for the point"); + str = str + originMomentum.toString("momentum of the particle at closest approach to the origin\n"); SquareMatrix C2 = new SquareMatrix(3, Cp); - str=str+C2.toString("covariance matrix for the momentum"); + str = str + C2.toString("covariance matrix for the momentum"); double tanL = helixAtOrigin.a.v[4]; double K = helixAtOrigin.a.v[2]; - double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); - str=str+String.format(" Energy unconstrained = %10.6f\n", energy); - } + double energy = FastMath.sqrt(1.0 + tanL * tanL) / Math.abs(K); + str = str + String.format(" Energy unconstrained = %10.6f\n", energy); + } if (propagatedE) { - str=str+String.format("Chi-squared of energy constrained fit = %10.5f\n", chi2_Econstraint); - str=str+helixAtOriginEconstraint.toString("E-constrained helix state for a pivot at the origin")+"\n"; + str = str + String.format("Chi-squared of energy constrained fit = %10.5f\n", chi2_Econstraint); + str = str + helixAtOriginEconstraint.toString("E-constrained helix state for a pivot at the origin") + "\n"; //str=str+originPoint.toString("point on the helix closest to the origin")+"\n"; //str=str+String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); //SquareMatrix C1 = new SquareMatrix(3, Cx); @@ -526,49 +628,52 @@ String toString(String s) { //str=str+C2.toString("covariance matrix for the momentum"); double tanL = helixAtOriginEconstraint.a.v[4]; double K = helixAtOriginEconstraint.a.v[2]; - double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); - str=str+String.format(" Energy constrained = %10.6f\n", energy); - } + double energy = FastMath.sqrt(1.0 + tanL * tanL) / Math.abs(K); + str = str + String.format(" Energy constrained = %10.6f\n", energy); + } MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { - str=str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); - str=str + String.format(" Energy constrained helix at layer %d: %s\n", site0.m.Layer, site0.aES.helix.a.toString()); + str = str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); + str = str + String.format(" Energy constrained helix at layer %d: %s\n", site0.m.Layer, site0.aES.helix.a.toString()); } for (int i = 0; i < SiteList.size(); i++) { MeasurementSite site = SiteList.get(i); SiModule m = site.m; int hitID = site.hitID; - str=str+String.format("Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f, Xscat=%10.8f Zscat=%10.8f, arc=%10.5f, hit=%d", m.Layer, m.detector, m.isStereo, + str = str + String.format("Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f, Xscat=%10.8f Zscat=%10.8f, arc=%10.5f, hit=%d", m.Layer, m.detector, m.isStereo, site.chi2inc, site.scatX(), site.scatZ(), site.arcLength, hitID); if (hitID < 0) { - str=str+"\n"; + str = str + "\n"; continue; } - str=str+String.format(", t=%5.1f", site.m.hits.get(site.hitID).time); + str = str + String.format(", t=%5.1f", site.m.hits.get(site.hitID).time); if (m.hits.get(hitID).tksMC != null) { - str=str+String.format(" MC tracks: "); + str = str + String.format(" MC tracks: "); for (int iMC : m.hits.get(hitID).tksMC) { - str=str+String.format(" %d ", iMC); + str = str + String.format(" %d ", iMC); } - str=str+"\n"; + str = str + "\n"; } if (interceptVects().containsKey(site)) { Vec interceptVec = interceptVects().get(site); Vec interceptMomVec = interceptMomVects().get(site); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site, false); - str=str+String.format(" Intercept=%s, p=%s, measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f+-%9.5f, error=%9.5f \n", interceptVec.toString(), + Pair unBiasedResid = unbiasedResidual(site, false); + str = str + String.format(" Intercept=%s, p=%s, measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f+-%9.5f, error=%9.5f \n", interceptVec.toString(), interceptMomVec.toString(), site.m.hits.get(hitID).v, site.aS.mPred, residual, unBiasedResid.getFirstElement(), unBiasedResid.getSecondElement(), FastMath.sqrt(site.aS.R)); } } - str=str+String.format("End of printing for KalTrack %s ID %d in event %d\n\n", s, ID, eventNumber); + str = str + String.format("End of printing for KalTrack %s ID %d in event %d\n\n", s, ID, eventNumber); return str; } /** - * Method to make simple yz plots of the track and the residuals. Note that in the yz plot of the track the hits are - * placed at the strip center, so they generally will not appear to be right on the track. Use gnuplot to display. - * @param path where to put the output + * Method to make simple yz plots of the track and the residuals. Note that + * in the yz plot of the track the hits are placed at the strip center, so + * they generally will not appear to be right on the track. Use gnuplot to + * display. + * + * @param path where to put the output */ public void plot(String path) { File file = new File(String.format("%s/Track%d_%d.gp", path, ID, eventNumber)); @@ -619,13 +724,19 @@ public void plot(String path) { pW.format("set yrange[-0.025 : 0.025]\n"); pW.format("$resids << EOD\n"); for (MeasurementSite site : SiteList) { - if (site.m.Layer < 0) continue; + if (site.m.Layer < 0) { + continue; + } double phiS = site.aS.helix.planeIntersect(site.m.p); - if (Double.isNaN(phiS)) { continue; } + if (Double.isNaN(phiS)) { + continue; + } Vec rHelixG = site.aS.helix.toGlobal(site.aS.helix.atPhi(phiS)); Vec rHelixL = site.m.toLocal(rHelixG); double residual = -999.; - if (site.hitID >= 0) residual = site.m.hits.get(site.hitID).v - rHelixL.v[1]; + if (site.hitID >= 0) { + residual = site.m.hits.get(site.hitID).v - rHelixL.v[1]; + } pW.format(" %10.5f %10.6f # %10.6f\n", rHelixG.v[1], residual, site.aS.r); } pW.format("EOD\n"); @@ -635,20 +746,28 @@ public void plot(String path) { /** * Arc length along track from the origin to the first measurement - * @return arc length in mm + * + * @return arc length in mm */ public double originArcLength() { - if (!propagated || arcLength == null) originHelix(); - if (arcLength == null) return 0.; + if (!propagated || arcLength == null) { + originHelix(); + } + if (arcLength == null) { + return 0.; + } return arcLength[0]; } - + /** * Runge Kutta propagation of the helix to the origin - * @return helix state at the origin + * + * @return helix state at the origin */ public boolean originHelix() { - if (propagated) return true; + if (propagated) { + return true; + } // Find the measurement site closest to the origin (target) MeasurementSite innerSite = null; @@ -669,13 +788,13 @@ public boolean originHelix() { return false; } Vec beamSpot = new Vec(3, kPar.beamSpot); - + // This propagated helix will have its pivot at the origin but is in the origin B-field frame // The StateVector method propagateRungeKutta transforms the origin plane into the origin B-field frame - Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); + Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); arcLength = new double[1]; if (uniformBatOrigin) { // Just a simple pivot transform is needed if the field is assumed uniform - Vec origin = new Vec(0.,0.,0.); + Vec origin = new Vec(0., 0., 0.); Vec newHelixParams = innerSite.aS.helix.pivotTransform(); DMatrixRMaj newCovariance = innerSite.aS.covariancePivotTransform(newHelixParams, 1.0); helixAtOrigin = new HelixState(newHelixParams, origin, origin, newCovariance, Bmag, tB); @@ -684,21 +803,25 @@ public boolean originHelix() { } else { helixAtOrigin = innerSite.aS.helix.propagateRungeKutta(originPlane, yScat, XLscat, innerSite.m.Bfield, arcLength); //helixAtOrigin.print("nonuniformB"); - if (debug) System.out.format("KalTrack::originHelix: arc length to the first measurement = %9.4f\n", arcLength[0]); - if (covNaN()) return false; + if (debug) { + System.out.format("KalTrack::originHelix: arc length to the first measurement = %9.4f\n", arcLength[0]); + } + if (covNaN()) { + return false; + } if (!solver.setA(helixAtOrigin.C.copy())) { logger.fine("KalTrack:originHelix, cannot invert the covariance matrix"); - for (int i=0; i<5; ++i) { // Fill the matrix and inverse with something not too crazy and continue . . . - for (int j=0; j<5; ++j) { + for (int i = 0; i < 5; ++i) { // Fill the matrix and inverse with something not too crazy and continue . . . + for (int j = 0; j < 5; ++j) { if (i == j) { - Cinv.unsafe_set(i,j,1.0/Math.abs(helixAtOrigin.C.unsafe_get(i, j))); + Cinv.unsafe_set(i, j, 1.0 / Math.abs(helixAtOrigin.C.unsafe_get(i, j))); helixAtOrigin.C.unsafe_set(i, j, Math.abs(helixAtOrigin.C.unsafe_get(i, j))); } else { Cinv.unsafe_set(i, j, 0.); helixAtOrigin.C.unsafe_set(i, j, 0.); } } - } + } } else { solver.invert(Cinv); } @@ -735,17 +858,23 @@ public boolean originHelix() { /** * Get the extrapolate point in the plane of the origin - * @return 3-vector array of coordinates + * + * @return 3-vector array of coordinates */ public double[] originX() { - if (!propagated) originHelix(); - if (!propagated) return new double [] {0.,0.,0.}; + if (!propagated) { + originHelix(); + } + if (!propagated) { + return new double[]{0., 0., 0.}; + } return originPoint.v.clone(); } /** * Get the covariance of the track point in the origin plane - * @return 2D array, 3 by 3 + * + * @return 2D array, 3 by 3 */ public double[][] originXcov() { return Cx.clone(); @@ -753,17 +882,23 @@ public double[][] originXcov() { /** * Get the track momentum at the origin - * @return 3-vector momentum + * + * @return 3-vector momentum */ public double[] originP() { - if (!propagated) originHelix(); - if (!propagated) return new double [] {0.,0.,0.}; + if (!propagated) { + originHelix(); + } + if (!propagated) { + return new double[]{0., 0., 0.}; + } return originMomentum.v.clone(); } /** * Get the covariance of the momentum at the origin - * @return 3 by 3 array + * + * @return 3 by 3 array */ public double[][] originPcov() { return Cp.clone(); @@ -771,24 +906,34 @@ public double[][] originPcov() { /** * Get the helix pivot point near the origin - * @return 3-vector array + * + * @return 3-vector array */ public double[] originPivot() { - if (propagated) return helixAtOrigin.X0.v.clone(); - else return null; + if (propagated) { + return helixAtOrigin.X0.v.clone(); + } else { + return null; + } } - + /** * Get the covariance of helix parameters at the origin - * @return 5 by 5 array + * + * @return 5 by 5 array */ public double[][] originCovariance() { - if (!propagated) originHelix(); - double [][] M = new double[5][5]; - for (int i=0; i<5; ++i) { - for (int j=0; j<5; ++j) { - if (propagated) M[i][j] = helixAtOrigin.C.unsafe_get(i, j); - else M[i][j] = SiteList.get(0).aS.helix.C.unsafe_get(i, j); + if (!propagated) { + originHelix(); + } + double[][] M = new double[5][5]; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + if (propagated) { + M[i][j] = helixAtOrigin.C.unsafe_get(i, j); + } else { + M[i][j] = SiteList.get(0).aS.helix.C.unsafe_get(i, j); + } } } return M; @@ -796,38 +941,51 @@ public double[][] originCovariance() { /** * Check whether all elements of the covariance are real numbers - * @return false if the covariance is rotten + * + * @return false if the covariance is rotten */ - public boolean covNaN() { - if (helixAtOrigin.C == null) return true; + public boolean covNaN() { + if (helixAtOrigin.C == null) { + return true; + } return MatrixFeatures_DDRM.hasNaN(helixAtOrigin.C); } - + /** * Return the helix parameters of the track at the origin - * @return array of 5 doubles + * + * @return array of 5 doubles */ public double[] originHelixParms() { - if (propagated) return helixAtOrigin.a.v.clone(); - else return null; + if (propagated) { + return helixAtOrigin.a.v.clone(); + } else { + return null; + } } - + /** - * Update the helix parameters at the "origin" by using the target position or vertex as a constraint - * @param vtx vertex location - * @param vtxCov vertex error matrix - * @return constrained helix state + * Update the helix parameters at the "origin" by using the target position + * or vertex as a constraint + * + * @param vtx vertex location + * @param vtxCov vertex error matrix + * @return constrained helix state */ - public HelixState originConstraint(double [] vtx, double [][] vtxCov) { - if (!propagated) originHelix(); - if (!propagated) return null; - + public HelixState originConstraint(double[] vtx, double[][] vtxCov) { + if (!propagated) { + originHelix(); + } + if (!propagated) { + return null; + } + // Transform the inputs in the the helix field-oriented coordinate system - Vec v = helixAtOrigin.toLocal(new Vec(3,vtx)); - SquareMatrix Cov = helixAtOrigin.Rot.rotate(new SquareMatrix(3,vtxCov)); + Vec v = helixAtOrigin.toLocal(new Vec(3, vtx)); + SquareMatrix Cov = helixAtOrigin.Rot.rotate(new SquareMatrix(3, vtxCov)); Vec X0 = helixAtOrigin.X0; double phi = phiDOCA(helixAtOrigin.a, v, X0, alpha); -/* if (debug) { // Test the DOCA algorithm + /* if (debug) { // Test the DOCA algorithm Vec rDoca = HelixState.atPhi(X0, helixAtOrigin.a, phi, alpha); System.out.format("originConstraint: phi of DOCA=%10.5e\n", phi); rDoca.print(" DOCA point"); @@ -846,54 +1004,58 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { double df2 = deriv * delPhi; System.out.format("Test of fDOCA derivative: df exact = %11.7f; df from derivative = %11.7f\n", df1, df2); }*/ - double [][] H = buildH(helixAtOrigin.a, v, X0, phi, alpha); + double[][] H = buildH(helixAtOrigin.a, v, X0, phi, alpha); Vec pntDOCA = HelixState.atPhi(X0, helixAtOrigin.a, phi, alpha); if (debug) { matrixPrint("H", H, 3, 5); - + // Derivative test HelixState hx = helixAtOrigin.copy(); - double daRel[] = { -0.04, 0.03, -0.16, -0.02, -0.015 }; - for (int i=0; i<5; ++i) daRel[i] = daRel[i]/100.; - for (int i = 0; i < 5; i++) { hx.a.v[i] = hx.a.v[i] * (1.0 + daRel[i]); } + double daRel[] = {-0.04, 0.03, -0.16, -0.02, -0.015}; + for (int i = 0; i < 5; ++i) { + daRel[i] = daRel[i] / 100.; + } + for (int i = 0; i < 5; i++) { + hx.a.v[i] = hx.a.v[i] * (1.0 + daRel[i]); + } Vec da = new Vec(hx.a.v[0] * daRel[0], hx.a.v[1] * daRel[1], hx.a.v[2] * daRel[2], hx.a.v[3] * daRel[3], hx.a.v[4] * daRel[4]); double phi2 = phiDOCA(hx.a, v, X0, alpha); Vec newX = HelixState.atPhi(X0, hx.a, phi2, alpha); Vec dxTrue = newX.dif(pntDOCA); dxTrue.print("originConstraint derivative test actual difference"); - + Vec dx = new Vec(3); - for (int i=0; i<3; ++i) { - for (int j=0; j<5; ++j) { - dx.v[i] += H[i][j]*da.v[j]; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 5; ++j) { + dx.v[i] += H[i][j] * da.v[j]; } } dx.print("difference from H derivative matrix"); - for (int i=0; i<3; ++i) { - double err = 100.*(dxTrue.v[i] - dx.v[i])/dxTrue.v[i]; + for (int i = 0; i < 3; ++i) { + double err = 100. * (dxTrue.v[i] - dx.v[i]) / dxTrue.v[i]; System.out.format(" Coordiante %d: percent difference = %10.6f\n", i, err); } System.out.println("helix covariance:"); helixAtOrigin.C.print(); } SquareMatrix Ginv = new SquareMatrix(3); - for (int i=0; i<3; ++i) { - for (int j=0; j<3; ++j) { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { Ginv.M[i][j] = Cov.M[i][j]; - for (int k=0; k<5; ++k) { - for (int l=0; l<5; ++l) { + for (int k = 0; k < 5; ++k) { + for (int l = 0; l < 5; ++l) { Ginv.M[i][j] += H[i][k] * helixAtOrigin.C.unsafe_get(k, l) * H[j][l]; } } } } SquareMatrix G = Ginv.fastInvert(); - double [][] K = new double[5][3]; // Kalman gain matrix - for (int i=0; i<5; ++i) { - for (int j=0; j<3; ++j) { - for (int k=0; k<5; ++k) { - for (int l=0; l<3; ++l) { + double[][] K = new double[5][3]; // Kalman gain matrix + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 3; ++j) { + for (int k = 0; k < 5; ++k) { + for (int l = 0; l < 3; ++l) { K[i][j] += helixAtOrigin.C.unsafe_get(i, k) * H[l][k] * G.M[l][j]; } } @@ -903,24 +1065,24 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { G.print("G"); matrixPrint("K", K, 5, 3); } - double [] newHelixParms = new double[5]; - double [][] newHelixCov = new double[5][5]; - for (int i=0; i<5; ++i) { + double[] newHelixParms = new double[5]; + double[][] newHelixCov = new double[5][5]; + for (int i = 0; i < 5; ++i) { newHelixParms[i] = helixAtOrigin.a.v[i]; - for (int j=0; j<3; ++j) { + for (int j = 0; j < 3; ++j) { newHelixParms[i] += K[i][j] * (vtx[j] - pntDOCA.v[j]); } - for (int j=0; j<5; ++j) { + for (int j = 0; j < 5; ++j) { newHelixCov[i][j] = helixAtOrigin.C.unsafe_get(i, j); - for (int k=0; k<3; ++k) { - for (int l=0; l<5; ++l) { + for (int k = 0; k < 3; ++k) { + for (int l = 0; l < 5; ++l) { newHelixCov[i][j] -= K[i][k] * H[k][l] * helixAtOrigin.C.unsafe_get(l, j); } } } } // Calculate the chi-squared contribution - Vec newHelix = new Vec(5,newHelixParms); + Vec newHelix = new Vec(5, newHelixParms); phi = phiDOCA(newHelix, v, X0, alpha); SquareMatrix CovInv = Cov.invert(); pntDOCA = HelixState.atPhi(X0, newHelix, phi, alpha); @@ -932,10 +1094,10 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { solver.setA(helixAtOrigin.C); solver.invert(Cinv); SquareMatrix CinvS = mToS(Cinv); - for (int i=0; i<5; ++i) { - for (int j=0; j<5; ++j) { - for (int k=0; k<3; ++k) { - for (int l=0; l<3; ++l) { + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; ++j) { + for (int k = 0; k < 3; ++k) { + for (int l = 0; l < 3; ++l) { CinvS.M[i][j] += H[k][i] * Vinv.M[k][l] * H[l][j]; } } @@ -943,11 +1105,11 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { } SquareMatrix Calt = CinvS.invert(); Calt.print("Alternative filtered covariance"); - double [][] Kp = new double[5][3]; - for (int i=0; i<5; ++i) { - for (int j=0; j<3; ++j) { - for (int k=0; k<5; ++k) { - for (int l=0; l<3; ++l) { + double[][] Kp = new double[5][3]; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 3; ++j) { + for (int k = 0; k < 5; ++k) { + for (int l = 0; l < 3; ++l) { Kp[i][j] += Calt.M[i][k] * H[l][k] * Vinv.M[l][j]; } } @@ -957,55 +1119,59 @@ public HelixState originConstraint(double [] vtx, double [][] vtxCov) { } return new HelixState(newHelix, X0, helixAtOrigin.origin, new DMatrixRMaj(newHelixCov), helixAtOrigin.B, helixAtOrigin.tB); } - + public double chi2incOrigin() { return chi2incVtx; } - - private static void matrixPrint(String s, double [][] A, int M, int N) { + + private static void matrixPrint(String s, double[][] A, int M, int N) { System.out.format("Dump of %d by %d matrix %s:\n", M, N, s); - for (int i=0; i= x2) { - Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING,"rtsafe: initial guess needs to be bracketed."); + Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING, "rtsafe: initial guess needs to be bracketed."); return xGuess; } fl = fDOCA(x1, a, v, X0, alpha); fh = fDOCA(x2, a, v, X0, alpha); int nTry = 0; - while (fl*fh > 0.0) { + while (fl * fh > 0.0) { if (nTry == 5) { - Logger.getLogger(KalTrack.class.getName()).log(Level.FINE,String.format("Root is not bracketed in zero finding, fl=%12.5e, fh=%12.5e, alpha=%10.6f, x1=%12.5f x2=%12.5f xGuess=%12.5f", + Logger.getLogger(KalTrack.class.getName()).log(Level.FINE, String.format("Root is not bracketed in zero finding, fl=%12.5e, fh=%12.5e, alpha=%10.6f, x1=%12.5f x2=%12.5f xGuess=%12.5f", fl, fh, alpha, x1, x2, xGuess)); return xGuess; } @@ -1032,8 +1198,12 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V nTry++; } //if (nTry > 0) System.out.format("KalTrack.rtsafe: %d tries needed to bracket solution.\n", nTry); - if (fl == 0.) return x1; - if (fh == 0.) return x2; + if (fl == 0.) { + return x1; + } + if (fh == 0.) { + return x2; + } if (fl < 0.0) { xl = x1; xh = x2; @@ -1045,19 +1215,23 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V dxold = Math.abs(x2 - x1); dx = dxold; f = fDOCA(rts, a, v, X0, alpha); - df = dfDOCAdPhi(rts,a, v, X0, alpha); + df = dfDOCAdPhi(rts, a, v, X0, alpha); for (int j = 1; j <= MAXIT; j++) { if ((((rts - xh) * df - f) * ((rts - xl) * df - f) > 0.0) || (Math.abs(2.0 * f) > Math.abs(dxold * df))) { dxold = dx; dx = 0.5 * (xh - xl); // Use bisection if the Newton-Raphson method is going bonkers rts = xl + dx; - if (xl == rts) return rts; + if (xl == rts) { + return rts; + } } else { dxold = dx; dx = f / df; // Newton-Raphson method temp = rts; rts -= dx; - if (temp == rts) return rts; + if (temp == rts) { + return rts; + } } if (Math.abs(dx) < xacc) { // System.out.format("KalTrack.rtSafe: solution converged in %d iterations.\n", @@ -1065,40 +1239,44 @@ private static double rtSafe(double xGuess, double x1, double x2, double xacc, V return rts; } f = fDOCA(rts, a, v, X0, alpha); - df = dfDOCAdPhi(rts,a, v, X0, alpha); + df = dfDOCAdPhi(rts, a, v, X0, alpha); if (f < 0.0) { xl = rts; } else { xh = rts; } } - Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING,"rtsafe: maximum number of iterations exceeded."); + Logger.getLogger(KalTrack.class.getName()).log(Level.WARNING, "rtsafe: maximum number of iterations exceeded."); return rts; } - + /** - * Function that is zero when the helix turning angle phi is at the point of closest approach to v - * @param phi turning angle - * @param a helix parameters - * @param v point of interest - * @param X0 helix pivot point - * @param alpha magnetic field constant - * @return deviation from zero + * Function that is zero when the helix turning angle phi is at the point of + * closest approach to v + * + * @param phi turning angle + * @param a helix parameters + * @param v point of interest + * @param X0 helix pivot point + * @param alpha magnetic field constant + * @return deviation from zero */ private static double fDOCA(double phi, Vec a, Vec v, Vec X0, double alpha) { Vec t = tangentVec(a, phi, alpha); Vec x = HelixState.atPhi(X0, a, phi, alpha); return (v.dif(x)).dot(t); } - + /** - * derivative of the fDOCA function with respect to phi, for the zero-finding algorithm - * @param phi turning angle - * @param a helix parameters - * @param v point of interest - * @param X0 helix pivot point - * @param alpha magnetic field constant - * @return derivative + * derivative of the fDOCA function with respect to phi, for the + * zero-finding algorithm + * + * @param phi turning angle + * @param a helix parameters + * @param v point of interest + * @param X0 helix pivot point + * @param alpha magnetic field constant + * @return derivative */ private static double dfDOCAdPhi(double phi, Vec a, Vec v, Vec X0, double alpha) { Vec x = HelixState.atPhi(X0, a, phi, alpha); @@ -1109,59 +1287,65 @@ private static double dfDOCAdPhi(double phi, Vec a, Vec v, Vec X0, double alpha) double dfdphi = -t.dot(dxdphi) + dfdt.dot(dtdphi); return dfdphi; } - + /** - * Derivatives of position along a helix with respect to the turning angle phi - * @param a helix parameters - * @param phi turning angle - * @param alpha magnetic field parameter - * @return derivative + * Derivatives of position along a helix with respect to the turning angle + * phi + * + * @param a helix parameters + * @param phi turning angle + * @param alpha magnetic field parameter + * @return derivative */ private static Vec dXdPhi(Vec a, double phi, double alpha) { return new Vec((alpha / a.v[2]) * FastMath.sin(a.v[1] + phi), -(alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), -(alpha / a.v[2]) * a.v[4]); } - + /** * A vector tangent to the helix 'a' at the alpha phi - * @param a helix parameters - * @param phi turning angle - * @param alpha magnetic field parameter - * @return tangent vector + * + * @param a helix parameters + * @param phi turning angle + * @param alpha magnetic field parameter + * @return tangent vector */ private static Vec tangentVec(Vec a, double phi, double alpha) { - return new Vec((alpha/a.v[2])*FastMath.sin(a.v[1]+phi), -(alpha/a.v[2])*FastMath.cos(a.v[1]+phi), -(alpha/a.v[2])*a.v[4]); + return new Vec((alpha / a.v[2]) * FastMath.sin(a.v[1] + phi), -(alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), -(alpha / a.v[2]) * a.v[4]); } - + private static Vec dTangentVecDphi(Vec a, double phi, double alpha) { - return new Vec((alpha/a.v[2])*FastMath.cos(a.v[1]+phi), (alpha/a.v[2])*FastMath.sin(a.v[2]+phi), 0.); + return new Vec((alpha / a.v[2]) * FastMath.cos(a.v[1] + phi), (alpha / a.v[2]) * FastMath.sin(a.v[2] + phi), 0.); } - + /** - * /Derivative matrix for the helix 'a' point of closet approach to point 'v' - * @param a helix parameters - * @param v 3D point for which we are finding the DOCA (the "measurement" point) - * @param X0 pivot point of helix - * @param phi angle along helix to the point of closet approach - * @param alpha constant to convert from curvature to 1/pt - * @return derivative matrix + * /Derivative matrix for the helix 'a' point of closet approach to point + * 'v' + * + * @param a helix parameters + * @param v 3D point for which we are finding the DOCA (the "measurement" + * point) + * @param X0 pivot point of helix + * @param phi angle along helix to the point of closet approach + * @param alpha constant to convert from curvature to 1/pt + * @return derivative matrix */ - private static double [][] buildH(Vec a, Vec v, Vec X0, double phi, double alpha) { + private static double[][] buildH(Vec a, Vec v, Vec X0, double phi, double alpha) { Vec x = HelixState.atPhi(X0, a, phi, alpha); Vec dxdphi = dXdPhi(a, phi, alpha); Vec t = tangentVec(a, phi, alpha); Vec dtdphi = dTangentVecDphi(a, phi, alpha); Vec dfdt = v.dif(x); double dfdphi = -t.dot(dxdphi) + dfdt.dot(dtdphi); - double [][] dtda = new double[3][5]; - dtda[0][1] = (alpha/a.v[2])*FastMath.cos(a.v[1]+phi); - dtda[0][2] = (-alpha/(a.v[2]*a.v[2]))*FastMath.sin(a.v[1]+phi); - dtda[1][1] = (alpha/a.v[2])*FastMath.sin(a.v[1]+phi); - dtda[1][2] = (alpha/(a.v[2]*a.v[2]))*FastMath.cos(a.v[1]+phi); - dtda[2][2] = (alpha/(a.v[2]*a.v[2]))*a.v[4]; - dtda[2][4] = -alpha/a.v[2]; - - double [][] dxda = new double[3][5]; + double[][] dtda = new double[3][5]; + dtda[0][1] = (alpha / a.v[2]) * FastMath.cos(a.v[1] + phi); + dtda[0][2] = (-alpha / (a.v[2] * a.v[2])) * FastMath.sin(a.v[1] + phi); + dtda[1][1] = (alpha / a.v[2]) * FastMath.sin(a.v[1] + phi); + dtda[1][2] = (alpha / (a.v[2] * a.v[2])) * FastMath.cos(a.v[1] + phi); + dtda[2][2] = (alpha / (a.v[2] * a.v[2])) * a.v[4]; + dtda[2][4] = -alpha / a.v[2]; + + double[][] dxda = new double[3][5]; dxda[0][0] = FastMath.cos(a.v[1]); dxda[1][0] = FastMath.sin(a.v[1]); dxda[0][1] = -(a.v[0] + alpha / a.v[2]) * FastMath.sin(a.v[1]) + (alpha / a.v[2]) * FastMath.sin(a.v[1] + phi); @@ -1171,30 +1355,31 @@ private static Vec dTangentVecDphi(Vec a, double phi, double alpha) { dxda[2][2] = (alpha / (a.v[2] * a.v[2])) * a.v[4] * phi; dxda[2][3] = 1.0; dxda[2][4] = -(alpha / a.v[2]) * phi; - + Vec dfda = new Vec(5); Vec dphida = new Vec(5); - for (int i=0; i<5; ++i) { - for (int j=0; j<3; ++j) { - dfda.v[i] += -t.v[j]*dxda[j][i] + dfdt.v[j]*dtda[j][i]; + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 3; ++j) { + dfda.v[i] += -t.v[j] * dxda[j][i] + dfdt.v[j] * dtda[j][i]; } - dphida.v[i] = -dfda.v[i]/dfdphi; + dphida.v[i] = -dfda.v[i] / dfdphi; } - - double [][] H = new double[3][5]; - for (int i=0; i<3; ++i) { - for (int j=0; j<5; ++j) { - H[i][j] = dxdphi.v[i]*dphida.v[j] + dxda[i][j]; - } + + double[][] H = new double[3][5]; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 5; ++j) { + H[i][j] = dxdphi.v[i] * dphida.v[j] + dxda[i][j]; + } } - + return H; } /** * Return the error on one of the helix parameters at the origin - * @param i 0 to 4 to select which parameter - * @return the error estimate + * + * @param i 0 to 4 to select which parameter + * @return the error estimate */ public double helixErr(int i) { return FastMath.sqrt(helixAtOrigin.C.unsafe_get(i, i)); @@ -1202,8 +1387,9 @@ public double helixErr(int i) { /** * Rotate a 3-vector from local field coordinates to global coordinates - * @param x input 3-vector - * @return output 3-vector + * + * @param x input 3-vector + * @return output 3-vector */ public double[] rotateToGlobal(double[] x) { Vec xIn = new Vec(x[0], x[1], x[2]); @@ -1211,9 +1397,10 @@ public double[] rotateToGlobal(double[] x) { } /** - * Rotate a 3-vector from global coordinates to local field coordinates - * @param x input 3-vector - * @return output 3-vector + * Rotate a 3-vector from global coordinates to local field coordinates + * + * @param x input 3-vector + * @return output 3-vector */ public double[] rotateToLocal(double[] x) { Vec xIn = new Vec(x[0], x[1], x[2]); @@ -1221,9 +1408,11 @@ public double[] rotateToLocal(double[] x) { } /** - * Figure out which measurement site on this track points to a given detector module - * @param module silicon module - * @return measurement site + * Figure out which measurement site on this track points to a given + * detector module + * + * @param module silicon module + * @return measurement site */ public int whichSite(SiModule module) { if (lyrMap != null) { @@ -1239,38 +1428,47 @@ public int whichSite(SiModule module) { /** * Sort the measurement sites in order of SVT layer number - * @param ascending true for ascending order, false for descending + * + * @param ascending true for ascending order, false for descending */ public void sortSites(boolean ascending) { - if (ascending) Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); - else Collections.sort(SiteList, MeasurementSite.SiteComparatorDn); + if (ascending) { + Collections.sort(SiteList, MeasurementSite.SiteComparatorUp); + } else { + Collections.sort(SiteList, MeasurementSite.SiteComparatorDn); + } } /** - * Remove a selected hit from a KalTrack object. Try to add a different hit. - * @param site The measurement site of the hit to be removed - * @param mxChi2Inc Maximum chi^2 increment to add another hit in the same layer - * @param mxTdif Maximum time difference of all hits to add another hit in the same layer + * Remove a selected hit from a KalTrack object. Try to add a different hit. + * + * @param site The measurement site of the hit to be removed + * @param mxChi2Inc Maximum chi^2 increment to add another hit in the same + * layer + * @param mxTdif Maximum time difference of all hits to add another hit in + * the same layer * @return */ public boolean removeHit(MeasurementSite site, double mxChi2Inc, double mxTdif) { boolean exchange = false; - if (debug) System.out.format("Event %d track %d remove hit %d on layer %d detector %d\n", - eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + if (debug) { + System.out.format("Event %d track %d remove hit %d on layer %d detector %d\n", + eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + } if (site.hitID < 0) { // This should never happen - logger.log(Level.WARNING, String.format("Event %d track %d, trying to remove nonexistent hit on layer %d detector %d", + logger.log(Level.WARNING, String.format("Event %d track %d, trying to remove nonexistent hit on layer %d detector %d", eventNumber, ID, site.m.Layer, site.m.detector)); return exchange; } if (site.m.hits.get(site.hitID).tracks.contains(this)) { site.m.hits.get(site.hitID).tracks.remove(this); } else { // This should never happen - logger.log(Level.WARNING, String.format("track %d is missing on hit %d track list in layer %d detector %d", + logger.log(Level.WARNING, String.format("track %d is missing on hit %d track list in layer %d detector %d", ID, site.hitID, site.m.Layer, site.m.detector)); } chi2 -= site.chi2inc; nHits--; - reducedChi2 = chi2/(double)nHits; + reducedChi2 = chi2 / (double) nHits; int oldID = site.hitID; site.removeHit(); // Check whether there might be another hit available @@ -1282,91 +1480,117 @@ public boolean removeHit(MeasurementSite site, double mxChi2Inc, double mxTdif) tMax = Math.max(tMax, newHit.time); exchange = true; nHits++; - if (debug) System.out.format("Event %d track %d added hit %d on layer %d detector %d\n", - eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + if (debug) { + System.out.format("Event %d track %d added hit %d on layer %d detector %d\n", + eventNumber, ID, site.hitID, site.m.Layer, site.m.detector); + } } else { SiteList.remove(site); } return exchange; } - + /** * Try to add missing hits to the track - * @param data All the SVT data - * @param mxResid Maximum residual - * @param mxChi2inc Maximum chi^2 increase - * @param mxTdif Maximum time difference of all hits, including the new one - * @param verbose true to spew lots of printout - * @return number of hits added + * + * @param data All the SVT data + * @param mxResid Maximum residual + * @param mxChi2inc Maximum chi^2 increase + * @param mxTdif Maximum time difference of all hits, including the new one + * @param verbose true to spew lots of printout + * @return number of hits added */ public int addHits(ArrayList data, double mxResid, double mxChi2inc, double mxTdif, boolean verbose) { - int numAdded = 0; + int numAdded = 0; int numLayers = 14; - if (nHits == numLayers) return numAdded; - - if (verbose) logger.setLevel(Level.FINER); - if (debug) System.out.format("addHits, event %d: trying to add hits to track %d\n", eventNumber, ID); - + if (nHits == numLayers) { + return numAdded; + } + + if (verbose) { + logger.setLevel(Level.FINER); + } + if (debug) { + System.out.format("addHits, event %d: trying to add hits to track %d\n", eventNumber, ID); + } + sortSites(true); if (debug) { String str = String.format("KalTrac.addHits: initial list of sites: "); for (MeasurementSite site : SiteList) { - str = str + String.format("(%d, %d, %d) ",site.m.Layer, site.m.detector, site.hitID); + str = str + String.format("(%d, %d, %d) ", site.m.Layer, site.m.detector, site.hitID); } System.out.format("%s\n", str); } - + ArrayList> moduleList = new ArrayList>(numLayers); for (int lyr = 0; lyr < numLayers; lyr++) { ArrayList modules = new ArrayList(); moduleList.add(modules); } for (SiModule thisSi : data) { - if (thisSi.hits.size() > 0) moduleList.get(thisSi.Layer).add(thisSi); + if (thisSi.hits.size() > 0) { + moduleList.get(thisSi.Layer).add(thisSi); + } } - + ArrayList newSites = new ArrayList(); - for (int idx = 0; idx < SiteList.size()-1; idx++) { - MeasurementSite site = SiteList.get(idx); - if (site.hitID < 0) continue; + for (int idx = 0; idx < SiteList.size() - 1; idx++) { + MeasurementSite site = SiteList.get(idx); + if (site.hitID < 0) { + continue; + } int nxtIdx = -1; - for (int jdx = idx+1; jdx < SiteList.size(); jdx++) { + for (int jdx = idx + 1; jdx < SiteList.size(); jdx++) { if (SiteList.get(jdx).hitID >= 0) { nxtIdx = jdx; break; } } - if (nxtIdx < 0) break; + if (nxtIdx < 0) { + break; + } MeasurementSite nxtSite = SiteList.get(nxtIdx); MeasurementSite siteFrom = site; - for (int lyr=site.m.Layer+1; lyr tMax) tMax = hitTime; - else if (hitTime < tMin) tMin = hitTime; + if (hitTime > tMax) { + tMax = hitTime; + } else if (hitTime < tMin) { + tMin = hitTime; + } break; } } } } - } + } } } if (numAdded > 0) { @@ -1378,37 +1602,49 @@ public int addHits(ArrayList data, double mxResid, double mxChi2inc, d break; } } - if (debug) System.out.format("KalTrack.addHits event %d: added hit %d on layer %d detector %d\n", eventNumber, site.hitID, site.m.Layer, site.m.detector); - if (siteToDelete != null) SiteList.remove(siteToDelete); + if (debug) { + System.out.format("KalTrack.addHits event %d: added hit %d on layer %d detector %d\n", eventNumber, site.hitID, site.m.Layer, site.m.detector); + } + if (siteToDelete != null) { + SiteList.remove(siteToDelete); + } SiteList.add(site); } sortSites(true); if (debug) { String str = String.format("KalTrack.addHits: final list of sites: "); for (MeasurementSite site : SiteList) { - str = str + String.format("(%d, %d, %d) ",site.m.Layer, site.m.detector, site.hitID); + str = str + String.format("(%d, %d, %d) ", site.m.Layer, site.m.detector, site.hitID); } System.out.format("%s\n", str); } } else { - if (debug) System.out.format("KalTrack.addHits: no hits added in event %d to track %d\n", eventNumber, ID); + if (debug) { + System.out.format("KalTrack.addHits: no hits added in event %d to track %d\n", eventNumber, ID); + } } return numAdded; } - + /** - * Re-fit the track - * @param keep true if there might be another recursion after dropping more hits - * @return true if the refit was successful + * Re-fit the track + * + * @param keep true if there might be another recursion after dropping more + * hits + * @return true if the refit was successful */ public boolean fit(boolean keep) { double chi2s = 0.; - if (debug) System.out.format("Entering KalTrack.fit for event %d, track %d\n", eventNumber, ID); + if (debug) { + System.out.format("Entering KalTrack.fit for event %d, track %d\n", eventNumber, ID); + } StateVector sH = SiteList.get(0).aS.copy(); boolean badC = KalmanPatRecHPS.negativeCov(sH.helix.C); if (badC) { - if (debug) System.out.format("KalTrack.fit: negative starting covariance, event %d track %d\n", eventNumber, ID); + if (debug) { + System.out.format("KalTrack.fit: negative starting covariance, event %d track %d\n", eventNumber, ID); + } KalmanPatRecHPS.setInitCov(sH.helix.C, sH.helix.a, false); } else { CommonOps_DDRM.scale(100., sH.helix.C); // Blow up the initial covariance matrix to avoid double counting measurements @@ -1420,42 +1656,56 @@ public boolean fit(boolean keep) { for (int idx = 0; idx < SiteList.size(); idx++) { // Redo all the filter steps MeasurementSite currentSite = SiteList.get(idx); MeasurementSite newSite = new MeasurementSite(currentSite.m.Layer, currentSite.m, kPar); - + boolean allowSharing = false; boolean pickupHits = false; boolean checkBounds = false; - double [] tRange = {-999., 999.}; + double[] tRange = {-999., 999.}; if (newSite.makePrediction(sH, prevMod, currentSite.hitID, allowSharing, pickupHits, checkBounds, tRange, 0) < 0) { - if (debug) System.out.format("KalTrack.fit: event %d, track %d failed to make prediction at layer %d!\n", eventNumber, ID, newSite.m.Layer); + if (debug) { + System.out.format("KalTrack.fit: event %d, track %d failed to make prediction at layer %d!\n", eventNumber, ID, newSite.m.Layer); + } return false; } if (!newSite.filter()) { - if (debug) System.out.format("KalTrack.fit: event %d, track %d failed to filter!\n", eventNumber, ID); + if (debug) { + System.out.format("KalTrack.fit: event %d, track %d failed to filter!\n", eventNumber, ID); + } return false; } if (KalmanPatRecHPS.negativeCov(currentSite.aF.helix.C)) { - if (debug) System.out.format("KalTrack: event %d, ID %d, negative covariance after filtering at layer %d\n", - eventNumber,ID,currentSite.m.Layer); + if (debug) { + System.out.format("KalTrack: event %d, ID %d, negative covariance after filtering at layer %d\n", + eventNumber, ID, currentSite.m.Layer); + } badCov = true; KalmanPatRecHPS.fixCov(currentSite.aF.helix.C, currentSite.aF.helix.a); } if (debug) { - if (newSite.hitID >= 0) chi2f += Math.max(currentSite.chi2inc,0.); + if (newSite.hitID >= 0) { + chi2f += Math.max(currentSite.chi2inc, 0.); + } } newSite.hitID = currentSite.hitID; sH = newSite.aF; - if (debug) System.out.format(" Layer %d hit %d filter, chi^2 increment=%10.5f, a=%s\n", - newSite.m.Layer, newSite.hitID, newSite.chi2inc, newSite.aF.helix.a.toString()); + if (debug) { + System.out.format(" Layer %d hit %d filter, chi^2 increment=%10.5f, a=%s\n", + newSite.m.Layer, newSite.hitID, newSite.chi2inc, newSite.aF.helix.a.toString()); + } prevMod = newSite.m; - if (keep) currentSite.chi2inc = newSite.chi2inc; // Residuals to cut out hits in next recursion + if (keep) { + currentSite.chi2inc = newSite.chi2inc; // Residuals to cut out hits in next recursion + } newSiteList.add(newSite); } if (badCov) { nBadCov[0]++; bad = true; - } - if (debug) System.out.format("KalTrack.fit: Track %d, Fit chi^2 after filtering = %12.4e\n", ID, chi2f); - + } + if (debug) { + System.out.format("KalTrack.fit: Track %d, Fit chi^2 after filtering = %12.4e\n", ID, chi2f); + } + chi2s = 0.; int nNewHits = 0; badCov = false; @@ -1469,18 +1719,20 @@ public boolean fit(boolean keep) { currentSite.smooth(nextSite); } if (currentSite.hitID >= 0) { - chi2s += Math.max(currentSite.chi2inc,0.); + chi2s += Math.max(currentSite.chi2inc, 0.); nNewHits++; } if (KalmanPatRecHPS.negativeCov(currentSite.aS.helix.C)) { - if (debug) System.out.format("KalTrack: event %d, ID %d, negative covariance after smoothing at layer %d\n", - eventNumber,ID,currentSite.m.Layer); + if (debug) { + System.out.format("KalTrack: event %d, ID %d, negative covariance after smoothing at layer %d\n", + eventNumber, ID, currentSite.m.Layer); + } badCov = true; KalmanPatRecHPS.fixCov(currentSite.aS.helix.C, currentSite.aS.helix.a); } if (debug) { - System.out.format(" Layer %d hit %d, smooth, chi^2 increment=%10.5f, a=%s\n", - currentSite.m.Layer, currentSite.hitID, currentSite.chi2inc, currentSite.aS.helix.a.toString()); + System.out.format(" Layer %d hit %d, smooth, chi^2 increment=%10.5f, a=%s\n", + currentSite.m.Layer, currentSite.hitID, currentSite.chi2inc, currentSite.aS.helix.a.toString()); } nextSite = currentSite; } @@ -1488,20 +1740,26 @@ public boolean fit(boolean keep) { nBadCov[1]++; bad = true; } - if (debug) System.out.format("KalTrack.fit: Track %d, Fit chi^2 after smoothing = %12.4e\n", ID, chi2s); + if (debug) { + System.out.format("KalTrack.fit: Track %d, Fit chi^2 after smoothing = %12.4e\n", ID, chi2s); + } if (!keep) { - if (chi2s/(double)nNewHits > reducedChi2*1.2) { - if (debug) System.out.format("KalTrack.fit event %d track %d: fit chisquared=%10.5f is not an improvement. Discard new fit.\n", - eventNumber, ID, chi2s); + if (chi2s / (double) nNewHits > reducedChi2 * 1.2) { + if (debug) { + System.out.format("KalTrack.fit event %d track %d: fit chisquared=%10.5f is not an improvement. Discard new fit.\n", + eventNumber, ID, chi2s); + } return false; } } SiteList = newSiteList; this.chi2 = chi2s; this.nHits = nNewHits; - this.reducedChi2 = chi2s/(double)nNewHits; + this.reducedChi2 = chi2s / (double) nNewHits; propagated = false; - if (debug) System.out.format("Exiting KalTrack.fit for event %d, track %d\n", eventNumber, ID); + if (debug) { + System.out.format("Exiting KalTrack.fit for event %d, track %d\n", eventNumber, ID); + } return true; } @@ -1509,26 +1767,27 @@ public boolean fit(boolean keep) { * Starting from the most downstream tracker hit, run a filter to include * the ECAL energy information (roughly a weighted mean with the SVT * momentum measurement). Then smooth back to the first layer and propagate - * to the origin. - * @param E ECAL energy - * @param sigmaE ECAL energy uncertainty + * to the origin. + * + * @param E ECAL energy + * @param sigmaE ECAL energy uncertainty */ void energyConstraint(double E, double sigmaE) { - // The prediction step from the last tracker site to the ECAL has F=1, - // since we don't alter an helix parameters in the prediction. - // The HelixState.energyConstrained method then uses the Kalman weighted - // means formalism for the filter step to update the helix parameters. - // The smoothing gain matrix for the step to the ECAL is simply unity, - // so the smoothed state vector with energy constraint at the last tracker - // site is exactly what is returned by the energyConstrained method. The - // smoothing back to the first tracker site can then proceed as usual. - int idxLast = SiteList.size()-1; - MeasurementSite lastSite = SiteList.get(idxLast); + // The prediction step from the last tracker site to the ECAL has F=1, + // since we don't alter an helix parameters in the prediction. + // The HelixState.energyConstrained method then uses the Kalman weighted + // means formalism for the filter step to update the helix parameters. + // The smoothing gain matrix for the step to the ECAL is simply unity, + // so the smoothed state vector with energy constraint at the last tracker + // site is exactly what is returned by the energyConstrained method. The + // smoothing back to the first tracker site can then proceed as usual. + int idxLast = SiteList.size() - 1; + MeasurementSite lastSite = SiteList.get(idxLast); HelixState energyConstrainedHelix = lastSite.aS.helix.energyConstrained(E, sigmaE); //System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aS.helix.a.v[2],energyConstrainedHelix.a.v[2]); lastSite.energyConstrained = true; lastSite.aES = new StateVector(lastSite.aS.kLow, lastSite.aS.uniformB); - lastSite.aES.helix = energyConstrainedHelix; + lastSite.aES.helix = energyConstrainedHelix; lastSite.aES.kUp = lastSite.aS.kUp; lastSite.aES.F = lastSite.aS.F; // Don't deep copy the F matrix lastSite.aES.mPred = lastSite.aS.mPred; @@ -1539,18 +1798,18 @@ void energyConstraint(double E, double sigmaE) { // Get the residual of the prediction at the ECAL double kappa = energyConstrainedHelix.a.v[2]; double tanl = energyConstrainedHelix.a.v[4]; - double ePredict = FastMath.sqrt(1.0+tanl*tanl)/Math.abs(kappa); - double chi = (E - ePredict)/sigmaE; + double ePredict = FastMath.sqrt(1.0 + tanl * tanl) / Math.abs(kappa); + double chi = (E - ePredict) / sigmaE; //System.out.format("KalTrack:energyConstraint E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n",E,ePredict,sigmaE,chi); - this.chi2_Econstraint = lastSite.chi2incE + chi*chi; + this.chi2_Econstraint = lastSite.chi2incE + chi * chi; MeasurementSite nS = lastSite; - for (int idx=idxLast-1; idx>=0; --idx) { - MeasurementSite thisSite = SiteList.get(idx); - thisSite.aES = thisSite.aF.smooth(nS.aES, nS.aP); - if (thisSite.hitID < 0) { - thisSite.energyConstrained = true; - continue; - } + for (int idx = idxLast - 1; idx >= 0; --idx) { + MeasurementSite thisSite = SiteList.get(idx); + thisSite.aES = thisSite.aF.smooth(nS.aES, nS.aP); + if (thisSite.hitID < 0) { + thisSite.energyConstrained = true; + continue; + } Measurement hit = thisSite.m.hits.get(thisSite.hitID); double V = hit.sigma * hit.sigma; double phiS = thisSite.aES.helix.planeIntersect(thisSite.m.p); @@ -1561,14 +1820,18 @@ void energyConstraint(double E, double sigmaE) { } thisSite.aES.mPred = thisSite.h(thisSite.aES, thisSite.m, phiS); thisSite.aES.r = hit.v - thisSite.aES.mPred; - if (tempV == null) tempV = new DMatrixRMaj(5,1); + if (tempV == null) { + tempV = new DMatrixRMaj(5, 1); + } CommonOps_DDRM.mult(thisSite.aES.helix.C, thisSite.H, tempV); thisSite.aES.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); if (thisSite.aES.R < 0) { - if (debug) System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aES.R); + if (debug) { + System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aES.R); + } //aS.print("the smoothed state"); //nS.print("the next site in the chain"); - thisSite.aES.R = 0.25*V; // A negative covariance makes no sense, hence this fudge + thisSite.aES.R = 0.25 * V; // A negative covariance makes no sense, hence this fudge } thisSite.chi2incE = (thisSite.aES.r * thisSite.aES.r) / thisSite.aES.R; @@ -1577,19 +1840,21 @@ void energyConstraint(double E, double sigmaE) { nS = thisSite; } Vec beamSpot = new Vec(3, kPar.beamSpot); - + // This propagated helix will have its pivot at the origin but is in the origin B-field frame // The StateVector method propagateRungeKutta transforms the origin plane into the origin B-field frame - Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); + Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); arcLengthE = new double[1]; - helixAtOriginEconstraint = SiteList.get(0).aES.helix.propagateRungeKutta(originPlane, yScat, XLscat, SiteList.get(0).m.Bfield, arcLengthE); + helixAtOriginEconstraint = SiteList.get(0).aES.helix.propagateRungeKutta(originPlane, yScat, XLscat, SiteList.get(0).m.Bfield, arcLengthE); propagatedE = true; } - + /** - * Derivative matrix for propagating the covariance of the helix parameters to a covariance of momentum - * @param a helix parameters - * @return 2D array of derivatives + * Derivative matrix for propagating the covariance of the helix parameters + * to a covariance of momentum + * + * @param a helix parameters + * @return 2D array of derivatives */ static double[][] DpTOa(Vec a) { double[][] M = new double[3][5]; @@ -1605,9 +1870,11 @@ static double[][] DpTOa(Vec a) { } /** - * Derivative matrix for propagating the covariance of the helix parameter to a - * covariance of the point of closest approach to the origin (i.e. at phi=0) - * @param a helix parameters + * Derivative matrix for propagating the covariance of the helix parameter + * to a covariance of the point of closest approach to the origin (i.e. at + * phi=0) + * + * @param a helix parameters */ static double[][] DxTOa(Vec a) { double[][] M = new double[3][5]; @@ -1620,61 +1887,83 @@ static double[][] DxTOa(Vec a) { } /** - * Comparator function for sorting tracks by quality + * Comparator function for sorting tracks by quality */ static Comparator TkrComparator = new Comparator() { public int compare(KalTrack t1, KalTrack t2) { double penalty1 = 1.0; double penalty2 = 1.0; - if (!t1.SiteList.get(0).aS.helix.goodCov()) penalty1 = 9.9e3; - if (!t2.SiteList.get(0).aS.helix.goodCov()) penalty2 = 9.9e3; + if (!t1.SiteList.get(0).aS.helix.goodCov()) { + penalty1 = 9.9e3; + } + if (!t2.SiteList.get(0).aS.helix.goodCov()) { + penalty2 = 9.9e3; + } - Double chi1 = Double.valueOf((penalty1*t1.chi2) / t1.nHits + 300.0*(1.0 - (double)t1.nHits/14.)); - Double chi2 = Double.valueOf((penalty2*t2.chi2) / t2.nHits + 300.0*(1.0 - (double)t2.nHits/14.)); + Double chi1 = Double.valueOf((penalty1 * t1.chi2) / t1.nHits + 300.0 * (1.0 - (double) t1.nHits / 14.)); + Double chi2 = Double.valueOf((penalty2 * t2.chi2) / t2.nHits + 300.0 * (1.0 - (double) t2.nHits / 14.)); return chi1.compareTo(chi2); } }; static double sigmaECAL(double E) { - return FastMath.sqrt(2.62 + (8.24 + 6.25*E)*E)/100.; + return FastMath.sqrt(2.62 + (8.24 + 6.25 * E) * E) / 100.; } - + /** * Transform a DMatrixRMaj object into a Kalman SquareMatrix object - * @param M DMatrixRMaj object - * @return the SquareMatrix equivalent + * + * @param M DMatrixRMaj object + * @return the SquareMatrix equivalent */ static SquareMatrix mToS(DMatrixRMaj M) { SquareMatrix S = new SquareMatrix(M.numRows); - if (M.numCols != M.numRows) return null; - for (int i=0; i sensors; private Map hitMap; @@ -80,108 +83,130 @@ public class KalmanInterface { private int maxHits; private int nBigEvents; private int eventNumber; - - private static final boolean debug = false; + + private static final boolean debug = false; private static final double c = 2.99793e8; // Speed of light in m/s - + /** - * Set the limit on the maximum number of hits in an event to analyze (larger events will be skipped) - * - * @param limit desired hit limit + * Set the limit on the maximum number of hits in an event to analyze + * (larger events will be skipped) + * + * @param limit desired hit limit */ public void setSiHitsLimit(int limit) { _siHitsLimit = limit; } - + /** * Get the current setting on the maximum number of hits to analyze - * - * @return hit limit + * + * @return hit limit */ public int getSiHitsLimit() { return _siHitsLimit; } - + /** * Get the HPS tracker hit corresponding to a Kalman hit - * - * @param km Kalman Measurement object - * @return identity of the HPS hit + * + * @param km Kalman Measurement object + * @return identity of the HPS hit */ public TrackerHit getHpsHit(Measurement km) { return hitMap.get(km); } - + /** * Get the HPS sensor that corresponds to a Kalman SiModule - * - * @param kalmanSiMod Kalman SiModule object (geometry of one sensor) - * @return HpsSiSensor object + * + * @param kalmanSiMod Kalman SiModule object (geometry of one sensor) + * @return HpsSiSensor object */ public HpsSiSensor getHpsSensor(SiModule kalmanSiMod) { - if (moduleMap == null) return null; - else { + if (moduleMap == null) { + return null; + } else { SiStripPlane temp = moduleMap.get(kalmanSiMod); - if (temp == null) return null; - else return (HpsSiSensor) (temp.getSensor()); + if (temp == null) { + return null; + } else { + return (HpsSiSensor) (temp.getSensor()); + } } } - + /** * Get the entire map relating HPS sensors to Kalman SiModule objects - * - * @return Map from HPS sensors to Kalman SiModule objects + * + * @return Map from HPS sensors to Kalman SiModule objects */ public Map getModuleMap() { return moduleMap; } /** - * Get the HPS B field at a provided location. The HPS field map is in the HPS global coordinate system. - * This routine includes the transformations to return the field in the Kalman global coordinate system - * given a coordinate in the same system. Don't confuse this with org.lcsim.geometry.FieldMap.getField, - * which works with double arrays in HPS coordinates. - * - * @param kalPos Kalman 3-vector object specifying the position - * @param hpsFm HPS field map - * @return Kalman 3-vector object specifying the magnetic field at the given location + * Get the HPS B field at a provided location. The HPS field map is in the + * HPS global coordinate system. This routine includes the transformations + * to return the field in the Kalman global coordinate system given a + * coordinate in the same system. Don't confuse this with + * org.lcsim.geometry.FieldMap.getField, which works with double arrays in + * HPS coordinates. + * + * @param kalPos Kalman 3-vector object specifying the position + * @param hpsFm HPS field map + * @return Kalman 3-vector object specifying the magnetic field at the given + * location */ static Vec getField(Vec kalPos, org.lcsim.geometry.FieldMap hpsFm) { return new Vec(3, getFielD(kalPos, hpsFm)); } /** - * Get the HPS B field at a provided location for stand-alone running of the Kalman code. - * For hps-java use KalmanInterface.getField to return a 3-vector Vec object. - * - * @param kalPos Kalman 3-vector object specifying the position - * @param hpsFm HPS field map - * @return Kalman 3-vector object specifying the magnetic field at the given location + * Get the HPS B field at a provided location for stand-alone running of the + * Kalman code. For hps-java use KalmanInterface.getField to return a + * 3-vector Vec object. + * + * @param kalPos Kalman 3-vector object specifying the position + * @param hpsFm HPS field map + * @return Kalman 3-vector object specifying the magnetic field at the given + * location */ - static double [] getFielD(Vec kalPos, org.lcsim.geometry.FieldMap hpsFm) { + static double[] getFielD(Vec kalPos, org.lcsim.geometry.FieldMap hpsFm) { // Field map for stand-alone running - if (FieldMap.class.isInstance(hpsFm)) return ((FieldMap) (hpsFm)).getField(kalPos); + if (FieldMap.class.isInstance(hpsFm)) { + return ((FieldMap) (hpsFm)).getField(kalPos); + } // Interface to the standard field map for running in hps-java, including conversions for Kalman coordinates //System.out.format("Accessing HPS field map for position %8.3f %8.3f %8.3f\n", kalPos.v[0], kalPos.v[1], kalPos.v[2]); - double[] hpsPos = { kalPos.v[0], -1.0 * kalPos.v[2], kalPos.v[1] }; + double[] hpsPos = {kalPos.v[0], -1.0 * kalPos.v[2], kalPos.v[1]}; //hpsPos[0] = 0.; //hpsPos[1] = 0.; //hpsPos[2] = 505.57; - + // To avoid going off the map and getting a field returned that is identically equal to zero - if (hpsPos[1] > 70.0) hpsPos[1] = 70.0; - if (hpsPos[1] < -70.0) hpsPos[1] = -70.0; - if (hpsPos[0] < -225.) hpsPos[0] = -225.; - if (hpsPos[0] > 270.) hpsPos[0] = 270.; + if (hpsPos[1] > 70.0) { + hpsPos[1] = 70.0; + } + if (hpsPos[1] < -70.0) { + hpsPos[1] = -70.0; + } + if (hpsPos[0] < -225.) { + hpsPos[0] = -225.; + } + if (hpsPos[0] > 270.) { + hpsPos[0] = 270.; + } double[] hpsField = hpsFm.getField(hpsPos); - double [] kalField = {hpsField[0], hpsField[2], -1.0 * hpsField[1]}; + double[] kalField = {hpsField[0], hpsField[2], -1.0 * hpsField[1]}; return kalField; } /** - * Set the layers to be used for finding seed tracks (not used by Kalman pattern recognition) - * @param input List of layers + * Set the layers to be used for finding seed tracks (not used by Kalman + * pattern recognition) + * + * @param input List of layers */ public void setSeedTrackLayers(List input) { SeedTrackLayers = input; @@ -193,15 +218,17 @@ public void setSeedTrackLayers(List input) { public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { this(kPar, fM); } - + /** - * Constructor for the interface between the Kalman-filter tracking code and other HPS-Java code - * - * @param kPar Object containing all the control parameters for the Kalman code - * @param fM The HPS field map + * Constructor for the interface between the Kalman-filter tracking code and + * other HPS-Java code + * + * @param kPar Object containing all the control parameters for the Kalman + * code + * @param fM The HPS field map */ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { - + this.det = det; this.sensors = sensors; this.fM = fM; @@ -210,10 +237,10 @@ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { logger.info("Entering the KalmanInterface constructor"); maxHits = 0; nBigEvents = 0; - - tempM = new DMatrixRMaj(5,5); - Ft = new DMatrixRMaj(5,5); - + + tempM = new DMatrixRMaj(5, 5); + Ft = new DMatrixRMaj(5, 5); + hitMap = new HashMap(); simHitMap = new HashMap(); moduleMap = new HashMap(); @@ -224,45 +251,46 @@ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { SeedTrackLayers.add(3); SeedTrackLayers.add(4); SeedTrackLayers.add(5); - + if (kPar.uniformB) { logger.log(Level.WARNING, "KalmanInterface WARNING: the magnetic field is set to a uniform value."); } uniformB = kPar.uniformB; - + // Transformation from HPS SVT tracking coordinates to Kalman global coordinates - double[][] HpsSvtToKalmanVals = { { 0, 1, 0 }, { 1, 0, 0 }, { 0, 0, -1 } }; + double[][] HpsSvtToKalmanVals = {{0, 1, 0}, {1, 0, 0}, {0, 0, -1}}; HpsSvtToKalman = new RotMatrix(HpsSvtToKalmanVals); HpsSvtToKalmanMatrix = new BasicHep3Matrix(); for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) { HpsSvtToKalmanMatrix.setElement(i, j, HpsSvtToKalmanVals[i][j]); + } } KalmanToHpsSvt = HpsSvtToKalman.invert(); if (debug) { HpsSvtToKalman.print("HPS tracking to Kalman conversion"); KalmanToHpsSvt.print("Kalman to HPS tracking conversion"); - Vec zHPS = new Vec(0.,0.,1.); - Vec xHPS = new Vec(1.,0.,0.); - Vec yHPS = new Vec(0.,1.,0.); + Vec zHPS = new Vec(0., 0., 1.); + Vec xHPS = new Vec(1., 0., 0.); + Vec yHPS = new Vec(0., 1., 0.); HpsSvtToKalman.rotate(xHPS).print("HPS tracking x axis in Kalman coordinates"); HpsSvtToKalman.rotate(yHPS).print("HPS tracking y axis in Kalman coordinates"); HpsSvtToKalman.rotate(zHPS).print("HPS tracking z axis in Kalman coordinates"); } - + // Seed the random number generator long rndSeed = -3263009337738135404L; rnd = new Random(); rnd.setSeed(rndSeed); - + kPat = new KalmanPatRecHPS(kPar); - + Vec centerB = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), fM); - logger.log(Level.INFO, String.format("The magnetic field at the detector center (%10.5f mm) is %10.5f\n", kPar.SVTcenter, centerB.mag())); + logger.log(Level.INFO, String.format("The magnetic field at the detector center (%10.5f mm) is %10.5f\n", kPar.SVTcenter, centerB.mag())); double conFac = 1.0e12 / c; - alphaCenter = conFac/ centerB.mag(); + alphaCenter = conFac / centerB.mag(); } - + /** * End-of-job formatted printout of some counters. */ @@ -277,31 +305,32 @@ public void summary() { } /** - * Return the reference to the parameter setting object for the driver to use - * - * @return reference to the parameter setting object KalParams.java + * Return the reference to the parameter setting object for the driver to + * use + * + * @return reference to the parameter setting object KalParams.java */ public KalmanParams getKalmanParams() { return kPar; } - + /** * Transformation from HPS global coordinates to Kalman global coordinates - * - * @param HPSvec 3-vector array of HPS global coordinates - * @return 3-vector object of Kalman global coordinates + * + * @param HPSvec 3-vector array of HPS global coordinates + * @return 3-vector object of Kalman global coordinates * @see Definitions of coordinate systems in the Kalman pdf documentation. */ - public static Vec vectorGlbToKalman(double[] HPSvec) { + public static Vec vectorGlbToKalman(double[] HPSvec) { Vec kalVec = new Vec(HPSvec[0], HPSvec[2], -HPSvec[1]); return kalVec; } - + /** * Transformation from Kalman global coordinates to HPS global coordinates - * - * @param KalVec 3-vector object of Kalman global coordinates - * @return 3-vector array of HPS global coordinates + * + * @param KalVec 3-vector object of Kalman global coordinates + * @return 3-vector array of HPS global coordinates * @see Definitions of coordinate systems in the Kalman pdf documentation. */ public static double[] vectorKalmanToGlb(Vec KalVec) { @@ -311,12 +340,12 @@ public static double[] vectorKalmanToGlb(Vec KalVec) { HPSvec[2] = KalVec.v[1]; return HPSvec; } - + /** * Transformation from Kalman global coordinates to HPS tracking coordinates - * - * @param KalVec Kalman 3-vector object in Kalman global coordinates - * @return HPS tracking coordinates a 3-vector array + * + * @param KalVec Kalman 3-vector object in Kalman global coordinates + * @return HPS tracking coordinates a 3-vector array * @see Definitions of coordinate systems in the Kalman pdf documentation. */ public static double[] vectorKalmanToTrk(Vec KalVec) { @@ -326,37 +355,37 @@ public static double[] vectorKalmanToTrk(Vec KalVec) { HPSvec[2] = -KalVec.v[2]; return HPSvec; } - + /** * Transformation from HPS tracking coordinates to Kalman global coordinates - * - * @param HPSvec HPS tracking coordinates a 3-vector array - * @return Kalman 3-vector object in Kalman global coordinates - * @see Definitions of coordinate systems in the Kalman pdf documentation. + * + * @param HPSvec HPS tracking coordinates a 3-vector array + * @return Kalman 3-vector object in Kalman global coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. */ - public static Vec vectorTrkToKalman(double[] HPSvec) { + public static Vec vectorTrkToKalman(double[] HPSvec) { Vec kalVec = new Vec(HPSvec[1], HPSvec[0], -HPSvec[2]); return kalVec; } - + /** * Transformation from HPS sensor coordinates to Kalman sensor coordinates - * - * @param HPSvec HPS 3-vector sensor coordinate double-precision array - * @return Kalman 3-vector object in Kalman sensor coordinates - * @see Definitions of coordinate systems in the Kalman pdf documentation. + * + * @param HPSvec HPS 3-vector sensor coordinate double-precision array + * @return Kalman 3-vector object in Kalman sensor coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. */ - public static Vec localHpsToKal(double[] HPSvec) { + public static Vec localHpsToKal(double[] HPSvec) { Vec kalVec = new Vec(-HPSvec[1], HPSvec[0], HPSvec[2]); return kalVec; } - + /** * Transformation from Kalman sensor coordinates to HPS sensor coordinates - * - * @param KalVec 3-vector object in Kalman coordinates - * @return 3-vector double-precision array in HPS coordinates - * @see Definitions of coordinate systems in the Kalman pdf documentation. + * + * @param KalVec 3-vector object in Kalman coordinates + * @return 3-vector double-precision array in HPS coordinates + * @see Definitions of coordinate systems in the Kalman pdf documentation. */ public static double[] localKalToHps(Vec KalVec) { double[] HPSvec = new double[3]; @@ -367,9 +396,10 @@ public static double[] localKalToHps(Vec KalVec) { } /** - * Return the entire list of Kalman SiModule (geometry description for the Kalman filter code) - * - * @return List of all SiModule objects + * Return the entire list of Kalman SiModule (geometry description for the + * Kalman filter code) + * + * @return List of all SiModule objects */ public ArrayList getSiModuleList() { return SiMlist; @@ -377,7 +407,8 @@ public ArrayList getSiModuleList() { /** * Return a list of all measurements for all SiModule - * @return List of all Measurement objects in the detector + * + * @return List of all Measurement objects in the detector */ public ArrayList getMeasurements() { ArrayList measList = new ArrayList();; @@ -390,8 +421,9 @@ public ArrayList getMeasurements() { } /** - * Clear the Kalman interface event hit and track information without deleting the SiModule geometry information - */ + * Clear the Kalman interface event hit and track information without + * deleting the SiModule geometry information + */ public void clearInterface() { logger.fine("Clearing the Kalman interface"); hitMap.clear(); @@ -401,84 +433,97 @@ public void clearInterface() { SiM.hits.clear(); } } - + /** - * Find the intersection of a track with a detector plane. All of the input and output are assumed to - * be in HPS coordinates (not Kalman coordinates). Memory must be provided for xLoc and pAtPlane, the - * charge must be -1 or +1, and the axis unit vectors uGlb, vGlb, tGlb must be normalized and right-handed. - * @param x 3-vector coordinate on the track - * @param p 3-vector momentum at the point x - * @param Q particle charge (-1.0 or +1.0) - * @param pointOnPlane 3-vector point on the detector plane - * @param uGlb 3-vector direction cosines of the u-axis of the detector plane (perp to strips) - * @param vGlb 3-vector direction cosines of the v-axis of the detector plane (parallel to strips) - * @param tGlb 3-vector direction cosines of the t-axis of the detector plane (perp to plane) - * @param fM HPS field map - * @param xLocal return 3-vector intersection in detector coordinates (u, v, t) - * @param pAtPlane return 3-vector momentum at the intersection, in global coordinates - * @return 3-vector point of intersection on the detector plane in global coordinates + * Find the intersection of a track with a detector plane. All of the input + * and output are assumed to be in HPS coordinates (not Kalman coordinates). + * Memory must be provided for xLoc and pAtPlane, the charge must be -1 or + * +1, and the axis unit vectors uGlb, vGlb, tGlb must be normalized and + * right-handed. + * + * @param x 3-vector coordinate on the track + * @param p 3-vector momentum at the point x + * @param Q particle charge (-1.0 or +1.0) + * @param pointOnPlane 3-vector point on the detector plane + * @param uGlb 3-vector direction cosines of the u-axis of the detector + * plane (perp to strips) + * @param vGlb 3-vector direction cosines of the v-axis of the detector + * plane (parallel to strips) + * @param tGlb 3-vector direction cosines of the t-axis of the detector + * plane (perp to plane) + * @param fM HPS field map + * @param xLocal return 3-vector intersection in detector coordinates (u, v, + * t) + * @param pAtPlane return 3-vector momentum at the intersection, in global + * coordinates + * @return 3-vector point of intersection on the detector plane in global + * coordinates */ - public static double [] hpsHelixIntersect(double [] x, double [] p, double Q, double [] pointOnPlane, double [] uGlb, double [] vGlb, double [] tGlb, - org.lcsim.geometry.FieldMap fM, double [] xLocal, double [] pAtPlane) { - - // Transform the input information into Kalman coordinates + public static double[] hpsHelixIntersect(double[] x, double[] p, double Q, double[] pointOnPlane, double[] uGlb, double[] vGlb, double[] tGlb, + org.lcsim.geometry.FieldMap fM, double[] xLocal, double[] pAtPlane) { + + // Transform the input information into Kalman coordinates Vec uK = (vectorGlbToKalman(vGlb).scale(-1.0)); // u and v are reversed in hps compared to kalman Vec vK = vectorGlbToKalman(uGlb); Vec tK = vectorGlbToKalman(tGlb); Vec pointOnPlaneTransformed = vectorGlbToKalman(pointOnPlane); Vec xK = vectorGlbToKalman(x); Vec pK = vectorGlbToKalman(p); - + // Define the detector plane (in Kalman coordinates) Plane detPln = new Plane(pointOnPlaneTransformed, tK, uK, vK); - + // Integrate to find the intersection with the detector plane HelixPlaneIntersect hpi = new HelixPlaneIntersect(); Vec pInt = new Vec(3); Vec intercept = hpi.rkIntersect(detPln, xK, pK, Q, fM, pInt); - + // Convert the results back to HPS coordinates - double [] interceptPos = vectorKalmanToGlb(intercept); + double[] interceptPos = vectorKalmanToGlb(intercept); pAtPlane = vectorKalmanToGlb(pInt); - + // Transform the intersection to local coordinates in the Kalman sensor system // and transform the result to HPS local coordinates. RotMatrix R = new RotMatrix(detPln.U(), detPln.V(), detPln.T()); Vec xLocK = R.rotate(intercept.dif(detPln.X())); xLocal = localKalToHps(xLocK); - - return interceptPos; + + return interceptPos; } - + /** - * Transform a Kalman helix to an HPS helix rotated to the global frame and with the pivot at the origin - * The calling routine must provide empty covHPS[15] and position[3] arrays. - * No rotations are needed if the field is assumed to be uniform. - * - * @param helixState description of the Kalman helix - * @param pln Kalman plane object (origin point and t,u,v axis directions) - * @param alphaCenter alpha value to use to convert between momentum and curvature - * @param covHPS return array of LCSIM helix parameter covariance (15 values for the symmetric matrix) - * @param position return 3-vector array of the location in HPS global coordinates of the original helix pivot - * @return array of 5 LCSIM helix parameters + * Transform a Kalman helix to an HPS helix rotated to the global frame and + * with the pivot at the origin The calling routine must provide empty + * covHPS[15] and position[3] arrays. No rotations are needed if the field + * is assumed to be uniform. + * + * @param helixState description of the Kalman helix + * @param pln Kalman plane object (origin point and t,u,v axis directions) + * @param alphaCenter alpha value to use to convert between momentum and + * curvature + * @param covHPS return array of LCSIM helix parameter covariance (15 values + * for the symmetric matrix) + * @param position return 3-vector array of the location in HPS global + * coordinates of the original helix pivot + * @return array of 5 LCSIM helix parameters */ - static double [] toHPShelix(HelixState helixState, Plane pln, double alphaCenter, double [] covHPS, double[] position) { + static double[] toHPShelix(HelixState helixState, Plane pln, double alphaCenter, double[] covHPS, double[] position) { final boolean debug = false; Vec finalHelixParams = null; Vec pivotGlobal = null; - DMatrixRMaj F = new DMatrixRMaj(5,5); - DMatrixRMaj covRotated = new DMatrixRMaj(5,5); + DMatrixRMaj F = new DMatrixRMaj(5, 5); + DMatrixRMaj covRotated = new DMatrixRMaj(5, 5); double phiInt = helixState.planeIntersect(pln); if (Double.isNaN(phiInt)) { Logger logger = Logger.getLogger(KalmanInterface.class.getName()); - logger.fine(String.format("toHPShelix: no intersection with the plane at %s",pln.toString())); + logger.fine(String.format("toHPShelix: no intersection with the plane at %s", pln.toString())); phiInt = 0.; } Vec newPivot = helixState.atPhi(phiInt); - Vec finalPivot = new Vec(0.,0.,0.); + Vec finalPivot = new Vec(0., 0., 0.); if (!uniformB) { // Transform helix to a pivot point on the helix itself (so rho0 and z0 become zero) - Vec helixParamsPivoted = helixState.pivotTransform(newPivot); + Vec helixParamsPivoted = helixState.pivotTransform(newPivot); helixState.makeF(helixParamsPivoted, F, 1.0); if (debug) { System.out.format("Entering KalmanInterface.toHPShelix"); @@ -490,22 +535,26 @@ public void clearInterface() { Vec intGlb = helixState.toGlobal(newPivot); intGlb.print("global intersection with plane"); } - + // Then rotate the helix to the global system. This isn't quite kosher, since the B-field will not // be aligned with the global system in general, but we have to do it to fit back into the HPS TrackState // coordinate convention, for which the field is assumed to be uniform and aligned. - DMatrixRMaj fRot = new DMatrixRMaj(5,5); + DMatrixRMaj fRot = new DMatrixRMaj(5, 5); Vec helixParamsRotated = HelixState.rotateHelix(helixParamsPivoted, helixState.Rot.invert(), fRot); - CommonOps_DDRM.mult(fRot, F, Ft); - + CommonOps_DDRM.mult(fRot, F, Ft); + CommonOps_DDRM.multTransB(helixState.C, Ft, tempM); CommonOps_DDRM.mult(Ft, tempM, covRotated); - if (debug) helixParamsRotated.print("rotated helix params"); - + if (debug) { + helixParamsRotated.print("rotated helix params"); + } + // Transform the pivot to the global system. pivotGlobal = helixState.toGlobal(newPivot); - if (debug) pivotGlobal.print("pivot in global system"); - + if (debug) { + pivotGlobal.print("pivot in global system"); + } + // Pivot transform to the final pivot at the origin finalHelixParams = HelixState.pivotTransform(finalPivot, helixParamsRotated, pivotGlobal, alphaCenter, 0.); HelixState.makeF(finalHelixParams, F, helixParamsRotated, alphaCenter, 1.0); @@ -530,40 +579,49 @@ public void clearInterface() { CommonOps_DDRM.mult(F, tempM, covRotated); } if (covHPS != null) { - double [] temp = KalmanInterface.getLCSimCov(covRotated, alphaCenter).asPackedArray(true); - for (int i=0; i<15; ++i) covHPS[i] = temp[i]; + double[] temp = KalmanInterface.getLCSimCov(covRotated, alphaCenter).asPackedArray(true); + for (int i = 0; i < 15; ++i) { + covHPS[i] = temp[i]; + } } if (position != null) { - double [] temp = KalmanInterface.vectorKalmanToGlb(pivotGlobal); - for (int i=0; i<3; ++i) position[i] = temp[i]; + double[] temp = KalmanInterface.vectorKalmanToGlb(pivotGlobal); + for (int i = 0; i < 3; ++i) { + position[i] = temp[i]; + } } return KalmanInterface.getLCSimParams(finalHelixParams.v, alphaCenter); } - + /** * Transform a Kalman HelixState to an HPS TrackState. - * - * @param helixState The Kalman HelixState object. - * @param pln The Kalman Plane object (origin point and t,u,v axis directions) - * @param alphaCenter The alpha value used to calculate momentum from curvature. - * @param loc Integer representation of the TrackState location (AtIP, AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) - * @return The HPS TrackState + * + * @param helixState The Kalman HelixState object. + * @param pln The Kalman Plane object (origin point and t,u,v axis + * directions) + * @param alphaCenter The alpha value used to calculate momentum from + * curvature. + * @param loc Integer representation of the TrackState location (AtIP, + * AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) + * @return The HPS TrackState */ static TrackState toTrackState(HelixState helixState, Plane pln, double alphaCenter, int loc) { - double [] covHPS = new double[15]; - double [] position = new double[3]; - double [] helixHPS = KalmanInterface.toHPShelix(helixState, pln, alphaCenter, covHPS, position); - - return new BaseTrackState( helixHPS, covHPS, position, loc); + double[] covHPS = new double[15]; + double[] position = new double[3]; + double[] helixHPS = KalmanInterface.toHPShelix(helixState, pln, alphaCenter, covHPS, position); + + return new BaseTrackState(helixHPS, covHPS, position, loc); } - + /** - * Create an HPS TrackState from a Kalman HelixState at the location of a particular SiModule - * - * @param ms Kalman MeasurementSite object - * @param loc Integer representation of the TrackState location (AtIP, AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) - * @param useSmoothed Choose between the smoothed vs filtered Kalman helix - * @return The HPS TrackState + * Create an HPS TrackState from a Kalman HelixState at the location of a + * particular SiModule + * + * @param ms Kalman MeasurementSite object + * @param loc Integer representation of the TrackState location (AtIP, + * AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) + * @param useSmoothed Choose between the smoothed vs filtered Kalman helix + * @return The HPS TrackState */ public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoothed) { // Note that the helix parameters that get stored in the TrackState assume a B-field exactly oriented in the @@ -571,19 +629,23 @@ public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoot // intersection point with the detector plane. StateVector sv = null; if (useSmoothed) { - if (!ms.smoothed) return null; + if (!ms.smoothed) { + return null; + } sv = ms.aS; } else { // using the filtered state is really not recommended - if (!ms.filtered) return null; + if (!ms.filtered) { + return null; + } sv = ms.aF; } return sv.helix.toTrackState(alphaCenter, ms.m.p, loc); } - + /** * Formatted debug printout of a single GBLStripClusterData object. - * - * @param clstr The GBLStripClusterData object to be printed + * + * @param clstr The GBLStripClusterData object to be printed */ public void printGBLStripClusterData(GBLStripClusterData clstr) { System.out.format("\nKalmanInterface.printGBLStripClusterData: cluster ID=%d, scatterOnly=%d\n", clstr.getId(), clstr.getScatterOnly()); @@ -599,80 +661,85 @@ public void printGBLStripClusterData(GBLStripClusterData clstr) { System.out.format(" Track intercept in sensor frame = %s\n", clstr.getTrackPos().toString()); System.out.format(" RMS projected scattering angle=%10.6f\n", clstr.getScatterAngle()); } - + /** - * Make a GBLStripClusterData object for each MeasurementSite of a Kalman track. This can be used to refit the track using - * the GBL program. - * - * @param kT The Kalman track - * @return List of strip-cluster data for this Kalman track + * Make a GBLStripClusterData object for each MeasurementSite of a Kalman + * track. This can be used to refit the track using the GBL program. + * + * @param kT The Kalman track + * @return List of strip-cluster data for this Kalman track */ public List createGBLStripClusterData(KalTrack kT) { List rtnList = new ArrayList(kT.SiteList.size()); - - double phi_org = kT.originHelixParms()[1]; - double phi_1state = kT.SiteList.get(0).aS.helix.a.v[1]; - double alpha = kT.helixAtOrigin.alpha; - double radius = Math.abs(alpha/kT.originHelixParms()[2]); - double arcLength2D = radius*(phi_1state-phi_org); - double arcLength = arcLength2D*Math.sqrt(1.0 + kT.originHelixParms()[4] * kT.originHelixParms()[4]); + + double phi_org = kT.originHelixParms()[1]; + double phi_1state = kT.SiteList.get(0).aS.helix.a.v[1]; + double alpha = kT.helixAtOrigin.alpha; + double radius = Math.abs(alpha / kT.originHelixParms()[2]); + double arcLength2D = radius * (phi_1state - phi_org); + double arcLength = arcLength2D * Math.sqrt(1.0 + kT.originHelixParms()[4] * kT.originHelixParms()[4]); double total_s3D = arcLength; double total_s2D = arcLength2D; - + double phiLast = 9999.; for (MeasurementSite site : kT.SiteList) { GBLStripClusterData clstr = new GBLStripClusterData(site.m.millipedeID); clstr.setVolume(site.m.topBottom); - + // Sites without hits are "scatter-only" - if (site.hitID < 0) clstr.setScatterOnly(1); - else clstr.setScatterOnly(0); - + if (site.hitID < 0) { + clstr.setScatterOnly(1); + } else { + clstr.setScatterOnly(0); + } + // Store the total Arc length in the GBLStripClusterData total_s3D += site.arcLength; clstr.setPath3D(total_s3D); double tanL = site.aS.helix.a.v[4]; - clstr.setPath(site.arcLength/FastMath.sqrt(1.+tanL*tanL)); - + clstr.setPath(site.arcLength / FastMath.sqrt(1. + tanL * tanL)); + Hep3Vector u = new BasicHep3Vector(vectorKalmanToTrk(site.m.p.V())); Hep3Vector v = new BasicHep3Vector(vectorKalmanToTrk(site.m.p.U().scale(-1.0))); Hep3Vector w = new BasicHep3Vector(vectorKalmanToTrk(site.m.p.T())); - + clstr.setU(u); clstr.setV(v); clstr.setW(w); - + // Direction of the track in the HPS tracking coordinate system // Find the momentum from the smoothed helix at the sensor location, make it a unit vector, // and then transform from the B-field frame to the Kalman global tracking frame. Vec momentum = site.aS.helix.getMom(0.); - Vec pDir= site.aS.helix.Rot.inverseRotate(momentum.unitVec()); + Vec pDir = site.aS.helix.Rot.inverseRotate(momentum.unitVec()); Hep3Vector trackDir = new BasicHep3Vector(vectorKalmanToTrk(pDir)); clstr.setTrackDir(trackDir); - + // Phi and lambda of the track (assuming standard spherical polar coordinates) double phi = FastMath.atan2(trackDir.y(), trackDir.x()); - double ct = trackDir.z()/trackDir.magnitude(); - double tanLambda = ct/FastMath.sqrt(1-ct*ct); // Should be very much the same as tanL above, after accounting for the field tilt + double ct = trackDir.z() / trackDir.magnitude(); + double tanLambda = ct / FastMath.sqrt(1 - ct * ct); // Should be very much the same as tanL above, after accounting for the field tilt if (debug) { - Vec tilted = site.aS.helix.Rot.inverseRotate(new Vec(0.,0.,1.)); + Vec tilted = site.aS.helix.Rot.inverseRotate(new Vec(0., 0., 1.)); double tiltAngle = FastMath.acos(tilted.v[2]); - System.out.format("KalmanInterface.createGBLStripClusterData: layer=%d det=%d tanL=%10.6f, tanLambda=%10.6f, tilt=%10.6f, sum=%10.6f\n", - site.m.Layer, site.m.detector, -tanL, tanLambda, tiltAngle, tiltAngle+tanLambda); + System.out.format("KalmanInterface.createGBLStripClusterData: layer=%d det=%d tanL=%10.6f, tanLambda=%10.6f, tilt=%10.6f, sum=%10.6f\n", + site.m.Layer, site.m.detector, -tanL, tanLambda, tiltAngle, tiltAngle + tanLambda); } clstr.setTrackPhi(phi); if (phiLast < 10.) { - if (Math.abs(phi - phiLast) > 1.2) System.out.format("Big phi change in event %d\n", eventNumber); + if (Math.abs(phi - phiLast) > 1.2) { + System.out.format("Big phi change in event %d\n", eventNumber); + } } phiLast = phi; clstr.setTrackLambda(FastMath.atan(tanLambda)); - + // Measured value in the sensor coordinates (u-value in the HPS system) double uMeas, uMeasErr; if (site.hitID >= 0) { - uMeas = site.m.hits.get(site.hitID).v; + uMeas = site.m.hits.get(site.hitID).v; uMeasErr = site.m.hits.get(site.hitID).sigma; //This is the un-biased residual error (post-fit) //uMeasErr = Math.sqrt(site.aS.R); @@ -683,45 +750,46 @@ public List createGBLStripClusterData(KalTrack kT) { } clstr.setMeas(uMeas); clstr.setMeasErr(uMeasErr); - + // Track position in local frame. First coordinate will be the predicted measurement. Vec rGlb = site.aS.helix.toGlobal(site.aS.helix.atPhi(0.)); Vec rLoc = site.m.toLocal(rGlb); Hep3Vector rLocHps = new BasicHep3Vector(localKalToHps(rLoc)); clstr.setTrackPos(rLocHps); - + // rms projected scattering angle double ctSensor = pDir.dot(site.m.p.T()); double XL = Math.abs((site.m.thickness / site.radLen) / ctSensor); clstr.setScatterAngle(HelixState.projMSangle(momentum.mag(), XL)); - + rtnList.add(clstr); } return rtnList; } /** - * Propagate a TrackState "stateHPS" to a plane given by "location" and "direction". - * The PropagatedTrackState object created includes the new propagated TrackState plus information on - * the intersection point of the track with the plane. This propagations is done using the full field map, + * Propagate a TrackState "stateHPS" to a plane given by "location" and + * "direction". The PropagatedTrackState object created includes the new + * propagated TrackState plus information on the intersection point of the + * track with the plane. This propagations is done using the full field map, * even if kPar.uniformB is true. - * - * @param stateHPS HPS trackstate at the plane in question - * @param location HPS location description (AtIP, AtFirstHit, AtLastHit, AtCalorimeter, AtVertex, AtOther) - * @param direction Direction cosines of the plane - * @return Kalman PropagatedTrackState object + * + * @param stateHPS HPS trackstate at the plane in question + * @param location HPS location description (AtIP, AtFirstHit, AtLastHit, + * AtCalorimeter, AtVertex, AtOther) + * @param direction Direction cosines of the plane + * @return Kalman PropagatedTrackState object */ - - public PropagatedTrackState propagateTrackState(TrackState stateHPS, double [] location, double [] direction) { + public PropagatedTrackState propagateTrackState(TrackState stateHPS, double[] location, double[] direction) { return new PropagatedTrackState(stateHPS, location, direction, detPlanes, kPar.SVTcenter, fM); } - + /** * Create an HPS track from a Kalman track - * - * @param kT Kalman track object - * @param storeTrackStates true to store in addition all of the track states - * @return HPS track + * + * @param kT Kalman track object + * @param storeTrackStates true to store in addition all of the track states + * @return HPS track */ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { if (kT.SiteList == null) { @@ -732,10 +800,10 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { logger.log(Level.FINE, "KalmanInterface.createTrack: Kalman track has NaN cov matrix."); return null; } - + kT.sortSites(true); BaseTrack newTrack = new BaseTrack(); - + // Add trackstate at IP as first trackstate, // and make this trackstate's params the overall track params DMatrixRMaj globalCov = new DMatrixRMaj(kT.originCovariance()); @@ -744,24 +812,26 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { // center of the magnet), so we need to use the magnetic field there to convert from 1/pt to curvature. // Field at the origin => For 2016 this is ~ 0.430612 T // In the center of SVT => For 2016 this is ~ 0.52340 T - Vec Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter ,0.), kT.SiteList.get(0).m.Bfield); + Vec Bfield = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), kT.SiteList.get(0).m.Bfield); double B = Bfield.mag(); double[] newParams = getLCSimParams(globalParams, alphaCenter); double[] newCov = getLCSimCov(globalCov, alphaCenter).asPackedArray(true); TrackState ts = new BaseTrackState(newParams, newCov, new double[]{0., 0., 0.}, TrackState.AtIP); if (ts != null) { - newTrack.getTrackStates().add(ts); + newTrack.getTrackStates().add(ts); newTrack.setTrackParameters(ts.getParameters(), B); newTrack.setCovarianceMatrix(new SymmetricMatrix(5, ts.getCovMatrix(), true)); } - + // Add the hits to the track for (MeasurementSite site : kT.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } newTrack.addHit(getHpsHit(site.m.hits.get(site.hitID))); } //System.out.printf("PF::Debug::newTrack site size %d \n",newTrack.getTrackerHits().size()); - + // Get the track states at each layer for (int i = 0; i < kT.SiteList.size(); i++) { MeasurementSite site = kT.SiteList.get(i); @@ -771,12 +841,12 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { //HpsSiSensor hssd = (HpsSiSensor) moduleMap.get(site.m).getSensor(); //int lay = hssd.getMillepedeId(); // System.out.printf("ssp id %d \n", hssd.getMillepedeId()); - if (i == 0) { loc = TrackState.AtFirstHit; - } else if (i == kT.SiteList.size() - 1) + } else if (i == kT.SiteList.size() - 1) { loc = TrackState.AtLastHit; - + } + /* //DO Not att the missing layer track states yet. if (storeTrackStates) { @@ -788,35 +858,36 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { } prevID = lay; } - */ - + */ if (loc == TrackState.AtFirstHit || loc == TrackState.AtLastHit || storeTrackStates) { ts = createTrackState(site, loc, true); - if (ts != null) newTrack.getTrackStates().add(ts); + if (ts != null) { + newTrack.getTrackStates().add(ts); + } } } - + // Extrapolate to the ECAL and make a new trackState there. BaseTrackState ts_ecal = TrackUtils.getTrackExtrapAtEcalRK(newTrack, fM); newTrack.getTrackStates().add(ts_ecal); - + // other track properties newTrack.setChisq(kT.chi2); newTrack.setNDF(kT.SiteList.size() - 5); newTrack.setTrackType(BaseTrack.TrackType.Y_FIELD.ordinal()); newTrack.setFitSuccess(true); - + return newTrack; } /** * Convert helix parameters from Kalman to LCSim - * - * @param oldParams Array of 5 Kalman helix parameters - * @param alpha Parameter to use to convert between momentum and curvature - * @return Array of 5 LCSim helix parameters + * + * @param oldParams Array of 5 Kalman helix parameters + * @param alpha Parameter to use to convert between momentum and curvature + * @return Array of 5 LCSim helix parameters */ - static double[] getLCSimParams(double[] oldParams, double alpha) { + static double[] getLCSimParams(double[] oldParams, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. double[] params = new double[5]; @@ -831,12 +902,12 @@ static double[] getLCSimParams(double[] oldParams, double alpha) { /** * Convert helix parameters from LCSim to Kalman - * - * @param oldParams Array of 5 LCSim helix parameters - * @param alpha Parameter to use to convert between momentum and curvature - * @return Array of 5 Kalman helix parameters + * + * @param oldParams Array of 5 LCSim helix parameters + * @param alpha Parameter to use to convert between momentum and curvature + * @return Array of 5 Kalman helix parameters */ - static double[] unGetLCSimParams(double[] oldParams, double alpha) { + static double[] unGetLCSimParams(double[] oldParams, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. double[] params = new double[5]; @@ -850,15 +921,15 @@ static double[] unGetLCSimParams(double[] oldParams, double alpha) { /** * Convert helix parameter covariance matrix from Kalman to LCSim - * - * @param oldCov Kalman covariance matrix - * @param alpha Parameter to use to convert between momentum and curvature - * @return Covariance matrix of LCSim helix parameters + * + * @param oldCov Kalman covariance matrix + * @param alpha Parameter to use to convert between momentum and curvature + * @return Covariance matrix of LCSim helix parameters */ static SymmetricMatrix getLCSimCov(DMatrixRMaj oldCov, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. - double[] d = { 1.0, -1.0, -1.0 / alpha, -1.0, -1.0 }; + double[] d = {1.0, -1.0, -1.0 / alpha, -1.0, -1.0}; SymmetricMatrix cov = new SymmetricMatrix(5); for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { @@ -876,15 +947,16 @@ static SymmetricMatrix getLCSimCov(DMatrixRMaj oldCov, double alpha) { /** * Convert helix parameter covariance matrix from LCSim to Kalman - * - * @param oldCov LCSIM symmetric covariance matrix as a linear array of 15 elements - * @param alpha Parameter to use to convert between momentum and curvature - * @return 5 by 5 covariance matrix for Kalman helix parameters + * + * @param oldCov LCSIM symmetric covariance matrix as a linear array of 15 + * elements + * @param alpha Parameter to use to convert between momentum and curvature + * @return 5 by 5 covariance matrix for Kalman helix parameters */ static double[][] ungetLCSimCov(double[] oldCov, double alpha) { // Note: since HPS-java has assumed a constant field for tracking, the alpha value used here should // correspond to the field at the center of the SVT or magnet. See the class variable alphaCenter. - double[] d = { 1.0, -1.0, -1.0 * alpha, -1.0, -1.0 }; + double[] d = {1.0, -1.0, -1.0 * alpha, -1.0, -1.0}; double[][] cov = new double[5][5]; cov[0][0] = oldCov[0] * d[0] * d[0]; cov[1][0] = oldCov[1] * d[1] * d[0]; @@ -910,18 +982,20 @@ static double[][] ungetLCSimCov(double[] oldCov, double alpha) { } /** - * Add hits to an existing track - * Used only for Kalman tracks made by fitting hits on a GBL track - * - * @param newTrack Track to which hits will be added + * Add hits to an existing track Used only for Kalman tracks made by fitting + * hits on a GBL track + * + * @param newTrack Track to which hits will be added */ private void addHitsToTrack(BaseTrack newTrack) { List measList = getMeasurements(); for (Measurement meas : measList) { TrackerHit hit = hitMap.get(meas); - if (hit != null) { - if (!newTrack.getTrackerHits().contains(hit)) newTrack.addHit(hit); + if (hit != null) { + if (!newTrack.getTrackerHits().contains(hit)) { + newTrack.addHit(hit); + } } } newTrack.setNDF(newTrack.getTrackerHits().size()); @@ -929,12 +1003,12 @@ private void addHitsToTrack(BaseTrack newTrack) { /** * Create an HPS track from a Kalman seed - * - * @param trk Seed - * @return HPS track + * + * @param trk Seed + * @return HPS track */ public BaseTrack createTrack(SeedTrack trk) { - double[] newPivot = { 0., 0., 0. }; + double[] newPivot = {0., 0., 0.}; double[] params = getLCSimParams(trk.pivotTransform(newPivot), alphaCenter); SymmetricMatrix cov = getLCSimCov(trk.covariance(), alphaCenter); BaseTrack newTrack = new BaseTrack(); @@ -948,25 +1022,26 @@ public BaseTrack createTrack(SeedTrack trk) { } /** - * Method to create one Kalman SiModule geometry object for each silicon-strip detector - * - * @param inputPlanes List of all the silicon-strip detector planes + * Method to create one Kalman SiModule geometry object for each + * silicon-strip detector + * + * @param inputPlanes List of all the silicon-strip detector planes */ public void createSiModules(List inputPlanes) { if (debug) { System.out.format("Entering KalmanInterface.creasteSiModules\n"); } detPlanes = inputPlanes; // keep this reference for use by other methods - + //2016 => 12 planes, 2019 => 14 planes int nPlanes = inputPlanes.size(); //System.out.printf("PF::nPlanes::%d", nPlanes); if (nPlanes == 40) { // 2019 vs 2016 detector first layer kPar.setFirstLayer(0); - } else { + } else { kPar.setFirstLayer(2); } - + SiMlist.clear(); for (SiStripPlane inputPlane : inputPlanes) { @@ -976,10 +1051,10 @@ public void createSiModules(List inputPlanes) { double[] uGlb = new double[3]; double[] vGlb = new double[3]; double[] tGlb = new double[3]; - for (int row=0; row<3; ++row) { - uGlb[row] = inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row,0); - vGlb[row] = inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row,1); - tGlb[row] = inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row,2); + for (int row = 0; row < 3; ++row) { + uGlb[row] = inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row, 0); + vGlb[row] = inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row, 1); + tGlb[row] = inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row, 2); } Vec uK = (vectorGlbToKalman(vGlb).scale(-1.0)); // u and v are reversed in hps compared to kalman Vec vK = vectorGlbToKalman(uGlb); @@ -989,18 +1064,18 @@ public void createSiModules(List inputPlanes) { Vec pointOnPlaneTransformed = vectorGlbToKalman(pointOnPlane); if (debug) { - System.out.format("\nSiTrackerHit local to global rotation matrix for %s:\n",temp.getName()); - for (int row=0; row<3; ++row) { - for (int col=0; col<3; ++col) { - System.out.format(" %10.6f", inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row,col)); + System.out.format("\nSiTrackerHit local to global rotation matrix for %s:\n", temp.getName()); + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 3; ++col) { + System.out.format(" %10.6f", inputPlane.getSensor().getGeometry().getLocalToGlobal().getRotation().getRotationMatrix().e(row, col)); } System.out.format("\n"); } - System.out.format("SiTrackerHit local to global translation vector for %s: %s\n",temp.getName(), - inputPlane.getSensor().getGeometry().getLocalToGlobal().getTranslation().getTranslationVector().toString()); + System.out.format("SiTrackerHit local to global translation vector for %s: %s\n", temp.getName(), + inputPlane.getSensor().getGeometry().getLocalToGlobal().getTranslation().getTranslationVector().toString()); uK.print("u in Kalman coordinates"); vK.print("v in Kalman coordinates"); - tK.print("t in Kalman coordinates"); + tK.print("t in Kalman coordinates"); pointOnPlaneTransformed.print("point on plane in Kalman coordinates"); Vec oldBadPoint = HpsSvtToKalman.rotate(new Vec(3, inputPlane.origin().v())); oldBadPoint.print("old, bad point on plane in Kalman coordinates"); @@ -1008,26 +1083,28 @@ public void createSiModules(List inputPlanes) { System.out.printf(" Building with Kalman plane: point %s normal %s \n", new BasicHep3Vector(pointOnPlaneTransformed.v).toString(), new BasicHep3Vector(tK.v).toString()); } - Plane p =new Plane(pointOnPlaneTransformed, tK, uK, vK); - - int kalLayer; + Plane p = new Plane(pointOnPlaneTransformed, tK, uK, vK); + + int kalLayer; boolean split; if (nPlanes == 40) { //Indexing valid for 2019 detector -> Include new layer-0, layers go from 0 to 13!! - kalLayer = temp.getLayerNumber()-1; + kalLayer = temp.getLayerNumber() - 1; split = (kalLayer < 4); } else { //Indexing valid for 2016 detector - kalLayer = temp.getLayerNumber()+1; + kalLayer = temp.getLayerNumber() + 1; split = false; } int topBottom = 1; - if (temp.isBottomLayer()) topBottom = 0; + if (temp.isBottomLayer()) { + topBottom = 0; + } int detector = temp.getModuleNumber(); if (kalLayer > 13) { - System.out.format("***KalmanInterface.createSiModules Warning: Kalman layer %d , tempLayer = %d out of range.***\n", kalLayer,temp.getLayerNumber()); - } + System.out.format("***KalmanInterface.createSiModules Warning: Kalman layer %d , tempLayer = %d out of range.***\n", kalLayer, temp.getLayerNumber()); + } int millipedeID = temp.getMillepedeId(); SiModule newMod = new SiModule(kalLayer, p, temp.isStereo(), inputPlane.getWidth(), inputPlane.getLength(), - split, inputPlane.getThickness(), fM, detector, millipedeID, topBottom); + split, inputPlane.getThickness(), fM, detector, millipedeID, topBottom); moduleMap.put(newMod, inputPlane); SiMlist.add(newMod); } @@ -1036,36 +1113,39 @@ public void createSiModules(List inputPlanes) { logger.log(Level.INFO, sim.toString()); } } - + /** * Method to feed simulated hits into the pattern recognition, for testing - * - * @param event Event header - * @param decoder Decoder for unpacking hit information - * @return true if it executed successfully + * + * @param event Event header + * @param decoder Decoder for unpacking hit information + * @return true if it executed successfully */ boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { boolean success = false; eventNumber = event.getEventNumber(); - if (debug || event.getEventNumber() < 50) System.out.format("KalmanInterface.fillAllSimHits: entering for event %d\n", event.getEventNumber()); - + if (debug || event.getEventNumber() < 50) { + System.out.format("KalmanInterface.fillAllSimHits: entering for event %d\n", event.getEventNumber()); + } + // Get the collection of 1D hits String stripHitInputCollectionName = "TrackerHits"; - if (!event.hasCollection(TrackerHit.class, stripHitInputCollectionName)) + if (!event.hasCollection(TrackerHit.class, stripHitInputCollectionName)) { return false; - + } + List striphits = event.get(SimTrackerHit.class, stripHitInputCollectionName); // Make a mapping from (Layer,Module) to hits - Map, ArrayList> hitSensorMap = new HashMap, ArrayList>(); + Map, ArrayList> hitSensorMap = new HashMap, ArrayList>(); for (SimTrackerHit hit1D : striphits) { //double[] tkMom = hit1D.getMomentum(); //System.out.format("MC true hit momentum=%10.5f %10.5f %10.5f\n",tkMom[0],tkMom[1],tkMom[2]); decoder.setID(hit1D.getCellID()); int Layer = decoder.getValue("layer") + 1; int Module = decoder.getValue("module"); - Pair sensor = new Pair(Layer,Module); + Pair sensor = new Pair(Layer, Module); ArrayList hitsInSensor = null; if (hitSensorMap.containsKey(sensor)) { @@ -1075,18 +1155,26 @@ boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { } hitsInSensor.add(hit1D); hitSensorMap.put(sensor, hitsInSensor); - if (debug) System.out.format(" Adding hit for layer %d, module %d\n", Layer, Module); + if (debug) { + System.out.format(" Adding hit for layer %d, module %d\n", Layer, Module); + } } int hitsFilled = 0; for (int modIndex = 0; modIndex < SiMlist.size(); ++modIndex) { SiModule module = SiMlist.get(modIndex); - Pair sensor = new Pair(module.Layer,module.detector); - if (debug) System.out.format(" Si module in layer %d module %d, extent=%8.3f to %8.3f\n", module.Layer, module.detector, module.yExtent[0], module.yExtent[1]); + Pair sensor = new Pair(module.Layer, module.detector); + if (debug) { + System.out.format(" Si module in layer %d module %d, extent=%8.3f to %8.3f\n", module.Layer, module.detector, module.yExtent[0], module.yExtent[1]); + } - if (!hitSensorMap.containsKey(sensor)) continue; + if (!hitSensorMap.containsKey(sensor)) { + continue; + } ArrayList hitsInSensor = hitSensorMap.get(sensor); - if (hitsInSensor == null) continue; + if (hitsInSensor == null) { + continue; + } for (int i = 0; i < hitsInSensor.size(); i++) { SimTrackerHit hit = hitsInSensor.get(i); @@ -1096,7 +1184,7 @@ boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { //module.p.print("Si module plane"); double du = 0.006; - double umeas = rLocal.v[1] + rnd.nextGaussian()*du; + double umeas = rLocal.v[1] + rnd.nextGaussian() * du; if (debug) { System.out.format("\nKalmanInterface:fillAllSimHits %d, the measurement uncertainty is set to %10.7f\n", i, du); @@ -1108,26 +1196,33 @@ boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { globalY.print("globalY"); System.out.format(" Adding measurement %10.5f to layer %d, module %d\n", umeas, module.Layer, module.detector); } - Measurement m = new Measurement(umeas, 0., du, 0., hit.getdEdx()*1000000., rGlobal, rLocal.v[1]); + Measurement m = new Measurement(umeas, 0., du, 0., hit.getdEdx() * 1000000., rGlobal, rLocal.v[1]); //rGlobal.print("new global hit location"); module.addMeasurement(m); simHitMap.put(m, hit); hitsFilled++; } - if (debug) { module.print("SiModule-filled"); } + if (debug) { + module.print("SiModule-filled"); + } + } + if (hitsFilled > 0) { + success = true; + } + if (debug) { + System.out.format("KalmanInterface.fillAllSimHits: %d hits were filled into Si Modules\n", hitsFilled); } - if (hitsFilled > 0) success = true; - if (debug) System.out.format("KalmanInterface.fillAllSimHits: %d hits were filled into Si Modules\n", hitsFilled); return success; } - + /** - * Method to fill all Si hits into the SiModule objects, to feed to the pattern recognition. - * - * @param event Event header - * @return true if it executed successfully + * Method to fill all Si hits into the SiModule objects, to feed to the + * pattern recognition. + * + * @param event Event header + * @return true if it executed successfully */ private boolean fillAllMeasurements(EventHeader event) { boolean success = false; @@ -1136,11 +1231,13 @@ private boolean fillAllMeasurements(EventHeader event) { // Get the collection of 1D hits String stripHitInputCollectionName = "StripClusterer_SiTrackerHitStrip1D"; List striphits = event.get(TrackerHit.class, stripHitInputCollectionName); - - if (striphits.size() > maxHits) maxHits = striphits.size(); + + if (striphits.size() > maxHits) { + maxHits = striphits.size(); + } if (_siHitsLimit > 0 && striphits.size() > _siHitsLimit) { nBigEvents++; - System.out.format("KalmanInterface::Skip event %d with %s %d hits > %d\n", event.getEventNumber(), + System.out.format("KalmanInterface::Skip event %d with %s %d hits > %d\n", event.getEventNumber(), stripHitInputCollectionName, striphits.size(), _siHitsLimit); return false; } else if (striphits.size() > 500) { @@ -1151,7 +1248,7 @@ private boolean fillAllMeasurements(EventHeader event) { Map> hitSensorMap = new HashMap>(); if (debug) { if (striphits.size() == 0) { - System.out.format("KalmanInterface:fillAllMeasurements, there are no strip hits in event %d\n",event.getEventNumber()); + System.out.format("KalmanInterface:fillAllMeasurements, there are no strip hits in event %d\n", event.getEventNumber()); } } for (TrackerHit hit1D : striphits) { @@ -1171,9 +1268,13 @@ private boolean fillAllMeasurements(EventHeader event) { for (int modIndex = 0; modIndex < SiMlist.size(); ++modIndex) { SiModule module = SiMlist.get(modIndex); SiStripPlane plane = moduleMap.get(module); - if (!hitSensorMap.containsKey(plane.getSensor())) continue; + if (!hitSensorMap.containsKey(plane.getSensor())) { + continue; + } ArrayList hitsInSensor = hitSensorMap.get(plane.getSensor()); - if (hitsInSensor == null) continue; + if (hitsInSensor == null) { + continue; + } for (int i = 0; i < hitsInSensor.size(); i++) { TrackerHit hit = hitsInSensor.get(i); @@ -1182,13 +1283,13 @@ private boolean fillAllMeasurements(EventHeader event) { if (debug) { System.out.format("\nFilling hits in SiModule for %s\n", plane.getName()); - SiTrackerHitStrip1D global = (new SiTrackerHitStrip1D(hit)).getTransformedHit(TrackerHitType.CoordinateSystem.GLOBAL); + SiTrackerHitStrip1D global = (new SiTrackerHitStrip1D(hit)).getTransformedHit(TrackerHitType.CoordinateSystem.GLOBAL); Vec rGlobal = vectorGlbToKalman(global.getPosition()); Vec rLocal = module.toLocal(rGlobal); - Vec hpsLocal = new Vec(3,localHit.getPosition()); + Vec hpsLocal = new Vec(3, localHit.getPosition()); rLocal.print("hps global hit transformed to Kalman local frame"); hpsLocal.print("hps local hit"); - + /* System.out.format("\nTesting the hps coordinate transformation matrices and displacements for %s\n",plane.getSensor().getName()); Vec hitGlobal = new Vec(3,global.getPosition()); @@ -1226,21 +1327,21 @@ private boolean fillAllMeasurements(EventHeader event) { newHitLocal.print("transformed global hit"); newHitLocal = lTogRot.inverseRotate(hitGlobal.dif(lTogTran)); newHitLocal.print("transformed global hit"); - */ + */ } - double [] lpos = localHit.getPosition(); + double[] lpos = localHit.getPosition(); double umeas = lpos[0]; double du = FastMath.sqrt(localHit.getCovarianceAsMatrix().diagonal(0)); - double time = localHit.getTime(); + double time = localHit.getTime(); double xStrip = -lpos[1]; // Center of strip, i.e. ~0 except in layers 0 and 1 if (xStrip > module.xExtent[1] || xStrip < module.xExtent[0]) { - logger.log(Level.WARNING, String.format("Event %d Layer %d, local hit at %9.4f %9.4f, %9.4f is outside detector extents %8.3f->%8.3f %8.3f->%8.3f", + logger.log(Level.WARNING, String.format("Event %d Layer %d, local hit at %9.4f %9.4f, %9.4f is outside detector extents %8.3f->%8.3f %8.3f->%8.3f", event.getEventNumber(), module.Layer, lpos[0], lpos[1], lpos[2], module.yExtent[0], module.yExtent[1], module.xExtent[0], module.xExtent[1])); } if (debug) { int nstrp = localHit.getRawHits().size(); - System.out.format("%d %d u = %9.4f +- %9.4f cov=%10.4e, %10.4e, %10.4e\n", module.Layer, nstrp, umeas, du, localHit.getCovarianceAsMatrix().e(0,0), - localHit.getCovarianceAsMatrix().e(1,0), localHit.getCovarianceAsMatrix().e(1,1)); + System.out.format("%d %d u = %9.4f +- %9.4f cov=%10.4e, %10.4e, %10.4e\n", module.Layer, nstrp, umeas, du, localHit.getCovarianceAsMatrix().e(0, 0), + localHit.getCovarianceAsMatrix().e(1, 0), localHit.getCovarianceAsMatrix().e(1, 1)); } // If HPS measured coordinate axis is opposite to Kalman measured coordinate axis @@ -1266,24 +1367,31 @@ private boolean fillAllMeasurements(EventHeader event) { globalX.print("globalX"); globalY.print("globalY"); } - Measurement m = new Measurement(umeas, xStrip, du, time, localHit.getdEdx()*1000000.); + Measurement m = new Measurement(umeas, xStrip, du, time, localHit.getdEdx() * 1000000.); module.addMeasurement(m); hitMap.put(m, hit); hitsFilled++; } - if (debug) { module.print("SiModule-filled"); } + if (debug) { + module.print("SiModule-filled"); + } + } + if (hitsFilled > 0) { + success = true; } - if (hitsFilled > 0) success = true; - if (debug) System.out.format("KalmanInterface.fillAllMeasurements: %d hits were filled into Si Modules\n", hitsFilled); - + if (debug) { + System.out.format("KalmanInterface.fillAllMeasurements: %d hits were filled into Si Modules\n", hitsFilled); + } + // Add MC truth information to each hit if it is available if (event.hasCollection(LCRelation.class, "SVTTrueHitRelations")) { RelationalTable rawtomc = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); - + List trueHitRelations = event.get(LCRelation.class, "SVTTrueHitRelations"); for (LCRelation relation : trueHitRelations) { - if (relation != null && relation.getFrom() != null && relation.getTo() != null) + if (relation != null && relation.getFrom() != null && relation.getTo() != null) { rawtomc.add(relation.getFrom(), relation.getTo()); + } } for (SiModule mod : SiMlist) { for (Measurement hit : mod.hits) { @@ -1293,22 +1401,28 @@ private boolean fillAllMeasurements(EventHeader event) { for (RawTrackerHit rawHit : rawHits) { Set simHits = rawtomc.allFrom(rawHit); for (SimTrackerHit simHit : simHits) { - if (hit.rGlobal == null) hit.rGlobal = vectorGlbToKalman(simHit.getPosition()); + if (hit.rGlobal == null) { + hit.rGlobal = vectorGlbToKalman(simHit.getPosition()); + } MCParticle mcp = simHit.getMCParticle(); - if (!hit.tksMC.contains(mcp.hashCode())) hit.tksMC.add(mcp.hashCode()); + if (!hit.tksMC.contains(mcp.hashCode())) { + hit.tksMC.add(mcp.hashCode()); + } } } } } - } + } return success; } /** - * Method to fill the Si hits into the SiModule objects for a given GBL track, in order to refit the track using Kalman filter - * - * @param hits1D 1-dimensional tracker hits - * @param addMode 0 to skip layers not already having hits; 1 to skip layers already having hits + * Method to fill the Si hits into the SiModule objects for a given GBL + * track, in order to refit the track using Kalman filter + * + * @param hits1D 1-dimensional tracker hits + * @param addMode 0 to skip layers not already having hits; 1 to skip layers + * already having hits * @return */ private double fillMeasurements(List hits1D, int addMode) { @@ -1318,8 +1432,11 @@ private double fillMeasurements(List hits1D, int addMode) { for (TrackerHit hit1D : hits1D) { HpsSiSensor temp = ((HpsSiSensor) ((RawTrackerHit) hit1D.getRawHits().get(0)).getDetectorElement()); int lay = temp.getLayerNumber(); - if (addMode == 0 && !SeedTrackLayers.contains((lay + 1) / 2)) continue; - else if (addMode == 1 && SeedTrackLayers.contains((lay + 1) / 2)) continue; + if (addMode == 0 && !SeedTrackLayers.contains((lay + 1) / 2)) { + continue; + } else if (addMode == 1 && SeedTrackLayers.contains((lay + 1) / 2)) { + continue; + } ArrayList hitsInLayer = null; if (hitsMap.containsKey(temp)) { @@ -1328,15 +1445,21 @@ private double fillMeasurements(List hits1D, int addMode) { hitsInLayer = new ArrayList(); } hitsInLayer.add(hit1D); - if (hit1D.getPosition()[2] < firstZ) firstZ = hit1D.getPosition()[2]; + if (hit1D.getPosition()[2] < firstZ) { + firstZ = hit1D.getPosition()[2]; + } hitsMap.put(temp, hitsInLayer); } for (SiModule mod : SiMlist) { SiStripPlane plane = moduleMap.get(mod); - if (!hitsMap.containsKey(plane.getSensor())) { continue; } + if (!hitsMap.containsKey(plane.getSensor())) { + continue; + } ArrayList temp = hitsMap.get(plane.getSensor()); - if (temp == null) { continue; } + if (temp == null) { + continue; + } Hep3Vector planeMeasuredVec = VecOp.mult(HpsSvtToKalmanMatrix, plane.getMeasuredCoordinate()); @@ -1353,9 +1476,9 @@ private double fillMeasurements(List hits1D, int addMode) { // if hps measured coord axis is opposite to kalman measured coord axis // This really should not happen, as the Kalman axis is copied directly from the hps geometry. - if (planeMeasuredVec.z() * mod.p.V().v[2] < 0) { + if (planeMeasuredVec.z() * mod.p.V().v[2] < 0) { System.out.format("*** KalmanInterface.fillMeasurements: flipping Kalman coordinate sign %d! ***\n", i); - umeas *= -1.0; + umeas *= -1.0; } if (debug) { @@ -1373,69 +1496,87 @@ private double fillMeasurements(List hits1D, int addMode) { globalX.print("globalX"); globalY.print("globalY"); } - Measurement m = new Measurement(umeas, xStrip, du, 0., hit.getdEdx()*1000000.); + Measurement m = new Measurement(umeas, xStrip, du, 0., hit.getdEdx() * 1000000.); - KalHit hitPair = new KalHit(mod,m); + KalHit hitPair = new KalHit(mod, m); trackHitsKalman.add(hitPair); mod.addMeasurement(m); hitMap.put(m, hit); } - if (debug) { mod.print("SiModule-filled"); } + if (debug) { + mod.print("SiModule-filled"); + } } return firstZ; } /** - * Make a linear fit to a set of hits to be used to initialize the Kalman Filter - * - * @param track GBL track - * @param hitToStrips Relation table for GBL hits to strips - * @param hitToRotated Relation table for GBL hits to rotated hits - * @return New track seed + * Make a linear fit to a set of hits to be used to initialize the Kalman + * Filter + * + * @param track GBL track + * @param hitToStrips Relation table for GBL hits to strips + * @param hitToRotated Relation table for GBL hits to rotated hits + * @return New track seed */ public SeedTrack createKalmanSeedTrack(Track track, RelationalTable hitToStrips, RelationalTable hitToRotated) { List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); double firstHitZ = fillMeasurements(hitsOnTrack, 0); - if (debug) System.out.printf("firstHitZ %f \n", firstHitZ); + if (debug) { + System.out.printf("firstHitZ %f \n", firstHitZ); + } return new SeedTrack(trackHitsKalman, firstHitZ, 0., kPar); } /** - * Method to refit an existing track's hits, using the Kalman seed-track to initialize the Kalman Filter. - * - * @param evtNumb Event number - * @param seed Kalman track seed - * @param track GBL track - * @param hitToStrips Relation table for GBL hits to strips - * @param hitToRotated Relation table for GBL hits to rotated hits - * @param nIt Number of iterations - * @return Kalman track fit object + * Method to refit an existing track's hits, using the Kalman seed-track to + * initialize the Kalman Filter. + * + * @param evtNumb Event number + * @param seed Kalman track seed + * @param track GBL track + * @param hitToStrips Relation table for GBL hits to strips + * @param hitToRotated Relation table for GBL hits to rotated hits + * @param nIt Number of iterations + * @return Kalman track fit object */ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated, int nIt) { double firstHitZ = 10000.; List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); - if (debug) { System.out.format("createKalmanTrackFit: number of hits on track = %d\n", hitsOnTrack.size()); } + if (debug) { + System.out.format("createKalmanTrackFit: number of hits on track = %d\n", hitsOnTrack.size()); + } for (TrackerHit hit1D : hitsOnTrack) { - if (hit1D.getPosition()[2] < firstHitZ) firstHitZ = hit1D.getPosition()[2]; + if (hit1D.getPosition()[2] < firstHitZ) { + firstHitZ = hit1D.getPosition()[2]; + } } ArrayList SiMoccupied = new ArrayList(); int startIndex = 0; fillMeasurements(hitsOnTrack, 1); for (SiModule SiM : SiMlist) { - if (!SiM.hits.isEmpty()) SiMoccupied.add(SiM); + if (!SiM.hits.isEmpty()) { + SiMoccupied.add(SiM); + } } Collections.sort(SiMoccupied, new SortByLayer()); for (int i = 0; i < SiMoccupied.size(); i++) { SiModule SiM = SiMoccupied.get(i); - if (SeedTrackLayers.contains((SiM.Layer + 1) / 2) && (i > startIndex)) { startIndex = i; } - if (debug) { SiM.print(String.format("SiMoccupied%d", i)); } + if (SeedTrackLayers.contains((SiM.Layer + 1) / 2) && (i > startIndex)) { + startIndex = i; + } + if (debug) { + SiM.print(String.format("SiMoccupied%d", i)); + } } - if (debug) { System.out.printf("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); } + if (debug) { + System.out.printf("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + } DMatrixRMaj cov = seed.covariance().copy(); CommonOps_DDRM.scale(10., cov); @@ -1444,47 +1585,57 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track t } /** - * Method to refit an existing GBL track, using the track's helix parameters and covariance to initialize the Kalman Filter. - * - * @param evtNumb Event number - * @param helixParams 5-vector of starting-guess Kalman helix parameters - * @param pivot Pivot point for the helix parameters - * @param cov Starting-guess Helix parameters covariance - * @param track GBL track - * @param hitToStrips Relation table for GBL hits to strips - * @param hitToRotated Relation table for GBL hits to rotated hits - * @param nIt Number of Kalman fit iterations - * @return Kalman track-fit object + * Method to refit an existing GBL track, using the track's helix parameters + * and covariance to initialize the Kalman Filter. + * + * @param evtNumb Event number + * @param helixParams 5-vector of starting-guess Kalman helix parameters + * @param pivot Pivot point for the helix parameters + * @param cov Starting-guess Helix parameters covariance + * @param track GBL track + * @param hitToStrips Relation table for GBL hits to strips + * @param hitToRotated Relation table for GBL hits to rotated hits + * @param nIt Number of Kalman fit iterations + * @return Kalman track-fit object */ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pivot, DMatrixRMaj cov, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated, int nIt) { List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); - if (debug) { System.out.format("createKalmanTrackFit: using GBL fit as start; number of hits on track = %d\n", hitsOnTrack.size()); } + if (debug) { + System.out.format("createKalmanTrackFit: using GBL fit as start; number of hits on track = %d\n", hitsOnTrack.size()); + } ArrayList SiMoccupied = new ArrayList(); fillMeasurements(hitsOnTrack, 2); for (SiModule SiM : SiMlist) { - if (!SiM.hits.isEmpty()) SiMoccupied.add(SiM); + if (!SiM.hits.isEmpty()) { + SiMoccupied.add(SiM); + } } Collections.sort(SiMoccupied, new SortByLayer()); for (int i = 0; i < SiMoccupied.size(); i++) { SiModule SiM = SiMoccupied.get(i); - if (debug) SiM.print(String.format("SiMoccupied%d", i)); + if (debug) { + SiM.print(String.format("SiMoccupied%d", i)); + } } int startIndex = 0; - if (debug) System.out.printf("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + if (debug) { + System.out.printf("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + } CommonOps_DDRM.scale(10., cov); return new KalmanTrackFit2(evtNumb, SiMoccupied, null, startIndex, nIt, pivot, helixParams, cov, kPar, fM); } /** * Sort the Kalman-filter geometry objects according to tracker layer - * + * */ class SortByLayer implements Comparator { + @Override public int compare(SiModule o1, SiModule o2) { return o1.Layer - o2.Layer; @@ -1492,29 +1643,36 @@ public int compare(SiModule o1, SiModule o2) { } /** - * Method to drive the Kalman-Filter based pattern recognition - * - * @param event Event header - * @param decoder Decoder for hit information - * @return Two lists of Kalman KalTrack objects, for top and bottom detectors + * Method to drive the Kalman-Filter based pattern recognition + * + * @param event Event header + * @param decoder Decoder for hit information + * @return Two lists of Kalman KalTrack objects, for top and bottom + * detectors */ public ArrayList[] KalmanPatRec(EventHeader event, IDDecoder decoder) { - if (debug) System.out.format("KalmanInterface: entering KalmanPatRec for event %d\n", event.getEventNumber()); + if (debug) { + System.out.format("KalmanInterface: entering KalmanPatRec for event %d\n", event.getEventNumber()); + } ArrayList[] outList = new ArrayList[2]; if (!fillAllMeasurements(event)) { - if (debug) System.out.format("KalmanInterface.KalmanPatRec: recon SVT hits not found for event %d\n",event.getEventNumber()); - for (int topBottom=0; topBottom<2; ++topBottom) { + if (debug) { + System.out.format("KalmanInterface.KalmanPatRec: recon SVT hits not found for event %d\n", event.getEventNumber()); + } + for (int topBottom = 0; topBottom < 2; ++topBottom) { outList[topBottom] = new ArrayList(); } return outList; // Return empty track lists if there are no hits } int evtNum = event.getEventNumber(); - - for (int topBottom=0; topBottom<2; ++topBottom) { + + for (int topBottom = 0; topBottom < 2; ++topBottom) { ArrayList SiMoccupied = new ArrayList(); for (SiModule SiM : SiMlist) { - if (SiM.topBottom != topBottom) continue; + if (SiM.topBottom != topBottom) { + continue; + } //if (topBottom == 0) { // if (SiM.p.X().v[2] < 0.) continue; //} else { @@ -1523,7 +1681,7 @@ public ArrayList[] KalmanPatRec(EventHeader event, IDDecoder decoder) SiMoccupied.add(SiM); // Need to keep all of these even if there are no hits!!!!!! } Collections.sort(SiMoccupied, new SortByLayer()); - + if (debug) { for (int i = 0; i < SiMoccupied.size(); i++) { SiModule SiM = SiMoccupied.get(i); @@ -1532,29 +1690,30 @@ public ArrayList[] KalmanPatRec(EventHeader event, IDDecoder decoder) System.out.format("KalmanInterface.KalmanPatRec event %d: calling KalmanPatRecHPS for topBottom=%d\n", event.getEventNumber(), topBottom); } outList[topBottom] = kPat.kalmanPatRec(event, hitMap, SiMoccupied, topBottom); - + //for (KalTrack tkr : outList[topBottom]) tkr.printLong("Kalman Track"); } return outList; } /** - * The following method is a debugging aid for comparing SeedTracker/GBL tracks to the Kalman counterparts. - * - * @param trackCollectionName Tracks to be compared to Kalman - * @param event Event header - * @param kPatList Lists of all Kalman tracks top and bottom + * The following method is a debugging aid for comparing SeedTracker/GBL + * tracks to the Kalman counterparts. + * + * @param trackCollectionName Tracks to be compared to Kalman + * @param event Event header + * @param kPatList Lists of all Kalman tracks top and bottom */ public void compareAllTracks(String trackCollectionName, EventHeader event, ArrayList[] kPatList) { if (!event.hasCollection(Track.class, trackCollectionName)) { - System.out.format("\nKalmanInterface.compareAllTracks: the track collection %s is missing. Abort.\n",trackCollectionName); + System.out.format("\nKalmanInterface.compareAllTracks: the track collection %s is missing. Abort.\n", trackCollectionName); return; } String stripHitInputCollectionName = "StripClusterer_SiTrackerHitStrip1D"; if (!event.hasCollection(TrackerHit.class, stripHitInputCollectionName)) { - System.out.format("\nKalmanInterface.compareAllTracks: the hit collection %s is missing. Abort.\n",stripHitInputCollectionName); + System.out.format("\nKalmanInterface.compareAllTracks: the hit collection %s is missing. Abort.\n", stripHitInputCollectionName); return; - } + } List tracksGBL = event.get(Track.class, trackCollectionName); System.out.format("\nPrinting %s tracks for event %d\n", trackCollectionName, event.getEventNumber()); RelationalTable hitToStrips = TrackUtils.getHitToStripsTable(event); @@ -1574,19 +1733,19 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra System.out.format("Track %d, missing TrackState.\n", tracksGBL.indexOf(tkr)); continue; } - double [] a = new double[5]; - for (int i=0; i<5; ++i) { + double[] a = new double[5]; + for (int i = 0; i < 5; ++i) { a[i] = ts1.getParameter(i); } double[] covHPS = ts1.getCovMatrix(); double Q = tkr.getCharge(); double chi2 = tkr.getChi2(); int nHits = tkr.getTrackerHits().size(); - System.out.format("Track %d, Q=%4.1f, %d 3D hits, chi^2=%7.1f, helix=%8.3f %9.6f %9.6f %8.4f %8.4f\n", tracksGBL.indexOf(tkr), + System.out.format("Track %d, Q=%4.1f, %d 3D hits, chi^2=%7.1f, helix=%8.3f %9.6f %9.6f %8.4f %8.4f\n", tracksGBL.indexOf(tkr), Q, nHits, chi2, a[0], a[1], a[2], a[3], a[4]); - Vec kalParms = new Vec(5,unGetLCSimParams(a, alphaCenter)); + Vec kalParms = new Vec(5, unGetLCSimParams(a, alphaCenter)); System.out.format(" Helix in Kalman parameterization = %s\n", kalParms.toString()); -/* for (TrackerHit hit3D : tkr.getTrackerHits()) { + /* for (TrackerHit hit3D : tkr.getTrackerHits()) { double [] hitPos3D = hit3D.getPosition(); System.out.format("compareAllTracks: tracker 3D hit %10.6f %10.6f %10.6f\n", hitPos3D[0], hitPos3D[1], hitPos3D[2]); List hits = new ArrayList(); @@ -1604,13 +1763,13 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra } } } - */ + */ List hitsOnTrack = TrackUtils.getStripHits(tkr, hitToStrips, hitToRotated); //System.out.format(" hitsOnTrack = %s, %d\n", hitsOnTrack.toString(), hitsOnTrack.size()); - int [] chanGBL = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - int [] chanKAL = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - for (TrackerHit ht : hitsOnTrack) { - double [] pnt = ht.getPosition(); + int[] chanGBL = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + int[] chanKAL = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + for (TrackerHit ht : hitsOnTrack) { + double[] pnt = ht.getPosition(); System.out.format(" Hit global position: %10.6f %10.6f %10.6f\n", pnt[0], pnt[1], pnt[2]); List rawHits = ht.getRawHits(); for (RawTrackerHit rawHit : rawHits) { @@ -1619,7 +1778,9 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra int Layer = sensor.getLayerNumber(); int sensorID = sensor.getModuleNumber(); System.out.format(" Raw hit in layer %d, sensor %d, channel %d\n", Layer, sensorID, chan); - if (sensorID*10000 + chan > chanGBL[Layer-1]) chanGBL[Layer-1] = sensorID*10000 + chan; + if (sensorID * 10000 + chan > chanGBL[Layer - 1]) { + chanGBL[Layer - 1] = sensorID * 10000 + chan; + } } } //double [] pnt0 = hitsOnTrack.get(0).getPosition(); @@ -1632,12 +1793,16 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra //} int topBottom = 0; int nGood = 0; - if (kalParms.v[4] < 0.) topBottom = 1; + if (kalParms.v[4] < 0.) { + topBottom = 1; + } KalTrack kMatch = null; for (KalTrack ktk : kPatList[topBottom]) { int nMatch = 0; for (MeasurementSite site : ktk.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } SiModule mod = site.m; if (mod != null) { TrackerHit ht = this.getHpsHit(mod.hits.get(site.hitID)); @@ -1647,7 +1812,7 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra HpsSiSensor sensor = (HpsSiSensor) rawHit.getDetectorElement(); int Layer = sensor.getLayerNumber(); int sensorID = sensor.getModuleNumber(); - if (chanGBL[Layer-1] == 10000*sensorID + chan) { + if (chanGBL[Layer - 1] == 10000 * sensorID + chan) { nMatch++; break; } @@ -1662,7 +1827,9 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra if (kMatch != null) { int nKalHits = 0; for (MeasurementSite site : kMatch.SiteList) { - if (site.hitID < 0) continue; + if (site.hitID < 0) { + continue; + } nKalHits++; SiModule mod = site.m; if (mod != null) { @@ -1673,12 +1840,14 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra HpsSiSensor sensor = (HpsSiSensor) rawHit.getDetectorElement(); int Layer = sensor.getLayerNumber(); int sensorID = sensor.getModuleNumber(); - if (10000*sensorID + chan > chanKAL[Layer-1]) chanKAL[Layer-1] = 10000*sensorID + chan; + if (10000 * sensorID + chan > chanKAL[Layer - 1]) { + chanKAL[Layer - 1] = 10000 * sensorID + chan; + } } } } System.out.format("GBL/Kalman match, ID=%d, %d hits, with %d matching layers\n", kMatch.ID, nKalHits, nGood); - for (int lyr=0; lyr<14; ++lyr) { + for (int lyr = 0; lyr < 14; ++lyr) { System.out.format(" Layer %d: GBL=%d KAL=%d\n", lyr, chanGBL[lyr], chanKAL[lyr]); } boolean refit = false; @@ -1695,16 +1864,19 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra if (refit) { ArrayList modList = new ArrayList(nGood); ArrayList hits = new ArrayList(nGood); - for (int lyr=0; lyr<14; ++lyr) { + for (int lyr = 0; lyr < 14; ++lyr) { if (chanGBL[lyr] >= 0) { for (SiModule mod : SiMlist) { - if (mod.Layer != lyr) continue; + if (mod.Layer != lyr) { + continue; + } int sensorID = mod.detector; - HitLoop: for (Measurement kalHt : mod.hits) { + HitLoop: + for (Measurement kalHt : mod.hits) { TrackerHit ht = this.getHpsHit(kalHt); List rawHits = ht.getRawHits(); for (RawTrackerHit rawHit : rawHits) { - if (10000*sensorID + rawHit.getIdentifierFieldValue("strip") == chanGBL[lyr]) { + if (10000 * sensorID + rawHit.getIdentifierFieldValue("strip") == chanGBL[lyr]) { modList.add(mod); hits.add(mod.hits.indexOf(kalHt)); break HitLoop; @@ -1713,8 +1885,8 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra } } } - } - + } + // The following is for testing by refitting the existing Kalman track //for (MeasurementSite site : kMatch.SiteList) { // if (site.hitID < 0) continue; @@ -1724,32 +1896,34 @@ public void compareAllTracks(String trackCollectionName, EventHeader event, Arra DMatrixRMaj cov = hx.C.copy(); CommonOps_DDRM.scale(10., cov); KalmanTrackFit2 kft2 = new KalmanTrackFit2(event.getEventNumber(), modList, hits, 0, 2, hx.X0, hx.a, cov, kPar, fM); - if (kft2 != null) kft2.printFit("refit with GBL hits"); + if (kft2 != null) { + kft2.printFit("refit with GBL hits"); + } } kMatch.print("matching Kalman track"); } } - } - + } + /** * Plots GBL tracks by creating an ASCII file to be displayed by GNUplot - * - * @param path Path to the folder where the output file should be written - * @param event Event header + * + * @param path Path to the folder where the output file should be written + * @param event Event header */ public void plotGBLtracks(String path, EventHeader event) { - + String trackCollectionName = "GBLTracks"; if (!event.hasCollection(Track.class, trackCollectionName)) { - System.out.format("KalmanInterface.plotGBLtracks: the track collection %s is missing. Abort.\n",trackCollectionName); + System.out.format("KalmanInterface.plotGBLtracks: the track collection %s is missing. Abort.\n", trackCollectionName); return; } String stripHitInputCollectionName = "StripClusterer_SiTrackerHitStrip1D"; if (!event.hasCollection(TrackerHit.class, stripHitInputCollectionName)) { - System.out.format("KalmanInterface.plotGBLtracks: the hit collection %s is missing. Abort.\n",stripHitInputCollectionName); + System.out.format("KalmanInterface.plotGBLtracks: the hit collection %s is missing. Abort.\n", stripHitInputCollectionName); return; } - + PrintWriter printWriter3 = null; int eventNumber = event.getEventNumber(); String fn = String.format("%sGBLhelix_%d.gp", path, eventNumber); @@ -1768,7 +1942,6 @@ public void plotGBLtracks(String path, EventHeader event) { printWriter3.format("set title 'GBL Event Number %d'\n", eventNumber); printWriter3.format("set xlabel 'X'\n"); printWriter3.format("set ylabel 'Y'\n"); - List tracksGBL = event.get(Track.class, trackCollectionName); RelationalTable hitToStrips = TrackUtils.getHitToStripsTable(event); @@ -1776,55 +1949,58 @@ public void plotGBLtracks(String path, EventHeader event) { double vPos = 0.9; for (Track tkr : tracksGBL) { - double [] a = new double[5]; - for (int i=0; i<5; ++i) { + double[] a = new double[5]; + for (int i = 0; i < 5; ++i) { a[i] = tkr.getTrackStates().get(0).getParameter(i); } double Q = tkr.getCharge(); double chi2 = tkr.getChi2(); int nHits = tkr.getTrackerHits().size(); - String s = String.format("Track %d, Q=%4.1f, %d hits, chi^2=%7.1f, helix=%8.3f %8.3f %8.3f %8.3f %8.3f", tracksGBL.indexOf(tkr), + String s = String.format("Track %d, Q=%4.1f, %d hits, chi^2=%7.1f, helix=%8.3f %8.3f %8.3f %8.3f %8.3f", tracksGBL.indexOf(tkr), Q, nHits, chi2, a[0], a[1], a[2], a[3], a[4]); printWriter3.format("set label '%s' at screen 0.1, %2.2f\n", s, vPos); vPos = vPos - 0.03; } - + for (Track tkr : tracksGBL) { printWriter3.format("$tkr%d << EOD\n", tracksGBL.indexOf(tkr)); for (TrackState state : tkr.getTrackStates()) { int loc = state.getLocation(); if (loc != state.AtIP && loc != state.AtCalorimeter && loc != state.AtOther && loc != state.AtVertex) { - double [] pnt = state.getReferencePoint(); + double[] pnt = state.getReferencePoint(); printWriter3.format(" %10.6f %10.6f %10.6f\n", pnt[0], pnt[2], -pnt[1]); } } printWriter3.format("EOD\n"); - } - + } + for (Track tkr : tracksGBL) { printWriter3.format("$tkp%d << EOD\n", tracksGBL.indexOf(tkr)); List hitsOnTrack = TrackUtils.getStripHits(tkr, hitToStrips, hitToRotated); - for (TrackerHit ht : hitsOnTrack) { - double [] pnt = ht.getPosition(); + for (TrackerHit ht : hitsOnTrack) { + double[] pnt = ht.getPosition(); printWriter3.format(" %10.6f %10.6f %10.6f\n", pnt[0], pnt[2], -pnt[1]); } printWriter3.format("EOD\n"); } - + List stripHits = event.get(TrackerHit.class, stripHitInputCollectionName); printWriter3.format("$pnts << EOD\n"); - unUsedHits: for (TrackerHit ht : stripHits) { + unUsedHits: + for (TrackerHit ht : stripHits) { for (Track tkr : tracksGBL) { List hitsOnTrack = TrackUtils.getStripHits(tkr, hitToStrips, hitToRotated); for (TrackerHit ht2 : hitsOnTrack) { - if (ht2 == ht) continue unUsedHits; + if (ht2 == ht) { + continue unUsedHits; + } } } - double [] pnt = ht.getPosition(); + double[] pnt = ht.getPosition(); printWriter3.format(" %10.6f %10.6f %10.6f\n", pnt[0], pnt[2], -pnt[1]); } printWriter3.format("EOD\n"); - + printWriter3.format("splot $pnts u 1:2:3 with points pt 6 ps 2"); for (Track tkr : tracksGBL) { printWriter3.format(", $tkp%d u 1:2:3 with points pt 7 ps 2", tracksGBL.indexOf(tkr)); @@ -1833,15 +2009,18 @@ public void plotGBLtracks(String path, EventHeader event) { printWriter3.format("\n"); printWriter3.close(); } + /** - * This method makes a Gnuplot file to display the Kalman tracks and hits in 3D. - * - * @param path Path to the folder where the output text file will be writtng - * @param event Event header - * @param patRecList Array of dimension 2 (up vs down) of lists of tracks to be plotted + * This method makes a Gnuplot file to display the Kalman tracks and hits in + * 3D. + * + * @param path Path to the folder where the output text file will be writtng + * @param event Event header + * @param patRecList Array of dimension 2 (up vs down) of lists of tracks to + * be plotted */ public void plotKalmanEvent(String path, EventHeader event, ArrayList[] patRecList) { - + boolean debug = false; PrintWriter printWriter3 = null; int eventNumber = event.getEventNumber(); @@ -1862,19 +2041,21 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ printWriter3.format("set xlabel 'X'\n"); printWriter3.format("set ylabel 'Y'\n"); double vPos = 0.9; - for (int topBottom=0; topBottom<2; ++topBottom) { + for (int topBottom = 0; topBottom < 2; ++topBottom) { for (KalTrack tkr : patRecList[topBottom]) { - double [] a = tkr.originHelixParms(); - if (a == null) a = tkr.SiteList.get(0).aS.helix.a.v; - String s = String.format("TB %d Track %d, %d hits, chi^2=%7.1f, a=%8.3f %8.3f %8.3f %8.3f %8.3f t=%6.1f", + double[] a = tkr.originHelixParms(); + if (a == null) { + a = tkr.SiteList.get(0).aS.helix.a.v; + } + String s = String.format("TB %d Track %d, %d hits, chi^2=%7.1f, a=%8.3f %8.3f %8.3f %8.3f %8.3f t=%6.1f", topBottom, tkr.ID, tkr.nHits, tkr.chi2, a[0], a[1], a[2], a[3], a[4], tkr.getTime()); printWriter3.format("set label '%s' at screen 0.1, %2.2f\n", s, vPos); vPos = vPos - 0.03; } } - int [] nTkpL = {0, 0}; - int [] nTkpS = {0, 0}; - for (int topBottom=0; topBottom<2; ++topBottom) { // Plotting tracks as lines + int[] nTkpL = {0, 0}; + int[] nTkpS = {0, 0}; + for (int topBottom = 0; topBottom < 2; ++topBottom) { // Plotting tracks as lines for (KalTrack tkr : patRecList[topBottom]) { printWriter3.format("$tkr%d_%d << EOD\n", tkr.ID, topBottom); for (MeasurementSite site : tkr.SiteList) { @@ -1891,7 +2072,9 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ continue; } double phiS = aS.helix.planeIntersect(module.p); - if (Double.isNaN(phiS)) continue; + if (Double.isNaN(phiS)) { + continue; + } Vec rLocal = aS.helix.atPhi(phiS); Vec rGlobal = aS.helix.toGlobal(rLocal); printWriter3.format(" %10.6f %10.6f %10.6f\n", rGlobal.v[0], rGlobal.v[1], rGlobal.v[2]); @@ -1914,10 +2097,16 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ for (MeasurementSite site : tkr.SiteList) { SiModule module = site.m; int hitID = site.hitID; - if (hitID < 0) continue; + if (hitID < 0) { + continue; + } Measurement mm = module.hits.get(hitID); - if (mm.energy < kPar.minSeedE[module.Layer]) continue; - if (mm.tracks.size() > 1) continue; + if (mm.energy < kPar.minSeedE[module.Layer]) { + continue; + } + if (mm.tracks.size() > 1) { + continue; + } Vec rLoc = null; if (mm.rGlobal == null) { // If there is no MC truth, use the track intersection for x and z StateVector aS = site.aS; @@ -1937,16 +2126,18 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ } } else { if (debug) { - System.out.format("plotKalmanEvent %d: tk %d lyr %d phiS is NaN.\n", event.getEventNumber(),tkr.ID, module.Layer); + System.out.format("plotKalmanEvent %d: tk %d lyr %d phiS is NaN.\n", event.getEventNumber(), tkr.ID, module.Layer); aS.helix.a.print(" helix parameters "); } - rLoc = new Vec(0.,0.,0.); + rLoc = new Vec(0., 0., 0.); } } else { rLoc = module.toLocal(mm.rGlobal); // Use MC truth for the x and z coordinates in the detector frame } Vec rmG = module.toGlobal(new Vec(rLoc.v[0], mm.v, rLoc.v[2])); - if (debug) System.out.format("plotKalmanEvent %d: tk %d lyr %d rmG=%s\n", event.getEventNumber(), tkr.ID, module.Layer, rmG.toString()); + if (debug) { + System.out.format("plotKalmanEvent %d: tk %d lyr %d rmG=%s\n", event.getEventNumber(), tkr.ID, module.Layer, rmG.toString()); + } printWriter3.format(" %10.6f %10.6f %10.6f\n", rmG.v[0], rmG.v[1], rmG.v[2]); } printWriter3.format("EOD\n"); @@ -1956,10 +2147,16 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ for (MeasurementSite site : tkr.SiteList) { SiModule module = site.m; int hitID = site.hitID; - if (hitID < 0) continue; + if (hitID < 0) { + continue; + } Measurement mm = module.hits.get(hitID); - if (mm.energy < kPar.minSeedE[module.Layer]) continue; - if (mm.tracks.size() <= 1) continue; + if (mm.energy < kPar.minSeedE[module.Layer]) { + continue; + } + if (mm.tracks.size() <= 1) { + continue; + } Vec rLoc = null; if (mm.rGlobal == null) { // If there is no MC truth, use the track intersection for x and z StateVector aS = site.aS; @@ -1969,7 +2166,7 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ Vec rGlobal = aS.helix.toGlobal(rLocal); // Position in the global frame rLoc = module.toLocal(rGlobal); // Position in the detector frame } else { - rLoc = new Vec(0.,0.,0.); + rLoc = new Vec(0., 0., 0.); } } else { rLoc = module.toLocal(mm.rGlobal); // Use MC truth for the x and z coordinates in the detector frame @@ -1985,9 +2182,13 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ for (MeasurementSite site : tkr.SiteList) { SiModule module = site.m; int hitID = site.hitID; - if (hitID < 0) continue; + if (hitID < 0) { + continue; + } Measurement mm = module.hits.get(hitID); - if (mm.energy >= kPar.minSeedE[module.Layer]) continue; + if (mm.energy >= kPar.minSeedE[module.Layer]) { + continue; + } Vec rLoc = null; if (mm.rGlobal == null) { // If there is no MC truth, use the track intersection for x and z StateVector aS = site.aS; @@ -1997,7 +2198,7 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ Vec rGlobal = aS.helix.toGlobal(rLocal); // Position in the global frame rLoc = module.toLocal(rGlobal); // Position in the detector frame } else { - rLoc = new Vec(0.,0.,0.); + rLoc = new Vec(0., 0., 0.); } } else { rLoc = module.toLocal(mm.rGlobal); // Use MC truth for the x and z coordinates in the detector frame @@ -2012,11 +2213,15 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ printWriter3.format("$pnts << EOD\n"); for (SiModule si : SiMlist) { for (Measurement mm : si.hits) { // Plotting high-amplitude hits not on tracks - if (mm.tracks.size() > 0) continue; - if (mm.energy < kPar.minSeedE[si.Layer]) continue; + if (mm.tracks.size() > 0) { + continue; + } + if (mm.energy < kPar.minSeedE[si.Layer]) { + continue; + } Vec rLoc = null; if (mm.rGlobal == null) { - rLoc = new Vec(0.,0.,0.); // Use the center of the detector if there is no MC truth info + rLoc = new Vec(0., 0., 0.); // Use the center of the detector if there is no MC truth info } else { rLoc = si.toLocal(mm.rGlobal); // Use MC truth for the x and z coordinates in the detector frame } @@ -2028,11 +2233,15 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ printWriter3.format("$pntsL << EOD\n"); for (SiModule si : SiMlist) { for (Measurement mm : si.hits) { // Plotting low-amplitude hits not on tracks - if (mm.tracks.size() > 0) continue; - if (mm.energy >= kPar.minSeedE[si.Layer]) continue; + if (mm.tracks.size() > 0) { + continue; + } + if (mm.energy >= kPar.minSeedE[si.Layer]) { + continue; + } Vec rLoc = null; if (mm.rGlobal == null) { - rLoc = new Vec(0.,0.,0.); // Use the center of the detector if there is no MC truth info + rLoc = new Vec(0., 0., 0.); // Use the center of the detector if there is no MC truth info } else { rLoc = si.toLocal(mm.rGlobal); // Use MC truth for the x and z coordinates in the detector frame } @@ -2045,13 +2254,17 @@ public void plotKalmanEvent(String path, EventHeader event, ArrayList[ printWriter3.format("splot $pnts u 1:2:3 with points pt 6 ps 2 lc %d", idx); idx++; printWriter3.format(", $pntsL u 1:2:3 with points pt 4 ps 1 lc %d", idx); - for (int topBottom=0; topBottom<2; ++topBottom) { - for (KalTrack tkr : patRecList[topBottom]) { + for (int topBottom = 0; topBottom < 2; ++topBottom) { + for (KalTrack tkr : patRecList[topBottom]) { idx++; - printWriter3.format(", $tkr%d_%d u 1:2:3 with lines lw 3 lc %d", tkr.ID, topBottom, idx); - printWriter3.format(", $tkp%d_%d u 1:2:3 with points pt 7 ps 2 lc %d", tkr.ID, topBottom, idx); - if (nTkpL[topBottom] > 0) printWriter3.format(", $tkpL%d_%d u 1:2:3 with points pt 9 ps 2 lc %d", tkr.ID, topBottom, idx); - if (nTkpS[topBottom] > 0) printWriter3.format(", $tkpS%d_%d u 1:2:3 with points pt 15 ps 2 lc %d", tkr.ID, topBottom, idx); + printWriter3.format(", $tkr%d_%d u 1:2:3 with lines lw 3 lc %d", tkr.ID, topBottom, idx); + printWriter3.format(", $tkp%d_%d u 1:2:3 with points pt 7 ps 2 lc %d", tkr.ID, topBottom, idx); + if (nTkpL[topBottom] > 0) { + printWriter3.format(", $tkpL%d_%d u 1:2:3 with points pt 9 ps 2 lc %d", tkr.ID, topBottom, idx); + } + if (nTkpS[topBottom] > 0) { + printWriter3.format(", $tkpS%d_%d u 1:2:3 with points pt 15 ps 2 lc %d", tkr.ID, topBottom, idx); + } } } printWriter3.format("\n"); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index 86fba49536..34b5660afb 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -6,57 +6,61 @@ /** * Parameters used by the Kalman-Filter pattern recognition and fitting - */ + */ public class KalmanParams { + static final int mxTrials = 2; // Max number of iterations through the entire pattern recognition; not configurable int nTrials; // Number of iterations through the entire pattern recognition int nIterations; // Number of Kalman-fit iterations in the final fit - double[] kMax; + double[] kMax; double kMin; - double[] tanlMax; - double[] dRhoMax; + double[] tanlMax; + double[] dRhoMax; double[] dzMax; - double[] chi2mx1; + double[] chi2mx1; int minHits0; - int[] minHits1; - double mxChi2Inc; + int[] minHits1; + double mxChi2Inc; double minChi2IncBad; double mxChi2Vtx; - double[] mxResid; - double mxResidShare; + double[] mxResid; + double mxResidShare; double mxChi2double; int firstLayer; - int mxShared; - int [] minStereo; + int mxShared; + int[] minStereo; int minAxial; double mxTdif; double lowPhThresh; double seedCompThr; // Compatibility threshold for seedTracks helix parameters; - ArrayList [] lyrList; - double [] beamSpot; - double [] vtxSize; - double [] minSeedE; + ArrayList[] lyrList; + double[] beamSpot; + double[] vtxSize; + double[] minSeedE; double edgeTolerance; static final int numLayers = 14; boolean uniformB; boolean eLoss; - - private int[] Swap = {1,0, 3,2, 5,4, 7,6, 9,8, 11,10, 13,12}; - private String [] tb; + + private int[] Swap = {1, 0, 3, 2, 5, 4, 7, 6, 9, 8, 11, 10, 13, 12}; + private String[] tb; private Logger logger; int maxListIter1; double SVTcenter = 505.57; // Location to evaluate the field in case it is assumed uniform - - /** - * Print all the Kalman Tracking parameters values (good idea to call it at the beginning of the run) + + /** + * Print all the Kalman Tracking parameters values (good idea to call it at + * the beginning of the run) */ public void print() { System.out.format("\nKalmanParams: dump of the Kalman pattern recognition cuts and parameters\n"); System.out.println(" (In the case of two values, they refer to the two iterations.)"); System.out.format(" There are %d layers in the tracker.\n", numLayers); - if (uniformB) System.out.format(" The magnetic field is assumed to be uniform!\n"); + if (uniformB) { + System.out.format(" The magnetic field is assumed to be uniform!\n"); + } System.out.format(" Cluster energy cuts for seeds, by layer: "); - for (int lyr=0; lyr(); - } + } // 0 1 2 3 4 5 6 // A S A S A S A S A S A S A S top // 0,1,2,3,4,5,6,7,8,9,10,11,12,13 @@ -198,7 +202,7 @@ public KalmanParams() { addStrategy("SBB0000"); addStrategy("SABS000"); maxListIter1 = 16; // The maximum index for lyrList for the first iteration - + beamSpot = new double[3]; beamSpot[0] = 0.; beamSpot[1] = 0.; @@ -224,266 +228,269 @@ public KalmanParams() { // beamSpot[2] = beamPosKal.v[2]; // } } - + public void setUniformB(boolean input) { logger.config(String.format("Setting the field to be uniform? %b", input)); uniformB = input; } - + public void setEloss(boolean eLoss) { - logger.config(String.format("Setting the energy loss to %b", eLoss)); - this.eLoss = eLoss; + logger.config(String.format("Setting the energy loss to %b", eLoss)); + this.eLoss = eLoss; } - + public void setLowPhThreshold(double cut) { - if (cut <0. || cut > 1.) { + if (cut < 0. || cut > 1.) { logger.warning(String.format("low pulse-height threshold %10.4f is not valid and is ignored.", cut)); return; } logger.config(String.format("Setting the low-pulse-height threshold to %10.4f", cut)); lowPhThresh = cut; } - + public void setMinSeedEnergy(double minE) { logger.config("Setting the minimum seed energy to " + Double.toString(minE)); - for (int lyr=0; lyr mxTrials) { - logger.log(Level.WARNING,String.format("Number of global iterations %d is not valid and is ignored.", nTrials)); + logger.log(Level.WARNING, String.format("Number of global iterations %d is not valid and is ignored.", nTrials)); return; } logger.log(Level.CONFIG, String.format("Setting the number of global patrec iterations to %d", nTrials)); this.nTrials = nTrials; } - + public void setFirstLayer(int firstLayer) { if (firstLayer != 0 && firstLayer != 2) { - logger.log(Level.WARNING,String.format("First layer of %d is not valid and is ignored.", firstLayer)); + logger.log(Level.WARNING, String.format("First layer of %d is not valid and is ignored.", firstLayer)); return; } logger.log(Level.CONFIG, String.format("Setting the first tracking layer to %d", firstLayer)); this.firstLayer = firstLayer; } - + public void setIterations(int N) { if (N < 1) { - logger.log(Level.WARNING,String.format("%d iterations not allowed.", N)); + logger.log(Level.WARNING, String.format("%d iterations not allowed.", N)); return; } - logger.log(Level.CONFIG,String.format("Setting the number of Kalman Filter iterations to %d.", N)); + logger.log(Level.CONFIG, String.format("Setting the number of Kalman Filter iterations to %d.", N)); nIterations = N; } - + public void setMxChi2Inc(double mxC) { if (mxC <= 1.) { - logger.log(Level.WARNING,String.format("Maximum chi^2 increment must be at least unity. %8.2f not valid.", mxC)); + logger.log(Level.WARNING, String.format("Maximum chi^2 increment must be at least unity. %8.2f not valid.", mxC)); return; } - logger.log(Level.CONFIG,String.format("Maximum chi^2 increment to add a hit to a track to %8.2f.", mxC)); + logger.log(Level.CONFIG, String.format("Maximum chi^2 increment to add a hit to a track to %8.2f.", mxC)); mxChi2Inc = mxC; } - + public void setMxChi2double(double mxDb) { if (mxDb <= 0.) { - logger.log(Level.WARNING,String.format("Maximum chi^2 increment of shared hit of %8.2f not allowed.", mxDb)); + logger.log(Level.WARNING, String.format("Maximum chi^2 increment of shared hit of %8.2f not allowed.", mxDb)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum chi^2 increment of shared hit to %8.2f sigma.", mxDb)); - mxChi2double = mxDb; + logger.log(Level.CONFIG, String.format("Setting the maximum chi^2 increment of shared hit to %8.2f sigma.", mxDb)); + mxChi2double = mxDb; } - + public void setMinChi2IncBad(double mnB) { if (mnB <= 3.0) { - logger.log(Level.WARNING,String.format("Minimum chi^2 increment to remove a bad hit must be at least 3. %8.2f not valid.", mnB)); + logger.log(Level.WARNING, String.format("Minimum chi^2 increment to remove a bad hit must be at least 3. %8.2f not valid.", mnB)); return; } - logger.log(Level.CONFIG,String.format("Setting the minimum chi^2 increment to remove a bad hit to %8.2f.", mnB)); - minChi2IncBad = mnB; + logger.log(Level.CONFIG, String.format("Setting the minimum chi^2 increment to remove a bad hit to %8.2f.", mnB)); + minChi2IncBad = mnB; } - + public void setMxResidShare(double mxSh) { if (mxSh <= 0.) { - logger.log(Level.WARNING,String.format("Maximum residual of shared hit of %8.2f not allowed.", mxSh)); + logger.log(Level.WARNING, String.format("Maximum residual of shared hit of %8.2f not allowed.", mxSh)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum residual for a shared hit to %8.2f sigma.", mxSh)); - mxResidShare = mxSh; + logger.log(Level.CONFIG, String.format("Setting the maximum residual for a shared hit to %8.2f sigma.", mxSh)); + mxResidShare = mxSh; } - + public void setMaxK(double kMx) { if (kMx <= 0.) { - logger.log(Level.WARNING,String.format("Max 1/pt of %8.2f not allowed.", kMx)); + logger.log(Level.WARNING, String.format("Max 1/pt of %8.2f not allowed.", kMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum 1/pt to %8.2f.", kMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum 1/pt to %8.2f.", kMx)); kMax[1] = kMx; - kMax[0] = Math.min(kMax[0], 0.5*kMx); + kMax[0] = Math.min(kMax[0], 0.5 * kMx); } - + void setMinK(double kMn) { if (kMn < 0.) { - logger.log(Level.WARNING,String.format("Min 1/pt of %8.2f not allowed.", kMn)); + logger.log(Level.WARNING, String.format("Min 1/pt of %8.2f not allowed.", kMn)); return; } kMin = kMn; } - + public void setMxResid(double mxR) { if (mxR <= 1.) { - logger.log(Level.WARNING,String.format("Max resid of %8.2f not allowed.", mxR)); + logger.log(Level.WARNING, String.format("Max resid of %8.2f not allowed.", mxR)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum residual to pick up hits to %8.2f sigma.", mxR)); + logger.log(Level.CONFIG, String.format("Setting the maximum residual to pick up hits to %8.2f sigma.", mxR)); mxResid[1] = mxR; - mxResid[0] = Math.min(mxResid[0], 0.5*mxR); + mxResid[0] = Math.min(mxResid[0], 0.5 * mxR); } - + public void setMaxTanL(double tlMx) { if (tlMx <= 0.) { - logger.log(Level.WARNING,String.format("Max seed tan(lambda) of %8.2f not allowed.", tlMx)); + logger.log(Level.WARNING, String.format("Max seed tan(lambda) of %8.2f not allowed.", tlMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum seed tan(lambda) to %8.2f.", tlMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum seed tan(lambda) to %8.2f.", tlMx)); tanlMax[1] = tlMx; - tanlMax[0] = Math.min(tanlMax[0], 0.8*tlMx); + tanlMax[0] = Math.min(tanlMax[0], 0.8 * tlMx); } - + public void setMaxdRho(double dMx) { if (dMx <= 0.0) { - logger.log(Level.WARNING,String.format("Max dRho of %8.2f not allowed.", dMx)); + logger.log(Level.WARNING, String.format("Max dRho of %8.2f not allowed.", dMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum dRho to %8.2f mm.", dMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum dRho to %8.2f mm.", dMx)); dRhoMax[1] = dMx; - dRhoMax[0] = Math.min(dRhoMax[0], 0.6*dMx); + dRhoMax[0] = Math.min(dRhoMax[0], 0.6 * dMx); } - + public void setMaxdZ(double zMx) { if (zMx <= 0.0) { - logger.log(Level.WARNING,String.format("Max dZ of %8.2f not allowed.", zMx)); + logger.log(Level.WARNING, String.format("Max dZ of %8.2f not allowed.", zMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum dz to %8.2f mm.", zMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum dz to %8.2f mm.", zMx)); dzMax[1] = zMx; - dzMax[0] = Math.min(dzMax[0], 0.4*zMx); + dzMax[0] = Math.min(dzMax[0], 0.4 * zMx); } - + public void setMaxChi2(double xMx) { if (xMx <= 0.) { - logger.log(Level.WARNING,String.format("Max chi2 of %8.2f not allowed.", xMx)); + logger.log(Level.WARNING, String.format("Max chi2 of %8.2f not allowed.", xMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum chi^2/hit to %8.2f.", xMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum chi^2/hit to %8.2f.", xMx)); chi2mx1[1] = xMx; - chi2mx1[0] = Math.min(chi2mx1[0], 0.5*xMx); + chi2mx1[0] = Math.min(chi2mx1[0], 0.5 * xMx); } - + public void setMaxChi2Vtx(double xMx) { if (xMx <= 0.) { - logger.log(Level.WARNING,String.format("Max chi2 of %8.2f not allowed.", xMx)); + logger.log(Level.WARNING, String.format("Max chi2 of %8.2f not allowed.", xMx)); return; } - logger.log(Level.CONFIG,String.format("Setting the maximum chi^2 for 5-hit tracks with vertex constraint to %8.2f.", xMx)); + logger.log(Level.CONFIG, String.format("Setting the maximum chi^2 for 5-hit tracks with vertex constraint to %8.2f.", xMx)); mxChi2Vtx = xMx; } - + public void setMinHits(int minH) { if (minH < 5) { - logger.log(Level.WARNING,String.format("Minimum number of hits = %d not allowed.", minH)); + logger.log(Level.WARNING, String.format("Minimum number of hits = %d not allowed.", minH)); return; } - logger.log(Level.CONFIG,String.format("Setting the minimum number of hits to %d.", minH)); + logger.log(Level.CONFIG, String.format("Setting the minimum number of hits to %d.", minH)); minHits1[1] = minH; - minHits1[0] = Math.max(minHits1[0], minH+1); + minHits1[0] = Math.max(minHits1[0], minH + 1); } - + public void setMinStereo(int minS) { if (minS < 3) { - logger.log(Level.WARNING,String.format("Minimum number of stereo hits = %d not allowed.", minS)); + logger.log(Level.WARNING, String.format("Minimum number of stereo hits = %d not allowed.", minS)); return; } - logger.log(Level.CONFIG,String.format("Setting the minimum number of stereo hits to %d.", minS)); + logger.log(Level.CONFIG, String.format("Setting the minimum number of stereo hits to %d.", minS)); minStereo[1] = minS; - minStereo[0] = Math.max(minStereo[0], minS+1); + minStereo[0] = Math.max(minStereo[0], minS + 1); } - + public void setMaxShared(int mxSh) { - logger.log(Level.CONFIG,String.format("Setting the maximum number of shared hits to %d.", mxSh)); + logger.log(Level.CONFIG, String.format("Setting the maximum number of shared hits to %d.", mxSh)); mxShared = mxSh; } - + public void setMaxTimeRange(double mxT) { - logger.log(Level.CONFIG,String.format("Setting the maximum time range for hits on a track to %8.2f ns.", mxT)); + logger.log(Level.CONFIG, String.format("Setting the maximum time range for hits on a track to %8.2f ns.", mxT)); mxTdif = mxT; } - public void setSeedCompThr(double seedComp_Thr) { + public void setSeedCompThr(double seedComp_Thr) { if (seedComp_Thr < 0.) { logger.log(Level.CONFIG, "SeedTracks duplicate removal is turned off."); return; } - logger.log(Level.CONFIG, String.format("Setting the SeedTracks duplicate removal threshold to %f percent.",seedComp_Thr*100.)); + logger.log(Level.CONFIG, String.format("Setting the SeedTracks duplicate removal threshold to %f percent.", seedComp_Thr * 100.)); seedCompThr = seedComp_Thr; } public void setBeamSpotY(double spot) { beamSpot[1] = spot; - logger.log(Level.CONFIG, String.format("Setting the Y beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); + logger.log(Level.CONFIG, String.format("Setting the Y beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); } - + public void setBeamSizeY(double size) { vtxSize[1] = size; - logger.log(Level.CONFIG, String.format("Setting the Y beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); + logger.log(Level.CONFIG, String.format("Setting the Y beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); } + public void setBeamSpotX(double spot) { beamSpot[0] = spot; - logger.log(Level.CONFIG, String.format("Setting the X beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); + logger.log(Level.CONFIG, String.format("Setting the X beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); } - + public void setBeamSizeX(double size) { vtxSize[0] = size; - logger.log(Level.CONFIG, String.format("Setting the X beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); + logger.log(Level.CONFIG, String.format("Setting the X beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); } + public void setBeamSpotZ(double spot) { beamSpot[2] = spot; - logger.log(Level.CONFIG, String.format("Setting the Z beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); + logger.log(Level.CONFIG, String.format("Setting the Z beam spot location to %f %f %f", beamSpot[0], beamSpot[1], beamSpot[2])); } - + public void setBeamSizeZ(double size) { vtxSize[2] = size; - logger.log(Level.CONFIG, String.format("Setting the Z beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); + logger.log(Level.CONFIG, String.format("Setting the Z beam spot size to %f %f %f", vtxSize[0], vtxSize[1], vtxSize[2])); } - + public void clrStrategies() { - logger.log(Level.CONFIG,String.format("Clearing all lists of search strategies..")); + logger.log(Level.CONFIG, String.format("Clearing all lists of search strategies..")); lyrList[0].clear(); lyrList[1].clear(); } - + public void setNumSeedIter1(int num) { int n = num; - for (int topBottom=0; topBottom<2; ++topBottom) { + for (int topBottom = 0; topBottom < 2; ++topBottom) { if (n > lyrList[topBottom].size()) { n = lyrList[topBottom].size(); } } logger.config(String.format("The number of seeds used in iteration 1 is set to %d", n)); - maxListIter1 = n-1; + maxListIter1 = n - 1; } - + private boolean addStrategy(String strategy) { - return addStrategy(strategy,"top") && addStrategy(strategy,"bottom"); + return addStrategy(strategy, "top") && addStrategy(strategy, "bottom"); } - + /** - * Add a seed search strategy for the bottom or top tracker - * @param strategy string specifying the strategy - * @param topBottom string specifying "top" or "bottom" - * @return true if successful + * Add a seed search strategy for the bottom or top tracker + * + * @param strategy string specifying the strategy + * @param topBottom string specifying "top" or "bottom" + * @return true if successful */ public boolean addStrategy(String strategy, String topBottom) { if (!(topBottom == "top" || topBottom == "bottom")) { @@ -495,42 +502,44 @@ public boolean addStrategy(String strategy, String topBottom) { return false; } int iTB; - if (topBottom=="top") { + if (topBottom == "top") { iTB = 1; } else { iTB = 0; } int nAxial = 0; int nStereo = 0; - int n=0; - int [] newList = new int[5]; + int n = 0; + int[] newList = new int[5]; String goodChars = "0AaSsBb"; - for (int lyr=0; lyr<7; ++lyr) { + for (int lyr = 0; lyr < 7; ++lyr) { if (goodChars.indexOf(strategy.charAt(lyr)) < 0) { - logger.warning(String.format("Character %c for layer %d in strategy %s is not recognized. Should be 0, A, S, B, a, s, or b", + logger.warning(String.format("Character %c for layer %d in strategy %s is not recognized. Should be 0, A, S, B, a, s, or b", strategy.charAt(lyr), lyr, strategy)); continue; } - if (strategy.charAt(lyr) == '0') continue; + if (strategy.charAt(lyr) == '0') { + continue; + } int nA = n; int nS = n; - if (strategy.charAt(lyr)=='B' || strategy.charAt(lyr)=='b') { - if (topBottom=="top") { - nS=n+1; + if (strategy.charAt(lyr) == 'B' || strategy.charAt(lyr) == 'b') { + if (topBottom == "top") { + nS = n + 1; } else { - nA=n+1; + nA = n + 1; } n += 2; - } else if (strategy.charAt(lyr)=='A' || strategy.charAt(lyr)=='a' || strategy.charAt(lyr)=='S' || strategy.charAt(lyr)=='s') { + } else if (strategy.charAt(lyr) == 'A' || strategy.charAt(lyr) == 'a' || strategy.charAt(lyr) == 'S' || strategy.charAt(lyr) == 's') { n++; } //System.out.format("addStrategy %s: lyr=%d, nA=%d, nS=%d, strategy=%s\n",topBottom,lyr,nA,nS,strategy); - if (strategy.charAt(lyr)=='A' || strategy.charAt(lyr)=='B' || strategy.charAt(lyr)=='a' || strategy.charAt(lyr)=='b') { + if (strategy.charAt(lyr) == 'A' || strategy.charAt(lyr) == 'B' || strategy.charAt(lyr) == 'a' || strategy.charAt(lyr) == 'b') { if (topBottom == "top") { if (nA > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nA] = 2*lyr; // The top tracker begins with an axial layer + newList[nA] = 2 * lyr; // The top tracker begins with an axial layer //System.out.format("addStrategy %s: adding axial element %d, lyr=%d\n", topBottom, nA, 2*lyr); nAxial++; } @@ -538,18 +547,18 @@ public boolean addStrategy(String strategy, String topBottom) { if (nA > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nA] = 2*lyr + 1; // The bottom tracker begins with a stereo layer + newList[nA] = 2 * lyr + 1; // The bottom tracker begins with a stereo layer //System.out.format("addStrategy %s: adding axial element %d, lyr=%d\n", topBottom, nA, 2*lyr+1); nAxial++; } } } - if (strategy.charAt(lyr)=='S' || strategy.charAt(lyr)=='B' || strategy.charAt(lyr)=='s' || strategy.charAt(lyr)=='b') { + if (strategy.charAt(lyr) == 'S' || strategy.charAt(lyr) == 'B' || strategy.charAt(lyr) == 's' || strategy.charAt(lyr) == 'b') { if (topBottom == "top") { if (nS > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nS] = 2*lyr + 1; // The top tracker begins with an axial layer + newList[nS] = 2 * lyr + 1; // The top tracker begins with an axial layer //System.out.format("addStrategy %s: adding stereo element %d, lyr=%d\n", topBottom, nS, 2*lyr+1); nStereo++; } @@ -557,7 +566,7 @@ public boolean addStrategy(String strategy, String topBottom) { if (nS > 4) { logger.warning("Strategy " + strategy + " has more than 5 layers! The extra ones are ignored"); } else { - newList[nS] = 2*lyr; // The bottom tracker begins with a stereo layer + newList[nS] = 2 * lyr; // The bottom tracker begins with a stereo layer //System.out.format("addStrategy %s: adding stereo element %d, lyr=%d\n", topBottom, nS, 2*lyr); nStereo++; } @@ -565,22 +574,24 @@ public boolean addStrategy(String strategy, String topBottom) { } } if (nAxial != 2 || nStereo != 3) { - logger.log(Level.WARNING,String.format("addStrategy: Invalid search strategy " + strategy + " for topBottom=%s: %d %d %d %d %d", - topBottom, newList[0],newList[1],newList[2],newList[3],newList[4])); + logger.log(Level.WARNING, String.format("addStrategy: Invalid search strategy " + strategy + " for topBottom=%s: %d %d %d %d %d", + topBottom, newList[0], newList[1], newList[2], newList[3], newList[4])); return false; } - for (int [] oldList : lyrList[iTB]) { + for (int[] oldList : lyrList[iTB]) { int nMatch = 0; - for (int i=0; i<5; ++i) { - if (oldList[i] == newList[i]) nMatch++; + for (int i = 0; i < 5; ++i) { + if (oldList[i] == newList[i]) { + nMatch++; + } } if (nMatch == 5) { - logger.log(Level.WARNING,String.format("addStrategy: strategy %s is already in the list", strategy)); + logger.log(Level.WARNING, String.format("addStrategy: strategy %s is already in the list", strategy)); return false; } } - logger.log(Level.CONFIG,String.format("addStrategy: adding search strategy %d %d %d %d %d for %s", - newList[0],newList[1],newList[2],newList[3],newList[4],topBottom)); + logger.log(Level.CONFIG, String.format("addStrategy: adding search strategy %d %d %d %d %d for %s", + newList[0], newList[1], newList[2], newList[3], newList[4], topBottom)); lyrList[iTB].add(newList); return true; } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java index bc8cc6a37f..6a6e84225d 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java @@ -34,7 +34,8 @@ import org.lcsim.geometry.IDDecoder; /** - * Driver for pattern recognition and fitting of HPS tracks using the Kalman Filter + * Driver for pattern recognition and fitting of HPS tracks using the Kalman + * Filter */ public class KalmanPatRecDriver extends Driver { @@ -58,7 +59,7 @@ public class KalmanPatRecDriver extends Driver { private KalmanParams kPar; private KalmanPatRecPlots kPlot; private static Logger logger; - + // Parameters for the Kalman pattern recognition that can be set by the user in the steering file: private ArrayList strategies; // List of seed strategies for both top and bottom trackers, from steering private ArrayList strategiesTop; // List of all the top tracker seed strategies from the steering file @@ -83,7 +84,7 @@ public class KalmanPatRecDriver extends Driver { private int numEvtPlots; // Number of event displays to plot (gnuplot files) private boolean doDebugPlots; // Whether to make all the debugging histograms private int siHitsLimit; // Maximum number of SiClusters in one event allowed for KF pattern reco - // (protection against monster events) + // (protection against monster events) private double seedCompThr; // Threshold for seedTrack helix parameters compatibility private int numStrategyIter1; // Number of seed strategies to use in the first iteration of pattern recognition private double beamPositionZ; // Beam spot location along the beam axis @@ -93,19 +94,20 @@ public class KalmanPatRecDriver extends Driver { private double beamPositionY; private double beamSigmaY; private double lowPhThresh; // Threshold in residual ratio to prefer a low-ph hit over a high-ph hit - private double minSeedEnergy=-1.; // Minimum energy of a hit for it to be used in a pattern recognition seed + private double minSeedEnergy = -1.; // Minimum energy of a hit for it to be used in a pattern recognition seed private boolean useBeamPositionConditions; // True to use beam position from database private boolean useFixedVertexZPosition; // True to override the database just for the z beam position private Level logLevel = Level.WARNING; // Set log level from steering private List sensors = null; // List of tracker sensors - + public String getOutputFullTrackCollectionName() { return outputFullTrackCollectionName; } /** - * Used to change the track collection name from the default "KalmanFullTracks" - * + * Used to change the track collection name from the default + * "KalmanFullTracks" + * * @param input Desired new name for the track collection */ public void setOutputFullTrackCollectionName(String input) { @@ -114,62 +116,71 @@ public void setOutputFullTrackCollectionName(String input) { /** * Optional call to force a change to the amount of debug printing - * - * @param input true to turn on lots of debug printing + * + * @param input true to turn on lots of debug printing */ public void setVerbose(boolean verbose) { this.verbose = verbose; - if (verbose) System.out.println("KalmanPatRecDriver: verbose print output is turned on!"); + if (verbose) { + System.out.println("KalmanPatRecDriver: verbose print output is turned on!"); + } } /** - * Option to treat the B field as uniform. This normally is used only for development and debugging work. - * + * Option to treat the B field as uniform. This normally is used only for + * development and debugging work. + * * @param input Set true to assume a uniform B field. */ public void setUniformB(boolean uniformB) { this.uniformB = uniformB; - if (uniformB) System.out.println("KalmanPatRecDriver: a uniform field will be used in track fitting: %b"); + if (uniformB) { + System.out.println("KalmanPatRecDriver: a uniform field will be used in track fitting: %b"); + } } public void setELoss(boolean eLoss) { - this.eLoss = eLoss; - if (eLoss) System.out.println("KalmanPatRecDriver: energy loss will be included in fits."); + this.eLoss = eLoss; + if (eLoss) { + System.out.println("KalmanPatRecDriver: energy loss will be included in fits."); + } } - + public void setMaterialManager(MaterialSupervisor mm) { _materialManager = mm; } /** * Determine whether residuals will be added to the event output - * - * @param input set true to force residuals to be output with the event + * + * @param input set true to force residuals to be output with the event */ public void setAddResiduals(boolean addResiduals) { this.addResiduals = addResiduals; System.out.format("KalmanPatRecDriver: residuals will be added to the event output: %b\n", addResiduals); } - + /** - * Do-nothing method. See instead setOutputFullTrackCollectionName. - * - * @param input ignored---the call will do nothing. + * Do-nothing method. See instead setOutputFullTrackCollectionName. + * + * @param input ignored---the call will do nothing. */ public void setTrackCollectionName(String input) { } - + /** - * Set the maximum number of SiClusters in one event allowed for KF pattern recognition (protection against monster events) - * + * Set the maximum number of SiClusters in one event allowed for KF pattern + * recognition (protection against monster events) + * * @param input Maximum number of hits to use */ public void setSiHitsLimit(int input) { siHitsLimit = input; - } + } /** - * Set up the geometry and parameters for the Kalman-filter track finding and fitting. + * Set up the geometry and parameters for the Kalman-filter track finding + * and fitting. */ @Override public void detectorChanged(Detector det) { @@ -178,84 +189,134 @@ public void detectorChanged(Detector det) { logger.setLevel(logLevel); //LogManager.getLogManager().getLogger(Logger.GLOBAL_LOGGER_NAME).setLevel(logLevel); } - verbose = (logger.getLevel()==Level.FINE); + verbose = (logger.getLevel() == Level.FINE); System.out.format("KalmanPatRecDriver: entering detectorChanged, logger level = %s\n", logger.getLevel().getName()); executionTime = 0.; interfaceTime = 0.; plottingTime = 0.; maxTime = 0.; - + _materialManager = new MaterialSupervisor(); _materialManager.buildModel(det); fm = det.getFieldMap(); - + System.out.format("B field map vs y in Kalman coordinates:\n"); double eCalLoc = 1394.; - for (double y=0.; y<1500.; y+=5.) { - double z1=-50.; - double z2= 50.; + for (double y = 0.; y < 1500.; y += 5.) { + double z1 = -50.; + double z2 = 50.; Vec B1 = KalmanInterface.getField(new Vec(0., y, z1), fm); Vec B2 = KalmanInterface.getField(new Vec(0., y, 0.), fm); Vec B3 = KalmanInterface.getField(new Vec(0., y, z2), fm); System.out.format("y=%6.1f z=%6.1f: %s z=0: %s z=%6.1f: %s\n", y, z1, B1.toString(), B2.toString(), z2, B3.toString()); } System.out.format("B field map vs z at ECAL, in Kalman coordinates:\n"); - for (double z=-200.; z<200.; z+=5.) { - double y=eCalLoc; + for (double z = -200.; z < 200.; z += 5.) { + double y = eCalLoc; Vec B = KalmanInterface.getField(new Vec(0., y, z), fm); System.out.format("x=0 y=%6.1f z=%6.1f: %s\n", y, z, B.toString()); } System.out.format("B field map vs x at ECAL, in Kalman coordinates:\n"); - for (double x=-200.; x<200.; x+=5.) { - double y=eCalLoc; - double z=20.; + for (double x = -200.; x < 200.; x += 5.) { + double y = eCalLoc; + double z = 20.; Vec B = KalmanInterface.getField(new Vec(x, y, z), fm); System.out.format("x=%6.1f y=%6.1f z=%6.1f: %s\n", x, y, z, B.toString()); - } - + } + detPlanes = new ArrayList(); List materialVols = ((MaterialSupervisor) (_materialManager)).getMaterialVolumes(); for (ScatteringDetectorVolume vol : materialVols) { detPlanes.add((SiStripPlane) (vol)); } - + sensors = det.getSubdetector("Tracker").getDetectorElement().findDescendants(HpsSiSensor.class); // Instantiate the interface to the Kalman-Filter code and set up the geometry KalmanParams kPar = new KalmanParams(); - + // Change Kalman parameters per settings supplied by the steering file // We assume that if not set by the steering file, then the parameters will have the Java default values for the primitives // Note that all of the parameters have defaults hard coded in KalmanParams.java - if (numPatRecIteration != 0) kPar.setGlbIterations(numPatRecIteration); - if (numKalmanIteration != 0) kPar.setIterations(numKalmanIteration); - if (maxPtInverse != 0.0) kPar.setMaxK(maxPtInverse); - if (maxD0 != 0.0) kPar.setMaxdRho(maxD0); - if (maxZ0 != 0.0) kPar.setMaxdZ(maxZ0); - if (maxChi2 != 0.0) kPar.setMaxChi2(maxChi2); - if (minHits != 0) kPar.setMinHits(minHits); - if (minStereo != 0) kPar.setMinStereo(minStereo); - if (maxSharedHits != 0) kPar.setMaxShared(maxSharedHits); - if (maxTimeRange != 0.0) kPar.setMaxTimeRange(maxTimeRange); - if (maxTanLambda != 0.0) kPar.setMaxTanL(maxTanLambda); - if (maxResidual != 0.0) kPar.setMxResid(maxResidual); - if (maxChi2Inc != 0.0) kPar.setMxChi2Inc(maxChi2Inc); - if (minChi2IncBad != 0.0) kPar.setMinChi2IncBad(minChi2IncBad); - if (maxResidShare != 0.0) kPar.setMxResidShare(maxResidShare); - if (maxChi2IncShare != 0.0) kPar.setMxChi2double(maxChi2IncShare); - if (seedCompThr != 0.0) kPar.setSeedCompThr(seedCompThr); - if (beamPositionZ != 0.0) kPar.setBeamSpotY(beamPositionZ); - if (beamSigmaZ != 0.0) kPar.setBeamSizeY(beamSigmaZ); - if (beamPositionX != 0.0) kPar.setBeamSpotX(beamPositionX); - if (beamSigmaX != 0.0) kPar.setBeamSizeX(beamSigmaX); - if (beamPositionY != 0.0) kPar.setBeamSpotZ(-beamPositionY); - if (beamSigmaY != 0.0) kPar.setBeamSizeZ(beamSigmaY); - if (mxChi2Vtx != 0.0) kPar.setMaxChi2Vtx(mxChi2Vtx); - if (minSeedEnergy >= 0.) kPar.setMinSeedEnergy(minSeedEnergy); + if (numPatRecIteration != 0) { + kPar.setGlbIterations(numPatRecIteration); + } + if (numKalmanIteration != 0) { + kPar.setIterations(numKalmanIteration); + } + if (maxPtInverse != 0.0) { + kPar.setMaxK(maxPtInverse); + } + if (maxD0 != 0.0) { + kPar.setMaxdRho(maxD0); + } + if (maxZ0 != 0.0) { + kPar.setMaxdZ(maxZ0); + } + if (maxChi2 != 0.0) { + kPar.setMaxChi2(maxChi2); + } + if (minHits != 0) { + kPar.setMinHits(minHits); + } + if (minStereo != 0) { + kPar.setMinStereo(minStereo); + } + if (maxSharedHits != 0) { + kPar.setMaxShared(maxSharedHits); + } + if (maxTimeRange != 0.0) { + kPar.setMaxTimeRange(maxTimeRange); + } + if (maxTanLambda != 0.0) { + kPar.setMaxTanL(maxTanLambda); + } + if (maxResidual != 0.0) { + kPar.setMxResid(maxResidual); + } + if (maxChi2Inc != 0.0) { + kPar.setMxChi2Inc(maxChi2Inc); + } + if (minChi2IncBad != 0.0) { + kPar.setMinChi2IncBad(minChi2IncBad); + } + if (maxResidShare != 0.0) { + kPar.setMxResidShare(maxResidShare); + } + if (maxChi2IncShare != 0.0) { + kPar.setMxChi2double(maxChi2IncShare); + } + if (seedCompThr != 0.0) { + kPar.setSeedCompThr(seedCompThr); + } + if (beamPositionZ != 0.0) { + kPar.setBeamSpotY(beamPositionZ); + } + if (beamSigmaZ != 0.0) { + kPar.setBeamSizeY(beamSigmaZ); + } + if (beamPositionX != 0.0) { + kPar.setBeamSpotX(beamPositionX); + } + if (beamSigmaX != 0.0) { + kPar.setBeamSizeX(beamSigmaX); + } + if (beamPositionY != 0.0) { + kPar.setBeamSpotZ(-beamPositionY); + } + if (beamSigmaY != 0.0) { + kPar.setBeamSizeZ(beamSigmaY); + } + if (mxChi2Vtx != 0.0) { + kPar.setMaxChi2Vtx(mxChi2Vtx); + } + if (minSeedEnergy >= 0.) { + kPar.setMinSeedEnergy(minSeedEnergy); + } kPar.setUniformB(uniformB); kPar.setEloss(eLoss); - + // Here we set the seed strategies for the pattern recognition if (strategies != null || (strategiesTop != null && strategiesBot != null)) { logger.config("The Kalman pattern recognition seed strategies are being set from the steering file"); @@ -285,29 +346,34 @@ public void detectorChanged(Detector det) { kPar.setNumSeedIter1(nA + nT); kPar.setNumSeedIter1(nA + nB); } - if (numStrategyIter1 != 0) kPar.setNumSeedIter1(numStrategyIter1); - + if (numStrategyIter1 != 0) { + kPar.setNumSeedIter1(numStrategyIter1); + } + // Setup optional usage of beam positions from database. final DatabaseConditionsManager mgr = DatabaseConditionsManager.getInstance(); if (useBeamPositionConditions && mgr.hasConditionsRecord("beam_positions")) { logger.config("Using Kalman beam position from the conditions database"); - BeamPositionCollection beamPositions = - mgr.getCachedConditions(BeamPositionCollection.class, "beam_positions").getCachedData(); - BeamPosition beamPositionCond = beamPositions.get(0); - if (!useFixedVertexZPosition) kPar.setBeamSpotY(beamPositionCond.getPositionZ()); - else logger.config("Using fixed Kalman beam Z position: " + kPar.beamSpot[1]); + BeamPositionCollection beamPositions + = mgr.getCachedConditions(BeamPositionCollection.class, "beam_positions").getCachedData(); + BeamPosition beamPositionCond = beamPositions.get(0); + if (!useFixedVertexZPosition) { + kPar.setBeamSpotY(beamPositionCond.getPositionZ()); + } else { + logger.config("Using fixed Kalman beam Z position: " + kPar.beamSpot[1]); + } kPar.setBeamSpotX(beamPositionCond.getPositionX()); // Includes a transformation to Kalman coordinates kPar.setBeamSpotZ(-beamPositionCond.getPositionY()); } else { logger.config("Using Kalman beam position from the steering file or default"); } logger.config("Using Kalman beam position [ Z, X, Y ]: " + String.format("[ %f, %f, %f ]", - kPar.beamSpot[0], -kPar.beamSpot[2], kPar.beamSpot[1]) + " in HPS coordinates."); - + kPar.beamSpot[0], -kPar.beamSpot[2], kPar.beamSpot[1]) + " in HPS coordinates."); + logger.config(String.format("KalmanPatRecDriver: the B field is assumed uniform? %b\n", uniformB)); logger.config("KalmanPatRecDriver: done with configuration changes."); kPar.print(); - + KI = new KalmanInterface(kPar, fm); KI.setSiHitsLimit(siHitsLimit); KI.createSiModules(detPlanes); @@ -318,54 +384,55 @@ public void detectorChanged(Detector det) { } /** - * Top level method called for each event, to do the Kalman-filter tracking finding and fitting - * - * @param event input the header for this event + * Top level method called for each event, to do the Kalman-filter tracking + * finding and fitting + * + * @param event input the header for this event */ @Override public void process(EventHeader event) { - + List outputFullTracks = new ArrayList(); - + //For additional track information List trackDataCollection = new ArrayList(); List trackDataRelations = new ArrayList(); - + //For GBL Refitting List allClstrs = new ArrayList(); - List gblStripClusterDataRelations = new ArrayList(); - + List gblStripClusterDataRelations = new ArrayList(); + //For hit-on-track residuals information List trackResiduals = new ArrayList(); List trackResidualsRelations = new ArrayList(); - + ArrayList[] kPatList = prepareTrackCollections(event, outputFullTracks, trackDataCollection, trackDataRelations, allClstrs, gblStripClusterDataRelations, trackResiduals, trackResidualsRelations); - + int flag = 1 << LCIOConstants.TRBIT_HITS; event.put(outputFullTrackCollectionName, outputFullTracks, Track.class, flag); event.put("KFGBLStripClusterData", allClstrs, GBLStripClusterData.class, flag); event.put("KFGBLStripClusterDataRelations", gblStripClusterDataRelations, LCRelation.class, flag); - event.put("KFTrackData",trackDataCollection, TrackData.class,0); - event.put("KFTrackDataRelations",trackDataRelations,LCRelation.class,0); - + event.put("KFTrackData", trackDataCollection, TrackData.class, 0); + event.put("KFTrackDataRelations", trackDataRelations, LCRelation.class, 0); + if (addResiduals) { - event.put("KFUnbiasRes", trackResiduals, TrackResidualsData.class,0); - event.put("KFUnbiasResRelations",trackResidualsRelations, LCRelation.class,0); + event.put("KFUnbiasRes", trackResiduals, TrackResidualsData.class, 0); + event.put("KFUnbiasResRelations", trackResidualsRelations, LCRelation.class, 0); } if (kPlot != null) { long startTime = System.nanoTime(); - + kPlot.process(event, sensors, kPatList, outputFullTracks); long endPlottingTime = System.nanoTime(); - double runTime = (double)(endPlottingTime - startTime)/1000000.; - plottingTime += runTime; + double runTime = (double) (endPlottingTime - startTime) / 1000000.; + plottingTime += runTime; } - + KI.clearInterface(); logger.log(Level.FINE, String.format("\n KalmanPatRecDriver.process: Done with event %d", event.getEventNumber())); } - + class SortByZ implements Comparator> { @Override @@ -373,42 +440,45 @@ public int compare(Pair o1, Pair o2) { return (int) (o1.getSecondElement()[2] - o2.getSecondElement()[2]); } } - + class SortByZ2 implements Comparator { - + @Override - public int compare(TrackerHit o1, TrackerHit o2) { + public int compare(TrackerHit o1, TrackerHit o2) { return (int) (o1.getPosition()[2] - o2.getPosition()[2]); } } /** - * Execute the Kalman pattern recognition. All but the first argument are outputs, but the calling routine has to - * supply the empty data structures. - * - * @param event input the header for this event - * @param outputFullTracks output the list of HPS tracks - * @param trackDataCollection output list of data that go with the tracks - * @param trackDataRelations output the relations between tracks and the data - * @param allClstrs output all the clusters needed for refitting the Kalman tracks by GBL - * @param gblStripClusterDataRelations output relations for the clusters - * @param trackResiduals output the residuals for hits on Kalman tracks - * @param trackResidualsRelations output relations for the residuals - * @return Two lists of native Kalman tracks, one for each of top and bottom detectors + * Execute the Kalman pattern recognition. All but the first argument are + * outputs, but the calling routine has to supply the empty data structures. + * + * @param event input the header for this event + * @param outputFullTracks output the list of HPS tracks + * @param trackDataCollection output list of data that go with the tracks + * @param trackDataRelations output the relations between tracks and the + * data + * @param allClstrs output all the clusters needed for refitting the Kalman + * tracks by GBL + * @param gblStripClusterDataRelations output relations for the clusters + * @param trackResiduals output the residuals for hits on Kalman tracks + * @param trackResidualsRelations output relations for the residuals + * @return Two lists of native Kalman tracks, one for each of top and bottom + * detectors */ private ArrayList[] prepareTrackCollections(EventHeader event, List outputFullTracks, List trackDataCollection, List trackDataRelations, List allClstrs, List gblStripClusterDataRelations, List trackResiduals, List trackResidualsRelations) { - + int evtNumb = event.getEventNumber(); String stripHitInputCollectionName = "StripClusterer_SiTrackerHitStrip1D"; if (!event.hasCollection(TrackerHit.class, stripHitInputCollectionName)) { System.out.format("KalmanPatRecDriver.process:" + stripHitInputCollectionName + " does not exist; skipping event %d\n", evtNumb); return null; } - + long startTime = System.nanoTime(); ArrayList[] kPatList = KI.KalmanPatRec(event, decoder); long endTime = System.nanoTime(); - double runTime = (double)(endTime - startTime)/1000000.; + double runTime = (double) (endTime - startTime) / 1000000.; executionTime += runTime; if (verbose) { if (runTime > 200.) { @@ -417,45 +487,49 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List maxTime) maxTime = runTime; + if (runTime > maxTime) { + maxTime = runTime; + } nEvents++; - logger.log(Level.FINE,"KalmanPatRecDriver.process: run time for pattern recognition at event "+evtNumb+" is "+runTime+" milliseconds"); - + logger.log(Level.FINE, "KalmanPatRecDriver.process: run time for pattern recognition at event " + evtNumb + " is " + runTime + " milliseconds"); + //List rawhits = event.get(RawTrackerHit.class, "SVTRawTrackerHits"); //if (rawhits == null) { // logger.log(Level.FINE, String.format("KalmanPatRecDriver.process: the raw hits collection is missing")); // return null; //} - int nKalTracks = 0; - for (int topBottom=0; topBottom<2; ++topBottom) { + for (int topBottom = 0; topBottom < 2; ++topBottom) { ArrayList kPat = kPatList[topBottom]; if (kPat.size() == 0) { logger.log(Level.FINE, String.format("KalmanPatRecDriver.process: pattern recognition failed to find tracks in tracker %d for event %d.", topBottom, evtNumb)); } for (KalTrack kTk : kPat) { - if (verbose) kTk.print(String.format(" PatRec for topBot=%d ",topBottom)); - double [][] covar = kTk.originCovariance(); - for (int ix=0; ix<5; ++ix) { - for (int iy=0; iy<5; ++iy) { + if (verbose) { + kTk.print(String.format(" PatRec for topBot=%d ", topBottom)); + } + double[][] covar = kTk.originCovariance(); + for (int ix = 0; ix < 5; ++ix) { + for (int iy = 0; iy < 5; ++iy) { if (Double.isNaN(covar[ix][iy])) { - logger.log(Level.FINE, String.format("KalmanPatRecDriver.process event %d: NaN at %d %d in covariance for track %d",evtNumb,ix,iy,kTk.ID)); + logger.log(Level.FINE, String.format("KalmanPatRecDriver.process event %d: NaN at %d %d in covariance for track %d", evtNumb, ix, iy, kTk.ID)); } } - } + } nKalTracks++; - + //Here is where the tracks to be persisted are formed Track KalmanTrackHPS = KI.createTrack(kTk, true); - if (KalmanTrackHPS == null) continue; - + if (KalmanTrackHPS == null) { + continue; + } + //pT cut //double [] hParams_check = kTk.originHelixParms(); //double ptInv_check = hParams_check[2]; //double pt = Math.abs(1./ptInv_check); - outputFullTracks.add(KalmanTrackHPS); - + // Create clusters that can be used to refit the Kalman track using GBL List clstrs = KI.createGBLStripClusterData(kTk); if (verbose) { @@ -463,68 +537,70 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List layers = new ArrayList(); - List residuals = new ArrayList(); - List sigmas = new ArrayList(); - - for (int ilay = 0; ilay<14; ilay++) { - Pair res_and_sigma = kTk.unbiasedResidual(ilay, false); - if (res_and_sigma.getSecondElement() > -1.) { + List layers = new ArrayList(); + List residuals = new ArrayList(); + List sigmas = new ArrayList(); + + for (int ilay = 0; ilay < 14; ilay++) { + Pair res_and_sigma = kTk.unbiasedResidual(ilay, false); + if (res_and_sigma.getSecondElement() > -1.) { layers.add(ilay); residuals.add(res_and_sigma.getFirstElement()); sigmas.add(res_and_sigma.getSecondElement().floatValue()); } } // End loop on layers - - TrackResidualsData resData = new TrackResidualsData(trackerVolume,layers,residuals,sigmas); + + TrackResidualsData resData = new TrackResidualsData(trackerVolume, layers, residuals, sigmas); trackResiduals.add(resData); - trackResidualsRelations.add(new BaseLCRelation(resData,KalmanTrackHPS)); + trackResidualsRelations.add(new BaseLCRelation(resData, KalmanTrackHPS)); /* if (KalmanTrackHPS.getTrackerHits().size() != residuals.size()) { System.out.println("KalmanPatRecDriver::Residuals consistency check failed."); System.out.printf("Track has %d hits while I have %d residuals \n", KalmanTrackHPS.getTrackerHits().size(), residuals.size()); } - */ - + */ + } // end of loop on tracks } // end of loop on trackers - + nTracks += nKalTracks; - + long endInterfaceTime = System.nanoTime(); - runTime = (double)(endInterfaceTime - endTime)/1000000.; + runTime = (double) (endInterfaceTime - endTime) / 1000000.; interfaceTime += runTime; - + return kPatList; } @@ -533,118 +609,149 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List(); @@ -652,6 +759,7 @@ public void setSeedStrategy(String seedStrategy) { strategies.add(seedStrategy); System.out.format("KalmanPatRecDriver: top and bottom strategy %s specified by steering.\n", seedStrategy); } + public void setSeedStrategyTop(String seedStrategy) { if (strategiesTop == null) { strategiesTop = new ArrayList(); @@ -659,6 +767,7 @@ public void setSeedStrategyTop(String seedStrategy) { strategiesTop.add(seedStrategy); System.out.format("KalmanPatRecDriver: top strategy %s specified by steering.\n", seedStrategy); } + public void setSeedStrategyBottom(String seedStrategy) { if (strategiesBot == null) { strategiesBot = new ArrayList(); @@ -666,9 +775,10 @@ public void setSeedStrategyBottom(String seedStrategy) { strategiesBot.add(seedStrategy); System.out.format("KalmanPatRecDriver: bottom strategy %s specified by steering.\n", seedStrategy); } + public void setLogLevel(String logLevel) { System.out.format("KalmanPatRecDriver: setting the logger level to %s\n", logLevel); this.logLevel = Level.parse(logLevel); - System.out.format(" logger level = %s\n",this.logLevel.getName()); + System.out.format(" logger level = %s\n", this.logLevel.getName()); } } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java index 7e1d1b2ac9..a4890221b8 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/RKhelix.java @@ -5,22 +5,23 @@ import org.apache.commons.math.util.FastMath; /** - * Runge-Kutta propagation through the detector, including Gaussian MCS at silicon planes. - * This code is only to help with internal testing of the Kalman package and is not part of the fitting or pattern recognition. + * Runge-Kutta propagation through the detector, including Gaussian MCS at + * silicon planes. This code is only to help with internal testing of the Kalman + * package and is not part of the fitting or pattern recognition. */ class RKhelix { Vec x; // point on the track Vec p; // momentum of the track at point x double Q; // charge of the particle - + private org.lcsim.geometry.FieldMap fM; private Random rndm; private HelixPlaneIntersect hpi; private double rho; private double radLen; private boolean doeLoss; - + RKhelix(Vec x, Vec p, double Q, org.lcsim.geometry.FieldMap fM, Random rndm, boolean doeLoss) { this.rndm = rndm; this.fM = fM; @@ -32,13 +33,13 @@ class RKhelix { rho = 2.329; // Density of silicon in g/cm^2 radLen = (21.82 / rho) * 10.0; // Radiation length of silicon in millimeters } - + RKhelix propagateRK(Plane pln) { Vec newP = new Vec(3); Vec newX = planeIntersect(pln, newP); return new RKhelix(newX, newP, Q, fM, rndm, doeLoss); } - + Vec planeIntersect(Plane pln, Vec pInt) { // phi value where the helix intersects the plane P (given in global coordinates) return hpi.rkIntersect(pln, x, p, Q, fM, pInt); } @@ -46,7 +47,7 @@ Vec planeIntersect(Plane pln, Vec pInt) { // phi value where the helix intersect void print(String s) { System.out.format("RKhelix %s: x=%s, p=%s, Q=%3.1f\n", s, x.toString(), p.toString(), Q); } - + // Get parameters for the helix passing through the point x. // pivotF is the pivot point in the helix field reference system. // Input "pivot", the desired pivot point in global coordinates. This will be the origin of the field reference system. @@ -60,16 +61,16 @@ Vec helixParameters(Vec pivot, Vec pivotF) { Vec helixAtX = HelixState.pTOa(pF, 0., 0., Q); // Helix with pivot at x in field frame // Transform the desired pivot point into the field frame Vec pivotTrans = Rot.rotate(pivot.dif(x)); - for (int i=0; i<3; ++i) { + for (int i = 0; i < 3; ++i) { pivotF.v[i] = pivotTrans.v[i]; } return HelixState.pivotTransform(pivotF, helixAtX, new Vec(0., 0., 0.), alpha, 0.); } - - RKhelix copy() { - return new RKhelix(x.copy(),p.copy(),Q,fM,rndm, doeLoss); + + RKhelix copy() { + return new RKhelix(x.copy(), p.copy(), Q, fM, rndm, doeLoss); } - + RotMatrix R(Vec position) { Vec B = KalmanInterface.getField(position, fM); double Bmag = B.mag(); @@ -79,7 +80,7 @@ RotMatrix R(Vec position) { Vec v = t.cross(u); return new RotMatrix(u, v, t); } - + RKhelix randomScat(Plane P, double X) { // Produce a new helix scattered randomly in a given plane P Vec t = p.unitVec(); @@ -89,9 +90,12 @@ RKhelix randomScat(Plane P, double X) { // Produce a new helix scattered randoml RotMatrix Rp = new RotMatrix(uhat, vhat, t); double ct = Math.abs(P.T().dot(t)); double theta0; - - if (X == 0.) theta0 = 0.; // Get the scattering angle - else theta0 = FastMath.sqrt((X / radLen) / ct) * (0.0136 / p.mag()) * (1.0 + 0.038 * FastMath.log((X / radLen) / ct)); + + if (X == 0.) { + theta0 = 0.; // Get the scattering angle + } else { + theta0 = FastMath.sqrt((X / radLen) / ct) * (0.0136 / p.mag()) * (1.0 + 0.038 * FastMath.log((X / radLen) / ct)); + } double thetaX = rndm.nextGaussian() * theta0; double thetaY = rndm.nextGaussian() * theta0; double tx = FastMath.sin(thetaX); @@ -102,8 +106,8 @@ RKhelix randomScat(Plane P, double X) { // Produce a new helix scattered randoml double E = p.mag(); // Everything is assumed electron double sp = 0.0; if (doeLoss) { - sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g - sp = sp*20; // ToDo temporary!!! + sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g + sp = sp * 20; // ToDo temporary!!! } double dEdx = 0.1 * sp * rho; // in GeV/mm double eLoss = dEdx * X / ct; From d6763930f9d545949211ff943bad6d77b7d6a22a Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Wed, 18 Jan 2023 14:32:59 -0800 Subject: [PATCH 10/17] recon particle refitting interfacing --- .../ReconstructedParticleRefitter.java | 78 +++++-- .../hps/recon/tracking/kalman/HelixTest3.java | 19 +- .../hps/recon/tracking/kalman/KalTrack.java | 61 +++--- .../tracking/kalman/KalmanInterface.java | 205 ++++++++++++++++-- .../recon/tracking/kalman/KalmanParams.java | 11 + .../tracking/kalman/MeasurementSite.java | 4 +- 6 files changed, 295 insertions(+), 83 deletions(-) diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java index 3d23b2c271..03fdd3cf77 100644 --- a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -4,6 +4,12 @@ import static java.lang.Math.abs; import java.util.ArrayList; import java.util.List; + +import org.hps.recon.tracking.MaterialSupervisor; +import org.hps.recon.tracking.MaterialSupervisor.ScatteringDetectorVolume; +import org.hps.recon.tracking.MaterialSupervisor.SiStripPlane; +import org.hps.recon.tracking.kalman.KalmanInterface; +import org.hps.recon.tracking.kalman.KalmanParams; import org.lcsim.detector.DetectorElementStore; import org.lcsim.detector.IDetectorElement; import org.lcsim.detector.identifier.IExpandedIdentifier; @@ -15,8 +21,10 @@ import org.lcsim.event.RawTrackerHit; import org.lcsim.event.ReconstructedParticle; import org.lcsim.event.Track; +import org.lcsim.event.TrackState; import org.lcsim.event.TrackerHit; import org.lcsim.event.base.BaseReconstructedParticle; +import org.lcsim.geometry.Detector; import org.lcsim.util.Driver; import org.lcsim.util.aida.AIDA; @@ -54,11 +62,59 @@ public class ReconstructedParticleRefitter extends Driver { * combination */ private double _eOverpCut = 0.1; + + /** + * The interface to the Kalman Filter code + */ + private KalmanInterface KI; + private org.lcsim.geometry.FieldMap fm; + + private double eRes0 = -1.0; + private double eRes1 = -1.0; + + /** + * ECal energy resolution parameterization, for the resolution as a % of E. + * @param eRes0 coefficient of the 1/sqrt(E) term + */ + public void setERes0(double eRes0) { + this.eRes0 = eRes0; + } + /** + * ECal energy resolution parameterization, for the resolution as a % of E. + * @param eRes1 the constant term + */ + public void setERes1(double eRes1) { + this.eRes1 = eRes1; + } + + /** + * Set up the geometry and parameters for the Kalman-filter track finding and fitting. + */ + @Override + public void detectorChanged(Detector det) { + MaterialSupervisor materialManager; + materialManager = new MaterialSupervisor(); + materialManager.buildModel(det); + + // Instantiate the interface to the Kalman-Filter code and set up the run parameters + KalmanParams kPar = new KalmanParams(); + // Override the default resolution parameters with numbers from the steering file + if (eRes0 > 0. || eRes1 > 0.) kPar.setEnergyRes(eRes0, eRes1); + + fm = det.getFieldMap(); // The HPS magnetic field map + KI = new KalmanInterface(kPar, fm); // Instantiate the Kalman interface + ArrayList detPlanes = new ArrayList(); + List materialVols = ((MaterialSupervisor) (materialManager)).getMaterialVolumes(); + for (ScatteringDetectorVolume vol : materialVols) { + detPlanes.add((SiStripPlane)(vol)); + } + KI.createSiModules(detPlanes); // The detector geometry objects used by the Kalman code + } + /** - * The action - * - * @param event + * The action per event + * @param event The hps-java event header */ public void process(EventHeader event) { if (event.hasCollection(ReconstructedParticle.class, _finalStateParticleCollectionName)) { @@ -77,7 +133,7 @@ public void process(EventHeader event) { // quick check on E/p so we don't try to fit to the energy of MIP tracks double eOverP = rp.getEnergy() / rp.getMomentum().magnitude(); aida.histogram1D("e over p", 100, 0., 2.).fill(eOverP); - if (abs(eOverP - 1) < _eOverpCut) { + if (abs(eOverP - 1.0) < _eOverpCut) { // create a new ReconstructedParticle here... refitReconstructedParticles.add(makeNewReconstructedParticle(rp)); } else { @@ -139,13 +195,9 @@ private Track refitTrack(ReconstructedParticle rp) { Cluster cluster = rp.getClusters().get(0); //the energy of the associated cluster double energy = cluster.getEnergy(); - //the list of tracker hits - List hits = track.getTrackerHits(); - // initial guess for the track parameters - Hep3Vector momentum = rp.getMomentum(); - // TODO fit a new track with this list of hits and the cluster energy. - // for now simply return the existing track - return track; + + // Fit a new track with this list of hits and the cluster energy. + return KI.refitTrackWithE(track, energy); } /** @@ -209,8 +261,8 @@ public void setRefitParticleCollectionName(String s) { } /** - * - * @param d + * Set the cut on E/p from the steering file + * @param d Maximum E/p allowed for electron/positron candidates */ public void set_eOverpCut(double d) { _eOverpCut = d; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index 233b60fb78..77cf62a75b 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -958,9 +958,7 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { else hResid1.entry(site.aF.r / Math.sqrt(site.aF.R)); } if (site.smoothed) { - StateVector aA = null; - if (residualsEconstrained) aA = site.aES; - else aA = site.aS; + StateVector aA = site.aS; if (site.m.Layer == 4) hReducedErr.entry(Math.sqrt(aA.R)); chi2s += Math.pow(aA.mPred - site.m.hits.get(site.hitID).vTrue, 2) / aA.R; hResidS0[siM.Layer].entry(aA.r / Math.sqrt(aA.R)); @@ -1165,23 +1163,22 @@ else if (kF.sites.indexOf(site) == kF.sites.size()-1) { System.out.format("True energy = %10.4f, ECAL energy = %10.4f, sigma(E)=%8.3f\n",Etrue,E,sigmaE); KalmanTrack.helixAtOrigin.a.print("helix at origin"); TkInitial.p.print("true helix"); - KalmanTrack.helixAtOriginEconstraint.a.print("energy constrained helix"); KalmanTrack.printLong("after adding energy constraint"); } hChi2E.entry(KalmanTrack.chi2_Econstraint); - for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.helixAtOriginEconstraint.a.v[i] - TkInitial.p.v[i]); + for (int i = 0; i < 5; ++i) hErr[i] = (KalmanTrack.helixAtOrigin.a.v[i] - TkInitial.p.v[i]); hEatanlcon.entry(hErr[4]); hEakcon.entry(hErr[2]); hERhocon.entry(hErr[0]); hEPhi0con.entry(hErr[1]); hEZ0con.entry(hErr[3]); - hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(0,0))); - hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(1,1))); - hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(2,2))); - hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(3,3))); - hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.helixAtOriginEconstraint.C.unsafe_get(4,4))); + hEdrhoCon.entry(hErr[0] / Math.sqrt(KalmanTrack.helixAtOrigin.C.unsafe_get(0,0))); + hEphi0Con.entry(hErr[1] / Math.sqrt(KalmanTrack.helixAtOrigin.C.unsafe_get(1,1))); + hEkCon.entry(hErr[2] / Math.sqrt(KalmanTrack.helixAtOrigin.C.unsafe_get(2,2))); + hEdzCon.entry(hErr[3] / Math.sqrt(KalmanTrack.helixAtOrigin.C.unsafe_get(3,3))); + hEtanlCon.entry(hErr[4] / Math.sqrt(KalmanTrack.helixAtOrigin.C.unsafe_get(4,4))); trueErr = new Vec(5,hErr); - helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(KalmanTrack.helixAtOriginEconstraint.C).invert())); + helixChi2 = trueErr.dot(trueErr.leftMultiply(KalTrack.mToS(KalmanTrack.helixAtOrigin.C).invert())); hChi2HelixE.entry(helixChi2); } } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index dbef9c983a..db89568dd6 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -38,8 +38,8 @@ public class KalTrack { public int eventNumber; public boolean bad; HelixState helixAtOrigin; - HelixState helixAtOriginEconstraint; - private boolean propagated, propagatedE; + private boolean propagated; + boolean energyConstrained; private RotMatrix Rot; // Rotation matrix between global and field coordinates at the beam spot private Vec originPoint; private Vec originMomentum; @@ -121,9 +121,8 @@ public class KalTrack { } helixAtOrigin = null; - helixAtOriginEconstraint = null; propagated = false; - propagatedE = false; + energyConstrained = false; MeasurementSite site0 = this.SiteList.get(0); Vec B = null; if (kPar.uniformB) { @@ -390,7 +389,6 @@ public Pair unbiasedResidual(MeasurementSite site, boolean eConst Vec aStar = null; if (site.hitID >= 0) { StateVector aA = site.aS; - if (eConstrain) aA = site.aES; double sigma = site.m.hits.get(site.hitID).sigma; DMatrixRMaj Cstar = new DMatrixRMaj(5,5); aStar = aA.inverseFilter(site.H, sigma*sigma, Cstar); @@ -460,7 +458,6 @@ public Pair biasedResidual(MeasurementSite site) { public void print(String s) { System.out.format("\nKalTrack %s: Event %d, ID=%d, %d hits, chi^2=%10.5f, t=%5.1f from %5.1f to %5.1f, bad=%b\n", s, eventNumber, ID, nHits, chi2, time, tMin, tMax, bad); if (propagated) System.out.format(" Helix parameters at origin = %s\n", helixAtOrigin.a.toString()); - if (propagatedE) System.out.format(" Helix parameters at origin energy constrainted = %s\n", helixAtOriginEconstraint.a.toString()); System.out.format(" Magnetic field magnitude = %10.5f and direction = %s\n", Bmag, tB.toString()); MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { @@ -514,9 +511,8 @@ String toString(String s) { double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); str=str+String.format(" Energy unconstrained = %10.6f\n", energy); } - if (propagatedE) { + if (energyConstrained) { str=str+String.format("Chi-squared of energy constrained fit = %10.5f\n", chi2_Econstraint); - str=str+helixAtOriginEconstraint.toString("E-constrained helix state for a pivot at the origin")+"\n"; //str=str+originPoint.toString("point on the helix closest to the origin")+"\n"; //str=str+String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); //SquareMatrix C1 = new SquareMatrix(3, Cx); @@ -524,15 +520,14 @@ String toString(String s) { //str=str+originMomentum.toString("momentum of the particle at closest approach to the origin\n"); //SquareMatrix C2 = new SquareMatrix(3, Cp); //str=str+C2.toString("covariance matrix for the momentum"); - double tanL = helixAtOriginEconstraint.a.v[4]; - double K = helixAtOriginEconstraint.a.v[2]; + double tanL = helixAtOrigin.a.v[4]; + double K = helixAtOrigin.a.v[2]; double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); str=str+String.format(" Energy constrained = %10.6f\n", energy); } MeasurementSite site0 = this.SiteList.get(0); if (site0.aS != null) { str=str + String.format(" Helix at layer %d: %s\n", site0.m.Layer, site0.aS.helix.a.toString()); - str=str + String.format(" Energy constrained helix at layer %d: %s\n", site0.m.Layer, site0.aES.helix.a.toString()); } for (int i = 0; i < SiteList.size(); i++) { MeasurementSite site = SiteList.get(i); @@ -1524,18 +1519,18 @@ void energyConstraint(double E, double sigmaE) { // smoothing back to the first tracker site can then proceed as usual. int idxLast = SiteList.size()-1; MeasurementSite lastSite = SiteList.get(idxLast); - HelixState energyConstrainedHelix = lastSite.aS.helix.energyConstrained(E, sigmaE); + HelixState energyConstrainedHelix = lastSite.aF.helix.energyConstrained(E, sigmaE); //System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aS.helix.a.v[2],energyConstrainedHelix.a.v[2]); lastSite.energyConstrained = true; - lastSite.aES = new StateVector(lastSite.aS.kLow, lastSite.aS.uniformB); - lastSite.aES.helix = energyConstrainedHelix; - lastSite.aES.kUp = lastSite.aS.kUp; - lastSite.aES.F = lastSite.aS.F; // Don't deep copy the F matrix - lastSite.aES.mPred = lastSite.aS.mPred; - lastSite.aES.R = lastSite.aS.R; - lastSite.aES.r = lastSite.aS.r; - lastSite.aES.K = lastSite.aS.K; - lastSite.chi2incE = (lastSite.aES.r * lastSite.aES.r) / lastSite.aES.R; + lastSite.aS = new StateVector(lastSite.aS.kLow, lastSite.aS.uniformB); + lastSite.aS.helix = energyConstrainedHelix; + lastSite.aS.kUp = lastSite.aF.kUp; + lastSite.aS.F = lastSite.aF.F; // Don't deep copy the F matrix + lastSite.aS.mPred = lastSite.aF.mPred; + lastSite.aS.R = lastSite.aF.R; + lastSite.aS.r = lastSite.aF.r; + lastSite.aS.K = lastSite.aF.K; + lastSite.chi2incE = (lastSite.aS.r * lastSite.aS.r) / lastSite.aS.R; // Get the residual of the prediction at the ECAL double kappa = energyConstrainedHelix.a.v[2]; double tanl = energyConstrainedHelix.a.v[4]; @@ -1546,32 +1541,32 @@ void energyConstraint(double E, double sigmaE) { MeasurementSite nS = lastSite; for (int idx=idxLast-1; idx>=0; --idx) { MeasurementSite thisSite = SiteList.get(idx); - thisSite.aES = thisSite.aF.smooth(nS.aES, nS.aP); + thisSite.aS = thisSite.aF.smooth(nS.aS, nS.aP); if (thisSite.hitID < 0) { thisSite.energyConstrained = true; continue; } Measurement hit = thisSite.m.hits.get(thisSite.hitID); double V = hit.sigma * hit.sigma; - double phiS = thisSite.aES.helix.planeIntersect(thisSite.m.p); + double phiS = thisSite.aS.helix.planeIntersect(thisSite.m.p); if (Double.isNaN(phiS)) { // This should almost never happen! logger.log(Level.FINE, "KalTrack.energyConstraint: no intersection of helix with the plane exists."); continue; } - thisSite.aES.mPred = thisSite.h(thisSite.aES, thisSite.m, phiS); - thisSite.aES.r = hit.v - thisSite.aES.mPred; + thisSite.aS.mPred = thisSite.h(thisSite.aS, thisSite.m, phiS); + thisSite.aS.r = hit.v - thisSite.aS.mPred; if (tempV == null) tempV = new DMatrixRMaj(5,1); - CommonOps_DDRM.mult(thisSite.aES.helix.C, thisSite.H, tempV); - thisSite.aES.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); - if (thisSite.aES.R < 0) { - if (debug) System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aES.R); + CommonOps_DDRM.mult(thisSite.aS.helix.C, thisSite.H, tempV); + thisSite.aS.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); + if (thisSite.aS.R < 0) { + if (debug) System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aS.R); //aS.print("the smoothed state"); //nS.print("the next site in the chain"); - thisSite.aES.R = 0.25*V; // A negative covariance makes no sense, hence this fudge + thisSite.aS.R = 0.25*V; // A negative covariance makes no sense, hence this fudge } - thisSite.chi2incE = (thisSite.aES.r * thisSite.aES.r) / thisSite.aES.R; + thisSite.chi2incE = (thisSite.aS.r * thisSite.aS.r) / thisSite.aS.R; this.chi2_Econstraint += thisSite.chi2incE; thisSite.energyConstrained = true; nS = thisSite; @@ -1582,8 +1577,8 @@ void energyConstraint(double E, double sigmaE) { // The StateVector method propagateRungeKutta transforms the origin plane into the origin B-field frame Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); arcLengthE = new double[1]; - helixAtOriginEconstraint = SiteList.get(0).aES.helix.propagateRungeKutta(originPlane, yScat, XLscat, SiteList.get(0).m.Bfield, arcLengthE); - propagatedE = true; + helixAtOrigin = SiteList.get(0).aS.helix.propagateRungeKutta(originPlane, yScat, XLscat, SiteList.get(0).m.Bfield, arcLengthE); + energyConstrained = true; } /** diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 2bbd52f0f3..d299b1c06b 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -77,6 +77,7 @@ public class KalmanInterface { public static BasicHep3Matrix HpsSvtToKalmanMatrix; private static DMatrixRMaj tempM; private static DMatrixRMaj Ft; + private static DMatrixRMaj helixCov; private int maxHits; private int nBigEvents; private int eventNumber; @@ -213,6 +214,7 @@ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { tempM = new DMatrixRMaj(5,5); Ft = new DMatrixRMaj(5,5); + helixCov = null; hitMap = new HashMap(); simHitMap = new HashMap(); @@ -778,7 +780,7 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { loc = TrackState.AtLastHit; /* - //DO Not att the missing layer track states yet. + //DO Not store the missing layer track states (yet). if (storeTrackStates) { for (int k = 1; k < lay - prevID; k++) { // uses new lcsim constructor @@ -801,14 +803,39 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { newTrack.getTrackStates().add(ts_ecal); // other track properties - newTrack.setChisq(kT.chi2); - newTrack.setNDF(kT.SiteList.size() - 5); + if (kT.energyConstrained) { + newTrack.setChisq(kT.chi2_Econstraint); + newTrack.setNDF(kT.SiteList.size() - 4); + } else { + newTrack.setChisq(kT.chi2); + newTrack.setNDF(kT.SiteList.size() - 5); + } newTrack.setTrackType(BaseTrack.TrackType.Y_FIELD.ordinal()); newTrack.setFitSuccess(true); return newTrack; } + /** + * Create an HPS track from a Kalman seed + * + * @param trk Seed + * @return HPS track + */ + public BaseTrack createTrack(SeedTrack trk) { + double[] newPivot = { 0., 0., 0. }; + double[] params = getLCSimParams(trk.pivotTransform(newPivot), alphaCenter); + SymmetricMatrix cov = getLCSimCov(trk.covariance(), alphaCenter); + BaseTrack newTrack = new BaseTrack(); + newTrack.setTrackParameters(params, trk.B()); + newTrack.setCovarianceMatrix(cov); + addHitsToTrack(newTrack); + newTrack.setTrackType(BaseTrack.TrackType.Y_FIELD.ordinal()); + newTrack.setFitSuccess(trk.success); + + return newTrack; + } + /** * Convert helix parameters from Kalman to LCSim * @@ -927,26 +954,6 @@ private void addHitsToTrack(BaseTrack newTrack) { newTrack.setNDF(newTrack.getTrackerHits().size()); } - /** - * Create an HPS track from a Kalman seed - * - * @param trk Seed - * @return HPS track - */ - public BaseTrack createTrack(SeedTrack trk) { - double[] newPivot = { 0., 0., 0. }; - double[] params = getLCSimParams(trk.pivotTransform(newPivot), alphaCenter); - SymmetricMatrix cov = getLCSimCov(trk.covariance(), alphaCenter); - BaseTrack newTrack = new BaseTrack(); - newTrack.setTrackParameters(params, trk.B()); - newTrack.setCovarianceMatrix(cov); - addHitsToTrack(newTrack); - newTrack.setTrackType(BaseTrack.TrackType.Y_FIELD.ordinal()); - newTrack.setFitSuccess(trk.success); - - return newTrack; - } - /** * Method to create one Kalman SiModule geometry object for each silicon-strip detector * @@ -1480,6 +1487,158 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pi return new KalmanTrackFit2(evtNumb, SiMoccupied, null, startIndex, nIt, pivot, helixParams, cov, kPar, fM); } + /** + * Refit an existing HPS track with the associated energy measurement added in. + * @param track + * @return + */ + public Track refitTrackWithE(Track track, double energy) { + // First we need initial guesses for the helix parameters and covariance. + // Preferentially take them from a TrackState. If there is no TrackState, + // then estimate from a linear fit to the set of hits. + List tkrStates = track.getTrackStates(); + TrackState theTrackState = null; + double [] helixParams = null; + Vec kalHelixParams = null; + Vec pivot = new Vec(0., 0., 0.); + if (tkrStates != null) { + for (TrackState tkrState : tkrStates) { + if (tkrState.getLocation() == TrackState.AtFirstHit) { + theTrackState = tkrState; + break; + } + } + if (theTrackState == null) { + for (TrackState tkrState : tkrStates) { + if (tkrState.getLocation() == TrackState.AtIP) { + theTrackState = tkrState; + break; + } + } + } + if (theTrackState != null) { + Vec refPnt = new Vec(3, theTrackState.getReferencePoint()); + helixParams = theTrackState.getParameters(); + double bField = KalmanInterface.getField(refPnt, fM).mag(); + double c = 2.99793e8; // Speed of light in m/s + double alpha = 1000.0 * 1.0e9 / (c * bField); + kalHelixParams = new Vec(5, KalmanInterface.unGetLCSimParams(helixParams, alpha)); + double[] covHPS = theTrackState.getCovMatrix(); + DMatrixRMaj helixCov = new DMatrixRMaj(KalmanInterface.ungetLCSimCov(covHPS, alpha)); + } + } + // Get the list of tracker hits on this track + List hitsOnTrack = track.getTrackerHits(); + double firstHitZ = fillMeasurements(hitsOnTrack, 0); + // Do a linear fit to the track hits to get the helix parameter guesses + if (theTrackState == null) { + if (debug) System.out.format("firstHitZ %f \n", firstHitZ); + SeedTrack seed = new SeedTrack(trackHitsKalman, firstHitZ, 0., kPar); + kalHelixParams = seed.helixParams(); + helixCov = seed.covariance(); + pivot.v[1] = seed.yOrigin; + } + + + ArrayList SiMoccupied = new ArrayList(); + for (SiModule SiM : SiMlist) { + if (!SiM.hits.isEmpty()) SiMoccupied.add(SiM); + } + Collections.sort(SiMoccupied, new SortByLayer()); + if (debug) { + for (int i = 0; i < SiMoccupied.size(); i++) { + SiModule SiM = SiMoccupied.get(i); + SiM.print(String.format("SiMoccupied%d", i)); + } + } + + int startIndex = 0; + if (debug) System.out.format("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + CommonOps_DDRM.scale(10., helixCov); + + // Do the Kalman track fit only up through the filter step + KalTrack newTrack = kalmanFilterTrack(0, track.hashCode(), SiMoccupied, null, kalHelixParams, pivot, helixCov); + + // Include the eCal information and smooth back toward the origin + double sigmaE = (kPar.eRes[0]/FastMath.sqrt(energy) + kPar.eRes[1])*energy/100.; + newTrack.energyConstraint(energy, sigmaE); + + // Convert the KalTrack object into and HPS Track and TrackState + Track outputTrack = createTrack(newTrack, true); + + trackHitsKalman.clear(); + return outputTrack; + } + + /** + * Run the Kalman filter (no smoothing) on a set of hits. + * @param eventNumber + * @param data List of Si modules with data points to be included in the fit + * @param hits Which hit to use in each SiModule. Can be null if each module has only 1 hit. + * @param helixParams 5 helix parameters for the starting "guess" helix + * @param pivot Pivot point for the starting "guess" helix + * @param C Full covariance matrix for the starting "guess" helix + * @return The new filtered KalTrack object + */ + KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, ArrayList hits, Vec helixParams, Vec pivot, DMatrixRMaj C) { + if (hits == null) { + hits = new ArrayList(data.size()); + for (int i=0; i sites = new ArrayList(data.size()); + MeasurementSite prevSite = null; + double chi2f = 0.; + MeasurementSite newSite = null; + boolean success = true; + for (int idx=0; idx= 0 && hitNumber >= 0) chi2f += newSite.chi2inc; + + sites.add(newSite); + prevSite = newSite; + } + ArrayList yScat = null; + ArrayList XLscat = null; + return new KalTrack(eventNumber, tkID, sites, yScat, XLscat, kPar); + } + /** * Sort the Kalman-filter geometry objects according to tracker layer * diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index 86fba49536..a0c1e14be8 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -40,6 +40,7 @@ public class KalmanParams { static final int numLayers = 14; boolean uniformB; boolean eLoss; + double [] eRes; private int[] Swap = {1,0, 3,2, 5,4, 7,6, 9,8, 11,10, 13,12}; private String [] tb; @@ -86,6 +87,7 @@ public void print() { System.out.format(" Maximum chi^2 for 5-hit tracks with a vertex constraint: %8.2f\n", mxChi2Vtx); System.out.format(" Include ionization energy loss in fit = %b\n", eLoss); System.out.format(" Default origin to use for vertex constraints:\n"); + System.out.format(" CAL energy resolution = %8.2f /sqrt(E) + %8.2f\n", eRes[0], eRes[1]); for (int i=0; i<3; ++i) { System.out.format(" %d: %8.3f +- %8.3f\n", i, beamSpot[i], vtxSize[i]); } @@ -166,6 +168,9 @@ public KalmanParams() { firstLayer = 0; // First layer in the tracking system (2 for pre-2019 data) lowPhThresh = 0.25; // Residual improvement ratio necessary to use a low-ph hit instead of high-ph seedCompThr = 0.05; // Remove SeedTracks with all Helix params within relative seedCompThr . If -1 do not apply duplicate removal + eRes = new double[2]; + eRes[0] = 3.0; // Cal energy resolution parameters in % sigmaE = eRes[0]/sqrt(E) + eRes[1] + eRes[1] = 1.0; // Load the default search strategies // Index 0 is for the bottom tracker (+z), 1 for the top (-z) @@ -225,6 +230,12 @@ public KalmanParams() { // } } + public void setEnergyRes(double a, double b) { + if (a > 0.) eRes[0] = a; + if (b > 0.) eRes[1] = b; + logger.config(String.format("Setting CAL energy resolution to %8.2f/sqrt(E) + %8.2f", eRes[0], eRes[1])); + } + public void setUniformB(boolean input) { logger.config(String.format("Setting the field to be uniform? %b", input)); uniformB = input; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java index 7e1187b7ec..fd31b62ac1 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java @@ -21,8 +21,7 @@ class MeasurementSite { boolean filtered; // True if the filtered state vector has been built StateVector aS; // Smoothed state vector boolean smoothed; // True if the smoothed state vector has been built - StateVector aES; // Energy-constrained smoothed state vector - boolean energyConstrained; // True if the energy constrained smoothed state vector has been built + boolean energyConstrained; // True if the smoothed state vector is energy constrained double chi2inc; // chi^2 increment for this site double chi2incE; // chi^2 increment for the energy-constrainted track DMatrixRMaj H; // Derivatives of the transformation from state vector to measurement @@ -82,7 +81,6 @@ String toString(String s) { if (predicted) str = str + aP.toString("predicted"); if (filtered) str = str + aF.toString("filtered"); if (smoothed) str = str + aS.toString("smoothed"); - if (energyConstrained) str = str + aES.toString("energy-constrained"); if (H != null) str = str + "matrix of the transformation from state vector to measurement:" + H.toString(); str=str+String.format(" Assumed electron dE/dx in GeV/mm = %10.6f; Detector thickness=%10.6f\n", dEdx, m.thickness); if (m.Layer < 0) { From 2d5f87f84cf313e2a32aef59b76e66efb612d30e Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Thu, 19 Jan 2023 15:50:24 -0800 Subject: [PATCH 11/17] fixed compile and style problems --- .../ReconstructedParticleRefitter.java | 5 +- .../hps/recon/tracking/kalman/HelixTest3.java | 2 +- .../hps/recon/tracking/kalman/KalTrack.java | 26 ++-- .../tracking/kalman/KalmanInterface.java | 116 +++++++++--------- .../recon/tracking/kalman/KalmanParams.java | 6 +- 5 files changed, 76 insertions(+), 79 deletions(-) diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java index 03fdd3cf77..cc5ff45745 100644 --- a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -1,6 +1,5 @@ package org.hps.recon.particle; -import hep.physics.vec.Hep3Vector; import static java.lang.Math.abs; import java.util.ArrayList; import java.util.List; @@ -21,8 +20,6 @@ import org.lcsim.event.RawTrackerHit; import org.lcsim.event.ReconstructedParticle; import org.lcsim.event.Track; -import org.lcsim.event.TrackState; -import org.lcsim.event.TrackerHit; import org.lcsim.event.base.BaseReconstructedParticle; import org.lcsim.geometry.Detector; import org.lcsim.util.Driver; @@ -97,7 +94,7 @@ public void detectorChanged(Detector det) { materialManager = new MaterialSupervisor(); materialManager.buildModel(det); - // Instantiate the interface to the Kalman-Filter code and set up the run parameters + // Instantiate the interface to the Kalman-Filter code and set up the run parameters KalmanParams kPar = new KalmanParams(); // Override the default resolution parameters with numbers from the steering file if (eRes0 > 0. || eRes1 > 0.) kPar.setEnergyRes(eRes0, eRes1); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index f63e09c57d..2d4b5c058e 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -1235,7 +1235,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code } hChi2E.entry(KalmanTrack.chi2_Econstraint); for (int i = 0; i < 5; ++i) { - hErr[i] = (KalmanTrack.helixAtOriginEconstraint.a.v[i] - TkInitial.p.v[i]); + hErr[i] = (KalmanTrack.helixAtOrigin.a.v[i] - TkInitial.p.v[i]); } hEatanlcon.entry(hErr[4]); hEakcon.entry(hErr[2]); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 0f366e8e5d..872f2b34e9 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -40,7 +40,7 @@ public class KalTrack { public boolean bad; HelixState helixAtOrigin; private boolean propagated; - boolean energyConstrained; + boolean energyConstrained; private RotMatrix Rot; // Rotation matrix between global and field coordinates at the beam spot private Vec originPoint; private Vec originMomentum; @@ -473,7 +473,7 @@ public Pair unbiasedResidual(MeasurementSite site, boolean eCons double varResid = -999.; Vec aStar = null; if (site.hitID >= 0) { - StateVector aA = site.aS; + StateVector aA = site.aS; double sigma = site.m.hits.get(site.hitID).sigma; DMatrixRMaj Cstar = new DMatrixRMaj(5, 5); aStar = aA.inverseFilter(site.H, sigma * sigma, Cstar); @@ -610,7 +610,7 @@ String toString(String s) { str=str+String.format(" Energy unconstrained = %10.6f\n", energy); } if (energyConstrained) { - str=str+String.format("Chi-squared of energy constrained fit = %10.5f\n", chi2_Econstraint); + str=str+String.format("Chi-squared of energy constrained fit = %10.5f\n", chi2_Econstraint); //str=str+originPoint.toString("point on the helix closest to the origin")+"\n"; //str=str+String.format(" arc length from the origin to the first measurement=%9.4f\n", arcLength[0]); //SquareMatrix C1 = new SquareMatrix(3, Cx); @@ -618,8 +618,8 @@ String toString(String s) { //str=str+originMomentum.toString("momentum of the particle at closest approach to the origin\n"); //SquareMatrix C2 = new SquareMatrix(3, Cp); //str=str+C2.toString("covariance matrix for the momentum"); - double tanL = helixAtOriginEconstraint.a.v[4]; - double K = helixAtOriginEconstraint.a.v[2]; + double tanL = helixAtOrigin.a.v[4]; + double K = helixAtOrigin.a.v[2]; double energy = FastMath.sqrt(1.0 + tanL * tanL) / Math.abs(K); str = str + String.format(" Energy constrained = %10.6f\n", energy); } @@ -1796,7 +1796,7 @@ void energyConstraint(double E, double sigmaE) { MeasurementSite nS = lastSite; for (int idx = idxLast - 1; idx >= 0; --idx) { MeasurementSite thisSite = SiteList.get(idx); - thisSite.aES = thisSite.aF.smooth(nS.aES, nS.aP); + thisSite.aS = thisSite.aF.smooth(nS.aS, nS.aP); if (thisSite.hitID < 0) { thisSite.energyConstrained = true; continue; @@ -1809,20 +1809,20 @@ void energyConstraint(double E, double sigmaE) { logger.log(Level.FINE, "KalTrack.energyConstraint: no intersection of helix with the plane exists."); continue; } - thisSite.aES.mPred = thisSite.h(thisSite.aES, thisSite.m, phiS); - thisSite.aES.r = hit.v - thisSite.aES.mPred; + thisSite.aS.mPred = thisSite.h(thisSite.aS, thisSite.m, phiS); + thisSite.aS.r = hit.v - thisSite.aS.mPred; if (tempV == null) { tempV = new DMatrixRMaj(5, 1); } - CommonOps_DDRM.mult(thisSite.aES.helix.C, thisSite.H, tempV); - thisSite.aES.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); - if (thisSite.aES.R < 0) { + CommonOps_DDRM.mult(thisSite.aS.helix.C, thisSite.H, tempV); + thisSite.aS.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); + if (thisSite.aS.R < 0) { if (debug) { - System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aES.R); + System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aS.R); } //aS.print("the smoothed state"); //nS.print("the next site in the chain"); - thisSite.aES.R = 0.25 * V; // A negative covariance makes no sense, hence this fudge + thisSite.aS.R = 0.25 * V; // A negative covariance makes no sense, hence this fudge } thisSite.chi2incE = (thisSite.aS.r * thisSite.aS.r) / thisSite.aS.R; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 4d5fd91859..f6345d8efb 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -875,11 +875,11 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { // other track properties if (kT.energyConstrained) { - newTrack.setChisq(kT.chi2_Econstraint); - newTrack.setNDF(kT.SiteList.size() - 4); + newTrack.setChisq(kT.chi2_Econstraint); + newTrack.setNDF(kT.SiteList.size() - 4); } else { - newTrack.setChisq(kT.chi2); - newTrack.setNDF(kT.SiteList.size() - 5); + newTrack.setChisq(kT.chi2); + newTrack.setNDF(kT.SiteList.size() - 5); } newTrack.setTrackType(BaseTrack.TrackType.Y_FIELD.ordinal()); newTrack.setFitSuccess(true); @@ -1652,30 +1652,30 @@ public Track refitTrackWithE(Track track, double energy) { Vec kalHelixParams = null; Vec pivot = new Vec(0., 0., 0.); if (tkrStates != null) { - for (TrackState tkrState : tkrStates) { - if (tkrState.getLocation() == TrackState.AtFirstHit) { - theTrackState = tkrState; - break; - } - } - if (theTrackState == null) { - for (TrackState tkrState : tkrStates) { - if (tkrState.getLocation() == TrackState.AtIP) { - theTrackState = tkrState; - break; - } - } - } - if (theTrackState != null) { - Vec refPnt = new Vec(3, theTrackState.getReferencePoint()); - helixParams = theTrackState.getParameters(); - double bField = KalmanInterface.getField(refPnt, fM).mag(); + for (TrackState tkrState : tkrStates) { + if (tkrState.getLocation() == TrackState.AtFirstHit) { + theTrackState = tkrState; + break; + } + } + if (theTrackState == null) { + for (TrackState tkrState : tkrStates) { + if (tkrState.getLocation() == TrackState.AtIP) { + theTrackState = tkrState; + break; + } + } + } + if (theTrackState != null) { + Vec refPnt = new Vec(3, theTrackState.getReferencePoint()); + helixParams = theTrackState.getParameters(); + double bField = KalmanInterface.getField(refPnt, fM).mag(); double c = 2.99793e8; // Speed of light in m/s double alpha = 1000.0 * 1.0e9 / (c * bField); - kalHelixParams = new Vec(5, KalmanInterface.unGetLCSimParams(helixParams, alpha)); + kalHelixParams = new Vec(5, KalmanInterface.unGetLCSimParams(helixParams, alpha)); double[] covHPS = theTrackState.getCovMatrix(); DMatrixRMaj helixCov = new DMatrixRMaj(KalmanInterface.ungetLCSimCov(covHPS, alpha)); - } + } } // Get the list of tracker hits on this track List hitsOnTrack = track.getTrackerHits(); @@ -1696,10 +1696,10 @@ public Track refitTrackWithE(Track track, double energy) { } Collections.sort(SiMoccupied, new SortByLayer()); if (debug) { - for (int i = 0; i < SiMoccupied.size(); i++) { - SiModule SiM = SiMoccupied.get(i); - SiM.print(String.format("SiMoccupied%d", i)); - } + for (int i = 0; i < SiMoccupied.size(); i++) { + SiModule SiM = SiMoccupied.get(i); + SiM.print(String.format("SiMoccupied%d", i)); + } } int startIndex = 0; @@ -1717,7 +1717,7 @@ public Track refitTrackWithE(Track track, double energy) { Track outputTrack = createTrack(newTrack, true); trackHitsKalman.clear(); - return outputTrack; + return outputTrack; } /** @@ -1753,35 +1753,35 @@ KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, MeasurementSite newSite = null; boolean success = true; for (int idx=0; idx= 0 && hitNumber >= 0) chi2f += newSite.chi2inc; - - sites.add(newSite); + SiModule m = data.get(idx); + if (m.hits.size() <= 0) { + continue; + } + newSite = new MeasurementSite(idx, m, kPar); + int hitNumber = hits.get(idx); + if (prevSite == null) { + if (newSite.makePrediction(sI, hitNumber, false, false) < 0) { + logger.warning(String.format("kalmanFilterTrack: failed to make initial prediction at site %d, idx=%d. Abort", sites.indexOf(newSite), idx)); + success = false; + break; + } + } else { + if (newSite.makePrediction(prevSite.aF, prevSite.m, hitNumber, false, false) < 0) { + logger.warning(String.format("kalmanFilterTrack: failed to make prediction at site %d, idx=%d. Abort", sites.indexOf(newSite), idx)); + success = false; + break; + } + } + + if (!newSite.filter()) { + logger.warning(String.format("kalmanFilterTrack failed to filter at site %d, idx=%d. Ignore remaining sites", sites.indexOf(newSite), idx)); + success = false; + break; + } + + if (m.Layer >= 0 && hitNumber >= 0) chi2f += newSite.chi2inc; + + sites.add(newSite); prevSite = newSite; } ArrayList yScat = null; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index 6d92a3452c..0d8e4a983f 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -235,9 +235,9 @@ public KalmanParams() { } public void setEnergyRes(double a, double b) { - if (a > 0.) eRes[0] = a; - if (b > 0.) eRes[1] = b; - logger.config(String.format("Setting CAL energy resolution to %8.2f/sqrt(E) + %8.2f", eRes[0], eRes[1])); + if (a > 0.) eRes[0] = a; + if (b > 0.) eRes[1] = b; + logger.config(String.format("Setting CAL energy resolution to %8.2f/sqrt(E) + %8.2f", eRes[0], eRes[1])); } public void setUniformB(boolean input) { From 935c0a85899b60bbe5b00c8087acbed2675c3795 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Fri, 20 Jan 2023 11:29:49 -0800 Subject: [PATCH 12/17] debugging --- .../hps/recon/tracking/kalman/KalTrack.java | 48 +++++++++++++------ .../tracking/kalman/KalmanInterface.java | 48 +++++++++++++++---- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 872f2b34e9..8b1208a271 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -54,7 +54,7 @@ public class KalTrack { private double time; double tMin; double tMax; - static final boolean debug = false; + private static final boolean debug = false; private KalmanParams kPar; private double chi2incVtx; private static DMatrixRMaj tempV; @@ -118,8 +118,8 @@ public class KalTrack { this.SiteList = new ArrayList(SiteList.size()); for (int idx = firstSite; idx <= lastSite; ++idx) { MeasurementSite site = SiteList.get(idx); - if (site.aS == null) { // This should never happen - logger.log(Level.SEVERE, String.format("Event %d: site of track %d is missing smoothed state vector for layer %d detector %d", + if (site.aS == null && site.aF == null) { // This should never happen + logger.log(Level.SEVERE, String.format("Event %d: site of track %d is missing a state vector for layer %d detector %d", eventNumber, ID, site.m.Layer, site.m.detector)); logger.log(Level.WARNING, site.toString("bad site")); bad = true; @@ -169,7 +169,9 @@ public class KalTrack { tMax = Math.max(tMax, site.m.hits.get(site.hitID).time); this.chi2 += site.chi2inc; if (debug) { - System.out.format(" Layer %d, chi^2 increment=%10.5f, a=%s\n", site.m.Layer, site.chi2inc, site.aS.helix.a.toString()); + StateVector aA = site.aS; + if (aA == null) aA = site.aF; + System.out.format(" Layer %d, chi^2 increment=%10.5f, a=%s\n", site.m.Layer, site.chi2inc, aA.helix.a.toString()); } } time = time / (double) nHits; @@ -260,9 +262,12 @@ public Map interceptMomVects() { */ public double[] moduleIntercept(SiModule mod, double[] rGbl) { HelixState hx = null; + StateVector aA = null; for (MeasurementSite site : SiteList) { + if (site.aS == null) aA = site.aF; + else aA = site.aS; if (site.m == mod) { - hx = site.aS.helix; + hx = aA.helix; } } if (hx == null) { @@ -273,12 +278,17 @@ public double[] moduleIntercept(SiModule mod, double[] rGbl) { } if (site.m.Layer > mxLayer) { mxLayer = site.m.Layer; - hx = site.aS.helix; + if (site.aS == null) aA = site.aF; + else aA = site.aS; + hx = aA.helix; } } } if (hx == null) { - hx = SiteList.get(0).aS.helix; + MeasurementSite site = SiteList.get(0); + if (site.aS == null) aA = site.aF; + else aA = site.aS; + hx = aA.helix; } double phiS = hx.planeIntersect(mod.p); if (Double.isNaN(phiS)) { @@ -474,6 +484,7 @@ public Pair unbiasedResidual(MeasurementSite site, boolean eCons Vec aStar = null; if (site.hitID >= 0) { StateVector aA = site.aS; + if (aA == null) aA = site.aF; double sigma = site.m.hits.get(site.hitID).sigma; DMatrixRMaj Cstar = new DMatrixRMaj(5, 5); aStar = aA.inverseFilter(site.H, sigma * sigma, Cstar); @@ -556,8 +567,13 @@ public void print(String s) { } System.out.format(" Magnetic field magnitude = %10.5f and direction = %s\n", Bmag, tB.toString()); MeasurementSite site0 = this.SiteList.get(0); - if (site0.aS != null) { - System.out.format(" Helix at layer %d: %s, pivot=%s\n", site0.m.Layer, site0.aS.helix.a.toString(), site0.aS.helix.X0.toString()); + StateVector aA = site0.aS; + if (aA == null) aA = site0.aF; + if (aA != null) { + double tanL = aA.helix.a.v[4]; + double K = aA.helix.a.v[2]; + double energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); + System.out.format(" Helix at layer %d: %s, pivot=%s, E=%9.4f\n", site0.m.Layer, aA.helix.a.toString(), aA.helix.X0.toString(), energy); } for (int i = 0; i < SiteList.size(); i++) { MeasurementSite site = SiteList.get(i); @@ -566,11 +582,13 @@ public void print(String s) { System.out.format(" Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f ", m.Layer, m.detector, m.isStereo, site.chi2inc); if (hitID >= 0) { + if (site.aS == null) aA = site.aF; + else aA = site.aS; System.out.format(" t=%5.1f ", site.m.hits.get(site.hitID).time); - double residual = site.m.hits.get(hitID).v - site.aS.mPred; + double residual = site.m.hits.get(hitID).v - aA.mPred; Pair unBiasedResid = unbiasedResidual(site, false); double[] lclint = moduleIntercept(m, null); - System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, site.aS.mPred, + System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, aA.mPred, residual, unBiasedResid.getFirstElement(), lclint[0], m.xExtent[1]); } else { System.out.format("\n"); @@ -1774,10 +1792,10 @@ void energyConstraint(double E, double sigmaE) { // smoothing back to the first tracker site can then proceed as usual. int idxLast = SiteList.size() - 1; MeasurementSite lastSite = SiteList.get(idxLast); - HelixState energyConstrainedHelix = lastSite.aS.helix.energyConstrained(E, sigmaE); - //System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aS.helix.a.v[2],energyConstrainedHelix.a.v[2]); + HelixState energyConstrainedHelix = lastSite.aF.helix.energyConstrained(E, sigmaE); + if (debug) System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aF.helix.a.v[2],energyConstrainedHelix.a.v[2]); lastSite.energyConstrained = true; - lastSite.aS = new StateVector(lastSite.aS.kLow, lastSite.aS.uniformB); + lastSite.aS = new StateVector(lastSite.aF.kLow, lastSite.aF.uniformB); lastSite.aS.helix = energyConstrainedHelix; lastSite.aS.kUp = lastSite.aF.kUp; lastSite.aS.F = lastSite.aF.F; // Don't deep copy the F matrix @@ -1791,7 +1809,7 @@ void energyConstraint(double E, double sigmaE) { double tanl = energyConstrainedHelix.a.v[4]; double ePredict = FastMath.sqrt(1.0 + tanl * tanl) / Math.abs(kappa); double chi = (E - ePredict) / sigmaE; - //System.out.format("KalTrack:energyConstraint E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n",E,ePredict,sigmaE,chi); + if (debug) System.out.format("KalTrack:energyConstraint E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n",E,ePredict,sigmaE,chi); this.chi2_Econstraint = lastSite.chi2incE + chi * chi; MeasurementSite nS = lastSite; for (int idx = idxLast - 1; idx >= 0; --idx) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index f6345d8efb..38064f4f20 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -1651,10 +1651,17 @@ public Track refitTrackWithE(Track track, double energy) { double [] helixParams = null; Vec kalHelixParams = null; Vec pivot = new Vec(0., 0., 0.); + boolean debug = true; + if (debug) { + System.out.format("Entering refitTrackWithE: energy = %10.4f\n", energy); + } if (tkrStates != null) { for (TrackState tkrState : tkrStates) { if (tkrState.getLocation() == TrackState.AtFirstHit) { theTrackState = tkrState; + if (debug) { + System.out.println("refitTrackWithE: trackstate at first hit"); + } break; } } @@ -1662,6 +1669,9 @@ public Track refitTrackWithE(Track track, double energy) { for (TrackState tkrState : tkrStates) { if (tkrState.getLocation() == TrackState.AtIP) { theTrackState = tkrState; + if (debug) { + System.out.println("refitTrackWithE: trackstate at IP"); + } break; } } @@ -1674,7 +1684,14 @@ public Track refitTrackWithE(Track track, double energy) { double alpha = 1000.0 * 1.0e9 / (c * bField); kalHelixParams = new Vec(5, KalmanInterface.unGetLCSimParams(helixParams, alpha)); double[] covHPS = theTrackState.getCovMatrix(); - DMatrixRMaj helixCov = new DMatrixRMaj(KalmanInterface.ungetLCSimCov(covHPS, alpha)); + helixCov = new DMatrixRMaj(KalmanInterface.ungetLCSimCov(covHPS, alpha)); + if (debug) { + System.out.format("refitTrackWithE: helix params = %s\n", kalHelixParams.toString("helix")); + } + } + } else { + if (debug) { + System.out.println("KalmanInterface:refitTrackWithE, no track states exist "); } } // Get the list of tracker hits on this track @@ -1682,14 +1699,13 @@ public Track refitTrackWithE(Track track, double energy) { double firstHitZ = fillMeasurements(hitsOnTrack, 0); // Do a linear fit to the track hits to get the helix parameter guesses if (theTrackState == null) { - if (debug) System.out.format("firstHitZ %f \n", firstHitZ); + if (debug) System.out.format("refitTrackWithE: firstHitZ %f \n", firstHitZ); SeedTrack seed = new SeedTrack(trackHitsKalman, firstHitZ, 0., kPar); kalHelixParams = seed.helixParams(); helixCov = seed.covariance(); pivot.v[1] = seed.yOrigin; } - - + ArrayList SiMoccupied = new ArrayList(); for (SiModule SiM : SiMlist) { if (!SiM.hits.isEmpty()) SiMoccupied.add(SiM); @@ -1698,20 +1714,27 @@ public Track refitTrackWithE(Track track, double energy) { if (debug) { for (int i = 0; i < SiMoccupied.size(); i++) { SiModule SiM = SiMoccupied.get(i); - SiM.print(String.format("SiMoccupied%d", i)); + int lyr = SiM.Layer; + int nHits = SiM.hits.size(); + if (nHits == 0) continue; + double vHit = SiM.hits.get(0).v; + double eHit = SiM.hits.get(0).sigma; + System.out.format("refitTrackWithE: layer %d stereo=%b #hits=%d m=%10.5f+-%9.5f\n", lyr, SiM.isStereo, nHits, vHit, eHit); } } int startIndex = 0; - if (debug) System.out.format("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + if (debug) System.out.format("refitTrackWithE: createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); CommonOps_DDRM.scale(10., helixCov); // Do the Kalman track fit only up through the filter step KalTrack newTrack = kalmanFilterTrack(0, track.hashCode(), SiMoccupied, null, kalHelixParams, pivot, helixCov); + if (debug) newTrack.print("filtered track"); // Include the eCal information and smooth back toward the origin double sigmaE = (kPar.eRes[0]/FastMath.sqrt(energy) + kPar.eRes[1])*energy/100.; newTrack.energyConstraint(energy, sigmaE); + if (debug) newTrack.print("energy constrainted"); // Convert the KalTrack object into and HPS Track and TrackState Track outputTrack = createTrack(newTrack, true); @@ -1783,9 +1806,16 @@ KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, sites.add(newSite); prevSite = newSite; - } - ArrayList yScat = null; - ArrayList XLscat = null; + } + boolean debug = true; + if (debug) { + for (MeasurementSite site : sites) { + System.out.format("kalmanFilterTrack: hit on layer %d\n", site.m.Layer); + } + System.out.format("kalmanFilterTrack: filter chi^2 = %9.3f\n", chi2f); + } + ArrayList yScat = new ArrayList(); + ArrayList XLscat = new ArrayList(); return new KalTrack(eventNumber, tkID, sites, yScat, XLscat, kPar); } From baf99e3ad2c4c3d6b80fa06be9d63400914c642b Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Thu, 23 Feb 2023 16:19:08 -0800 Subject: [PATCH 13/17] Refit with energy code complete, although the results are very suspect still --- .../ReconstructedParticleRefitter.java | 159 ++++++++++++++++-- .../hps/recon/tracking/kalman/KalTrack.java | 47 ++++++ .../tracking/kalman/KalmanDriverHPS.java | 6 +- .../tracking/kalman/KalmanInterface.java | 109 +++++++++--- .../recon/tracking/kalman/Measurement.java | 23 +-- 5 files changed, 289 insertions(+), 55 deletions(-) diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java index cc5ff45745..6e6d0a7108 100644 --- a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -1,9 +1,13 @@ package org.hps.recon.particle; import static java.lang.Math.abs; + +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import org.apache.commons.math.util.FastMath; import org.hps.recon.tracking.MaterialSupervisor; import org.hps.recon.tracking.MaterialSupervisor.ScatteringDetectorVolume; import org.hps.recon.tracking.MaterialSupervisor.SiStripPlane; @@ -17,10 +21,17 @@ import org.lcsim.detector.tracker.silicon.SiSensor; import org.lcsim.event.Cluster; import org.lcsim.event.EventHeader; +import org.lcsim.event.LCRelation; +import org.lcsim.event.MCParticle; import org.lcsim.event.RawTrackerHit; import org.lcsim.event.ReconstructedParticle; +import org.lcsim.event.RelationalTable; +import org.lcsim.event.SimTrackerHit; import org.lcsim.event.Track; +import org.lcsim.event.TrackState; +import org.lcsim.event.TrackerHit; import org.lcsim.event.base.BaseReconstructedParticle; +import org.lcsim.event.base.BaseRelationalTable; import org.lcsim.geometry.Detector; import org.lcsim.util.Driver; import org.lcsim.util.aida.AIDA; @@ -41,7 +52,7 @@ public class ReconstructedParticleRefitter extends Driver { * The histogram handler */ private AIDA aida = AIDA.defaultInstance(); - + private String outputFileName = "ReconPartRefit.root"; /** * The name of the input ReconstructedParticle collection to process */ @@ -69,6 +80,7 @@ public class ReconstructedParticleRefitter extends Driver { private double eRes0 = -1.0; private double eRes1 = -1.0; + private static final boolean debug = false; /** * ECal energy resolution parameterization, for the resolution as a % of E. * @param eRes0 coefficient of the 1/sqrt(E) term @@ -114,6 +126,21 @@ public void detectorChanged(Detector det) { * @param event The hps-java event header */ public void process(EventHeader event) { + if (debug) { + System.out.println("Entering process"); + if (event.hasCollection(LCRelation.class, "SVTTrueHitRelations")) { + System.out.println("SVTTrueHitRelations are present"); + } + if (event.hasCollection(SimTrackerHit.class, "TrackerHits")) { + System.out.println("Sim TrackerHits are present"); + } + if (event.hasCollection(MCParticle.class, "MCParticles")) { + System.out.println("MCParticles are present"); + } + if (event.hasCollection(LCRelation.class, "KalmanFullTracksToTruthTrackRelations")) { + System.out.println("KalmanFullTracksToTruthTrackRelations are present"); + } + } if (event.hasCollection(ReconstructedParticle.class, _finalStateParticleCollectionName)) { // setup the hit-to-sensor associations // should not need to do this if not reading in events from disk @@ -129,10 +156,53 @@ public void process(EventHeader event) { if (!rp.getClusters().isEmpty()) { // quick check on E/p so we don't try to fit to the energy of MIP tracks double eOverP = rp.getEnergy() / rp.getMomentum().magnitude(); - aida.histogram1D("e over p", 100, 0., 2.).fill(eOverP); + aida.histogram1D("e over p before refit", 100, 0., 2.).fill(eOverP); + if (debug) System.out.format("ReconstructedParticleRefitter: event %d, E/P=%10.5f, cut=%10.5f\n", event.getEventNumber(), eOverP, _eOverpCut); if (abs(eOverP - 1.0) < _eOverpCut) { + // Get the old track info + Track oldTrack = rp.getTracks().get(0); + TrackState oldTsAtIP = null; + for (TrackState ts : oldTrack.getTrackStates()) { + if (ts.getLocation() == TrackState.AtIP) { + oldTsAtIP = ts; + break; + } + } + if (oldTsAtIP == null) oldTsAtIP = oldTrack.getTrackStates().get(0); + double[] oldParams = oldTsAtIP.getParameters(); // create a new ReconstructedParticle here... - refitReconstructedParticles.add(makeNewReconstructedParticle(rp)); + ReconstructedParticle refitParticle = makeNewReconstructedParticle(event, rp); + refitReconstructedParticles.add(refitParticle); + Track newTrack = refitParticle.getTracks().get(0); + TrackState newTsAtIP = null; + for (TrackState ts : newTrack.getTrackStates()) { + if (ts.getLocation() == TrackState.AtIP) { + newTsAtIP = ts; + break; + } + } + if (newTsAtIP == null) newTsAtIP = newTrack.getTrackStates().get(0); + double[] newParams = newTsAtIP.getParameters(); + for (int i=0; i<5; ++i) { + aida.histogram1D(String.format("Helix parameter %d new minus old over old", i), 100, -0.5, 0.5).fill((newParams[i]-oldParams[i])/oldParams[i]); + } + aida.histogram1D("old track chi2/dof", 100, 0., 50.).fill(oldTrack.getChi2()/oldTrack.getNDF()); + aida.histogram1D("new track chi2/dof", 100, 0., 50.).fill(newTrack.getChi2()/newTrack.getNDF()); + double [] P = newTsAtIP.getMomentum(); + double pMag = FastMath.sqrt(P[0]*P[0]+P[1]*P[1]+P[2]*P[2]); + double changeInP = pMag/rp.getMomentum().magnitude(); + aida.histogram1D("new momentum over old momentum", 100, 0.5, 1.5).fill(changeInP); + aida.histogram1D("new particle E over p", 100, 0., 2.).fill(rp.getEnergy()/pMag); + if (debug) System.out.format("ReconstructedParticleRefitter: Event %d, eOverP=%10.4f, changeInP=%10.4f\n", + event.getEventNumber(), eOverP, changeInP); + MCParticle theMatch = getMCmatch(event, rp); + if (theMatch != null) { + double eMC = theMatch.getEnergy(); + double newPoverEMC = pMag/eMC; + if (debug) System.out.format(" MC match: p/E MC = %10.4f\n", newPoverEMC); + aida.histogram1D("new momentum over MC energy", 100, 0.5, 1.5).fill(newPoverEMC); + aida.histogram1D("old momentum over MC energy", 100, 0.5, 1.5).fill(rp.getMomentum().magnitude()/eMC); + } } else { refitReconstructedParticles.add(rp); } @@ -146,6 +216,7 @@ public void process(EventHeader event) { // add the new collection to the event event.put(_refitParticleCollectionName, refitReconstructedParticles, ReconstructedParticle.class, 0); } + KI.clearInterface(); } /** @@ -156,20 +227,24 @@ public void process(EventHeader event) { * @param rp the ReconstructedParticle to refit * @return */ - private ReconstructedParticle makeNewReconstructedParticle(ReconstructedParticle rp) { + private ReconstructedParticle makeNewReconstructedParticle(EventHeader event, ReconstructedParticle rp) { // Create a reconstructed particle to represent the track. ReconstructedParticle particle = new BaseReconstructedParticle(); Cluster cluster = rp.getClusters().get(0); // refit the track with the cluster energy - Track newTrack = refitTrack(rp); + Track newTrack = refitTrack(event, rp); // Store the track in the particle. - particle.addTrack(newTrack); + particle.addTrack(newTrack); + if (debug) { + double [] trackP = newTrack.getTrackStates().get(0).getMomentum(); + double P=FastMath.sqrt(trackP[0]*trackP[0]+trackP[1]*trackP[1]+trackP[2]*trackP[2]); + System.out.format("makeNewReconstructedParticle: track p=%10.4f\n", P); + } - // Set the type of the particle. This is used to identify - // the tracking strategy used in finding the track associated with - // this particle. - // Modify this to flag that we have refit with the energy - // for now, just add 1000 + // Set the type of the particle. This is used to identify the tracking + // strategy used in finding the track associated with this particle. + // Modify this to flag that we have refit with the energy. + // For now, just add 1000 ((BaseReconstructedParticle) particle).setType(1000 + newTrack.getType()); ((BaseReconstructedParticle) particle).setParticleIdUsed(new SimpleParticleID(rp.getParticleIDUsed().getPDG(), 0, 0, 0)); // add cluster to the particle: @@ -181,20 +256,65 @@ private ReconstructedParticle makeNewReconstructedParticle(ReconstructedParticle return particle; } + private MCParticle getMCmatch(EventHeader event, ReconstructedParticle rp) { + MCParticle theMatch = null; + RelationalTable rawtomc = null; + if (event.hasCollection(LCRelation.class, "SVTTrueHitRelations")) { + rawtomc = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); + List trueHitRelations = event.get(LCRelation.class, "SVTTrueHitRelations"); + for (LCRelation relation : trueHitRelations) { + if (relation != null && relation.getFrom() != null && relation.getTo() != null) { + rawtomc.add(relation.getFrom(), relation.getTo()); + } + } + } else { + return theMatch; + } + if (debug) System.out.println("getMCmatch: relation table constructed."); + ArrayList pMC = new ArrayList(1); + ArrayList cnt = new ArrayList(1); + Track track = rp.getTracks().get(0); + List hitsOnTrack = track.getTrackerHits(); + for (TrackerHit hit : hitsOnTrack) { + List rawHits = hit.getRawHits(); + for (RawTrackerHit rawHit : rawHits) { + Set simHits = rawtomc.allFrom(rawHit); + for (SimTrackerHit simHit : simHits) { + MCParticle mcp = simHit.getMCParticle(); + if (!pMC.contains(mcp)) { + pMC.add(mcp); + cnt.add(1); + } else { + int idx = pMC.indexOf(mcp); + cnt.set(idx,cnt.get(idx)+1); + } + } + } + } + if (debug) System.out.format("getMCmatch: %d MC matches found\n", pMC.size()); + int maxCnt = 0; + for (int i=0; i maxCnt) { + maxCnt = cnt.get(i); + theMatch = pMC.get(i); + } + } + return theMatch; + } /** - * Method stub for refitting a track + * Method for refitting a track * * @param rp The input ReconstructedParticle with track and matching cluster * @return the newly refit track including the cluster energy */ - private Track refitTrack(ReconstructedParticle rp) { + private Track refitTrack(EventHeader event, ReconstructedParticle rp) { Track track = rp.getTracks().get(0); Cluster cluster = rp.getClusters().get(0); //the energy of the associated cluster double energy = cluster.getEnergy(); // Fit a new track with this list of hits and the cluster energy. - return KI.refitTrackWithE(track, energy); + return KI.refitTrackWithE(event, track, energy); } /** @@ -264,4 +384,15 @@ public void setRefitParticleCollectionName(String s) { public void set_eOverpCut(double d) { _eOverpCut = d; } + + public void endOfData() { + System.out.format("ReconstructedParticleRefitter: end-of-data reached.\n"); + try { + System.out.format("Outputting the aida histograms now to file %s\n", outputFileName); + aida.saveAs(outputFileName); + } catch (IOException ex) { + System.out.println("ReconstructedParticleRefitter: exception when writing out histograms"); + } + KI.summary(); + } } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 8b1208a271..662cfb3ace 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -7,7 +7,9 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +//import java.util.List; import java.util.Map; +//import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -18,6 +20,7 @@ import org.ejml.dense.row.factory.LinearSolverFactory_DDRM; import org.ejml.interfaces.linsol.LinearSolverDense; import org.hps.util.Pair; +import org.lcsim.event.MCParticle; /** * Track followed and fitted by the Kalman filter @@ -113,6 +116,12 @@ public class KalTrack { break; } } + if (firstSite < 0 || lastSite == 999) { + firstSite = 0; + lastSite = SiteList.size() - 1; + logger.log(Level.WARNING, String.format("Event %d: no sites on track %d. Number of sites=%d", + eventNumber, ID, SiteList.size())); + } // Make a new list of sites, without empty sites at beginning or end this.SiteList = new ArrayList(SiteList.size()); @@ -1858,6 +1867,44 @@ void energyConstraint(double E, double sigmaE) { energyConstrained = true; } + /** + * Return the best matched MC particle for this track + * @return best match MCParticle + */ + MCParticle getBestMCmatch() { + MCParticle bestMC = null; + ArrayList mcParts = new ArrayList(); + ArrayList mcCnt = new ArrayList(); + for (MeasurementSite site : SiteList) { + StateVector aS = site.aS; + SiModule mod = site.m; + if (aS != null && mod != null) { + if (site.hitID >= 0) { + Measurement hit = mod.hits.get(site.hitID); + if (hit.pMC != null) { + for (MCParticle mcp : hit.pMC) { + if (mcParts.contains(mcp)) { + int id = mcParts.indexOf(mcp); + mcCnt.set(id, mcCnt.get(id)+1); + } else { + mcParts.add(mcp); + mcCnt.add(1); + } + } + } + } + } + } + int maxCnt = 0; + for (int i=0; i maxCnt) { + maxCnt = mcCnt.get(i); + bestMC = mcParts.get(i); + } + } + return bestMC; + } + /** * Derivative matrix for propagating the covariance of the helix parameters * to a covariance of momentum diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java index 07260d4c26..3c1a76bc29 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java @@ -261,7 +261,7 @@ public void process(EventHeader event) { if (createSeed) { // Start with the linear helix fit if (verbose) System.out.println("Use a linear helix fit to get a starting guess for the Kalman filter\n"); - SeedTrack seedKalmanTrack = KI.createKalmanSeedTrack(trk, hitToStrips, hitToRotated); + SeedTrack seedKalmanTrack = KI.createKalmanSeedTrack(event, trk, hitToStrips, hitToRotated); if (verbose) { System.out.println("\nPrinting info for Kalman SeedTrack:"); seedKalmanTrack.print("testKalmanTrack"); @@ -278,7 +278,7 @@ public void process(EventHeader event) { outputSeedTracks.add(HPStrk); //full track - ktf2 = KI.createKalmanTrackFit(evtNumb, seedKalmanTrack, trk, hitToStrips, hitToRotated, 2); + ktf2 = KI.createKalmanTrackFit(event, seedKalmanTrack, trk, hitToStrips, hitToRotated, 2); if (!ktf2.success) { KI.clearInterface(); continue; @@ -344,7 +344,7 @@ public void process(EventHeader event) { cov.print("GBL covariance for starting Kalman fit"); } //full track - ktf2 = KI.createKalmanTrackFit(evtNumb, kalParams, newPivot, cov, trk, hitToStrips, hitToRotated, 2); + ktf2 = KI.createKalmanTrackFit(event, kalParams, newPivot, cov, trk, hitToStrips, hitToRotated, 2); if (!ktf2.success) { KI.clearInterface(); continue; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 38064f4f20..952e15a6e0 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -253,7 +253,7 @@ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { SeedTrackLayers.add(3); SeedTrackLayers.add(4); SeedTrackLayers.add(5); - + if (kPar.uniformB) { logger.log(Level.WARNING, "KalmanInterface WARNING: the magnetic field is set to a uniform value."); } @@ -510,7 +510,6 @@ public static double[] hpsHelixIntersect(double[] x, double[] p, double Q, doubl * @return array of 5 LCSIM helix parameters */ static double[] toHPShelix(HelixState helixState, Plane pln, double alphaCenter, double[] covHPS, double[] position) { - final boolean debug = false; Vec finalHelixParams = null; Vec pivotGlobal = null; DMatrixRMaj F = new DMatrixRMaj(5, 5); @@ -794,14 +793,17 @@ public PropagatedTrackState propagateTrackState(TrackState stateHPS, double[] lo * @return HPS track */ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { + if (kT.SiteList == null) { logger.log(Level.WARNING, "KalmanInterface.createTrack: Kalman track is incomplete."); + if (debug) System.out.format("createTrack: track %d is incomplete\n", kT.ID); return null; } if (kT.covNaN()) { logger.log(Level.FINE, "KalmanInterface.createTrack: Kalman track has NaN cov matrix."); return null; } + if (debug) System.out.format("Entering createTrack for track %d\n", kT.ID); kT.sortSites(true); BaseTrack newTrack = new BaseTrack(); @@ -832,17 +834,18 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { } newTrack.addHit(getHpsHit(site.m.hits.get(site.hitID))); } - //System.out.printf("PF::Debug::newTrack site size %d \n",newTrack.getTrackerHits().size()); + if (debug) System.out.printf("createTrack: newTrack site size %d \n",newTrack.getTrackerHits().size()); // Get the track states at each layer for (int i = 0; i < kT.SiteList.size(); i++) { MeasurementSite site = kT.SiteList.get(i); ts = null; int loc = TrackState.AtOther; - - //HpsSiSensor hssd = (HpsSiSensor) moduleMap.get(site.m).getSensor(); - //int lay = hssd.getMillepedeId(); - // System.out.printf("ssp id %d \n", hssd.getMillepedeId()); + if (debug) { + HpsSiSensor hssd = (HpsSiSensor) moduleMap.get(site.m).getSensor(); + int lay = hssd.getMillepedeId(); + System.out.printf("createTrack: ssp id %d in layer %d\n", hssd.getMillepedeId(), lay); + } if (i == 0) { loc = TrackState.AtFirstHit; } else if (i == kT.SiteList.size() - 1) { @@ -1393,7 +1396,7 @@ private boolean fillAllMeasurements(EventHeader event) { // Add MC truth information to each hit if it is available if (event.hasCollection(LCRelation.class, "SVTTrueHitRelations")) { RelationalTable rawtomc = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); - + //if (debug) System.out.println("SVTTrueHitRelations found"); List trueHitRelations = event.get(LCRelation.class, "SVTTrueHitRelations"); for (LCRelation relation : trueHitRelations) { if (relation != null && relation.getFrom() != null && relation.getTo() != null) { @@ -1402,7 +1405,7 @@ private boolean fillAllMeasurements(EventHeader event) { } for (SiModule mod : SiMlist) { for (Measurement hit : mod.hits) { - hit.tksMC = new ArrayList(); + hit.pMC = new ArrayList(1); TrackerHit hpsHit = getHpsHit(hit); List rawHits = hpsHit.getRawHits(); for (RawTrackerHit rawHit : rawHits) { @@ -1410,10 +1413,12 @@ private boolean fillAllMeasurements(EventHeader event) { for (SimTrackerHit simHit : simHits) { if (hit.rGlobal == null) { hit.rGlobal = vectorGlbToKalman(simHit.getPosition()); + hit.vTrue = mod.toLocal(hit.rGlobal).v[1]; } MCParticle mcp = simHit.getMCParticle(); - if (!hit.tksMC.contains(mcp.hashCode())) { - hit.tksMC.add(mcp.hashCode()); + if (!hit.pMC.contains(mcp)) { + hit.pMC.add(mcp); + //if (debug) System.out.println("adding MC particle to hit"); } } } @@ -1432,8 +1437,10 @@ private boolean fillAllMeasurements(EventHeader event) { * already having hits * @return */ - private double fillMeasurements(List hits1D, int addMode) { + private double fillMeasurements(EventHeader event, List hits1D, int addMode) { double firstZ = 10000; + //boolean debug = true; + boolean hasMC = false; Map> hitsMap = new HashMap>(); for (TrackerHit hit1D : hits1D) { @@ -1458,6 +1465,20 @@ private double fillMeasurements(List hits1D, int addMode) { hitsMap.put(temp, hitsInLayer); } + // Add MC truth information to each hit if it is available + RelationalTable rawtomc = null; + if (event.hasCollection(LCRelation.class, "SVTTrueHitRelations")) { + rawtomc = new BaseRelationalTable(RelationalTable.Mode.MANY_TO_MANY, RelationalTable.Weighting.UNWEIGHTED); + if (debug) System.out.println("KalmanInterface.fillMeasurements: SVTTrueHitRelations found"); + List trueHitRelations = event.get(LCRelation.class, "SVTTrueHitRelations"); + for (LCRelation relation : trueHitRelations) { + if (relation != null && relation.getFrom() != null && relation.getTo() != null) { + rawtomc.add(relation.getFrom(), relation.getTo()); + } + } + hasMC = true; + } + for (SiModule mod : SiMlist) { SiStripPlane plane = moduleMap.get(mod); if (!hitsMap.containsKey(plane.getSensor())) { @@ -1489,7 +1510,8 @@ private double fillMeasurements(List hits1D, int addMode) { } if (debug) { - System.out.format("\nKalmanInterface:fillMeasurements Measurement %d, the measurement uncertainty is set to %10.7f\n", i, + System.out.format("\nKalmanInterface:fillMeasurements, Event %d, Layer %d\n",event.getEventNumber(),mod.Layer); + System.out.format("KalmanInterface:fillMeasurements Measurement %d, the measurement uncertainty is set to %10.7f\n", i, du); System.out.printf("Filling SiMod: %s \n", plane.getName()); System.out.printf("HPSplane MeasuredCoord %s UnmeasuredCoord %s Normal %s umeas %f\n", @@ -1504,7 +1526,25 @@ private double fillMeasurements(List hits1D, int addMode) { globalY.print("globalY"); } Measurement m = new Measurement(umeas, xStrip, du, 0., hit.getdEdx() * 1000000.); - + if (hasMC) { + m.pMC = new ArrayList(1); + List rawHits = hit.getRawHits(); + for (RawTrackerHit rawHit : rawHits) { + Set simHits = rawtomc.allFrom(rawHit); + for (SimTrackerHit simHit : simHits) { + if (m.rGlobal == null) { + m.rGlobal = vectorGlbToKalman(simHit.getPosition()); + m.vTrue = mod.toLocal(m.rGlobal).v[1]; + } + MCParticle mcp = simHit.getMCParticle(); + if (!m.pMC.contains(mcp)) { + m.pMC.add(mcp); + if (debug) System.out.println("KalmanInterface.fillMeasurements: adding MC particle to hit"); + } + } + } + } + KalHit hitPair = new KalHit(mod, m); trackHitsKalman.add(hitPair); mod.addMeasurement(m); @@ -1514,6 +1554,7 @@ private double fillMeasurements(List hits1D, int addMode) { if (debug) { mod.print("SiModule-filled"); } + } return firstZ; } @@ -1527,9 +1568,9 @@ private double fillMeasurements(List hits1D, int addMode) { * @param hitToRotated Relation table for GBL hits to rotated hits * @return New track seed */ - public SeedTrack createKalmanSeedTrack(Track track, RelationalTable hitToStrips, RelationalTable hitToRotated) { + public SeedTrack createKalmanSeedTrack(EventHeader event, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated) { List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); - double firstHitZ = fillMeasurements(hitsOnTrack, 0); + double firstHitZ = fillMeasurements(event, hitsOnTrack, 0); if (debug) { System.out.printf("firstHitZ %f \n", firstHitZ); } @@ -1548,9 +1589,10 @@ public SeedTrack createKalmanSeedTrack(Track track, RelationalTable hitToStrips, * @param nIt Number of iterations * @return Kalman track fit object */ - public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track track, RelationalTable hitToStrips, + public KalmanTrackFit2 createKalmanTrackFit(EventHeader event, SeedTrack seed, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated, int nIt) { double firstHitZ = 10000.; + int evtNumb = event.getEventNumber(); List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); if (debug) { System.out.format("createKalmanTrackFit: number of hits on track = %d\n", hitsOnTrack.size()); @@ -1563,7 +1605,7 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track t ArrayList SiMoccupied = new ArrayList(); int startIndex = 0; - fillMeasurements(hitsOnTrack, 1); + fillMeasurements(event, hitsOnTrack, 1); for (SiModule SiM : SiMlist) { if (!SiM.hits.isEmpty()) { SiMoccupied.add(SiM); @@ -1605,7 +1647,7 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, SeedTrack seed, Track t * @param nIt Number of Kalman fit iterations * @return Kalman track-fit object */ - public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pivot, DMatrixRMaj cov, Track track, + public KalmanTrackFit2 createKalmanTrackFit(EventHeader event, Vec helixParams, Vec pivot, DMatrixRMaj cov, Track track, RelationalTable hitToStrips, RelationalTable hitToRotated, int nIt) { List hitsOnTrack = TrackUtils.getStripHits(track, hitToStrips, hitToRotated); if (debug) { @@ -1614,7 +1656,7 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pi ArrayList SiMoccupied = new ArrayList(); - fillMeasurements(hitsOnTrack, 2); + fillMeasurements(event, hitsOnTrack, 2); for (SiModule SiM : SiMlist) { if (!SiM.hits.isEmpty()) { SiMoccupied.add(SiM); @@ -1634,6 +1676,7 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pi System.out.printf("createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); } CommonOps_DDRM.scale(10., cov); + int evtNumb = event.getEventNumber(); return new KalmanTrackFit2(evtNumb, SiMoccupied, null, startIndex, nIt, pivot, helixParams, cov, kPar, fM); } @@ -1642,16 +1685,16 @@ public KalmanTrackFit2 createKalmanTrackFit(int evtNumb, Vec helixParams, Vec pi * @param track * @return */ - public Track refitTrackWithE(Track track, double energy) { + public Track refitTrackWithE(EventHeader event, Track track, double energy) { // First we need initial guesses for the helix parameters and covariance. // Preferentially take them from a TrackState. If there is no TrackState, // then estimate from a linear fit to the set of hits. + //boolean debug = true; List tkrStates = track.getTrackStates(); TrackState theTrackState = null; double [] helixParams = null; Vec kalHelixParams = null; Vec pivot = new Vec(0., 0., 0.); - boolean debug = true; if (debug) { System.out.format("Entering refitTrackWithE: energy = %10.4f\n", energy); } @@ -1696,7 +1739,7 @@ public Track refitTrackWithE(Track track, double energy) { } // Get the list of tracker hits on this track List hitsOnTrack = track.getTrackerHits(); - double firstHitZ = fillMeasurements(hitsOnTrack, 0); + double firstHitZ = fillMeasurements(event, hitsOnTrack, 0); // Do a linear fit to the track hits to get the helix parameter guesses if (theTrackState == null) { if (debug) System.out.format("refitTrackWithE: firstHitZ %f \n", firstHitZ); @@ -1723,23 +1766,36 @@ public Track refitTrackWithE(Track track, double energy) { } } + if (SiMoccupied.size() < 6) { + System.out.format("refitTrackWithE: only %d hits on track, and we need at least 6\n", SiMoccupied.size()); + return track; + } + int startIndex = 0; - if (debug) System.out.format("refitTrackWithE: createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + System.out.format("refitTrackWithE: createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); CommonOps_DDRM.scale(10., helixCov); // Do the Kalman track fit only up through the filter step KalTrack newTrack = kalmanFilterTrack(0, track.hashCode(), SiMoccupied, null, kalHelixParams, pivot, helixCov); if (debug) newTrack.print("filtered track"); + if (newTrack.bad || newTrack.nHits < 6) { + System.out.format("refitTrackWithE: bad filter of track %d, only %d hits\n", newTrack.ID, newTrack.nHits); + return track; + } + // Include the eCal information and smooth back toward the origin double sigmaE = (kPar.eRes[0]/FastMath.sqrt(energy) + kPar.eRes[1])*energy/100.; newTrack.energyConstraint(energy, sigmaE); - if (debug) newTrack.print("energy constrainted"); + if (debug) newTrack.print("energy constrained"); // Convert the KalTrack object into and HPS Track and TrackState Track outputTrack = createTrack(newTrack, true); trackHitsKalman.clear(); + for (SiModule SiM : SiMlist) { + SiM.hits.clear(); + } return outputTrack; } @@ -1807,7 +1863,6 @@ KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, sites.add(newSite); prevSite = newSite; } - boolean debug = true; if (debug) { for (MeasurementSite site : sites) { System.out.format("kalmanFilterTrack: hit on layer %d\n", site.m.Layer); @@ -2209,8 +2264,6 @@ public void plotGBLtracks(String path, EventHeader event) { * be plotted */ public void plotKalmanEvent(String path, EventHeader event, ArrayList[] patRecList) { - - boolean debug = false; PrintWriter printWriter3 = null; int eventNumber = event.getEventNumber(); String fn = String.format("%shelix3_%d.gp", path, eventNumber); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java index 4dd869c19a..5d24404259 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/Measurement.java @@ -2,6 +2,8 @@ import java.util.ArrayList; +import org.lcsim.event.MCParticle; + /** * Holds a single silicon-strip measurement (single-sided), to interface with the Kalman fit */ @@ -14,7 +16,8 @@ class Measurement { // double vTrue; // MC truth measurement value Vec rGlobal; // Global MC truth ArrayList tracks; // Tracks that this hit lies on - ArrayList tksMC; // MC tracks that contributed to this hit + ArrayList tksMC; // MC track IDs that contributed to this hit (for stand-alone test program) + ArrayList pMC; // List of hps-java MCparticle objects that contributed to this hit /** * Constructor with no MC truth info stored @@ -29,6 +32,7 @@ class Measurement { // vTrue = 0.; rGlobal = null; tksMC = null; + pMC = null; } /** @@ -51,6 +55,7 @@ class Measurement { // this.vTrue = vTrue; tracks = new ArrayList(); tksMC = new ArrayList(); + pMC = null; } /** @@ -66,15 +71,7 @@ void addMC(int idx) { * @param s Arbitrary string for the user's reference */ void print(String s) { - System.out.format("Measurement %s: Measurement value=%10.5f+-%8.6f; xStrip=%7.2f, MC truth=%10.5f; t=%8.3f; E=%8.3f", s, v, sigma, x, vTrue, time, energy); - if (tracks.size() == 0) { - System.out.format(" Not on any track.\n"); - } else { - System.out.format(" Tracks: "); - for (KalTrack tk : tracks) System.out.format(" %d ", tk.ID); - System.out.format("\n"); - } - if (rGlobal != null) rGlobal.print("global location from MC truth"); + System.out.format("%s", toString(s)); } /** @@ -91,6 +88,12 @@ String toString(String s) { str = str + String.format("\n"); } if (rGlobal != null) str = str + rGlobal.toString("global location from MC truth"); + if (pMC != null) { + str = str + "\n"; + for (MCParticle mcp : pMC) { + str = str + String.format(" MC particle type %d, E=%9.5f\n", mcp.getPDGID(),mcp.getEnergy()); + } + } return str; } } From 222b4576bb2bec91c8372d2263fa322030830989 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Thu, 2 Mar 2023 19:35:40 -0800 Subject: [PATCH 14/17] Fixed a bug, and now it even seems to work --- .../ReconstructedParticleRefitter.java | 9 ++++++-- .../hps/recon/tracking/kalman/KalTrack.java | 22 ++++++++++++++----- .../tracking/kalman/KalmanInterface.java | 17 +++++++++----- .../recon/tracking/kalman/KalmanParams.java | 2 +- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java index 6e6d0a7108..e712d4951c 100644 --- a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -186,8 +186,8 @@ public void process(EventHeader event) { for (int i=0; i<5; ++i) { aida.histogram1D(String.format("Helix parameter %d new minus old over old", i), 100, -0.5, 0.5).fill((newParams[i]-oldParams[i])/oldParams[i]); } - aida.histogram1D("old track chi2/dof", 100, 0., 50.).fill(oldTrack.getChi2()/oldTrack.getNDF()); - aida.histogram1D("new track chi2/dof", 100, 0., 50.).fill(newTrack.getChi2()/newTrack.getNDF()); + aida.histogram1D("old track chi2 per dof", 100, 0., 50.).fill(oldTrack.getChi2()/oldTrack.getNDF()); + aida.histogram1D("new track chi2 per dof", 100, 0., 50.).fill(newTrack.getChi2()/newTrack.getNDF()); double [] P = newTsAtIP.getMomentum(); double pMag = FastMath.sqrt(P[0]*P[0]+P[1]*P[1]+P[2]*P[2]); double changeInP = pMag/rp.getMomentum().magnitude(); @@ -202,6 +202,7 @@ public void process(EventHeader event) { if (debug) System.out.format(" MC match: p/E MC = %10.4f\n", newPoverEMC); aida.histogram1D("new momentum over MC energy", 100, 0.5, 1.5).fill(newPoverEMC); aida.histogram1D("old momentum over MC energy", 100, 0.5, 1.5).fill(rp.getMomentum().magnitude()/eMC); + aida.histogram1D("ECAL over MC energy", 100, 0.5, 1.5).fill(rp.getEnergy()/eMC); } } else { refitReconstructedParticles.add(rp); @@ -230,6 +231,7 @@ public void process(EventHeader event) { private ReconstructedParticle makeNewReconstructedParticle(EventHeader event, ReconstructedParticle rp) { // Create a reconstructed particle to represent the track. ReconstructedParticle particle = new BaseReconstructedParticle(); + if (debug) System.out.format("Entering makeNewReconstructedParticle for event %d\n", event.getEventNumber()); Cluster cluster = rp.getClusters().get(0); // refit the track with the cluster energy Track newTrack = refitTrack(event, rp); @@ -312,6 +314,9 @@ private Track refitTrack(EventHeader event, ReconstructedParticle rp) { Cluster cluster = rp.getClusters().get(0); //the energy of the associated cluster double energy = cluster.getEnergy(); + int nHits = track.getTrackerHits().size(); + if (debug) System.out.format("refitTrack in event %d with %d hits, chi2=%8.3f for %d dof, cluster E=%9.4f\n", + event.getEventNumber(), nHits, track.getChi2(), track.getNDF(), energy); // Fit a new track with this list of hits and the cluster energy. return KI.refitTrackWithE(event, track, energy); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 662cfb3ace..5793016953 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -1799,13 +1799,19 @@ void energyConstraint(double E, double sigmaE) { // so the smoothed state vector with energy constraint at the last tracker // site is exactly what is returned by the energyConstrained method. The // smoothing back to the first tracker site can then proceed as usual. + //boolean debug = true; + final boolean noConstraint = false; // for debugging purposes, should just do a normal Kalman smoothing int idxLast = SiteList.size() - 1; MeasurementSite lastSite = SiteList.get(idxLast); HelixState energyConstrainedHelix = lastSite.aF.helix.energyConstrained(E, sigmaE); if (debug) System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aF.helix.a.v[2],energyConstrainedHelix.a.v[2]); - lastSite.energyConstrained = true; - lastSite.aS = new StateVector(lastSite.aF.kLow, lastSite.aF.uniformB); - lastSite.aS.helix = energyConstrainedHelix; + lastSite.energyConstrained = !noConstraint; + lastSite.aS = new StateVector(lastSite.aF.kLow, lastSite.aF.uniformB); // Blank new state vector + if (noConstraint) { + lastSite.aS.helix = lastSite.aF.helix; + } else { + lastSite.aS.helix = energyConstrainedHelix; + } lastSite.aS.kUp = lastSite.aF.kUp; lastSite.aS.F = lastSite.aF.F; // Don't deep copy the F matrix lastSite.aS.mPred = lastSite.aF.mPred; @@ -1819,13 +1825,17 @@ void energyConstraint(double E, double sigmaE) { double ePredict = FastMath.sqrt(1.0 + tanl * tanl) / Math.abs(kappa); double chi = (E - ePredict) / sigmaE; if (debug) System.out.format("KalTrack:energyConstraint E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n",E,ePredict,sigmaE,chi); - this.chi2_Econstraint = lastSite.chi2incE + chi * chi; + if (noConstraint) { + this.chi2_Econstraint = lastSite.chi2incE; + } else { + this.chi2_Econstraint = lastSite.chi2incE + chi * chi; + } MeasurementSite nS = lastSite; for (int idx = idxLast - 1; idx >= 0; --idx) { MeasurementSite thisSite = SiteList.get(idx); thisSite.aS = thisSite.aF.smooth(nS.aS, nS.aP); if (thisSite.hitID < 0) { - thisSite.energyConstrained = true; + thisSite.energyConstrained = !noConstraint; continue; } Measurement hit = thisSite.m.hits.get(thisSite.hitID); @@ -1854,7 +1864,7 @@ void energyConstraint(double E, double sigmaE) { thisSite.chi2incE = (thisSite.aS.r * thisSite.aS.r) / thisSite.aS.R; this.chi2_Econstraint += thisSite.chi2incE; - thisSite.energyConstrained = true; + thisSite.energyConstrained = !noConstraint; nS = thisSite; } Vec beamSpot = new Vec(3, kPar.beamSpot); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 952e15a6e0..80f7a216d9 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -1434,7 +1434,7 @@ private boolean fillAllMeasurements(EventHeader event) { * * @param hits1D 1-dimensional tracker hits * @param addMode 0 to skip layers not already having hits; 1 to skip layers - * already having hits + * already having hits; 2 not to skip * @return */ private double fillMeasurements(EventHeader event, List hits1D, int addMode) { @@ -1442,7 +1442,8 @@ private double fillMeasurements(EventHeader event, List hits1D, int //boolean debug = true; boolean hasMC = false; Map> hitsMap = new HashMap>(); - + if (debug) System.out.format("Entering fillMeasurements in event %d for %d hits, addmode %d\n", + event.getEventNumber(), hits1D.size(), addMode); for (TrackerHit hit1D : hits1D) { HpsSiSensor temp = ((HpsSiSensor) ((RawTrackerHit) hit1D.getRawHits().get(0)).getDetectorElement()); int lay = temp.getLayerNumber(); @@ -1729,7 +1730,9 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { double[] covHPS = theTrackState.getCovMatrix(); helixCov = new DMatrixRMaj(KalmanInterface.ungetLCSimCov(covHPS, alpha)); if (debug) { - System.out.format("refitTrackWithE: helix params = %s\n", kalHelixParams.toString("helix")); + System.out.format("refitTrackWithE: LCSim helix params = %9.5f %9.5f %9.5f %9.5f %9.5f\n", + helixParams[0], helixParams[1], helixParams[2], helixParams[3], helixParams[4]); + System.out.format("refitTrackWithE: Kalman helix params = %s\n", kalHelixParams.toString("helix")); } } } else { @@ -1739,7 +1742,7 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { } // Get the list of tracker hits on this track List hitsOnTrack = track.getTrackerHits(); - double firstHitZ = fillMeasurements(event, hitsOnTrack, 0); + double firstHitZ = fillMeasurements(event, hitsOnTrack, 2); // Do a linear fit to the track hits to get the helix parameter guesses if (theTrackState == null) { if (debug) System.out.format("refitTrackWithE: firstHitZ %f \n", firstHitZ); @@ -1762,7 +1765,8 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { if (nHits == 0) continue; double vHit = SiM.hits.get(0).v; double eHit = SiM.hits.get(0).sigma; - System.out.format("refitTrackWithE: layer %d stereo=%b #hits=%d m=%10.5f+-%9.5f\n", lyr, SiM.isStereo, nHits, vHit, eHit); + double vTrue = SiM.hits.get(0).vTrue; + System.out.format("refitTrackWithE: layer %d stereo=%b #hits=%d m=%9.5f+-%8.5f vTrue=%9.5f\n", lyr, SiM.isStereo, nHits, vHit, eHit, vTrue); } } @@ -1772,7 +1776,7 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { } int startIndex = 0; - System.out.format("refitTrackWithE: createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); + if (debug) System.out.format("refitTrackWithE: createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); CommonOps_DDRM.scale(10., helixCov); // Do the Kalman track fit only up through the filter step @@ -1816,6 +1820,7 @@ KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, hits.add(0); } } + //boolean debug = true; // Create an state vector to initialize the Kalman filter Vec Bfield = null; if (kPar.uniformB) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index 0d8e4a983f..8c1529b8e6 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -173,7 +173,7 @@ public KalmanParams() { lowPhThresh = 0.25; // Residual improvement ratio necessary to use a low-ph hit instead of high-ph seedCompThr = 0.05; // Remove SeedTracks with all Helix params within relative seedCompThr . If -1 do not apply duplicate removal eRes = new double[2]; - eRes[0] = 3.0; // Cal energy resolution parameters in % sigmaE = eRes[0]/sqrt(E) + eRes[1] + eRes[0] = 10.0; // Cal energy resolution parameters in % sigmaE = eRes[0]/sqrt(E) + eRes[1] eRes[1] = 1.0; // Load the default search strategies From 0e0f7239ec167572badf5ce33ac292891ad699a1 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Mon, 1 May 2023 22:54:31 -0700 Subject: [PATCH 15/17] refit with and without E constraint seems to work now --- .../ReconstructedParticleRefitter.java | 201 +++++++++-- .../hps/recon/tracking/kalman/HelixTest3.java | 6 +- .../hps/recon/tracking/kalman/KalTrack.java | 129 +++---- .../tracking/kalman/KalmanDriverHPS.java | 2 +- .../tracking/kalman/KalmanInterface.java | 331 ++++++++++++++---- .../tracking/kalman/KalmanKinkFitDriver.java | 2 +- .../recon/tracking/kalman/KalmanParams.java | 9 +- .../tracking/kalman/KalmanPatRecDriver.java | 10 +- .../tracking/kalman/MeasurementSite.java | 5 +- .../hps/recon/tracking/kalman/SiModule.java | 2 +- 10 files changed, 521 insertions(+), 176 deletions(-) diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java index e712d4951c..aa6f472704 100644 --- a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -5,7 +5,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Random; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import org.apache.commons.math.util.FastMath; import org.hps.recon.tracking.MaterialSupervisor; @@ -13,6 +16,7 @@ import org.hps.recon.tracking.MaterialSupervisor.SiStripPlane; import org.hps.recon.tracking.kalman.KalmanInterface; import org.hps.recon.tracking.kalman.KalmanParams; +import org.hps.recon.tracking.kalman.KalmanPatRecDriver; import org.lcsim.detector.DetectorElementStore; import org.lcsim.detector.IDetectorElement; import org.lcsim.detector.identifier.IExpandedIdentifier; @@ -33,6 +37,7 @@ import org.lcsim.event.base.BaseReconstructedParticle; import org.lcsim.event.base.BaseRelationalTable; import org.lcsim.geometry.Detector; +import org.lcsim.geometry.IDDecoder; import org.lcsim.util.Driver; import org.lcsim.util.aida.AIDA; @@ -43,7 +48,7 @@ * * A new collection of ReconstructedParticles is added to the event. * - * @author Norman A. Graf + * @authors Norman A. Graf and Robert P. Johnson * */ public class ReconstructedParticleRefitter extends Driver { @@ -70,30 +75,73 @@ public class ReconstructedParticleRefitter extends Driver { * combination */ private double _eOverpCut = 0.1; + private boolean _doEconstraint = true; + private boolean _cheat = false; + private Random ran; /** * The interface to the Kalman Filter code */ private KalmanInterface KI; + private KalmanParams kPar = null; private org.lcsim.geometry.FieldMap fm; + private static Logger logger; + private Level _logLevel = Level.WARNING; + private IDDecoder decoder; - private double eRes0 = -1.0; - private double eRes1 = -1.0; + private double _eRes0 = -1.0; + private double _eRes1 = -1.0; private static final boolean debug = false; + /** + * Feature to allow setting the log level from steering + * @param logLevel + */ + public void set_logLevel(String logLevel) { + System.out.format("ReconstructedParticleRefitter: setting the logger level to %s\n", logLevel); + _logLevel = Level.parse(logLevel); + System.out.format(" logger level = %s\n", _logLevel.getName()); + } + /** + * Option to fake the ECAL info with a Gaussian random number, for testing + * @param b true to fake the ECAL info using MC truth + */ + public void set_cheat(boolean b) { + System.out.format("ReconstructedParticleRefitter: setting control parameter for using MC truth for the energy constraint to %b\n", b); + _cheat = b; + } + /** + * Control whether the ECAL energy constraint is applied to refitted tracks + * @param b true to refit with ECAL constraint + */ + public void set_doEconstraint(boolean b) { + System.out.format("ReconstructedParticleRefitter: setting control parameter for doing the ECAL constraint to %b\n", b); + _doEconstraint = b; + } + + private ArrayList layerSkip = null; + public void set_removeLayer(int lyr) { + if (layerSkip == null) { + layerSkip = new ArrayList(); + } + layerSkip.add(lyr); + System.out.format("ReconstructedParticleRefitter: remove layer %d from the fit.\n", lyr); + } /** * ECal energy resolution parameterization, for the resolution as a % of E. * @param eRes0 coefficient of the 1/sqrt(E) term */ - public void setERes0(double eRes0) { - this.eRes0 = eRes0; + public void set_eRes0(double eRes0) { + System.out.format("ReconstructedParticleRefitter: setting the eRes0 ECAL resolution parameter to %9.4f\n", eRes0); + _eRes0 = eRes0; } /** * ECal energy resolution parameterization, for the resolution as a % of E. * @param eRes1 the constant term */ - public void setERes1(double eRes1) { - this.eRes1 = eRes1; + public void set_eRes1(double eRes1) { + System.out.format("ReconstructedParticleRefitter: setting the eRes1 ECAL resolution parameter to %9.4f\n", eRes1); + _eRes1 = eRes1; } /** @@ -101,18 +149,30 @@ public void setERes1(double eRes1) { */ @Override public void detectorChanged(Detector det) { - + logger = Logger.getLogger(KalmanPatRecDriver.class.getName()); + if (_logLevel != null) { + logger.setLevel(_logLevel); + //LogManager.getLogManager().getLogger(Logger.GLOBAL_LOGGER_NAME).setLevel(_logLevel); + } + System.out.format("ReconstructedParticleRefitter: entering detectorChanged, logger level = %s\n", logger.getLevel().getName()); MaterialSupervisor materialManager; materialManager = new MaterialSupervisor(); materialManager.buildModel(det); + ran = new Random(); + if (layerSkip != null) { + for (int lyr : layerSkip) { + System.out.format("ReconstructedParticleRefitter: layer %d will be removed in track refits\n", lyr); + } + } + decoder = det.getSubdetector("Tracker").getIDDecoder(); // Instantiate the interface to the Kalman-Filter code and set up the run parameters - KalmanParams kPar = new KalmanParams(); + kPar = new KalmanParams(); // Override the default resolution parameters with numbers from the steering file - if (eRes0 > 0. || eRes1 > 0.) kPar.setEnergyRes(eRes0, eRes1); + if (_eRes0 > 0. || _eRes1 > 0.) kPar.setEnergyRes(_eRes0, _eRes1); fm = det.getFieldMap(); // The HPS magnetic field map - KI = new KalmanInterface(kPar, fm); // Instantiate the Kalman interface + KI = new KalmanInterface(kPar, det, fm); // Instantiate the Kalman interface ArrayList detPlanes = new ArrayList(); List materialVols = ((MaterialSupervisor) (materialManager)).getMaterialVolumes(); for (ScatteringDetectorVolume vol : materialVols) { @@ -154,10 +214,10 @@ public void process(EventHeader event) { if (rp.getParticleIDUsed().getPDG() != 22) { // skip particles without an associated cluster if (!rp.getClusters().isEmpty()) { - // quick check on E/p so we don't try to fit to the energy of MIP tracks + // cut on on E/p so we don't try to fit to the energy of MIP tracks double eOverP = rp.getEnergy() / rp.getMomentum().magnitude(); aida.histogram1D("e over p before refit", 100, 0., 2.).fill(eOverP); - if (debug) System.out.format("ReconstructedParticleRefitter: event %d, E/P=%10.5f, cut=%10.5f\n", event.getEventNumber(), eOverP, _eOverpCut); + if (debug) System.out.format("ReconstructedParticleRefitter: event %d, E/P=%10.5f, cut=%10.5f, E=%10.5f, p=%10.5f\n", event.getEventNumber(), eOverP, _eOverpCut, rp.getEnergy(), rp.getMomentum().magnitude()); if (abs(eOverP - 1.0) < _eOverpCut) { // Get the old track info Track oldTrack = rp.getTracks().get(0); @@ -170,8 +230,34 @@ public void process(EventHeader event) { } if (oldTsAtIP == null) oldTsAtIP = oldTrack.getTrackStates().get(0); double[] oldParams = oldTsAtIP.getParameters(); + Track track = rp.getTracks().get(0); + int nHits = track.getTrackerHits().size(); + if (debug) { + boolean skipped = false; + int lastLyr = -1; + for (TrackerHit hit : track.getTrackerHits()) { + List rawHits = hit.getRawHits(); + int Layer = -1; + for (RawTrackerHit rawHit : rawHits) { + long ID = rawHit.getCellID(); + decoder.setID(ID); + Layer = decoder.getValue("layer") - 1; + } + if (lastLyr >= 0) { + if (lastLyr != Layer - 1) skipped = true; + } + lastLyr = Layer; + } + System.out.format("event %d, %d track hits, nDOF=%d, skipped layer=%b\n", event.getEventNumber(), nHits, track.getNDF(), skipped); + } + int nDOF = nHits-5; + // create a new ReconstructedParticle here... ReconstructedParticle refitParticle = makeNewReconstructedParticle(event, rp); + if (refitParticle.getTracks().size() == 0) { + if (debug) System.out.println(" Track refit failed"); + return; + } refitReconstructedParticles.add(refitParticle); Track newTrack = refitParticle.getTracks().get(0); TrackState newTsAtIP = null; @@ -182,19 +268,33 @@ public void process(EventHeader event) { } } if (newTsAtIP == null) newTsAtIP = newTrack.getTrackStates().get(0); - double[] newParams = newTsAtIP.getParameters(); - for (int i=0; i<5; ++i) { - aida.histogram1D(String.format("Helix parameter %d new minus old over old", i), 100, -0.5, 0.5).fill((newParams[i]-oldParams[i])/oldParams[i]); - } - aida.histogram1D("old track chi2 per dof", 100, 0., 50.).fill(oldTrack.getChi2()/oldTrack.getNDF()); + double[] newParams = newTsAtIP.getParameters(); + aida.histogram1D("Helix parameter d0 new minus old over old", 100, -2.5, 2.5).fill((newParams[0]-oldParams[0])/oldParams[0]); + aida.histogram1D("Helix parameter phi0 new minus old over old", 100, -0.5, 0.5).fill((newParams[1]-oldParams[1])/oldParams[1]); + aida.histogram1D("Helix parameter omega new minus old over old", 100, -0.5, 0.5).fill((newParams[2]-oldParams[2])/oldParams[2]); + aida.histogram1D("Helix parameter z0 new minus old over old", 100, -0.5, 0.5).fill((newParams[3]-oldParams[3])/oldParams[3]); + aida.histogram1D("Helix parameter tan(lambda) new minus old over old", 100, -0.5, 0.5).fill((newParams[4]-oldParams[4])/oldParams[4]); + aida.histogram1D("old track chi2 per dof", 100, 0., 50.).fill(oldTrack.getChi2()/nDOF); + aida.histogram1D("old track number degrees of freedom",20,0.,20.).fill(nDOF); aida.histogram1D("new track chi2 per dof", 100, 0., 50.).fill(newTrack.getChi2()/newTrack.getNDF()); + aida.histogram1D("new track number degrees of freedom",20,0.,20.).fill(newTrack.getNDF()); double [] P = newTsAtIP.getMomentum(); double pMag = FastMath.sqrt(P[0]*P[0]+P[1]*P[1]+P[2]*P[2]); double changeInP = pMag/rp.getMomentum().magnitude(); aida.histogram1D("new momentum over old momentum", 100, 0.5, 1.5).fill(changeInP); + if (Math.abs(changeInP-1.0) > 0.04) { + aida.histogram1D("Bad new track chi2 per dof", 100, 0., 50.).fill(newTrack.getChi2()/newTrack.getNDF()); + aida.histogram1D("Bad new track number degrees of freedom",20,0.,20.).fill(newTrack.getNDF()); + aida.histogram1D("Bad helix parameter d0 new minus old over old", 100, -2.5, 2.5).fill((newParams[0]-oldParams[0])/oldParams[0]); + aida.histogram1D("Bad helix parameter phi0 new minus old over old", 100, -0.5, 0.5).fill((newParams[1]-oldParams[1])/oldParams[1]); + aida.histogram1D("Bad helix parameter z0 new minus old over old", 100, -0.5, 0.5).fill((newParams[3]-oldParams[3])/oldParams[3]); + + } aida.histogram1D("new particle E over p", 100, 0., 2.).fill(rp.getEnergy()/pMag); if (debug) System.out.format("ReconstructedParticleRefitter: Event %d, eOverP=%10.4f, changeInP=%10.4f\n", event.getEventNumber(), eOverP, changeInP); + + // Comparisons for MC events MCParticle theMatch = getMCmatch(event, rp); if (theMatch != null) { double eMC = theMatch.getEnergy(); @@ -203,6 +303,31 @@ public void process(EventHeader event) { aida.histogram1D("new momentum over MC energy", 100, 0.5, 1.5).fill(newPoverEMC); aida.histogram1D("old momentum over MC energy", 100, 0.5, 1.5).fill(rp.getMomentum().magnitude()/eMC); aida.histogram1D("ECAL over MC energy", 100, 0.5, 1.5).fill(rp.getEnergy()/eMC); + TrackState newTsAtLastHit = null; + for (TrackState ts : newTrack.getTrackStates()) { + if (ts == null) { + System.out.format("ReconstructedParticle Refitter: event %d, null Trackstate pointer!\n", event.getEventNumber()); + break; + } + if (debug) System.out.format("ReconstructedParticle Refitter: trackstate at %d of %d\n", ts.getLocation(), newTrack.getTrackStates().size()); + if (ts.getLocation() == TrackState.AtLastHit) { + newTsAtLastHit = ts; + break; + } + } + if (newTsAtLastHit != null) { + double [] Plast = newTsAtLastHit.getMomentum(); + double pMagLst = FastMath.sqrt(Plast[0]*Plast[0]+Plast[1]*Plast[1]+Plast[2]*Plast[2]); + if (debug) { + double [] refPnt = newTsAtLastHit.getReferencePoint(); + double [] helix = newTsAtLastHit.getParameters(); + System.out.format("Event %d at last hit, p=%10.5f, ref=%8.3f %8.3f %8.3f\n", event.getEventNumber(), pMagLst, refPnt[0], refPnt[1], refPnt[2]); + System.out.format(" Helix at last hit = %9.5f %9.5f %9.5f %9.5f %9.5f\n", helix[0], helix[1], helix[2], helix[3], helix[4]); + } + aida.histogram1D("p at last hit over MC energy", 100, 0.5, 1.5).fill(pMagLst/eMC); + double pFrstvsPlst = (pMag - pMagLst)/pMagLst; + aida.histogram1D("fractional change of p from smoothing", 100, -0.1, 0.1).fill(pFrstvsPlst); + } } } else { refitReconstructedParticles.add(rp); @@ -233,14 +358,20 @@ private ReconstructedParticle makeNewReconstructedParticle(EventHeader event, Re ReconstructedParticle particle = new BaseReconstructedParticle(); if (debug) System.out.format("Entering makeNewReconstructedParticle for event %d\n", event.getEventNumber()); Cluster cluster = rp.getClusters().get(0); - // refit the track with the cluster energy + // refit the track Track newTrack = refitTrack(event, rp); + if (newTrack == null) { + if (debug) System.out.format("makeNewReconstrucedParticle: failed to make new track in event %d\n", event.getEventNumber()); + return particle; + } // Store the track in the particle. particle.addTrack(newTrack); if (debug) { double [] trackP = newTrack.getTrackStates().get(0).getMomentum(); double P=FastMath.sqrt(trackP[0]*trackP[0]+trackP[1]*trackP[1]+trackP[2]*trackP[2]); - System.out.format("makeNewReconstructedParticle: track p=%10.4f\n", P); + int nHits = newTrack.getTrackerHits().size(); + int nDOF = newTrack.getNDF(); + System.out.format("makeNewReconstructedParticle: track p=%10.4f, %d hits, NDF=%d \n", P, nHits, nDOF); } // Set the type of the particle. This is used to identify the tracking @@ -314,12 +445,25 @@ private Track refitTrack(EventHeader event, ReconstructedParticle rp) { Cluster cluster = rp.getClusters().get(0); //the energy of the associated cluster double energy = cluster.getEnergy(); - int nHits = track.getTrackerHits().size(); - if (debug) System.out.format("refitTrack in event %d with %d hits, chi2=%8.3f for %d dof, cluster E=%9.4f\n", - event.getEventNumber(), nHits, track.getChi2(), track.getNDF(), energy); - + if (debug) { + int nHits = track.getTrackerHits().size(); + System.out.format("refitTrack in event %d with %d hits, chi2=%8.3f for %d dof, cluster E=%9.4f\n", event.getEventNumber(), nHits, track.getChi2(), track.getNDF(), energy); + } // Fit a new track with this list of hits and the cluster energy. - return KI.refitTrackWithE(event, track, energy); + if (_cheat) { + MCParticle theMatch = getMCmatch(event, rp); + if (theMatch != null) { + double energyMC = theMatch.getEnergy(); + double sigmaE = (kPar.getEres(0)/FastMath.sqrt(energy) + kPar.getEres(1))*energy/100.; + energy = energyMC + sigmaE*ran.nextGaussian(); + if (debug) System.out.format("refitTrack: MC cheat energy = %10.5f+=%9.5f\n", energy, sigmaE); + aida.histogram1D("Cheat ECAL over MC energy", 100, 0.5, 1.5).fill(energy/energyMC); + } else { + System.out.format("refitTrack: no MC match was found\n"); + } + } + if (layerSkip == null) layerSkip = new ArrayList(); + return KI.refitTrackWithE(event, track, energy, _doEconstraint, layerSkip); } /** @@ -368,7 +512,8 @@ private void setupSensors(EventHeader event) { * * @param s */ - public void setFinalStateParticleCollectionName(String s) { + public void set_finalStateParticleCollectionName(String s) { + System.out.format("ReconstructedParticleRefitter: setting the final state particle collection name to %s\n", s); _finalStateParticleCollectionName = s; } @@ -378,7 +523,8 @@ public void setFinalStateParticleCollectionName(String s) { * * @param s */ - public void setRefitParticleCollectionName(String s) { + public void set_refitParticleCollectionName(String s) { + System.out.format("ReconstructedParticleRefitter: setting the output collection name to %s\n", s); _refitParticleCollectionName = s; } @@ -387,6 +533,7 @@ public void setRefitParticleCollectionName(String s) { * @param d Maximum E/p allowed for electron/positron candidates */ public void set_eOverpCut(double d) { + System.out.format("ReconstructedParticleRefitter: setting the E/p cut value to %9.4f\n", d); _eOverpCut = d; } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index 2d4b5c058e..1f4303e53b 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -20,6 +20,7 @@ import org.hps.util.Pair; import org.lcsim.event.TrackState; +import org.lcsim.geometry.Detector; /** * This is for stand-alone testing of the Kalman fit only and is not part of the @@ -505,7 +506,8 @@ class HelixTest3 { // Program for testing the Kalman fitting code TkInitial.print("TkInitial: initial helix at the origin"); helixBegin.print("helixBegin: starting helix at layer 1"); RKhelix TkEnd = helixBeginRK; - KalmanInterface KI = new KalmanInterface(kPar, fM); + Detector detect = null; + KalmanInterface KI = new KalmanInterface(kPar, detect, fM); for (int iTrial = 0; iTrial < nTrials; iTrial++) { RKhelix Tk = helixBeginRK.copy(); if (verbose) { @@ -812,7 +814,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code } double sigmaE = 0.03 * FastMath.sqrt(Etrue); double E = Etrue + rnd.nextGaussian() * sigmaE; - KalmanTrack.energyConstraint(E, sigmaE); + KalmanTrack.smoothIt(false, E, sigmaE); // Check on the covariance matrix Matrix C = new Matrix(KalmanTrack.originCovariance()); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 5793016953..872ecdf06b 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -136,6 +136,12 @@ public class KalTrack { } this.SiteList.add(site); } + if (debug) { + for (MeasurementSite site: SiteList) { + SiModule mod = site.m; + System.out.format("KalTrack: SiModule on layer %d wafer %d, %d hits\n", mod.Layer, mod.detector, mod.hits.size()); + } + } helixAtOrigin = null; propagated = false; @@ -587,9 +593,17 @@ public void print(String s) { for (int i = 0; i < SiteList.size(); i++) { MeasurementSite site = SiteList.get(i); SiModule m = site.m; + StateVector aSite = site.aS; + if (aSite == null) aSite = site.aF; + double energy = 0.; + if (aSite != null) { + double tanL = aSite.helix.a.v[4]; + double K = aSite.helix.a.v[2]; + energy = FastMath.sqrt(1.0+tanL*tanL)/Math.abs(K); + } int hitID = site.hitID; - System.out.format(" Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f ", m.Layer, m.detector, m.isStereo, - site.chi2inc); + System.out.format(" Layer %d, detector %d, stereo=%b, chi^2 inc.=%10.6f E=%8.3f ", m.Layer, m.detector, m.isStereo, + site.chi2inc, energy); if (hitID >= 0) { if (site.aS == null) aA = site.aF; else aA = site.aS; @@ -1648,8 +1662,7 @@ public int addHits(ArrayList data, double mxResid, double mxChi2inc, d /** * Re-fit the track * - * @param keep true if there might be another recursion after dropping more - * hits + * @param keep true if there might be another recursion after dropping more hits * @return true if the refit was successful */ public boolean fit(boolean keep) { @@ -1787,10 +1800,11 @@ public boolean fit(boolean keep) { * momentum measurement). Then smooth back to the first layer and propagate * to the origin. * - * @param E ECAL energy - * @param sigmaE ECAL energy uncertainty + * @param eConstraint true to include the energy constraint + * @param E ECAL energy + * @param sigmaE ECAL energy uncertainty */ - void energyConstraint(double E, double sigmaE) { + void smoothIt(boolean eConstraint, double E, double sigmaE) { // The prediction step from the last tracker site to the ECAL has F=1, // since we don't alter an helix parameters in the prediction. // The HelixState.energyConstrained method then uses the Kalman weighted @@ -1800,71 +1814,64 @@ void energyConstraint(double E, double sigmaE) { // site is exactly what is returned by the energyConstrained method. The // smoothing back to the first tracker site can then proceed as usual. //boolean debug = true; - final boolean noConstraint = false; // for debugging purposes, should just do a normal Kalman smoothing + if (debug) System.out.format("Entering KalTrack.smoothIt, eConstraint=%b, E=%f, sigmaE=%f\n", eConstraint, E, sigmaE); int idxLast = SiteList.size() - 1; MeasurementSite lastSite = SiteList.get(idxLast); - HelixState energyConstrainedHelix = lastSite.aF.helix.energyConstrained(E, sigmaE); - if (debug) System.out.format("KalTrack:energyConstraint kappa=%9.4f, kappaE=%9.4f\n",lastSite.aF.helix.a.v[2],energyConstrainedHelix.a.v[2]); - lastSite.energyConstrained = !noConstraint; - lastSite.aS = new StateVector(lastSite.aF.kLow, lastSite.aF.uniformB); // Blank new state vector - if (noConstraint) { - lastSite.aS.helix = lastSite.aF.helix; + lastSite.energyConstrained = eConstraint; + if (!eConstraint) { + lastSite.aS = lastSite.aF; + this.chi2 = lastSite.chi2inc; } else { + if (debug) { + double kappa = lastSite.aF.helix.a.v[2]; + double tanl = lastSite.aF.helix.a.v[4]; + double ePredict = FastMath.sqrt(1.0 + tanl * tanl) / Math.abs(kappa); + System.out.format("KalTrack.smoothIt at last layer, E=%9.4f\n", ePredict); + } + lastSite.aS = new StateVector(lastSite.aF.kLow, lastSite.aF.uniformB); // Blank new state vector + HelixState energyConstrainedHelix = lastSite.aF.helix.energyConstrained(E, sigmaE); lastSite.aS.helix = energyConstrainedHelix; + lastSite.aS.kUp = lastSite.aF.kUp; + lastSite.aS.F = lastSite.aF.F; // Don't deep copy the F matrix + lastSite.aS.mPred = lastSite.aF.mPred; + lastSite.aS.R = lastSite.aF.R; + lastSite.aS.r = lastSite.aF.r; + lastSite.aS.K = lastSite.aF.K; + lastSite.smoothed = true; + lastSite.chi2inc = (lastSite.aS.r * lastSite.aS.r) / lastSite.aS.R; + + // Get the residual of the prediction at the ECAL + double kappa = energyConstrainedHelix.a.v[2]; + double tanl = energyConstrainedHelix.a.v[4]; + double ePredict = FastMath.sqrt(1.0 + tanl * tanl) / Math.abs(kappa); + double chi = (E - ePredict) / sigmaE; + this.chi2_Econstraint = lastSite.chi2inc + chi * chi; + if (debug) { + System.out.format("KalTrack:smoothIt E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n", + E,ePredict,sigmaE,chi); + System.out.format(" constrained helix = %s\n", energyConstrainedHelix.a.toString()); + } } - lastSite.aS.kUp = lastSite.aF.kUp; - lastSite.aS.F = lastSite.aF.F; // Don't deep copy the F matrix - lastSite.aS.mPred = lastSite.aF.mPred; - lastSite.aS.R = lastSite.aF.R; - lastSite.aS.r = lastSite.aF.r; - lastSite.aS.K = lastSite.aF.K; - lastSite.chi2incE = (lastSite.aS.r * lastSite.aS.r) / lastSite.aS.R; - // Get the residual of the prediction at the ECAL - double kappa = energyConstrainedHelix.a.v[2]; - double tanl = energyConstrainedHelix.a.v[4]; - double ePredict = FastMath.sqrt(1.0 + tanl * tanl) / Math.abs(kappa); - double chi = (E - ePredict) / sigmaE; - if (debug) System.out.format("KalTrack:energyConstraint E=%9.4f, Epredict=%9.4f, sigmaE=%9.4f, chi=%9.4f\n",E,ePredict,sigmaE,chi); - if (noConstraint) { - this.chi2_Econstraint = lastSite.chi2incE; - } else { - this.chi2_Econstraint = lastSite.chi2incE + chi * chi; - } + lastSite.smoothed = true; MeasurementSite nS = lastSite; for (int idx = idxLast - 1; idx >= 0; --idx) { MeasurementSite thisSite = SiteList.get(idx); - thisSite.aS = thisSite.aF.smooth(nS.aS, nS.aP); - if (thisSite.hitID < 0) { - thisSite.energyConstrained = !noConstraint; - continue; - } - Measurement hit = thisSite.m.hits.get(thisSite.hitID); - double V = hit.sigma * hit.sigma; - double phiS = thisSite.aS.helix.planeIntersect(thisSite.m.p); - - if (Double.isNaN(phiS)) { // This should almost never happen! - logger.log(Level.FINE, "KalTrack.energyConstraint: no intersection of helix with the plane exists."); - continue; - } - thisSite.aS.mPred = thisSite.h(thisSite.aS, thisSite.m, phiS); - thisSite.aS.r = hit.v - thisSite.aS.mPred; - if (tempV == null) { - tempV = new DMatrixRMaj(5, 1); + thisSite.smooth(nS); + thisSite.energyConstrained = eConstraint; + thisSite.smoothed = true; + if (eConstraint) { + this.chi2_Econstraint += thisSite.chi2inc; + } else { + this.chi2 += thisSite.chi2inc; } - CommonOps_DDRM.mult(thisSite.aS.helix.C, thisSite.H, tempV); - thisSite.aS.R = V - CommonOps_DDRM.dot(thisSite.H, tempV); - if (thisSite.aS.R < 0) { + if (KalmanPatRecHPS.negativeCov(thisSite.aS.helix.C)) { if (debug) { - System.out.format("KalTrack.energyConstraint, measurement covariance %12.4e is negative\n", thisSite.aS.R); + System.out.format("KalTrack: event %d, ID %d, negative covariance after smoothing at layer %d\n", + eventNumber, ID, thisSite.m.Layer); } - //aS.print("the smoothed state"); - //nS.print("the next site in the chain"); - thisSite.aS.R = 0.25 * V; // A negative covariance makes no sense, hence this fudge + bad = true; + KalmanPatRecHPS.fixCov(thisSite.aS.helix.C, thisSite.aS.helix.a); } - - thisSite.chi2incE = (thisSite.aS.r * thisSite.aS.r) / thisSite.aS.R; - this.chi2_Econstraint += thisSite.chi2incE; - thisSite.energyConstrained = !noConstraint; nS = thisSite; } Vec beamSpot = new Vec(3, kPar.beamSpot); @@ -1874,7 +1881,7 @@ void energyConstraint(double E, double sigmaE) { Plane originPlane = new Plane(beamSpot, new Vec(0., 1., 0.)); arcLengthE = new double[1]; helixAtOrigin = SiteList.get(0).aS.helix.propagateRungeKutta(originPlane, yScat, XLscat, SiteList.get(0).m.Bfield, arcLengthE); - energyConstrained = true; + energyConstrained = eConstraint; } /** diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java index 3c1a76bc29..b1ee6dc65a 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanDriverHPS.java @@ -174,7 +174,7 @@ public void detectorChanged(Detector det) { KalmanParams kPar = new KalmanParams(); kPar.setUniformB(uniformB); - KI = new KalmanInterface(kPar, fm); + KI = new KalmanInterface(kPar, det, fm); KI.createSiModules(detPlanes); System.out.format("KalmanDriver: the B field is assumed uniform? %b\n", uniformB); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index 80f7a216d9..68649c45e4 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; @@ -58,10 +59,10 @@ public class KalmanInterface { private Detector det; - private List sensors; private Map hitMap; private Map simHitMap; private Map moduleMap; + private Map> layerMap; private ArrayList trackHitsKalman; private ArrayList SiMlist; private List SeedTrackLayers = null; @@ -84,7 +85,11 @@ public class KalmanInterface { private int maxHits; private int nBigEvents; private int eventNumber; + private static Vec centerB; + private IDDecoder decoder; + private HelixPlaneIntersect hpi; + private static final int mxLyrs = 14; private static final boolean debug = false; private static final double c = 2.99793e8; // Speed of light in m/s @@ -216,8 +221,8 @@ public void setSeedTrackLayers(List input) { /** * alternate constructor for backward compatibility */ - public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { - this(kPar, fM); + public KalmanInterface(boolean uniformB, KalmanParams kPar, Detector det, org.lcsim.geometry.FieldMap fM) { + this(kPar, det, fM); } /** @@ -228,12 +233,11 @@ public KalmanInterface(boolean uniformB, KalmanParams kPar, org.lcsim.geometry.F * code * @param fM The HPS field map */ - public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { + public KalmanInterface(KalmanParams kPar, Detector det, org.lcsim.geometry.FieldMap fM) { - this.det = det; - this.sensors = sensors; this.fM = fM; this.kPar = kPar; + this.decoder = det.getSubdetector("Tracker").getIDDecoder(); logger = Logger.getLogger(KalmanInterface.class.getName()); logger.info("Entering the KalmanInterface constructor"); maxHits = 0; @@ -246,13 +250,15 @@ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { hitMap = new HashMap(); simHitMap = new HashMap(); moduleMap = new HashMap(); + layerMap = new HashMap>(); trackHitsKalman = new ArrayList(); // Used only to refit existing GBL tracks SiMlist = new ArrayList(); SeedTrackLayers = new ArrayList(); - // SeedTrackLayers.add(2); - SeedTrackLayers.add(3); - SeedTrackLayers.add(4); - SeedTrackLayers.add(5); + //SeedTrackLayers.add(2); + //SeedTrackLayers.add(3); + //SeedTrackLayers.add(4); + //SeedTrackLayers.add(5); + hpi = new HelixPlaneIntersect(); if (kPar.uniformB) { logger.log(Level.WARNING, "KalmanInterface WARNING: the magnetic field is set to a uniform value."); @@ -287,7 +293,7 @@ public KalmanInterface(KalmanParams kPar, org.lcsim.geometry.FieldMap fM) { kPat = new KalmanPatRecHPS(kPar); - Vec centerB = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), fM); + centerB = KalmanInterface.getField(new Vec(0., kPar.SVTcenter, 0.), fM); logger.log(Level.INFO, String.format("The magnetic field at the detector center (%10.5f mm) is %10.5f\n", kPar.SVTcenter, centerB.mag())); double conFac = 1.0e12 / c; alphaCenter = conFac / centerB.mag(); @@ -476,8 +482,8 @@ public static double[] hpsHelixIntersect(double[] x, double[] p, double Q, doubl Plane detPln = new Plane(pointOnPlaneTransformed, tK, uK, vK); // Integrate to find the intersection with the detector plane - HelixPlaneIntersect hpi = new HelixPlaneIntersect(); Vec pInt = new Vec(3); + HelixPlaneIntersect hpi = new HelixPlaneIntersect(); Vec intercept = hpi.rkIntersect(detPln, xK, pK, Q, fM, pInt); // Convert the results back to HPS coordinates @@ -611,12 +617,14 @@ static TrackState toTrackState(HelixState helixState, Plane pln, double alphaCen double[] position = new double[3]; double[] helixHPS = KalmanInterface.toHPShelix(helixState, pln, alphaCenter, covHPS, position); - return new BaseTrackState(helixHPS, covHPS, position, loc); + TrackState newState = new BaseTrackState(helixHPS, covHPS, position, loc); + + ((BaseTrackState) newState).computeMomentum(centerB.mag()); + return newState; } /** - * Create an HPS TrackState from a Kalman HelixState at the location of a - * particular SiModule + * Create an HPS TrackState from a Kalman HelixState at the location of a particular SiModule * * @param ms Kalman MeasurementSite object * @param loc Integer representation of the TrackState location (AtIP, @@ -628,6 +636,7 @@ public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoot // Note that the helix parameters that get stored in the TrackState assume a B-field exactly oriented in the // z direction and a pivot point at the origin (0,0,0). The referencePoint of the TrackState is set to the // intersection point with the detector plane. + //boolean debug = true; StateVector sv = null; if (useSmoothed) { if (!ms.smoothed) { @@ -640,7 +649,14 @@ public TrackState createTrackState(MeasurementSite ms, int loc, boolean useSmoot } sv = ms.aF; } - return sv.helix.toTrackState(alphaCenter, ms.m.p, loc); + if (debug) System.out.format("createTrackState: created TrackState at layer %d location %d\n",ms.m.Layer,loc); + TrackState newTrackState = sv.helix.toTrackState(alphaCenter, ms.m.p, loc); + if (debug) { + double [] p= newTrackState.getMomentum(); + double pmag = Math.sqrt(p[0]*p[0]+p[1]*p[1]+p[2]*p[2]); + System.out.format(" new trackstate momentum = %10.5f\n", pmag); + } + return newTrackState; } /** @@ -794,6 +810,7 @@ public PropagatedTrackState propagateTrackState(TrackState stateHPS, double[] lo */ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { + //boolean debug = true; if (kT.SiteList == null) { logger.log(Level.WARNING, "KalmanInterface.createTrack: Kalman track is incomplete."); if (debug) System.out.format("createTrack: track %d is incomplete\n", kT.ID); @@ -864,6 +881,7 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { prevID = lay; } */ + if (debug) System.out.format("createTrack: at layer %d, loc=%d\n", site.m.Layer, loc); if (loc == TrackState.AtFirstHit || loc == TrackState.AtLastHit || storeTrackStates) { ts = createTrackState(site, loc, true); if (ts != null) { @@ -874,15 +892,20 @@ public BaseTrack createTrack(KalTrack kT, boolean storeTrackStates) { // Extrapolate to the ECAL and make a new trackState there. BaseTrackState ts_ecal = TrackUtils.getTrackExtrapAtEcalRK(newTrack, fM); + if (debug) { + double [] p = ts_ecal.getMomentum(); + double pmag = Math.sqrt(p[0]*p[0]+p[1]*p[1]+p[2]*p[2]); + System.out.format("createTrack: at ECAL, loc=%d, p=%10.5f\n",ts_ecal.getLocation(), pmag); + } newTrack.getTrackStates().add(ts_ecal); - // other track properties + // Other track properties if (kT.energyConstrained) { newTrack.setChisq(kT.chi2_Econstraint); - newTrack.setNDF(kT.SiteList.size() - 4); + newTrack.setNDF(newTrack.getTrackerHits().size() - 4); } else { newTrack.setChisq(kT.chi2); - newTrack.setNDF(kT.SiteList.size() - 5); + newTrack.setNDF(newTrack.getTrackerHits().size() - 5); } newTrack.setTrackType(BaseTrack.TrackType.Y_FIELD.ordinal()); newTrack.setFitSuccess(true); @@ -1012,7 +1035,7 @@ static double[][] ungetLCSimCov(double[] oldCov, double alpha) { } /** - * Add hits to an existing track Used only for Kalman tracks made by fitting + * Add hits to an existing track. Used only for Kalman tracks made by fitting * hits on a GBL track * * @param newTrack Track to which hits will be added @@ -1109,7 +1132,7 @@ public void createSiModules(List inputPlanes) { topBottom = 0; } int detector = temp.getModuleNumber(); - if (kalLayer > 13) { + if (kalLayer >= mxLyrs) { System.out.format("***KalmanInterface.createSiModules Warning: Kalman layer %d , tempLayer = %d out of range.***\n", kalLayer, temp.getLayerNumber()); } int millipedeID = temp.getMillepedeId(); @@ -1121,6 +1144,24 @@ public void createSiModules(List inputPlanes) { Collections.sort(SiMlist, new SortByLayer()); for (SiModule sim : SiMlist) { logger.log(Level.INFO, sim.toString()); + int lyr = sim.Layer; + if (layerMap.containsKey(lyr)) { + layerMap.get(lyr).add(sim); + } else { + layerMap.put(lyr, new ArrayList()); + layerMap.get(lyr).add(sim); + } + } + for (int lyr=0; lyr mods = layerMap.get(lyr); + if (mods == null) { + System.out.format("KalmanInterface.createSiModules: error in layer %d, no module list\n", lyr); + continue; + } + for (SiModule mod : mods) { + if (mod.Layer != lyr) System.out.format("KalmanInterface.createSiModules: error in layer map at layer %d\n", lyr); + System.out.format("KalmanInterface.createSiModules: layer %d module %d has SiModule at location %s\n", lyr, mod.detector, mod.p.X().toString()); + } } } @@ -1128,10 +1169,9 @@ public void createSiModules(List inputPlanes) { * Method to feed simulated hits into the pattern recognition, for testing * * @param event Event header - * @param decoder Decoder for unpacking hit information * @return true if it executed successfully */ - boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { + boolean fillAllSimHits(EventHeader event) { boolean success = false; eventNumber = event.getEventNumber(); @@ -1153,7 +1193,7 @@ boolean fillAllSimHits(EventHeader event, IDDecoder decoder) { //double[] tkMom = hit1D.getMomentum(); //System.out.format("MC true hit momentum=%10.5f %10.5f %10.5f\n",tkMom[0],tkMom[1],tkMom[2]); decoder.setID(hit1D.getCellID()); - int Layer = decoder.getValue("layer") + 1; + int Layer = decoder.getValue("layer") - 1; int Module = decoder.getValue("module"); Pair sensor = new Pair(Layer, Module); @@ -1445,8 +1485,8 @@ private double fillMeasurements(EventHeader event, List hits1D, int if (debug) System.out.format("Entering fillMeasurements in event %d for %d hits, addmode %d\n", event.getEventNumber(), hits1D.size(), addMode); for (TrackerHit hit1D : hits1D) { - HpsSiSensor temp = ((HpsSiSensor) ((RawTrackerHit) hit1D.getRawHits().get(0)).getDetectorElement()); - int lay = temp.getLayerNumber(); + HpsSiSensor sensor = ((HpsSiSensor) ((RawTrackerHit) hit1D.getRawHits().get(0)).getDetectorElement()); + int lay = sensor.getLayerNumber(); if (addMode == 0 && !SeedTrackLayers.contains((lay + 1) / 2)) { continue; } else if (addMode == 1 && SeedTrackLayers.contains((lay + 1) / 2)) { @@ -1454,8 +1494,8 @@ private double fillMeasurements(EventHeader event, List hits1D, int } ArrayList hitsInLayer = null; - if (hitsMap.containsKey(temp)) { - hitsInLayer = hitsMap.get(temp); + if (hitsMap.containsKey(sensor)) { + hitsInLayer = hitsMap.get(sensor); } else { hitsInLayer = new ArrayList(); } @@ -1463,7 +1503,7 @@ private double fillMeasurements(EventHeader event, List hits1D, int if (hit1D.getPosition()[2] < firstZ) { firstZ = hit1D.getPosition()[2]; } - hitsMap.put(temp, hitsInLayer); + hitsMap.put(sensor, hitsInLayer); } // Add MC truth information to each hit if it is available @@ -1480,20 +1520,21 @@ private double fillMeasurements(EventHeader event, List hits1D, int hasMC = true; } + int nHitsFilled = 0; for (SiModule mod : SiMlist) { SiStripPlane plane = moduleMap.get(mod); if (!hitsMap.containsKey(plane.getSensor())) { continue; } - ArrayList temp = hitsMap.get(plane.getSensor()); - if (temp == null) { + ArrayList theHits = hitsMap.get(plane.getSensor()); + if (theHits == null) { continue; } Hep3Vector planeMeasuredVec = VecOp.mult(HpsSvtToKalmanMatrix, plane.getMeasuredCoordinate()); - for (int i = 0; i < temp.size(); i++) { - TrackerHit hit = temp.get(i); + for (int i = 0; i < theHits.size(); i++) { + TrackerHit hit = theHits.get(i); SiTrackerHitStrip1D local = (new SiTrackerHitStrip1D(hit)).getTransformedHit(TrackerHitType.CoordinateSystem.SENSOR); // SiTrackerHitStrip1D global = (new @@ -1502,6 +1543,7 @@ private double fillMeasurements(EventHeader event, List hits1D, int double umeas = local.getPosition()[0]; double xStrip = -local.getPosition()[1]; double du = FastMath.sqrt(local.getCovarianceAsMatrix().diagonal(0)); + double time = local.getTime(); // if hps measured coord axis is opposite to kalman measured coord axis // This really should not happen, as the Kalman axis is copied directly from the hps geometry. @@ -1515,9 +1557,9 @@ private double fillMeasurements(EventHeader event, List hits1D, int System.out.format("KalmanInterface:fillMeasurements Measurement %d, the measurement uncertainty is set to %10.7f\n", i, du); System.out.printf("Filling SiMod: %s \n", plane.getName()); - System.out.printf("HPSplane MeasuredCoord %s UnmeasuredCoord %s Normal %s umeas %f\n", + System.out.printf("HPSplane MeasuredCoord %s UnmeasuredCoord %s Normal %s umeas %f xStrip %f time %f\n", plane.getMeasuredCoordinate().toString(), plane.getUnmeasuredCoordinate().toString(), plane.normal().toString(), - umeas); + umeas, xStrip, time); System.out.printf(" converted to Kalman Coords Measured %s Unmeasured %s umeas %f \n", planeMeasuredVec.toString(), VecOp.mult(HpsSvtToKalmanMatrix, plane.getUnmeasuredCoordinate()).toString(), umeas); mod.p.print("Corresponding KalmanPlane"); @@ -1526,7 +1568,8 @@ private double fillMeasurements(EventHeader event, List hits1D, int globalX.print("globalX"); globalY.print("globalY"); } - Measurement m = new Measurement(umeas, xStrip, du, 0., hit.getdEdx() * 1000000.); + Measurement m = new Measurement(umeas, xStrip, du, time, local.getdEdx() * 1000000.); + nHitsFilled++; if (hasMC) { m.pMC = new ArrayList(1); List rawHits = hit.getRawHits(); @@ -1557,6 +1600,7 @@ private double fillMeasurements(EventHeader event, List hits1D, int } } + if (debug) System.out.format("KalmanInterface.fillMeasurements: total hits filled = %d\n", nHitsFilled); return firstZ; } @@ -1683,10 +1727,14 @@ public KalmanTrackFit2 createKalmanTrackFit(EventHeader event, Vec helixParams, /** * Refit an existing HPS track with the associated energy measurement added in. - * @param track - * @return + * @param event Event header + * @param track HPS track to refit + * @param energy ECal energy in case the constraint is needed (otherwise not used) + * @param eConstraint true to include the ECal energy constraint + * @param layerSkip List of layers to skip in the fit (any hit on these layers is deleted) + * @return The new HPS track */ - public Track refitTrackWithE(EventHeader event, Track track, double energy) { + public Track refitTrackWithE(EventHeader event, Track track, double energy, boolean eConstraint, ArrayList layerSkip) { // First we need initial guesses for the helix parameters and covariance. // Preferentially take them from a TrackState. If there is no TrackState, // then estimate from a linear fit to the set of hits. @@ -1697,7 +1745,10 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { Vec kalHelixParams = null; Vec pivot = new Vec(0., 0., 0.); if (debug) { - System.out.format("Entering refitTrackWithE: energy = %10.4f\n", energy); + System.out.format("Entering refitTrackWithE: event=%d, energy = %10.4f, E-constraint=%b\n", event.getEventNumber(), energy, eConstraint); + for (int lyr : layerSkip) { + System.out.format(" Layer %d will be skipped.\n", lyr); + } } if (tkrStates != null) { for (TrackState tkrState : tkrStates) { @@ -1724,12 +1775,15 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { Vec refPnt = new Vec(3, theTrackState.getReferencePoint()); helixParams = theTrackState.getParameters(); double bField = KalmanInterface.getField(refPnt, fM).mag(); - double c = 2.99793e8; // Speed of light in m/s double alpha = 1000.0 * 1.0e9 / (c * bField); kalHelixParams = new Vec(5, KalmanInterface.unGetLCSimParams(helixParams, alpha)); double[] covHPS = theTrackState.getCovMatrix(); helixCov = new DMatrixRMaj(KalmanInterface.ungetLCSimCov(covHPS, alpha)); if (debug) { + ((BaseTrackState) theTrackState).computeMomentum(centerB.mag()); + double [] ptk = theTrackState.getMomentum(); + double ptkmag = Math.sqrt(ptk[0]*ptk[0]+ptk[1]*ptk[1]+ptk[2]*ptk[2]); + System.out.format("refitTrackWithE: at IP, p=%10.5f, E to constrain to = %10.5f\n", ptkmag, energy); System.out.format("refitTrackWithE: LCSim helix params = %9.5f %9.5f %9.5f %9.5f %9.5f\n", helixParams[0], helixParams[1], helixParams[2], helixParams[3], helixParams[4]); System.out.format("refitTrackWithE: Kalman helix params = %s\n", kalHelixParams.toString("helix")); @@ -1737,36 +1791,147 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { } } else { if (debug) { - System.out.println("KalmanInterface:refitTrackWithE, no track states exist "); + System.out.println("KalmanInterface.refitTrackWithE: no track states exist "); } } // Get the list of tracker hits on this track List hitsOnTrack = track.getTrackerHits(); + if (debug) { + for (TrackerHit hit : hitsOnTrack) { + double time = hit.getTime(); + double [] pos = hit.getPosition(); + System.out.format("KalmanInterface.refitTrackWithE: hit with time=%9.3f, position=(%9.4f, %9.4f, %9.4f)\n", + time, pos[0], pos[1], pos[2]); + List rawHits = hit.getRawHits(); + for (RawTrackerHit rawHit : rawHits) { + long ID = rawHit.getCellID(); + decoder.setID(ID); + int Layer = decoder.getValue("layer") - 1; + int Module = decoder.getValue("module"); + double [] ps = rawHit.getPosition(); + int t = rawHit.getTime(); + System.out.format(" raw hit on layer %d, module %d, time=%d, position=(%9.4f, %9.4f, %9.4f)\n", + Layer, Module, t, ps[0], ps[1], ps[2]); + } + } + } + // Remove hits on certain layers, if requested + if (layerSkip.size() > 0) { + Iterator iter = hitsOnTrack.iterator(); + while (iter.hasNext()) { + TrackerHit hit = iter.next(); + List rawHits = hit.getRawHits(); + long ID = rawHits.get(0).getCellID(); + decoder.setID(ID); + int Layer = decoder.getValue("layer") - 1; + for (int lyr : layerSkip) { + if (lyr == Layer) { + hitsOnTrack.remove(hit); + if (debug) System.out.format("KalmanInterface.refitTrackWithE: removing hit on layer %d\n", Layer); + } + } + } + } + // Fill the hit information into the SiModule objects double firstHitZ = fillMeasurements(event, hitsOnTrack, 2); - // Do a linear fit to the track hits to get the helix parameter guesses + + // If no TrackState info was available, do a linear fit to the track hits to get the helix parameter guesses if (theTrackState == null) { - if (debug) System.out.format("refitTrackWithE: firstHitZ %f \n", firstHitZ); + if (debug) System.out.format("refitTrackWithE: firstHitZ %f. Generate a seed from linear fit.\n", firstHitZ); SeedTrack seed = new SeedTrack(trackHitsKalman, firstHitZ, 0., kPar); kalHelixParams = seed.helixParams(); helixCov = seed.covariance(); pivot.v[1] = seed.yOrigin; } - + + // Find the range of tracker layers covered by the hits on the track + int lyrMin = 20; + int lyrMax = -1; + for (TrackerHit hit : hitsOnTrack) { + List rawHits = hit.getRawHits(); + long ID = rawHits.get(0).getCellID(); + decoder.setID(ID); + int Layer = decoder.getValue("layer") - 1; + if (Layer > lyrMax) lyrMax = Layer; + if (Layer < lyrMin) lyrMin = Layer; + } + if (debug) System.out.format("refitTrackWithE: first layer = %d, last layer = %d\n", lyrMin, lyrMax); + + // Select SiModules along the range of the track, to use in the fit + double tanL = kalHelixParams.v[4]; + int topBottom; + if (tanL > 0) topBottom = 0; + else topBottom = 1; + if (debug) System.out.format("refitTrackWithE: look for modules in detector %d\n", topBottom); ArrayList SiMoccupied = new ArrayList(); - for (SiModule SiM : SiMlist) { - if (!SiM.hits.isEmpty()) SiMoccupied.add(SiM); + for (int lyr=lyrMin; lyr<=lyrMax; ++lyr) { + if (layerMap.containsKey(lyr)) { + int nHitsLyr = 0; + List mods = layerMap.get(lyr); + for (SiModule mod : mods) { + int tb; + if (mod.p.X().v[2] > 0) tb = 0; + else tb = 1; + if (debug) System.out.format(" module %d in layer %d detector %d\n", mod.detector, mod.Layer, tb); + if (mod.hits.size() > 0) { + nHitsLyr += mod.hits.size(); + SiMoccupied.add(mod); + if (tb != topBottom) { + System.out.format("refitTrackWithE: wrong detector at layer %d, tb=%d\n", lyr, tb); + } + if (debug) System.out.format("refitTrackWithE: module in layer %d wafer %d has %d hits\n", lyr, mod.detector, mod.hits.size()); + break; + } + } + if (nHitsLyr == 0) { + SiModule modHit = null; + for (SiModule mod : mods) { + int tb; + if (mod.p.X().v[2] > 0) tb = 0; + else tb = 1; + if (tb != topBottom) continue; + modHit = mod; + double bField = centerB.mag(); + double alpha = 1000.0 * 1.0e9 / (c * bField); + double phiInt = hpi.planeIntersect(kalHelixParams, pivot, alpha, mod.p); + if (Double.isNaN(phiInt)) { + continue; + } + Vec rGlob = HelixState.atPhi(pivot, kalHelixParams, phiInt, alpha); + Vec rLoc = mod.toLocal(rGlob); + if (debug) { + System.out.format("refitTrackWithE: layer %d wafer %d with no hits, intersection=%s\n", lyr, mod.detector, rGlob.toString()); + System.out.format(" Intersection in local coordinates=%s\n", rLoc.toString()); + System.out.format(" module X extents: %9.4f %9.4f\n", mod.xExtent[0], mod.xExtent[1]); + System.out.format(" module Y extents: %9.4f %9.4f\n", mod.yExtent[0], mod.yExtent[1]); + } + if (rLoc.v[0] > mod.xExtent[0] && rGlob.v[0] < mod.xExtent[1]) { + if (debug) System.out.format(" Adding SiModule with no hits at layer %d wafer %d\n", lyr, mod.detector); + modHit = mod; + break; + } + } + SiMoccupied.add(modHit); + } + } else { + logger.warning(String.format("SVT layer %d has no SiModules", lyr)); + } } + Collections.sort(SiMoccupied, new SortByLayer()); if (debug) { for (int i = 0; i < SiMoccupied.size(); i++) { SiModule SiM = SiMoccupied.get(i); int lyr = SiM.Layer; int nHits = SiM.hits.size(); - if (nHits == 0) continue; + if (nHits == 0) { + System.out.format("refitTrackWithE: layer %d det %d with no hits\n", lyr, SiM.detector); + continue; + } double vHit = SiM.hits.get(0).v; double eHit = SiM.hits.get(0).sigma; double vTrue = SiM.hits.get(0).vTrue; - System.out.format("refitTrackWithE: layer %d stereo=%b #hits=%d m=%9.5f+-%8.5f vTrue=%9.5f\n", lyr, SiM.isStereo, nHits, vHit, eHit, vTrue); + System.out.format("refitTrackWithE: layer %d det %d stereo=%b #hits=%d m=%9.5f+-%8.5f vTrue=%9.5f\n", lyr, SiM.detector, SiM.isStereo, nHits, vHit, eHit, vTrue); } } @@ -1777,10 +1942,11 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { int startIndex = 0; if (debug) System.out.format("refitTrackWithE: createKTF: using %d SiModules, startIndex %d \n", SiMoccupied.size(), startIndex); - CommonOps_DDRM.scale(10., helixCov); + CommonOps_DDRM.scale(1000., helixCov); // Do the Kalman track fit only up through the filter step - KalTrack newTrack = kalmanFilterTrack(0, track.hashCode(), SiMoccupied, null, kalHelixParams, pivot, helixCov); + KalTrack newTrack = kalmanFilterTrack(event.getEventNumber(), track.hashCode(), SiMoccupied, null, kalHelixParams, pivot, helixCov); + if (newTrack == null) return null; if (debug) newTrack.print("filtered track"); if (newTrack.bad || newTrack.nHits < 6) { @@ -1788,10 +1954,21 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { return track; } - // Include the eCal information and smooth back toward the origin + // Include the eCal information and smooth back toward the target double sigmaE = (kPar.eRes[0]/FastMath.sqrt(energy) + kPar.eRes[1])*energy/100.; - newTrack.energyConstraint(energy, sigmaE); - if (debug) newTrack.print("energy constrained"); + newTrack.smoothIt(eConstraint, energy, sigmaE); + if (debug) { + if (eConstraint) newTrack.print("energy constrained"); + else newTrack.print("newly smoothed"); + } + + // Iterate the track fit + if (!eConstraint) { + newTrack.fit(true); // This refit method does not include energy constraint + if (debug) { + newTrack.print("newly smoothed after refit"); + } + } // Convert the KalTrack object into and HPS Track and TrackState Track outputTrack = createTrack(newTrack, true); @@ -1814,13 +1991,12 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy) { * @return The new filtered KalTrack object */ KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, ArrayList hits, Vec helixParams, Vec pivot, DMatrixRMaj C) { - if (hits == null) { - hits = new ArrayList(data.size()); - for (int i=0; i data, } double B = Bfield.mag(); Vec t = Bfield.unitVec(B); - StateVector sI = new StateVector(-1, helixParams, C, new Vec(0., 0., 0.), B, t, pivot, kPar.uniformB); + StateVector sI = new StateVector(-1, helixParams, C, pivot, B, t, new Vec(0., 0., 0.), kPar.uniformB); + if (debug) sI.print("KalmanFilterTrack initial"); ArrayList sites = new ArrayList(data.size()); MeasurementSite prevSite = null; double chi2f = 0.; @@ -1838,45 +2015,56 @@ KalTrack kalmanFilterTrack(int eventNumber, int tkID, ArrayList data, boolean success = true; for (int idx=0; idx 0) { + if (hits == null) { + hitNumber = 0; + } else { + hitNumber = hits.get(idx); + } } + if (debug) System.out.format("KalmanFilterTrack: layer %d, hit=%d\n", m.Layer, hitNumber); newSite = new MeasurementSite(idx, m, kPar); - int hitNumber = hits.get(idx); if (prevSite == null) { - if (newSite.makePrediction(sI, hitNumber, false, false) < 0) { + int rc = newSite.makePrediction(sI, hitNumber, false, false); + if (rc < 0) { logger.warning(String.format("kalmanFilterTrack: failed to make initial prediction at site %d, idx=%d. Abort", sites.indexOf(newSite), idx)); success = false; break; } + if (debug) System.out.format(" Initial prediction rc=%d\n", rc); } else { - if (newSite.makePrediction(prevSite.aF, prevSite.m, hitNumber, false, false) < 0) { + int rc = newSite.makePrediction(prevSite.aF, prevSite.m, hitNumber, false, false); + if ( rc < 0) { logger.warning(String.format("kalmanFilterTrack: failed to make prediction at site %d, idx=%d. Abort", sites.indexOf(newSite), idx)); success = false; break; } + if (debug) System.out.format(" Prediction at layer %d rc=%d\n", m.Layer, rc); } - + //if (debug) newSite.print("new site"); if (!newSite.filter()) { logger.warning(String.format("kalmanFilterTrack failed to filter at site %d, idx=%d. Ignore remaining sites", sites.indexOf(newSite), idx)); + if (debug) System.out.format("KalmanFilterTrack: aborting at layer %d for bad filter\n", newSite.m.Layer); success = false; break; } if (m.Layer >= 0 && hitNumber >= 0) chi2f += newSite.chi2inc; - + newSite.filtered = true; sites.add(newSite); prevSite = newSite; } if (debug) { for (MeasurementSite site : sites) { - System.out.format("kalmanFilterTrack: hit on layer %d\n", site.m.Layer); + System.out.format("kalmanFilterTrack: hit on layer %d, ID=%d\n", site.m.Layer, site.hitID); } System.out.format("kalmanFilterTrack: filter chi^2 = %9.3f\n", chi2f); } ArrayList yScat = new ArrayList(); ArrayList XLscat = new ArrayList(); - return new KalTrack(eventNumber, tkID, sites, yScat, XLscat, kPar); + if (success) return new KalTrack(eventNumber, tkID, sites, yScat, XLscat, kPar); + else return null; } /** @@ -1895,11 +2083,10 @@ public int compare(SiModule o1, SiModule o2) { * Method to drive the Kalman-Filter based pattern recognition * * @param event Event header - * @param decoder Decoder for hit information * @return Two lists of Kalman KalTrack objects, for top and bottom * detectors */ - public ArrayList[] KalmanPatRec(EventHeader event, IDDecoder decoder) { + public ArrayList[] KalmanPatRec(EventHeader event) { if (debug) { System.out.format("KalmanInterface: entering KalmanPatRec for event %d\n", event.getEventNumber()); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java index 64ec754976..9d4578fd16 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanKinkFitDriver.java @@ -69,7 +69,7 @@ public void detectorChanged(Detector det) { KalmanParams kPar = new KalmanParams(); kPar.print(); - KI = new KalmanInterface(kPar, fm); + KI = new KalmanInterface(kPar, det, fm); KI.createSiModules(detPlanes); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index 8c1529b8e6..ee6e7a9492 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -173,7 +173,7 @@ public KalmanParams() { lowPhThresh = 0.25; // Residual improvement ratio necessary to use a low-ph hit instead of high-ph seedCompThr = 0.05; // Remove SeedTracks with all Helix params within relative seedCompThr . If -1 do not apply duplicate removal eRes = new double[2]; - eRes[0] = 10.0; // Cal energy resolution parameters in % sigmaE = eRes[0]/sqrt(E) + eRes[1] + eRes[0] = 5.0; // Cal energy resolution parameters in % sigmaE = eRes[0]/sqrt(E) + eRes[1] eRes[1] = 1.0; // Load the default search strategies @@ -239,6 +239,13 @@ public void setEnergyRes(double a, double b) { if (b > 0.) eRes[1] = b; logger.config(String.format("Setting CAL energy resolution to %8.2f/sqrt(E) + %8.2f", eRes[0], eRes[1])); } + public double getEres(int i) { + if (i<0 || i>1) { + logger.warning(String.format("KalmanParams: invalid eRes index %d\n", i)); + return eRes[0]; + } + return eRes[i]; + } public void setUniformB(boolean input) { logger.config(String.format("Setting the field to be uniform? %b", input)); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java index 6a6e84225d..03178ddb18 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java @@ -374,7 +374,7 @@ public void detectorChanged(Detector det) { logger.config("KalmanPatRecDriver: done with configuration changes."); kPar.print(); - KI = new KalmanInterface(kPar, fm); + KI = new KalmanInterface(kPar, det, fm); KI.setSiHitsLimit(siHitsLimit); KI.createSiModules(detPlanes); decoder = det.getSubdetector("Tracker").getIDDecoder(); @@ -456,10 +456,8 @@ public int compare(TrackerHit o1, TrackerHit o2) { * @param event input the header for this event * @param outputFullTracks output the list of HPS tracks * @param trackDataCollection output list of data that go with the tracks - * @param trackDataRelations output the relations between tracks and the - * data - * @param allClstrs output all the clusters needed for refitting the Kalman - * tracks by GBL + * @param trackDataRelations output the relations between tracks and the data + * @param allClstrs output all the clusters needed for refitting the Kalman tracks by GBL * @param gblStripClusterDataRelations output relations for the clusters * @param trackResiduals output the residuals for hits on Kalman tracks * @param trackResidualsRelations output relations for the residuals @@ -476,7 +474,7 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List[] kPatList = KI.KalmanPatRec(event, decoder); + ArrayList[] kPatList = KI.KalmanPatRec(event); long endTime = System.nanoTime(); double runTime = (double) (endTime - startTime) / 1000000.; executionTime += runTime; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java index fd31b62ac1..332c995d19 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/MeasurementSite.java @@ -23,7 +23,6 @@ class MeasurementSite { boolean smoothed; // True if the smoothed state vector has been built boolean energyConstrained; // True if the smoothed state vector is energy constrained double chi2inc; // chi^2 increment for this site - double chi2incE; // chi^2 increment for the energy-constrainted track DMatrixRMaj H; // Derivatives of the transformation from state vector to measurement double arcLength; // Arc length from the previous measurement private double conFac; // Conversion from B to alpha @@ -76,7 +75,7 @@ String toString(String s) { Vec tB = Bfield.unitVec(); str=str+String.format(" Magnetic field strength=%10.6f; alpha=%10.6f\n", B, alpha); str = str + tB.toString("magnetic field direction") + "\n"; - str=str+String.format(" chi^2 increment=%12.4e; E constrained=%12.4E\n", chi2inc, chi2incE); + str=str+String.format(" chi^2 increment=%12.4e\n", chi2inc); str=str+String.format(" x scattering angle=%10.8f, y scattering angle=%10.8f\n", scatX(), scatZ()); if (predicted) str = str + aP.toString("predicted"); if (filtered) str = str + aF.toString("filtered"); @@ -121,7 +120,6 @@ String toString(String s) { double sp = 0.002; // Estar collision stopping power for electrons in silicon at about a GeV, in GeV cm2/g dEdx = -0.1 * sp * rho; // in GeV/mm chi2inc = 0.; - chi2incE = 0.; H = new DMatrixRMaj(5,1); if (!initialized) { tempV = new DMatrixRMaj(5,1); @@ -557,7 +555,6 @@ boolean removeHit() { if (hitID < 0) return false; hitID = -1; chi2inc = 0.; - chi2incE = 0.; smoothed = false; filtered = false; return true; diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java index 972c7dd6fc..12cfab4c40 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/SiModule.java @@ -12,7 +12,7 @@ class SiModule { int Layer; // Tracker layer number, or a negative integer for a dummy layer added just for stepping in a // non-uniform field - int detector; // Detector number within the layer + int detector; // Detector or module number within the layer int millipedeID; // ID used by millipede for alignment ArrayList hits; // Hits ordered by coordinate value, from minimum to maximum Plane p; // Orientation and offset of the detector measurement plane in global coordinates From 16befb62ab5dcf84719f7ec4ff42c3acf67bc01a Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Tue, 2 May 2023 16:48:24 -0700 Subject: [PATCH 16/17] fixed compilation error and removed unused argument in a KalTrack method --- .../drivers/trackrecon/KFSVTOpeningAlignment.java | 2 +- .../org/hps/recon/tracking/kalman/HelixTest3.java | 2 +- .../org/hps/recon/tracking/kalman/KalTrack.java | 14 +++++++------- .../recon/tracking/kalman/KalmanPatRecDriver.java | 2 +- .../recon/tracking/kalman/KalmanPatRecPlots.java | 2 +- .../org/hps/recon/tracking/kalman/PatRecTest.java | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/online-monitoring/src/main/java/org/hps/monitoring/drivers/trackrecon/KFSVTOpeningAlignment.java b/online-monitoring/src/main/java/org/hps/monitoring/drivers/trackrecon/KFSVTOpeningAlignment.java index 1ae98b696e..724404c7c8 100644 --- a/online-monitoring/src/main/java/org/hps/monitoring/drivers/trackrecon/KFSVTOpeningAlignment.java +++ b/online-monitoring/src/main/java/org/hps/monitoring/drivers/trackrecon/KFSVTOpeningAlignment.java @@ -280,7 +280,7 @@ protected void detectorChanged(Detector detector) { kPar = new KalmanParams(); kPar.print(); - KI = new KalmanInterface(false, kPar, fm); + KI = new KalmanInterface(false, kPar, detector, fm); KI.createSiModules(detPlanes); // plotterParsBot = pfac.create("Bot Track Pars"); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java index 1f4303e53b..24d97f72c7 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/HelixTest3.java @@ -1021,7 +1021,7 @@ class HelixTest3 { // Program for testing the Kalman fitting code } } for (int layer = 0; layer < nLayers; ++layer) { - Pair resid = KalmanTrack.unbiasedResidual(layer, residualsEconstrained); + Pair resid = KalmanTrack.unbiasedResidual(layer); if (resid.getSecondElement() > -999.) { double variance = resid.getSecondElement(); double sigma = Math.sqrt(variance); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java index 872ecdf06b..afb4339623 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalTrack.java @@ -458,12 +458,12 @@ public double chi2prime() { * @param millipedeID * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidualMillipede(int millipedeID, boolean eConstrain) { + public Pair unbiasedResidualMillipede(int millipedeID) { if (millipedeMap == null) { makeMillipedeMap(); } if (millipedeMap.containsKey(millipedeID)) { - return unbiasedResidual(millipedeMap.get(millipedeID), eConstrain); + return unbiasedResidual(millipedeMap.get(millipedeID)); } else { return new Pair(-999., -999.); } @@ -475,12 +475,12 @@ public Pair unbiasedResidualMillipede(int millipedeID, boolean e * @param layer * @return pair of unbiased residual and its error estimate */ - public Pair unbiasedResidual(int layer, boolean eConstrain) { + public Pair unbiasedResidual(int layer) { if (lyrMap == null) { makeLyrMap(); } if (lyrMap.containsKey(layer)) { - return unbiasedResidual(lyrMap.get(layer), eConstrain); + return unbiasedResidual(lyrMap.get(layer)); } else { return new Pair(-999., -999.); } @@ -493,7 +493,7 @@ public Pair unbiasedResidual(int layer, boolean eConstrain) { * @param site measurement site * @return residual and error */ - public Pair unbiasedResidual(MeasurementSite site, boolean eConstrain) { + public Pair unbiasedResidual(MeasurementSite site) { double resid = -999.; double varResid = -999.; Vec aStar = null; @@ -609,7 +609,7 @@ public void print(String s) { else aA = site.aS; System.out.format(" t=%5.1f ", site.m.hits.get(site.hitID).time); double residual = site.m.hits.get(hitID).v - aA.mPred; - Pair unBiasedResid = unbiasedResidual(site, false); + Pair unBiasedResid = unbiasedResidual(site); double[] lclint = moduleIntercept(m, null); System.out.format(" measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f, x=%9.5f limit=%9.5f \n", site.m.hits.get(hitID).v, aA.mPred, residual, unBiasedResid.getFirstElement(), lclint[0], m.xExtent[1]); @@ -690,7 +690,7 @@ String toString(String s) { Vec interceptVec = interceptVects().get(site); Vec interceptMomVec = interceptMomVects().get(site); double residual = site.m.hits.get(hitID).v - site.aS.mPred; - Pair unBiasedResid = unbiasedResidual(site, false); + Pair unBiasedResid = unbiasedResidual(site); str = str + String.format(" Intercept=%s, p=%s, measurement=%10.5f, predicted=%10.5f, residual=%9.5f, unbiased=%9.5f+-%9.5f, error=%9.5f \n", interceptVec.toString(), interceptMomVec.toString(), site.m.hits.get(hitID).v, site.aS.mPred, residual, unBiasedResid.getFirstElement(), unBiasedResid.getSecondElement(), FastMath.sqrt(site.aS.R)); } diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java index 03178ddb18..409bdb3a7e 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecDriver.java @@ -572,7 +572,7 @@ private ArrayList[] prepareTrackCollections(EventHeader event, List sigmas = new ArrayList(); for (int ilay = 0; ilay < 14; ilay++) { - Pair res_and_sigma = kTk.unbiasedResidual(ilay, false); + Pair res_and_sigma = kTk.unbiasedResidual(ilay); if (res_and_sigma.getSecondElement() > -1.) { layers.add(ilay); residuals.add(res_and_sigma.getFirstElement()); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java index 9a81930328..08a8d8aafb 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanPatRecPlots.java @@ -577,7 +577,7 @@ void process(EventHeader event, List sensors, ArrayList[] aida.histogram1D(String.format("Layers/Kalman kink in xy, layer %d", mod.Layer)).fill(kTk.scatX(mod.Layer)); aida.histogram1D(String.format("Layers/Kalman kink in zy, layer %d", mod.Layer)).fill(kTk.scatZ(mod.Layer)); } - Pair residPr = kTk.unbiasedResidual(site.m.Layer, false); + Pair residPr = kTk.unbiasedResidual(site.m.Layer); if (residPr.getSecondElement() > -999. && kTk.nHits >= 10) { double variance = residPr.getSecondElement(); if (variance <= 0.) { diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java index 2e408223d4..b408fb4ba2 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/PatRecTest.java @@ -642,7 +642,7 @@ class PatRecTest { } } for (int layer=2; layer < nLayers; ++layer) { - Pair resid = tkr.unbiasedResidual(layer, false); + Pair resid = tkr.unbiasedResidual(layer); if (resid.getSecondElement() > -999.) { double unbResid = resid.getFirstElement(); double variance = resid.getSecondElement(); From c2b5ed140c6bb375bfb91dbb735da058c31fe1d0 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Wed, 10 May 2023 23:41:44 -0700 Subject: [PATCH 17/17] fixed bugs in hit-removal code --- .../ReconstructedParticleRefitter.java | 40 +++++++++++++++---- .../tracking/kalman/KalmanInterface.java | 7 ++-- .../recon/tracking/kalman/KalmanParams.java | 14 ++++--- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java index aa6f472704..ddcd4fe765 100644 --- a/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java +++ b/recon/src/main/java/org/hps/recon/particle/ReconstructedParticleRefitter.java @@ -91,6 +91,7 @@ public class ReconstructedParticleRefitter extends Driver { private double _eRes0 = -1.0; private double _eRes1 = -1.0; + private double _eRes2 = -1.0; private static final boolean debug = false; /** @@ -129,7 +130,7 @@ public void set_removeLayer(int lyr) { } /** * ECal energy resolution parameterization, for the resolution as a % of E. - * @param eRes0 coefficient of the 1/sqrt(E) term + * @param eRes0 coefficient of the 1/E term */ public void set_eRes0(double eRes0) { System.out.format("ReconstructedParticleRefitter: setting the eRes0 ECAL resolution parameter to %9.4f\n", eRes0); @@ -137,11 +138,19 @@ public void set_eRes0(double eRes0) { } /** * ECal energy resolution parameterization, for the resolution as a % of E. - * @param eRes1 the constant term + * @param eRes1 coefficient of the 1/sqrt(E) term */ public void set_eRes1(double eRes1) { System.out.format("ReconstructedParticleRefitter: setting the eRes1 ECAL resolution parameter to %9.4f\n", eRes1); _eRes1 = eRes1; + } + /** + * ECal energy resolution parameterization, for the resolution as a % of E. + * @param eRes2 the constant term + */ + public void set_eRes2(double eRes2) { + System.out.format("ReconstructedParticleRefitter: setting the eRes2 ECAL resolution parameter to %9.4f\n", eRes2); + _eRes2 = eRes2; } /** @@ -169,7 +178,7 @@ public void detectorChanged(Detector det) { // Instantiate the interface to the Kalman-Filter code and set up the run parameters kPar = new KalmanParams(); // Override the default resolution parameters with numbers from the steering file - if (_eRes0 > 0. || _eRes1 > 0.) kPar.setEnergyRes(_eRes0, _eRes1); + if (_eRes0 > 0. || _eRes1 > 0. || _eRes2 > 0.) kPar.setEnergyRes(_eRes0, _eRes1, _eRes2); fm = det.getFieldMap(); // The HPS magnetic field map KI = new KalmanInterface(kPar, det, fm); // Instantiate the Kalman interface @@ -268,7 +277,9 @@ public void process(EventHeader event) { } } if (newTsAtIP == null) newTsAtIP = newTrack.getTrackStates().get(0); - double[] newParams = newTsAtIP.getParameters(); + double[] newParams = newTsAtIP.getParameters(); + aida.histogram1D("New helix d0", 100, -5., 5.).fill(newParams[0]); + aida.histogram1D("Old helix d0", 100, -5., 5.).fill(oldParams[0]); aida.histogram1D("Helix parameter d0 new minus old over old", 100, -2.5, 2.5).fill((newParams[0]-oldParams[0])/oldParams[0]); aida.histogram1D("Helix parameter phi0 new minus old over old", 100, -0.5, 0.5).fill((newParams[1]-oldParams[1])/oldParams[1]); aida.histogram1D("Helix parameter omega new minus old over old", 100, -0.5, 0.5).fill((newParams[2]-oldParams[2])/oldParams[2]); @@ -434,6 +445,20 @@ private MCParticle getMCmatch(EventHeader event, ReconstructedParticle rp) { } return theMatch; } + + /** + * ECAL energy resolution + * + * @param ECAL energy + * @return expected sigma of the energy measurement + */ + static double sigmaE(double E) { + double r1 = 1.62/E; + double r2 = 2.87/FastMath.sqrt(E); + double r3 = 2.5; + double res = FastMath.sqrt(r1*r1 + r2*r2 + r3*r3)/100.; + return res*E; + } /** * Method for refitting a track * @@ -454,16 +479,15 @@ private Track refitTrack(EventHeader event, ReconstructedParticle rp) { MCParticle theMatch = getMCmatch(event, rp); if (theMatch != null) { double energyMC = theMatch.getEnergy(); - double sigmaE = (kPar.getEres(0)/FastMath.sqrt(energy) + kPar.getEres(1))*energy/100.; - energy = energyMC + sigmaE*ran.nextGaussian(); - if (debug) System.out.format("refitTrack: MC cheat energy = %10.5f+=%9.5f\n", energy, sigmaE); + energy = energyMC + sigmaE(energyMC)*ran.nextGaussian(); + if (debug) System.out.format("refitTrack: MC cheat energy = %10.5f+=%9.5f\n", energy, sigmaE(energyMC)); aida.histogram1D("Cheat ECAL over MC energy", 100, 0.5, 1.5).fill(energy/energyMC); } else { System.out.format("refitTrack: no MC match was found\n"); } } if (layerSkip == null) layerSkip = new ArrayList(); - return KI.refitTrackWithE(event, track, energy, _doEconstraint, layerSkip); + return KI.refitTrackWithE(event, track, energy, sigmaE(energy), _doEconstraint, layerSkip); } /** diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java index ca2a660aa4..9d6e9ca054 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanInterface.java @@ -1734,11 +1734,12 @@ public KalmanTrackFit2 createKalmanTrackFit(EventHeader event, Vec helixParams, * @param event Event header * @param track HPS track to refit * @param energy ECal energy in case the constraint is needed (otherwise not used) + * @param sigmaE ECAL energy resolution * @param eConstraint true to include the ECal energy constraint * @param layerSkip List of layers to skip in the fit (any hit on these layers is deleted) * @return The new HPS track */ - public Track refitTrackWithE(EventHeader event, Track track, double energy, boolean eConstraint, ArrayList layerSkip) { + public Track refitTrackWithE(EventHeader event, Track track, double energy, double sigmaE, boolean eConstraint, ArrayList layerSkip) { // First we need initial guesses for the helix parameters and covariance. // Preferentially take them from a TrackState. If there is no TrackState, // then estimate from a linear fit to the set of hits. @@ -1830,8 +1831,9 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy, bool int Layer = decoder.getValue("layer") - 1; for (int lyr : layerSkip) { if (lyr == Layer) { - hitsOnTrack.remove(hit); + iter.remove(); if (debug) System.out.format("KalmanInterface.refitTrackWithE: removing hit on layer %d\n", Layer); + break; } } } @@ -1959,7 +1961,6 @@ public Track refitTrackWithE(EventHeader event, Track track, double energy, bool } // Include the eCal information and smooth back toward the target - double sigmaE = (kPar.eRes[0]/FastMath.sqrt(energy) + kPar.eRes[1])*energy/100.; newTrack.smoothIt(eConstraint, energy, sigmaE); if (debug) { if (eConstraint) newTrack.print("energy constrained"); diff --git a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java index ee6e7a9492..f1c57b063b 100644 --- a/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java +++ b/tracking/src/main/java/org/hps/recon/tracking/kalman/KalmanParams.java @@ -91,7 +91,7 @@ public void print() { System.out.format(" Maximum chi^2 for 5-hit tracks with a vertex constraint: %8.2f\n", mxChi2Vtx); System.out.format(" Include ionization energy loss in fit = %b\n", eLoss); System.out.format(" Default origin to use for vertex constraints:\n"); - System.out.format(" CAL energy resolution = %8.2f /sqrt(E) + %8.2f\n", eRes[0], eRes[1]); + System.out.format(" ECAL percent energy resolution = %8.2f /E + %8.2f /sqrt(E) + %8.2f in quadrature\n", eRes[0], eRes[1], eRes[2]); for (int i=0; i<3; ++i) { System.out.format(" %d: %8.3f +- %8.3f\n", i, beamSpot[i], vtxSize[i]); } @@ -172,9 +172,10 @@ public KalmanParams() { firstLayer = 0; // First layer in the tracking system (2 for pre-2019 data) lowPhThresh = 0.25; // Residual improvement ratio necessary to use a low-ph hit instead of high-ph seedCompThr = 0.05; // Remove SeedTracks with all Helix params within relative seedCompThr . If -1 do not apply duplicate removal - eRes = new double[2]; - eRes[0] = 5.0; // Cal energy resolution parameters in % sigmaE = eRes[0]/sqrt(E) + eRes[1] - eRes[1] = 1.0; + eRes = new double[3]; + eRes[0] = 1.62; // Cal energy resolution parameters in % sigmaE = eRes[0]/E + eRes[1]/sqrt(E) + eRes[2] in quadrature + eRes[1] = 2.87; + eRes[2] = 2.5; // Load the default search strategies // Index 0 is for the bottom tracker (+z), 1 for the top (-z) @@ -234,10 +235,11 @@ public KalmanParams() { // } } - public void setEnergyRes(double a, double b) { + public void setEnergyRes(double a, double b, double c) { if (a > 0.) eRes[0] = a; if (b > 0.) eRes[1] = b; - logger.config(String.format("Setting CAL energy resolution to %8.2f/sqrt(E) + %8.2f", eRes[0], eRes[1])); + if (c > 0.) eRes[2] = c; + logger.config(String.format("Setting CAL energy resolution to %8.2f/E + %8.2f/sqrt(E) + %8.2f", eRes[0], eRes[1], eRes[2])); } public double getEres(int i) { if (i<0 || i>1) {