diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0aef82debb458e44c3b9bfa1dceb4d2f79548bee..521e9b519646214a6154fc49f8195c60ed6806b2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,10 +16,11 @@ build_image:
   # description: triggers a build of the project as a Docker image,
   #              each branch will have an individual Docker image that will be used
   #              in the following stages of the pipeline for testing the code
+  image:
+    name: gitlab-registry.cern.ch/linuxsupport/cc7-base:latest
   stage: build
   tags:
-    - cvmfs
-    - docker
+    - k8s-cvmfs
   script:
     - yum -y install redhat-lsb redhat-lsb-core man uuid-devel libuuid libuuid-devel mesa-libGL-devel libXpm-devel
     - mkdir build
@@ -33,12 +34,13 @@ build_image:
     - build/
 
 test_unittest:
+  image:
+    name: gitlab-registry.cern.ch/linuxsupport/cc7-base:latest
   stage: test
   tags:
-    - cvmfs
-    - docker
+    - k8s-cvmfs
   script: 
-    - yum -y install man
+    - yum -y install man git make cmake3 gcc-c++ gcc binutils libX11-devel libXpm-devel libXft-devel libXext-devel python openssl-devel
     - cd build
     - set +e && source ${ATLAS_LOCAL_ROOT_BASE}/user/atlasLocalSetup.sh; set -e
     - set +e && asetup --input=../../calypso/asetup.faser Athena,22.0.49; set -e 
@@ -46,3 +48,6 @@ test_unittest:
     - ctest -j3
   dependencies:
     - build_image
+  artifacts:
+    paths:
+      - LastTest.log
diff --git a/Control/CalypsoExample/TruthEventDumper/python/TruthEventDumperAlg.py b/Control/CalypsoExample/TruthEventDumper/python/TruthEventDumperAlg.py
index 934433e2f375d6f0883a5a66e66dba70f1515427..8986c1165ece677bb37a42998bc2e0d05642cd8e 100644
--- a/Control/CalypsoExample/TruthEventDumper/python/TruthEventDumperAlg.py
+++ b/Control/CalypsoExample/TruthEventDumper/python/TruthEventDumperAlg.py
@@ -36,4 +36,4 @@ class TruthEventDumperAlg(PyAthena.Alg):
         # print("Mid part: ", self.maxMid, " out of ", 1000000 - 200000, " (",100*self.maxMid/(1000000-200000),"% of overflow")
         # print("Hi  part: ", self.maxHi, " out of ", (1<<31)//1000000, " (", 100*self.maxHi/((1<<31)//1000000),"% of overflow")
 
-        return StatusCode.Success
\ No newline at end of file
+        return StatusCode.Success
diff --git a/DetectorDescription/GeoModel/FaserGeoModel/data/geomDB.sql b/DetectorDescription/GeoModel/FaserGeoModel/data/geomDB.sql
index 046c5462f7dde50bdaa43a4851b9b303c82a4770..d4f4890493841ab030f6570cecb52742fba53f69 100644
--- a/DetectorDescription/GeoModel/FaserGeoModel/data/geomDB.sql
+++ b/DetectorDescription/GeoModel/FaserGeoModel/data/geomDB.sql
@@ -1038,6 +1038,8 @@ INSERT INTO "HVS_TAG2NODE" VALUES (41, "Ecal-TB01", 107857, NULL, 0, 0,    16858
 INSERT INTO "HVS_TAG2NODE" VALUES (410, "EcalTopLevel-00", 100058, NULL, 0, 0, 1599350400000000000, NULL, 22);
 INSERT INTO "HVS_TAG2NODE" VALUES (410, "EcalTopLevel-02", 107797, NULL, 0, 0, 1619222400000000000, NULL, 22);
 INSERT INTO "HVS_TAG2NODE" VALUES (410, "EcalTopLevel-TB00", 107822, NULL, 0, 0, 1627862400000000000, NULL, 22);
+INSERT INTO "HVS_TAG2NODE" VALUES (410, "EcalTopLevel-04", 107886, NULL, 0, 0,   1695600000000000000, NULL, 22);
+INSERT INTO "HVS_TAG2NODE" VALUES (410, "EcalTopLevel-TB01", 107887, NULL, 0, 0, 1695600000000000000, NULL, 22);
 INSERT INTO "HVS_TAG2NODE" VALUES (411, "EcalRowGeneral-00", 100059, NULL, 0, 0, 1599350400000000000, NULL, 22);
 INSERT INTO "HVS_TAG2NODE" VALUES (411, "EcalRowGeneral-TB00", 107830, NULL, 0, 0, 1627862400000000000, NULL, 22);
 INSERT INTO "HVS_TAG2NODE" VALUES (414, "EcalSwitches-00", 100057, NULL, 0, 0, 1593907200000000000, NULL, 22);
@@ -2009,7 +2011,7 @@ INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "DipoleSwitches",     "DipoleSw
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "DipoleWrappingGeneral", "DipoleWrappingGeneral-01",   107885);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "Calorimeter",        "Calorimeter-04",          107858);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "Ecal",               "Ecal-04",                 107856);
-INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "EcalTopLevel",       "EcalTopLevel-02",         107797);
+INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "EcalTopLevel",       "EcalTopLevel-04",         107886);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "EcalRowGeneral",     "EcalRowGeneral-00",       100059);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "EcalSwitches",       "EcalSwitches-01",         107855);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASERNU-04", "VetoTopLevel",       "VetoTopLevel-02",         107798);
@@ -2087,7 +2089,7 @@ INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "DipoleSwitches",     "DipoleSw
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "DipoleWrappingGeneral", "DipoleWrappingGeneral-01",   107885);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "Calorimeter",        "Calorimeter-TB01",        107859);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "Ecal",               "Ecal-TB01",               107857);
-INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "EcalTopLevel",       "EcalTopLevel-TB00",       107822);
+INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "EcalTopLevel",       "EcalTopLevel-TB00",       107887);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "EcalRowGeneral",     "EcalRowGeneral-TB00",     107830);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "EcalSwitches",       "EcalSwitches-01",         107855);
 INSERT INTO "HVS_TAGCACHE" VALUES ("FASER-TB01", "VetoTopLevel",       "VetoTopLevel-TB00",       107823);
@@ -2856,13 +2858,17 @@ INSERT INTO "PRESHOWERWRAPPINGGENERAL_DATA2TAG" VALUES (107875, 0);
 INSERT INTO "PRESHOWERWRAPPINGGENERAL_DATA2TAG" VALUES (107876, 1);
 --
 --
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (0, 0.0,   0.0, 3099.2, 0.0, 0.0, 0.0, 321, "Ecal");
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (1, 0.0, -71.6,    0.0, 0.0, 2.8, 0.0, 321, "BottomRow");
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (2, 0.0,  49.6,    0.0, 0.0, 2.8, 0.0, 321, "TopRow");
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (3, 0.0,   0.0, 3098.9, 0.0, 0.0, 0.0, 321, "Ecal");
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (4, 0.0, -71.6,    0.0, 0.0, 0.0, 0.0, 321, "BottomRow");
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (5, 0.0,  49.6,    0.0, 0.0, 0.0, 0.0, 321, "TopRow");
-INSERT INTO "ECALTOPLEVEL_DATA" VALUES (6, -17.22, -22.80, 1088.66, -2.8, -2.8, 0.0, 321, "Ecal");
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (0, 0.0,   0.0, 3099.2, 0.0, 0.0, 0.0, 321, "Ecal"); -- Old TI12
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (1, 0.0, -71.6,    0.0, 0.0, 2.8, 0.0, 321, "BottomRow"); -- TI12 (with overlap)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (2, 0.0,  49.6,    0.0, 0.0, 2.8, 0.0, 321, "TopRow");    -- TI12 (with overlap)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (3, 0.0,   0.0, 3098.9, 0.0, 0.0, 0.0, 321, "Ecal"); -- TI12
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (4, 0.0, -71.6,    0.0, 0.0, 0.0, 0.0, 321, "BottomRow"); -- TB00 (with overlap)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (5, 0.0,  49.6,    0.0, 0.0, 0.0, 0.0, 321, "TopRow");    -- TB00 (with overlap)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (6, -17.22, -22.80, 1088.66, -2.8, -2.8, 0.0, 321, "Ecal"); -- test beam
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (7, 0.0, -72.0,    0.0, 0.0, 2.8, 0.0, 321, "BottomRow");   -- TI12 (overlap fixed)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (8, 0.0,  50.0,    0.0, 0.0, 2.8, 0.0, 321, "TopRow");      -- TI12 (overlap fixed)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (9, 0.0, -72.0,    0.0, 0.0, 0.0, 0.0, 321, "BottomRow");   -- TB01 (overlap fixed)
+INSERT INTO "ECALTOPLEVEL_DATA" VALUES (10, 0.0, 50.0,    0.0, 0.0, 0.0, 0.0, 321, "TopRow");     -- TB01 (overlap fixed)
 INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (100058, 0);
 INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (100058, 1);
 INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (100058, 2);
@@ -2872,6 +2878,13 @@ INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107797, 3);
 INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107822, 4);
 INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107822, 5);
 INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107822, 6);
+INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107886, 3);
+INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107886, 7);
+INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107886, 8);
+INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107887, 6);
+INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107887, 9);
+INSERT INTO "ECALTOPLEVEL_DATA2TAG" VALUES (107887, 10);
+
 --
 --
 INSERT INTO "ECALROWGENERAL_DATA" VALUES (0, 2, 0.0, 2.8);
diff --git a/Generators/TestBeamReader/CMakeLists.txt b/Generators/TestBeamReader/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..64e5ec13d29414fcbf2468e64170029a8cf6c44b
--- /dev/null
+++ b/Generators/TestBeamReader/CMakeLists.txt
@@ -0,0 +1,11 @@
+################################################################################
+# Package: ReadTestBeam
+################################################################################
+
+# Declare the package name:
+atlas_subdir( TestBeamReader )
+
+# Install files from the package:
+atlas_install_python_modules( python/*.py POST_BUILD_CMD ${ATLAS_FLAKE8} )
+
+atlas_install_joboptions( share/*.py )
\ No newline at end of file
diff --git a/Generators/TestBeamReader/python/TestBeamReaderAlgH2_CMS.py b/Generators/TestBeamReader/python/TestBeamReaderAlgH2_CMS.py
new file mode 100644
index 0000000000000000000000000000000000000000..959242ab0aaa0639c672ba669b9c16400f5f726d
--- /dev/null
+++ b/Generators/TestBeamReader/python/TestBeamReaderAlgH2_CMS.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+
+from AthenaCommon.AppMgr import ServiceMgr as svcMgr
+from GeneratorModules.EvgenAlg import EvgenAlg
+from AthenaPython.PyAthena import StatusCode, EventInfo, EventID, EventType
+from AthenaCommon.SystemOfUnits import GeV, m
+import ROOT
+import math
+
+
+class TestBeamReader(EvgenAlg):
+    def __init__(self, name="TestBeamReader", MCEventKey="BeamTruthEvent"):
+        super(TestBeamReader,self).__init__(name=name)
+        self.McEventKey = MCEventKey
+        return
+
+    def fillEvent(self, evt):
+        try:
+          from AthenaPython.PyAthena import HepMC3  as HepMC
+        except ImportError:
+          from AthenaPython.PyAthena import HepMC   as HepMC
+        evt.weights().push_back(1.0)
+        eventNumber = 0
+        treeEventId = 0
+        runNumber = 100
+        mcEventType = EventType()
+        mcEventType.add_type(EventType.IS_SIMULATION)
+
+        mcEventId = EventID(run_number = runNumber, event_number = eventNumber)
+        mcEventInfo = EventInfo(id = mcEventId, type = mcEventType)
+        self.evtStore.record(mcEventInfo, "McEventInfo", True, False)
+        ROOT.SetOwnership(mcEventType, False)
+        ROOT.SetOwnership(mcEventId, False)
+        ROOT.SetOwnership(mcEventInfo, False)
+        pos = HepMC.FourVector(13.8*0.001*m, 22.7*0.001*m, 0*m, 0)
+        gv = HepMC.GenVertex(pos)
+        ROOT.SetOwnership(gv, False)
+        evt.add_vertex(gv)
+
+        nParticles = list(self.evtStore["N"])
+
+        pdgclol = list(self.evtStore["vecPDG"])
+        px =list(self.evtStore["vecPx"])
+        py = list(self.evtStore["vecPy"])
+        pz = list(self.evtStore["vecPz"])
+       
+        for i in range(nParticles[0]):
+            gp = HepMC.GenParticle()
+            pdgc = int()
+
+            pdgc = math.floor(pdgclol[i])
+            if (pdgc ==-11 or pdgc ==11):
+                 M = (0.511*0.001)
+            elif (pdgc == 22):
+                 M = 0.0
+            else:
+                 M = 0.0
+            E = math.sqrt(((px[i])*(px[i]))+((py[i])*(py[i]))+((pz[i])*(pz[i]))+((M)*(M)))  
+            mom = HepMC.FourVector(px[i]*0.001*GeV, py[i]*0.001*GeV, pz[i]*0.001*GeV, E*0.001*GeV)            
+            gp.set_momentum(mom)
+            gp.set_generated_mass(M*GeV)
+            gp.set_pdg_id(pdgc)
+
+            hepmc_status = 4
+            gp.set_status(hepmc_status)
+            ROOT.SetOwnership(gp, False)
+            gv.add_particle_in(gp)
+            
+         
+
+        for i in range(nParticles[0]):
+            gp = HepMC.GenParticle()
+
+            pdgc = int()
+            pdgc = math.floor(pdgclol[i])
+            if (pdgc ==-11 or pdgc ==11):
+                 M = (0.511*0.001)
+            elif (pdgc == 22):
+                 M = 0.0
+            else:
+                 M = 0.0
+            E = math.sqrt(((px[i])*(px[i]))+((py[i])*(py[i]))+((pz[i])*(pz[i]))+((M)*(M)))
+            mom = HepMC.FourVector(px[i]*0.001*GeV, py[i]*0.001*GeV, pz[i]*0.001*GeV, E*0.001*GeV)
+            gp.set_momentum(mom)
+            gp.set_generated_mass(M*GeV)
+            gp.set_pdg_id(pdgc)
+                                                                       
+            hepmc_status = 1
+            gp.set_status(hepmc_status)
+            ROOT.SetOwnership(gp, False)
+                                                                                           
+            gv.add_particle_out(gp)    
+
+
+        return StatusCode.Success
diff --git a/Generators/TestBeamReader/python/__init__.py b/Generators/TestBeamReader/python/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d13ae164caa4e93bf2899cea4e67d0ec515784a2
--- /dev/null
+++ b/Generators/TestBeamReader/python/__init__.py
@@ -0,0 +1 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
diff --git a/Generators/TestBeamReader/share/Numpy_H2.py b/Generators/TestBeamReader/share/Numpy_H2.py
new file mode 100644
index 0000000000000000000000000000000000000000..9644c3d584d8157b8aa6b1acdcba8d1b7fad5b32
--- /dev/null
+++ b/Generators/TestBeamReader/share/Numpy_H2.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python                                                                                                                         
+
+# Set up (Py)ROOT.                                                                                                                            
+import ROOT
+import datetime
+from array import array
+import math
+import datetime
+import pickle
+import pandas
+import uproot3
+import uproot
+import numpy as np
+import awkward as ak
+t1 = uproot3.open("/eos/project/f/faser-preshower/simulations/H2_beamline/1M_events/FASER_e-_v46_80GeV.root")["NTuples/GoodParticle"]
+
+
+FASERdf= t1.pandas.df(["FASER_Px", "FASER_Py","FASER_Pz" ,"FASER_PDGid", "FASER_EventID", "FASER_x", "FASER_y"] )
+pandas.set_option("display.max_rows", None, "display.max_columns", None)
+
+
+FASERdf_new = FASERdf.groupby('FASER_EventID').agg(list)
+
+
+FASERdf_new =FASERdf_new.drop(FASERdf_new.index[0])
+
+
+data = {key: FASERdf_new[key].values for key in [ "FASER_Px", "FASER_Py","FASER_Pz" ,"FASER_PDGid", "FASER_x", "FASER_y"]}
+
+a = ak.Array(data["FASER_Px"])
+print(a)
+#ta =a 
+b = ak.Array(data["FASER_Py"])
+print(b)
+c = ak.Array(data["FASER_Pz"])
+print(c)
+d = ak.Array(data["FASER_PDGid"])
+print(d)
+e = ak.Array(data["FASER_x"])
+print(e)
+f = ak.Array(data["FASER_y"])
+print(f)
+
+with uproot.recreate("80GeV_first.root") as rootfile:
+     rootfile["tree1"] = ak.zip({"FASER_Px": a, "FASER_Py": b, "FASER_Pz": c, "FASER_PDGid":d, "FASER_x":e, "FASER_y": f})
+
+f = ROOT.TFile.Open("80GeV_e-_calypso_H2input.root", "recreate")
+t = ROOT.TTree("t","t")
+
+f1 = ROOT.TFile("80GeV_first.root")
+tree1 = f1.Get("tree1")
+
+
+
+
+vecPx = ROOT.std.vector(float)()
+vecPy = ROOT.std.vector(float)()
+vecPz = ROOT.std.vector(float)()
+vecPDG = ROOT.std.vector(float)()
+Num = ROOT.std.vector(int)()
+t.Branch("vecPx", vecPx)
+t.Branch("vecPy", vecPy)
+t.Branch("vecPz", vecPz)
+t.Branch("vecPDG", vecPDG)
+t.Branch("N", Num)
+print(tree1.GetEntries())
+for x in range(tree1.GetEntries()):
+     tree1.GetEntry(x)
+     px= list(tree1.FASER_Px)
+     py= list(tree1.FASER_Py)
+     pz=list(tree1.FASER_Pz)
+     pdg=list(tree1.FASER_PDGid)
+     n = (tree1.n)
+     j = 0
+     vecPx.clear()
+     vecPy.clear()
+     vecPz.clear()
+     vecPDG.clear()
+     Num.clear()
+     for j in range(n):
+      vecPx.push_back(px[j])
+      vecPy.push_back(py[j])
+      vecPz.push_back(pz[j])
+      vecPDG.push_back(pdg[j])
+      Num.push_back(n)                                                                                          
+     t.Fill()
+
+
+f.Write()
+f.Close()
diff --git a/Generators/TestBeamReader/share/TestBeamReader_jobOptionsH2_CMS.py b/Generators/TestBeamReader/share/TestBeamReader_jobOptionsH2_CMS.py
new file mode 100644
index 0000000000000000000000000000000000000000..3656ceefec3cfa79d329341d492e2f9c7e0e0e29
--- /dev/null
+++ b/Generators/TestBeamReader/share/TestBeamReader_jobOptionsH2_CMS.py
@@ -0,0 +1,39 @@
+#
+#  Usage: athena.py -c'INPUT=["/eos/project/f/faser-preshower/simulations/H2_beamline/1M_events/FASER_e-_v46_100GeV.root"]; OUTPUT="100GeV.H2.EVNT.pool.root"' TestBeamReader/TestBeamReader_jobOptionsH2_CMS.py
+#  INPUT and OUTPUT are mandatory (INPUT can be a list, as shown above)
+#  EVTMAX can be omitted; then all input files are read to the end
+#
+
+if not 'INPUT' in dir():
+    print("Missing INPUT parameter")
+    exit()
+
+if not isinstance(INPUT, (list,tuple)):
+    INPUT = [INPUT]
+    pass
+
+if not 'OUTPUT' in dir():
+    print("Missing OUTPUT parameter")
+    exit()
+
+import AthenaRootComps.ReadAthenaRoot
+
+svcMgr.EventSelector.InputCollections = INPUT
+svcMgr.EventSelector.TupleName = "t"
+
+from TestBeamReader.TestBeamReaderAlgH2_CMS import TestBeamReader
+
+from AthenaCommon.AlgSequence import AlgSequence
+job = AlgSequence()
+job += TestBeamReader()
+
+from AthenaPoolCnvSvc.WriteAthenaPool import AthenaPoolOutputStream
+ostream = AthenaPoolOutputStream( "StreamEVGEN" , OUTPUT, noTag=True )
+ostream.ItemList.remove("EventInfo#*")
+ostream.ItemList += [ "EventInfo#McEventInfo", 
+                      "McEventCollection#*" ]
+print(ostream.ItemList)
+if not 'EVTMAX' in dir():
+    EVTMAX = -1
+
+theApp.EvtMax = EVTMAX
diff --git a/PhysicsAnalysis/NtupleDumper/CMakeLists.txt b/PhysicsAnalysis/NtupleDumper/CMakeLists.txt
index 607c58d892d201731098f64829a82d556f1a302a..dd8faf032f48b2c3d78453b84494e1903d64367d 100644
--- a/PhysicsAnalysis/NtupleDumper/CMakeLists.txt
+++ b/PhysicsAnalysis/NtupleDumper/CMakeLists.txt
@@ -7,7 +7,7 @@ atlas_add_component(
         src/NtupleDumperAlg.h
         src/NtupleDumperAlg.cxx
         src/component/NtupleDumper_entries.cxx
-        LINK_LIBRARIES AthenaBaseComps StoreGateLib xAODFaserWaveform xAODFaserCalorimeter xAODFaserTrigger xAODFaserLHC ScintIdentifier FaserCaloIdentifier GeneratorObjects FaserActsGeometryLib TrackerSimEvent TrackerSimData TrackerIdentifier TrackerReadoutGeometry TrkTrack GeoPrimitives TrackerRIO_OnTrack TrackerSpacePoint FaserActsKalmanFilterLib
+        LINK_LIBRARIES AthenaBaseComps StoreGateLib xAODFaserWaveform xAODFaserCalorimeter xAODFaserTrigger xAODFaserLHC ScintIdentifier FaserCaloIdentifier GeneratorObjects FaserActsGeometryLib TrackerSimEvent TrackerSimData TrackerIdentifier TrackerReadoutGeometry TrkTrack GeoPrimitives TrackerRIO_OnTrack TrackerSpacePoint FaserActsKalmanFilterLib FaserActsmanVertexingLib
 	PRIVATE_LINK_LIBRARIES nlohmann_json::nlohmann_json 
 )
 
diff --git a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx
index 6e4c2aae531c6feee89df6102a90d118f6276dff..0e88d1e46f6d84ed87bda1b669c9f4ca35aa3ea6 100644
--- a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx
+++ b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.cxx
@@ -17,7 +17,9 @@
 #include <cmath>
 #include <TH1F.h>
 #include <numeric>
+#include <limits>
 
+constexpr double NaN = std::numeric_limits<double>::quiet_NaN();
 using namespace Acts::UnitLiterals;
 
 NtupleDumperAlg::NtupleDumperAlg(const std::string &name, 
@@ -119,6 +121,7 @@ StatusCode NtupleDumperAlg::initialize()
   ATH_CHECK(m_trackingGeometryTool.retrieve());
   ATH_CHECK(m_trackTruthMatchingTool.retrieve());
   ATH_CHECK(m_fiducialParticleTool.retrieve());
+  ATH_CHECK(m_vertexingTool.retrieve());
 
   ATH_CHECK(m_spacePointContainerKey.initialize());
 
@@ -222,6 +225,9 @@ StatusCode NtupleDumperAlg::initialize()
   m_tree->Branch("longTracks", &m_longTracks, "longTracks/I");
   m_tree->Branch("Track_Chi2", &m_Chi2);
   m_tree->Branch("Track_nDoF", &m_DoF);
+  m_tree->Branch("Track_module_eta0", &m_module_eta0);
+  m_tree->Branch("Track_module_phi0", &m_module_phi0);
+  m_tree->Branch("Track_hitSet", &m_hitSet);
   m_tree->Branch("Track_x0", &m_xup);
   m_tree->Branch("Track_y0", &m_yup);
   m_tree->Branch("Track_z0", &m_zup);
@@ -289,6 +295,8 @@ StatusCode NtupleDumperAlg::initialize()
   //TRUTH
   m_tree->Branch("t_pdg", &m_t_pdg);
   m_tree->Branch("t_barcode", &m_t_barcode);
+  m_tree->Branch("t_pdg_parent", &m_t_pdg_parent);
+  m_tree->Branch("t_barcode_parent", &m_t_barcode_parent);
   m_tree->Branch("t_truthHitRatio", &m_t_truthHitRatio);
 
   m_tree->Branch("t_prodVtx_x", &m_t_prodVtx_x);
@@ -375,6 +383,19 @@ StatusCode NtupleDumperAlg::initialize()
   m_tree->Branch("truthd1_y", &m_truthd1_y);
   m_tree->Branch("truthd1_z", &m_truthd1_z);
 
+  m_tree->Branch("vertex_x", &m_vertex_x, "vertex_x/D");
+  m_tree->Branch("vertex_y", &m_vertex_y, "vertex_y/D");
+  m_tree->Branch("vertex_z", &m_vertex_z, "vertex_z/D");
+  m_tree->Branch("vertex_chi2", &m_vertex_chi2, "vertex_chi2/D");
+  m_tree->Branch("vertex_px0", &m_vertex_px0, "vertex_px0/D");
+  m_tree->Branch("vertex_py0", &m_vertex_py0, "vertex_py0/D");
+  m_tree->Branch("vertex_pz0", &m_vertex_pz0, "vertex_pz0/D");
+  m_tree->Branch("vertex_p0", &m_vertex_p0, "vertex_p0/D");
+  m_tree->Branch("vertex_px1", &m_vertex_px1, "vertex_px1/D");
+  m_tree->Branch("vertex_py1", &m_vertex_py1, "vertex_py1/D");
+  m_tree->Branch("vertex_pz1", &m_vertex_pz1, "vertex_pz1/D");
+  m_tree->Branch("vertex_p1", &m_vertex_p1, "vertex_p1/D");
+
   ATH_CHECK(histSvc()->regTree("/HIST2/tree", m_tree));
 
   // Register histograms
@@ -885,6 +906,8 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
   }
   ATH_CHECK(trackCollection.isValid());
 
+  // create list of good tracks in fiducial region (r < 95mm) and chi2/nDoF < 25, #Layers >= 7, #Hits >= 12
+  std::vector<const Trk::Track*> goodTracks {};
   for (const Trk::Track* track : *trackCollection)
   {
     if (track == nullptr) continue;
@@ -892,16 +915,20 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
     std::set<std::pair<int, int>> layerMap;
     std::set<int> stationMap;
     // Check for hit in the three downstream stations
+    HitSet hitSet {24};
     for (auto measurement : *(track->measurementsOnTrack())) {
         const Tracker::FaserSCT_ClusterOnTrack* cluster = dynamic_cast<const Tracker::FaserSCT_ClusterOnTrack*>(measurement);
         if (cluster != nullptr) {
             Identifier id = cluster->identify();
             int station = m_sctHelper->station(id);
             int layer = m_sctHelper->layer(id);
+            int side = m_sctHelper->side(id);
             stationMap.emplace(station);
             layerMap.emplace(station, layer);
+            hitSet.set(23 - (station * 6 + layer * 2 + side));
         }
     }
+    m_hitSet.push_back(hitSet.to_ulong());
     if (stationMap.count(1) == 0 || stationMap.count(2) == 0 || stationMap.count(3) == 0) continue;
 
     const Trk::TrackParameters* upstreamParameters = track->trackParameters()->front();
@@ -913,6 +940,12 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
 
     m_Chi2.push_back(track->fitQuality()->chiSquared());
     m_DoF.push_back(track->fitQuality()->numberDoF());
+    const Trk::MeasurementBase *measurement = track->measurementsOnTrack()->front();
+    const Tracker::FaserSCT_ClusterOnTrack* cluster =
+      dynamic_cast<const Tracker::FaserSCT_ClusterOnTrack*>(measurement);
+    Identifier id = cluster->identify();
+    m_module_eta0.push_back(m_sctHelper->eta_module(id));
+    m_module_phi0.push_back(m_sctHelper->phi_module(id));
 
     m_nHit0.push_back(stationMap.count(0));
     m_nHit1.push_back(stationMap.count(1));
@@ -951,6 +984,13 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
     m_y_atMaxRadius.push_back(positionAtMaxRadius.y());
     m_z_atMaxRadius.push_back(positionAtMaxRadius.z());
 
+    if ((m_Chi2.back() / m_DoF.back() < m_maxChi2NDOF) &&
+        (m_nLayers.back() >= m_minLayers) &&
+        (m_DoF.back() + 5 >= m_minHits) &&
+        (m_r_atMaxRadius.back() < 95)) {
+      goodTracks.push_back(track);
+    }
+
     if (isMC) { // if simulation, store track truth info as well
       auto [truthParticle, hitCount] = m_trackTruthMatchingTool->getTruthParticle(track);
       if (truthParticle != nullptr) {
@@ -958,6 +998,13 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
           truthParticleCount[truthParticle->barcode()] += 1;
         m_t_pdg.push_back(truthParticle->pdgId());
         m_t_barcode.push_back(truthParticle->barcode());
+        if (truthParticle->nParents() > 0){
+          m_t_pdg_parent.push_back(truthParticle->parent(0)->pdgId());
+          m_t_barcode_parent.push_back(truthParticle->parent(0)->barcode());
+        } else {
+          m_t_pdg_parent.push_back(0);
+          m_t_barcode_parent.push_back(-1);
+        }
         // the track fit eats up 5 degrees of freedom, thus the number of hits on track is m_DoF + 5
         m_t_truthHitRatio.push_back(hitCount / (m_DoF.back() + 5));
         m_isFiducial.push_back(m_fiducialParticleTool->isFiducial(truthParticle->barcode()));
@@ -1152,6 +1199,41 @@ StatusCode NtupleDumperAlg::execute(const EventContext &ctx) const
     m_longTracks++;
   }
 
+  if (m_runVertexing && goodTracks.size() >= 2) {
+    // sort tracks my momentum and reconstruct vertex
+    std::sort(goodTracks.begin(), goodTracks.end(), [](const Trk::Track *lhs, const Trk::Track *rhs){
+      return lhs->trackParameters()->front()->momentum().z() < rhs->trackParameters()->front()->momentum().z();
+    });
+    std::vector<const Trk::Track*> lowMomentumTracks {goodTracks[0], goodTracks[1]};
+
+    std::optional<FaserTracking::POCA> vertex = m_vertexingTool->getVertex(lowMomentumTracks);
+    if (vertex) {
+      Eigen::Vector3d position = vertex->position;
+      m_vertex_x = position.x();
+      m_vertex_y = position.y();
+      m_vertex_z = position.z();
+      m_vertex_chi2 = vertex->chi2;
+
+      // get track parameters of two tracks with lowest momentum at vertex
+      auto vertexTrackParameters0 = m_vertexingTool->extrapolateTrack(goodTracks[0], m_vertex_z);
+      if (vertexTrackParameters0 != nullptr) {
+        // convert momentum from GeV to MeV
+        m_vertex_px0 = vertexTrackParameters0->momentum().x() * 1e3;
+        m_vertex_py0 = vertexTrackParameters0->momentum().y() * 1e3;
+        m_vertex_pz0 = vertexTrackParameters0->momentum().z() * 1e3;
+        m_vertex_p0 = std::sqrt(m_vertex_px0*m_vertex_px0 + m_vertex_py0*m_vertex_py0 + m_vertex_pz0*m_vertex_pz0);
+      }
+      auto vertexTrackParameters1 = m_vertexingTool->extrapolateTrack(goodTracks[1], m_vertex_z);
+      if (vertexTrackParameters1 != nullptr) {
+        // convert momentum from GeV to MeV
+        m_vertex_px1 = vertexTrackParameters1->momentum().x() * 1e3;
+        m_vertex_py1 = vertexTrackParameters1->momentum().y() * 1e3;
+        m_vertex_pz1 = vertexTrackParameters1->momentum().z() * 1e3;
+        m_vertex_p1 = std::sqrt(m_vertex_px1*m_vertex_px1 + m_vertex_py1*m_vertex_py1 + m_vertex_pz1*m_vertex_pz1);
+      }
+    }
+  }
+
   if (!isMC) {
     if (m_doTrackFilter) { // filter events: colliding bunches have at least one long track, non-colliding bunches have at least one long track or one calo module with raw_peak > 3 mV
       if (m_longTracks == 0 && abs(m_distanceToCollidingBCID) <= 1) {
@@ -1349,6 +1431,9 @@ NtupleDumperAlg::clearTree() const
 
   m_Chi2.clear();
   m_DoF.clear();
+  m_module_eta0.clear();
+  m_module_phi0.clear();
+  m_hitSet.clear();
   m_charge.clear();
   m_nLayers.clear();
   m_longTracks = 0;
@@ -1400,6 +1485,8 @@ NtupleDumperAlg::clearTree() const
 
   m_t_pdg.clear();
   m_t_barcode.clear();
+  m_t_pdg_parent.clear();
+  m_t_barcode_parent.clear();
   m_t_truthHitRatio.clear();
   m_t_prodVtx_x.clear();
   m_t_prodVtx_y.clear();
@@ -1466,11 +1553,25 @@ NtupleDumperAlg::clearTree() const
   m_truthd1_y.clear();
   m_truthd1_z.clear();
 
+  m_vertex_x = NaN;
+  m_vertex_y = NaN;
+  m_vertex_z = NaN;
+  m_vertex_chi2 = NaN;
+  m_vertex_px0 = NaN;
+  m_vertex_py0 = NaN;
+  m_vertex_pz0 = NaN;
+  m_vertex_p0 = NaN;
+  m_vertex_px1 = NaN;
+  m_vertex_py1 = NaN;
+  m_vertex_pz1 = NaN;
+  m_vertex_p1 = NaN;
 }
 
 void NtupleDumperAlg::clearTrackTruth() const {
   m_t_pdg.push_back(0);
   m_t_barcode.push_back(-1);
+  m_t_pdg_parent.push_back(0);
+  m_t_barcode_parent.push_back(-1);
   m_t_truthHitRatio.push_back(0);
   m_t_prodVtx_x.push_back(999999);
   m_t_prodVtx_y.push_back(999999);
diff --git a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h
index 67f3bacf8c8f4a6ef134741637d093fdea60c24b..a741bcbe55097ba754a256fe3804f0e6cc170db9 100644
--- a/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h
+++ b/PhysicsAnalysis/NtupleDumper/src/NtupleDumperAlg.h
@@ -23,10 +23,14 @@
 #include "TrackerSimEvent/FaserSiHitCollection.h"
 #include "xAODEventInfo/EventInfo.h"
 #include "StoreGate/ReadDecorHandle.h"
+#include "FaserActsVertexing/IVertexingTool.h"
+#include <boost/dynamic_bitset.hpp>
 
 #include <vector>
 #include <nlohmann/json.hpp>
 
+using HitSet = boost::dynamic_bitset<>;
+
 class TTree;
 class TH1;
 class FaserSCT_ID;
@@ -93,6 +97,7 @@ private:
   ToolHandle<IFaserActsTrackingGeometryTool> m_trackingGeometryTool {this, "TrackingGeometryTool", "FaserActsTrackingGeometryTool"};
   ToolHandle<ITrackTruthMatchingTool> m_trackTruthMatchingTool {this, "TrackTruthMatchingTool", "TrackTruthMatchingTool"};
   ToolHandle<IFiducialParticleTool> m_fiducialParticleTool {this, "FiducialParticleTool", "FiducialParticleTool"};
+  ToolHandle<FaserTracking::IVertexingTool> m_vertexingTool { this, "VertexingTool", "FaserTracking::PointOfClosestApproachSearchTool"};
 
   const TrackerDD::SCT_DetectorManager* m_detMgr {nullptr};
 
@@ -124,6 +129,12 @@ private:
   BooleanProperty m_applyGoodRunsList    {this, "ApplyGoodRunsList", false, "Only write out events passing GRL (data only)"};
   StringProperty m_goodRunsList          {this, "GoodRunsList", "", "GoodRunsList in json format"};
 
+  // track quality cuts
+  UnsignedIntegerProperty m_minLayers{this, "minLayers", 7, "Miminimum number of layers of a track."};
+  UnsignedIntegerProperty m_minHits{this, "minHits", 12, "Miminimum number of hits of a track."};
+  DoubleProperty m_maxChi2NDOF{this, "maxChi2NDOF", 25, "Maximum chi2 per degree of freedom."};
+  BooleanProperty m_runVertexing{ this, "RunVertexing", true, "Run the vertexing, defaults to true." };
+
   // json object to hold data read from GRL file (or empty if not)
   nlohmann::json m_grl;
 
@@ -217,6 +228,9 @@ private:
 
   mutable int    m_longTracks;
   mutable int m_propagationError;
+  mutable std::vector<int> m_module_eta0;
+  mutable std::vector<int> m_module_phi0;
+  mutable std::vector<unsigned long> m_hitSet;
   mutable std::vector<double> m_Chi2;
   mutable std::vector<double> m_DoF;
   mutable std::vector<double> m_xup;
@@ -274,6 +288,8 @@ private:
 
   mutable std::vector<int> m_t_pdg; // pdg code of the truth matched particle
   mutable std::vector<int> m_t_barcode; // barcode of the truth matched particle
+  mutable std::vector<int> m_t_pdg_parent; // pdg code of the parent of the truth matched particle
+  mutable std::vector<int> m_t_barcode_parent; // barcode of the parent of the truth matched particle
   mutable std::vector<double> m_t_truthHitRatio; // ratio of hits on track matched to the truth particle over all hits on track
   mutable std::vector<double> m_t_prodVtx_x; // x component of the production vertex in mm
   mutable std::vector<double> m_t_prodVtx_y; // y component of the production vertex in mm
@@ -355,6 +371,19 @@ private:
 
   mutable int    m_eventsPassed = 0;
   mutable int    m_eventsFailedGRL = 0;
+
+  mutable double m_vertex_x; // components of reconstructed vertex in mm
+  mutable double m_vertex_y;
+  mutable double m_vertex_z;
+  mutable double m_vertex_chi2; // chi2 value of vertex fit
+  mutable double m_vertex_px0; // components of lowest momentum track extrapolated to vertex in MeV
+  mutable double m_vertex_py0;
+  mutable double m_vertex_pz0;
+  mutable double m_vertex_p0;
+  mutable double m_vertex_px1; // components of second lowest momentum track extrapolated to vertex in MeV
+  mutable double m_vertex_py1;
+  mutable double m_vertex_pz1;
+  mutable double m_vertex_p1;
 };
 
 inline const ServiceHandle <ITHistSvc> &NtupleDumperAlg::histSvc() const {
diff --git a/Scintillator/ScintDetDescr/VetoGeoModel/src/VetoStation.cxx b/Scintillator/ScintDetDescr/VetoGeoModel/src/VetoStation.cxx
index 7c0723d54966f8c65a8a26f322fa2c460af89c9b..05e4a8b0bcb628a699b917cfecf9612d7910aeae 100644
--- a/Scintillator/ScintDetDescr/VetoGeoModel/src/VetoStation.cxx
+++ b/Scintillator/ScintDetDescr/VetoGeoModel/src/VetoStation.cxx
@@ -102,7 +102,7 @@ VetoStation::build(VetoIdentifier id)
     id.setPlate(iPlate);
     GeoAlignableTransform* transform = new GeoAlignableTransform(GeoTrf::Translate3D(0.0, 
                                                                                      0.0,
-                                                                                     (plateThickness - activeDepth)/2 + iPlate * m_platePitch));
+                                                                                     plateCenterZ));
     station->add(transform);
     GeoVPhysVol* platePV = m_plate->build(id);
     station->add(platePV);
diff --git a/Simulation/G4Faser/G4FaserAlg/CMakeLists.txt b/Simulation/G4Faser/G4FaserAlg/CMakeLists.txt
index 979897a94c4b476433d0ef3fe2ceeb31105f9ef5..ec2babac5f7c92340ffff8b39f318e7e7013fde2 100644
--- a/Simulation/G4Faser/G4FaserAlg/CMakeLists.txt
+++ b/Simulation/G4Faser/G4FaserAlg/CMakeLists.txt
@@ -31,21 +31,32 @@ atlas_add_test( G4FaserAlgConfig_TestFaser
                 PROPERTIES TIMEOUT 300 
                 PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
 
-atlas_add_test( G4FaserAlgConfig_TestFaserNu
-                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASERNU-03'" IOVDb.GlobalTag="'OFLCOND-FASER-02'" Output.HITSFileName='faserNu.HITS.pool.root'
+atlas_add_test( G4FaserAlgConfig_TestFaserNu03
+                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASERNU-03'" IOVDb.GlobalTag="'OFLCOND-FASER-02'" Output.HITSFileName='faserNu03.HITS.pool.root'
                 PROPERTIES TIMEOUT 300 
                 PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
 
-atlas_add_test( G4FaserAlgConfig_TestFaserNu_NewField
-                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASERNU-03'" IOVDb.GlobalTag="'OFLCOND-FASER-03'" Output.HITSFileName='faserNu.HITS.pool.root'
+atlas_add_test( G4FaserAlgConfig_TestFaserNu03_NewField
+                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASERNU-03'" IOVDb.GlobalTag="'OFLCOND-FASER-03'" Output.HITSFileName='faserNu03_NewField.HITS.pool.root'
                 PROPERTIES TIMEOUT 300 
                 PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
 
-atlas_add_test( G4FaserAlgConfig_TestTestbeam
-                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASER-TB00'" IOVDb.GlobalTag="'OFLCOND-FASER-TB00'" Output.HITSFileName='tb.HITS.pool.root'
+atlas_add_test( G4FaserAlgConfig_TestFaserNu04
+                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASERNU-04'" IOVDb.GlobalTag="'OFLCOND-FASER-03'" Output.HITSFileName='faserNu04.HITS.pool.root'
                 PROPERTIES TIMEOUT 300 
                 PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
 
+atlas_add_test( G4FaserAlgConfig_TestTestbeam00
+                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASER-TB00'" IOVDb.GlobalTag="'OFLCOND-FASER-TB00'" Output.HITSFileName='tb00.HITS.pool.root'
+                PROPERTIES TIMEOUT 300 
+                PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+
+atlas_add_test( G4FaserAlgConfig_TestTestbeam01
+                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/G4FaserAlgConfigNew_Test.py GeoModel.FaserVersion="'FASER-TB01'" IOVDb.GlobalTag="'OFLCOND-FASER-TB00'" Output.HITSFileName='tb01.HITS.pool.root'
+                PROPERTIES TIMEOUT 300 
+                PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
+
+
 # Install files from the package:
 atlas_install_python_modules( python/*.py )
 atlas_install_scripts( test/*.py )
diff --git a/Simulation/G4Faser/G4FaserAlg/test/G4FaserAlgConfigNew_Test.py b/Simulation/G4Faser/G4FaserAlg/test/G4FaserAlgConfigNew_Test.py
index df6abb7ab1a0fec74ad0da4b2a4fc0cad8ca84a8..fb07e943ab87584ec8990cf7e1dbdb2d75359795 100755
--- a/Simulation/G4Faser/G4FaserAlg/test/G4FaserAlgConfigNew_Test.py
+++ b/Simulation/G4Faser/G4FaserAlg/test/G4FaserAlgConfigNew_Test.py
@@ -48,7 +48,7 @@ if __name__ == '__main__':
     ConfigFlags.addFlag("Sim.Beam.xshift", 0)  # Potential beam shift
     ConfigFlags.addFlag("Sim.Beam.yshift", 0)        
 
-    ConfigFlags.GeoModel.FaserVersion = "FASERNU-03"             # Geometry set-up
+    ConfigFlags.GeoModel.FaserVersion = "FASERNU-04"             # Geometry set-up
     ConfigFlags.IOVDb.GlobalTag = "OFLCOND-FASER-03"             # Conditions set-up
     ConfigFlags.addFlag("Input.InitialTimeStamp", 0)             # To avoid autoconfig 
     ConfigFlags.GeoModel.Align.Dynamic = False
@@ -172,6 +172,11 @@ if __name__ == '__main__':
 
     #cfg.getEventAlgo("OutputStreamHITS").ItemList += ["McEventCollection#BeamTruthEvent_ATLASCoord"]    
 #
+# Uncomment to check volumes for overlap - will cause CTest to fail due to overwriting file
+#
+#    from G4DebuggingTools.G4DebuggingToolsConfigNew import VolumeDebugger
+#    cfg.merge(VolumeDebugger(ConfigFlags, name="G4UA::UserActionSvc", TargetVolume="", Verbose=True))
+#
 # Dump config
 #
     from AthenaConfiguration.ComponentFactory import CompFactory
diff --git a/Tracker/TrackerDetDescr/DipoleGeoModel/src/DipoleFactory.cxx b/Tracker/TrackerDetDescr/DipoleGeoModel/src/DipoleFactory.cxx
index 6129c49ccd609137bd92e9b22f0eb2a161a7cf67..ccfda716028494d56d7c2cac629c42113252b1cd 100644
--- a/Tracker/TrackerDetDescr/DipoleGeoModel/src/DipoleFactory.cxx
+++ b/Tracker/TrackerDetDescr/DipoleGeoModel/src/DipoleFactory.cxx
@@ -93,13 +93,16 @@ void DipoleFactory::create(GeoPhysVol *world )
   double upstreamThickness = wrappingParameters->thickness1();
   double downstreamThickness = wrappingParameters->thickness2();
   double wrappingRadius = wrappingParameters->radius();
+  msg(MSG::ALWAYS) << "Upstream wrapping thickness: " << upstreamThickness << "; Downstream wrapping thickness: " << downstreamThickness << endmsg;
+  msg(MSG::ALWAYS) << "Upstream wrapping position: " << -(parameters->longLength() + upstreamThickness)/2 << "; Downstream wrapping position: " << (parameters->longLength() + downstreamThickness)/2 << endmsg;
 
   GeoTrf::Transform3D dipoleTransform = parameters->partTransform("Dipole");
   const GeoTube* shortShape = new GeoTube(parameters->innerRadius(), parameters->outerRadius(), parameters->shortLength()/2);
   GeoLogVol* shortLog = new GeoLogVol("ShortDipole", shortShape, NdFeB);
   const GeoTube* longShape = new GeoTube(parameters->innerRadius(), parameters->outerRadius(), parameters->longLength()/2);
   GeoLogVol* longLog = new GeoLogVol("LongDipole", longShape, NdFeB);
-  const GeoTube* wrappedShape = new GeoTube(parameters->innerRadius(), std::max(parameters->outerRadius(), wrappingRadius), parameters->longLength()/2 + std::max(upstreamThickness, downstreamThickness));
+  // const GeoTube* wrappedShape = new GeoTube(parameters->innerRadius(), std::max(parameters->outerRadius(), wrappingRadius), parameters->longLength()/2 + std::max(upstreamThickness, downstreamThickness));
+  const GeoTube* wrappedShape = new GeoTube(0.0, std::max(parameters->outerRadius(), wrappingRadius), parameters->longLength()/2 + std::max(upstreamThickness, downstreamThickness));
   GeoLogVol* wrappedLog = new GeoLogVol("WrappedLongDipole", wrappedShape, air);
   GeoPhysVol* wrappedPV = new GeoPhysVol(wrappedLog);
   GeoPhysVol* longPV = new GeoPhysVol(longLog);
@@ -113,7 +116,7 @@ void DipoleFactory::create(GeoPhysVol *world )
   const GeoTube* downstreamShape = new GeoTube(0.0, wrappingRadius, downstreamThickness/2);
   GeoLogVol* downstreamLog = new GeoLogVol("DownstreamWrapping", downstreamShape, downstreamMaterial);
   GeoPhysVol* downstreamPV = new GeoPhysVol(downstreamLog);
-  wrappedPV->add(new GeoTransform(GeoTrf::Translate3D(0.0, 0.0, (parameters->longLength() + upstreamThickness)/2)));
+  wrappedPV->add(new GeoTransform(GeoTrf::Translate3D(0.0, 0.0, (parameters->longLength() + downstreamThickness)/2)));
   wrappedPV->add(downstreamPV);
 
   std::vector<std::string> partNames {"UpstreamDipole", "CentralDipole", "DownstreamDipole"};
diff --git a/Tracking/Acts/FaserActsGeometry/CMakeLists.txt b/Tracking/Acts/FaserActsGeometry/CMakeLists.txt
index e15adcec74c6efaee85c11e388517683262c6522..58c40ebec78ecb3ab4585b9dc744392c4aa19042 100755
--- a/Tracking/Acts/FaserActsGeometry/CMakeLists.txt
+++ b/Tracking/Acts/FaserActsGeometry/CMakeLists.txt
@@ -56,6 +56,8 @@ atlas_add_component( FaserActsGeometry
                          src/FaserActsAlignmentCondAlg.cxx
                          src/NominalAlignmentCondAlg.cxx
                          src/FaserActsVolumeMappingTool.cxx
+                         src/FaserActsGeometryBoundaryTestAlg.h
+                         src/FaserActsGeometryBoundaryTestAlg.cxx
                          src/components/*.cxx
                          PUBLIC_HEADERS FaserActsGeometry
                          INCLUDE_DIRS ${CLHEP_INCLUDE_DIRS} ${EIGEN_INCLUDE_DIRS} ${BOOST_INCLUDE_DIRS}
@@ -84,3 +86,7 @@ atlas_install_headers( FaserActsGeometry )
 atlas_install_python_modules( python/*.py )
 atlas_install_scripts( test/*.py )
 
+atlas_add_test( FaserActsGeometryBoundary_test
+                SCRIPT python ${CMAKE_CURRENT_SOURCE_DIR}/test/FaserActsGeometryBoundary_test.py
+                PROPERTIES WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+                PROPERTIES TIMEOUT 300 )
diff --git a/Tracking/Acts/FaserActsGeometry/src/FaserActsExtrapolationTool.cxx b/Tracking/Acts/FaserActsGeometry/src/FaserActsExtrapolationTool.cxx
index d31fe8d3c11e804e115aef2cc6dfc08a2354b9e7..14d2699822166993a61b0de47a74c1fc5b139f65 100644
--- a/Tracking/Acts/FaserActsGeometry/src/FaserActsExtrapolationTool.cxx
+++ b/Tracking/Acts/FaserActsGeometry/src/FaserActsExtrapolationTool.cxx
@@ -71,7 +71,7 @@ FaserActsExtrapolationTool::initialize()
 
   ATH_MSG_INFO("Initializing ACTS extrapolation");
 
-  m_logger = Acts::getDefaultLogger("ExtrapolationTool", Acts::Logging::INFO);
+  m_logger = Acts::getDefaultLogger("ExtrapolationTool", Acts::Logging::ERROR);
 //  m_logger = makeActsAthenaLogger(this, "Prop", "FaserActsExtrapTool");
 
   ATH_CHECK( m_trackingGeometryTool.retrieve() );
diff --git a/Tracking/Acts/FaserActsGeometry/src/FaserActsGeometryBoundaryTestAlg.cxx b/Tracking/Acts/FaserActsGeometry/src/FaserActsGeometryBoundaryTestAlg.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..57c1e3a2fe68bea133d167ea00dd539edfc0f3fe
--- /dev/null
+++ b/Tracking/Acts/FaserActsGeometry/src/FaserActsGeometryBoundaryTestAlg.cxx
@@ -0,0 +1,97 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS and FASER collaborations
+*/
+
+
+#include "FaserActsGeometryBoundaryTestAlg.h"
+#include "FaserActsGeometry/FaserActsGeometryContext.h"
+#include "Acts/Surfaces/Surface.hpp"
+#include "Acts/Surfaces/PlaneSurface.hpp"
+#include "Acts/Surfaces/RectangleBounds.hpp"
+#include "Acts/Geometry/TrackingGeometry.hpp"
+#include "Acts/Geometry/DetectorElementBase.hpp"
+#include "Acts/Geometry/TrackingVolume.hpp"
+
+
+FaserActsGeometryBoundaryTestAlg::FaserActsGeometryBoundaryTestAlg(const std::string &name, ISvcLocator *pSvcLocator)
+    : AthReentrantAlgorithm(name, pSvcLocator) {}
+
+
+StatusCode FaserActsGeometryBoundaryTestAlg::initialize() {
+  ATH_CHECK(m_trackingGeometryTool.retrieve());
+  return StatusCode::SUCCESS;
+}
+
+
+StatusCode FaserActsGeometryBoundaryTestAlg::execute(const EventContext &ctx) const {
+
+  std::shared_ptr<const Acts::TrackingGeometry> trackingGeometry
+      = m_trackingGeometryTool->trackingGeometry();
+
+  const Acts::GeometryContext gctx =
+      m_trackingGeometryTool->getGeometryContext(ctx).context();
+
+  // loop over all tracking layer and check if contained detector elements are insie boundaries
+  // return StatusCode::Failure so that test fails, if the edge of any detector element is outside boundaries
+  const Acts::TrackingVolume *trackingVolume = trackingGeometry->highestTrackingVolume();
+  for (auto volume : trackingVolume->confinedVolumes()->arrayObjects()) {
+    for (const auto& layer : volume->confinedLayers()->arrayObjects()) {
+      if (layer->layerType() == Acts::LayerType::active) {
+        // get inner and outer boundaries from approach descriptor:
+        // - innerBoundary is approach suface with minimum z position
+        // - outerBoundary is approach suface with maximum z position
+        Acts::SurfaceVector approachSurfaces = layer->approachDescriptor()->containedSurfaces();
+        const Acts::Surface *innerApproachSurface = *std::min_element(
+            approachSurfaces.begin(), approachSurfaces.end(),
+            [&gctx](const auto &lhs, const auto &rhs) {
+                return lhs->center(gctx).z() < rhs->center(gctx).z();
+            }
+        );
+        const Acts::Surface *outerApproachSurface = *std::max_element(
+            approachSurfaces.begin(), approachSurfaces.end(),
+            [&gctx](const auto &lhs, const auto &rhs) {
+                return lhs->center(gctx).z() < rhs->center(gctx).z();
+            }
+        );
+        double zInnerBoundary = innerApproachSurface->center(gctx).z();
+        double zOuterBoundary = outerApproachSurface->center(gctx).z();
+
+        // loop over surface array and check if all edges are between inner and outer boundary
+        for (const Acts::Surface *surface : layer->surfaceArray()->surfaces()) {
+          auto planeSurface = dynamic_cast<const Acts::PlaneSurface *>(surface);
+          // make sure surface has a associated detector element (there are other active detector elements 
+          // e.g. containing material or at the tracking volume boundaries)
+          if (surface->associatedDetectorElement() != nullptr) {
+            auto bounds = dynamic_cast<const Acts::RectangleBounds*>(&surface->bounds());
+            const Acts::Vector2 min = bounds->min();
+            const Acts::Vector2 max = bounds->max();
+            // create dummpy momentum vector: local to global transformation requires  momentum vector,
+            // which is ignored for PlaneSurface
+            Acts::Vector3 dummyMomentum {1., 1., 1.};
+            // get global position at all edges of the surface
+            std::vector<Acts::Vector3> edges {};
+            edges.push_back(planeSurface->localToGlobal(gctx, {min.x(), min.y()}, dummyMomentum));
+            edges.push_back(planeSurface->localToGlobal(gctx, {min.x(), max.y()}, dummyMomentum));
+            edges.push_back(planeSurface->localToGlobal(gctx, {max.x(), min.y()}, dummyMomentum));
+            edges.push_back(planeSurface->localToGlobal(gctx, {max.x(), max.y()}, dummyMomentum));
+            for (const Acts::Vector3 &edgePosition : edges) {
+              if ((edgePosition.z() < zInnerBoundary) || (edgePosition.z() > zOuterBoundary)) {
+                std::cout << "?? surface outside boundaries\n";
+                std::cout << "inner Boundary: " << zInnerBoundary << std::endl;
+                std::cout << "outer Boundary: " << zOuterBoundary << std::endl;
+                std::cout << "edge: " << edgePosition.x() << ", " << edgePosition.y() << ", " << edgePosition.z() << std::endl;
+                return StatusCode::FAILURE;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return StatusCode::SUCCESS;
+}
+
+StatusCode FaserActsGeometryBoundaryTestAlg::finalize() {
+  return StatusCode::SUCCESS;
+}
diff --git a/Tracking/Acts/FaserActsGeometry/src/FaserActsGeometryBoundaryTestAlg.h b/Tracking/Acts/FaserActsGeometry/src/FaserActsGeometryBoundaryTestAlg.h
new file mode 100644
index 0000000000000000000000000000000000000000..b2c0bf3c73b35ac1d0b9857a8c0aae72cfb3c56c
--- /dev/null
+++ b/Tracking/Acts/FaserActsGeometry/src/FaserActsGeometryBoundaryTestAlg.h
@@ -0,0 +1,25 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS and FASER collaborations
+*/
+
+#ifndef FASERACTSGEOMETRY_FASERACTSGEOMETRYBOUNDARYTESTALG_H
+#define FASERACTSGEOMETRY_FASERACTSGEOMETRYBOUNDARYTESTALG_H
+
+#include "AthenaBaseComps/AthReentrantAlgorithm.h"
+#include "FaserActsGeometryInterfaces/IFaserActsTrackingGeometryTool.h"
+
+
+class FaserActsGeometryBoundaryTestAlg : public AthReentrantAlgorithm {
+public:
+  FaserActsGeometryBoundaryTestAlg(const std::string &name, ISvcLocator *pSvcLocator);
+  virtual StatusCode initialize() override final;
+  virtual StatusCode execute(const EventContext &ctx) const override final;
+  virtual StatusCode finalize() override final;
+
+private:
+  ToolHandle<IFaserActsTrackingGeometryTool> m_trackingGeometryTool {
+    this, "TrackingGeometryTool", "FaserActsTrackingGeometryTool"};
+};
+
+
+#endif // FASERACTSGEOMETRY_FASERACTSGEOMETRYBOUNDARYTESTALG_H
\ No newline at end of file
diff --git a/Tracking/Acts/FaserActsGeometry/src/FaserActsLayerBuilder.cxx b/Tracking/Acts/FaserActsGeometry/src/FaserActsLayerBuilder.cxx
index c5f6a68ab01b8da95f3d62149d23bdf41684263d..649f2503cb69c93b96b8e454727998656898eccf 100644
--- a/Tracking/Acts/FaserActsGeometry/src/FaserActsLayerBuilder.cxx
+++ b/Tracking/Acts/FaserActsGeometry/src/FaserActsLayerBuilder.cxx
@@ -206,7 +206,8 @@ FaserActsLayerBuilder::buildLayers(const Acts::GeometryContext& gctx,
   std::shared_ptr<const Acts::ProtoSurfaceMaterial> materialProxy = nullptr;
 
   double layerZ = 0.5 * (pl.min(Acts::binZ) + pl.max(Acts::binZ));
-  double layerThickness = (pl.max(Acts::binZ) - pl.min(Acts::binZ));
+  // increase layerThickness by 4 mm so that after alignment shifts all modules are still within the boundaries
+  double layerThickness = (pl.max(Acts::binZ) - pl.min(Acts::binZ)) + 4_mm;
   double layerZInner = layerZ - layerThickness/2.;
   double layerZOuter = layerZ + layerThickness/2.;
 
diff --git a/Tracking/Acts/FaserActsGeometry/src/components/FaserActsGeometry_entries.cxx b/Tracking/Acts/FaserActsGeometry/src/components/FaserActsGeometry_entries.cxx
index e7b7fb2fe49208911b0d98c2ba4a6d6b960df89f..a103c7a9466eb66446036cc2978e82dba8889fee 100755
--- a/Tracking/Acts/FaserActsGeometry/src/components/FaserActsGeometry_entries.cxx
+++ b/Tracking/Acts/FaserActsGeometry/src/components/FaserActsGeometry_entries.cxx
@@ -16,6 +16,7 @@
 #include "../FaserActsMaterialMapping.h"
 #include "../FaserActsSurfaceMappingTool.h"
 #include "../FaserActsMaterialTrackWriterSvc.h"
+#include "../FaserActsGeometryBoundaryTestAlg.h"
 
 DECLARE_COMPONENT( FaserActsTrackingGeometrySvc )
 DECLARE_COMPONENT( FaserActsTrackingGeometryTool )
@@ -30,3 +31,4 @@ DECLARE_COMPONENT( FaserActsMaterialJsonWriterTool )
 DECLARE_COMPONENT( FaserActsMaterialTrackWriterSvc )
 DECLARE_COMPONENT( FaserActsMaterialMapping )
 DECLARE_COMPONENT( FaserActsSurfaceMappingTool )
+DECLARE_COMPONENT( FaserActsGeometryBoundaryTestAlg )
diff --git a/Tracking/Acts/FaserActsGeometry/test/FaserActsGeometryBoundary_test.py b/Tracking/Acts/FaserActsGeometry/test/FaserActsGeometryBoundary_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..b6a841642b17b6ecd412fc55ced498524ece9ca5
--- /dev/null
+++ b/Tracking/Acts/FaserActsGeometry/test/FaserActsGeometryBoundary_test.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+"""
+Copyright (C) 2002-2023 CERN for the benefit of the ATLAS and FASER collaboration
+"""
+
+
+def FaserActsGeometryBoundaryTestCfg(flags, name="FaserActsGeometryBoundaryTestAlg", **kwargs):
+
+    from FaserSCT_GeoModel.FaserSCT_GeoModelConfig import FaserSCT_GeometryCfg
+    from MagFieldServices.MagFieldServicesConfig import MagneticFieldSvcCfg
+    from FaserActsGeometry.ActsGeometryConfig import ActsTrackingGeometryToolCfg
+    from AthenaConfiguration.ComponentFactory import CompFactory
+
+    acc = FaserSCT_GeometryCfg(flags)
+    acc.merge(MagneticFieldSvcCfg(flags))
+    result, actsTrackingGeometryTool = ActsTrackingGeometryToolCfg(flags)
+    test_alg = CompFactory.FaserActsGeometryBoundaryTestAlg
+    test_alg.TrackingGeometryTool = actsTrackingGeometryTool
+    acc.merge(result)
+    acc.addEventAlgo(test_alg(name, **kwargs))
+    return acc
+
+
+if __name__ == "__main__":
+    
+    import sys
+    from AthenaCommon.Configurable import Configurable
+    from CalypsoConfiguration.AllConfigFlags import ConfigFlags
+    from CalypsoConfiguration.MainServicesConfig import MainServicesCfg
+    from FaserGeoModel.FaserGeoModelConfig import FaserGeometryCfg
+
+    Configurable.configurableRun3Behavior = True
+    ConfigFlags.Input.isMC = False
+    ConfigFlags.GeoModel.Align.Dynamic = False
+    ConfigFlags.IOVDb.DatabaseInstance = "CONDBR3"
+    ConfigFlags.IOVDb.GlobalTag = "OFLCOND-FASER-04"
+    ConfigFlags.GeoModel.FaserVersion = "FASERNU-03"
+    ConfigFlags.Detector.GeometryFaserSCT = True
+    ConfigFlags.lock()
+
+    acc = MainServicesCfg(ConfigFlags)
+    acc.merge(FaserGeometryCfg(ConfigFlags))
+    acc.merge(FaserActsGeometryBoundaryTestCfg(ConfigFlags))
+
+    replicaSvc = acc.getService("DBReplicaSvc")
+    replicaSvc.COOLSQLiteVetoPattern = ""
+    replicaSvc.UseCOOLSQLite = True
+    replicaSvc.UseCOOLFrontier = False
+    replicaSvc.UseGeomSQLite = True
+
+    sys.exit(int(acc.run(maxEvents=1).isFailure()))
diff --git a/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/CMakeLists.txt b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..61908a643b1d9f03eaa3b4ebf9c6eb2d34786a6a
--- /dev/null
+++ b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/CMakeLists.txt
@@ -0,0 +1,44 @@
+atlas_subdir(FaserActsVertexing)
+
+find_package( Eigen )
+find_package( Acts COMPONENTS Core )
+
+atlas_add_library( FaserActsmanVertexingLib
+  PUBLIC_HEADERS FaserActsVertexing
+  INCLUDE_DIRS ${EIGEN_INCLUDE_DIRS}
+  LINK_LIBRARIES ${EIGEN_LIBRARIES}
+  ActsCore
+  AthenaBaseComps
+  FaserActsGeometryLib
+  FaserActsKalmanFilterLib
+  MagFieldConditions
+  StoreGateLib
+  TrackerPrepRawData
+  TrackerRIO_OnTrack
+  TrackerSimEvent
+  TrackerSpacePoint
+  TrkTrack
+  xAODTruth
+)
+
+atlas_add_component(
+        FaserActsVertexing
+        FaserActsVertexing/IVertexingTool.h
+        src/PointOfClosestApproachSearchTool.h
+        src/PointOfClosestApproachSearchTool.cxx
+        src/components/FaserActsVertexing_entries.cxx
+        INCLUDE_DIRS ${EIGEN_INCLUDE_DIRS}
+        LINK_LIBRARIES ${EIGEN_LIBRARIES}
+        ActsCore
+        AthenaBaseComps
+        FaserActsGeometryLib
+        FaserActsKalmanFilterLib
+        MagFieldConditions
+        StoreGateLib
+        TrackerPrepRawData
+        TrackerRIO_OnTrack
+        TrackerSimEvent
+        TrackerSpacePoint
+        TrkTrack
+        xAODTruth
+)
diff --git a/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/FaserActsVertexing/IVertexingTool.h b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/FaserActsVertexing/IVertexingTool.h
new file mode 100644
index 0000000000000000000000000000000000000000..3c6f93714b6cf07f2428a80c157318399d7b632e
--- /dev/null
+++ b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/FaserActsVertexing/IVertexingTool.h
@@ -0,0 +1,37 @@
+#ifndef FASERACTSVERTEXING_IVERTEXINGTOOL_H
+#define FASERACTSVERTEXING_IVERTEXINGTOOL_H
+
+#include "Acts/EventData/TrackParameters.hpp"
+#include "GaudiKernel/IAlgTool.h"
+#include "GaudiKernel/IInterface.h"
+#include "TrkTrack/Track.h"
+#include <vector>
+
+namespace FaserTracking {
+
+// Point of closest approach between n tracks. Contains the three-dimensional
+// reconstructed position, the chi2 value of the vertex fit and the tracks that
+// originate from this vertex.
+struct POCA {
+  POCA(Eigen::Vector3d vertex, double chi2, std::vector<const Trk::Track *> tracks)
+      : position(vertex), chi2(chi2), tracks(tracks) {}
+  Eigen::Vector3d position;
+  double chi2;
+  std::vector<const Trk::Track *> tracks;
+};
+
+class IVertexingTool : virtual public IAlgTool {
+public:
+  DeclareInterfaceID(IVertexingTool, 1, 0);
+
+  // Run vertex fit and return the fitted POCA.
+  virtual std::optional<POCA> getVertex(const std::vector<const Trk::Track *> &tracks) const = 0;
+
+  // Get bound track parameters at given z position.
+  virtual std::unique_ptr<const Acts::BoundTrackParameters>
+  extrapolateTrack(const Trk::Track *track, double targetPosition) const = 0;
+};
+
+} // namespace FaserTracking
+
+#endif // FASERACTSVERTEXING_IVERTEXINGTOOL_H
diff --git a/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/PointOfClosestApproachSearchTool.cxx b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/PointOfClosestApproachSearchTool.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..ea423d703372dbfcc954dc2089b8169282cdc634
--- /dev/null
+++ b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/PointOfClosestApproachSearchTool.cxx
@@ -0,0 +1,212 @@
+#include "PointOfClosestApproachSearchTool.h"
+#include "TrkParameters/TrackParameters.h"
+#include <climits>
+
+namespace FaserTracking {
+
+PointOfClosestApproachSearchTool::PointOfClosestApproachSearchTool(const std::string &type,
+                                                                   const std::string &name,
+                                                                   const IInterface *parent)
+    : base_class(type, name, parent) {}
+
+StatusCode PointOfClosestApproachSearchTool::initialize() {
+  ATH_CHECK(m_trackingGeometryTool.retrieve());
+  ATH_CHECK(m_extrapolationTool.retrieve());
+  return StatusCode::SUCCESS;
+}
+
+StatusCode PointOfClosestApproachSearchTool::finalize() { return StatusCode::SUCCESS; }
+
+std::optional<POCA>
+PointOfClosestApproachSearchTool::getVertex(const std::vector<const Trk::Track *> &tracks) const {
+
+  if (tracks.size() < 2) {
+    ATH_MSG_DEBUG("Found " << tracks.size() << " tracks, but require at least 2 tracks.");
+    return {};
+  } else if (tracks.size() > 3) {
+    ATH_MSG_DEBUG("Found " << tracks.size() << " tracks, but require at most 3 tracks.");
+    return {};
+  }
+
+  clear();
+
+  m_ctx = Gaudi::Hive::currentContext();
+  m_gctx = m_trackingGeometryTool->getNominalGeometryContext().context();
+
+  // convert to Acts::BoundTrackParameters
+  std::vector<Acts::BoundTrackParameters> trackParameters{};
+  for (const Trk::Track *track : tracks) {
+    trackParameters.push_back(getParametersFromTrack(track->trackParameters()->front()));
+  }
+
+  // extrapolate to nSteps z positions betweeen zMin and zMax and calculate center of tracks and
+  // chi2 for each position.
+  size_t nSteps = (m_zMax - m_zMin) / m_stepSize + 1;
+  m_allVertexCandidates.reserve(nSteps);
+  for (int z = m_zMin; z <= m_zMax; z += m_stepSize) {
+    auto vertexCandidate = getVertexCandidate(trackParameters, z);
+    if (vertexCandidate) {
+      m_allVertexCandidates.push_back(*vertexCandidate);
+    }
+  }
+
+  // find vertex candidates with minium (local) chi2 value
+  for (size_t i = 1; i < nSteps - 1; ++i) {
+    if ((m_allVertexCandidates[i - 1].chi2 > m_allVertexCandidates[i].chi2) and
+        (m_allVertexCandidates[i + 1].chi2 > m_allVertexCandidates[i].chi2)) {
+      m_goodVertexCandidates.push_back(m_allVertexCandidates[i]);
+    }
+  }
+
+  // Check if there are one or two local minima and calculate mean in the case of two minima.
+  if (m_goodVertexCandidates.size() == 0) {
+    ATH_MSG_DEBUG("Cannot find minimum. Extrapolation failed. This is likely an error.");
+    return {};
+  } else if (m_goodVertexCandidates.size() == 1) {
+    const VertexCandidate &vx = m_goodVertexCandidates.front();
+    return POCA(vx.position, vx.chi2, tracks);
+  } else if (m_goodVertexCandidates.size() == 2) {
+    const VertexCandidate &vx0 = m_goodVertexCandidates.at(0);
+    const VertexCandidate &vx1 = m_goodVertexCandidates.at(1);
+    return POCA(0.5 * (vx0.position + vx1.position), 0.5 * (vx0.chi2 + vx1.chi2), tracks);
+  } else {
+    ATH_MSG_DEBUG("Cannot find global minimum. Expect a maximum of two local chi2 minima but found "
+                  << m_goodVertexCandidates.size() << " minima.");
+    return {};
+  }
+}
+
+// Calculate center of tracks
+Eigen::Vector2d PointOfClosestApproachSearchTool::getCenter(
+    const std::vector<PointOfClosestApproachSearchTool::TrackPosition> &trackPositions) const {
+  Eigen::Vector2d center{0, 0};
+  Eigen::Vector2d sumInverse{0, 0};
+  for (const TrackPosition &value : trackPositions) {
+    center(0) += value.inv(0, 0) * value.pos(0);
+    sumInverse(0) += value.inv(0, 0);
+    center(1) += value.inv(1, 1) * value.pos(1);
+    sumInverse(1) += value.inv(1, 1);
+  }
+  center(0) /= sumInverse(0);
+  center(1) /= sumInverse(1);
+  return center;
+}
+
+// Extrapolate track to given z positon.
+std::unique_ptr<const Acts::BoundTrackParameters>
+PointOfClosestApproachSearchTool::extrapolateTrack(const Trk::Track *track,
+                                                   double targetPosition) const {
+  Acts::BoundTrackParameters startParameters =
+      getParametersFromTrack(track->trackParameters()->front());
+  auto targetSurface = Acts::Surface::makeShared<Acts::PlaneSurface>(
+      Acts::Vector3(0, 0, targetPosition), Acts::Vector3(0, 0, 1));
+  Acts::NavigationDirection navigationDirection =
+      targetPosition > startParameters.referenceSurface().center(m_gctx).z() ? Acts::forward
+                                                                             : Acts::backward;
+  return m_extrapolationTool->propagate(m_ctx, startParameters, *targetSurface,
+                                        navigationDirection);
+}
+
+std::optional<PointOfClosestApproachSearchTool::TrackPosition>
+PointOfClosestApproachSearchTool::extrapolateTrackParameters(
+    const Acts::BoundTrackParameters &startParameters, double z) const {
+  auto targetSurface =
+      Acts::Surface::makeShared<Acts::PlaneSurface>(Acts::Vector3(0, 0, z), Acts::Vector3(0, 0, 1));
+  Acts::NavigationDirection navigationDirection =
+      z > startParameters.referenceSurface().center(m_gctx).z() ? Acts::forward : Acts::backward;
+  std::unique_ptr<const Acts::BoundTrackParameters> targetParameters =
+      m_extrapolationTool->propagate(m_ctx, startParameters, *targetSurface, navigationDirection);
+  if (targetParameters != nullptr && targetParameters->covariance().has_value()) {
+    // extrapolate covariance matrix?
+    // return TrackPosition{targetParameters->position(m_gctx),
+    // startParameters.covariance().value().topLeftCorner(2, 2)};
+    return TrackPosition(targetParameters->position(m_gctx),
+                         targetParameters->covariance().value().topLeftCorner(2, 2));
+  } else {
+    ATH_MSG_DEBUG("Extrapolation failed.");
+    return std::nullopt;
+  }
+}
+
+std::optional<PointOfClosestApproachSearchTool::VertexCandidate>
+PointOfClosestApproachSearchTool::getVertexCandidate(
+    std::vector<Acts::BoundTrackParameters> &trackParameters, double z) const {
+  // extrapolate tracks to given z position
+  std::vector<TrackPosition> trackPositions{};
+  for (const Acts::BoundTrackParameters &startParameters : trackParameters) {
+    auto tp = extrapolateTrackParameters(startParameters, z);
+    if (tp) {
+      trackPositions.push_back(*tp);
+    }
+  }
+  if (trackPositions.size() < 2) {
+    ATH_MSG_DEBUG(
+        "Extrapolation failed for too many tracks. Require atleast 2 extrapolated positions.");
+    return std::nullopt;
+  }
+
+  // calculate center weighted by inverse covariance matrix
+  Eigen::Vector2d trackCenter = getCenter(trackPositions);
+
+  // either use weighted x and y position or only y position to calculate chi2 value
+  double vertexChi2 = chi2Y(trackPositions, trackCenter);
+
+  return PointOfClosestApproachSearchTool::VertexCandidate(z, vertexChi2, trackCenter);
+}
+
+double PointOfClosestApproachSearchTool::chi2(const std::vector<TrackPosition> &trackPositions,
+                                              const Eigen::Vector2d &trackCenter) const {
+  double chi2 = 0;
+  for (const TrackPosition &value : trackPositions) {
+    Eigen::Vector2d delta = trackCenter - value.pos;
+    chi2 += delta.dot(value.inv.diagonal().cwiseProduct(delta));
+  }
+  return chi2;
+}
+
+double PointOfClosestApproachSearchTool::chi2Y(const std::vector<TrackPosition> &trackPositions,
+                                               const Eigen::Vector2d &trackCenter) const {
+  double chi2 = 0;
+  for (const TrackPosition &value : trackPositions) {
+    Eigen::Vector2d delta = trackCenter - value.pos;
+    chi2 += delta.y() * value.inv(1, 1) * delta.y();
+  }
+  return chi2;
+}
+
+Acts::BoundTrackParameters PointOfClosestApproachSearchTool::getParametersFromTrack(
+    const Trk::TrackParameters *trackParameters) const {
+  using namespace Acts::UnitLiterals;
+
+  Acts::GeometryContext geometryContext =
+      m_trackingGeometryTool->getNominalGeometryContext().context();
+  Acts::AngleAxis3 rotZ(M_PI / 2, Acts::Vector3(0., 0., 1.));
+  Acts::Translation3 translation{0., 0., trackParameters->associatedSurface().center().z()};
+  auto transform = Acts::Transform3(translation * rotZ);
+  auto surface = Acts::Surface::makeShared<Acts::PlaneSurface>(transform);
+
+  auto bound = Acts::detail::transformFreeToBoundParameters(
+      trackParameters->position(), 0, trackParameters->momentum(),
+      trackParameters->charge() / trackParameters->momentum().mag() / 1_MeV, *surface,
+      geometryContext);
+  if (!bound.ok()) {
+    ATH_MSG_WARNING("FreeToBound parameter transformation failed");
+  }
+  // convert covariance matrix to GeV
+  Acts::BoundSymMatrix cov = Acts::BoundSymMatrix::Identity();
+  cov.topLeftCorner(5, 5) = *trackParameters->covariance();
+  for (int i = 0; i < cov.rows(); i++) {
+    cov(i, 4) = cov(i, 4) / 1_MeV;
+  }
+  for (int i = 0; i < cov.cols(); i++) {
+    cov(4, i) = cov(4, i) / 1_MeV;
+  }
+  return Acts::BoundTrackParameters{surface, bound.value(), trackParameters->charge(), cov};
+}
+
+void PointOfClosestApproachSearchTool::clear() const {
+  m_allVertexCandidates.clear();
+  m_goodVertexCandidates.clear();
+}
+
+} // namespace FaserTracking
diff --git a/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/PointOfClosestApproachSearchTool.h b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/PointOfClosestApproachSearchTool.h
new file mode 100644
index 0000000000000000000000000000000000000000..9ed794948483ee01fcfb13203971ba7a897657da
--- /dev/null
+++ b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/PointOfClosestApproachSearchTool.h
@@ -0,0 +1,84 @@
+#ifndef FASERACTSVERTEXING_POINTOFCLOSESTAPPROACHSEARCHTOOL_H
+#define FASERACTSVERTEXING_POINTOFCLOSESTAPPROACHSEARCHTOOL_H
+
+#include "TrackerPrepRawData/FaserSCT_ClusterContainer.h"
+#include "Acts/EventData/TrackParameters.hpp"
+#include "AthenaBaseComps/AthAlgTool.h"
+#include "FaserActsGeometryInterfaces/IFaserActsExtrapolationTool.h"
+#include "FaserActsGeometryInterfaces/IFaserActsTrackingGeometryTool.h"
+#include "FaserActsVertexing/IVertexingTool.h"
+#include "TrkTrack/Track.h"
+
+namespace FaserTracking {
+
+class PointOfClosestApproachSearchTool : public extends<AthAlgTool, IVertexingTool> {
+public:
+  PointOfClosestApproachSearchTool(const std::string &type, const std::string &name,
+                                   const IInterface *parent);
+  virtual ~PointOfClosestApproachSearchTool() = default;
+  virtual StatusCode initialize() override;
+  virtual StatusCode finalize() override;
+
+  std::optional<POCA> getVertex(const std::vector<const Trk::Track *> &tracks) const override;
+
+  std::unique_ptr<const Acts::BoundTrackParameters>
+  extrapolateTrack(const Trk::Track *track, double targetPosition) const override;
+
+private:
+  struct VertexCandidate {
+    VertexCandidate(double z, double chi2, const Eigen::Vector2d &center) : chi2(chi2) {
+      position = Eigen::Vector3d(center.x(), center.y(), z);
+    }
+    double chi2;
+    Eigen::Vector3d position;
+  };
+
+  struct TrackPosition {
+    TrackPosition(const Acts::Vector3 &position, const Acts::BoundSymMatrix &covariance) {
+      pos << position.x(), position.y();
+      cov << covariance(1, 1), 0, 0, covariance(0, 0);
+      inv = cov.inverse();
+    }
+    Eigen::Vector2d pos;
+    Eigen::Matrix2d cov;
+    Eigen::Matrix2d inv;
+  };
+
+  ToolHandle<IFaserActsTrackingGeometryTool> m_trackingGeometryTool{
+      this, "TrackingGeometryTool", "FaserActsTrackingGeometryTool"};
+  ToolHandle<IFaserActsExtrapolationTool> m_extrapolationTool{
+    this, "ExtrapolationTool", "FaserActsExtrapolationTool"};
+  DoubleProperty m_stepSize{this, "StepSize", 50};
+  DoubleProperty m_zMin{this, "ZMin", -2000};
+  DoubleProperty m_zMax{this, "ZMax", 500};
+  StringProperty m_debugLevel{this, "DebugLevel", "INFO"};
+
+  void clear() const;
+
+  std::optional<TrackPosition>
+  extrapolateTrackParameters(const Acts::BoundTrackParameters &startParameters, double z) const;
+
+  Eigen::Vector2d getCenter(const std::vector<TrackPosition> &trackPositions) const;
+
+  Acts::BoundTrackParameters
+  getParametersFromTrack(const Trk::TrackParameters *trackParameters) const;
+
+  double chi2(const std::vector<TrackPosition> &trackPositions,
+              const Eigen::Vector2d &trackCenter) const;
+
+  double chi2Y(const std::vector<TrackPosition> &trackPositions,
+               const Eigen::Vector2d &trackCenter) const;
+
+  std::optional<VertexCandidate>
+  getVertexCandidate(std::vector<Acts::BoundTrackParameters> &trackParameters, double z) const;
+
+  mutable EventContext m_ctx;
+  mutable Acts::GeometryContext m_gctx;
+
+  mutable std::vector<VertexCandidate> m_allVertexCandidates;
+  mutable std::vector<VertexCandidate> m_goodVertexCandidates;
+};
+
+} // namespace FaserTracking
+
+#endif /* FASERACTSVERTEXING_POINTOFCLOSESTAPPROACHSEARCHTOOL_H */
diff --git a/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/components/FaserActsVertexing_entries.cxx b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/components/FaserActsVertexing_entries.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..4f27a5a3d95ba97b48d36ac0b43c634c1d99d9ae
--- /dev/null
+++ b/Tracking/Acts/FaserActsKalmanFilter/FaserActsVertexing/src/components/FaserActsVertexing_entries.cxx
@@ -0,0 +1,3 @@
+#include "../PointOfClosestApproachSearchTool.h"
+
+DECLARE_COMPONENT(FaserTracking::PointOfClosestApproachSearchTool)
\ No newline at end of file