diff --git a/Generators/DIFGenerator/python/DIFSampler.py b/Generators/DIFGenerator/python/DIFSampler.py
index 24659c0178d8cbefc9843a24a1a10ef38bbcde21..69fe82660a4b9d9ae555799d2a8f76dbca5629c6 100644
--- a/Generators/DIFGenerator/python/DIFSampler.py
+++ b/Generators/DIFGenerator/python/DIFSampler.py
@@ -111,7 +111,7 @@ class DIFSampler(PG.ParticleSampler):
         return sqrt(m0**4 - (2*m0**2 * m1**2) + (m1**4) - (2*m0**2 * m2**2) - (2*m1**2 *m2**2) + (m2**4)) / (2*m0) 
 
     def lorentz_transformation(self):
-        self.mother_mom = self.mother.mom.shoot()
+        #self.mother_mom = self.mother.mom.shoot()
         Bx = self.mother_mom.Px() / self.mother_mom.E()
         By = self.mother_mom.Py() / self.mother_mom.E()
         Bz = self.mother_mom.Pz() / self.mother_mom.E()
@@ -132,6 +132,11 @@ class DIFSampler(PG.ParticleSampler):
 
         ## The magnitude of the momentum will be equal and opposite
         self.mother = self.mother_sampler
+        self.mother.mass_override = False
+        mother_part = self.mother.shoot()[0]
+        self.mother_mom = mother_part.mom
+        self.mother_pos = mother_part.pos
+        
         p = self.calculate_decay_p(self.mother.mom.mass(),self.daughter1.mass,self.daughter2.mass)
 
         self.daughter1.E = sqrt(self.daughter1.mass**2 + p**2)
@@ -412,4 +417,4 @@ if __name__ == "__main__":
     DIFS = DIFSampler() 
     DIFS.mother_sampler = SP()
 
-    print("DIFSampler: All unit tests passed")
\ No newline at end of file
+    print("DIFSampler: All unit tests passed")
diff --git a/Generators/FaserParticleGun/python/FaserParticleGunConfig.py b/Generators/FaserParticleGun/python/FaserParticleGunConfig.py
index 086d3c6aa5e3b5827df893a727d7e6a59f858780..6357163705cd12872a421651495c32db58f021c7 100644
--- a/Generators/FaserParticleGun/python/FaserParticleGunConfig.py
+++ b/Generators/FaserParticleGun/python/FaserParticleGunConfig.py
@@ -159,30 +159,25 @@ def FaserParticleGunForeseeCfg(ConfigFlags, **kwargs) :
 
     pg = cfg.getPrimary()
 
-    from DIFGenerator.DIFSampler import CylinderSampler
-    
-    from DIFGenerator import DIFSampler
-    pg.sampler = DIFSampler(
-        daughter1_pid = kwargs.get("daughter1_pid", 11),
-        daughter2_pid = kwargs.get("daughter2_pid", -11),
-        mother_pid = kwargs.get("mother_pid", None),
-        mother_pos = kwargs.get("mother_pos", CylinderSampler([0, 100**2],[0, 2*pi],[-1500, 0],0))
-        )
-
     from ForeseeGenerator.ForeseeSampler import ForeseeNumpySampler
-    mother_mom = ForeseeNumpySampler(
+    mother_part = ForeseeNumpySampler(
         model_path = kwargs.get("model_path", "."),
         model_name = kwargs.get("model_name", "DarkPhoton"),
         com_energy = kwargs.get("com_energy", "14"),
         mother_mass = kwargs.get("mother_mass", 0.01),
+        mother_pid = kwargs.get("mother_pid", None),
         daughter1_pid = kwargs.get("daughter1_pid", 11),
         daughter2_pid = kwargs.get("daughter2_pid", -11),
         randomSeed = kwargs.get("randomSeed", None)        
         )
-    
-    pg.sampler.mother_sampler = PG.ParticleSampler(pid=kwargs.get("mother_pid", None),mom=mother_mom,n=1,pos=pg.sampler.mother_sampler.pos)
-    pg.sampler.mother_sampler.mass_override = False
 
+    from DIFGenerator import DIFSampler
+    pg.sampler = DIFSampler(
+        daughter1_pid = kwargs.get("daughter1_pid", 11),
+        daughter2_pid = kwargs.get("daughter2_pid", -11),
+        )
+        
+    pg.sampler.mother_sampler = mother_part
 
     return cfg
 
diff --git a/Generators/FlukaReader/CMakeLists.txt b/Generators/FlukaReader/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..eebebd79e2b466203bb35ba1aa3d3d265d39920d
--- /dev/null
+++ b/Generators/FlukaReader/CMakeLists.txt
@@ -0,0 +1,11 @@
+################################################################################
+# Package: FlukaReader
+################################################################################
+
+# Declare the package name:
+atlas_subdir( FlukaReader )
+
+# Install files from the package:
+atlas_install_python_modules( python/*.py POST_BUILD_CMD ${ATLAS_FLAKE8} )
+
+#atlas_install_joboptions( share/*.py )
diff --git a/Generators/FlukaReader/python/FlukaReaderAlg.py b/Generators/FlukaReader/python/FlukaReaderAlg.py
new file mode 100644
index 0000000000000000000000000000000000000000..429a443d9bacad678356d794054018a59ea815aa
--- /dev/null
+++ b/Generators/FlukaReader/python/FlukaReaderAlg.py
@@ -0,0 +1,395 @@
+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, MeV, cm
+from AthenaCommon.Constants import DEBUG
+
+from FaserCosmicGenerator import Range
+
+import ROOT
+
+import numpy as np
+import math
+
+class FlukaReader(EvgenAlg):
+    def __init__(self, name="FlukaReader", MCEventKey="BeamTruthEvent", file_name = "", dist = 0, randomSeed = None, nsamples = 1, test = False):
+        super(FlukaReader,self).__init__(name=name)
+        self.McEventKey = MCEventKey
+        self.file_name = file_name
+        self.dist = dist * 100
+        self.isample = 0
+        self.nsamples = nsamples
+        self.test = test
+
+        self.columns = ["run", "event", "type", "gen", "E", "w", "x", "y", "cosX", "cosY", "age", "_"]
+
+        if randomSeed is not None:
+            self.msg.info(f"Setting seed to {randomSeed}")
+            self.rng = np.random.default_rng(randomSeed)
+        else:
+            self.rng = np.random.default_rng()        
+
+        self.file = open(self.file_name)
+
+        if self.test:
+            self.before = dict(zip(self.columns, [[] for i in range(len(self.columns))]))
+            self.after = dict(zip(self.columns, [[] for i in range(len(self.columns))]))                           
+               
+        return
+
+    def genFinalize(self):
+
+        if self.test:
+            self.plot()
+        
+        self.file.close()
+        return StatusCode.Success
+
+    def fillEvent(self, evt):
+        "This is called for every real event * the number of samplings"
+
+        # If the sample gets to the number requested, then reset to 0
+        if self.isample == self.nsamples:
+            self.msg.debug("Reseting samples")
+            self.isample = 0
+
+        # Only if the sample is 0 load the new fluka entry
+        if self.isample == 0:
+            self.msg.debug("Loading new fluka event")
+            try:
+                l = next(self.file)
+            except StopIteration:
+                return StatusCode.Success
+            
+            entry =  dict(zip(self.columns, l.strip("\n").split()))
+            for i,c in enumerate(self.columns):
+                if i < 4:
+                    entry[c] = int(entry[c])
+                else:
+                    entry[c] = float(entry[c])
+
+            self.entry = entry
+
+        # Call for each sample of each event
+        self.msg.debug(f"Processing sample {self.isample}")
+        self.process(self.entry, evt)
+        self.isample += 1
+
+        return StatusCode.Success
+
+    def angle(self, cosTheta):
+        "Convert cos(theta) wrt x or y axis to theta wrt z axis"
+        return np.pi/2 - np.arccos(cosTheta)
+
+    def pid(self, ftype):
+        "Convert fluka particle type to PID"
+        if ftype == 10:  # mu+
+            return -13
+        elif ftype == 11: # mu-
+            return 13
+        else:
+            return 0
+
+    def path_length(self, z, cosThetaX, cosThetaY):
+        "Get path length traversed in the material, taking into account incident angle"
+        
+        # Convert theta wrt x and y axis to wrt z axis
+        thetaX = self.angle(cosThetaX)
+        thetaY = self.angle(cosThetaY)        
+        
+        # Correct z for angle 
+        zcorr = z / np.abs(np.cos(thetaX)) / np.abs(np.cos(thetaY))
+        
+        return zcorr
+
+    def energy_after_loss_exact(self, e, zcorr):
+        "Calculate exact energy after loss in material"
+        return Range.muPropagate(e, zcorr/1000.)
+
+    def energy_after_loss(self, e, cosThetaX, cosThetaY, zcorr, a = 2e-3, b = 4e-6):
+        "Calculate approximate energy after loss in material"
+    
+        # Based on
+        # http://www.bartol.udel.edu/~stanev/lectures/apr17.pdf
+        # and PDG chapter 27 fig 27.1
+        # https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiG9YjNtvr2AhUbiVwKHdQfD9sQFnoECAsQAQ&url=https%3A%2F%2Fpdg.lbl.gov%2F2005%2Freviews%2Fpassagerpp.pdf&usg=AOvVaw1HGA5PZtC2UiqA6B7_C5dz        
+                
+        eps = a/b
+        return (e + eps) * np.exp(-b * zcorr) - eps
+
+    def mean_scattering_angle(self, e, cosThetaX, cosThetaY, zcorr, X0 = 10.02, m = 105.66e-3, charge = 1, beta = 1):
+        "Calculate mean scattering angle over many scatters for given energy and length z"
+        
+        # Based on PDG chapter 27 eqns 27.10, 27.16, 27.17
+        # https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiG9YjNtvr2AhUbiVwKHdQfD9sQFnoECAsQAQ&url=https%3A%2F%2Fpdg.lbl.gov%2F2005%2Freviews%2Fpassagerpp.pdf&usg=AOvVaw1HGA5PZtC2UiqA6B7_C5dz        
+        # and
+        # https://pdg.lbl.gov/2014/AtomicNuclearProperties/HTML/standard_rock.html
+        
+        # Convert E to momentum [GeV]
+        p = np.sqrt(e**2 - m**2)  
+        
+        # Mean angle [GeV and cm]
+        c = 1 # n.u
+        theta0 = 13.6e-3 / (p * c * beta) * charge *  np.sqrt(zcorr/X0) * (1 + 0.38 * math.log(zcorr, X0))
+        return theta0
+
+    def scattering_angle(self, cosTheta, theta0, rand1):
+        "Calculate actual scattering angle over many scatters for given start angle and mean angle"
+        
+        # Convert theta wrt x or y axis to wrt z axis
+        theta = self.angle(cosTheta)
+        
+        # Add random scattering angle
+        return theta + rand1 * theta0
+
+    def scattering_postition(self, x, cosThetaX, cosThetaY, zcorr, theta0, rand1, rand2):
+        "Calculate transverse scattering position over many scatters for given start angle and mean angle + length z"
+        
+        # Convert theta wrt x to wrt z axis
+        thetaX = self.angle(cosThetaX)
+        
+        xout = np.copy(x)
+        
+        # Add displacement due to initial angle
+        #xang = z * np.tan(thetaX)
+        xang = zcorr * np.sin(thetaX)            
+        xout += xang
+        
+        # Add displacement due to multiple scattering
+        dx = rand1 * zcorr * theta0 / np.sqrt(12) + rand2 * zcorr * theta0/2
+        xout += dx
+    
+        return xout
+
+    def propagate(self, entry):
+        "Propagate the particle through a given distance of standard rock using the small-angle approxiumation"
+
+        if self.dist == 0:
+            return entry
+
+        # Random numbers
+        rand1 = self.rng.normal(0, 1)
+        rand2 = self.rng.normal(0, 1)        
+
+        # Get entry info
+        e = entry["E"]
+        x = entry["x"]
+        y = entry["y"]        
+        cosX = entry["cosX"]
+        cosY = entry["cosY"]
+
+        # Correct path length for angles
+        z = self.path_length(self.dist, cosX, cosY)
+
+        # Account for energy loss
+        #eout = self.energy_after_loss(e, cosX, cosY, z)        
+        eout = self.energy_after_loss_exact(e, z) 
+
+        # Account for scattering on angle and position
+        theta0 = self.mean_scattering_angle(e, cosX, cosY, z)
+        thetaXout = self.scattering_angle(cosX, theta0, rand1)
+        thetaYout = self.scattering_angle(cosY, theta0, rand1)
+        xout = self.scattering_postition(x, cosX, cosY, z, theta0, rand1, rand2)
+        yout = self.scattering_postition(y, cosY, cosX, z, theta0, rand1, rand2)        
+
+        # Update entry info
+        entry["E"] = eout
+        entry["x"] = xout
+        entry["y"] = yout        
+        entry["cosX"] =  np.cos(np.pi/2 + thetaXout)
+        entry["cosY"] =  np.cos(np.pi/2 + thetaYout)
+        
+        return entry
+
+
+    def process(self, entry, evt):
+
+        if self.test:
+            for k,v in entry.items():
+                self.before[k].append(float(v))
+
+        if self.msg.level > DEBUG: 
+            print("Original Entry", entry)
+
+        if self.dist != 0:
+            # Propoagate to FASER            
+            entry = self.propagate(entry)
+        elif self.nsamples != 1:
+            # Else smear if sampling more than once
+            entry["E"] *= self.rng.normal(1, 0.05)
+            entry["x"] *= self.rng.normal(1, 0.05)
+            entry["y"] *= self.rng.normal(1, 0.05)            
+            entry["cosX"] = np.cos(np.arccos(entry["cosX"]) * self.rng.normal(1, 0.05))
+            entry["cosY"] = np.cos(np.arccos(entry["cosY"]) * self.rng.normal(1, 0.05))
+
+        if self.msg.level > DEBUG: 
+            print("Propagated/Smeared Entry", entry)
+
+            
+        if self.test:
+            for k,v in entry.items():
+                self.after[k].append(float(v))
+        
+        try:
+          from AthenaPython.PyAthena import HepMC3  as HepMC
+        except ImportError:
+          from AthenaPython.PyAthena import HepMC   as HepMC
+
+        # Add weight, correcting for mutliple sampling
+        evt.weights().push_back(entry["w"] / self.nsamples)
+
+
+        # Setup MC event
+        mcEventType = EventType()
+        mcEventType.add_type(EventType.IS_SIMULATION)
+
+        mcEventId = EventID(run_number = entry["run"], event_number = entry["event"])
+
+        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)
+
+        # Create HepMC Vertex
+        z = 0
+        pos = HepMC.FourVector(entry["x"] * cm, entry["y"] * cm, z, 0)
+        gv = HepMC.GenVertex(pos)
+
+        ROOT.SetOwnership(gv, False)
+        evt.add_vertex(gv)
+
+        # Create HepMC particle
+        gp = HepMC.GenParticle()
+
+        m = 105.66
+        e = entry["E"] * 1000.
+        p = np.sqrt(e**2 - m**2)
+
+        thetaX = self.angle(entry["cosX"])
+        thetaY = self.angle(entry["cosY"])        
+        px = p * np.sin(thetaX) * np.cos(thetaY)
+        py = p * np.cos(thetaX) * np.sin(thetaY)        
+        pz = p * np.cos(thetaX) * np.cos(thetaY)
+        
+        mom = HepMC.FourVector(px, py, pz, e)
+
+        gp.set_momentum(mom)
+        gp.set_generated_mass(m)
+        gp.set_pdg_id(self.pid(entry["type"]))
+        gp.set_status(1)
+        
+        ROOT.SetOwnership(gp, False)
+        gv.add_particle_out(gp)
+
+        return
+
+    def plot(self):
+        "Plot entries before and after propagation/smeating for tests"
+
+        if not self.test:
+            return
+
+        import matplotlib.pyplot as plt
+        
+        plt.figure()
+        ebins = np.linspace(0, 5000, 50)
+        plt.hist(self.before["E"], bins=ebins, histtype='step', color = "g", fill = False)
+        plt.hist(self.after["E"], bins = ebins, histtype='step', color = "r", fill = False)
+        plt.gca().set_yscale('log')
+        plt.savefig("energy.eps")
+
+        plt.figure()
+        thetaX =  np.pi/2. - np.arccos(np.array(self.before["cosX"]))
+        thetaXout =  np.pi/2. - np.arccos(np.array(self.after["cosX"]))        
+        tbins = np.linspace(-0.5, 0.5, 100)
+        plt.hist(thetaX, bins = tbins, histtype='step', color = "g", fill = False)
+        plt.hist(thetaXout, bins = tbins, histtype='step', color = "r", fill = False)
+        plt.gca().set_yscale('log')
+        plt.savefig("thetaX.eps")
+
+        plt.figure()
+        thetaY =  np.pi/2. - np.arccos(np.array(self.before["cosY"]))
+        thetaYout =  np.pi/2. - np.arccos(np.array(self.after["cosY"]))        
+        plt.hist(thetaY, bins = tbins, histtype='step', color = "g", fill = False)
+        plt.hist(thetaYout, bins = tbins, histtype='step', color = "r", fill = False)
+        plt.gca().set_yscale('log')
+        plt.savefig("thetaY.eps")
+
+        plt.figure()
+        xbins = np.linspace(-300, 300, 100)
+        plt.hist(self.before["x"], bins = xbins, histtype='step', color = "g", fill = False)
+        plt.hist(self.after["x"], bins = xbins, histtype='step', color = "r", fill = False)
+        plt.gca().set_yscale('log')
+        plt.savefig("x.eps")
+
+        plt.figure()
+        plt.hist(self.before["y"], bins = xbins, histtype='step', color = "g", fill = False)
+        plt.hist(self.after["y"], bins = xbins, histtype='step', color = "r", fill = False)        
+        plt.gca().set_yscale('log')
+        plt.savefig("y.eps")
+
+        return
+
+if __name__ == "__main__":
+
+#     from AthenaCommon.AlgSequence import AlgSequence
+#     job = AlgSequence()
+#     job += FlukaReader(file_name = "/user/gwilliam/unit30_Nm", dist = (480-409))
+# 
+#     from AthenaPoolCnvSvc.WriteAthenaPool import AthenaPoolOutputStream
+#     ostream = AthenaPoolOutputStream( "StreamEVGEN" , "evgen.pool.root", noTag=True )    
+#     ostream.ItemList.remove("EventInfo#*")
+#     ostream.ItemList += [ "EventInfo#McEventInfo", 
+#                            "McEventCollection#*" ]
+#                 
+#     theApp.EvtMax = 1000
+
+
+    import argparse, sys    
+    parser = argparse.ArgumentParser(description="Run Fluka reader")
+    parser.add_argument("file", help = "Path to fluka file")
+    parser.add_argument("--dist", "-z", default = 0, type = float, help = "depth of standard rock to propagate through [m]")
+    parser.add_argument("--output", "-o",  default = "evgen.pool.root", help = "Name of output file")
+    parser.add_argument("--mcEventKey", "-k",  default = "BeamTruthEvent", help = "Name of MC collection")
+    parser.add_argument("--nevents", "-n", default = -1, type = int, help = "Number of events to process")
+    parser.add_argument("--randomSeed", "-r", default=12345, type=int, help = "Seed for random number generator")
+    parser.add_argument("--nsamples", "-s", default = 1, type = int, help = "Number of times to sample each event")
+    parser.add_argument("--test", "-t", action = "store_true", help = "Make test plots")    
+    args = parser.parse_args()
+
+
+    from AthenaCommon.Logging import log
+    from AthenaCommon.Constants import DEBUG, INFO
+    
+    from AthenaCommon.Configurable import Configurable
+    Configurable.configurableRun3Behavior = 1
+
+    from CalypsoConfiguration.AllConfigFlags import ConfigFlags
+    ConfigFlags.Input.isMC = True
+    ConfigFlags.IOVDb.GlobalTag = "OFLCOND-FASER-01"             # Always needed; must match FaserVersion
+    ConfigFlags.GeoModel.FaserVersion     = "FASER-01"           # Default FASER geometry
+    ConfigFlags.Detector.EnableFaserSCT = True
+    ConfigFlags.Output.EVNTFileName = args.output    
+    ConfigFlags.lock()
+
+    from CalypsoConfiguration.MainServicesConfig import MainServicesCfg
+    cfg = MainServicesCfg(ConfigFlags)
+    
+    from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
+    from AthenaConfiguration.ComponentFactory import CompFactory
+
+    acc = ComponentAccumulator()
+    reader = FlukaReader("FlukaReader", MCEventKey=args.mcEventKey, file_name = args.file, dist = args.dist, randomSeed = args.randomSeed, nsamples = args.nsamples, test = args.test)
+    reader.OutputLevel = INFO
+    acc.addEventAlgo(reader)    
+    cfg.merge(acc)
+
+    itemList = [ "EventInfo#McEventInfo", "McEventCollection#*" ]
+    from OutputStreamAthenaPool.OutputStreamConfig import OutputStreamCfg
+    cfg.merge(OutputStreamCfg(ConfigFlags, "EVNT", itemList, disableEventTag = True))
+
+    sc = cfg.run(maxEvents = args.nevents * args.nsamples)
+    sys.exit(not sc.isSuccess())    
diff --git a/Generators/ForeseeGenerator/python/ForeseeSampler.py b/Generators/ForeseeGenerator/python/ForeseeSampler.py
index d80be0aa3aa080d93eb9ec7510b61aebf6089464..2f7d7241e560e552b8df3bcd9cb9b62e321f17ff 100644
--- a/Generators/ForeseeGenerator/python/ForeseeSampler.py
+++ b/Generators/ForeseeGenerator/python/ForeseeSampler.py
@@ -4,18 +4,24 @@ import numpy as np
 
 import ParticleGun as PG
 
-class ForeseeNumpySampler(PG.MomSampler):
+from DIFGenerator.DIFSampler import CylinderSampler
+
+class ForeseeNumpySampler(PG.ParticleSampler):
     """ Sample from the output of Foresee generation in numpy format with columns E, theta, weight"""
     def __init__(self, model_path = ".", model_name = "DarkPhoton",  com_energy = "14", mother_mass = 0.01,
-                 coupling = None, daughter1_pid = 11, daughter2_pid = -11, randomSeed = None):
+                 coupling = None, mother_pid = None, daughter1_pid = 11, daughter2_pid = -11, randomSeed = None):
 
         self.path = os.path.expanduser(os.path.expandvars(model_path))
         self.modelname = model_name
         self.energy = com_energy
-        self._mass = mother_mass
+        self.mass = mother_mass
         self.coupling = coupling
+        self.pid = mother_pid
         self.daughter1_pid = daughter1_pid
         self.daughter2_pid = daughter2_pid
+        self.n = 1
+        self.distance = 480 # m
+        self.mass_override = False
 
         if randomSeed is not None:
             print(f"Setting seed to {randomSeed}")
@@ -33,9 +39,9 @@ class ForeseeNumpySampler(PG.MomSampler):
         if self.path.endswith(".npy"):
             filename = self.path
         elif self.coupling is None:
-            filename = f"{self.path}/files/models/{self.modelname}/events/events_{self.energy}TeV_m{self._mass}GeV_to_{self.daughter1_pid}_{self.daughter2_pid}.npy"
+            filename = f"{self.path}/files/models/{self.modelname}/events/events_{self.energy}TeV_m{self.mass}GeV_to_{self.daughter1_pid}_{self.daughter2_pid}.npy"
         else:
-            filename = f"{self.path}/files/models/{self.modelname}/events/events_{self.energy}TeV_m{self._mass}GeV_c{self.coupling}to_{self.daughter1_pid}_{self.daughter2_pid}.npy"
+            filename = f"{self.path}/files/models/{self.modelname}/events/events_{self.energy}TeV_m{self.mass}GeV_c{self.coupling}to_{self.daughter1_pid}_{self.daughter2_pid}.npy"
 
         print(f"Reading data from file: {filename}")
         self.data = np.load(filename)
@@ -45,22 +51,42 @@ class ForeseeNumpySampler(PG.MomSampler):
         
         return
 
-    def mass(self):
-        "Mass converted from GeV to MeV"
-        return self._mass * 1000
+#     def mass(self):
+#         "Mass converted from GeV to MeV"
+#         return self._mass * 1000
 
     def shoot(self):
         "Choose a random item from the data, based on the probability"
 
-        # TODO: what about reuse of events?
         energy, theta, weight = self.rng.choice(self.data, axis = 1, p = self.prob)
 
         self.xs += weight
 
+        # Convert mass to MeV
+        mass = self.mass * 1000
+
+        # Sample phi
+        phi = self.rng.uniform(0, 2*np.pi)
+
+        # Sample z
+        z = self.rng.uniform(-1500, 0)        
+
         # Create mother momentum, randomly sampling phi
-        mother_mom = PG.EThetaMPhiSampler(energy, theta, self.mass(), [0, 2*np.pi])
+        self.mom = PG.EThetaMPhiSampler(energy, theta, mass, phi)
 
-        return mother_mom.shoot()
+        # Create mother pos, randomly sampling phi
+        r = (self.distance * 1000 + abs(z))  * np.tan(theta)
+        
+        self.pos = CylinderSampler(r**2, phi, z, 0)
+
+        # Create particle
+        p = PG.SampledParticle(self.pid)
+        p.mom = self.mom.shoot()
+        p.pos = self.pos.shoot()
+        p.mass = mass
+        #self.mom.mass = mass        
+
+        return [p]
 
 class ForeseeSampler(PG.MomSampler):
     """Create events from foresee directly on the fly
@@ -266,7 +292,7 @@ if __name__ == "__main__":
     
     
     path = os.path.expandvars("$Calypso_DIR/../calypso/Generators/ForeseeGenerator/data/events_14TeV_m0.1GeV_c1e-05to_11_-11.npy")
-
+    path = "files/models/DarkPhoton/events/events_14TeV_m0.1GeV_c1e-05to_11_-11.npy"
 
     modelname = "DarkPhoton"
     mass = 0.1
@@ -281,21 +307,17 @@ if __name__ == "__main__":
     d1mom = []
 
     # Accounting for rounding
-    epsilon = 5
+    epsilon = 6
 
-    # Mother position sampler over Z
-    mother_pos = PG.PosSampler(x=0,y=0,z=[-1500,0],t=0) 
-    
-    # Create mother momentum sampler reading data from foresee
-    mother_mom_sampler = ForeseeNumpySampler(model_path = path, model_name = modelname, com_energy = "14", mother_mass = 0.1, coupling = 1e-5,  daughter1_pid = 11, daughter2_pid = -11)
+    # Create mother sampler reading data from foresee
+    mother_sampler = ForeseeNumpySampler(model_path = path, model_name = modelname, com_energy = "14", mother_mass = 0.1, coupling = 1e-5,  mother_pid = None, daughter1_pid = 11, daughter2_pid = -11)
     
     # Create decay-in-flight
     d = DIFSampler(11, -11, None)
-    d.mother_sampler = PG.ParticleSampler(pid=None,mom=mother_mom_sampler,n=1,pos=mother_pos)
-    d.mother_sampler.mass_override = False
+    d.mother_sampler = mother_sampler
     
     # Loop over a range of events
-    for i in range(1000):        
+    for i in range(100000):        
 
         # Shoot the decay in flight
         daughters = d.shoot()
@@ -434,7 +456,7 @@ if __name__ == "__main__":
     plt.savefig(f"{modelname}_PG_d1_m{mass}.png")
     
 
-    print (f"x-sect = {mother_mom_sampler.xs} pb")
+    print (f"x-sect = {mother_sampler.xs} pb")
     
 
 
diff --git a/Generators/ForeseeGenerator/python/Validate.py b/Generators/ForeseeGenerator/python/Validate.py
index 8b53c84caad9190e4a9bda75f05970ec5f524da8..9c6be63475b6e066ece1ceac0aea059b8837a0dc 100644
--- a/Generators/ForeseeGenerator/python/Validate.py
+++ b/Generators/ForeseeGenerator/python/Validate.py
@@ -63,11 +63,11 @@ class HistSvc(object):
 class EvgenValidation(EvgenAnalysisAlg):
     "Gen-level validation"
 
-    def __init__(self, name = "EvgenValidation"):
+    def __init__(self, name = "EvgenValidation", ndaughters = 2, outname = "validation.root"):
         super(EvgenValidation, self).__init__(name=name)
         self.hists = HistSvc()
-        self.ndaughter = 2
-        self.outname = "validation.root"
+        self.ndaughters = ndaughters
+        self.outname = outname
 
     def binning(self):
         "binning for theta vs phi plot"
@@ -84,8 +84,10 @@ class EvgenValidation(EvgenAnalysisAlg):
 
         # Daughter i
         tbins, pbins = self.binning()
-        for i in range(self.ndaughter):
+        for i in range(self.ndaughters):
+            self.hists.add(f"E_d{i}", 100, 0, 10000)                        
             self.hists.add(f"P_d{i}", 100, 0, 10000)
+            self.hists.add(f"Pz_d{i}", 100, 0, 10000)
             self.hists.add(f"Pt_d{i}", 100, 0, 1)
             self.hists.add(f"Theta_d{i}", 20, 0, 0.001)
             self.hists.add(f"Phi_d{i}", 16, -3.2, 3.2)
@@ -93,53 +95,82 @@ class EvgenValidation(EvgenAnalysisAlg):
             self.hists.add(f"Mass_d{i}", 200, 0, 0.01)            
 
         # Mother
-        self.hists.add("P_M", 100, 0, 10000) 
+        self.hists.add("E_M", 100, 0, 10000)
+        self.hists.add("P_M", 100, 0, 10000)
+        self.hists.add("Pz_M", 100, 0, 10000)         
         self.hists.add("Pt_M", 100, 0, 1)       
         self.hists.add("Theta_M", 20, 0, 0.001)
         self.hists.add("Phi_M", 16, -3.2, 3.2)
         self.hists.add("Mass_M", 200, 0, 1)
         self.hists.add("ThetaVsP_M", arrayX = tbins, arrayY = pbins)
+
+        # Vertex
+        self.hists.add("Vtx_X", 50, -100, 100)
+        self.hists.add("Vtx_Y", 50, -100, 100)
+        # For fluka
+        #self.hists.add("Vtx_X", 100, -3000, 3000)
+        #self.hists.add("Vtx_Y", 100, -3000, 3000)        
+        self.hists.add("Vtx_Z", 50, -1500, 0)
+        self.hists.add("Vtx_XY", 50, -100, 100, 50, -100, 100)        
         
         return StatusCode.Success
 
 
     def fillKin(self, label, p, mass = True, twoD = True):
 
-        self.hists[f"P_{label}"].Fill(p.rho()/1000, 1)        
-        self.hists[f"Pt_{label}"].Fill(p.perp()/1000, 1)
-        self.hists[f"Theta_{label}"].Fill(p.theta(), 1)
-        self.hists[f"Phi_{label}"].Fill(p.phi(), 1)
+        self.hists[f"E_{label}"].Fill(p.t()/1000, self.weight)        
+        self.hists[f"P_{label}"].Fill(p.rho()/1000, self.weight)
+        self.hists[f"Pz_{label}"].Fill(p.pz()/1000, self.weight)                
+        self.hists[f"Pt_{label}"].Fill(p.perp()/1000, self.weight)
+        self.hists[f"Theta_{label}"].Fill(p.theta(), self.weight)
+        self.hists[f"Phi_{label}"].Fill(p.phi(), self.weight)
 
         if mass:
-            self.hists[f"Mass_{label}"].Fill(p.m()/1000, 1)
+            self.hists[f"Mass_{label}"].Fill(p.m()/1000, self.weight)
 
         if twoD:
-            self.hists[f"ThetaVsP_{label}"].Fill(p.theta(), p.rho()/1000, 1)
+            self.hists[f"ThetaVsP_{label}"].Fill(p.theta(), p.rho()/1000, self.weight)
 
         return
 
     def fillDaughter(self, p):
-        self.hists["PIDs"].Fill(p.pdg_id())
+        self.hists["PIDs"].Fill(p.pdg_id(), self.weight)
         return
 
+    def fillVertex(self, v):
+        self.hists["Vtx_X"].Fill(v.x(), self.weight)
+        self.hists["Vtx_Y"].Fill(v.y(), self.weight)
+        self.hists["Vtx_Z"].Fill(v.z(), self.weight)
+        self.hists["Vtx_XY"].Fill(v.x(), v.y(), self.weight)        
+        return
+    
+
     def execute(self):
         evt = self.events()[0]
+        self.weight = evt.weights()[0]
 
         # Loop over all particles in events (assuming mother not stored)
         momenta = []
         mother = HepMC.FourVector(0,0,0,0)
+        llp_vtx = None
         for i, p in enumerate(evt.particles):
             #p.print()
             self.fillDaughter(p)
             momenta.append(p.momentum())    
             mother += p.momentum()
+            if i == 0:
+                #p.production_vertex().print()
+                llp_vtx = p.production_vertex().point3d()
 
         # Fill daughter plots
-        for i in range(self.ndaughter):
+        for i in range(self.ndaughters):
             self.fillKin(f"d{i}", momenta[i])
 
         # Fill mother plots
         self.fillKin("M", mother, mass = True)
+
+        # Fill vertex plots
+        self.fillVertex(llp_vtx)
             
         return StatusCode.Success
 
@@ -153,7 +184,7 @@ if __name__ == "__main__":
     import argparse, sys
     parser = argparse.ArgumentParser(description="Run gen-level validation")
     parser.add_argument("file", nargs="+", help = "full path to imput file")
-    parser.add_argument("--ndaugthers", "-d", default = 2, type = float, help = "Number of daugthers to plot")
+    parser.add_argument("--ndaugthers", "-d", default = 2, type = int, help = "Number of daugthers to plot")
     parser.add_argument("--output", "-o",  default = "validation.root", help = "Name of output file")
     parser.add_argument("--mcEventKey", "-k",  default = "BeamTruthEvent", help = "Name of MC collection")
     parser.add_argument("--nevents", "-n", default = -1, type = int, help = "Number of events to process")    
@@ -187,10 +218,8 @@ if __name__ == "__main__":
     fix()
     
     acc = ComponentAccumulator()
-    valid = EvgenValidation()
+    valid = EvgenValidation("EvgenValidation", ndaughters =  args.ndaugthers, outname = args.output)
     valid.McEventKey = args.mcEventKey
-    valid.ndaughters = args.ndaugthers
-    valid.outname = args.output
     acc.addEventAlgo(valid)    
     cfg.merge(acc)
 
diff --git a/Generators/ForeseeGenerator/share/generate_forsee_events.py b/Generators/ForeseeGenerator/share/generate_forsee_events.py
index 2a9e7a7fefd775c9d012fdfefe38c4ca8227b6d7..ba63087bf08bf1e2571865651f255cd9ad177084 100644
--- a/Generators/ForeseeGenerator/share/generate_forsee_events.py
+++ b/Generators/ForeseeGenerator/share/generate_forsee_events.py
@@ -39,7 +39,9 @@ class ForeseeGenerator(object):
 
         # Set detector ...
         self.foresee = Foresee()
-        self.foresee.set_detector(selection="np.sqrt(x.x**2 + x.y**2)< 0.1", channels=[self.mode], distance=480, length=1.5 , luminosity=150)        
+        self.foresee.set_detector(selection="np.sqrt(x.x**2 + x.y**2)< 0.1",
+                                  channels=[self.mode], distance=480, length=1.5 ,
+                                  luminosity=1/1000.) # 1 pb-1        
 
         # Set model ...
         self.model = Model(self.modelname)
@@ -72,7 +74,6 @@ class ForeseeGenerator(object):
             energy = self.energy,
             nsample = 10)
 
-        # This is a bit handwavey according to Felix
         self.model.add_production_mixing(
             pid = "113",
             mixing = "coupling * 0.3/5. * 0.77545**2/abs(mass**2-0.77545**2+0.77545*0.147*1j)",
@@ -80,7 +81,6 @@ class ForeseeGenerator(object):
             energy = self.energy,
             )
 
-        # Question on validity as FASER gets larger but OK for currrent size according to Felix
         self.model.add_production_direct(
             label = "Brem",
             energy = self.energy,
@@ -153,15 +153,17 @@ class ForeseeGenerator(object):
             )
 
         # Get LLP spectrum
-        self.foresee.set_model(model=self.model)        
-        plt = self.foresee.get_llp_spectrum(self.mass, coupling=1, do_plot=True)  # This is just a reference coupling 
+        self.foresee.set_model(model=self.model)
+        # This is just a reference coupling 
+        plt = self.foresee.get_llp_spectrum(self.mass, coupling=1, do_plot=True)  
         plt.savefig(f"{self.modelname}_m{self.mass}.png")
 
         def flatten(l):
             return [i for sublist in l for i in sublist]
 
         # Get list of events within detector
-        coups, ctaus, nsigs, energies, weights, thetas = self.foresee.get_events(mass=self.mass, energy=self.energy, couplings=self.couplings)        
+        output = self.foresee.get_events(mass=self.mass, energy=self.energy, couplings=self.couplings)        
+        coups, ctaus, nsigs, energies, weights, thetas = output
 
         self.plot(flatten(thetas), flatten(energies), flatten(weights))
 
diff --git a/Generators/ForeseeGenerator/share/plot_validation.py b/Generators/ForeseeGenerator/share/plot_validation.py
index 5b3d8b56f2ec4dc60f308be26374191ca47bfc2e..8d5a04701c82deedadada27fc76d11fa4a6a8d30 100644
--- a/Generators/ForeseeGenerator/share/plot_validation.py
+++ b/Generators/ForeseeGenerator/share/plot_validation.py
@@ -14,8 +14,10 @@ def plot(f, name, xtitle, ytitle, xlo = None, xhi = None, ylo = None, yhi = None
 
     if ylo is not None and yhi is not None:
         h.SetAxisRange(ylo, yhi, "Y")
-        
-    if r != 1:
+
+    if isinstance(r, tuple):
+        h.Rebin2D(r[0], r[1])        
+    elif r != 1:
         h.Rebin(r)
 
     if xtitle is not None:
@@ -110,3 +112,11 @@ if __name__ == "__main__":
               ]                   
 
     plotn(f, config, 2, 2, "twod")
+
+    config = [Hist("Vtx_X", xtitle = "x [mm]", r = 5),
+              Hist("Vtx_Y", xtitle = "y [mm]", r = 5),
+              Hist("Vtx_Z", xtitle = "z [mm]", r = 5),
+              Hist("Vtx_XY", xtitle = "x [mm]", ytitle = "y [mm]", d = "colz", r = (5,5))
+              ]
+
+    plotn(f, config, 2, 2, "vtx")
diff --git a/Generators/ParticleGun/CMakeLists.txt b/Generators/ParticleGun/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1794305ed0291e77478aae72b1216db87fce0810
--- /dev/null
+++ b/Generators/ParticleGun/CMakeLists.txt
@@ -0,0 +1,14 @@
+################################################################################
+# Package: ParticleGun
+################################################################################
+
+# Declare the package name:
+atlas_subdir( ParticleGun )
+
+# Install files from the package:
+atlas_install_python_modules( python/*.py POST_BUILD_CMD ${ATLAS_FLAKE8} )
+
+# Install files from the package:
+atlas_install_joboptions( share/common/*.py
+                          share/example/*.py )
+
diff --git a/Generators/ParticleGun/README b/Generators/ParticleGun/README
new file mode 100644
index 0000000000000000000000000000000000000000..6cdb698eda4f549c076ac3a3ce732ad65225c11d
--- /dev/null
+++ b/Generators/ParticleGun/README
@@ -0,0 +1,5 @@
+ParticleGun documentation
+-------------------------
+See https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/ParticleGunForAtlas
+for some coherent documentation that should be kept up to date.
+
diff --git a/Generators/ParticleGun/python/__init__.py b/Generators/ParticleGun/python/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e88fc56c49679a210ad57b62055a6a7c0867ddd
--- /dev/null
+++ b/Generators/ParticleGun/python/__init__.py
@@ -0,0 +1,129 @@
+# 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 ParticleGun.samplers import ParticleSampler
+from ParticleGun.samplers import * # noqa: F401, F403 (import into our namespace)
+# commenting out the HepMC import for now
+#try:
+#          from AthenaPython.PyAthena import HepMC3  as HepMC
+#except ImportError:
+#          from AthenaPython.PyAthena import HepMC   as HepMC  
+
+from AthenaPython.PyAthena import StatusCode
+import ROOT,random
+
+__author__ = "Andy Buckley <andy.buckley@cern.ch>"
+
+class ParticleGun(EvgenAlg):
+    """
+    A simple but flexible algorithm for generating events from simple distributions.
+    """
+
+    def __init__(self, name="ParticleGun", randomSvcName="AtRndmGenSvc", randomStream="ParticleGun", randomSeed=None):
+        super(ParticleGun, self).__init__(name=name)
+        self.samplers = [ParticleSampler()]
+        self.randomStream = randomStream
+        self.randomSvcName = randomSvcName
+        self.randomSeed = randomSeed
+
+    @property
+    def sampler(self):
+        "Get the first (and presumed only) sampler"
+        return self.samplers[0] if self.samplers else None
+    @sampler.setter
+    def sampler(self, s):
+        "Set the samplers list to include only a single sampler, s"
+        self.samplers = [s]
+
+
+    def initialize(self):
+        """
+        Pass the AtRndmGenSvc seed to Python's random module, or use a fixed value set via pg.randomSeed.
+        """
+        seed = None
+        ## Use self.randomSeed directly, or if it's None find a seed string from the ATLAS random number service
+        if self.randomSeed is not None:
+            seed = self.randomSeed
+        else:
+            randomSvc = getattr(svcMgr, self.randomSvcName, None)
+            if randomSvc is not None:
+                for seedstr in randomSvc.Seeds:
+                    if seedstr.startswith(self.randomStream):
+                        seed = seedstr
+                        self.msg.info("ParticleGun: Using random seed '%s' ", seed)
+                        break
+                if seed is None:
+                    self.msg.warning("ParticleGun: Failed to find a seed for the random stream named '%s' ", self.randomStream)
+            else:
+                self.msg.warning("ParticleGun: Failed to find random number service called '%s' ", self.randomSvcName)
+        ## Apply the seed
+        if seed is not None:
+            random.seed(seed)
+            return StatusCode.Success
+        else:
+            self.msg.error("ParticleGun: randomSeed property not set, and no %s random number service found", self.randomSvcName)
+            return StatusCode.Failure
+
+
+    def fillEvent(self, evt):
+        """
+        Sample a list of particle properties, which are then used to create a new GenEvent in StoreGate.
+        """
+        ## Set event weight(s)
+        # TODO: allow weighted sampling?
+        try:
+          from AthenaPython.PyAthena import HepMC3  as HepMC
+        except ImportError:
+          from AthenaPython.PyAthena import HepMC   as HepMC   
+        evt.weights().push_back(1.0)
+
+        ## Make and fill particles
+        for s in self.samplers:
+            particles = s.shoot()
+            for p in particles:
+                ## Debug printout of particle properties
+                #print DEBUG0, p.pid, p.mom.E(), p.mom.Pt(), p.mom.M()
+                #print "DEBUG1 (px,py,pz,E) = (%0.2e, %0.2e, %0.2e, %0.2e)" % (p.mom.Px(), p.mom.Py(), p.mom.Pz(), p.mom.E())
+                #print "DEBUG2 (eta,phi,pt,m) = (%0.2e, %0.2e, %0.2e, %0.2e)" % (p.mom.Eta(), p.mom.Phi(), p.mom.Pt(), p.mom.M())
+                #print "DEBUG3 (x,y,z,t) = (%0.2e, %0.2e, %0.2e, %0.2e)" % (p.pos.X(), p.pos.Y(), p.pos.Z(), p.pos.T())
+
+                ## Make particle-creation vertex
+                # TODO: do something cleverer than one vertex per particle?
+                pos = HepMC.FourVector(p.pos.X(), p.pos.Y(), p.pos.Z(), p.pos.T())
+                gv = HepMC.GenVertex(pos)
+                ROOT.SetOwnership(gv, False)
+                evt.add_vertex(gv)
+
+                ## Make particle with status == 1
+                mom = HepMC.FourVector(p.mom.Px(), p.mom.Py(), p.mom.Pz(), p.mom.E())
+                gp = HepMC.GenParticle()
+                gp.set_status(1)
+                gp.set_pdg_id(p.pid)
+                gp.set_momentum(mom)
+                if p.mass is not None:
+                    gp.set_generated_mass(p.mass)
+                ROOT.SetOwnership(gp, False)
+                gv.add_particle_out(gp)
+
+        return StatusCode.Success
+
+
+## PyAthena HepMC notes
+#
+## evt.print() isn't valid syntax in Python2 due to reserved word
+# TODO: Add a Pythonisation, e.g. evt.py_print()?
+#getattr(evt, 'print')()
+#
+## How to check that the StoreGate key exists and is an McEventCollection
+# if self.sg.contains(McEventCollection, self.sgkey):
+#     print self.sgkey + " found!"
+#
+## Modifying an event other than that supplied as an arg
+# mcevts = self.sg[self.sgkey]
+# for vtx in mcevts[0].vertices: # only way to get the first vtx?!
+#     gp2 = HepMC.GenParticle()
+#     gp2.set_momentum(HepMC.FourVector(1,2,3,4))
+#     gp2.set_status(1)
+#     vtx.add_particle_out(gp2)
+#     break
diff --git a/Generators/ParticleGun/python/histsampling.py b/Generators/ParticleGun/python/histsampling.py
new file mode 100644
index 0000000000000000000000000000000000000000..c64112cc84e23963b63ea381817d1457f1c2a122
--- /dev/null
+++ b/Generators/ParticleGun/python/histsampling.py
@@ -0,0 +1,132 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+
+"""
+Tools for histogram sampling, in particular inverse transform sampling which is
+missing from ROOT's TH2 classes.
+"""
+
+__author__ = "Andy Buckley <andy.buckley@cern.ch>"
+
+import random, ROOT
+
+
+def load_hist(*args):
+    """
+    Load a histogram from a filename/TFile and histo name. If a single arg is
+    provided, it has to be a histo object and will be cloned before return.
+    """
+    h = None
+    if len(args) == 1 and issubclass(type(args[0]), ROOT.TH1):
+        h = args[0].Clone()
+    elif len(args) == 2:
+        if isinstance(args[0], str) and isinstance(args[1], str) :
+            f = ROOT.TFile.Open(args[0])
+            h = f.Get(args[1]).Clone()
+            #f.Close()
+        elif type(args[0]) is ROOT.TFile and type(args[1]) is str:
+            h = args[0].Get(args[1]).Clone()
+    if h is None:
+        raise Exception("Error in histogram loading from " + args)
+    return h
+
+
+def get_sampling_vars(h):
+    """
+    Get the following from a histogram h, since the ROOT API sucks:
+    * list of global bin IDs (not even contiguous for 2D, gee thanks ROOT)
+    * dict mapping global bin IDs to a tuple of axis bin IDs
+    * list of nbins+1 cumulative bin values, in the same order as globalbins
+    """
+    globalbin_to_axisbin = {} # for reverse axis bin lookup to get edges
+    globalbins = [] # because they aren't easily predicted, nor contiguous
+    cheights = [0] # cumulative "histogram" from which to uniformly sample
+    if issubclass(type(h), ROOT.TH1):
+        for ix in range(1, h.GetNbinsX()+1):
+            iglobal = h.GetBin(ix)
+            globalbins.append(iglobal)
+            globalbin_to_axisbin[iglobal] = (ix,)
+            cheights.append(cheights[-1] + h.GetBinContent(iglobal))
+    elif issubclass(type(h), ROOT.TH2):
+        for ix in range(1, h.GetNbinsX()+1):
+            for iy in range(1, h.GetNbinsY()+1):
+                iglobal = h.GetBin(ix, iy)
+                globalbins.append(iglobal)
+                globalbin_to_axisbin[iglobal] = (ix, iy)
+                cheights.append(cheights[-1] + h.GetBinContent(iglobal))
+    return globalbins, globalbin_to_axisbin, cheights
+
+
+def get_random_bin(globalbins, cheights):
+    """
+    Choose a random bin from the cumulative distribution list of nbins+1 entries.
+
+    TODO: Search more efficiently (lin and log guesses, then lin search or
+    binary split depending on vector size).
+    """
+    assert len(cheights) == len(globalbins)+1
+    randomheight = random.uniform(0, cheights[-1])
+    for i, iglobal in enumerate(globalbins):
+        if randomheight >= cheights[i] and randomheight < cheights[i+1]:
+            return iglobal
+    raise Exception("Sample fell outside range of cumulative distribution?!?!")
+
+
+def get_random_x(h, globalbins, cheights, globalbin_to_axisbin):
+    """
+    Choose a random bin via get_random_bin, then pick a uniform random x
+    point in that bin (without any attempt at estimating the in-bin distribution).
+    """
+    irand = get_random_bin(globalbins, cheights)
+    axisids = globalbin_to_axisbin.get(irand)
+    assert axisids is not None
+    xrand = random.uniform(h.GetXaxis().GetBinLowEdge(axisids[0]), h.GetXaxis().GetBinUpEdge(axisids[0]))
+    return xrand
+
+
+def get_random_xy(h2, globalbins, cheights, globalbin_to_axisbin):
+    """
+    Choose a random bin via get_random_bin, then pick a uniform random x,y
+    point in that bin (without any attempt at estimating the in-bin distribution).
+    """
+    irand = get_random_bin(globalbins, cheights)
+    axisids = globalbin_to_axisbin.get(irand)
+    assert axisids is not None
+    xrand = random.uniform(h2.GetXaxis().GetBinLowEdge(axisids[0]), h2.GetXaxis().GetBinUpEdge(axisids[0]))
+    yrand = random.uniform(h2.GetYaxis().GetBinLowEdge(axisids[1]), h2.GetYaxis().GetBinUpEdge(axisids[1]))
+    return xrand, yrand
+
+
+class TH1(object):
+    "Minimal wrapper for ROOT TH1, for sampling consistency and easy loading"
+
+    def __init__(self, *args):
+        self.th1 = load_hist(*args)
+        self.globalbins, self.globalbin_to_axisbin, self.cheights = None, None, None
+
+    def GetRandom(self):
+        "A GetRandom that works for TH1s and uses Python random numbers"
+        if self.globalbins is None or self.globalbin_to_axisbin is None or self.cheights is None:
+            self.globalbins, self.globalbin_to_axisbin, self.cheights = get_sampling_vars(self.th1)
+        return get_random_x(self.th1, self.globalbins, self.cheights, self.globalbin_to_axisbin)
+
+    def __getattr__(self, attr):
+        "Forward all attributes to the contained TH1"
+        return getattr(self.th1, attr)
+
+
+class TH2(object):
+    "Minimal wrapper for ROOT TH2, for easy loading and to allow 2D sampling"
+
+    def __init__(self, *args):
+        self.th2 = load_hist(*args)
+        self.globalbins, self.globalbin_to_axisbin, self.cheights = None, None, None
+
+    def GetRandom(self):
+        "A GetRandom that works for TH2s"
+        if self.globalbins is None or self.globalbin_to_axisbin is None or self.cheights is None:
+            self.globalbins, self.globalbin_to_axisbin, self.cheights = get_sampling_vars(self.th2)
+        return get_random_xy(self.th2, self.globalbins, self.cheights, self.globalbin_to_axisbin)
+
+    def __getattr__(self, attr):
+        "Forward other attributes to the contained TH2"
+        return getattr(self.th2, attr)
diff --git a/Generators/ParticleGun/python/samplers.py b/Generators/ParticleGun/python/samplers.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a232f7d7120136f29f07b493b3dd64c0137d0d3
--- /dev/null
+++ b/Generators/ParticleGun/python/samplers.py
@@ -0,0 +1,909 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+
+import ROOT, math, random
+from ParticleGun.histsampling import TH1
+
+## For convenience
+PI = math.pi
+TWOPI = 2*math.pi
+
+
+class Sampler(object):
+    "Base class for all samplers"
+
+    def shoot(self):
+        return RuntimeError("Can't sample from an abstract sampler object.")
+
+    def __call__(self):
+        """This is the call method that will actually be used (so that normal
+        functions can also be passed in as samplers)."""
+        return self.shoot()
+
+    # TODO: add a sampling weight?
+
+
+class ConstSampler(Sampler):
+    "A special-case sampler which just returns one value rather than sampling."
+
+    def __init__(self, val):
+        self.val = val
+
+    def shoot(self):
+        return self.val
+
+    def __repr__(self):
+        return "ConstSampler[%s]" % str(self.val)
+
+
+## Continuous distribution samplers
+
+class ContinuousSampler(Sampler):
+    "Base class for samplers from continuous distributions."
+    pass
+
+
+class UniformSampler(ContinuousSampler):
+    "Uniformly sample in the range [low,high)."
+
+    def __init__(self, low, high):
+        assert(low <= high)
+        self.low = float(low)
+        self.high = float(high)
+
+    def shoot(self):
+        return random.uniform(self.low, self.high)
+
+
+class ModUniformSampler(ContinuousSampler):
+    "Uniformly sample in the modulus range (-high,low]+[low,high)."
+
+    def __init__(self, low, high):
+        assert(low == abs(low) and high == abs(high))
+        assert(low <= high)
+        self.low = float(low)
+        self.high = float(high)
+
+    def shoot(self):
+        val = random.uniform(self.low, self.high)
+        if random.random() > 0.5:
+            val *= -1
+        return val
+
+
+class DisjointUniformSampler(ContinuousSampler):
+    "Uniformly sample from a set of disjoint intervals."
+
+    def __init__(self, ranges):
+        """
+        The ranges variable can either be a list of increasing numbers or a
+        list of pairs of numbers.
+
+        The former case will be treated as
+        defining alternating on/off ranges for sampling, starting with an active
+        one (i.e. it's a list of bin edges). The latter way specifically lists
+        the 'on' regions only, with their start and end values in the pairs.
+
+        The behaviour is undefined if the numbers are not ordered or overlap --
+        i.e. it might work but hasn't been designed that way and might change in
+        future. Don't rely on this behaviour!
+        """
+        if not ranges:
+            raise Exception("You must supply at least one non-null sampling range")
+        if hasattr(ranges[0], "__len__"):
+            assert all(len(x) == 2 for x in ranges)
+            self.ranges = ranges
+        else:
+            assert len(ranges) > 1
+            lows = [x for x in ranges[:-1]]
+            highs = [x for x in ranges[1:]]
+            myranges = []
+            for i, pair in enumerate(zip(lows, highs)):
+                if i % 2 == 0:
+                    myranges.append(pair)
+            assert len(myranges) == len(ranges) // 2
+            self.ranges = myranges
+
+    def _getRanges(self):
+        return self._ranges
+
+    def _setRanges(self, ranges):
+        # TODO: Check that ranges don't overlap
+        self._ranges = ranges
+        self._totalwidth = sum(r[1] - r[0] for r in ranges)
+
+        runningwidth = 0.0
+        self._divisions = [0.0]
+        for r in ranges:
+            assert(r[1] >= r[0])
+            runningwidth += float(r[1] - r[0])
+            self._divisions.append(runningwidth)
+        self._totalwidth = runningwidth
+        for i in range(len(self._divisions)):
+            self._divisions[i] = float(self._divisions[i]) / float(self._totalwidth)
+
+    ranges = property(_getRanges, _setRanges)
+
+    def _map_unit_to_val(self, x):
+        assert(x >= 0 and x <= 1)
+        idx = None
+        rem = None
+        for i in range(len(self._divisions) - 1):
+            if x >= self._divisions[i] and x < self._divisions[i+1]:
+                idx = i
+                rem = x - self._divisions[i]
+                break
+        if idx is None:
+            raise ValueError("No matching division found in unit interval! How?")
+        val = self.ranges[idx][0] + self._totalwidth * rem
+        return val
+
+    def shoot(self):
+        rand = random.random()
+        val = self._map_unit_to_val(rand)
+        return val
+
+
+class LogSampler(ContinuousSampler):
+    "Randomly sample from an exponential distribution (i.e. uniformly on a log scale)."
+
+    def __init__(self, low, high):
+        self.low = float(low)
+        self.high = float(high)
+
+    def shoot(self):
+        rand = random.random()
+        logval = rand * math.log(self.high) + (1 - rand) * math.log(self.low)
+        val = math.exp(logval)
+        return val
+
+
+class GaussianSampler(ContinuousSampler):
+    "Randomly sample from a 1D Gaussian distribution."
+
+    def __init__(self, mean, sigma):
+        self.mean = float(mean)
+        self.sigma = float(sigma)
+
+    def shoot(self):
+        return random.gauss(self.mean, self.sigma)
+
+
+class InvSampler(ContinuousSampler):
+    "Randomly sample from a 1/x distribution."
+
+    def __init__(self, low, high):
+        self.low = float(low)
+        self.high = float(high)
+
+    def shoot(self):
+        invx = random.uniform(1/self.high, 1/self.low) #< limit inversion not actually necessary
+        return 1./invx
+
+
+########################################
+
+
+class TH1Sampler(ContinuousSampler):
+    "Randomly sample from a 1D ROOT histogram."
+
+    def __init__(self, *args):
+        self.hist = TH1(*args)
+        if self.hist.GetEntries() < 1:
+            raise Exception("Histogram %s is EMPTY! Cannot sample" % self.hist.GetName())
+
+    def shoot(self):
+        return self.hist.GetRandom()
+
+
+########################################
+
+
+## Discrete sequence samplers
+
+class DiscreteSampler(Sampler):
+    "Base class for samplers from lists of discrete values"
+    pass
+
+
+class RandomSeqSampler(DiscreteSampler):
+    "Uniformly random sample from a list of values."
+
+    def __init__(self, *args):
+        if len(args) == 1:
+            self.sequence = args[0]
+        else:
+            self.sequence = args
+
+    def shoot(self):
+        return random.choice(self.sequence)
+# Alias:
+RndmSeq = RandomSeqSampler
+
+
+class CyclicSeqSampler(DiscreteSampler):
+    "Sequentially sample from a list of values, returning to the beginning once exhausted."
+
+    def __init__(self, *args):
+        if len(args) == 1:
+            self.sequence = args[0]
+        else:
+            self.sequence = args
+        self.index = 0
+
+    def shoot(self):
+        self.index = (self.index + 1) % len(self.sequence)
+        return self.sequence[self.index]
+## Alias:
+Sequence = CyclicSeqSampler
+
+
+########################################
+
+
+## Convenience function for sampler-making from Python literals
+
+def mksampler(x):
+    """
+    Automatically cast the provided object to a sampler type. This is used
+    extensively inside the particle and position samplers, so that the user
+    can pass in a primitive type like a number or list and it will be
+    treated as if the more verbose sampler constructors had been called.
+
+    Behaviour:
+     - if x can be called, i.e. x() is valid, we just return x;
+     - a Python list (square brackets) will be converted to a continuous
+       UniformSampler or DisjointUniformSampler;
+     - a Python tuple (round brackets/parentheses) will be treated
+       as a discrete CyclicSeqSampler;
+     - a Python set (curly brackets/braces) will be treated
+       as a discrete RandomSeqSampler;
+     - otherwise a ConstSampler will be created from x, so that x is
+       returned when the sampler is called.
+    """
+    if hasattr(x, "__call__"):
+        return x
+    elif type(x) is list:
+        # NB: disjoint ranges can be given as nested lists, e.g. [(1,2), (4,5)]
+        if len(x) == 2 and type(x[0]) in (int,float) and type(x[1]) in (int,float):
+            #print "MKSAMPLER: Casting %s to UniformSampler" % str(x)
+            return UniformSampler(*x)
+        elif len(x) > 2 or (len(x) > 0 and type(x[0]) not in (int,float)):
+            #print "MKSAMPLER: Casting %s to DisjointUniformSampler" % str(x)
+            return DisjointUniformSampler(x)
+        if len(x) < 2:
+            raise Exception("Supplied list could not be converted to a continuous sampler")
+    elif type(x) is tuple:
+        #print "MKSAMPLER: Casting %s to CyclicSeqSampler" % str(x)
+        return CyclicSeqSampler(*x)
+    elif type(x) is set:
+        #print "MKSAMPLER: Casting %s to RandomSeqSampler" % str(x)
+        return RandomSeqSampler(*x)
+    else:
+        #print "MKSAMPLER: Casting %s to ConstSampler" % str(x)
+        return ConstSampler(x)
+
+
+########################################
+
+
+## Beam-spot (origin vertex) sampling
+
+class PosSampler(Sampler):
+    """
+    Sampler of position 3-vectors, for modelling a beamspot.
+    """
+
+    def __init__(self, x, y, z, t=0):
+        self.x = x
+        self.y = y
+        self.z = z
+        self.t = t
+
+    @property
+    def x(self):
+        "x position sampler"
+        return self._x
+    @x.setter
+    def x(self, x):
+        self._x = mksampler(x)
+
+    @property
+    def y(self):
+        "y position sampler"
+        return self._y
+    @y.setter
+    def y(self, y):
+        self._y = mksampler(y)
+
+    @property
+    def z(self):
+        "z position sampler"
+        return self._z
+    @z.setter
+    def z(self, z):
+        self._z = mksampler(z)
+
+    @property
+    def t(self):
+        "Time sampler"
+        return self._t
+    @t.setter
+    def t(self, t):
+        self._t = mksampler(t)
+
+    def shoot(self):
+        x = self.x()
+        y = self.y()
+        z = self.z()
+        t = self.t()
+        #print "POS =", x, y, z, t
+        return ROOT.TLorentzVector(x, y, z, t)
+
+
+# TODO: Make a 3-Gaussian BeamspotSampler
+
+
+## Momentum sampling
+
+class MomSampler(Sampler):
+    """
+    Base class for four-momentum sampling.
+
+    There are many ways to unambiguously specify four-momenta. Not all are sensible/useful,
+    though. The following are implemented here:
+     * M,px,py,pz
+     * E,M,phi,eta
+     * E,M,phi,y
+     * E,M,phi,theta
+     * pT,M,phi,eta
+     * pT,M,phi,y
+     * pT,M,phi,theta
+
+    Possibly the following (not yet implemented) could be useful: let us know if you
+    need one of these:
+     * E,px,py,pz
+     * pT,E,M,phi
+    """
+    pass
+
+
+class NullMomSampler(MomSampler):
+    "A momentum sampler which just returns a null vector with the given mass."
+
+    def __init__(self, mass=0.0):
+        self.mass = mass
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    def shoot(self):
+        v4 = ROOT.TLorentzVector(0, 0, 0, self.mass)
+        return v4
+
+
+class MXYZSampler(MomSampler):
+    "Create a 4-momentum vector from mass, px, py, pz distributions/samplers."
+
+    def __init__(self, px, py, pz, mass=0.0):
+        self.mass = mass
+        self.px = px
+        self.py = py
+        self.pz = pz
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def px(self):
+        "px sampler"
+        return self._px
+    @px.setter
+    def px(self, x):
+        self._px = mksampler(x)
+
+    @property
+    def py(self):
+        "py sampler"
+        return self._py
+    @py.setter
+    def py(self, x):
+        self._py = mksampler(x)
+
+    @property
+    def pz(self):
+        "pz sampler"
+        return self._pz
+    @pz.setter
+    def pz(self, x):
+        self._pz = mksampler(x)
+
+    def shoot(self):
+        m = self.mass()
+        px = self.px()
+        py = self.py()
+        pz = self.pz()
+        e = math.sqrt(px**2 + py**2 + pz**2 + m**2)
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+class EEtaMPhiSampler(MomSampler):
+    "Create a 4-momentum vector from E, eta, m and phi distributions/samplers."
+
+    # TODO: ensure that E >= m!
+
+    def __init__(self, energy, eta, mass=0.0, phi=[0, TWOPI]):
+        self.energy = energy
+        self.eta = eta
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def energy(self):
+        "Energy sampler"
+        return self._e
+    @energy.setter
+    def energy(self, x):
+        self._e = mksampler(x)
+
+    @property
+    def eta(self):
+        "Pseudorapidity sampler"
+        return self._eta
+    @eta.setter
+    def eta(self, x):
+        self._eta = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        """
+        eta = - ln(tan(theta/2)) / 2
+        => theta = 2 atan( exp(-eta) )
+        """
+        eta = self.eta()
+        theta = 2 * math.atan(math.exp(-eta))
+        e = self.energy()
+        m = self.mass()
+        p = math.sqrt( e**2 - m**2 )
+        pz = p * math.cos(theta)
+        pt = p * math.sin(theta)
+        phi = self.phi()
+        px = pt * math.cos(phi)
+        py = pt * math.sin(phi)
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+class ERapMPhiSampler(MomSampler):
+    "Create a 4-momentum vector from E, y, m and phi distributions."
+
+    # TODO: ensure that E >= m!
+
+    def __init__(self, energy, rap, mass=0.0, phi=[0, TWOPI]):
+        self.energy = energy
+        self.rap = rap
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def energy(self):
+        "Energy sampler"
+        return self._e
+    @energy.setter
+    def energy(self, x):
+        self._e = mksampler(x)
+
+    @property
+    def rap(self):
+        "Rapidity sampler"
+        return self._rap
+    @rap.setter
+    def rap(self, x):
+        self._rap = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        """
+        y = 0.5 * ln((E+pz)/(E-pz))
+        -> (E^2 - pz^2) exp(2y) = (E+pz)^2
+         & (E^2 - pz^2) exp(-2y) = (E-pz)^2
+        -> E = sqrt(pt^2 + m^2) cosh(y)
+        -> pz = sqrt(pt^2 + m^2) sinh(y)
+        -> sqrt(pt^2 + m^2) = E / cosh(y)
+        """
+        e = self.energy()
+        y = self.rap()
+        sqrt_pt2_m2 = e / math.cosh(y)
+        pz = sqrt_pt2_m2 * math.sinh(y)
+        m = self.mass()
+        pt = math.sqrt( sqrt_pt2_m2**2 - m**2 )
+        phi = self.phi()
+        px = pt * math.cos(phi)
+        py = pt * math.sin(phi)
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+class EThetaMPhiSampler(MomSampler):
+    "Create a 4-momentum vector from E, theta, m and phi distributions/samplers."
+
+    # TODO: ensure that E >= m!
+
+    def __init__(self, energy, theta, mass=0.0, phi=[0, TWOPI]):
+        self.energy = energy
+        self.theta = theta
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def energy(self):
+        "Energy sampler"
+        return self._e
+    @energy.setter
+    def energy(self, x):
+        self._e = mksampler(x)
+
+    @property
+    def theta(self):
+        "Polar angle sampler"
+        return self._theta
+    @theta.setter
+    def theta(self, x):
+        self._theta = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler" 
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        """
+        p = sqrt(e^2 - m^2)
+        pz = p cos(theta)
+        pt = p sin(theta)
+        """
+        e = self.energy()
+        m = self.mass()
+        p = math.sqrt( e**2 - m**2 )
+        theta = self.theta()
+        pz = p * math.cos(theta)
+        pt = p * math.sin(theta)
+        phi = self.phi()
+        px = pt * math.cos(phi)
+        py = pt * math.sin(phi)
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+class PtEtaMPhiSampler(MomSampler):
+    "Create a 4-momentum vector from pt, eta, m and phi distributions/samplers."
+
+    def __init__(self, pt, eta, mass=0.0, phi=[0, TWOPI]):
+        self.pt = pt
+        self.eta = eta
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def pt(self):
+        "Transverse momentum sampler"
+        return self._pt
+    @pt.setter
+    def pt(self, x):
+        self._pt = mksampler(x)
+
+    @property
+    def eta(self):
+        "Pseudorapidity sampler"
+        return self._eta
+    @eta.setter
+    def eta(self, x):
+        self._eta = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        """
+        eta = - ln(tan(theta/2)) / 2
+        => theta = 2 atan( exp(-eta) )
+        """
+        eta = self.eta()
+        theta = 2 * math.atan(math.exp(-eta))
+        pt = self.pt()
+        p = pt / math.sin(theta)
+        phi = self.phi()
+        px = pt * math.cos(phi)
+        py = pt * math.sin(phi)
+        pz = p * math.cos(theta)
+        m = self.mass()
+        e = math.sqrt( p**2 + m**2 )
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+class PtRapMPhiSampler(MomSampler):
+    "Create a 4-momentum vector from pt, y, m and phi distributions/samplers."
+
+    def __init__(self, pt, rap, mass=0.0, phi=[0, TWOPI]):
+        self.pt = pt
+        self.rap = rap
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def pt(self):
+        "Transverse momentum sampler"
+        return self._pt
+    @pt.setter
+    def pt(self, x):
+        self._pt = mksampler(x)
+
+    @property
+    def rap(self):
+        "Rapidity sampler"
+        return self._rap
+    @rap.setter
+    def rap(self, x):
+        self._rap = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        """
+        y = 0.5 * ln((E+pz)/(E-pz))
+        -> (E^2 - pz^2) exp(2y) = (E+pz)^2
+         & (E^2 - pz^2) exp(-2y) = (E-pz)^2
+        -> E = sqrt(pt^2 + m^2) cosh(y)
+        -> pz = sqrt(pt^2 + m^2) sinh(y)
+        -> sqrt(pt^2 + m^2) = E / cosh(y)
+        """
+        pt = self.pt()
+        assert pt >= 0
+        m = self.mass()
+        assert m >= 0
+        sqrt_pt2_m2 = math.sqrt( pt**2 + m**2 )
+        y = self.rap()
+        e = sqrt_pt2_m2 * math.cosh(y)
+        pz = sqrt_pt2_m2 * math.sinh(y)
+        phi = self.phi()
+        px = pt * math.cos(phi)
+        py = pt * math.sin(phi)
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+class PtThetaMPhiSampler(MomSampler):
+    "Create a 4-momentum vector from pt, theta, m and phi distributions/samplers."
+
+    def __init__(self, pt, theta, mass=0.0, phi=[0, TWOPI]):
+        self.pt = pt
+        self.theta = theta
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def pt(self):
+        "Transverse momentum sampler"
+        return self._pt
+    @pt.setter
+    def pt(self, x):
+        self._pt = mksampler(x)
+
+    @property
+    def theta(self):
+        "Polar angle sampler"
+        return self._theta
+    @theta.setter
+    def theta(self, x):
+        self._theta = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        """
+        p = pt / math.sin(theta)
+        pz = p cos(theta)
+        pt = p sin(theta)
+        E = sqrt(p^2 + m^2)
+        """
+        theta = self.theta()
+        pt = self.pt()
+        p = pt / math.sin(theta)
+        phi = self.phi()
+        px = pt * math.cos(phi)
+        py = pt * math.sin(phi)
+        pz = p * math.cos(theta)
+        m = self.mass()
+        e = math.sqrt( p**2 + m**2 )
+        v4 = ROOT.TLorentzVector(px, py, pz, e)
+        return v4
+
+
+# TODO: add the missing ways to specify/sample 4-momenta
+
+
+###########################################################
+
+
+## Combined samplers returning a particle configuration
+
+
+## A default dictionary of particle masses (in MeV)
+MASSES = { 22   :    0.0, # photon
+           11   :    0.5, # electron
+           12   :    0.0, # nu_e
+           13   :  105.7, # muon
+           14   :    0.0, # nu_mu
+           15   : 1777.8, # tau
+           16   :    0.0, # nu_tau
+           2212 :  938.0, # proton
+           2112 :  940.0, # neutron
+           111  :  135.0, # pi0
+           211  :  140.0, # pi+-
+           221  :  547.0, # eta
+           321  :  494.0, # K+-
+           311  :  598.0  # K0
+           }
+
+
+class SampledParticle(object):
+    """
+    A particle object for use as a return value from the particle samplers.
+    """
+    def __init__(self, pid=None, mom=ROOT.TLorentzVector(0,0,0,0), pos=ROOT.TLorentzVector(0,0,0,0)):
+        """
+        Constructor/initializer: PID is the (int) PDG particle ID code
+        of this particle, mom is its momentum 4-vector, and pos is
+        the vertex 4-position (both as ROOT.TLorentzVector, in MeV).
+        """
+        self.pid = pid
+        self.mom = mom
+        self.pos = pos
+        self.mass = None
+
+
+class ParticleSampler(Sampler):
+    """
+    A simple N-independent-particle sampler.
+    """
+
+    def __init__(self, pid=999,
+                 mom=NullMomSampler(),
+                 n=1,
+                 pos=PosSampler(0, 0, 0)):
+        self.pid = pid
+        self.mom = mom
+        self.n = n
+        self.pos = pos
+        self.massdict = MASSES
+        self.mass_override = True
+
+    @property
+    def pid(self):
+        "Particle ID code sampler"
+        return self._pid
+    @pid.setter
+    def pid(self, x):
+        self._pid = mksampler(x)
+
+    @property
+    def n(self):
+        "Particle number sampler"
+        return self._n
+    @n.setter
+    def n(self, x):
+        self._n = mksampler(x)
+
+    def shoot(self):
+        "Return a vector of sampled particles"
+        numparticles = self.n()
+        rtn = []
+        for i in range(numparticles):
+            ## Sample the particle ID and create a particle
+            pid = self.pid()
+            p = SampledParticle(pid)
+            ## Pass mass info to the v4 sampler and set same generated mass
+            if self.mass_override and abs(pid) in self.massdict:
+                m = self.massdict[abs(pid)]
+                self.mom.mass = m
+                p.mass = m
+            # TODO: Should the particle generated_mass be set from the sampler by default?
+            ## Sample momentum and vertex positions into the particle
+            p.mom = self.mom()
+            p.pos = self.pos()
+            ## Add particle to output list
+            rtn.append(p)
+        return rtn
diff --git a/Generators/ParticleGun/share/common/ParticleGun_Common.py b/Generators/ParticleGun/share/common/ParticleGun_Common.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fab6fb0b7c4093fe92bd0e34e57bc7093b0281a
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_Common.py
@@ -0,0 +1,4 @@
+## Common setup for ParticleGun
+import ParticleGun as PG
+genSeq += PG.ParticleGun()
+evgenConfig.generators += ["ParticleGun"]
diff --git a/Generators/ParticleGun/share/common/ParticleGun_EoverP_Config.py b/Generators/ParticleGun/share/common/ParticleGun_EoverP_Config.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b78a953f31c253a9ead42158bf4d8b0dab77ce0
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_EoverP_Config.py
@@ -0,0 +1,66 @@
+#! -*- python -*-
+evgenConfig.description = "Single particle gun for E/p event generation"
+evgenConfig.keywords = ["singleParticle",]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["zach.marshall@cern.ch"]
+
+import ParticleGun as PG
+import ROOT
+from ParticleGun.samplers import *
+class PEtaSampler(PG.MomSampler):
+    "Create a 4-momentum vector from pt, eta, m and phi distributions/samplers."
+
+    def __init__(self, momentum, eta, pid=211, phi=[0, math.pi*2.]):
+        self.momentum = momentum
+        self.eta = eta
+        pdg_table = ROOT.TDatabasePDG.Instance()
+        mass = pdg_table.GetParticle(pid).Mass()
+        self.mass = mass
+        self.phi = phi
+
+    @property
+    def momentum(self):
+        "Momentum sampler"
+        return self._momentum
+    @momentum.setter
+    def momentum(self, x):
+        self._momentum = mksampler(x)
+
+    @property
+    def eta(self):
+        "Pseudorapidity sampler"
+        return self._eta
+    @eta.setter
+    def eta(self, x):
+        self._eta = mksampler(x)
+
+    @property
+    def mass(self):
+        "Mass sampler"
+        return self._m
+    @mass.setter
+    def mass(self, x):
+        self._m = mksampler(x)
+
+    @property
+    def phi(self):
+        "Azimuthal angle sampler"
+        return self._phi
+    @phi.setter
+    def phi(self, x):
+        self._phi = mksampler(x)
+
+    def shoot(self):
+        v4 = ROOT.TLorentzVector()
+        pt = p / math.cosh(self.eta())
+        v4.SetPtEtaPhiM(pt, self.eta(), self.phi(), self.mass())
+        return v4
+
+a_particle = int(jofile.split('_')[-1].split('.py')[0].replace('m','-'))
+
+pg = PG.ParticleGun()
+pg.sampler.pid = int(a_particle) #PID
+pg.sampler.mom = PEtaSampler(momentum=(500,800,1000,1200,1500,2000,3000,4000,5000,6000,7000,8000,9000,10000,11000,13000,15000,17000,20000,\
+                                       25000,35000,50000,75000,100000,200000,350000,500000), eta=[-0.3,0.3], pid=int(a_particle))
+genSeq += pg
+
diff --git a/Generators/ParticleGun/share/common/ParticleGun_FastCalo_ChargeFlip_Config.py b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_ChargeFlip_Config.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5399a64019b935aa729244f88c4b5c0ebe5b35f
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_ChargeFlip_Config.py
@@ -0,0 +1,78 @@
+#! -*- python -*-
+evgenConfig.description = "Single particle gun for FastCaloSim event generation"
+evgenConfig.keywords = ["singleParticle",]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["david.sosa@cern.ch"]
+
+import ParticleGun as PG
+import ROOT
+
+class MyParticleSampler(PG.ParticleSampler):
+    def __init__(self,energy,eta,pid,shift_z=0):
+        self.pid = pid
+        self.shift_z = shift_z
+        pdg_table = ROOT.TDatabasePDG.Instance()
+        mass = pdg_table.GetParticle(self.pid()).Mass()
+        self.mom1 = PG.EEtaMPhiSampler(energy=energy,eta=eta,mass=mass)
+
+    def shoot(self):
+        pid = self.pid()
+        
+        shift_z = self.shift_z
+
+        mom = self.mom1.shoot()
+        pos_temp = mom.Vect().Unit()
+        
+        # Would it hit the barrel, or the endcap?
+        if abs(pos_temp.Z())/3550.<pos_temp.Perp()/1148.: # Hit the barrel!
+            pos_temp *= 1148./pos_temp.Perp()
+        else: # Hit the endcap!
+            pos_temp *= 3550./abs(pos_temp.Z())
+
+        # Shift position of vector in the Z direction
+        pos_temp_2 = ROOT.TVector3()
+        pos_temp_2.SetXYZ(pos_temp.X(), pos_temp.Y(), pos_temp.Z()+shift_z)
+        pos_temp_2 *= 1. / pos_temp_2.Mag(); # reduce magnitude of vector
+
+        # recalculate; Would it hit the barrel, or the endcap?
+        if abs(pos_temp_2.Z())/3550.<pos_temp_2.Perp()/1148.:
+            pos_temp_2 *= 1148./pos_temp_2.Perp()
+        else:
+            pos_temp_2 *= 3550./abs(pos_temp_2.Z())
+
+        pos = ROOT.TLorentzVector(pos_temp_2.X(),pos_temp_2.Y(),pos_temp_2.Z(), pos_temp_2.Mag())
+        
+        #print "pid ",pid
+        
+        return [ PG.SampledParticle( pid , mom , pos ) ]
+
+myE = float(jofile.split('_E')[1].split('_')[0])
+myZV = float(jofile.split('_')[-1].split('.py')[0].replace("m","-"))
+
+myPDGID = jofile.split('_pid')[1].split('_')[0].replace('n','-')
+myPDGID = int(float(myPDGID.replace('p','')))
+
+eta_li = []
+
+if "disj" in jofile:
+    myLowEta1  = 0.01*float(jofile.split('eta_')[1].split('_')[0].replace('m','-'))
+    myLowEta2  = 0.01*float(jofile.split('eta_')[1].split('_')[1].replace('m','-'))
+    myHighEta1 = 0.01*float(jofile.split('eta_')[1].split('_')[2].replace('m','-'))
+    myHighEta2 = 0.01*float(jofile.split('eta_')[1].split('_')[3].replace('m','-'))
+    eta_li.extend([myLowEta1,myLowEta2,myHighEta1,myHighEta2])
+
+else:
+    myLowEta  = 0.01*float(jofile.split('eta')[1].split('_')[0].replace('m','-'))
+    myHighEta = 0.01*float(jofile.split('eta')[1].split('_')[1].replace('m','-'))
+    eta_li.extend([myLowEta,myHighEta])
+
+
+print "================ SETTTINGS ================="
+print ("energy = ", myE)
+print ("eta = ", eta_li)
+print ("pid = ", myPDGID)
+print ("shift_z = ", myZV)
+print "============================================"
+
+genSeq += PG.ParticleGun()
+genSeq.ParticleGun.sampler = MyParticleSampler(energy=myE,eta=eta_li,pid=(myPDGID,myPDGID),shift_z=myZV) #unmixed
diff --git a/Generators/ParticleGun/share/common/ParticleGun_FastCalo_Config.py b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_Config.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b2e9a68bc5e1c1612bf2e294c58dcc472f700fb
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_Config.py
@@ -0,0 +1,101 @@
+#! -*- python -*-
+evgenConfig.description = "Single particle gun for FastCaloSim event generation"
+evgenConfig.keywords = ["singleParticle",]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["david.sosa@cern.ch"]
+
+import ParticleGun as PG
+import ROOT
+
+class MyParticleSampler(PG.ParticleSampler):
+    def __init__(self,energy,eta,pid,shift_z=0):
+        self.pid = pid
+        self.shift_z = shift_z
+        pdg_table = ROOT.TDatabasePDG.Instance()
+        mass = pdg_table.GetParticle(self.pid()).Mass()
+        self.mom1 = PG.EEtaMPhiSampler(energy=energy,eta=eta,mass=mass)
+
+    def shoot(self):
+        pid = self.pid()
+
+        shift_z = self.shift_z
+
+        mom = self.mom1.shoot()
+        pos_temp = mom.Vect().Unit()
+
+        # Define geometry
+        barrelR1 = 1148.0
+        barrelR2 = 120.0
+        barrelR3 = 41.0
+        endcapZ1 = 3550.0
+        endcapZ2 = 4587.0
+
+        # Would it hit the barrel, or the endcap?
+        tanTheta = pos_temp.Perp() / abs( pos_temp.Z() );
+        if   tanTheta > barrelR1 / endcapZ1:
+            pos_temp *= barrelR1 / pos_temp.Perp()
+        elif tanTheta > barrelR2 / endcapZ1:
+            pos_temp *= endcapZ1 / abs( pos_temp.Z() )
+        elif tanTheta > barrelR2 / endcapZ2:
+            pos_temp *= barrelR2 / pos_temp.Perp()
+        elif tanTheta > barrelR3 / endcapZ2:
+            pos_temp *= endcapZ2 / abs( pos_temp.Z() )
+        else:
+            pos_temp *= barrelR3 / pos_temp.Perp()
+
+        # Shift position of vector in the Z direction
+        pos_temp_2 = ROOT.TVector3()
+        pos_temp_2.SetXYZ(pos_temp.X(), pos_temp.Y(), pos_temp.Z()+shift_z)
+        pos_temp_2 *= 1. / pos_temp_2.Mag(); # reduce magnitude of vector
+
+        # recalculate; Would it hit the barrel, or the endcap?
+        tanTheta_2 = pos_temp_2.Perp() / abs( pos_temp_2.Z() );
+        if   tanTheta_2 > barrelR1 / endcapZ1:
+            pos_temp_2 *= barrelR1 / pos_temp_2.Perp()
+        elif tanTheta_2 > barrelR2 / endcapZ1:
+            pos_temp_2 *= endcapZ1 / abs( pos_temp_2.Z() )
+        elif tanTheta_2 > barrelR2 / endcapZ2:
+            pos_temp_2 *= barrelR2 / pos_temp_2.Perp()
+        elif tanTheta_2 > barrelR3 / endcapZ2:
+            pos_temp_2 *= endcapZ2 / abs( pos_temp_2.Z() )
+        else:
+            pos_temp_2 *= barrelR3 / pos_temp_2.Perp()
+
+        pos = ROOT.TLorentzVector(pos_temp_2.X(),pos_temp_2.Y(),pos_temp_2.Z(), pos_temp_2.Mag())
+
+        #print "pid ",pid
+
+        return [ PG.SampledParticle( pid , mom , pos ) ]
+
+myE = float(jofile.split('_E')[1].split('_')[0])
+myZV = float(jofile.split('_')[-1].split('.py')[0].replace("m","-"))
+myPDGID = int(float(jofile.split('_pid')[1].split('_')[0].replace('m','-')))
+
+eta_li = []
+
+if "disj" in jofile:
+    myLowEta1  = 0.01*float(jofile.split('eta_')[1].split('_')[0].replace('m','-'))
+    myLowEta2  = 0.01*float(jofile.split('eta_')[1].split('_')[1].replace('m','-'))
+    myHighEta1 = 0.01*float(jofile.split('eta_')[1].split('_')[2].replace('m','-'))
+    myHighEta2 = 0.01*float(jofile.split('eta_')[1].split('_')[3].replace('m','-'))
+    eta_li.extend([myLowEta1,myLowEta2,myHighEta1,myHighEta2])
+
+else:
+    myLowEta  = 0.01*float(jofile.split('eta')[1].split('_')[0].replace('m','-'))
+    myHighEta = 0.01*float(jofile.split('eta')[1].split('_')[1].replace('m','-'))
+    eta_li.extend([myLowEta,myHighEta])
+
+
+print "================ SETTTINGS ================="
+print ("energy = ", myE)
+print ("eta = ", eta_li)
+print ("pid = ", myPDGID)
+print ("shift_z = ", myZV)
+print "============================================"
+
+genSeq += PG.ParticleGun()
+if myPDGID != 22:
+    genSeq.ParticleGun.sampler = MyParticleSampler(energy=myE,eta=eta_li,pid=(-myPDGID,myPDGID),shift_z=myZV)
+else:
+    genSeq.ParticleGun.sampler = MyParticleSampler(energy=myE,eta=eta_li,pid=myPDGID,shift_z=myZV)
+
diff --git a/Generators/ParticleGun/share/common/ParticleGun_FastCalo_Config_Erange.py b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_Config_Erange.py
new file mode 100644
index 0000000000000000000000000000000000000000..75ebc0621e7ba4e6803ae25c20a6d7438fc45466
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_Config_Erange.py
@@ -0,0 +1,103 @@
+#! -*- python -*-
+evgenConfig.description = "Single particle gun for FastCaloSim event generation"
+evgenConfig.keywords = ["singleParticle",]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["david.sosa@cern.ch"]
+
+import ParticleGun as PG
+import ROOT
+
+class MyParticleSampler(PG.ParticleSampler):
+    def __init__(self,energy,eta,pid,shift_z=0):
+        self.pid = pid
+        self.shift_z = shift_z
+        pdg_table = ROOT.TDatabasePDG.Instance()
+        mass = pdg_table.GetParticle(self.pid()).Mass()
+        self.mom1 = PG.EEtaMPhiSampler(energy=energy,eta=eta,mass=mass)
+
+    def shoot(self):
+        pid = self.pid()
+
+        shift_z = self.shift_z
+
+        mom = self.mom1.shoot()
+        pos_temp = mom.Vect().Unit()
+
+        # Define geometry
+        barrelR1 = 1148.0
+        barrelR2 = 120.0
+        barrelR3 = 41.0
+        endcapZ1 = 3550.0
+        endcapZ2 = 4587.0
+
+        # Would it hit the barrel, or the endcap?
+        tanTheta = pos_temp.Perp() / abs( pos_temp.Z() );
+        if   tanTheta > barrelR1 / endcapZ1:
+            pos_temp *= barrelR1 / pos_temp.Perp()
+        elif tanTheta > barrelR2 / endcapZ1:
+            pos_temp *= endcapZ1 / abs( pos_temp.Z() )
+        elif tanTheta > barrelR2 / endcapZ2:
+            pos_temp *= barrelR2 / pos_temp.Perp()
+        elif tanTheta > barrelR3 / endcapZ2:
+            pos_temp *= endcapZ2 / abs( pos_temp.Z() )
+        else:
+            pos_temp *= barrelR3 / pos_temp.Perp()
+
+        # Shift position of vector in the Z direction
+        pos_temp_2 = ROOT.TVector3()
+        pos_temp_2.SetXYZ(pos_temp.X(), pos_temp.Y(), pos_temp.Z()+shift_z)
+        pos_temp_2 *= 1. / pos_temp_2.Mag(); # reduce magnitude of vector
+
+        # recalculate; Would it hit the barrel, or the endcap?
+        tanTheta_2 = pos_temp_2.Perp() / abs( pos_temp_2.Z() );
+        if   tanTheta_2 > barrelR1 / endcapZ1:
+            pos_temp_2 *= barrelR1 / pos_temp_2.Perp()
+        elif tanTheta_2 > barrelR2 / endcapZ1:
+            pos_temp_2 *= endcapZ1 / abs( pos_temp_2.Z() )
+        elif tanTheta_2 > barrelR2 / endcapZ2:
+            pos_temp_2 *= barrelR2 / pos_temp_2.Perp()
+        elif tanTheta_2 > barrelR3 / endcapZ2:
+            pos_temp_2 *= endcapZ2 / abs( pos_temp_2.Z() )
+        else:
+            pos_temp_2 *= barrelR3 / pos_temp_2.Perp()
+
+        pos = ROOT.TLorentzVector(pos_temp_2.X(),pos_temp_2.Y(),pos_temp_2.Z(), pos_temp_2.Mag())
+
+        #print "pid ",pid
+
+        return [ PG.SampledParticle( pid , mom , pos ) ]
+
+E_li = []
+myLowE = float(jofile.split('_E')[1].split('_')[0])
+myHighE = float(jofile.split('_E')[1].split('_')[1])
+E_li.extend([myLowE,myHighE])
+
+myZV = float(jofile.split('_')[-1].split('.py')[0].replace("m","-"))
+myPDGID = int(float(jofile.split('_pid')[1].split('_')[0].replace('m','-')))
+
+eta_li = []
+
+if "disj" in jofile:
+    myLowEta1  = 0.01*float(jofile.split('eta_')[1].split('_')[0].replace('m','-'))
+    myLowEta2  = 0.01*float(jofile.split('eta_')[1].split('_')[1].replace('m','-'))
+    myHighEta1 = 0.01*float(jofile.split('eta_')[1].split('_')[2].replace('m','-'))
+    myHighEta2 = 0.01*float(jofile.split('eta_')[1].split('_')[3].replace('m','-'))
+    eta_li.extend([myLowEta1,myLowEta2,myHighEta1,myHighEta2])
+
+else:
+    myLowEta  = 0.01*float(jofile.split('eta')[1].split('_')[0].replace('m','-'))
+    myHighEta = 0.01*float(jofile.split('eta')[1].split('_')[1].replace('m','-'))
+    eta_li.extend([myLowEta,myHighEta])
+
+
+print "================ SETTTINGS ================="
+print ("energy = ", E_li)
+print ("eta = ", eta_li)
+print ("pid = ", myPDGID)
+print ("shift_z = ", myZV)
+print "============================================"
+
+genSeq += PG.ParticleGun()
+print "E_li = ", E_li, ", eta_li = ", eta_li, ", pid = ", myPDGID, ", myZV = ", myZV
+genSeq.ParticleGun.sampler = MyParticleSampler(energy=E_li,eta=eta_li,pid=myPDGID,shift_z=myZV)
+
diff --git a/Generators/ParticleGun/share/common/ParticleGun_FastCalo_NoChargeFlip_Config.py b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_NoChargeFlip_Config.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ba60ef2bc9d3a9195ada72e9a2232864f173567
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_FastCalo_NoChargeFlip_Config.py
@@ -0,0 +1,78 @@
+#! -*- python -*-
+evgenConfig.description = "Single particle gun for FastCaloSim event generation"
+evgenConfig.keywords = ["singleParticle",]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["david.sosa@cern.ch"]
+
+import ParticleGun as PG
+import ROOT
+
+class MyParticleSampler(PG.ParticleSampler):
+    def __init__(self,energy,eta,pid,shift_z=0):
+        self.pid = pid
+        self.shift_z = shift_z
+        pdg_table = ROOT.TDatabasePDG.Instance()
+        mass = pdg_table.GetParticle(self.pid()).Mass()
+        self.mom1 = PG.EEtaMPhiSampler(energy=energy,eta=eta,mass=mass)
+
+    def shoot(self):
+        pid = self.pid()
+        
+        shift_z = self.shift_z
+
+        mom = self.mom1.shoot()
+        pos_temp = mom.Vect().Unit()
+        
+        # Would it hit the barrel, or the endcap?
+        if abs(pos_temp.Z())/3550.<pos_temp.Perp()/1148.: # Hit the barrel!
+            pos_temp *= 1148./pos_temp.Perp()
+        else: # Hit the endcap!
+            pos_temp *= 3550./abs(pos_temp.Z())
+
+        # Shift position of vector in the Z direction
+        pos_temp_2 = ROOT.TVector3()
+        pos_temp_2.SetXYZ(pos_temp.X(), pos_temp.Y(), pos_temp.Z()+shift_z)
+        pos_temp_2 *= 1. / pos_temp_2.Mag(); # reduce magnitude of vector
+
+        # recalculate; Would it hit the barrel, or the endcap?
+        if abs(pos_temp_2.Z())/3550.<pos_temp_2.Perp()/1148.:
+            pos_temp_2 *= 1148./pos_temp_2.Perp()
+        else:
+            pos_temp_2 *= 3550./abs(pos_temp_2.Z())
+
+        pos = ROOT.TLorentzVector(pos_temp_2.X(),pos_temp_2.Y(),pos_temp_2.Z(), pos_temp_2.Mag())
+        
+        #print "pid ",pid
+        
+        return [ PG.SampledParticle( pid , mom , pos ) ]
+
+myE = float(jofile.split('_E')[1].split('_')[0])
+myZV = float(jofile.split('_')[-1].split('.py')[0].replace("m","-"))
+
+myPDGID = jofile.split('_pid')[1].split('_')[0].replace('n','-')
+myPDGID = int(float(myPDGID.split('_pid')[1].split('_')[0].replace('p','')))
+
+eta_li = []
+
+if "disj" in jofile:
+    myLowEta1  = 0.01*float(jofile.split('eta_')[1].split('_')[0].replace('m','-'))
+    myLowEta2  = 0.01*float(jofile.split('eta_')[1].split('_')[1].replace('m','-'))
+    myHighEta1 = 0.01*float(jofile.split('eta_')[1].split('_')[2].replace('m','-'))
+    myHighEta2 = 0.01*float(jofile.split('eta_')[1].split('_')[3].replace('m','-'))
+    eta_li.extend([myLowEta1,myLowEta2,myHighEta1,myHighEta2])
+
+else:
+    myLowEta  = 0.01*float(jofile.split('eta')[1].split('_')[0].replace('m','-'))
+    myHighEta = 0.01*float(jofile.split('eta')[1].split('_')[1].replace('m','-'))
+    eta_li.extend([myLowEta,myHighEta])
+
+
+print "================ SETTTINGS ================="
+print ("energy = ", myE)
+print ("eta = ", eta_li)
+print ("pid = ", myPDGID)
+print ("shift_z = ", myZV)
+print "============================================"
+
+genSeq += PG.ParticleGun()
+genSeq.ParticleGun.sampler = MyParticleSampler(energy=myE,eta=eta_li,pid=(myPDGID,myPDGID),shift_z=myZV) #unmixed
diff --git a/Generators/ParticleGun/share/common/ParticleGun_SamplingFraction.py b/Generators/ParticleGun/share/common/ParticleGun_SamplingFraction.py
new file mode 100644
index 0000000000000000000000000000000000000000..54557f0d5d2afacedb09999acac22e02ad8576ac
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_SamplingFraction.py
@@ -0,0 +1,97 @@
+#! -*- python -*-
+evgenConfig.description = "Single particle gun for Sampling Fraction event generation"
+evgenConfig.keywords = ["singleParticle",]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["michael.duehrssen@cern.ch"]
+
+import ParticleGun as PG
+import ROOT, math, random
+
+class MyParticleSampler(PG.ParticleSampler):
+    """
+    Projective showers starting at entrance of calorimeter, flat in eta, constant energy
+    """
+
+    def __init__(self,pid=11,momentum=50000.,eta1=0.,eta2=1.4,bec=0,radius=1500.,z=3740.5):
+        self.pid = pid
+        self.momentum = momentum
+        self.eta1 = eta1
+        self.eta2 = eta2
+        pdg_table = ROOT.TDatabasePDG.Instance()
+        self.mass = pdg_table.GetParticle(self.pid()).Mass()
+        self.bec=bec
+        self.radius=radius
+        self.z=z
+
+    def shoot(self):
+       rtn=[]
+       eta = random.uniform(self.eta1, self.eta2) 
+       phi = random.uniform(0, math.tau)  # tau = 2 * pi
+       v4 = ROOT.TLorentzVector()
+       pt = self.momentum / math.cosh(eta)
+       v4.SetPtEtaPhiM(pt, eta, phi, self.mass)
+       if self.bec==0:
+           radius= self.radius
+           x=radius*math.cos(phi)
+           y=radius*math.sin(phi)
+           z=radius*math.sinh(eta)
+       else:
+           z=self.z
+           radius=z/math.sinh(eta)
+           x=radius*math.cos(phi)
+           y=radius*math.sin(phi)
+       t=math.sqrt(x*x+y*y+z*z)
+       vp = ROOT.TLorentzVector(x,y,z,t)
+       p = PG.SampledParticle(pid=self.pid(),mom=v4,pos=vp)
+       #print "E,eta,phi,mass ",e,eta,phi,self.mass,"  position ",x,y,z," pid=",p.pid
+       rtn.append(p)
+       return rtn
+
+##MC15 style with Generate_tf.py
+#args=jofile.split('.py')[0]
+
+##MC16 style with Gen_tf.py 
+FIRST_DIR = (os.environ['JOBOPTSEARCHPATH']).split(":")[0]
+jofiles = [f for f in os.listdir(FIRST_DIR) if (f.startswith('mc') and f.endswith('.py'))]
+
+print "================ SETTTINGS ================="
+print ("jofiles     = ", jofiles)
+
+### parse options from MC job-options filename
+args = jofiles[0].split('.py')[0]
+print ("args     = ", args)
+
+myMomentum = float(args.split('_Mom')[1].split('_')[0])
+print ("Momentum = ", myMomentum,"MeV")
+
+myPDGID = int(float(args.split('_pid')[1].split('_')[0].replace('m','-')))
+print ("pid      = ", myPDGID)
+
+myLowEta  = 0.01*float(args.split('eta_')[1].split('_')[0].replace('m','-'))
+print ("etalow   = ", myLowEta)
+
+myHighEta = 0.01*float(args.split('eta_')[1].split('_')[1].replace('m','-'))
+print ("etahigh   = ", myHighEta)
+
+if "_Radius" in args:
+  myRadius = 0.001*float(args.split('_Radius')[1].split('_')[0]) #Argument needs to by in mum, since a "." in the filename is not allowed
+else: 
+  myRadius = 1500.
+print ("radius   = ", myRadius,"mm")
+
+if "_Z" in args:
+  myZ = 0.001*float(args.split('_Z')[1].split('_')[0]) #Argument needs to by in mum, since a "." in the filename is not allowed
+else:  
+  myZ = 3740.5
+print ("Z        = ", myZ,"mm")
+  
+if "bec" in args:
+  bec=1
+else:
+  bec=0
+print ("bec      = ", bec)
+print "============================================"
+
+genSeq += PG.ParticleGun()
+genSeq.ParticleGun.sampler = MyParticleSampler(momentum=myMomentum,eta1=myLowEta,eta2=myHighEta,pid=myPDGID,bec=bec,radius=myRadius,z=myZ)
+
diff --git a/Generators/ParticleGun/share/common/ParticleGun_SingleHECO.py b/Generators/ParticleGun/share/common/ParticleGun_SingleHECO.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ffb36dd28235948f0ec1d298c121902267f27d1
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_SingleHECO.py
@@ -0,0 +1,74 @@
+
+PDG = 10000000 + int(float(charge)*100.0)
+loE = (float(mass) + 10.)*1000.
+hiE  = (float(mass) + 6000.)*1000.	
+MeVmass=float(mass)*1000.
+#--------------------------------------------------------------
+# Configuration for EvgenJobTransforms
+#--------------------------------------------------------------
+evgenConfig.description = "Single HECO generation for Mass=%s, Charge=%s in MC15" % (mass,charge)
+evgenConfig.keywords = ["exotic", "singleParticle","highElectricChargeObject"]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["anlionti@cern.ch"]
+
+evgenConfig.specialConfig = 'MASS=%s;CHARGE=%s;preInclude=SimulationJobOptions/preInclude.Qball.py' % (mass,charge)
+
+
+
+#--------------------------------------------------------------
+# Configuration for ParticleGun
+#--------------------------------------------------------------
+include("ParticleGun/ParticleGun_Common.py")
+
+import ParticleGun as PG
+PG.MASSES[PDG] = float(MeVmass)
+genSeq.ParticleGun.sampler.pid = (-PDG, PDG)
+genSeq.ParticleGun.sampler.mom = PG.EEtaMPhiSampler(energy=[loE,hiE], eta=[-2,2])
+
+
+#--------------------------------------------------------------
+# Edit PDGTABLE.MeV with monopole mass
+#--------------------------------------------------------------
+ALINE1="M %s                          %s.E+03       +0.0E+00 -0.0E+00 Monopole        0" % (PDG,mass)
+ALINE2="W %s                          0.E+00         +0.0E+00 -0.0E+00 Monopole        0" % (PDG)
+
+import os
+import sys
+
+pdgmod = os.path.isfile('PDGTABLE.MeV')
+if pdgmod is True:
+    os.remove('PDGTABLE.MeV')
+os.system('get_files -data PDGTABLE.MeV')
+f=open('PDGTABLE.MeV','a')
+f.writelines(str(ALINE1))
+f.writelines('\n')
+f.writelines(str(ALINE2))
+f.writelines('\n')
+f.close()
+
+del ALINE1
+del ALINE2
+
+#--------------------------------------------------------------
+# Edit G4particle_whitelist.txt with monopole
+#--------------------------------------------------------------
+
+ALINE1="%s   qb  %s.E+03 (Mev/c) lepton %s" % (PDG,mass,charge)
+ALINE2="-%s  qbbar  %s.E+03 (Mev/c) lepton -%s" % (PDG,mass,charge)
+
+import os
+import sys
+
+pdgmod = os.path.isfile('G4particle_whitelist.txt')
+if pdgmod is True:
+    os.remove('G4particle_whitelist.txt')
+os.system('get_files -data G4particle_whitelist.txt')
+f=open('G4particle_whitelist.txt','a')
+f.writelines(str(ALINE1))
+f.writelines('\n')
+f.writelines(str(ALINE2))
+f.writelines('\n')
+f.close()
+
+del ALINE1
+del ALINE2
diff --git a/Generators/ParticleGun/share/common/ParticleGun_SingleMonopole.py b/Generators/ParticleGun/share/common/ParticleGun_SingleMonopole.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a6bf0574561af0f4cab4a20e68248dcc3111bae
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_SingleMonopole.py
@@ -0,0 +1,74 @@
+
+PDG = 4110000
+loE = (float(monmass) + 10.)*1000.
+hiE  = (float(monmass) + 6000.)*1000.
+MeVmass=float(monmass)*1000.
+#--------------------------------------------------------------
+# Configuration for EvgenJobTransforms
+#--------------------------------------------------------------
+evgenConfig.description = "Single magnetic monopole generation for Mass=%s, Gcharge=%s in MC15" % (monmass,gcharge)
+evgenConfig.keywords = ["exotic", "magneticMonopole", "singleParticle"]
+evgenConfig.generators = ["ParticleGun"]
+evgenConfig.contact = ["anlionti@cern.ch"]
+
+evgenConfig.specialConfig = 'MASS=%s;GCHARGE=%s;preInclude=SimulationJobOptions/preInclude.Monopole.py' % (monmass,gcharge)
+
+
+
+#--------------------------------------------------------------
+# Configuration for ParticleGun
+#--------------------------------------------------------------
+include("ParticleGun/ParticleGun_Common.py")
+
+import ParticleGun as PG
+PG.MASSES[4110000] = float(MeVmass)
+genSeq.ParticleGun.sampler.pid = (-PDG, PDG)
+genSeq.ParticleGun.sampler.mom = PG.EEtaMPhiSampler(energy=[loE,hiE], eta=[-2,2])
+
+
+#--------------------------------------------------------------
+# Edit PDGTABLE.MeV with monopole mass
+#--------------------------------------------------------------
+ALINE1="M 4110000                          %s.E+03       +0.0E+00 -0.0E+00 Monopole        0" % (monmass)
+ALINE2="W 4110000                          0.E+00         +0.0E+00 -0.0E+00 Monopole        0"
+
+import os
+import sys
+
+pdgmod = os.path.isfile('PDGTABLE.MeV')
+if pdgmod is True:
+    os.remove('PDGTABLE.MeV')
+os.system('get_files -data PDGTABLE.MeV')
+f=open('PDGTABLE.MeV','a')
+f.writelines(str(ALINE1))
+f.writelines('\n')
+f.writelines(str(ALINE2))
+f.writelines('\n')
+f.close()
+
+del ALINE1
+del ALINE2
+
+#--------------------------------------------------------------
+# Edit G4particle_whitelist.txt with monopole
+#--------------------------------------------------------------
+
+ALINE1="4110000   mm  %s.E+03 (Mev/c) lepton %s" % (monmass,gcharge)
+ALINE2="-4110000  mmbar  %s.E+03 (Mev/c) lepton -%s" % (monmass,gcharge)
+
+import os
+import sys
+
+pdgmod = os.path.isfile('G4particle_whitelist.txt')
+if pdgmod is True:
+    os.remove('G4particle_whitelist.txt')
+os.system('get_files -data G4particle_whitelist.txt')
+f=open('G4particle_whitelist.txt','a')
+f.writelines(str(ALINE1))
+f.writelines('\n')
+f.writelines(str(ALINE2))
+f.writelines('\n')
+f.close()
+
+del ALINE1
+del ALINE2
diff --git a/Generators/ParticleGun/share/common/ParticleGun_egammaET.py b/Generators/ParticleGun/share/common/ParticleGun_egammaET.py
new file mode 100644
index 0000000000000000000000000000000000000000..04ea9b92a3e209afc279d743307dff393f1d8839
--- /dev/null
+++ b/Generators/ParticleGun/share/common/ParticleGun_egammaET.py
@@ -0,0 +1,51 @@
+__doc__ = "Holds a 4-momentum sampler according to the egamma Et spectrum"
+
+import ParticleGun as PG
+from GaudiKernel.SystemOfUnits import GeV
+
+def dbnFermiDirac(x,mu,kT):
+   import math
+   arg = (x-mu)/kT
+   if arg < -20 :	# avoid numerical underflows
+      result = 1
+   elif arg > 20 :	# avoid numerical overflows
+      result = 0
+   else :
+      div = math.exp(arg)+1
+      result = 1/div
+   return result
+
+class egammaETSampler(PG.PtEtaMPhiSampler):
+  "4-momentum sampler according to the egamma Et spectrum."
+  def __init__(self, pid, eta=[-2.5, 2.5], phi=[0, PG.TWOPI],
+    mu1 = 0.5, kT1 = 0.1, mu2 = 200, kT2 = 20, y0 = 0.005, PtMin = 0 , PtMax = 3e3, nBins=None):
+    """ 
+    Parameters for the MVA-shaped spectrum : higher density in the < 100 GeV range
+    PtMin = 0 # minimum Pt
+    PtMax = 3000 # maximum Pt (3 TeV)
+    nBins # number of bins (one every 100 MeV by default)
+    mu1 = 0.5		# mu1,kT1 : smooth but steep ramp-up from 0 to 1 GeV (requested by TauCP)
+    kT1 = 0.1
+    mu2 = 200		# mu2,kT2 : smooth, slow ramp-down in the 100-300 GeV range
+    kT2 = 20
+    y0 = 0.005		# y0 : baseline for low-density at high ET up to PtMax
+    """
+    self.m = PG.MASSES[abs(pid)]
+    self.eta = eta
+    self.phi = phi
+    
+    # Create and fill a very fine-grained histogram 
+    from ROOT import TH1D
+    etSpectrumFullRange = TH1D("ETSpectrumFullRange", 
+                               "Reference ET spectrum for egamma MVA calib",
+                               int(nBins or (PtMax - PtMin)*10), PtMin , PtMax)
+    for i in xrange(etSpectrumFullRange.GetNbinsX()):
+         x = etSpectrumFullRange.GetBinCenter(i+1)
+         y1 = dbnFermiDirac(x,mu1,kT1)
+         y2 = dbnFermiDirac(x,mu2,kT2)
+         y = y0 - y1 + y2
+         etSpectrumFullRange.SetBinContent(i+1,y)
+    self.hist = PG.TH1(etSpectrumFullRange) #< wrap *after* populating
+  
+  def pt(self):
+    return self.hist.GetRandom() * GeV
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_constenergy_flateta.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_constenergy_flateta.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c3cb0321b37e7f3d5ec3fcfc2602b2f1cba2cb0
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_constenergy_flateta.py
@@ -0,0 +1,17 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+pg = PG.ParticleGun()
+pg.randomSeed = 123456
+pg.sampler.pid = {11,-11,211,111}
+pg.sampler.mom = PG.EEtaMPhiSampler(energy=10000, eta=[-2,2])
+topSeq += pg
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_correlated.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_correlated.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb0a9437388d3b21fffd744c67cfe590dbddaf4e
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_correlated.py
@@ -0,0 +1,34 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+
+class MyParticleSampler(PG.ParticleSampler):
+    "A special sampler with two _correlated_ particles."
+
+    def __init__(self):
+        self.mom1 = PG.PtEtaMPhiSampler(pt=25000, eta=[-2,2])
+
+    def shoot(self):
+        "Return a vector of sampled particles"
+        p1 = PG.SampledParticle(11, self.mom1.shoot())
+        eta1 = p1.mom.Eta()
+        phi1 = p1.mom.Phi()
+        # TODO: will phi be properly wrapped into range?
+        mom2 = PG.PtEtaMPhiSampler(pt=25000,
+                                   eta=[eta1-0.5, eta1+0.5],
+                                   phi=[phi1-0.5, phi1+0.5])
+        p2 = PG.SampledParticle(11, mom2.shoot())
+        return [p1, p2]
+
+topSeq += PG.ParticleGun()
+topSeq.ParticleGun.randomSeed = 123456
+topSeq.ParticleGun.sampler = MyParticleSampler()
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_corrhist.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_corrhist.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c0cd41b88e35f0290b137e5eebe80296a90b3ca
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_corrhist.py
@@ -0,0 +1,41 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+## ROOT 2D histogram sampling alg (in ParticleGun.histsampling) by Andy Buckley
+## Thanks to Alejandro Alonso for the initial Athena example on which this is based.
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+
+class PtEtaHistParticleSampler(PG.ParticleSampler):
+    "Particle sampler with correlated pT and eta from a 2D histogram."
+
+    def __init__(self, pid, histfile, num=100):
+        self.pid = PG.mksampler(pid)
+        self.hist = PG.TH2(histfile, "h_pt_eta")
+        self.numparticles = num
+
+    def shoot(self):
+        "Return a vector of sampled particles from the provided pT--eta histogram"
+        particles = []
+        for i in xrange(self.numparticles):
+            ptrand, etarand = self.hist.GetRandom()
+            ptrand *= 1000 # NB. This _particular_ histogram is in GeV, but Athena needs MeV!
+            # TODO: Provide 4-mom construction functions to avoid building this one-time sampler
+            pid = self.pid()
+            mom = PG.PtEtaMPhiSampler(pt=ptrand, eta=etarand, mass=PG.MASSES[abs(pid)])
+            p = PG.SampledParticle(pid, mom())
+            #print p.mom.Pt(), "\t", p.mom.Eta(), "\t", p.mom.Phi(), "\t", p.mom.M()
+            particles.append(p)
+        return particles
+
+topSeq += PG.ParticleGun()
+topSeq.ParticleGun.randomSeed = 123456
+topSeq.ParticleGun.sampler = PtEtaHistParticleSampler(11, "data_histos_el_1470pt.root")
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_flatcurvature_flatip.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_flatcurvature_flatip.py
new file mode 100644
index 0000000000000000000000000000000000000000..38233563de9673b1d9ebd1ae434b57f6051ae70e
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_flatcurvature_flatip.py
@@ -0,0 +1,41 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+
+class MyParticleSampler(PG.ParticleSampler):
+    """
+    A special sampler to generate single particles flat in 1/pT and in
+    impact parameter to the beam, with flat z0.
+    """
+
+    def __init__(self):
+        psamp = PG.PtEtaMPhiSampler(pt=PG.InvSampler(4000, 400000), eta=[0.1,0.3], phi=[0.3, 0.5])
+        xsamp = PG.PosSampler(0, 0, [-150,150], 0)
+        PG.ParticleSampler.__init__(self, pid={13,-13}, mom=psamp, pos=xsamp)
+        self.ip = PG.mksampler([-2,2])
+
+    def shoot(self):
+        "Return a vector of sampled particles"
+        ps = PG.ParticleSampler.shoot(self)
+        assert len(ps) == 1
+        p = ps[0]
+        from math import sqrt
+        m = -p.mom.X() / p.mom.Y() #< gradient of azimuthal IP sampling line, perp to mom
+        x = self.ip() / sqrt(1 + m**2) #< just decomposing sampled IP into x component...
+        y = m*x #< ... and y-component
+        p.pos.SetX(x)
+        p.pos.SetY(m*x)
+        return [p]
+
+topSeq += PG.ParticleGun()
+topSeq.ParticleGun.randomSeed = 123456
+topSeq.ParticleGun.sampler = MyParticleSampler()
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_flatpt_2particle.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_flatpt_2particle.py
new file mode 100644
index 0000000000000000000000000000000000000000..97ed64f8857e82dbcaeb06085fc84d84eb246c0b
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_flatpt_2particle.py
@@ -0,0 +1,20 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+pg = PG.ParticleGun()
+pg.randomSeed = 123456
+pg.samplers.append(PG.ParticleSampler()) # add a second sampler
+pg.samplers[0].pid = (-13, 13) # cycle mu+-
+pg.samplers[0].mom = PG.PtEtaMPhiSampler(pt=[4000, 100000], eta=[1.0, 3.2]) # flat in pt and +ve eta
+pg.samplers[1].pid = (13, -13) # cycle mu-+
+pg.samplers[1].mom = PG.PtEtaMPhiSampler(pt=[4000, 100000], eta=[-3.2, -1.0]) # flat in pt and -ve eta
+topSeq += pg
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_fwd_sequence.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_fwd_sequence.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1d4746fb7b7002fb8e096058c1b2b5d1c4eb348
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_fwd_sequence.py
@@ -0,0 +1,19 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+pg = PG.ParticleGun()
+pg.randomSeed = 123456
+pg.sampler.pid = (2112, 22, 2112, 22)
+pg.sampler.mom = PG.EThetaMPhiSampler(energy=(1360000, 500000, 1360000, 500000),
+                                      theta=(0, 0, PG.PI, PG.PI))
+pg.sampler.pos = PG.PosSampler(x=[-120,-100], y=[-10,10], z=203950)
+topSeq += pg
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")
diff --git a/Generators/ParticleGun/share/examples/jobOption.ParticleGun_vtx.py b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_vtx.py
new file mode 100644
index 0000000000000000000000000000000000000000..cdacb7ff5a678cb95894a39a14f0ead507c41a4b
--- /dev/null
+++ b/Generators/ParticleGun/share/examples/jobOption.ParticleGun_vtx.py
@@ -0,0 +1,18 @@
+#! -*- python -*-
+
+# Copyright (C) 2002-2017 CERN for the benefit of the ATLAS collaboration
+
+include("GeneratorUtils/StdEvgenSetup.py")
+theApp.EvtMax = 100
+
+import ParticleGun as PG
+pg = PG.ParticleGun()
+pg.randomSeed = 123456
+pg.sampler.pid = 13
+pg.sampler.pos = PG.PosSampler(x=3140.0, y=[-154.134,154.134], z=[4938.76,5121.29], t=5929.7)
+pg.sampler.mom = PG.EEtaMPhiSampler(energy=100000, eta=1.25, phi=0.0)
+topSeq += pg
+
+include("GeneratorUtils/postJO.CopyWeights.py")
+include("GeneratorUtils/postJO.PoolOutput.py")
+include("GeneratorUtils/postJO.DumpMC.py")