From 38fc89125df66856e2ed2d272e1829d477670047 Mon Sep 17 00:00:00 2001
From: Eric Torrence <eric.torrence@cern.ch>
Date: Fri, 11 Nov 2022 05:40:37 +0100
Subject: [PATCH] First commit of LHC information

---
 .../Reconstruction/scripts/faser_reco.py      |   5 +
 .../FaserAuthentication/CMakeLists.txt        |   1 +
 .../FaserAuthentication/data/dblookup.xml     |   5 +
 LHCData/LHCDataAlgs/CMakeLists.txt            |  19 +
 .../LHCDataAlgs/python/LHCDataAlgConfig.py    |  20 +
 LHCData/LHCDataAlgs/src/LHCDataAlg.cxx        | 136 ++++
 LHCData/LHCDataAlgs/src/LHCDataAlg.h          |  41 +
 .../src/components/LHCDataAlgs_entries.cxx    |   3 +
 LHCData/LHCDataTools/CMakeLists.txt           |  27 +
 .../LHCDataTools/LHCDataTools/ILHCDataTool.h  |  81 ++
 .../LHCDataTools/python/LHCDataToolsConfig.py |  19 +
 LHCData/LHCDataTools/src/LHCDataTool.cxx      | 261 +++++++
 LHCData/LHCDataTools/src/LHCDataTool.h        | 107 +++
 .../src/components/LHCDataTools_entries.cxx   |   3 +
 LHCData/LHCDataUtils/CMakeLists.txt           |  10 +
 LHCData/LHCDataUtils/README.md                |   1 +
 LHCData/LHCDataUtils/python/CoolDataReader.py | 181 +++++
 .../LHCDataUtils/python/LumiBlobConversion.py | 388 +++++++++
 LHCData/LHCDataUtils/python/LumiDBHandler.py  | 121 +++
 .../LHCDataUtils/scripts/makeLHCFillData.py   | 734 ++++++++++++++++++
 xAOD/xAODFaserLHC/CMakeLists.txt              |  27 +
 xAOD/xAODFaserLHC/Root/xAODFaserLHCCLIDs.cxx  |   8 +
 .../Root/xAODFaserLHCDataAux_v1.cxx           |  45 ++
 .../xAODFaserLHC/Root/xAODFaserLHCData_v1.cxx |  78 ++
 xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCData.h |  22 +
 .../xAODFaserLHC/FaserLHCDataAux.h            |  22 +
 xAOD/xAODFaserLHC/xAODFaserLHC/selection.xml  |  13 +
 .../versions/FaserLHCDataAux_v1.h             |  68 ++
 .../xAODFaserLHC/versions/FaserLHCData_v1.h   | 103 +++
 .../xAODFaserLHC/xAODFaserLHCDict.h           |  27 +
 xAOD/xAODFaserLHCAthenaPool/CMakeLists.txt    |  14 +
 .../src/xAODFaserLHCDataAuxCnv.cxx            |   5 +
 .../src/xAODFaserLHCDataAuxCnv.h              |  16 +
 .../src/xAODFaserLHCDataCnv.cxx               |   5 +
 .../src/xAODFaserLHCDataCnv.h                 |  17 +
 35 files changed, 2633 insertions(+)
 create mode 100644 LHCData/LHCDataAlgs/CMakeLists.txt
 create mode 100644 LHCData/LHCDataAlgs/python/LHCDataAlgConfig.py
 create mode 100644 LHCData/LHCDataAlgs/src/LHCDataAlg.cxx
 create mode 100644 LHCData/LHCDataAlgs/src/LHCDataAlg.h
 create mode 100644 LHCData/LHCDataAlgs/src/components/LHCDataAlgs_entries.cxx
 create mode 100644 LHCData/LHCDataTools/CMakeLists.txt
 create mode 100644 LHCData/LHCDataTools/LHCDataTools/ILHCDataTool.h
 create mode 100644 LHCData/LHCDataTools/python/LHCDataToolsConfig.py
 create mode 100644 LHCData/LHCDataTools/src/LHCDataTool.cxx
 create mode 100644 LHCData/LHCDataTools/src/LHCDataTool.h
 create mode 100644 LHCData/LHCDataTools/src/components/LHCDataTools_entries.cxx
 create mode 100644 LHCData/LHCDataUtils/CMakeLists.txt
 create mode 100644 LHCData/LHCDataUtils/README.md
 create mode 100644 LHCData/LHCDataUtils/python/CoolDataReader.py
 create mode 100644 LHCData/LHCDataUtils/python/LumiBlobConversion.py
 create mode 100644 LHCData/LHCDataUtils/python/LumiDBHandler.py
 create mode 100755 LHCData/LHCDataUtils/scripts/makeLHCFillData.py
 create mode 100644 xAOD/xAODFaserLHC/CMakeLists.txt
 create mode 100644 xAOD/xAODFaserLHC/Root/xAODFaserLHCCLIDs.cxx
 create mode 100644 xAOD/xAODFaserLHC/Root/xAODFaserLHCDataAux_v1.cxx
 create mode 100644 xAOD/xAODFaserLHC/Root/xAODFaserLHCData_v1.cxx
 create mode 100644 xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCData.h
 create mode 100644 xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCDataAux.h
 create mode 100644 xAOD/xAODFaserLHC/xAODFaserLHC/selection.xml
 create mode 100644 xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCDataAux_v1.h
 create mode 100644 xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCData_v1.h
 create mode 100644 xAOD/xAODFaserLHC/xAODFaserLHC/xAODFaserLHCDict.h
 create mode 100644 xAOD/xAODFaserLHCAthenaPool/CMakeLists.txt
 create mode 100644 xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.cxx
 create mode 100644 xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.h
 create mode 100644 xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.cxx
 create mode 100644 xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.h

diff --git a/Control/CalypsoExample/Reconstruction/scripts/faser_reco.py b/Control/CalypsoExample/Reconstruction/scripts/faser_reco.py
index 4e233bf4..96752694 100755
--- a/Control/CalypsoExample/Reconstruction/scripts/faser_reco.py
+++ b/Control/CalypsoExample/Reconstruction/scripts/faser_reco.py
@@ -176,6 +176,9 @@ else:
 from FaserGeoModel.FaserGeoModelConfig import FaserGeometryCfg
 acc.merge(FaserGeometryCfg(ConfigFlags))
 
+from LHCDataAlgs.LHCDataAlgConfig import LHCDataAlgCfg
+acc.merge(LHCDataAlgCfg(ConfigFlags))
+
 # Set up algorithms
 from WaveRecAlgs.WaveRecAlgsConfig import WaveformReconstructionCfg    
 acc.merge(WaveformReconstructionCfg(ConfigFlags))
@@ -223,6 +226,8 @@ itemList = [ "xAOD::EventInfo#*"
              , "xAOD::EventAuxInfo#*"
              , "xAOD::FaserTriggerData#*"
              , "xAOD::FaserTriggerDataAux#*"
+             , "xAOD::FaserLHCData#*"
+             , "xAOD::FaserLHCDataAux#*"
              , "FaserSiHitCollection#*"  # Strip hits, do we want this?
              , "FaserSCT_RDO_Container#*" 
              , "FaserSCT_SpacePointContainer#*"
diff --git a/Database/ConnectionManagement/FaserAuthentication/CMakeLists.txt b/Database/ConnectionManagement/FaserAuthentication/CMakeLists.txt
index ecf7b9bb..eb1c72ae 100644
--- a/Database/ConnectionManagement/FaserAuthentication/CMakeLists.txt
+++ b/Database/ConnectionManagement/FaserAuthentication/CMakeLists.txt
@@ -39,6 +39,7 @@ set( FaserAuthentication_home
 # Apparently not needed, because we don't have it...
 #atlas_install_xmls( ${FaserAuthentication_home}/authentication.xml )
 
+
 # Configure the environment setup module:
 configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/FaserAuthenticationEnvironmentConfig.cmake.in
diff --git a/Database/ConnectionManagement/FaserAuthentication/data/dblookup.xml b/Database/ConnectionManagement/FaserAuthentication/data/dblookup.xml
index 958fb2ba..9913f4be 100644
--- a/Database/ConnectionManagement/FaserAuthentication/data/dblookup.xml
+++ b/Database/ConnectionManagement/FaserAuthentication/data/dblookup.xml
@@ -36,4 +36,9 @@
    <service name="sqlite_file:///cvmfs/faser.cern.ch/repo/sw/database/DBRelease/current/sqlite200/ALLP200.db" accessMode="read" />   
 </logicalservice>
 
+<logicalservice name="COOLOFL_LHC">
+   <service name="sqlite_file:data/sqlite200/ALLP200.db" accessMode="read" />
+   <service name="sqlite_file:///cvmfs/faser.cern.ch/repo/sw/database/DBRelease/current/sqlite200/ALLP200.db" accessMode="read" />   
+</logicalservice>
+
 </servicelist>
diff --git a/LHCData/LHCDataAlgs/CMakeLists.txt b/LHCData/LHCDataAlgs/CMakeLists.txt
new file mode 100644
index 00000000..17abef2b
--- /dev/null
+++ b/LHCData/LHCDataAlgs/CMakeLists.txt
@@ -0,0 +1,19 @@
+###############################################################################
+# Package: LHCDataAlgs
+################################################################################
+
+# Declare the package name:
+atlas_subdir( LHCDataAlgs )
+
+# External dependencies:
+find_package( Boost COMPONENTS filesystem thread system )
+
+# Component(s) in the package:
+atlas_add_component( LHCDataAlgs
+                     src/*.h
+                     src/*.cxx
+                     src/components/*.cxx
+                     LINK_LIBRARIES AthenaBaseComps StoreGateLib xAODFaserLHC LHCDataToolsLib )
+
+# Install files from the package:
+atlas_install_python_modules( python/*.py )
diff --git a/LHCData/LHCDataAlgs/python/LHCDataAlgConfig.py b/LHCData/LHCDataAlgs/python/LHCDataAlgConfig.py
new file mode 100644
index 00000000..195fbbd1
--- /dev/null
+++ b/LHCData/LHCDataAlgs/python/LHCDataAlgConfig.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
+from AthenaConfiguration.ComponentFactory import CompFactory
+
+from LHCDataTools.LHCDataToolsConfig import LHCDataCfg
+
+#LHCDataTool = CompFactory.LHCDataTool
+
+def LHCDataAlgCfg(flags, name="LHCDataAlg", **kwargs):
+
+    acc = ComponentAccumulator()
+
+    tool = CompFactory.LHCDataTool(name="LHCDataTool")
+    alg = CompFactory.LHCDataAlg(name, **kwargs)
+    alg.LHCDataTool = tool
+
+    acc.addEventAlgo(alg)
+    acc.merge(LHCDataCfg(flags))
+
+    return acc
diff --git a/LHCData/LHCDataAlgs/src/LHCDataAlg.cxx b/LHCData/LHCDataAlgs/src/LHCDataAlg.cxx
new file mode 100644
index 00000000..9463ab87
--- /dev/null
+++ b/LHCData/LHCDataAlgs/src/LHCDataAlg.cxx
@@ -0,0 +1,136 @@
+#include "LHCDataAlg.h"
+
+#include "xAODFaserLHC/FaserLHCDataAux.h"
+
+LHCDataAlg::LHCDataAlg(const std::string& name, ISvcLocator* pSvcLocator)
+  : AthReentrantAlgorithm(name, pSvcLocator) { 
+}
+
+StatusCode
+LHCDataAlg::initialize() {
+  ATH_MSG_INFO(name() << "::initalize()" );
+
+  // Initalize tools
+  ATH_CHECK( m_lhcTool.retrieve() );
+
+  // Read handle
+  ATH_CHECK( m_eventInfo.initialize() );
+
+  // Write handle
+  ATH_CHECK( m_lhcDataKey.initialize() );
+
+  return StatusCode::SUCCESS;
+}
+
+StatusCode
+LHCDataAlg::finalize() {
+  return StatusCode::SUCCESS;
+}
+
+StatusCode
+LHCDataAlg::execute(const EventContext& ctx) const {
+
+  ATH_MSG_DEBUG("Executing");
+
+  SG::WriteHandle<xAOD::FaserLHCData> lhcDataHandle(m_lhcDataKey, ctx);
+  ATH_CHECK( lhcDataHandle.record( std::make_unique<xAOD::FaserLHCData>(),
+				   std::make_unique<xAOD::FaserLHCDataAux>() ) );
+
+  ATH_MSG_DEBUG("FaserLHCData " << lhcDataHandle.name() << " initialized");
+
+  lhcDataHandle->set_fillNumber(m_lhcTool->getFillNumber(ctx));
+  lhcDataHandle->set_machineMode(m_lhcTool->getMachineMode(ctx));
+  lhcDataHandle->set_injectionScheme(m_lhcTool->getInjectionScheme(ctx));
+  lhcDataHandle->set_beamType1(m_lhcTool->getBeamType1(ctx));
+  lhcDataHandle->set_beamType2(m_lhcTool->getBeamType2(ctx));
+
+  lhcDataHandle->set_numBunchBeam1(m_lhcTool->getNumBunchBeam1(ctx));
+  lhcDataHandle->set_numBunchBeam2(m_lhcTool->getNumBunchBeam2(ctx));
+  lhcDataHandle->set_numBunchColliding(m_lhcTool->getNumBunchColl(ctx));
+
+  lhcDataHandle->set_beamMode(m_lhcTool->getBeamMode(ctx));
+  lhcDataHandle->set_betaStar(m_lhcTool->getBetaStar(ctx));
+  lhcDataHandle->set_crossingAngle(m_lhcTool->getCrossingAngle(ctx));
+
+  //lhcDataHandle->set_beam1Bunches(m_lhcTool->getBeam1Bunches(ctx));
+  //lhcDataHandle->set_beam2Bunches(m_lhcTool->getBeam2Bunches(ctx));
+  //lhcDataHandle->set_luminousBunches(m_lhcTool->getLuminousBunches(ctx));
+
+  // Fill BCID information
+
+  // Get the BCID mask
+  std::vector<unsigned char> bcid_mask = m_lhcTool->getBCIDMasks(ctx);
+
+  // Get the event bcid value
+  SG::ReadHandle<xAOD::EventInfo> xevt(m_eventInfo, ctx);
+  unsigned int bcid = xevt->bcid();
+
+  int nearest = findDistance(bcid, bcid_mask, 3);  // Colliding beams
+  lhcDataHandle->set_distanceToCollidingBCID(nearest);
+  ATH_MSG_DEBUG("Found distance of " << nearest << " from BCID " << bcid 
+		<< " to the nearest colliding BCID ");
+
+  nearest = findDistance(bcid, bcid_mask, 1);  // Beam1 unpaired
+  lhcDataHandle->set_distanceToUnpairedB1(nearest);
+  ATH_MSG_DEBUG("Found distance of " << nearest << " from BCID " << bcid 
+		<< " to the nearest unpaired B1 ");
+
+  nearest = findDistance(bcid, bcid_mask, 2);  // Beam2 unpaired
+  lhcDataHandle->set_distanceToUnpairedB2(nearest);
+  ATH_MSG_DEBUG("Found distance of " << nearest << " from BCID " << bcid 
+		<< " to the nearest unpaired B2 ");
+
+  return StatusCode::SUCCESS;
+}
+
+// Function to find distance to nearest BCID
+// mask indicates the condition: 1 - unpaired B1, 2 - unpaired B2, 3 - colliding
+int
+LHCDataAlg::findDistance(unsigned int bcid, const std::vector<unsigned char>& bcid_mask, unsigned char mask) const {
+
+  // Does the BCID make sense?
+  if ((bcid > 3564) || (bcid <= 0)) {
+    ATH_MSG_WARNING("Requested distance from invalid BCID " << bcid << "!");
+    return -3565;
+  }
+
+  unsigned int test_bcid;
+
+  // Move outwards from the bcid (starting at offset of 0)
+  for (unsigned int i=0; i < 3564/2; i++) {
+
+    // Try positive offsets
+    test_bcid = bcid + i;
+
+    // Wrap around
+    if (test_bcid >= 3564) test_bcid -= 3564;
+
+    // BCID 0 doesn't exist in the real machine
+    // BCID 3564 doesn't exist in our array (but will always be empty in real machine)
+    // So avoid these pathologies
+    if (test_bcid != 0) 
+      if (mask == bcid_mask[test_bcid]) return i;
+
+    if(i == 0) continue;  // No need to check -0
+
+    // Try negative offsets
+    test_bcid = bcid - i;
+
+    // Wrap around
+    if (test_bcid < 1) test_bcid += 3564;
+
+    // BCID 0 doesn't exist in the real machine
+    // BCID 3564 doesn't exist in our array (but will always be empty)
+    // So avoid these pathologies
+    if (test_bcid != 3564) 
+      if (mask == bcid_mask[test_bcid]) return -i;
+
+  }
+
+  // If we got to here, there was no match
+  // Does the BCID make sense?
+  ATH_MSG_WARNING("Couldn't find distance from BCID " << bcid << " and pattern " << mask << "!");
+  ATH_MSG_WARNING(bcid_mask);
+  return -3565;
+
+}
diff --git a/LHCData/LHCDataAlgs/src/LHCDataAlg.h b/LHCData/LHCDataAlgs/src/LHCDataAlg.h
new file mode 100644
index 00000000..f53257bc
--- /dev/null
+++ b/LHCData/LHCDataAlgs/src/LHCDataAlg.h
@@ -0,0 +1,41 @@
+#ifndef LHCDATAALG_H
+#define LHCDATAALG_H
+
+#include "AthenaBaseComps/AthReentrantAlgorithm.h"
+
+#include "StoreGate/ReadCondHandleKey.h"
+#include "StoreGate/WriteCondHandleKey.h"
+
+#include "GaudiKernel/ToolHandle.h"
+#include "GaudiKernel/ICondSvc.h"
+#include "GaudiKernel/ServiceHandle.h"
+
+#include "xAODFaserLHC/FaserLHCData.h"
+#include "xAODEventInfo/EventInfo.h"
+
+#include "LHCDataTools/ILHCDataTool.h"
+
+class LHCDataAlg : public AthReentrantAlgorithm {
+ public:
+  LHCDataAlg(const std::string& name, ISvcLocator* pSvcLocator);
+  virtual ~LHCDataAlg() = default;
+
+  virtual StatusCode initialize() override;
+  virtual StatusCode execute(const EventContext& ctx) const override;
+  virtual StatusCode finalize() override;
+  virtual bool isClonable() const override { return true; };
+
+ private:
+  ToolHandle<ILHCDataTool> m_lhcTool{this, "LHCDataTool", "LHCDataTool"};
+  SG::ReadHandleKey<xAOD::EventInfo> m_eventInfo{ this, "EventInfoKey", "EventInfo", "ReadHandleKey for xAOD::EventInfo"};
+  SG::WriteHandleKey<xAOD::FaserLHCData> m_lhcDataKey
+    {this, "FaserLHCDataKey", "FaserLHCData"};
+
+  // Utility function to find nearest BCID
+  int findDistance(unsigned int bcid, const std::vector<unsigned char>& bcid_mask, 
+		   unsigned char mask) const;
+
+  //ServiceHandle<ICondSvc> m_condSvc{this, "CondSvc", "CondSvc"};
+};
+
+#endif // LHCDATAALG_H
diff --git a/LHCData/LHCDataAlgs/src/components/LHCDataAlgs_entries.cxx b/LHCData/LHCDataAlgs/src/components/LHCDataAlgs_entries.cxx
new file mode 100644
index 00000000..1620e8dc
--- /dev/null
+++ b/LHCData/LHCDataAlgs/src/components/LHCDataAlgs_entries.cxx
@@ -0,0 +1,3 @@
+#include "../LHCDataAlg.h"
+
+DECLARE_COMPONENT( LHCDataAlg )
diff --git a/LHCData/LHCDataTools/CMakeLists.txt b/LHCData/LHCDataTools/CMakeLists.txt
new file mode 100644
index 00000000..248abc91
--- /dev/null
+++ b/LHCData/LHCDataTools/CMakeLists.txt
@@ -0,0 +1,27 @@
+###############################################################################
+# Package: LHCDataTools
+################################################################################
+
+# Declare the package name:
+atlas_subdir( LHCDataTools )
+
+# External dependencies:
+find_package( CLHEP )
+find_package( ROOT COMPONENTS Core Tree MathCore Hist RIO pthread )
+
+# Component(s) in the package:
+atlas_add_component ( LHCDataTools
+                      src/components/*.cxx
+                      INCLUDE_DIRS ${ROOT_INCLUDE_DIRS} ${CLHEP_INCLUDE_DIRS}
+                      LINK_LIBRARIES ${ROOT_LIBRARIES} ${CLHEP_LIBRARIES} AthenaKernel LHCDataToolsLib GaudiKernel AthenaBaseComps AthenaPoolUtilities StoreGateLib xAODEventInfo )
+
+
+atlas_add_library( LHCDataToolsLib
+                  src/*.cxx
+                  PUBLIC_HEADERS LHCDataTools
+                  INCLUDE_DIRS ${ROOT_INCLUDE_DIRS} ${CLHEP_INCLUDE_DIRS}
+                  LINK_LIBRARIES ${ROOT_LIBRARIES} ${CLHEP_LIBRARIES} AthenaKernel  GaudiKernel AthenaBaseComps AthenaPoolUtilities StoreGateLib xAODEventInfo )
+
+# Install files from the package:
+atlas_install_python_modules( python/*.py )
+
diff --git a/LHCData/LHCDataTools/LHCDataTools/ILHCDataTool.h b/LHCData/LHCDataTools/LHCDataTools/ILHCDataTool.h
new file mode 100644
index 00000000..cf6abd17
--- /dev/null
+++ b/LHCData/LHCDataTools/LHCDataTools/ILHCDataTool.h
@@ -0,0 +1,81 @@
+/*
+  Copyright (C) 2002-2019 CERN for the benefit of the ATLAS and FAsER collaborations
+*/
+
+/** @file ILHCDataTool.h Interface file for LHCDataTool.
+ */
+
+// Multiple inclusion protection
+#ifndef ILHCDATATOOL
+#define ILHCDATATOOL
+
+//STL includes
+#include <map>
+
+//Gaudi Includes
+#include "GaudiKernel/IAlgTool.h"
+#include "GaudiKernel/EventContext.h"
+
+class ILHCDataTool: virtual public IAlgTool {
+
+ public:
+  
+  //----------Public Member Functions----------//
+  // Structors
+  virtual ~ILHCDataTool() = default; //!< Destructor
+
+  /// Creates the InterfaceID and interfaceID() method
+  DeclareInterfaceID(ILHCDataTool, 1, 0);
+
+  // Methods to return fill data
+  virtual int getFillNumber(const EventContext& ctx) const = 0;
+  virtual int getFillNumber(void) const = 0;
+
+  virtual std::string getMachineMode(const EventContext& ctx) const = 0;
+  virtual std::string getMachineMode(void) const = 0;
+
+  virtual std::string getInjectionScheme(const EventContext& ctx) const = 0;
+  virtual std::string getInjectionScheme(void) const = 0;
+
+  virtual int getBeamType1(const EventContext& ctx) const = 0;
+  virtual int getBeamType1(void) const = 0;
+
+  virtual int getBeamType2(const EventContext& ctx) const = 0;
+  virtual int getBeamType2(void) const = 0;
+
+  virtual unsigned int getNumBunchBeam1(const EventContext& ctx) const = 0;
+  virtual unsigned int getNumBunchBeam1(void) const = 0;
+
+  virtual unsigned int getNumBunchBeam2(const EventContext& ctx) const = 0;
+  virtual unsigned int getNumBunchBeam2(void) const = 0;
+
+  virtual unsigned int getNumBunchColl(const EventContext& ctx) const = 0;
+  virtual unsigned int getNumBunchColl(void) const = 0;
+
+  // Methods to return beam data
+  virtual std::string getBeamMode(const EventContext& ctx) const = 0;
+  virtual std::string getBeamMode(void) const = 0;
+
+  virtual float getBetaStar(const EventContext& ctx) const = 0;
+  virtual float getBetaStar(void) const = 0;
+
+  virtual float getCrossingAngle(const EventContext& ctx) const = 0;
+  virtual float getCrossingAngle(void) const = 0;
+
+  // Methods to return BCID data
+  virtual unsigned int getBeam1Bunches(const EventContext& ctx) const = 0;
+  virtual unsigned int getBeam1Bunches(void) const = 0;
+
+  virtual unsigned int getBeam2Bunches(const EventContext& ctx) const = 0;
+  virtual unsigned int getBeam2Bunches(void) const = 0;
+
+  virtual unsigned int getLuminousBunches(const EventContext& ctx) const = 0;
+  virtual unsigned int getLuminousBunches(void) const = 0;
+
+  virtual std::vector<unsigned char> getBCIDMasks(const EventContext& ctx) const = 0;
+  virtual std::vector<unsigned char> getBCIDMasks(void) const = 0;
+
+};
+
+//---------------------------------------------------------------------- 
+#endif // LHCDATATOOL
diff --git a/LHCData/LHCDataTools/python/LHCDataToolsConfig.py b/LHCData/LHCDataTools/python/LHCDataToolsConfig.py
new file mode 100644
index 00000000..cdd2d267
--- /dev/null
+++ b/LHCData/LHCDataTools/python/LHCDataToolsConfig.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
+from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
+from AthenaConfiguration.ComponentFactory import CompFactory
+from IOVDbSvc.IOVDbSvcConfig import addFolders
+
+def LHCDataCfg(flags, **kwargs):
+    acc = ComponentAccumulator()
+    dbInstance = kwargs.get("dbInstance", "COOLOFL_LHC")
+    dbName = flags.IOVDb.DatabaseInstance # e.g. CONDBR3 
+
+    #acc.merge(addFolders(flags, folder_list, dbInstance, className="AthenaAttributeList"))
+    # COOLOFL_LHC is not known to ATLAS IOVDBSvc
+    # Must use non-shorthand folder specification here
+    folder_list = ["/LHC/FillData", "/LHC/BeamData", "/LHC/BCIDData"]
+    for folder_name in folder_list:
+        folder_string = f"<db>{dbInstance}/{dbName}</db> {folder_name}"
+        acc.merge(addFolders(flags, folder_string, className="AthenaAttributeList"))
+    return acc
+
diff --git a/LHCData/LHCDataTools/src/LHCDataTool.cxx b/LHCData/LHCDataTools/src/LHCDataTool.cxx
new file mode 100644
index 00000000..64a2d536
--- /dev/null
+++ b/LHCData/LHCDataTools/src/LHCDataTool.cxx
@@ -0,0 +1,261 @@
+/*
+  Copyright (C) 2002-2019 CERN for the benefit of the ATLAS and FASER collaborations
+*/
+
+/** @file LHCDataTool.cxx Implementation file for LHCDataTool.
+    @author Eric Torrence (05/02/22)
+*/
+
+#include "LHCDataTool.h"
+#include "CoralBase/Blob.h"
+
+//----------------------------------------------------------------------
+LHCDataTool::LHCDataTool (const std::string& type, const std::string& name, const IInterface* parent) :
+  base_class(type, name, parent)
+{
+}
+
+//----------------------------------------------------------------------
+StatusCode 
+LHCDataTool::initialize() {
+
+  ATH_MSG_DEBUG("LHCDataTool::initialize()");
+
+  // Read Handles
+  ATH_CHECK(m_fillDataKey.initialize());
+  ATH_CHECK(m_beamDataKey.initialize());
+  ATH_CHECK(m_bcidDataKey.initialize());
+
+  return StatusCode::SUCCESS;
+}
+
+//----------------------------------------------------------------------
+StatusCode
+LHCDataTool::finalize() {
+  // Print where you are
+  return StatusCode::SUCCESS;
+}
+
+//----------------------------------------------------------------------
+int
+LHCDataTool::getFillNumber(const EventContext& ctx) const {
+  // Read Cond Handle
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return (**readHandle)["FillNumber"].data<int>();
+} 
+
+int
+LHCDataTool::getFillNumber(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getFillNumber(ctx);
+}
+
+//----------------------------------------------------------------------
+std::string
+LHCDataTool::getMachineMode(const EventContext& ctx) const {
+  // Read Cond Handle
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return (**readHandle)["MachineMode"].data<std::string>();
+} 
+
+std::string
+LHCDataTool::getMachineMode(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getMachineMode(ctx);
+}
+
+//----------------------------------------------------------------------
+std::string
+LHCDataTool::getInjectionScheme(const EventContext& ctx) const {
+  // Read Cond Handle
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return (**readHandle)["InjectionScheme"].data<std::string>();
+} 
+
+std::string
+LHCDataTool::getInjectionScheme(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getInjectionScheme(ctx);
+}
+
+//----------------------------------------------------------------------
+int
+LHCDataTool::getBeamType1(const EventContext& ctx) const {
+  // Read Cond Handle
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return (**readHandle)["BeamType1"].data<int>();
+} 
+
+int
+LHCDataTool::getBeamType1(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBeamType1(ctx);
+}
+
+//----------------------------------------------------------------------
+int
+LHCDataTool::getBeamType2(const EventContext& ctx) const {
+  // Read Cond Handle
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return (**readHandle)["BeamType2"].data<int>();
+} 
+
+int
+LHCDataTool::getBeamType2(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBeamType2(ctx);
+}
+
+//----------------------------------------------------------------------
+unsigned int
+LHCDataTool::getNumBunchBeam1(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return(**readHandle)["NumBunchBeam1"].data<unsigned int>();
+}
+
+unsigned int
+LHCDataTool::getNumBunchBeam1(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getNumBunchBeam1(ctx);
+}
+
+unsigned int
+LHCDataTool::getNumBunchBeam2(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return(**readHandle)["NumBunchBeam2"].data<unsigned int>();
+}
+
+unsigned int
+LHCDataTool::getNumBunchBeam2(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getNumBunchBeam2(ctx);
+}
+
+unsigned int
+LHCDataTool::getNumBunchColl(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_fillDataKey, ctx};
+  return(**readHandle)["NumBunchColl"].data<unsigned int>();
+}
+
+unsigned int
+LHCDataTool::getNumBunchColl(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getNumBunchColl(ctx);
+}
+
+//----------------------------------------------------------------------
+std::string
+LHCDataTool::getBeamMode(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_beamDataKey, ctx};
+  return(**readHandle)["BeamMode"].data<std::string>();
+}
+
+std::string
+LHCDataTool::getBeamMode(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBeamMode(ctx);
+}
+
+//----------------------------------------------------------------------
+float
+LHCDataTool::getBetaStar(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_beamDataKey, ctx};
+  return(**readHandle)["BetaStar"].data<float>();
+}
+
+float
+LHCDataTool::getBetaStar(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBetaStar(ctx);
+}
+
+//----------------------------------------------------------------------
+float
+LHCDataTool::getCrossingAngle(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_beamDataKey, ctx};
+  return(**readHandle)["CrossingAngle"].data<float>();
+}
+
+float
+LHCDataTool::getCrossingAngle(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getCrossingAngle(ctx);
+}
+
+//----------------------------------------------------------------------
+unsigned int
+LHCDataTool::getBeam1Bunches(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_bcidDataKey, ctx};
+  return(**readHandle)["Beam1Bunches"].data<unsigned int>();
+}
+
+unsigned int
+LHCDataTool::getBeam1Bunches(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBeam1Bunches(ctx);
+}
+
+unsigned int
+LHCDataTool::getBeam2Bunches(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_bcidDataKey, ctx};
+  return(**readHandle)["Beam2Bunches"].data<unsigned int>();
+}
+
+unsigned int
+LHCDataTool::getBeam2Bunches(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBeam2Bunches(ctx);
+}
+
+unsigned int
+LHCDataTool::getLuminousBunches(const EventContext& ctx) const {
+  SG::ReadCondHandle<AthenaAttributeList> readHandle{m_bcidDataKey, ctx};
+  return(**readHandle)["LuminousBunches"].data<unsigned int>();
+}
+
+unsigned int
+LHCDataTool::getLuminousBunches(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getLuminousBunches(ctx);
+}
+
+
+//----------------------------------------------------------------------
+// This function unpacks the blob every time this is accesed
+// Should probably cache this using a callback
+std::vector<unsigned char>
+LHCDataTool::getBCIDMasks(const EventContext& ctx) const {
+
+  SG::ReadCondHandle<AthenaAttributeList> bcidHandle{m_bcidDataKey, ctx};
+  const coral::Blob& blob = (**bcidHandle)["BCIDmasks"].data<coral::Blob>();
+  const unsigned char* p = static_cast<const unsigned char*>(blob.startingAddress());
+
+  // Should always be 3564 BCIDs
+  if (blob.size() != 3564) {
+    ATH_MSG_WARNING("Found BCID blob with size " << blob.size() << "!");
+  }
+
+  std::vector<unsigned char> bcid_vector(3564);
+
+  bool first = true;
+  for (int i=0; i<blob.size(); i++) {
+    // First BCID is 1, but this is stored at location i=1
+    // So you can index this vector as bcid_vector[bcid_number]
+    bcid_vector[i] = *p++;  
+    if (first && (bcid_vector[i] == 3)) {
+      first = false;
+      ATH_MSG_DEBUG("Found first colliding BCID at " << i);
+    }
+  }
+
+  return bcid_vector;
+}
+
+std::vector<unsigned char>
+LHCDataTool::getBCIDMasks(void) const {
+  const EventContext& ctx{Gaudi::Hive::currentContext()};
+  return getBCIDMasks(ctx);
+}
+
+
+
diff --git a/LHCData/LHCDataTools/src/LHCDataTool.h b/LHCData/LHCDataTools/src/LHCDataTool.h
new file mode 100644
index 00000000..6a969cc0
--- /dev/null
+++ b/LHCData/LHCDataTools/src/LHCDataTool.h
@@ -0,0 +1,107 @@
+// -*- C++ -*-
+
+/*
+  Copyright (C) 2002-2019 CERN for the benefit of the ATLAS and CERN collaborations
+*/
+
+/** @file LHCDataTool.h Header file for LHCDataTool.
+    @author Eric Torrence, 20/04/22
+*/
+
+// Multiple inclusion protection
+#ifndef LHCDATA_TOOL
+#define LHCDATA_TOOL
+
+// Include interface class
+#include "AthenaBaseComps/AthAlgTool.h"
+#include "LHCDataTools/ILHCDataTool.h"
+
+// Include Athena stuff
+#include "AthenaPoolUtilities/CondAttrListCollection.h"
+#include "StoreGate/ReadCondHandleKey.h"
+
+#include "GaudiKernel/ICondSvc.h"
+#include "Gaudi/Property.h"
+
+// Include Gaudi classes
+#include "GaudiKernel/EventContext.h"
+
+/** This class contains a Tool that reads Waveform range data and makes it available to 
+    other algorithms. The current implementation reads the data from a COOL database. 
+*/
+
+class LHCDataTool: public extends<AthAlgTool, ILHCDataTool> {
+
+ public:
+  //----------Public Member Functions----------//
+  // Structors
+  LHCDataTool(const std::string& type, const std::string& name, const IInterface* parent); //!< Constructor
+  virtual ~LHCDataTool() = default; //!< Destructor
+
+  // Standard Gaudi functions
+  virtual StatusCode initialize() override; //!< Gaudi initialiser
+  virtual StatusCode finalize() override; //!< Gaudi finaliser
+
+  // Methods to return fill data
+  virtual int getFillNumber(const EventContext& ctx) const override;
+  virtual int getFillNumber(void) const override;
+
+  virtual std::string getMachineMode(const EventContext& ctx) const override;
+  virtual std::string getMachineMode(void) const override;
+
+  virtual std::string getInjectionScheme(const EventContext& ctx) const override;
+  virtual std::string getInjectionScheme(void) const override;
+
+  virtual int getBeamType1(const EventContext& ctx) const override;
+  virtual int getBeamType1(void) const override;
+
+  virtual int getBeamType2(const EventContext& ctx) const override;
+  virtual int getBeamType2(void) const override;
+
+  virtual unsigned int getNumBunchBeam1(const EventContext& ctx) const override;
+  virtual unsigned int getNumBunchBeam1(void) const override;
+
+  virtual unsigned int getNumBunchBeam2(const EventContext& ctx) const override;
+  virtual unsigned int getNumBunchBeam2(void) const override;
+
+  virtual unsigned int getNumBunchColl(const EventContext& ctx) const override;
+  virtual unsigned int getNumBunchColl(void) const override;
+
+  // Methods to return beam data
+  virtual std::string getBeamMode(const EventContext& ctx) const override;
+  virtual std::string getBeamMode(void) const override;
+
+  virtual float getBetaStar(const EventContext& ctx) const override;
+  virtual float getBetaStar(void) const override;
+
+  virtual float getCrossingAngle(const EventContext& ctx) const override;
+  virtual float getCrossingAngle(void) const override;
+
+  // Methods to return BCID data
+  virtual unsigned int getBeam1Bunches(const EventContext& ctx) const override;
+  virtual unsigned int getBeam1Bunches(void) const override;
+
+  virtual unsigned int getBeam2Bunches(const EventContext& ctx) const override;
+  virtual unsigned int getBeam2Bunches(void) const override;
+
+  virtual unsigned int getLuminousBunches(const EventContext& ctx) const override;
+  virtual unsigned int getLuminousBunches(void) const override;
+
+  // This returns a char for each BCID encoding beam1/beam2
+  // A colliding BCID will have value 3
+  // BCIDs always count starting at 1
+  virtual std::vector<unsigned char> getBCIDMasks(const EventContext& ctx) const override;
+  virtual std::vector<unsigned char> getBCIDMasks(void) const override;
+
+ private:
+  // Read Cond Handles
+  SG::ReadCondHandleKey<AthenaAttributeList> m_fillDataKey{this, "FillDataKey", "/LHC/FillData", "Key of fill data folder"};
+  SG::ReadCondHandleKey<AthenaAttributeList> m_beamDataKey{this, "BeamDataKey", "/LHC/BeamData", "Key of fill data folder"};
+  SG::ReadCondHandleKey<AthenaAttributeList> m_bcidDataKey{this, "BcidDataKey", "/LHC/BCIDData", "Key of fill data folder"};
+
+  ServiceHandle<ICondSvc> m_condSvc{this, "CondSvc", "CondSvc"};
+
+};
+
+//---------------------------------------------------------------------- 
+#endif // LHCDATA_TOOL
diff --git a/LHCData/LHCDataTools/src/components/LHCDataTools_entries.cxx b/LHCData/LHCDataTools/src/components/LHCDataTools_entries.cxx
new file mode 100644
index 00000000..1f44bf09
--- /dev/null
+++ b/LHCData/LHCDataTools/src/components/LHCDataTools_entries.cxx
@@ -0,0 +1,3 @@
+#include "../LHCDataTool.h"
+
+DECLARE_COMPONENT( LHCDataTool )
diff --git a/LHCData/LHCDataUtils/CMakeLists.txt b/LHCData/LHCDataUtils/CMakeLists.txt
new file mode 100644
index 00000000..2c449495
--- /dev/null
+++ b/LHCData/LHCDataUtils/CMakeLists.txt
@@ -0,0 +1,10 @@
+################################################################################
+# Package: LHCDataUtils
+################################################################################
+
+# Declare the package name:
+atlas_subdir( LHCDataUtils )
+
+atlas_install_python_modules( python/*.py )
+
+atlas_install_scripts( scripts/*.sh scripts/*.py )
diff --git a/LHCData/LHCDataUtils/README.md b/LHCData/LHCDataUtils/README.md
new file mode 100644
index 00000000..0ca447fe
--- /dev/null
+++ b/LHCData/LHCDataUtils/README.md
@@ -0,0 +1 @@
+Utilities to produce and update COOL databases for LHC information
diff --git a/LHCData/LHCDataUtils/python/CoolDataReader.py b/LHCData/LHCDataUtils/python/CoolDataReader.py
new file mode 100644
index 00000000..238f8987
--- /dev/null
+++ b/LHCData/LHCDataUtils/python/CoolDataReader.py
@@ -0,0 +1,181 @@
+# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+
+#
+# CoolDataReader
+#
+# Eric Torrence - October 2010
+#
+# Contents:
+# CoolDataReader - utility object to handle reading of COOL DB folders.
+#                 The benefit over just using AtlCoolLib directly is that each DB connection is
+#                 cached, so multiple connections to the same DB will not be made.
+#
+#                 CoolDataReader.readData() returns a list the full IObject for maximal flexibility
+#
+# General usage example
+# myReader = CoolDataReader('COOLONL_TRIGGER/COMP200', '/TRIGGER/LUMI/LBLESTONL')
+# myReader.setIOVRange(startIOV, endIOV)
+# myReader.readData()
+# for obj in myReader.data:
+#   ...
+#
+# One can specify specific channels or IOV ranges if desired, but by default all data will be loaded
+#
+# The CoolDataReader uses the LumiDBHandler internally to cache multiple CoolConnections
+#
+
+from __future__ import print_function
+from PyCool import cool
+
+# Get our global DB handler object
+from LHCDataUtils.LumiDBHandler import LumiDBHandler
+
+
+class CoolDataReader:
+
+    def __init__(self, dbstr=None, folderstr=None):
+
+        self.verbose = False
+
+        # Defined variables
+        self.dbstr = None
+        self.folderstr = None
+        self.channelIdList = []
+        self.tag = ''
+        self.iovstart = None
+        self.iovend = None
+
+        self.folder = None
+        self.data = []
+        
+        # Initialize to default values
+        self.setChannel()
+        self.setTag()
+        self.setFolder(dbstr, folderstr)
+        self.setIOVRange()
+            
+    def setFolder(self, dbstr, folderstr):
+        # Force re-opening connection if these are different
+        if (dbstr != self.dbstr) or (folderstr != self.folderstr):
+            self.folder = None
+            
+        self.dbstr = dbstr
+        self.folderstr = folderstr
+
+    def setTag(self, tagstr=''):
+        self.tag = tagstr
+
+    def setChannel(self, channelIdList=[]):
+        self.channelIdList = channelIdList
+        
+    def setChannelAll(self):
+        self.setChannel()
+
+    def setChannelId(self, channelId):
+        self.setChannel([channelId])
+        
+    def setIOVRange(self, iovstart=cool.ValidityKeyMin, iovend=cool.ValidityKeyMax):
+        self.iovstart = iovstart
+        self.iovend = iovend
+
+    def setIOVRangeFromRun(self, runnum, startOfNextRun=False):
+        self.iovstart = runnum << 32
+        if startOfNextRun:
+            self.iovend = ((runnum+1) << 32)
+        else:
+            self.iovend = ((runnum+1) << 32) - 1
+
+    # Call to get data after all other parameters are properly set
+    # Data is returned as a list of IObject values, one per DB entry.
+    # This gives maximal flexibility to manipulate the items
+    def readData(self):
+
+        self.data = []
+
+        # Open the DB connection here if needed
+        if self.folder is None:
+            dbHandler = LumiDBHandler()
+            self.folder = dbHandler.getFolder(self.dbstr, self.folderstr)
+            
+            if self.folder is None:
+                print("Can't access DB", self.dbstr, 'folder', self.folderstr, '!')
+                return self.data
+
+        # Create the channel list
+        if len(self.channelIdList) == 0:
+            channels = cool.ChannelSelection.all()
+            self.readChannelList(channels)
+
+        else:
+            # Build the channel list here
+            self.channelIdList.sort()  # Must be sorted!
+
+            # Must read channels 50 at a time due to COOL limit...
+            ichan = 0
+            while (ichan < len(self.channelIdList)) :
+
+                jchan = 0
+                channels = None
+                firstChan = True
+            
+                for channelId in self.channelIdList[ichan:]:
+                    jchan += 1
+                    if firstChan:
+                        firstChan = False
+                        channels = cool.ChannelSelection(channelId)
+                    else:
+                        channels.addChannel(channelId)
+                    if jchan == 50: break 
+
+                # Remeber how many we have read for next time
+                if self.verbose:
+                    print('CoolDataReader.readData() - loaded %d channels from %d' % (jchan, ichan))
+                ichan += jchan
+
+                if self.verbose:
+                    print('CoolDataReader.readData() - browsing', self.iovstart, self.iovend, 'with channel', channels, 'and tag', self.tag)
+
+                self.readChannelList(channels)
+
+            # End of loop building channel list and reading
+
+        # End of if statement reading data
+        return self.data
+
+    def readChannelList(self, channels):
+
+        # Open iterator over our defined IOV range
+        try:
+            itr = self.folder.browseObjects(self.iovstart, self.iovend, channels, self.tag)
+        except Exception as e:
+            print('CoolDataReader.readData() - exception reading folder:', self.folderstr)
+            print(e)
+            print('CoolDataReader.readData() - will try to reconnect (once)')
+
+            # Force re-opening connection
+            dbHandler = LumiDBHandler()
+            dbHandler.verbose = True
+            self.folder = dbHandler.getFolder(self.dbstr, self.folderstr, force=True)
+            
+            if self.folder is None:
+                print('CoolDataReader.readData() - forced re-opening failed!')
+                return self.data
+
+            # OK, lets try reading this again
+            print('CoolDataReader.readData() - trying to re-read re-opened folder!')
+            try:
+                itr = self.folder.browseObjects(self.iovstart, self.iovend, channels, self.tag)
+            except Exception as e:
+                print('CoolDataReader.readData() - exception reading folder:', self.folderstr)
+                print(e)
+                return self.data
+                
+        while itr.goToNext():
+            obj = itr.currentRef()
+            # print obj.payload()
+            self.data.append(obj.clone())
+            
+        itr.close()
+
+
+        
diff --git a/LHCData/LHCDataUtils/python/LumiBlobConversion.py b/LHCData/LHCDataUtils/python/LumiBlobConversion.py
new file mode 100644
index 00000000..bf57d1d2
--- /dev/null
+++ b/LHCData/LHCDataUtils/python/LumiBlobConversion.py
@@ -0,0 +1,388 @@
+# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+
+from __future__ import print_function
+from builtins import range
+import sys
+import array
+import struct
+
+# import cppyy
+# cppyy.gbl.cool.IDatabase # force the load of the dictionary (to stay on the safe side)
+# from cppyy import gbl
+# def blob_read(self, size = -1):
+#     if size < 0:
+#         endpos = self.size()
+#     else:
+#         endpos = self.pos + size
+#     beginpos = self.pos
+#     self.pos = endpos
+#     buf = self.startingAddress()
+#     buf.SetSize(self.size())
+#     return buf[beginpos:endpos]
+
+# add the new functions
+# getattr(gbl,"coral::Blob").read = blob_read
+
+def bConvert(b, nbyte=1):
+    # routine to store an unsigned int (1, 2, 4 or 8 byte) in a blob
+    packopt=dict([[1,'B'],[2,'H'],[4,'f'],[8,'d']])
+    if nbyte in packopt:
+        # print 'bConvert - b:[', b[0:nbyte], '] nbyte:', nbyte, ' fmt:', packopt[nbyte], type(b)
+        ival=struct.unpack(packopt[nbyte], b[0:nbyte])
+    else:
+        print(f'bConvert: Unrecognized pack option {nbyte}')
+        sys.exit()
+
+    return ival[0]
+
+# Optional arguemnt to nval to specify number of values to read
+def bConvertList(b, nbyte=1, nval=1):
+    # routine to store an unsigned int (1, 2, 4 or 8 byte) in a blob
+    packopt=dict([[1,'B'],[2,'H'],[4,'f'],[8,'d']])
+    if nbyte in packopt:
+        # print 'bConvert - b:[', b[0:nbyte], '] nbyte:', nbyte, ' fmt:', packopt[nbyte], type(b)
+        fmt = '%d%s' % (nval, packopt[nbyte])
+        ival=struct.unpack(fmt, b[0:nval*nbyte])
+    else:
+        print(f'bConvertList: Unrecognized pack option {nbyte}')
+        sys.exit()
+
+    return list(ival)
+
+# Unpack bunch group bgrp.  By default, bgrp=1 is the physics bunch group. 
+def unpackBunchGroup(blob, bgrp=1):
+    physBG = []
+    if blob is None: return
+    if blob.size() == 0: return
+    
+    blobCopy = blob.read()
+    mask = (1 << int(bgrp))
+    
+    ivallist = bConvertList(blobCopy, 1, 3564)
+    for i in range(3564):
+        if ivallist[i] & mask:
+            physBG.append(i)
+
+#     blobCounter = 0
+#     for i in range(3564):
+#         try:
+#             b = blobCopy[blobCounter:blobCounter+1]
+#             blobCounter += 1
+#             ival = struct.unpack('B', b)
+#             #s = struct.unpack('B', b)
+#             #ival = bConvert(s)
+#         except Exception, e:
+#             print e
+#             ival = 0
+#         if (ival>>1) & 1 == 1:
+#             physBG.append(i)
+
+    return physBG
+            
+# Unpack bunch group bgrp.  By default, bgrp=1 is the physics bunch group. 
+def unpackBunchGroupList(blob, bgrp=[1]):
+    physBG = dict()
+    mask = dict()
+
+    if blob is None: return    
+    if blob.size() == 0: return
+    
+    blobCopy = blob.read()
+
+    for id in bgrp:
+        mask[id] = (1 << int(id))
+        physBG[id] = []
+        
+    ivallist = bConvertList(blobCopy, 1, 3564)
+    for i in range(3564):
+        for id in bgrp:
+            if ivallist[i] & mask[id]:
+                physBG[id].append(i)
+
+    return physBG
+
+# Generic routine to unpack BCID mask
+# The nb1, nb2, nlumi are for backwards compatibility to Run1
+# These are not needed to unpack the Run2 BCID mask
+# Return values are a list of beam1, beam2, and colliding BCIDs
+def unpackBCIDMask(blob,nb1=0,nb2=0,nlumi=0):
+
+    if blob is None:
+        return [],[],[]
+
+    bloblength = blob.size()
+
+    if bloblength == 0:
+        return [],[],[]
+
+    if bloblength == 3564:
+        return unpackRun2BCIDMask(blob)
+    else:
+        return unpackRun1BCIDMask(blob,nb1,nb2,nlumi)
+
+# routine to unpack the BCID mask stored in COOL
+# This is the run2 version
+def unpackRun2BCIDMask(blob):
+    beam1=[]
+    beam2=[]
+    coll=[]
+    blobCopy = blob.read()
+    rawData = bConvertList(blobCopy, 1, 3564)
+
+    for i in range(3564):
+        val = rawData[i]
+        if val & 0x01:
+            beam1.append(i)
+        if val & 0x02: 
+            beam2.append(i)
+        if (val & 0x03) == 0x03:
+            coll.append(i)
+
+    # print('unpackRun2BCIDMask found:')
+    # print(' Beam1:', beam1)
+    # print(' Beam2:', beam2)
+    # print(' Coll: ', coll)
+
+    return beam1,beam2,coll
+
+# routine to unpack the BCID mask stored in COOL
+# This is the run1 version
+def unpackRun1BCIDMask(blob,nb1,nb2,nlumi):
+    beam1=[]
+    beam2=[]
+    coll=[]
+    blobCopy = blob.read()
+    beam1 = bConvertList(blobCopy, 2, nb1)
+    beam2 = bConvertList(blobCopy[2*nb1:], 2, nb2)
+    coll = bConvertList(blobCopy[2*(nb1+nb2):], 2, nlumi)
+    #unpackfmt = '%dH' % nb1
+    #list(struct.unpack(unpackfmt, blobCopy[0:(2*nb1)]))
+    #unpackfmt = '%dH' % nb2
+    #beam2 = list(struct.unpack(unpackfmt, blobCopy[(2*nb1):2*(nb1+nb2)]))
+    #unpackfmt = '%dH' % nlumi
+    #coll = list(struct.unpack(unpackfmt, blobCopy[2*(nb1+nb2):2*(nb1+nb2+nlumi)]))
+                 
+#    blobCounter = 0
+#     for i in range(nb1):
+#         b = blobCopy[blobCounter:blobCounter+2]
+#         blobCounter += 2
+#         val=struct.unpack('H', b)
+#         beam1.append(val)
+        
+#     for i in range(nb2):
+#         b = blobCopy[blobCounter:blobCounter+2]
+#         blobCounter += 2
+#         val=struct.unpack('H', b)
+#         beam2.append(val)
+
+#     for i in range(nlumi):
+#         b = blobCopy[blobCounter:blobCounter+2]
+#         blobCounter += 2
+#         val=struct.unpack('H', b)
+#         coll.append(val)
+
+    return beam1,beam2,coll
+
+# routine to unpack values (raw lumi or currents) stored as blob in COOL
+# blob - COOL blob with per-BCID values
+# mask - BCID mask appropriate for quantity being unpacked (i.e.: beam1, collisions, ...)
+# normValue - Normalization value from same COOL folder as BLOB (i.e.: B1BunchAverage)
+#
+# Note, the normValue is only used in certain storage modes.  If you want to renormalize, do this yourself.
+# Specifying a different value for the normValue will likely cause unpredictable results.
+
+def unpackBCIDValues(blob, mask=[], normValue=1):
+
+    bss, bcidVec, lvec = unpackBunches(blob, mask)
+    
+    if bss>0:
+      if not (len(bcidVec)==len(lvec)):
+        print('unpackBCIDValues - length mismatch: len(bcidVec)=', len(bcidVec), 'len(lvec)=', len(lvec))
+        sys.exit()
+        
+      bLumi=[]
+      for i in range(len(bcidVec)):
+        if bss<4:
+          bLumi.append(lvec[i]*normValue/pow(100,bss))
+        else:
+          bLumi.append(lvec[i])
+
+      #for i in range(len(bcidVec)):
+      #    print 'BCID:', bcidVec[i], 'Raw:', bLumi[i]
+          
+      return bcidVec,bLumi
+
+    else:
+      return [],[]
+    
+def unpackBunches(blob,mask=[]):
+    # routine to unpack Intensity/Luminosity info stored in COOL
+    # the mask given as input has to match the quantity to be
+    # unpacked (beam1, beam2, beamsand for B1, B2 intensities and
+    # luminosities, respectively)
+
+    if blob is None or blob.size() == 0:
+        return 0,[],[]
+    
+    blobCopy = blob.read()
+    blobCounter = 0
+    try:
+        b = blobCopy[blobCounter:blobCounter+1]
+        blobCounter += 1
+        flag=bConvert(b)
+        bss=(flag%100)//10
+        smod=flag%10
+        # print 'Storage mode for',str, 'is', smod, 'with bss=', bss
+            
+        if smod==2:
+            b = blobCopy[blobCounter:blobCounter+2]
+            blobCounter += 2
+            vlen=bConvert(b, 2)
+            #print 'Bunch vector has length ',vlen
+            bcidVec=[]
+            bcidVec = bConvertList(blobCopy[blobCounter:], 2, vlen)
+            blobCounter += 2*vlen
+            # for i in range(vlen):
+            #     valb = blobCopy[blobCounter:blobCounter+2]
+            #     blobCounter += 2
+            #     val=struct.unpack('H', valb)
+            #     bcidVec.append(val)
+            
+        elif smod==0:
+            # Make sure this is a list, and sorted (can pass set for example)
+            bcidVec=list(mask)
+            bcidVec.sort()
+            vlen=len(mask)
+        elif smod==3:
+            print('storage mode 3 not implemented in unpackBunches')
+            sys.exit()
+        elif smod==1:
+            bcidVec=[i for i in range(3564)]
+            vlen=3564
+        else:
+            print('Unknown storage mode ',smod)
+            sys.exit()
+        valueVec=[]
+
+        valueVec = bConvertList(blobCopy[blobCounter:], bss, vlen)
+#         for i in range(vlen):
+#             valb = blobCopy[blobCounter:blobCounter+bss]
+#             blobCounter += bss
+#             val=bConvert(valb,bss)
+#             valueVec.append(val)
+
+        return bss,bcidVec,valueVec
+
+    except RuntimeError as e:
+        print(e)
+        return 0,[],[]
+                                  
+# Unpack live fraction into vector keyed by bcid-1
+# Takes payload of /TRIGGER/LUMI/PerBcidDeadtime folder
+def unpackLiveFraction(trigPayload, priority = 'high'):
+
+    liveVec = array.array('f', 3564*[0.])
+    
+    if priority == 'high':
+        blob = trigPayload['HighPriority']
+    elif priority == 'low':
+        blob = trigPayload['LowPriority']
+    else:
+        print('unpackLiveFraction - unknown priority requested %s', str(priority))
+        return liveVec
+    
+    bloblength = blob.size()
+
+    # Due to a bug, the blob was sometimes written at 3654 rather than desired 3564
+    # More bugs, use anything long enough 
+    if bloblength < 3*3564: #!= 3*3654 and bloblength != 3*3564:
+        # Corrupt, don't trust anything
+        print('unpackLiveFraction found blob length %d!' % bloblength)
+        return liveVec
+
+    blobCopy = blob.read()
+    # blobCounter = 0
+
+    # No counts, no work to do
+    turnCounter = trigPayload['TurnCounter']
+    if not turnCounter > 0:
+        return liveVec
+
+    # Even if longer blob is present, only care about this range
+    
+    byte = bConvertList(blobCopy, 1, 3*3564)
+    
+    for i in range(3564):
+
+        busyCounter = byte[3*i] | (byte[3*i+1] << 8) | (byte[3*i+2] << 16)
+        
+        # byte0 = struct.unpack('B', blobCopy[blobCounter:blobCounter+1])
+        # blobCounter += 1
+        # byte1 = struct.unpack('B', blobCopy[blobCounter:blobCounter+1])
+        # blobCounter += 1
+        # byte2 = struct.unpack('B', blobCopy[blobCounter:blobCounter+1])
+        # blobCounter += 1
+        # busyCounter = byte0 | (byte1 << 8) | (byte2 << 16)
+        
+        liveFrac = 1 - float(busyCounter) / turnCounter
+
+        liveVec[i] = liveFrac
+
+        # print 'BCID: %d Busy: %d Turn: %d Live: %f' % (i+1, busyCounter, turnCounter, liveFrac)
+
+    return liveVec
+
+# Unpack live fraction into vector keyed by bcid-1
+# Takes payload of /TRIGGER/LUMI/PerBcidDeadtime folder
+def unpackLiveFractionRun2(trigPayload, priority = 'high'):
+
+    liveVec = array.array('f', 3564*[0.])
+    
+    if priority == 'high':
+        blob = trigPayload['DT0']
+    elif priority == 'low':
+        blob = trigPayload['DT1']
+    else:
+        print('unpackLiveFraction - unknown priority requested %s', str(priority))
+        return liveVec
+    
+    bloblength = blob.size()
+
+    if bloblength < 3*(3564+2): #!= 3*3654 and bloblength != 3*3564:
+        # Corrupt, don't trust anything
+        print('unpackLiveFraction found blob length %d!' % bloblength)
+        return liveVec
+
+    blobCopy = blob.read()
+    # blobCounter = 0
+
+    # Turn counter is now at the end, so we must unpack everything
+    byte = bConvertList(blobCopy, 1, 3*3566)
+
+    i = 3565
+    turnCounter = byte[3*i] | (byte[3*i+1] << 8) | (byte[3*i+2] << 16)
+
+    if not turnCounter > 0:
+        return liveVec
+
+    # Entry 0 is LB number, which we can skip
+    for i in range(1, 3564):
+
+        busyCounter = byte[3*i] | (byte[3*i+1] << 8) | (byte[3*i+2] << 16)
+        
+        # byte0 = struct.unpack('B', blobCopy[blobCounter:blobCounter+1])
+        # blobCounter += 1
+        # byte1 = struct.unpack('B', blobCopy[blobCounter:blobCounter+1])
+        # blobCounter += 1
+        # byte2 = struct.unpack('B', blobCopy[blobCounter:blobCounter+1])
+        # blobCounter += 1
+        # busyCounter = byte0 | (byte1 << 8) | (byte2 << 16)
+        
+        liveFrac = float(turnCounter - busyCounter) / turnCounter
+
+        liveVec[i] = liveFrac
+
+        # print 'BCID: %d Busy: %d Turn: %d Live: %f' % (i+1, busyCounter, turnCounter, liveFrac)
+
+    return liveVec
+
diff --git a/LHCData/LHCDataUtils/python/LumiDBHandler.py b/LHCData/LHCDataUtils/python/LumiDBHandler.py
new file mode 100644
index 00000000..7e6d5619
--- /dev/null
+++ b/LHCData/LHCDataUtils/python/LumiDBHandler.py
@@ -0,0 +1,121 @@
+# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+
+#
+# LumiDBHandler
+#
+# Eric Torrence - October 2010
+#
+# Contents:
+# LumiDBHandler - utility object to handle opening and closing COOL DB connections within
+#                 a large python script.  The benefit over just using AtlCoolLib directly
+#                 is that each DB connection is cached, so multiple connections to the same
+#                 DB will not be made.
+#
+#                 The parent script should call closeAllDB in its __del__ function to close
+#                 the DB connections, even if the script crashes.
+#
+# General usage example
+# dbH = LumiDBHandler()
+# myFolder = dbH.getFolder('COOLONL_TRIGGER/COMP200', '/TRIGGER/LUMI/LBLESTONL')
+#
+# One can then browse the folder as usual using browseObjects
+#
+# The CoolDataReader uses this class internally to make for more easy access
+#
+
+import CoolConvUtilities.AtlCoolLib as AtlCoolLib
+
+class LumiDBHandler:
+
+    # Define dbDict here at class scope
+    # Then access with self.__class__.dbDict and it will be the same for all instances of the class
+    # This is a pythonish way to create static classes
+
+    # Dict to store DB connection indexed by text DB connection name
+    dbDict = dict()
+
+    
+    def __init__(self):
+
+        # Debug output (can be changed for each instance, slick...)
+        self.verbose = False
+        
+    # Return a folder reference for the dbstring, folder specified
+    # DB will be opened if necessary
+    # Example: getFolder('COOLONL_TRIGGER/COMP200', '/TRIGGER/LUMI/LBLESTONL')
+    def getFolder(self, dbstring, folder, force=False):
+
+        if self.verbose:
+            print('LumiDBHandler.getFolder(', dbstring, ',', folder, ') called')
+
+        if not self.openDB(dbstring, force=force):
+            print("LumiDBHandler.getFolder - can't connect to DB!")
+            return None
+
+        return self.__class__.dbDict[dbstring].getFolder(folder)
+    
+    # Open a COOL DB connection based on a name such as "COOLONL_INDET/OFLP200"
+    # Returns True if successful (or DB already open) 
+    def openDB(self, dbstring, oracle=False, debug=False, force=False):
+
+        if self.verbose:
+            print('LumiDBHandler.openDB(', dbstring, ') called')
+
+        # Check if already open
+        if dbstring in self.__class__.dbDict:
+
+            # No force, just return 
+            if not force:
+                if self.verbose:
+                    print('LumiDBHandler.openDB - Connection already exists')
+                return True # Yes it is
+
+            # Force specified, close so we can re-open
+            if self.verbose:
+                print('LumiDBHandler.openDB - Connection already exists, closing first due to force=True')
+            self.closeDB(dbstring)
+
+        # Try to open DB connection
+        if self.verbose:
+            print('LumiDBHandler.openDB - Connecting to', dbstring)
+            
+        try:
+            db = AtlCoolLib.indirectOpen(dbstring, readOnly=True, oracle=oracle, debug=debug)
+        except Exception as e:
+            print(e)
+            return False
+
+        # OK, opened.  Save this to our dict for later use
+        self.__class__.dbDict[dbstring] = db
+        
+        return True
+
+    # Close specific DB
+    def closeDB(self, dbstring):
+
+        if self.verbose:
+            print('LumiDBHandler.closeDB - Closing connection to', dbstring)
+
+        if dbstring not in self.__class__.dbDict:
+            print("LumiDBHandler.closeDB - DB doesn't exist:", dbstring)
+        else:
+            try:
+                self.__class__.dbDict[dbstring].closeDatabase()
+            except Exception as e:
+                print(e)
+            self.__class__.dbDict.pop(dbstring)
+
+    # Called by default in the destructor, but not guaranteed if there are problems
+    def closeAllDB(self):
+        self.closeAll()
+        
+    def closeAll(self):
+        
+        if self.verbose:
+            print('LumiDBHandler.closeAllDB called')
+
+        # Can't use iterkeys here as we are deleting the elements
+        # In python3 must create explicit list
+        for dbstring in list(self.__class__.dbDict.keys()):
+            self.closeDB(dbstring)
+            
diff --git a/LHCData/LHCDataUtils/scripts/makeLHCFillData.py b/LHCData/LHCDataUtils/scripts/makeLHCFillData.py
new file mode 100755
index 00000000..f0187ab2
--- /dev/null
+++ b/LHCData/LHCDataUtils/scripts/makeLHCFillData.py
@@ -0,0 +1,734 @@
+#!/usr/bin/env python3
+import os
+import sys
+import argparse
+
+import time
+import calendar
+import datetime
+
+from pathlib import Path
+
+#from LHCDataUtils.LumiDBHandler import LumiDBHandler
+#from LHCDataUtils.CoolDataReader import CoolDataReader
+
+from PyCool import cool
+
+# Useful utilities for manipulating COOL files
+# See https://gitlab.cern.ch/atlas/athena/-/blob/master/Database/CoolConvUtilities/python/AtlCoolLib.py
+from CoolConvUtilities.AtlCoolLib import ensureFolder,forceOpen,athenaDesc,timeVal,timeString
+
+
+import CoolConvUtilities.AtlCoolLib as AtlCoolLib
+
+class LumiDBHandler:
+
+    # Define dbDict here at class scope
+    # Then access with self.__class__.dbDict and it will be the same for all instances of the class
+    # This is a pythonish way to create static classes
+
+    # Dict to store DB connection indexed by text DB connection name
+    dbDict = dict()
+
+    
+    def __init__(self):
+
+        # Debug output (can be changed for each instance, slick...)
+        self.verbose = False
+        
+    # Return a folder reference for the dbstring, folder specified
+    # DB will be opened if necessary
+    # Example: getFolder('COOLONL_TRIGGER/COMP200', '/TRIGGER/LUMI/LBLESTONL')
+    def getFolder(self, dbstring, folder, force=False):
+
+        if self.verbose:
+            print('LumiDBHandler.getFolder(', dbstring, ',', folder, ') called')
+
+        if not self.openDB(dbstring, force=force):
+            print("LumiDBHandler.getFolder - can't connect to DB!")
+            return None
+
+        return self.__class__.dbDict[dbstring].getFolder(folder)
+    
+    # Open a COOL DB connection based on a name such as "COOLONL_INDET/OFLP200"
+    # Returns True if successful (or DB already open) 
+    def openDB(self, dbstring, oracle=False, debug=False, force=False):
+
+        if self.verbose:
+            print('LumiDBHandler.openDB(', dbstring, ') called')
+
+        # Check if already open
+        if dbstring in self.__class__.dbDict:
+
+            # No force, just return 
+            if not force:
+                if self.verbose:
+                    print('LumiDBHandler.openDB - Connection already exists')
+                return True # Yes it is
+
+            # Force specified, close so we can re-open
+            if self.verbose:
+                print('LumiDBHandler.openDB - Connection already exists, closing first due to force=True')
+            self.closeDB(dbstring)
+
+        # Try to open DB connection
+        if self.verbose:
+            print('LumiDBHandler.openDB - Connecting to', dbstring)
+            
+        try:
+            db = AtlCoolLib.indirectOpen(dbstring, readOnly=True, oracle=oracle, debug=debug)
+        except Exception as e:
+            print(e)
+            return False
+
+        # OK, opened.  Save this to our dict for later use
+        self.__class__.dbDict[dbstring] = db
+        
+        return True
+
+    # Close specific DB
+    def closeDB(self, dbstring):
+
+        if self.verbose:
+            print('LumiDBHandler.closeDB - Closing connection to', dbstring)
+
+        if dbstring not in self.__class__.dbDict:
+            print("LumiDBHandler.closeDB - DB doesn't exist:", dbstring)
+        else:
+            try:
+                self.__class__.dbDict[dbstring].closeDatabase()
+            except Exception as e:
+                print(e)
+            self.__class__.dbDict.pop(dbstring)
+
+    # Called by default in the destructor, but not guaranteed if there are problems
+    def closeAllDB(self):
+        self.closeAll()
+        
+    def closeAll(self):
+        
+        if self.verbose:
+            print('LumiDBHandler.closeAllDB called')
+
+        # Can't use iterkeys here as we are deleting the elements
+        # In python3 must create explicit list
+        for dbstring in list(self.__class__.dbDict.keys()):
+            self.closeDB(dbstring)
+            
+# End of class LumiDBHandler
+
+
+class CoolDataReader:
+
+    def __init__(self, dbstr=None, folderstr=None):
+
+        self.verbose = False
+
+        # Defined variables
+        self.dbstr = None
+        self.folderstr = None
+        self.channelIdList = []
+        self.tag = ''
+        self.iovstart = None
+        self.iovend = None
+
+        self.folder = None
+        self.data = []
+        
+        # Initialize to default values
+        self.setChannel()
+        self.setTag()
+        self.setFolder(dbstr, folderstr)
+        self.setIOVRange()
+            
+    def setFolder(self, dbstr, folderstr):
+        # Force re-opening connection if these are different
+        if (dbstr != self.dbstr) or (folderstr != self.folderstr):
+            self.folder = None
+            
+        self.dbstr = dbstr
+        self.folderstr = folderstr
+
+    def setTag(self, tagstr=''):
+        self.tag = tagstr
+
+    def setChannel(self, channelIdList=[]):
+        self.channelIdList = channelIdList
+        
+    def setChannelAll(self):
+        self.setChannel()
+
+    def setChannelId(self, channelId):
+        self.setChannel([channelId])
+        
+    def setIOVRange(self, iovstart=cool.ValidityKeyMin, iovend=cool.ValidityKeyMax):
+        self.iovstart = iovstart
+        self.iovend = iovend
+
+    def setIOVRangeFromRun(self, runnum, startOfNextRun=False):
+        self.iovstart = runnum << 32
+        if startOfNextRun:
+            self.iovend = ((runnum+1) << 32)
+        else:
+            self.iovend = ((runnum+1) << 32) - 1
+
+    # Call to get data after all other parameters are properly set
+    # Data is returned as a list of IObject values, one per DB entry.
+    # This gives maximal flexibility to manipulate the items
+    def readData(self):
+
+        self.data = []
+
+        # Open the DB connection here if needed
+        if self.folder is None:
+            dbHandler = LumiDBHandler()
+            self.folder = dbHandler.getFolder(self.dbstr, self.folderstr)
+            
+            if self.folder is None:
+                print("Can't access DB", self.dbstr, 'folder', self.folderstr, '!')
+                return self.data
+
+        # Create the channel list
+        if len(self.channelIdList) == 0:
+            channels = cool.ChannelSelection.all()
+            self.readChannelList(channels)
+
+        else:
+            # Build the channel list here
+            self.channelIdList.sort()  # Must be sorted!
+
+            # Must read channels 50 at a time due to COOL limit...
+            ichan = 0
+            while (ichan < len(self.channelIdList)) :
+
+                jchan = 0
+                channels = None
+                firstChan = True
+            
+                for channelId in self.channelIdList[ichan:]:
+                    jchan += 1
+                    if firstChan:
+                        firstChan = False
+                        channels = cool.ChannelSelection(channelId)
+                    else:
+                        channels.addChannel(channelId)
+                    if jchan == 50: break 
+
+                # Remeber how many we have read for next time
+                if self.verbose:
+                    print('CoolDataReader.readData() - loaded %d channels from %d' % (jchan, ichan))
+                ichan += jchan
+
+                if self.verbose:
+                    print('CoolDataReader.readData() - browsing', self.iovstart, self.iovend, 'with channel', channels, 'and tag', self.tag)
+
+                self.readChannelList(channels)
+
+            # End of loop building channel list and reading
+
+        # End of if statement reading data
+        return self.data
+
+    def readChannelList(self, channels):
+
+        # Open iterator over our defined IOV range
+        try:
+            itr = self.folder.browseObjects(self.iovstart, self.iovend, channels, self.tag)
+        except Exception as e:
+            print('CoolDataReader.readData() - exception reading folder:', self.folderstr)
+            print(e)
+            print('CoolDataReader.readData() - will try to reconnect (once)')
+
+            # Force re-opening connection
+            dbHandler = LumiDBHandler()
+            dbHandler.verbose = True
+            self.folder = dbHandler.getFolder(self.dbstr, self.folderstr, force=True)
+            
+            if self.folder is None:
+                print('CoolDataReader.readData() - forced re-opening failed!')
+                return self.data
+
+            # OK, lets try reading this again
+            print('CoolDataReader.readData() - trying to re-read re-opened folder!')
+            try:
+                itr = self.folder.browseObjects(self.iovstart, self.iovend, channels, self.tag)
+            except Exception as e:
+                print('CoolDataReader.readData() - exception reading folder:', self.folderstr)
+                print(e)
+                return self.data
+                
+        while itr.goToNext():
+            obj = itr.currentRef()
+            # print obj.payload()
+            self.data.append(obj.clone())
+            
+        itr.close()
+
+# End of class CoolDataReader
+        
+
+def parse_arguments():
+
+    description = "Script to create LHC data database"
+    parser = argparse.ArgumentParser(description)
+
+    parser.add_argument("--verbose", "-v", action="store_true", 
+                        help="Print debugging information")
+
+    parser.add_argument("--fills", "-f", nargs='+', 
+                        help="Fills to find information on")
+
+    parser.add_argument("--recent", action="store_true",
+                        help="Update new fills not already in DB")
+
+    parser.add_argument("--output", "-o", default="fill_data.db", 
+                        help="Specify output DB")
+
+    parser.add_argument("--create", "-c", action="store_true", 
+                        help="Overwrite existing DB")
+
+    return parser.parse_args()
+
+
+# Take a string and turn it into a list of integers
+# Can specify single values, ranges, or comma separated lists of both
+# Can also specify file name with list of numbers
+def parseFillList(filllist):
+
+    fill_list = []
+
+    # Check if this is a file with fill numbers
+    if len(filllist) == 1:
+        path = Path(filllist[0])
+        if path.exists() and path.is_file():
+            print(f"Reading fills from {path}")
+            # Try reading each line as a fill number
+            with path.open() as f: 
+                for line in f.readlines():
+                    line = line.strip()
+                    if len(line) == 0: continue
+                    if line[0] in ['#', '!']: continue
+                    if not line.isnumeric():
+                        print(f"Error parsing {line}")
+                        continue
+                    fill_list.append(int(line))
+            # Done reading file
+            return(fill_list)
+        elif '-' in filllist[0]:
+            pass
+        elif ',' in filllist[0]:
+            pass
+        elif not filllist[0].isnumeric():
+            print(f"File {path} doesn't exist!")
+            return fill_list
+
+    for string in filllist:
+        tokens = string.split(',')
+
+        for segment in tokens:
+
+            if len(segment) == 0: continue
+
+            if '-' in segment:  # Range of fills
+                start, end = segment.split('-')
+                if not start.isnumeric():
+                    print(f"Found invalid fill {start}")
+                    continue
+                if not end.isnumeric():
+                    print(f"Found invalid fill {end}")
+                    continue
+                start = int(start)
+                end = int(end)
+                fill_list.extend(list(range(int(start), int(end)+1)))
+
+            else:
+                if not segment.isnumeric():
+                    print(f"Found invalid fill {segment}")
+                    continue
+                fill_list.append(int(segment))
+
+    return(fill_list)
+
+def getIOVDict(args, first_fill):
+
+    if args.verbose:
+        print(f"Searching for IOV for fill {first_fill}")
+
+    # Utility to read ATLAS DB
+    dbHandler = LumiDBHandler()
+    dbname = "COOLOFL_DCS/CONDBR2"
+    db = dbHandler.getFolder(dbname, "/LHC/DCS/FILLSTATE")
+    db.setPrefetchAll(False)
+
+    # Use channel selector to give us a reverse iterator
+    channel = cool.ChannelSelection(order=cool.ChannelSelection.channelBeforeSinceDesc)
+
+    # Limit how much of DB to read
+    iovstart = 1000000000 * timeVal("2022-06-01:00:00:00")
+    #iovstart = int(1E9) * int(calendar.timegm(time.strptime("2022-06-01", "%Y-%m-%d")))
+
+    if args.verbose:
+        print(f"Starting from {timeString(iovstart)}")
+
+    # Now step backwards reading until we find our first fill
+    itr = db.browseObjects(iovstart, cool.ValidityKeyMax, channel)
+
+    iov_dict = {}
+    last_fill = None
+    while itr.goToNext():
+        obj = itr.currentRef()
+        fill = obj.payloadValue('FillNumber')
+        # Skip any invalid values
+        if not fill.isnumeric():
+            print(f"Found {fill} for FillNumber, skipping!")
+            continue
+
+        # Replace with integer
+        fill = int(obj.payloadValue('FillNumber'))
+
+        if fill == 0:
+            print(f"Found FillNumber = {fill}, skipping!")
+            continue
+
+        # Lots of output...
+        #if args.verbose:
+        #    print(f"Fill {obj.payloadValue('FillNumber')} Since {timeString(obj.since())}")
+
+        # Have we gone far enough?
+        if fill < first_fill: break
+
+        # Check if we found a new fill
+        if not iov_dict.get(fill, None):
+
+            # Update previous fill
+            if last_fill:
+                iov = iov_dict[last_fill]
+                iov_dict[last_fill] = (obj.until(), iov[1])
+
+            last_fill = fill
+            iov_dict[fill] = (obj.since(), obj.until())
+
+        # Update fill range
+        iov = iov_dict[fill]
+        iov_dict[fill] = (obj.since(), iov[1])
+
+    # Done, print out what we found if desired
+    if args.verbose:
+        for fill in sorted(iov_dict.keys()):
+
+            #time_lo = iov_dict[fill][0] // int(1E9)
+            #time_hi = iov_dict[fill][1] // int(1E9)
+
+            #print(f"Fill {fill} from {datetime.datetime.fromtimestamp(time_lo)} to {datetime.datetime.fromtimestamp(time_hi)}")
+            print(f"Fill {fill} from {timeString(iov_dict[fill][0])} to {timeString(iov_dict[fill][1])}")
+
+    # Close our database here
+    dbHandler.closeDB(dbname)
+
+    return iov_dict
+
+
+class FillObject:
+    def __init__(self):
+        self.fillNumber = 0
+        self.machineMode = ''
+        self.beamType1 = 0
+        self.beamType2 = 0
+        self.nBeam1 = 0
+        self.nBeam2 = 0
+        self.nColl = 0
+        self.injScheme = ''
+        self.since = cool.ValidityKeyMin
+        self.until = cool.ValidityKeyMax
+
+        # List with tuple of IOV range and payload dict (since, until, payload)
+        self.iov_list = []
+
+        # List with tuple of IOV range and payload dict (since, until, payload)
+        self.bcid_list = []
+
+    def updateFill(self, obj):
+
+        payload = obj.payload()
+
+        # Set the fill number
+        if self.fillNumber == 0:
+            self.updateFillParams(payload)
+
+        # For bunches, use max
+        self.nBeam1 = max(self.nBeam1, payload["NumBunchBeam1"])
+        self.nBeam2 = max(self.nBeam2, payload["NumBunchBeam2"])
+        self.nColl  = max(self.nColl,  payload["NumBunchColl"])
+
+        self.updateIOVList(obj.since(), obj.until(), obj.payload())
+
+    def updateBCID(self, obj):
+
+        # Need to make sure we don't duplicate IOVs from FILLPARAMS
+        # Truncate records to fit into fill IOV range
+        if obj.since() < self.since: 
+            since = self.since
+        else:
+            since = obj.since()
+
+        if obj.until() > self.until:
+            until = self.until
+        else:
+            until = obj.until()
+
+        if since == until: # Could happen?
+            return
+
+        # Need to copy here?
+        #valdict = {}
+        #for key in ["Beam1Bunches", "Beam2Bunches", "LuminousBunches", "BCIDmasks"]:
+        #    valdict[key] = obj.payload()[key]
+
+        self.bcid_list.append((since, until, obj.payload()))
+
+    def updateIOVList(self, since, until, payload):
+
+        # We want a subset of the payload
+        valdict = {}
+        for key in ["BeamMode", "BetaStar", "CrossingAngle"]:
+            valdict[key] = payload[key]
+
+        # No list, append current value
+        if len(self.iov_list) == 0:
+            self.iov_list.append((since, until, valdict))
+
+        else:
+            # Extend existing IOV to start of this one
+            self.iov_list[-1] = (self.iov_list[-1][0], since, self.iov_list[-1][2])
+
+            # Add new IOV if value is different
+            # Check values separately, as we don't want to update betastar
+            # unless we are in stable beams
+            last_payload = self.iov_list[-1][2]
+            if valdict["BeamMode"] != last_payload["BeamMode"]:
+                self.iov_list.append((since, until, valdict))
+
+            elif valdict["CrossingAngle"] != last_payload["CrossingAngle"]:  
+                self.iov_list.append((since, until, valdict))
+
+            elif valdict["BeamMode"] == "STABLE BEAMS" and valdict["BetaStar"] != last_payload["BetaStar"]:
+                self.iov_list.append((since, until, valdict))
+
+    def updateFillParams(self, payload):
+        # Update things that shouldn't change
+        try:
+            self.fillNumber = int(payload['FillNumber'])
+        except Exception as e:
+            print(f'Error setting fill number from {payload["FillNumber"]}') 
+            print(e)
+
+        self.machineMode = payload['MachineMode']
+        self.beamType1 = payload['BeamType1']
+        self.beamType2 = payload['BeamType2']
+        self.injScheme = payload['InjectionScheme']
+
+    def setLast(self):
+        ''' Set the final entry in the self.iov_list to an open-ended IOV '''
+        if len(self.iov_list) == 0: return
+
+        self.iov_list[-1] = (self.iov_list[-1][0], cool.ValidityKeyMax, self.iov_list[-1][2])
+
+    def __str__(self):
+        return f'Fill: {self.fillNumber} Mode: {self.machineMode} B1/2: {self.beamType1}/{self.beamType2} Bunches B1/B2/Coll: {self.nBeam1}/{self.nBeam2}/{self.nColl} {self.injScheme}'
+
+def findRecentFills(args):
+    fill_list = []
+    print("findRecentFills not implemented!")
+    return fill_list
+
+#
+# Start execution here
+#
+args = parse_arguments()
+
+if args.verbose:
+    print(f"Updating fill {args.fills}")
+    print(f"Recent: {args.recent}")
+    print(f"Output: {args.output}")
+    print(f"Create: {args.create}")
+
+if args.recent:
+
+    if args.fills:
+        print("Can't specify --fills and --recent!")
+        sys.exit(1)
+
+    fill_list = findRecentFills(args)
+
+    if len(fill_list) == 0:
+        print("No new fills found!")
+        sys.exit(0) 
+
+else:
+    if not args.fills:
+        print("No fills specified!  Use --fills to provide fill numbers")
+        sys.exit(1)
+
+    fill_list = parseFillList(args.fills)
+    fill_list.sort()
+
+    if len(fill_list) == 0:
+        print("No fills specified!  Use --fills to provide fill numbers")
+        sys.exit(1)
+
+if args.verbose:
+    print(f"Fill list:\n{fill_list}")
+
+# To speed things up, lets find the IOV ranges for each fill in our fill list
+# Do this by reverse lookup in FILLSTATE
+iov_dict = getIOVDict(args, fill_list[0])
+
+# Open (or create) the database
+connectString = f'sqlite://;schema={args.output};dbname=CONDBR3'
+
+if args.verbose:
+    print(f"Opening DB using connection string {connectString}")
+
+if os.path.exists(args.output):
+    if args.create:
+        print(f"Deleting {args.output} due to --create")
+        os.remove(args.output)
+
+    else:
+        print(f"Output DB file {args.output} already exists!")
+        print(f"Writing in place, use --create to delete")
+
+else:
+    print(f"Creating new DB {args.output}")
+
+# Opens or creates as needed
+db = forceOpen(connectString)
+
+if not db:
+    print("Error with {connectString}")
+    sys.exit(1)
+
+# Create folders (use CoolConvUtilities function)
+description = athenaDesc(runLumi=False, datatype="AthenaAttributeList")
+if args.verbose:
+    print(f"Folder description: {description}")
+
+# Order matters here!
+lhc_spec = cool.RecordSpecification()
+lhc_spec.extend("FillNumber", cool.StorageType.Int32)
+lhc_spec.extend("MachineMode", cool.StorageType.String4k)
+lhc_spec.extend("InjectionScheme", cool.StorageType.String4k)
+lhc_spec.extend("BeamType1", cool.StorageType.Int32)
+lhc_spec.extend("BeamType2", cool.StorageType.Int32)
+lhc_spec.extend("NumBunchBeam1", cool.StorageType.UInt32)
+lhc_spec.extend("NumBunchBeam2", cool.StorageType.UInt32)
+lhc_spec.extend("NumBunchColl", cool.StorageType.UInt32)
+
+beam_spec = cool.RecordSpecification()
+beam_spec.extend("BeamMode", cool.StorageType.String4k)
+beam_spec.extend("BetaStar", cool.StorageType.Float)
+beam_spec.extend("CrossingAngle", cool.StorageType.Float)
+
+bcid_spec = cool.RecordSpecification()
+bcid_spec.extend("Beam1Bunches", cool.StorageType.UInt32)
+bcid_spec.extend("Beam2Bunches", cool.StorageType.UInt32)
+bcid_spec.extend("LuminousBunches", cool.StorageType.UInt32)
+bcid_spec.extend("BCIDmasks", cool.StorageType.Blob64k)
+
+# Ensure folder opens or creates as needed
+# Create storage buffer for writing
+try:
+    lhc_folder  = ensureFolder(db, "/LHC/FillData", lhc_spec, description)
+    beam_folder = ensureFolder(db, "/LHC/BeamData", beam_spec, description)
+    bcid_folder = ensureFolder(db, "/LHC/BCIDData", bcid_spec, description)
+    lhc_folder.setupStorageBuffer()
+    beam_folder.setupStorageBuffer()
+    bcid_folder.setupStorageBuffer()
+
+except Exception as e:
+    print("Could not access or create folders!")
+    print(e)
+    sys.exit(1)
+
+db_lhc  = CoolDataReader("COOLOFL_DCS/CONDBR2", "/LHC/DCS/FILLSTATE")
+db_bcid = CoolDataReader("COOLONL_TDAQ/CONDBR2", "/TDAQ/OLC/LHC/FILLPARAMS")
+
+for fill in fill_list:
+
+    if not iov_dict.get(fill, None):  # Should have just found this
+        print(f"Can't find fill {fill} in IOV dictionary!")
+        sys.exit(1)
+
+    time_lo = iov_dict[fill][0]
+    time_hi = iov_dict[fill][1]
+    if args.verbose:
+        print(f"Working on fill {fill} from {timeString(time_lo)} to {timeString(time_hi)}")
+
+    fill_obj = FillObject()
+    fill_obj.since = time_lo
+    fill_obj.until = time_hi
+
+    db_lhc.setIOVRange(time_lo, time_hi)
+    db_lhc.readData()
+
+    for obj in db_lhc.data:
+        fill_obj.updateFill(obj)
+
+    # Is this the last fill?
+    if fill == fill_list[-1]:
+        fill_obj.setLast()
+
+    db_bcid.setIOVRange(time_lo, time_hi)
+    db_bcid.readData()
+
+    for obj in db_bcid.data:
+        fill_obj.updateBCID(obj)
+
+    # Now we want to fill our folders
+    lhc_record = cool.Record(lhc_spec)
+    lhc_record["FillNumber"] = fill_obj.fillNumber
+    lhc_record["MachineMode"] = fill_obj.machineMode
+    lhc_record["InjectionScheme"] = fill_obj.injScheme
+    lhc_record["BeamType1"] = fill_obj.beamType1
+    lhc_record["BeamType2"] = fill_obj.beamType2
+    lhc_record["NumBunchBeam1"] = fill_obj.nBeam1
+    lhc_record["NumBunchBeam2"] = fill_obj.nBeam2
+    lhc_record["NumBunchColl"] = fill_obj.nColl
+
+    if args.verbose: 
+        print(f"Writing fill {fill_obj.fillNumber}")
+        print(fill_obj)
+
+    chan = 0 # No channels here, but need to pass dummy argument
+    lhc_folder.storeObject(fill_obj.since, fill_obj.until, lhc_record, chan)
+
+    if args.verbose: print("Writing beam folder:")
+
+    for since, until, payload in fill_obj.iov_list:
+        beam_record = cool.Record(beam_spec)
+        for key in payload:
+            beam_record[key] = payload[key]
+
+        if args.verbose:
+            print(f"{timeString(since)} - {timeString(until)}: {beam_record}")
+
+        beam_folder.storeObject(since, until, beam_record, chan)
+
+    for since, until, payload in fill_obj.bcid_list:
+        bcid_record = cool.Record(bcid_spec)
+        for key in payload:
+            bcid_record[key] = payload[key]
+
+        if args.verbose:
+            print(f"{timeString(since)} - {timeString(until)}: {bcid_record}")
+
+        bcid_folder.storeObject(since, until, bcid_record, chan)
+
+# Make sure everything is writen
+lhc_folder.flushStorageBuffer()
+beam_folder.flushStorageBuffer()
+bcid_folder.flushStorageBuffer()
+
+# End of loop over fills
+
+db.closeDatabase()
diff --git a/xAOD/xAODFaserLHC/CMakeLists.txt b/xAOD/xAODFaserLHC/CMakeLists.txt
new file mode 100644
index 00000000..577da2cc
--- /dev/null
+++ b/xAOD/xAODFaserLHC/CMakeLists.txt
@@ -0,0 +1,27 @@
+# Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+
+# Declare the package name.
+atlas_subdir( xAODFaserLHC )
+
+# External dependencies.
+find_package( xAODUtilities )
+
+# Component(s) in the package.
+atlas_add_library( xAODFaserLHC
+   xAODFaserLHC/*.h xAODFaserLHC/versions/*.h Root/*.cxx
+   PUBLIC_HEADERS xAODFaserLHC
+   LINK_LIBRARIES xAODCore )
+
+atlas_add_xaod_smart_pointer_dicts(
+   INPUT xAODFaserLHC/selection.xml
+   OUTPUT _selectionFile
+   OBJECTS "xAOD::FaserLHCData_v1" "xAOD::FaserLHCDataAux_v1")
+
+atlas_add_dictionary( xAODFaserLHCDict
+   xAODFaserLHC/xAODFaserLHCDict.h
+   ${_selectionFile}
+   LINK_LIBRARIES xAODCore xAODFaserLHC
+   EXTRA_FILES Root/dict/*.cxx )
+
+# Understand what this does...
+atlas_generate_cliddb( xAODFaserLHC )
diff --git a/xAOD/xAODFaserLHC/Root/xAODFaserLHCCLIDs.cxx b/xAOD/xAODFaserLHC/Root/xAODFaserLHCCLIDs.cxx
new file mode 100644
index 00000000..86811f0c
--- /dev/null
+++ b/xAOD/xAODFaserLHC/Root/xAODFaserLHCCLIDs.cxx
@@ -0,0 +1,8 @@
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+//simple includes to force the CLASS_DEF etc to be encountered during compile
+
+#include "xAODFaserLHC/FaserLHCData.h"
+#include "xAODFaserLHC/FaserLHCDataAux.h"
diff --git a/xAOD/xAODFaserLHC/Root/xAODFaserLHCDataAux_v1.cxx b/xAOD/xAODFaserLHC/Root/xAODFaserLHCDataAux_v1.cxx
new file mode 100644
index 00000000..37e37fa7
--- /dev/null
+++ b/xAOD/xAODFaserLHC/Root/xAODFaserLHCDataAux_v1.cxx
@@ -0,0 +1,45 @@
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+// $Id: $
+
+// Local include(s):
+#include "xAODFaserLHC/versions/FaserLHCDataAux_v1.h"
+
+namespace xAOD {
+
+  FaserLHCDataAux_v1::FaserLHCDataAux_v1()
+    : AuxInfoBase(),
+      fillNumber(0), 
+      machineMode(""),
+      beamMode(""),
+      beamType1(0),
+      beamType2(0),
+      numBunchBeam1(0),
+      numBunchBeam2(0),
+      numBunchColliding(0),
+      betaStar(0),
+      crossingAngle(0),
+      injectionScheme(""),
+      distanceToCollidingBCID(0),
+      distanceToUnpairedB1(0),
+      distanceToUnpairedB2(0)
+  {
+    AUX_VARIABLE( fillNumber );
+    AUX_VARIABLE( machineMode );
+    AUX_VARIABLE( beamMode );
+    AUX_VARIABLE( beamType1 );
+    AUX_VARIABLE( beamType2 );
+    AUX_VARIABLE( numBunchBeam1 );
+    AUX_VARIABLE( numBunchBeam2 );
+    AUX_VARIABLE( numBunchColliding );
+    AUX_VARIABLE( betaStar );
+    AUX_VARIABLE( crossingAngle );
+    AUX_VARIABLE( injectionScheme );
+    AUX_VARIABLE( distanceToCollidingBCID );
+    AUX_VARIABLE( distanceToUnpairedB1 );
+    AUX_VARIABLE( distanceToUnpairedB2 );
+  }
+
+} // namespace xAOD
diff --git a/xAOD/xAODFaserLHC/Root/xAODFaserLHCData_v1.cxx b/xAOD/xAODFaserLHC/Root/xAODFaserLHCData_v1.cxx
new file mode 100644
index 00000000..05403bcf
--- /dev/null
+++ b/xAOD/xAODFaserLHC/Root/xAODFaserLHCData_v1.cxx
@@ -0,0 +1,78 @@
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+// $Id: $
+
+// xAOD include(s):
+#include "xAODCore/AuxStoreAccessorMacros.h"
+
+// Local include(s):
+#include "xAODFaserLHC/versions/FaserLHCData_v1.h"
+
+namespace xAOD {
+
+  FaserLHCData_v1::FaserLHCData_v1() 
+    : SG::AuxElement() {
+  }
+
+  /////////////////////////////////////////////////////////////////////////////
+  //
+  //              Implementation for the start time functions
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, unsigned int,
+					fillNumber, set_fillNumber )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, std::string,
+					machineMode, set_machineMode )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, std::string, 
+					beamMode, set_beamMode )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, int,
+					beamType1, set_beamType1 )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, int,
+					beamType2, set_beamType2 )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, unsigned int, 
+					numBunchBeam1, set_numBunchBeam1 )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, unsigned int, 
+					numBunchBeam2, set_numBunchBeam2 )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, unsigned int, 
+					numBunchColliding, set_numBunchColliding )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, float, 
+					betaStar, set_betaStar )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, float, 
+					crossingAngle, set_crossingAngle )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, std::string, 
+					injectionScheme, set_injectionScheme )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, int, 
+					distanceToCollidingBCID, set_distanceToCollidingBCID )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, int, 
+					distanceToUnpairedB1, set_distanceToUnpairedB1 )
+
+  AUXSTORE_PRIMITIVE_SETTER_AND_GETTER( FaserLHCData_v1, int, 
+					distanceToUnpairedB2, set_distanceToUnpairedB2 )
+
+} // namespace xAOD
+
+namespace xAOD {
+
+  std::ostream& operator<<(std::ostream& s, const xAOD::FaserLHCData_v1& td) {
+    s << "xAODFaserLHCData: fill=" << td.fillNumber()
+      << " beamMode=" << td.beamMode()
+      << " betaStar=" << td.betaStar()
+      << " distanceToCollidingBCID=" << td.distanceToCollidingBCID()
+      << std::endl;
+
+    return s;
+  }
+
+} // namespace xAOD
diff --git a/xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCData.h b/xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCData.h
new file mode 100644
index 00000000..561c8782
--- /dev/null
+++ b/xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCData.h
@@ -0,0 +1,22 @@
+// Dear emacs, this is -*- c++ -*-
+
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHC_FASERLHCDATA_H
+#define XAODFASERLHC_FASERLHCDATA_H
+
+// Local include(s):
+#include "xAODFaserLHC/versions/FaserLHCData_v1.h"
+
+namespace xAOD {
+  /// Declare the latest version of the class
+  typedef FaserLHCData_v1 FaserLHCData;
+}
+
+// Set up a CLID for the container:
+#include "xAODCore/CLASS_DEF.h"
+CLASS_DEF( xAOD::FaserLHCData, 255278152, 1 )
+
+#endif // XAODFASERLHC_FASERLHCDATA_H
diff --git a/xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCDataAux.h b/xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCDataAux.h
new file mode 100644
index 00000000..6eeac626
--- /dev/null
+++ b/xAOD/xAODFaserLHC/xAODFaserLHC/FaserLHCDataAux.h
@@ -0,0 +1,22 @@
+// Dear emacs, this is -*- c++ -*-
+
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHC_FASERLHCDATAAUX_H
+#define XAODFASERLHC_FASERLHCDATAAUX_H
+
+// Local include(s):
+#include "xAODFaserLHC/versions/FaserLHCDataAux_v1.h"
+
+namespace xAOD {
+  /// Declare the latest version of the class
+  typedef FaserLHCDataAux_v1 FaserLHCDataAux;
+}
+
+// Set up a CLID for the container:
+#include "xAODCore/CLASS_DEF.h"
+CLASS_DEF( xAOD::FaserLHCDataAux, 151779941, 1 )
+
+#endif // XAODFASERLHC_FASERLHCDATAAUX_H
diff --git a/xAOD/xAODFaserLHC/xAODFaserLHC/selection.xml b/xAOD/xAODFaserLHC/xAODFaserLHC/selection.xml
new file mode 100644
index 00000000..c4ba39f9
--- /dev/null
+++ b/xAOD/xAODFaserLHC/xAODFaserLHC/selection.xml
@@ -0,0 +1,13 @@
+<!-- Copyright (C) 2020 CERN for the benefit of the FASER collaboration -->
+<!-- Missing id values here, need to figure out where these come from... -->
+<lcgdict>
+
+  <class name="xAOD::FaserLHCData_v1" 
+	 id="6c373036-fad1-476c-9a8b-15591faccf00"/>
+  <typedef name="xAOD::FaserLHCData" />
+
+  <class name="xAOD::FaserLHCDataAux_v1" 
+	 id="484ef174-8efb-426a-888c-827f72ed972a"/>
+  <typedef name="xAOD::FaserLHCDataAux" />
+
+</lcgdict>
diff --git a/xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCDataAux_v1.h b/xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCDataAux_v1.h
new file mode 100644
index 00000000..c2daf68d
--- /dev/null
+++ b/xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCDataAux_v1.h
@@ -0,0 +1,68 @@
+// Dear emacs, this is -*- c++ -*-
+
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHC_VERSIONS_FASERLHCDATAAUX_V1_H
+#define XAODFASERLHC_VERSIONS_FASERLHCDATAAUX_V1_H
+
+// System include(s):
+extern "C" {
+#   include "stdint.h"
+}
+
+// xAOD include(s):
+#include "xAODCore/AuxInfoBase.h"
+
+namespace xAOD {
+
+  /// Class holding the data handled by FaserLHCData_v1
+  ///
+  /// This class holds the payload of the TLB raw data
+  ///
+  /// @author Eric Torrence <torrence@uoregon.edu>
+  ///
+  /// $Revision:  $
+  /// $Date: $
+
+  class FaserLHCDataAux_v1 : public AuxInfoBase {
+
+  public:
+    /// Default constructor
+    FaserLHCDataAux_v1();
+
+  private:
+    /// @name LHCData payload
+    /// @{
+    unsigned int fillNumber;
+    std::string machineMode;
+    std::string beamMode;
+    int beamType1;
+    int beamType2;
+
+    unsigned int numBunchBeam1;
+    unsigned int numBunchBeam2;
+    unsigned int numBunchColliding;
+
+    float betaStar;
+    float crossingAngle;
+
+    std::string injectionScheme;
+
+    int distanceToCollidingBCID;
+    int distanceToUnpairedB1;
+    int distanceToUnpairedB2;
+    /// @}
+
+  }; // class FaserLHCDataAuxContainer_v1
+
+} // namespace xAOD
+
+// Set up the StoreGate inheritance of the class:
+#include "xAODCore/BaseInfo.h"
+SG_BASE( xAOD::FaserLHCDataAux_v1, xAOD::AuxInfoBase );
+
+#endif // XAODFASERLHC_VERSIONS_FASERLHCDATAAUX_V1_H
+
+
diff --git a/xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCData_v1.h b/xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCData_v1.h
new file mode 100644
index 00000000..982ce105
--- /dev/null
+++ b/xAOD/xAODFaserLHC/xAODFaserLHC/versions/FaserLHCData_v1.h
@@ -0,0 +1,103 @@
+// Dear emacs, this is -*- c++ -*-
+
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHC_VERSIONS_FASERLHCDATA_V1_H
+#define XAODFASERLHC_VERSIONS_FASERLHCDATA_V1_H
+
+// System include(s):
+extern "C" {
+#   include "stdint.h"
+}
+
+// Core EDM include(s):
+#include "AthContainers/AuxElement.h"
+
+namespace xAOD {
+
+  /// Class describing the Faser LHC information
+  ///
+  ///
+  class FaserLHCData_v1 : public SG::AuxElement {
+
+  public:
+    /// Default constructor
+    FaserLHCData_v1();
+
+    /// @name Access LHC elements
+    /// @{
+
+    /// First batch comes from ATLAS /LHC/DCS/FILLSTATE folder
+    /// which is a copy of information sent to ATLAS by LHC over DIP
+
+    /// LHC fill number
+    unsigned int fillNumber() const;
+    void set_fillNumber(unsigned int value);
+
+    /// LHC machine mode (i.e. PROTON PHYSICS)
+    std::string machineMode() const; 
+    void set_machineMode(std::string value);
+
+    /// LHC Beam Mode (i.e. STABLE BEAMS)
+    std::string beamMode() const; 
+    void set_beamMode(std::string value);
+
+    /// LHC Beam type (charge of particle, 1-proton, 82-lead)
+    int beamType1() const;
+    void set_beamType1(int value);
+
+    int beamType2() const;
+    void set_beamType2(int value);
+
+    /// Nominal bunches (derived from DIP "Circulating Bunch Configuration")
+    unsigned int numBunchBeam1() const;
+    void set_numBunchBeam1(unsigned int value);
+    unsigned int numBunchBeam2() const;
+    void set_numBunchBeam2(unsigned int value);
+    unsigned int numBunchColliding() const;
+    void set_numBunchColliding(unsigned int value);
+
+    // Beta* at IP1, in cm
+    float betaStar() const;
+    void set_betaStar(float value);
+
+    // Crossing angle at IP1, in uRad
+    float crossingAngle() const;
+    void set_crossingAngle(float value);
+
+    // Injection scheme
+    std::string injectionScheme() const;
+    void set_injectionScheme(std::string value);
+
+    // Second batch is information is related to the BCID structure
+
+    // Number of BCIDs difference between current event BCID (from EventInfo)
+    // and a colliding BCID in the nominal bunch pattern
+    int distanceToCollidingBCID() const;
+    void set_distanceToCollidingBCID(int value);
+
+    int distanceToUnpairedB1() const;
+    void set_distanceToUnpairedB1(int value);
+
+    int distanceToUnpairedB2() const;
+    void set_distanceToUnpairedB2(int value);
+
+    /// Collimator settings?
+
+
+    /// @}
+
+
+  }; // class FaserLHCData_v1
+
+  std::ostream& operator<<(std::ostream& s, const xAOD::FaserLHCData_v1& td);
+
+} // namespace xAOD
+
+// Declare the inheritance of the type:
+#include "xAODCore/BaseInfo.h"
+SG_BASE( xAOD::FaserLHCData_v1, SG::AuxElement );
+
+#endif // XAODFASERLHC_VERSIONS_FASERLHCDATA_V1_H
diff --git a/xAOD/xAODFaserLHC/xAODFaserLHC/xAODFaserLHCDict.h b/xAOD/xAODFaserLHC/xAODFaserLHC/xAODFaserLHCDict.h
new file mode 100644
index 00000000..4715ad86
--- /dev/null
+++ b/xAOD/xAODFaserLHC/xAODFaserLHC/xAODFaserLHCDict.h
@@ -0,0 +1,27 @@
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHC_XAODFASERLHCDICT_H
+#define XAODFASERLHC_XAODFASERLHCDICT_H
+
+// Local include(s):
+#include "xAODFaserLHC/FaserLHCData.h"
+#include "xAODFaserLHC/FaserLHCDataAux.h"
+#include "xAODFaserLHC/versions/FaserLHCData_v1.h"
+#include "xAODFaserLHC/versions/FaserLHCDataAux_v1.h"
+
+// EDM include(s).
+#include "xAODCore/tools/DictHelpers.h"
+
+namespace {
+  struct GCCXML_DUMMY_INSTANTIATION_XAODFASERLHC {
+    // Local type(s).
+    XAOD_INSTANTIATE_NS_OBJECT_TYPES( xAOD, FaserLHCData_v1 );
+
+  };
+}
+
+
+#endif // XAODFASERLHC_XAODFASERLHCDICT_H
+
diff --git a/xAOD/xAODFaserLHCAthenaPool/CMakeLists.txt b/xAOD/xAODFaserLHCAthenaPool/CMakeLists.txt
new file mode 100644
index 00000000..d15a49cc
--- /dev/null
+++ b/xAOD/xAODFaserLHCAthenaPool/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+
+# Declare the package name.
+atlas_subdir( xAODFaserLHCAthenaPool )
+
+# Component(s) in the package:
+atlas_add_poolcnv_library( xAODFaserLHCAthenaPoolPoolCnv
+   src/*.h src/*.cxx
+   FILES xAODFaserLHC/FaserLHCData.h xAODFaserLHC/FaserLHCDataAux.h
+   TYPES_WITH_NAMESPACE xAOD::FaserLHCData xAOD::FaserLHCDataAux
+   CNV_PFX xAOD
+   LINK_LIBRARIES AthenaPoolCnvSvcLib AthenaPoolUtilities xAODFaserLHC )
+
+
diff --git a/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.cxx b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.cxx
new file mode 100644
index 00000000..2ae94670
--- /dev/null
+++ b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.cxx
@@ -0,0 +1,5 @@
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+// Dummy source file so that cmake will know this is a custom converter.
diff --git a/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.h b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.h
new file mode 100644
index 00000000..dc3f3f9a
--- /dev/null
+++ b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataAuxCnv.h
@@ -0,0 +1,16 @@
+// Dear emacs, this is -*- c++ -*-
+
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHCDATAATHENAPOOL_XAODFASERLHCDATAAUXCNV_H
+#define XAODFASERLHCDATAATHENAPOOL_XAODFASERLHCDATAAUXCNV_H
+
+#include "xAODFaserLHC/FaserLHCDataAux.h"
+#include "AthenaPoolCnvSvc/T_AthenaPoolAuxContainerCnv.h"
+
+typedef T_AthenaPoolAuxContainerCnv<xAOD::FaserLHCDataAux> xAODFaserLHCDataAuxCnv;
+
+
+#endif // XAODFASERLHCDATAATHENAPOOL_XAODFASERLHCDATAAUXCNV_H
diff --git a/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.cxx b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.cxx
new file mode 100644
index 00000000..2ae94670
--- /dev/null
+++ b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.cxx
@@ -0,0 +1,5 @@
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+// Dummy source file so that cmake will know this is a custom converter.
diff --git a/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.h b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.h
new file mode 100644
index 00000000..a73c439c
--- /dev/null
+++ b/xAOD/xAODFaserLHCAthenaPool/src/xAODFaserLHCDataCnv.h
@@ -0,0 +1,17 @@
+// Dear emacs, this is -*- c++ -*-
+
+/*
+  Copyright (C) 2020 CERN for the benefit of the FASER collaboration
+*/
+
+#ifndef XAODFASERLHCDATAATHENAPOOL_XAODFASERLHCDATACNV_H
+#define XAODFASERLHCDATAATHENAPOOL_XAODFASERLHCDATACNV_H
+
+#include "xAODFaserLHC/FaserLHCData.h"
+#include "AthenaPoolCnvSvc/T_AthenaPoolxAODCnv.h"
+
+
+typedef T_AthenaPoolxAODCnv<xAOD::FaserLHCData> xAODFaserLHCDataCnv;
+
+
+#endif // XAODFASERLHCDATAATHENAPOOL_XAODFASERLHCDATACNV_H
-- 
GitLab